diff --git a/lerna.json b/lerna.json index 647c9f202d..0dc09b27be 100644 --- a/lerna.json +++ b/lerna.json @@ -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": { diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts index 6ceda9cd3a..5a5a25b461 100644 --- a/packages/backend-core/src/middleware/errorHandling.ts +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -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 diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 5f462ee144..9b0c49d9f8 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -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), diff --git a/packages/backend-core/src/sql/utils.ts b/packages/backend-core/src/sql/utils.ts index 16b352995b..b07854b2a0 100644 --- a/packages/backend-core/src/sql/utils.ts +++ b/packages/backend-core/src/sql/utils.ts @@ -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) { diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index 37abd7f1eb..2260892913 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -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, }) } diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index f94bad2147..bcd59cd948 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -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)} /> diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte index 8d99d406fd..71fd4c0be3 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte @@ -52,7 +52,7 @@ ] const removeUser = async id => { - await groups.actions.removeUser(groupId, id) + await groups.removeUser(groupId, id) fetchGroupUsers.refresh() } diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte index 97120c55d4..c77e40c964 100644 --- a/packages/builder/src/pages/builder/portal/users/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte @@ -251,6 +251,7 @@ passwordModal.show() await fetch.refresh() } catch (error) { + console.error(error) notifications.error("Error creating user") } } diff --git a/packages/builder/src/stores/builder/queries.js b/packages/builder/src/stores/builder/queries.js deleted file mode 100644 index 7aeb9ff8fd..0000000000 --- a/packages/builder/src/stores/builder/queries.js +++ /dev/null @@ -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() diff --git a/packages/builder/src/stores/builder/queries.ts b/packages/builder/src/stores/builder/queries.ts new file mode 100644 index 0000000000..c6511dc346 --- /dev/null +++ b/packages/builder/src/stores/builder/queries.ts @@ -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) => { + 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 { + 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 = {} + 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() diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.ts similarity index 52% rename from packages/builder/src/stores/portal/users.js rename to packages/builder/src/stores/portal/users.ts index 99ead22317..605f8612aa 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.ts @@ -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 { + 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() diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.ts similarity index 94% rename from packages/client/src/api/api.js rename to packages/client/src/api/api.ts index d4c8faa4d2..b944f7bd7c 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.ts @@ -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({ diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index 5eb6b2b6f4..3c53045cfd 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -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 diff --git a/packages/client/src/index.d.ts b/packages/client/src/index.d.ts new file mode 100644 index 0000000000..7e13670b33 --- /dev/null +++ b/packages/client/src/index.d.ts @@ -0,0 +1,5 @@ +interface Window { + "##BUDIBASE_APP_ID##": string + "##BUDIBASE_IN_BUILDER##": string + MIGRATING_APP: boolean +} diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 40d066f2ee..68d75d2806 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -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" diff --git a/packages/client/src/stores/auth.js b/packages/client/src/stores/auth.ts similarity index 94% rename from packages/client/src/stores/auth.js rename to packages/client/src/stores/auth.ts index 214cc7bce2..15f44e7c32 100644 --- a/packages/client/src/stores/auth.js +++ b/packages/client/src/stores/auth.ts @@ -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 () => { diff --git a/packages/client/src/stores/derived/currentRole.js b/packages/client/src/stores/derived/currentRole.js index 8bb4c5a25d..056a05f8ab 100644 --- a/packages/client/src/stores/derived/currentRole.js +++ b/packages/client/src/stores/derived/currentRole.js @@ -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 diff --git a/packages/client/src/stores/notification.js b/packages/client/src/stores/notification.ts similarity index 80% rename from packages/client/src/stores/notification.js rename to packages/client/src/stores/notification.ts index 054117aaba..fa28b9f40a 100644 --- a/packages/client/src/stores/notification.js +++ b/packages/client/src/stores/notification.ts @@ -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, diff --git a/packages/client/src/stores/routes.js b/packages/client/src/stores/routes.ts similarity index 82% rename from packages/client/src/stores/routes.js rename to packages/client/src/stores/routes.ts index 8e318af2e3..3f200a9c88 100644 --- a/packages/client/src/stores/routes.js +++ b/packages/client/src/stores/routes.ts @@ -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 } diff --git a/packages/client/src/utils/schema.js b/packages/client/src/utils/schema.ts similarity index 57% rename from packages/client/src/utils/schema.js rename to packages/client/src/utils/schema.ts index 60800afc53..5400d62087 100644 --- a/packages/client/src/utils/schema.js +++ b/packages/client/src/utils/schema.ts @@ -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 +) => { if (!schema) { return null } - let relationshipAdditions = {} + let relationshipAdditions: Record = {} 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, diff --git a/packages/frontend-core/src/api/index.ts b/packages/frontend-core/src/api/index.ts index 36f4e7ed61..d3209e0a54 100644 --- a/packages/frontend-core/src/api/index.ts +++ b/packages/frontend-core/src/api/index.ts @@ -68,13 +68,13 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => { ): Promise => { // 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 => { diff --git a/packages/frontend-core/src/api/types.ts b/packages/frontend-core/src/api/types.ts index 0db1049591..4819b4cd3b 100644 --- a/packages/frontend-core/src/api/types.ts +++ b/packages/frontend-core/src/api/types.ts @@ -46,7 +46,7 @@ export type Headers = Record 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: ( params: APICallParams ) => Promise - error: (message: string) => void invalidateCache: () => void getAppID: () => string } export type APIError = { message?: string - url?: string + url: string method?: HTTPMethod json: any status: number diff --git a/packages/frontend-core/src/api/user.ts b/packages/frontend-core/src/api/user.ts index 84ec68644d..cf66751078 100644 --- a/packages/frontend-core/src/api/user.ts +++ b/packages/frontend-core/src/api/user.ts @@ -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 - saveUser: (user: User) => Promise + saveUser: (user: UnsavedUser) => Promise deleteUser: (userId: string) => Promise - deleteUsers: ( - users: Array<{ - userId: string - email: string - }> - ) => Promise + deleteUsers: (users: UserIdentifier[]) => Promise onboardUsers: (data: InviteUsersRequest) => Promise getUserInvite: (code: string) => Promise getUserInvites: () => Promise @@ -60,7 +56,7 @@ export interface UserEndpoints { getAccountHolder: () => Promise searchUsers: (data: SearchUsersRequest) => Promise createUsers: ( - users: User[], + users: UnsavedUser[], groups: any[] ) => Promise updateUserInvite: ( diff --git a/packages/frontend-core/src/components/grid/lib/constants.js b/packages/frontend-core/src/components/grid/lib/constants.ts similarity index 100% rename from packages/frontend-core/src/components/grid/lib/constants.js rename to packages/frontend-core/src/components/grid/lib/constants.ts diff --git a/packages/frontend-core/src/components/grid/lib/renderers.js b/packages/frontend-core/src/components/grid/lib/renderers.ts similarity index 82% rename from packages/frontend-core/src/components/grid/lib/renderers.js rename to packages/frontend-core/src/components/grid/lib/renderers.ts index a860d01b53..b009806cc4 100644 --- a/packages/frontend-core/src/components/grid/lib/renderers.js +++ b/packages/frontend-core/src/components/grid/lib/renderers.ts @@ -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 ) } diff --git a/packages/frontend-core/src/components/grid/lib/utils.js b/packages/frontend-core/src/components/grid/lib/utils.js deleted file mode 100644 index ee74a14bf0..0000000000 --- a/packages/frontend-core/src/components/grid/lib/utils.js +++ /dev/null @@ -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) -} diff --git a/packages/frontend-core/src/components/grid/lib/websocket.js b/packages/frontend-core/src/components/grid/lib/websocket.ts similarity index 86% rename from packages/frontend-core/src/components/grid/lib/websocket.js rename to packages/frontend-core/src/components/grid/lib/websocket.ts index e7b89ff58a..bc41d594f4 100644 --- a/packages/frontend-core/src/components/grid/lib/websocket.js +++ b/packages/frontend-core/src/components/grid/lib/websocket.ts @@ -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) } } diff --git a/packages/frontend-core/src/fetch/CustomFetch.ts b/packages/frontend-core/src/fetch/CustomFetch.ts index afd3d18ba9..dfd29c4a02 100644 --- a/packages/frontend-core/src/fetch/CustomFetch.ts +++ b/packages/frontend-core/src/fetch/CustomFetch.ts @@ -1,6 +1,7 @@ import DataFetch from "./DataFetch" interface CustomDatasource { + type: "custom" data: any } diff --git a/packages/frontend-core/src/fetch/DataFetch.ts b/packages/frontend-core/src/fetch/DataFetch.ts index 0079fec057..b10a8b0a69 100644 --- a/packages/frontend-core/src/fetch/DataFetch.ts +++ b/packages/frontend-core/src/fetch/DataFetch.ts @@ -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 | 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 = {} for (const fieldKey of Object.keys(schema)) { diff --git a/packages/frontend-core/src/fetch/FieldFetch.ts b/packages/frontend-core/src/fetch/FieldFetch.ts index ac1e683c51..694443a5dc 100644 --- a/packages/frontend-core/src/fetch/FieldFetch.ts +++ b/packages/frontend-core/src/fetch/FieldFetch.ts @@ -1,7 +1,10 @@ import { Row } from "@budibase/types" import DataFetch from "./DataFetch" -export interface FieldDatasource { +type Types = "field" | "queryarray" | "jsonarray" + +export interface FieldDatasource { + 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 extends DataFetch< + FieldDatasource, FieldDefinition > { async getDefinition(): Promise { diff --git a/packages/frontend-core/src/fetch/GroupUserFetch.ts b/packages/frontend-core/src/fetch/GroupUserFetch.ts index a14623bfb0..e07e5331d4 100644 --- a/packages/frontend-core/src/fetch/GroupUserFetch.ts +++ b/packages/frontend-core/src/fetch/GroupUserFetch.ts @@ -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, }, }) diff --git a/packages/frontend-core/src/fetch/JSONArrayFetch.ts b/packages/frontend-core/src/fetch/JSONArrayFetch.ts index cae9a1e521..d746c923f8 100644 --- a/packages/frontend-core/src/fetch/JSONArrayFetch.ts +++ b/packages/frontend-core/src/fetch/JSONArrayFetch.ts @@ -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 diff --git a/packages/frontend-core/src/fetch/NestedProviderFetch.ts b/packages/frontend-core/src/fetch/NestedProviderFetch.ts index 666340610f..af121fcef8 100644 --- a/packages/frontend-core/src/fetch/NestedProviderFetch.ts +++ b/packages/frontend-core/src/fetch/NestedProviderFetch.ts @@ -2,6 +2,7 @@ import { Row, TableSchema } from "@budibase/types" import DataFetch from "./DataFetch" interface NestedProviderDatasource { + type: "provider" value?: { schema: TableSchema primaryDisplay: string diff --git a/packages/frontend-core/src/fetch/QueryArrayFetch.ts b/packages/frontend-core/src/fetch/QueryArrayFetch.ts index 9142000fe6..7f4d34aaa6 100644 --- a/packages/frontend-core/src/fetch/QueryArrayFetch.ts +++ b/packages/frontend-core/src/fetch/QueryArrayFetch.ts @@ -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 diff --git a/packages/frontend-core/src/fetch/QueryFetch.ts b/packages/frontend-core/src/fetch/QueryFetch.ts index 0754edd267..09dde86cbd 100644 --- a/packages/frontend-core/src/fetch/QueryFetch.ts +++ b/packages/frontend-core/src/fetch/QueryFetch.ts @@ -4,6 +4,7 @@ import { ExecuteQueryRequest, Query } from "@budibase/types" import { get } from "svelte/store" interface QueryDatasource { + type: "query" _id: string fields: Record & { pagination?: { diff --git a/packages/frontend-core/src/fetch/RelationshipFetch.ts b/packages/frontend-core/src/fetch/RelationshipFetch.ts index f853a753cd..89a85ab0e4 100644 --- a/packages/frontend-core/src/fetch/RelationshipFetch.ts +++ b/packages/frontend-core/src/fetch/RelationshipFetch.ts @@ -2,6 +2,7 @@ import { Table } from "@budibase/types" import DataFetch from "./DataFetch" interface RelationshipDatasource { + type: "link" tableId: string rowId: string rowTableId: string diff --git a/packages/frontend-core/src/fetch/TableFetch.ts b/packages/frontend-core/src/fetch/TableFetch.ts index f5927262cb..67cac6b6a7 100644 --- a/packages/frontend-core/src/fetch/TableFetch.ts +++ b/packages/frontend-core/src/fetch/TableFetch.ts @@ -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 { +interface TableDatasource { + type: "table" + tableId: string +} + +export default class TableFetch extends DataFetch { async determineFeatureFlags() { return { supportsSearch: true, diff --git a/packages/frontend-core/src/fetch/UserFetch.ts b/packages/frontend-core/src/fetch/UserFetch.ts index 656cd840fe..36aebac506 100644 --- a/packages/frontend-core/src/fetch/UserFetch.ts +++ b/packages/frontend-core/src/fetch/UserFetch.ts @@ -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) { 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 = { diff --git a/packages/frontend-core/src/fetch/ViewFetch.ts b/packages/frontend-core/src/fetch/ViewFetch.ts index b6830e7118..df00b9bbfc 100644 --- a/packages/frontend-core/src/fetch/ViewFetch.ts +++ b/packages/frontend-core/src/fetch/ViewFetch.ts @@ -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 { +export default class ViewFetch extends DataFetch { async getDefinition() { const { datasource } = this.options diff --git a/packages/frontend-core/src/fetch/ViewV2Fetch.ts b/packages/frontend-core/src/fetch/ViewV2Fetch.ts index cdd3bab6ed..aa5fbd60a2 100644 --- a/packages/frontend-core/src/fetch/ViewV2Fetch.ts +++ b/packages/frontend-core/src/fetch/ViewV2Fetch.ts @@ -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 { +interface ViewDatasource { + type: "viewV2" + id: string +} + +export default class ViewV2Fetch extends DataFetch< + ViewDatasource, + ViewV2Enriched +> { async determineFeatureFlags() { return { supportsSearch: true, diff --git a/packages/frontend-core/src/fetch/index.ts b/packages/frontend-core/src/fetch/index.ts index c1f35abef2..d80aa10df6 100644 --- a/packages/frontend-core/src/fetch/index.ts +++ b/packages/frontend-core/src/fetch/index.ts @@ -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 = ({ 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, diff --git a/packages/frontend-core/src/index.ts b/packages/frontend-core/src/index.ts index 37951dc776..c0baa63ab6 100644 --- a/packages/frontend-core/src/index.ts +++ b/packages/frontend-core/src/index.ts @@ -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" diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index c424aea5b2..55603b0129 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -209,6 +209,9 @@ export const buildFormBlockButtonConfig = props => { { "##eventHandlerType": "Close Side Panel", }, + { + "##eventHandlerType": "Close Modal", + }, ...(actionUrl ? [ diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 968ce9c798..e5cd54e5a5 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -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 diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index e94e567b43..4de92f21e5 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -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", () => { diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 14b524fd95..8595a3483e 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -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 diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index b711d4cb61..afe99d9565 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -699,7 +699,27 @@ export function runQuery>( 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 = diff --git a/packages/types/src/api/web/global/self.ts b/packages/types/src/api/web/global/self.ts index 517559d1ca..9d99a1f1a5 100644 --- a/packages/types/src/api/web/global/self.ts +++ b/packages/types/src/api/web/global/self.ts @@ -15,5 +15,5 @@ export interface GetGlobalSelfResponse extends User { license: License budibaseAccess: boolean accountPortalAccess: boolean - csrfToken: boolean + csrfToken: string } diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index a42449d550..c1f37fd3f0 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -22,6 +22,8 @@ export interface UserDetails { password?: string } +export type UnsavedUser = Omit + 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 { diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts index b2cedf31a9..a8c32118d3 100644 --- a/packages/types/src/documents/app/screen.ts +++ b/packages/types/src/documents/app/screen.ts @@ -33,11 +33,6 @@ export interface ScreenRoutesViewOutput extends Document { export type ScreenRoutingJson = Record< string, { - subpaths: Record< - string, - { - screens: Record - } - > + subpaths: Record } > diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index 7b61b70772..996d3bba8d 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -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 diff --git a/packages/types/src/ui/stores/grid/columns.ts b/packages/types/src/ui/stores/grid/columns.ts index 7f20145246..2517d2a3e0 100644 --- a/packages/types/src/ui/stores/grid/columns.ts +++ b/packages/types/src/ui/stores/grid/columns.ts @@ -14,6 +14,7 @@ export type UIColumn = FieldSchema & { type: FieldType readonly: boolean autocolumn: boolean + cellRenderType?: FieldType | "role" } calculationType: CalculationType __idx: number diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 83f2f41b0e..0bcdadfefc 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -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) => { +export const save = async (ctx: UserCtx) => { 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)