Merge branch 'master' into s3-upload-fixes
This commit is contained in:
commit
6a120d4d79
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.2.37",
|
||||
"version": "3.2.39",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -32,8 +32,12 @@ export async function errorHandling(ctx: any, next: any) {
|
|||
}
|
||||
|
||||
if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) {
|
||||
let rootErr = err
|
||||
while (rootErr.cause) {
|
||||
rootErr = rootErr.cause
|
||||
}
|
||||
// @ts-ignore
|
||||
error.stack = err.stack
|
||||
error.stack = rootErr.stack
|
||||
}
|
||||
|
||||
ctx.body = error
|
||||
|
|
|
@ -816,14 +816,29 @@ class InternalBuilder {
|
|||
filters.oneOf,
|
||||
ArrayOperator.ONE_OF,
|
||||
(q, key: string, array) => {
|
||||
const schema = this.getFieldSchema(key)
|
||||
const values = Array.isArray(array) ? array : [array]
|
||||
if (shouldOr) {
|
||||
q = q.or
|
||||
}
|
||||
if (this.client === SqlClient.ORACLE) {
|
||||
// @ts-ignore
|
||||
key = this.convertClobs(key)
|
||||
} else if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
for (const value of values) {
|
||||
if (value != null) {
|
||||
q = q.or.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||
} else {
|
||||
q = q.or.whereNull(key)
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
return q.whereIn(key, Array.isArray(array) ? array : [array])
|
||||
return q.whereIn(key, values)
|
||||
},
|
||||
(q, key: string[], array) => {
|
||||
if (shouldOr) {
|
||||
|
@ -882,6 +897,19 @@ class InternalBuilder {
|
|||
let high = value.high
|
||||
let low = value.low
|
||||
|
||||
if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
if (high != null) {
|
||||
high = `${high.toISOString().slice(0, 10)}T23:59:59.999Z`
|
||||
}
|
||||
if (low != null) {
|
||||
low = low.toISOString().slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.client === SqlClient.ORACLE) {
|
||||
rawKey = this.convertClobs(key)
|
||||
} else if (
|
||||
|
@ -914,6 +942,7 @@ class InternalBuilder {
|
|||
}
|
||||
if (filters.equal) {
|
||||
iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
|
||||
const schema = this.getFieldSchema(key)
|
||||
if (shouldOr) {
|
||||
q = q.or
|
||||
}
|
||||
|
@ -928,6 +957,16 @@ class InternalBuilder {
|
|||
// @ts-expect-error knex types are wrong, raw is fine here
|
||||
subq.whereNotNull(identifier).andWhere(identifier, value)
|
||||
)
|
||||
} else if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
if (value != null) {
|
||||
return q.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||
} else {
|
||||
return q.whereNull(key)
|
||||
}
|
||||
} else {
|
||||
return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [
|
||||
this.rawQuotedIdentifier(key),
|
||||
|
@ -938,6 +977,7 @@ class InternalBuilder {
|
|||
}
|
||||
if (filters.notEqual) {
|
||||
iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
|
||||
const schema = this.getFieldSchema(key)
|
||||
if (shouldOr) {
|
||||
q = q.or
|
||||
}
|
||||
|
@ -959,6 +999,18 @@ class InternalBuilder {
|
|||
// @ts-expect-error knex types are wrong, raw is fine here
|
||||
.or.whereNull(identifier)
|
||||
)
|
||||
} else if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
if (value != null) {
|
||||
return q.not
|
||||
.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||
.or.whereNull(key)
|
||||
} else {
|
||||
return q.not.whereNull(key)
|
||||
}
|
||||
} else {
|
||||
return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [
|
||||
this.rawQuotedIdentifier(key),
|
||||
|
|
|
@ -14,7 +14,7 @@ import environment from "../environment"
|
|||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||
const ROW_ID_REGEX = /^\[.*]$/g
|
||||
const ENCODED_SPACE = encodeURIComponent(" ")
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}.\d{3}Z)?$/
|
||||
const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
|
||||
|
||||
export function isExternalTableID(tableId: string) {
|
||||
|
@ -149,15 +149,7 @@ export function isInvalidISODateString(str: string) {
|
|||
}
|
||||
|
||||
export function isValidISODateString(str: string) {
|
||||
const trimmedValue = str.trim()
|
||||
if (!ISO_DATE_REGEX.test(trimmedValue)) {
|
||||
return false
|
||||
}
|
||||
let d = new Date(trimmedValue)
|
||||
if (isNaN(d.getTime())) {
|
||||
return false
|
||||
}
|
||||
return d.toISOString() === trimmedValue
|
||||
return ISO_DATE_REGEX.test(str.trim())
|
||||
}
|
||||
|
||||
export function isValidFilter(value: any) {
|
||||
|
|
|
@ -442,13 +442,11 @@
|
|||
|
||||
const onUpdateUserInvite = async (invite, role) => {
|
||||
let updateBody = {
|
||||
code: invite.code,
|
||||
apps: {
|
||||
...invite.apps,
|
||||
[prodAppId]: role,
|
||||
},
|
||||
}
|
||||
|
||||
if (role === Constants.Roles.CREATOR) {
|
||||
updateBody.builder = updateBody.builder || {}
|
||||
updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId]
|
||||
|
@ -456,7 +454,7 @@
|
|||
} else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
|
||||
invite.builder.apps = []
|
||||
}
|
||||
await users.updateInvite(updateBody)
|
||||
await users.updateInvite(invite.code, updateBody)
|
||||
await filterInvites(query)
|
||||
}
|
||||
|
||||
|
@ -470,8 +468,7 @@
|
|||
let updated = { ...invite }
|
||||
delete updated.info.apps[prodAppId]
|
||||
|
||||
return await users.updateInvite({
|
||||
code: updated.code,
|
||||
return await users.updateInvite(updated.code, {
|
||||
apps: updated.apps,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -191,8 +191,14 @@
|
|||
? "View errors"
|
||||
: "View error"}
|
||||
on:dismiss={async () => {
|
||||
await automationStore.actions.clearLogErrors({ appId })
|
||||
await appsStore.load()
|
||||
const automationId = Object.keys(automationErrors[appId] || {})[0]
|
||||
if (automationId) {
|
||||
await automationStore.actions.clearLogErrors({
|
||||
appId,
|
||||
automationId,
|
||||
})
|
||||
await appsStore.load()
|
||||
}
|
||||
}}
|
||||
message={automationErrorMessage(appId)}
|
||||
/>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
]
|
||||
|
||||
const removeUser = async id => {
|
||||
await groups.actions.removeUser(groupId, id)
|
||||
await groups.removeUser(groupId, id)
|
||||
fetchGroupUsers.refresh()
|
||||
}
|
||||
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
passwordModal.show()
|
||||
await fetch.refresh()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error creating user")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
import { datasources } from "./datasources"
|
||||
import { integrations } from "./integrations"
|
||||
import { API } from "@/api"
|
||||
import { duplicateName } from "@/helpers/duplicate"
|
||||
|
||||
const sortQueries = queryList => {
|
||||
queryList.sort((q1, q2) => {
|
||||
return q1.name.localeCompare(q2.name)
|
||||
})
|
||||
}
|
||||
|
||||
export function createQueriesStore() {
|
||||
const store = writable({
|
||||
list: [],
|
||||
selectedQueryId: null,
|
||||
})
|
||||
const derivedStore = derived(store, $store => ({
|
||||
...$store,
|
||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||
}))
|
||||
|
||||
const fetch = async () => {
|
||||
const queries = await API.getQueries()
|
||||
sortQueries(queries)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: queries,
|
||||
}))
|
||||
}
|
||||
|
||||
const save = async (datasourceId, query) => {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
store.update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return {
|
||||
list: queries,
|
||||
selectedQueryId: savedQuery._id,
|
||||
}
|
||||
})
|
||||
return savedQuery
|
||||
}
|
||||
|
||||
const importQueries = async ({ data, datasourceId }) => {
|
||||
return await API.importQueries(datasourceId, data)
|
||||
}
|
||||
|
||||
const select = id => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedQueryId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
const preview = async query => {
|
||||
const result = await API.previewQuery(query)
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema = {}
|
||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||
schema[field] = metadata || { type: "string" }
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
||||
const deleteQuery = async query => {
|
||||
await API.deleteQuery(query._id, query._rev)
|
||||
store.update(state => {
|
||||
state.list = state.list.filter(existing => existing._id !== query._id)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const duplicate = async query => {
|
||||
let list = get(store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
return await save(datasourceId, newQuery)
|
||||
}
|
||||
|
||||
const removeDatasourceQueries = datasourceId => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
init: fetch,
|
||||
select,
|
||||
save,
|
||||
import: importQueries,
|
||||
delete: deleteQuery,
|
||||
preview,
|
||||
duplicate,
|
||||
removeDatasourceQueries,
|
||||
}
|
||||
}
|
||||
|
||||
export const queries = createQueriesStore()
|
|
@ -0,0 +1,156 @@
|
|||
import { derived, get, Writable } from "svelte/store"
|
||||
import { datasources } from "./datasources"
|
||||
import { integrations } from "./integrations"
|
||||
import { API } from "@/api"
|
||||
import { duplicateName } from "@/helpers/duplicate"
|
||||
import { DerivedBudiStore } from "@/stores/BudiStore"
|
||||
import {
|
||||
Query,
|
||||
QueryPreview,
|
||||
PreviewQueryResponse,
|
||||
SaveQueryRequest,
|
||||
ImportRestQueryRequest,
|
||||
QuerySchema,
|
||||
} from "@budibase/types"
|
||||
|
||||
const sortQueries = (queryList: Query[]) => {
|
||||
queryList.sort((q1, q2) => {
|
||||
return q1.name.localeCompare(q2.name)
|
||||
})
|
||||
}
|
||||
|
||||
interface BuilderQueryStore {
|
||||
list: Query[]
|
||||
selectedQueryId: string | null
|
||||
}
|
||||
|
||||
interface DerivedQueryStore extends BuilderQueryStore {
|
||||
selected?: Query
|
||||
}
|
||||
|
||||
export class QueryStore extends DerivedBudiStore<
|
||||
BuilderQueryStore,
|
||||
DerivedQueryStore
|
||||
> {
|
||||
constructor() {
|
||||
const makeDerivedStore = (store: Writable<BuilderQueryStore>) => {
|
||||
return derived(store, ($store): DerivedQueryStore => {
|
||||
return {
|
||||
list: $store.list,
|
||||
selectedQueryId: $store.selectedQueryId,
|
||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
super(
|
||||
{
|
||||
list: [],
|
||||
selectedQueryId: null,
|
||||
},
|
||||
makeDerivedStore
|
||||
)
|
||||
|
||||
this.select = this.select.bind(this)
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
const queries = await API.getQueries()
|
||||
sortQueries(queries)
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: queries,
|
||||
}))
|
||||
}
|
||||
|
||||
async save(datasourceId: string, query: SaveQueryRequest) {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
this.store.update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return {
|
||||
list: queries,
|
||||
selectedQueryId: savedQuery._id || null,
|
||||
}
|
||||
})
|
||||
return savedQuery
|
||||
}
|
||||
|
||||
async importQueries(data: ImportRestQueryRequest) {
|
||||
return await API.importQueries(data)
|
||||
}
|
||||
|
||||
select(id: string | null) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
selectedQueryId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
async preview(query: QueryPreview): Promise<PreviewQueryResponse> {
|
||||
const result = await API.previewQuery(query)
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema: Record<string, QuerySchema> = {}
|
||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||
schema[field] = (metadata as QuerySchema) || { type: "string" }
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
||||
async delete(query: Query) {
|
||||
if (!query._id || !query._rev) {
|
||||
throw new Error("Query ID or Revision is missing")
|
||||
}
|
||||
await API.deleteQuery(query._id, query._rev)
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(existing => existing._id !== query._id),
|
||||
}))
|
||||
}
|
||||
|
||||
async duplicate(query: Query) {
|
||||
let list = get(this.store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
return await this.save(datasourceId, newQuery)
|
||||
}
|
||||
|
||||
removeDatasourceQueries(datasourceId: string) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
||||
}))
|
||||
}
|
||||
|
||||
init = this.fetch
|
||||
}
|
||||
|
||||
export const queries = new QueryStore()
|
|
@ -1,41 +1,71 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { update } from "lodash"
|
||||
import { licensing } from "."
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import {
|
||||
DeleteInviteUsersRequest,
|
||||
InviteUsersRequest,
|
||||
SearchUsersRequest,
|
||||
SearchUsersResponse,
|
||||
UpdateInviteRequest,
|
||||
User,
|
||||
UserIdentifier,
|
||||
UnsavedUser,
|
||||
} from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
export function createUsersStore() {
|
||||
const { subscribe, set } = writable({})
|
||||
interface UserInfo {
|
||||
email: string
|
||||
password: string
|
||||
forceResetPassword?: boolean
|
||||
role: keyof typeof Constants.BudibaseRoles
|
||||
}
|
||||
|
||||
// opts can contain page and search params
|
||||
async function search(opts = {}) {
|
||||
type UserState = SearchUsersResponse & SearchUsersRequest
|
||||
|
||||
class UserStore extends BudiStore<UserState> {
|
||||
constructor() {
|
||||
super({
|
||||
data: [],
|
||||
})
|
||||
}
|
||||
|
||||
async search(opts: SearchUsersRequest = {}) {
|
||||
const paged = await API.searchUsers(opts)
|
||||
set({
|
||||
this.set({
|
||||
...paged,
|
||||
...opts,
|
||||
})
|
||||
return paged
|
||||
}
|
||||
|
||||
async function get(userId) {
|
||||
async get(userId: string) {
|
||||
try {
|
||||
return await API.getUser(userId)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
const fetch = async () => {
|
||||
|
||||
async fetch() {
|
||||
return await API.getUsers()
|
||||
}
|
||||
|
||||
// One or more users.
|
||||
async function onboard(payload) {
|
||||
async onboard(payload: InviteUsersRequest) {
|
||||
return await API.onboardUsers(payload)
|
||||
}
|
||||
|
||||
async function invite(payload) {
|
||||
const users = payload.map(user => {
|
||||
async invite(
|
||||
payload: {
|
||||
admin?: boolean
|
||||
builder?: boolean
|
||||
creator?: boolean
|
||||
email: string
|
||||
apps?: any[]
|
||||
groups?: any[]
|
||||
}[]
|
||||
) {
|
||||
const users: InviteUsersRequest = payload.map(user => {
|
||||
let builder = undefined
|
||||
if (user.admin || user.builder) {
|
||||
builder = { global: true }
|
||||
|
@ -55,11 +85,16 @@ export function createUsersStore() {
|
|||
return API.inviteUsers(users)
|
||||
}
|
||||
|
||||
async function removeInvites(payload) {
|
||||
async removeInvites(payload: DeleteInviteUsersRequest) {
|
||||
return API.removeUserInvites(payload)
|
||||
}
|
||||
|
||||
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
||||
async acceptInvite(
|
||||
inviteCode: string,
|
||||
password: string,
|
||||
firstName: string,
|
||||
lastName?: string
|
||||
) {
|
||||
return API.acceptInvite({
|
||||
inviteCode,
|
||||
password,
|
||||
|
@ -68,21 +103,25 @@ export function createUsersStore() {
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchInvite(inviteCode) {
|
||||
async fetchInvite(inviteCode: string) {
|
||||
return API.getUserInvite(inviteCode)
|
||||
}
|
||||
|
||||
async function getInvites() {
|
||||
async getInvites() {
|
||||
return API.getUserInvites()
|
||||
}
|
||||
|
||||
async function updateInvite(invite) {
|
||||
return API.updateUserInvite(invite.code, invite)
|
||||
async updateInvite(code: string, invite: UpdateInviteRequest) {
|
||||
return API.updateUserInvite(code, invite)
|
||||
}
|
||||
|
||||
async function create(data) {
|
||||
let mappedUsers = data.users.map(user => {
|
||||
const body = {
|
||||
async getUserCountByApp(appId: string) {
|
||||
return await API.getUserCountByApp(appId)
|
||||
}
|
||||
|
||||
async create(data: { users: UserInfo[]; groups: any[] }) {
|
||||
let mappedUsers: UnsavedUser[] = data.users.map((user: any) => {
|
||||
const body: UnsavedUser = {
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
roles: {},
|
||||
|
@ -92,17 +131,17 @@ export function createUsersStore() {
|
|||
}
|
||||
|
||||
switch (user.role) {
|
||||
case "appUser":
|
||||
case Constants.BudibaseRoles.AppUser:
|
||||
body.builder = { global: false }
|
||||
body.admin = { global: false }
|
||||
break
|
||||
case "developer":
|
||||
case Constants.BudibaseRoles.Developer:
|
||||
body.builder = { global: true }
|
||||
break
|
||||
case "creator":
|
||||
case Constants.BudibaseRoles.Creator:
|
||||
body.builder = { creator: true, global: false }
|
||||
break
|
||||
case "admin":
|
||||
case Constants.BudibaseRoles.Admin:
|
||||
body.admin = { global: true }
|
||||
body.builder = { global: true }
|
||||
break
|
||||
|
@ -111,43 +150,47 @@ export function createUsersStore() {
|
|||
return body
|
||||
})
|
||||
const response = await API.createUsers(mappedUsers, data.groups)
|
||||
licensing.setQuotaUsage()
|
||||
|
||||
// re-search from first page
|
||||
await search()
|
||||
await this.search()
|
||||
return response
|
||||
}
|
||||
|
||||
async function del(id) {
|
||||
async delete(id: string) {
|
||||
await API.deleteUser(id)
|
||||
update(users => users.filter(user => user._id !== id))
|
||||
licensing.setQuotaUsage()
|
||||
}
|
||||
|
||||
async function getUserCountByApp(appId) {
|
||||
return await API.getUserCountByApp(appId)
|
||||
async bulkDelete(users: UserIdentifier[]) {
|
||||
const res = API.deleteUsers(users)
|
||||
licensing.setQuotaUsage()
|
||||
return res
|
||||
}
|
||||
|
||||
async function bulkDelete(users) {
|
||||
return API.deleteUsers(users)
|
||||
async save(user: User) {
|
||||
const res = await API.saveUser(user)
|
||||
licensing.setQuotaUsage()
|
||||
return res
|
||||
}
|
||||
|
||||
async function save(user) {
|
||||
return await API.saveUser(user)
|
||||
}
|
||||
|
||||
async function addAppBuilder(userId, appId) {
|
||||
async addAppBuilder(userId: string, appId: string) {
|
||||
return await API.addAppBuilder(userId, appId)
|
||||
}
|
||||
|
||||
async function removeAppBuilder(userId, appId) {
|
||||
async removeAppBuilder(userId: string, appId: string) {
|
||||
return await API.removeAppBuilder(userId, appId)
|
||||
}
|
||||
|
||||
async function getAccountHolder() {
|
||||
async getAccountHolder() {
|
||||
return await API.getAccountHolder()
|
||||
}
|
||||
|
||||
const getUserRole = user => {
|
||||
if (user && user.email === user.tenantOwnerEmail) {
|
||||
getUserRole(user?: User & { tenantOwnerEmail?: string }) {
|
||||
if (!user) {
|
||||
return Constants.BudibaseRoles.AppUser
|
||||
}
|
||||
if (user.email === user.tenantOwnerEmail) {
|
||||
return Constants.BudibaseRoles.Owner
|
||||
} else if (sdk.users.isAdmin(user)) {
|
||||
return Constants.BudibaseRoles.Admin
|
||||
|
@ -159,38 +202,6 @@ export function createUsersStore() {
|
|||
return Constants.BudibaseRoles.AppUser
|
||||
}
|
||||
}
|
||||
|
||||
const refreshUsage =
|
||||
fn =>
|
||||
async (...args) => {
|
||||
const response = await fn(...args)
|
||||
await licensing.setQuotaUsage()
|
||||
return response
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
search,
|
||||
get,
|
||||
getUserRole,
|
||||
fetch,
|
||||
invite,
|
||||
onboard,
|
||||
fetchInvite,
|
||||
getInvites,
|
||||
removeInvites,
|
||||
updateInvite,
|
||||
getUserCountByApp,
|
||||
addAppBuilder,
|
||||
removeAppBuilder,
|
||||
// any operation that adds or deletes users
|
||||
acceptInvite,
|
||||
create: refreshUsage(create),
|
||||
save: refreshUsage(save),
|
||||
bulkDelete: refreshUsage(bulkDelete),
|
||||
delete: refreshUsage(del),
|
||||
getAccountHolder,
|
||||
}
|
||||
}
|
||||
|
||||
export const users = createUsersStore()
|
||||
export const users = new UserStore()
|
|
@ -1,6 +1,10 @@
|
|||
import { createAPIClient } from "@budibase/frontend-core"
|
||||
import { authStore } from "../stores/auth.js"
|
||||
import { notificationStore, devToolsEnabled, devToolsStore } from "../stores/"
|
||||
import { authStore } from "../stores/auth"
|
||||
import {
|
||||
notificationStore,
|
||||
devToolsEnabled,
|
||||
devToolsStore,
|
||||
} from "../stores/index"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const API = createAPIClient({
|
|
@ -1,5 +1,5 @@
|
|||
import { API } from "./api.js"
|
||||
import { patchAPI } from "./patches.js"
|
||||
import { API } from "./api"
|
||||
import { patchAPI } from "./patches"
|
||||
|
||||
// Certain endpoints which return rows need patched so that they transform
|
||||
// and enrich the row docs, so that they can be correctly handled by the
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
interface Window {
|
||||
"##BUDIBASE_APP_ID##": string
|
||||
"##BUDIBASE_IN_BUILDER##": string
|
||||
MIGRATING_APP: boolean
|
||||
}
|
|
@ -29,7 +29,7 @@ import { ActionTypes } from "./constants"
|
|||
import {
|
||||
fetchDatasourceSchema,
|
||||
fetchDatasourceDefinition,
|
||||
} from "./utils/schema.js"
|
||||
} from "./utils/schema"
|
||||
import { getAPIKey } from "./utils/api.js"
|
||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
||||
|
|
|
@ -2,7 +2,9 @@ import { API } from "api"
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
const createAuthStore = () => {
|
||||
const store = writable(null)
|
||||
const store = writable<{
|
||||
csrfToken?: string
|
||||
} | null>(null)
|
||||
|
||||
// Fetches the user object if someone is logged in and has reloaded the page
|
||||
const fetchUser = async () => {
|
|
@ -1,7 +1,7 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { devToolsStore } from "../devTools.js"
|
||||
import { authStore } from "../auth.js"
|
||||
import { authStore } from "../auth"
|
||||
import { devToolsEnabled } from "./devToolsEnabled.js"
|
||||
|
||||
// Derive the current role of the logged-in user
|
||||
|
|
|
@ -6,7 +6,7 @@ const DEFAULT_NOTIFICATION_TIMEOUT = 3000
|
|||
const createNotificationStore = () => {
|
||||
let block = false
|
||||
|
||||
const store = writable([])
|
||||
const store = writable<{ id: string; message: string; count: number }[]>([])
|
||||
|
||||
const blockNotifications = (timeout = 1000) => {
|
||||
block = true
|
||||
|
@ -14,11 +14,11 @@ const createNotificationStore = () => {
|
|||
}
|
||||
|
||||
const send = (
|
||||
message,
|
||||
message: string,
|
||||
type = "info",
|
||||
icon,
|
||||
icon: string,
|
||||
autoDismiss = true,
|
||||
duration,
|
||||
duration?: number,
|
||||
count = 1
|
||||
) => {
|
||||
if (block) {
|
||||
|
@ -66,7 +66,7 @@ const createNotificationStore = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const dismiss = id => {
|
||||
const dismiss = (id: string) => {
|
||||
store.update(state => {
|
||||
return state.filter(n => n.id !== id)
|
||||
})
|
||||
|
@ -76,13 +76,13 @@ const createNotificationStore = () => {
|
|||
subscribe: store.subscribe,
|
||||
actions: {
|
||||
send,
|
||||
info: (msg, autoDismiss, duration) =>
|
||||
info: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "info", "Info", autoDismiss ?? true, duration),
|
||||
success: (msg, autoDismiss, duration) =>
|
||||
success: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "success", "CheckmarkCircle", autoDismiss ?? true, duration),
|
||||
warning: (msg, autoDismiss, duration) =>
|
||||
warning: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "warning", "Alert", autoDismiss ?? true, duration),
|
||||
error: (msg, autoDismiss, duration) =>
|
||||
error: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "error", "Alert", autoDismiss ?? false, duration),
|
||||
blockNotifications,
|
||||
dismiss,
|
|
@ -4,8 +4,24 @@ import { API } from "api"
|
|||
import { peekStore } from "./peek"
|
||||
import { builderStore } from "./builder"
|
||||
|
||||
interface Route {
|
||||
path: string
|
||||
screenId: string
|
||||
}
|
||||
|
||||
interface StoreType {
|
||||
routes: Route[]
|
||||
routeParams: {}
|
||||
activeRoute?: Route | null
|
||||
routeSessionId: number
|
||||
routerLoaded: boolean
|
||||
queryParams?: {
|
||||
peek?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const createRouteStore = () => {
|
||||
const initialState = {
|
||||
const initialState: StoreType = {
|
||||
routes: [],
|
||||
routeParams: {},
|
||||
activeRoute: null,
|
||||
|
@ -22,7 +38,7 @@ const createRouteStore = () => {
|
|||
} catch (error) {
|
||||
routeConfig = null
|
||||
}
|
||||
let routes = []
|
||||
const routes: Route[] = []
|
||||
Object.values(routeConfig?.routes || {}).forEach(route => {
|
||||
Object.entries(route.subpaths || {}).forEach(([path, config]) => {
|
||||
routes.push({
|
||||
|
@ -43,13 +59,13 @@ const createRouteStore = () => {
|
|||
return state
|
||||
})
|
||||
}
|
||||
const setRouteParams = routeParams => {
|
||||
const setRouteParams = (routeParams: StoreType["routeParams"]) => {
|
||||
store.update(state => {
|
||||
state.routeParams = routeParams
|
||||
return state
|
||||
})
|
||||
}
|
||||
const setQueryParams = queryParams => {
|
||||
const setQueryParams = (queryParams: { peek?: boolean }) => {
|
||||
store.update(state => {
|
||||
state.queryParams = {
|
||||
...queryParams,
|
||||
|
@ -60,13 +76,13 @@ const createRouteStore = () => {
|
|||
return state
|
||||
})
|
||||
}
|
||||
const setActiveRoute = route => {
|
||||
const setActiveRoute = (route: string) => {
|
||||
store.update(state => {
|
||||
state.activeRoute = state.routes.find(x => x.path === route)
|
||||
return state
|
||||
})
|
||||
}
|
||||
const navigate = (url, peek, externalNewTab) => {
|
||||
const navigate = (url: string, peek: boolean, externalNewTab: boolean) => {
|
||||
if (get(builderStore).inBuilder) {
|
||||
return
|
||||
}
|
||||
|
@ -93,7 +109,7 @@ const createRouteStore = () => {
|
|||
const setRouterLoaded = () => {
|
||||
store.update(state => ({ ...state, routerLoaded: true }))
|
||||
}
|
||||
const createFullURL = relativeURL => {
|
||||
const createFullURL = (relativeURL: string) => {
|
||||
if (!relativeURL?.startsWith("/")) {
|
||||
return relativeURL
|
||||
}
|
|
@ -1,13 +1,5 @@
|
|||
import { API } from "api"
|
||||
import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch"
|
||||
import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch"
|
||||
import QueryFetch from "@budibase/frontend-core/src/fetch/QueryFetch"
|
||||
import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFetch"
|
||||
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch"
|
||||
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch"
|
||||
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch"
|
||||
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch"
|
||||
import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
||||
import { DataFetchMap, DataFetchType } from "@budibase/frontend-core"
|
||||
|
||||
/**
|
||||
* Constructs a fetch instance for a given datasource.
|
||||
|
@ -16,22 +8,20 @@ import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
|||
* @param datasource the datasource
|
||||
* @returns
|
||||
*/
|
||||
const getDatasourceFetchInstance = datasource => {
|
||||
const handler = {
|
||||
table: TableFetch,
|
||||
view: ViewFetch,
|
||||
viewV2: ViewV2Fetch,
|
||||
query: QueryFetch,
|
||||
link: RelationshipFetch,
|
||||
provider: NestedProviderFetch,
|
||||
field: FieldFetch,
|
||||
jsonarray: JSONArrayFetch,
|
||||
queryarray: QueryArrayFetch,
|
||||
}[datasource?.type]
|
||||
const getDatasourceFetchInstance = <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>(
|
||||
datasource: TDatasource
|
||||
) => {
|
||||
const handler = DataFetchMap[datasource?.type]
|
||||
if (!handler) {
|
||||
return null
|
||||
}
|
||||
return new handler({ API, datasource })
|
||||
return new handler({
|
||||
API,
|
||||
datasource: datasource as never,
|
||||
query: null as any,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,21 +29,23 @@ const getDatasourceFetchInstance = datasource => {
|
|||
* @param datasource the datasource to fetch the schema for
|
||||
* @param options options for enriching the schema
|
||||
*/
|
||||
export const fetchDatasourceSchema = async (
|
||||
datasource,
|
||||
export const fetchDatasourceSchema = async <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>(
|
||||
datasource: TDatasource,
|
||||
options = { enrichRelationships: false, formSchema: false }
|
||||
) => {
|
||||
const instance = getDatasourceFetchInstance(datasource)
|
||||
const definition = await instance?.getDefinition(datasource)
|
||||
if (!definition) {
|
||||
const definition = await instance?.getDefinition()
|
||||
if (!instance || !definition) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the normal schema as long as we aren't wanting a form schema
|
||||
let schema
|
||||
let schema: any
|
||||
if (datasource?.type !== "query" || !options?.formSchema) {
|
||||
schema = instance.getSchema(definition)
|
||||
} else if (definition.parameters?.length) {
|
||||
schema = instance.getSchema(definition as any)
|
||||
} else if ("parameters" in definition && definition.parameters?.length) {
|
||||
schema = {}
|
||||
definition.parameters.forEach(param => {
|
||||
schema[param.name] = { ...param, type: "string" }
|
||||
|
@ -73,7 +65,12 @@ export const fetchDatasourceSchema = async (
|
|||
}
|
||||
|
||||
// Enrich schema with relationships if required
|
||||
if (definition?.sql && options?.enrichRelationships) {
|
||||
if (
|
||||
definition &&
|
||||
"sql" in definition &&
|
||||
definition.sql &&
|
||||
options?.enrichRelationships
|
||||
) {
|
||||
const relationshipAdditions = await getRelationshipSchemaAdditions(schema)
|
||||
schema = {
|
||||
...schema,
|
||||
|
@ -89,20 +86,26 @@ export const fetchDatasourceSchema = async (
|
|||
* Fetches the definition of any kind of datasource.
|
||||
* @param datasource the datasource to fetch the schema for
|
||||
*/
|
||||
export const fetchDatasourceDefinition = async datasource => {
|
||||
export const fetchDatasourceDefinition = async <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>(
|
||||
datasource: TDatasource
|
||||
) => {
|
||||
const instance = getDatasourceFetchInstance(datasource)
|
||||
return await instance?.getDefinition(datasource)
|
||||
return await instance?.getDefinition()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the schema of relationship fields for a SQL table schema
|
||||
* @param schema the schema to enrich
|
||||
*/
|
||||
export const getRelationshipSchemaAdditions = async schema => {
|
||||
export const getRelationshipSchemaAdditions = async (
|
||||
schema: Record<string, any>
|
||||
) => {
|
||||
if (!schema) {
|
||||
return null
|
||||
}
|
||||
let relationshipAdditions = {}
|
||||
let relationshipAdditions: Record<string, any> = {}
|
||||
for (let fieldKey of Object.keys(schema)) {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema?.type === "link") {
|
||||
|
@ -110,7 +113,10 @@ export const getRelationshipSchemaAdditions = async schema => {
|
|||
type: "table",
|
||||
tableId: fieldSchema?.tableId,
|
||||
})
|
||||
Object.keys(linkSchema || {}).forEach(linkKey => {
|
||||
if (!linkSchema) {
|
||||
continue
|
||||
}
|
||||
Object.keys(linkSchema).forEach(linkKey => {
|
||||
relationshipAdditions[`${fieldKey}.${linkKey}`] = {
|
||||
type: linkSchema[linkKey].type,
|
||||
externalType: linkSchema[linkKey].externalType,
|
|
@ -68,13 +68,13 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
): Promise<APIError> => {
|
||||
// Try to read a message from the error
|
||||
let message = response.statusText
|
||||
let json: any = null
|
||||
let json = null
|
||||
try {
|
||||
json = await response.json()
|
||||
if (json?.message) {
|
||||
message = json.message
|
||||
} else if (json?.error) {
|
||||
message = json.error
|
||||
message = JSON.stringify(json.error)
|
||||
}
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
|
@ -93,7 +93,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
// Generates an error object from a string
|
||||
const makeError = (
|
||||
message: string,
|
||||
url?: string,
|
||||
url: string,
|
||||
method?: HTTPMethod
|
||||
): APIError => {
|
||||
return {
|
||||
|
@ -232,7 +232,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
return await handler(callConfig)
|
||||
} catch (error) {
|
||||
if (config?.onError) {
|
||||
config.onError(error)
|
||||
config.onError(error as APIError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
@ -245,13 +245,9 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
patch: requestApiCall(HTTPMethod.PATCH),
|
||||
delete: requestApiCall(HTTPMethod.DELETE),
|
||||
put: requestApiCall(HTTPMethod.PUT),
|
||||
error: (message: string) => {
|
||||
throw makeError(message)
|
||||
},
|
||||
invalidateCache: () => {
|
||||
cache = {}
|
||||
},
|
||||
|
||||
// Generic utility to extract the current app ID. Assumes that any client
|
||||
// that exists in an app context will be attaching our app ID header.
|
||||
getAppID: (): string => {
|
||||
|
|
|
@ -46,7 +46,7 @@ export type Headers = Record<string, string>
|
|||
export type APIClientConfig = {
|
||||
enableCaching?: boolean
|
||||
attachHeaders?: (headers: Headers) => void
|
||||
onError?: (error: any) => void
|
||||
onError?: (error: APIError) => void
|
||||
onMigrationDetected?: (migration: string) => void
|
||||
}
|
||||
|
||||
|
@ -86,14 +86,13 @@ export type BaseAPIClient = {
|
|||
patch: <RequestT = null, ResponseT = void>(
|
||||
params: APICallParams<RequestT, ResponseT>
|
||||
) => Promise<ResponseT>
|
||||
error: (message: string) => void
|
||||
invalidateCache: () => void
|
||||
getAppID: () => string
|
||||
}
|
||||
|
||||
export type APIError = {
|
||||
message?: string
|
||||
url?: string
|
||||
url: string
|
||||
method?: HTTPMethod
|
||||
json: any
|
||||
status: number
|
||||
|
|
|
@ -21,11 +21,12 @@ import {
|
|||
SaveUserResponse,
|
||||
SearchUsersRequest,
|
||||
SearchUsersResponse,
|
||||
UnsavedUser,
|
||||
UpdateInviteRequest,
|
||||
UpdateInviteResponse,
|
||||
UpdateSelfMetadataRequest,
|
||||
UpdateSelfMetadataResponse,
|
||||
User,
|
||||
UserIdentifier,
|
||||
} from "@budibase/types"
|
||||
import { BaseAPIClient } from "./types"
|
||||
|
||||
|
@ -38,14 +39,9 @@ export interface UserEndpoints {
|
|||
createAdminUser: (
|
||||
user: CreateAdminUserRequest
|
||||
) => Promise<CreateAdminUserResponse>
|
||||
saveUser: (user: User) => Promise<SaveUserResponse>
|
||||
saveUser: (user: UnsavedUser) => Promise<SaveUserResponse>
|
||||
deleteUser: (userId: string) => Promise<DeleteUserResponse>
|
||||
deleteUsers: (
|
||||
users: Array<{
|
||||
userId: string
|
||||
email: string
|
||||
}>
|
||||
) => Promise<BulkUserDeleted | undefined>
|
||||
deleteUsers: (users: UserIdentifier[]) => Promise<BulkUserDeleted | undefined>
|
||||
onboardUsers: (data: InviteUsersRequest) => Promise<InviteUsersResponse>
|
||||
getUserInvite: (code: string) => Promise<CheckInviteResponse>
|
||||
getUserInvites: () => Promise<GetUserInvitesResponse>
|
||||
|
@ -60,7 +56,7 @@ export interface UserEndpoints {
|
|||
getAccountHolder: () => Promise<LookupAccountHolderResponse>
|
||||
searchUsers: (data: SearchUsersRequest) => Promise<SearchUsersResponse>
|
||||
createUsers: (
|
||||
users: User[],
|
||||
users: UnsavedUser[],
|
||||
groups: any[]
|
||||
) => Promise<BulkUserCreated | undefined>
|
||||
updateUserInvite: (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
import { FieldType, UIColumn } from "@budibase/types"
|
||||
|
||||
import OptionsCell from "../cells/OptionsCell.svelte"
|
||||
import DateCell from "../cells/DateCell.svelte"
|
||||
|
@ -40,13 +40,23 @@ const TypeComponentMap = {
|
|||
// Custom types for UI only
|
||||
role: RoleCell,
|
||||
}
|
||||
export const getCellRenderer = column => {
|
||||
|
||||
function getCellRendererByType(type: FieldType | "role" | undefined) {
|
||||
if (!type) {
|
||||
return
|
||||
}
|
||||
|
||||
return TypeComponentMap[type as keyof typeof TypeComponentMap]
|
||||
}
|
||||
|
||||
export const getCellRenderer = (column: UIColumn) => {
|
||||
if (column.calculationType) {
|
||||
return NumberCell
|
||||
}
|
||||
|
||||
return (
|
||||
TypeComponentMap[column?.schema?.cellRenderType] ||
|
||||
TypeComponentMap[column?.schema?.type] ||
|
||||
getCellRendererByType(column.schema?.cellRenderType) ||
|
||||
getCellRendererByType(column.schema?.type) ||
|
||||
TextCell
|
||||
)
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// TODO: remove when all stores are typed
|
||||
|
||||
import { GeneratedIDPrefix, CellIDSeparator } from "./constants"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
export const parseCellID = cellId => {
|
||||
if (!cellId) {
|
||||
return { rowId: undefined, field: undefined }
|
||||
}
|
||||
const parts = cellId.split(CellIDSeparator)
|
||||
const field = parts.pop()
|
||||
return { rowId: parts.join(CellIDSeparator), field }
|
||||
}
|
||||
|
||||
export const getCellID = (rowId, fieldName) => {
|
||||
return `${rowId}${CellIDSeparator}${fieldName}`
|
||||
}
|
||||
|
||||
export const parseEventLocation = e => {
|
||||
return {
|
||||
x: e.clientX ?? e.touches?.[0]?.clientX,
|
||||
y: e.clientY ?? e.touches?.[0]?.clientY,
|
||||
}
|
||||
}
|
||||
|
||||
export const generateRowID = () => {
|
||||
return `${GeneratedIDPrefix}${Helpers.uuid()}`
|
||||
}
|
||||
|
||||
export const isGeneratedRowID = id => {
|
||||
return id?.startsWith(GeneratedIDPrefix)
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
import { get } from "svelte/store"
|
||||
import { createWebsocket } from "../../../utils"
|
||||
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
||||
import { Store } from "../stores"
|
||||
import { UIDatasource, UIUser } from "@budibase/types"
|
||||
|
||||
export const createGridWebsocket = context => {
|
||||
export const createGridWebsocket = (context: Store) => {
|
||||
const { rows, datasource, users, focusedCellId, definition, API } = context
|
||||
const socket = createWebsocket("/socket/grid")
|
||||
|
||||
const connectToDatasource = datasource => {
|
||||
const connectToDatasource = (datasource: UIDatasource) => {
|
||||
if (!socket.connected) {
|
||||
return
|
||||
}
|
||||
|
@ -18,7 +20,7 @@ export const createGridWebsocket = context => {
|
|||
datasource,
|
||||
appId,
|
||||
},
|
||||
({ users: gridUsers }) => {
|
||||
({ users: gridUsers }: { users: UIUser[] }) => {
|
||||
users.set(gridUsers)
|
||||
}
|
||||
)
|
||||
|
@ -65,7 +67,7 @@ export const createGridWebsocket = context => {
|
|||
GridSocketEvent.DatasourceChange,
|
||||
({ datasource: newDatasource }) => {
|
||||
// Listen builder renames, as these aren't handled otherwise
|
||||
if (newDatasource?.name !== get(definition).name) {
|
||||
if (newDatasource?.name !== get(definition)?.name) {
|
||||
definition.set(newDatasource)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import DataFetch from "./DataFetch"
|
||||
|
||||
interface CustomDatasource {
|
||||
type: "custom"
|
||||
data: any
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
UISearchFilter,
|
||||
} from "@budibase/types"
|
||||
import { APIClient } from "../api/types"
|
||||
import { DataFetchType } from "."
|
||||
|
||||
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
|
||||
|
||||
|
@ -59,7 +60,7 @@ export interface DataFetchParams<
|
|||
* For other types of datasource, this class is overridden and extended.
|
||||
*/
|
||||
export default abstract class DataFetch<
|
||||
TDatasource extends {},
|
||||
TDatasource extends { type: DataFetchType },
|
||||
TDefinition extends {
|
||||
schema?: Record<string, any> | null
|
||||
primaryDisplay?: string
|
||||
|
@ -368,7 +369,7 @@ export default abstract class DataFetch<
|
|||
* @param schema the datasource schema
|
||||
* @return {object} the enriched datasource schema
|
||||
*/
|
||||
private enrichSchema(schema: TableSchema): TableSchema {
|
||||
enrichSchema(schema: TableSchema): TableSchema {
|
||||
// Check for any JSON fields so we can add any top level properties
|
||||
let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
|
||||
for (const fieldKey of Object.keys(schema)) {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { Row } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
|
||||
export interface FieldDatasource {
|
||||
type Types = "field" | "queryarray" | "jsonarray"
|
||||
|
||||
export interface FieldDatasource<TType extends Types> {
|
||||
type: TType
|
||||
tableId: string
|
||||
fieldType: "attachment" | "array"
|
||||
value: string[] | Row[]
|
||||
|
@ -15,8 +18,8 @@ function isArrayOfStrings(value: string[] | Row[]): value is string[] {
|
|||
return Array.isArray(value) && !!value[0] && typeof value[0] !== "object"
|
||||
}
|
||||
|
||||
export default class FieldFetch extends DataFetch<
|
||||
FieldDatasource,
|
||||
export default class FieldFetch<TType extends Types> extends DataFetch<
|
||||
FieldDatasource<TType>,
|
||||
FieldDefinition
|
||||
> {
|
||||
async getDefinition(): Promise<FieldDefinition | null> {
|
||||
|
|
|
@ -8,6 +8,7 @@ interface GroupUserQuery {
|
|||
}
|
||||
|
||||
interface GroupUserDatasource {
|
||||
type: "groupUser"
|
||||
tableId: TableNames.USERS
|
||||
}
|
||||
|
||||
|
@ -20,6 +21,7 @@ export default class GroupUserFetch extends DataFetch<
|
|||
super({
|
||||
...opts,
|
||||
datasource: {
|
||||
type: "groupUser",
|
||||
tableId: TableNames.USERS,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import FieldFetch from "./FieldFetch"
|
||||
import { getJSONArrayDatasourceSchema } from "../utils/json"
|
||||
|
||||
export default class JSONArrayFetch extends FieldFetch {
|
||||
export default class JSONArrayFetch extends FieldFetch<"jsonarray"> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Row, TableSchema } from "@budibase/types"
|
|||
import DataFetch from "./DataFetch"
|
||||
|
||||
interface NestedProviderDatasource {
|
||||
type: "provider"
|
||||
value?: {
|
||||
schema: TableSchema
|
||||
primaryDisplay: string
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
generateQueryArraySchemas,
|
||||
} from "../utils/json"
|
||||
|
||||
export default class QueryArrayFetch extends FieldFetch {
|
||||
export default class QueryArrayFetch extends FieldFetch<"queryarray"> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ExecuteQueryRequest, Query } from "@budibase/types"
|
|||
import { get } from "svelte/store"
|
||||
|
||||
interface QueryDatasource {
|
||||
type: "query"
|
||||
_id: string
|
||||
fields: Record<string, any> & {
|
||||
pagination?: {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Table } from "@budibase/types"
|
|||
import DataFetch from "./DataFetch"
|
||||
|
||||
interface RelationshipDatasource {
|
||||
type: "link"
|
||||
tableId: string
|
||||
rowId: string
|
||||
rowTableId: string
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { get } from "svelte/store"
|
||||
import DataFetch from "./DataFetch"
|
||||
import { SortOrder, Table, UITable } from "@budibase/types"
|
||||
import { SortOrder, Table } from "@budibase/types"
|
||||
|
||||
export default class TableFetch extends DataFetch<UITable, Table> {
|
||||
interface TableDatasource {
|
||||
type: "table"
|
||||
tableId: string
|
||||
}
|
||||
|
||||
export default class TableFetch extends DataFetch<TableDatasource, Table> {
|
||||
async determineFeatureFlags() {
|
||||
return {
|
||||
supportsSearch: true,
|
||||
|
|
|
@ -2,11 +2,7 @@ import { get } from "svelte/store"
|
|||
import DataFetch, { DataFetchParams } from "./DataFetch"
|
||||
import { TableNames } from "../constants"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import {
|
||||
BasicOperator,
|
||||
SearchFilters,
|
||||
SearchUsersRequest,
|
||||
} from "@budibase/types"
|
||||
import { SearchFilters, SearchUsersRequest } from "@budibase/types"
|
||||
|
||||
interface UserFetchQuery {
|
||||
appId: string
|
||||
|
@ -14,18 +10,22 @@ interface UserFetchQuery {
|
|||
}
|
||||
|
||||
interface UserDatasource {
|
||||
tableId: string
|
||||
type: "user"
|
||||
tableId: TableNames.USERS
|
||||
}
|
||||
|
||||
interface UserDefinition {}
|
||||
|
||||
export default class UserFetch extends DataFetch<
|
||||
UserDatasource,
|
||||
{},
|
||||
UserDefinition,
|
||||
UserFetchQuery
|
||||
> {
|
||||
constructor(opts: DataFetchParams<UserDatasource, UserFetchQuery>) {
|
||||
super({
|
||||
...opts,
|
||||
datasource: {
|
||||
type: "user",
|
||||
tableId: TableNames.USERS,
|
||||
},
|
||||
})
|
||||
|
@ -52,7 +52,7 @@ export default class UserFetch extends DataFetch<
|
|||
|
||||
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
|
||||
? rest
|
||||
: { [BasicOperator.EMPTY]: { email: null } }
|
||||
: {}
|
||||
|
||||
try {
|
||||
const opts: SearchUsersRequest = {
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import { Table, View } from "@budibase/types"
|
||||
import { Table } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
|
||||
type ViewV1 = View & { name: string }
|
||||
type ViewV1Datasource = {
|
||||
type: "view"
|
||||
name: string
|
||||
tableId: string
|
||||
calculation: string
|
||||
field: string
|
||||
groupBy: string
|
||||
}
|
||||
|
||||
export default class ViewFetch extends DataFetch<ViewV1, Table> {
|
||||
export default class ViewFetch extends DataFetch<ViewV1Datasource, Table> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import { SortOrder, UIView, ViewV2, ViewV2Type } from "@budibase/types"
|
||||
import { SortOrder, ViewV2Enriched, ViewV2Type } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
import { get } from "svelte/store"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export default class ViewV2Fetch extends DataFetch<UIView, ViewV2> {
|
||||
interface ViewDatasource {
|
||||
type: "viewV2"
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class ViewV2Fetch extends DataFetch<
|
||||
ViewDatasource,
|
||||
ViewV2Enriched
|
||||
> {
|
||||
async determineFeatureFlags() {
|
||||
return {
|
||||
supportsSearch: true,
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import TableFetch from "./TableFetch.js"
|
||||
import ViewFetch from "./ViewFetch.js"
|
||||
import ViewV2Fetch from "./ViewV2Fetch.js"
|
||||
import TableFetch from "./TableFetch"
|
||||
import ViewFetch from "./ViewFetch"
|
||||
import ViewV2Fetch from "./ViewV2Fetch"
|
||||
import QueryFetch from "./QueryFetch"
|
||||
import RelationshipFetch from "./RelationshipFetch"
|
||||
import NestedProviderFetch from "./NestedProviderFetch"
|
||||
import FieldFetch from "./FieldFetch"
|
||||
import JSONArrayFetch from "./JSONArrayFetch"
|
||||
import UserFetch from "./UserFetch.js"
|
||||
import UserFetch from "./UserFetch"
|
||||
import GroupUserFetch from "./GroupUserFetch"
|
||||
import CustomFetch from "./CustomFetch"
|
||||
import QueryArrayFetch from "./QueryArrayFetch.js"
|
||||
import { APIClient } from "../api/types.js"
|
||||
import QueryArrayFetch from "./QueryArrayFetch"
|
||||
import { APIClient } from "../api/types"
|
||||
|
||||
const DataFetchMap = {
|
||||
export type DataFetchType = keyof typeof DataFetchMap
|
||||
|
||||
export const DataFetchMap = {
|
||||
table: TableFetch,
|
||||
view: ViewFetch,
|
||||
viewV2: ViewV2Fetch,
|
||||
|
@ -24,15 +26,14 @@ const DataFetchMap = {
|
|||
|
||||
// Client specific datasource types
|
||||
provider: NestedProviderFetch,
|
||||
field: FieldFetch,
|
||||
field: FieldFetch<"field">,
|
||||
jsonarray: JSONArrayFetch,
|
||||
queryarray: QueryArrayFetch,
|
||||
}
|
||||
|
||||
// Constructs a new fetch model for a certain datasource
|
||||
export const fetchData = ({ API, datasource, options }: any) => {
|
||||
const Fetch =
|
||||
DataFetchMap[datasource?.type as keyof typeof DataFetchMap] || TableFetch
|
||||
const Fetch = DataFetchMap[datasource?.type as DataFetchType] || TableFetch
|
||||
const fetch = new Fetch({ API, datasource, ...options })
|
||||
|
||||
// Initially fetch data but don't bother waiting for the result
|
||||
|
@ -43,29 +44,27 @@ export const fetchData = ({ API, datasource, options }: any) => {
|
|||
|
||||
// Creates an empty fetch instance with no datasource configured, so no data
|
||||
// will initially be loaded
|
||||
const createEmptyFetchInstance = <
|
||||
TDatasource extends {
|
||||
type: keyof typeof DataFetchMap
|
||||
}
|
||||
>({
|
||||
const createEmptyFetchInstance = <TDatasource extends { type: DataFetchType }>({
|
||||
API,
|
||||
datasource,
|
||||
}: {
|
||||
API: APIClient
|
||||
datasource: TDatasource
|
||||
}) => {
|
||||
const handler = DataFetchMap[datasource?.type as keyof typeof DataFetchMap]
|
||||
const handler = DataFetchMap[datasource?.type as DataFetchType]
|
||||
if (!handler) {
|
||||
return null
|
||||
}
|
||||
return new handler({ API, datasource: null as any, query: null as any })
|
||||
return new handler({
|
||||
API,
|
||||
datasource: null as never,
|
||||
query: null as any,
|
||||
})
|
||||
}
|
||||
|
||||
// Fetches the definition of any type of datasource
|
||||
export const getDatasourceDefinition = async <
|
||||
TDatasource extends {
|
||||
type: keyof typeof DataFetchMap
|
||||
}
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>({
|
||||
API,
|
||||
datasource,
|
||||
|
@ -79,9 +78,7 @@ export const getDatasourceDefinition = async <
|
|||
|
||||
// Fetches the schema of any type of datasource
|
||||
export const getDatasourceSchema = <
|
||||
TDatasource extends {
|
||||
type: keyof typeof DataFetchMap
|
||||
}
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>({
|
||||
API,
|
||||
datasource,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export { createAPIClient } from "./api"
|
||||
export { fetchData } from "./fetch"
|
||||
export { fetchData, DataFetchMap } from "./fetch"
|
||||
export type { DataFetchType } from "./fetch"
|
||||
export * as Constants from "./constants"
|
||||
export * from "./stores"
|
||||
export * from "./utils"
|
||||
|
|
|
@ -209,6 +209,9 @@ export const buildFormBlockButtonConfig = props => {
|
|||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Modal",
|
||||
},
|
||||
|
||||
...(actionUrl
|
||||
? [
|
||||
|
|
|
@ -2341,7 +2341,7 @@ if (descriptions.length) {
|
|||
[FieldType.ARRAY]: ["options 2", "options 4"],
|
||||
[FieldType.NUMBER]: generator.natural(),
|
||||
[FieldType.BOOLEAN]: generator.bool(),
|
||||
[FieldType.DATETIME]: generator.date().toISOString(),
|
||||
[FieldType.DATETIME]: generator.date().toISOString().slice(0, 10),
|
||||
[FieldType.ATTACHMENTS]: [setup.structures.basicAttachment()],
|
||||
[FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(),
|
||||
[FieldType.FORMULA]: undefined, // generated field
|
||||
|
|
|
@ -1683,6 +1683,151 @@ if (descriptions.length) {
|
|||
})
|
||||
})
|
||||
|
||||
describe("datetime - date only", () => {
|
||||
describe.each([true, false])(
|
||||
"saved with timestamp: %s",
|
||||
saveWithTimestamp => {
|
||||
describe.each([true, false])(
|
||||
"search with timestamp: %s",
|
||||
searchWithTimestamp => {
|
||||
const SAVE_SUFFIX = saveWithTimestamp
|
||||
? "T00:00:00.000Z"
|
||||
: ""
|
||||
const SEARCH_SUFFIX = searchWithTimestamp
|
||||
? "T00:00:00.000Z"
|
||||
: ""
|
||||
|
||||
const JAN_1ST = `2020-01-01`
|
||||
const JAN_10TH = `2020-01-10`
|
||||
const JAN_30TH = `2020-01-30`
|
||||
const UNEXISTING_DATE = `2020-01-03`
|
||||
const NULL_DATE__ID = `null_date__id`
|
||||
|
||||
beforeAll(async () => {
|
||||
tableOrViewId = await createTableOrView({
|
||||
dateid: { name: "dateid", type: FieldType.STRING },
|
||||
date: {
|
||||
name: "date",
|
||||
type: FieldType.DATETIME,
|
||||
dateOnly: true,
|
||||
},
|
||||
})
|
||||
|
||||
await createRows([
|
||||
{ dateid: NULL_DATE__ID, date: null },
|
||||
{ date: `${JAN_1ST}${SAVE_SUFFIX}` },
|
||||
{ date: `${JAN_10TH}${SAVE_SUFFIX}` },
|
||||
])
|
||||
})
|
||||
|
||||
describe("equal", () => {
|
||||
it("successfully finds a row", async () => {
|
||||
await expectQuery({
|
||||
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
|
||||
}).toContainExactly([{ date: JAN_1ST }])
|
||||
})
|
||||
|
||||
it("successfully finds an ISO8601 row", async () => {
|
||||
await expectQuery({
|
||||
equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` },
|
||||
}).toContainExactly([{ date: JAN_10TH }])
|
||||
})
|
||||
|
||||
it("finds a row with ISO8601 timestamp", async () => {
|
||||
await expectQuery({
|
||||
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
|
||||
}).toContainExactly([{ date: JAN_1ST }])
|
||||
})
|
||||
|
||||
it("fails to find nonexistent row", async () => {
|
||||
await expectQuery({
|
||||
equal: {
|
||||
date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`,
|
||||
},
|
||||
}).toFindNothing()
|
||||
})
|
||||
})
|
||||
|
||||
describe("notEqual", () => {
|
||||
it("successfully finds a row", async () => {
|
||||
await expectQuery({
|
||||
notEqual: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
|
||||
}).toContainExactly([
|
||||
{ date: JAN_10TH },
|
||||
{ dateid: NULL_DATE__ID },
|
||||
])
|
||||
})
|
||||
|
||||
it("fails to find nonexistent row", async () => {
|
||||
await expectQuery({
|
||||
notEqual: { date: `${JAN_30TH}${SEARCH_SUFFIX}` },
|
||||
}).toContainExactly([
|
||||
{ date: JAN_1ST },
|
||||
{ date: JAN_10TH },
|
||||
{ dateid: NULL_DATE__ID },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("oneOf", () => {
|
||||
it("successfully finds a row", async () => {
|
||||
await expectQuery({
|
||||
oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] },
|
||||
}).toContainExactly([{ date: JAN_1ST }])
|
||||
})
|
||||
|
||||
it("fails to find nonexistent row", async () => {
|
||||
await expectQuery({
|
||||
oneOf: {
|
||||
date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`],
|
||||
},
|
||||
}).toFindNothing()
|
||||
})
|
||||
})
|
||||
|
||||
describe("range", () => {
|
||||
it("successfully finds a row", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
date: {
|
||||
low: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||||
high: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||||
},
|
||||
},
|
||||
}).toContainExactly([{ date: JAN_1ST }])
|
||||
})
|
||||
|
||||
it("successfully finds multiple rows", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
date: {
|
||||
low: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||||
high: `${JAN_10TH}${SEARCH_SUFFIX}`,
|
||||
},
|
||||
},
|
||||
}).toContainExactly([
|
||||
{ date: JAN_1ST },
|
||||
{ date: JAN_10TH },
|
||||
])
|
||||
})
|
||||
|
||||
it("successfully finds no rows", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
date: {
|
||||
low: `${JAN_30TH}${SEARCH_SUFFIX}`,
|
||||
high: `${JAN_30TH}${SEARCH_SUFFIX}`,
|
||||
},
|
||||
},
|
||||
}).toFindNothing()
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
!isInMemory &&
|
||||
describe("AI Column", () => {
|
||||
|
|
|
@ -411,6 +411,15 @@ export async function coreOutputProcessing(
|
|||
row[property] = `${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
}
|
||||
} else if (column.type === FieldType.DATETIME && column.dateOnly) {
|
||||
for (const row of rows) {
|
||||
if (typeof row[property] === "string") {
|
||||
row[property] = new Date(row[property])
|
||||
}
|
||||
if (row[property] instanceof Date) {
|
||||
row[property] = row[property].toISOString().slice(0, 10)
|
||||
}
|
||||
}
|
||||
} else if (column.type === FieldType.LINK) {
|
||||
for (let row of rows) {
|
||||
// if relationship is empty - remove the array, this has been part of the API for some time
|
||||
|
|
|
@ -699,7 +699,27 @@ export function runQuery<T extends Record<string, any>>(
|
|||
return docValue._id === testValue
|
||||
}
|
||||
|
||||
return docValue === testValue
|
||||
if (docValue === testValue) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (docValue == null && testValue != null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (docValue != null && testValue == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const leftDate = dayjs(docValue)
|
||||
if (leftDate.isValid()) {
|
||||
const rightDate = dayjs(testValue)
|
||||
if (rightDate.isValid()) {
|
||||
return leftDate.isSame(rightDate)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const not =
|
||||
|
|
|
@ -15,5 +15,5 @@ export interface GetGlobalSelfResponse extends User {
|
|||
license: License
|
||||
budibaseAccess: boolean
|
||||
accountPortalAccess: boolean
|
||||
csrfToken: boolean
|
||||
csrfToken: string
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ export interface UserDetails {
|
|||
password?: string
|
||||
}
|
||||
|
||||
export type UnsavedUser = Omit<User, "tenantId">
|
||||
|
||||
export interface BulkUserRequest {
|
||||
delete?: {
|
||||
users: Array<{
|
||||
|
@ -31,7 +33,7 @@ export interface BulkUserRequest {
|
|||
}
|
||||
create?: {
|
||||
roles?: any[]
|
||||
users: User[]
|
||||
users: UnsavedUser[]
|
||||
groups: any[]
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +126,7 @@ export interface AcceptUserInviteRequest {
|
|||
inviteCode: string
|
||||
password: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
lastName?: string
|
||||
}
|
||||
|
||||
export interface AcceptUserInviteResponse {
|
||||
|
|
|
@ -33,11 +33,6 @@ export interface ScreenRoutesViewOutput extends Document {
|
|||
export type ScreenRoutingJson = Record<
|
||||
string,
|
||||
{
|
||||
subpaths: Record<
|
||||
string,
|
||||
{
|
||||
screens: Record<string, string>
|
||||
}
|
||||
>
|
||||
subpaths: Record<string, any>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
export enum FeatureFlag {
|
||||
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
||||
|
||||
// Account-portal
|
||||
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
|
||||
}
|
||||
|
||||
export const FeatureFlagDefaults = {
|
||||
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
|
||||
|
||||
// Account-portal
|
||||
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,
|
||||
}
|
||||
|
||||
export type FeatureFlags = typeof FeatureFlagDefaults
|
||||
|
|
|
@ -14,6 +14,7 @@ export type UIColumn = FieldSchema & {
|
|||
type: FieldType
|
||||
readonly: boolean
|
||||
autocolumn: boolean
|
||||
cellRenderType?: FieldType | "role"
|
||||
}
|
||||
calculationType: CalculationType
|
||||
__idx: number
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
SaveUserResponse,
|
||||
SearchUsersRequest,
|
||||
SearchUsersResponse,
|
||||
UnsavedUser,
|
||||
UpdateInviteRequest,
|
||||
UpdateInviteResponse,
|
||||
User,
|
||||
|
@ -49,6 +50,7 @@ import {
|
|||
tenancy,
|
||||
db,
|
||||
locks,
|
||||
context,
|
||||
} from "@budibase/backend-core"
|
||||
import { checkAnyUserExists } from "../../../utilities/users"
|
||||
import { isEmailConfigured } from "../../../utilities/email"
|
||||
|
@ -66,10 +68,11 @@ const generatePassword = (length: number) => {
|
|||
.slice(0, length)
|
||||
}
|
||||
|
||||
export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
|
||||
export const save = async (ctx: UserCtx<UnsavedUser, SaveUserResponse>) => {
|
||||
try {
|
||||
const currentUserId = ctx.user?._id
|
||||
const requestUser = ctx.request.body
|
||||
const tenantId = context.getTenantId()
|
||||
const requestUser: User = { ...ctx.request.body, tenantId }
|
||||
|
||||
// Do not allow the account holder role to be changed
|
||||
if (
|
||||
|
@ -151,7 +154,12 @@ export const bulkUpdate = async (
|
|||
let created, deleted
|
||||
try {
|
||||
if (input.create) {
|
||||
created = await bulkCreate(input.create.users, input.create.groups)
|
||||
const tenantId = context.getTenantId()
|
||||
const users: User[] = input.create.users.map(user => ({
|
||||
...user,
|
||||
tenantId,
|
||||
}))
|
||||
created = await bulkCreate(users, input.create.groups)
|
||||
}
|
||||
if (input.delete) {
|
||||
deleted = await bulkDelete(input.delete.users, currentUserId)
|
||||
|
|
Loading…
Reference in New Issue