From 52916f11a8129cd8dec61dcd73fc30c2f894f741 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Jan 2025 14:06:03 +0000 Subject: [PATCH 01/49] Convert portal user store to TS --- .../_components/BuilderSidePanel.svelte | 7 +- .../src/stores/portal/{users.js => users.ts} | 144 ++++++++++-------- packages/frontend-core/src/api/user.ts | 2 +- packages/types/src/api/web/user.ts | 2 +- 4 files changed, 85 insertions(+), 70 deletions(-) rename packages/builder/src/stores/portal/{users.js => users.ts} (55%) 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/stores/portal/users.js b/packages/builder/src/stores/portal/users.ts similarity index 55% rename from packages/builder/src/stores/portal/users.js rename to packages/builder/src/stores/portal/users.ts index 99ead22317..7c0bec296e 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.ts @@ -1,41 +1,68 @@ -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, +} from "@budibase/types" +import { BudiStore } from "../BudiStore" -export function createUsersStore() { - const { subscribe, set } = writable({}) +type UserState = SearchUsersResponse & SearchUsersRequest - // opts can contain page and search params - async function search(opts = {}) { +class UserStore extends BudiStore { + constructor() { + super({ + data: [], + }) + + // Update quotas after any add or remove operation + this.create = this.refreshUsage(this.create) + this.save = this.refreshUsage(this.save) + this.delete = this.refreshUsage(this.delete) + this.bulkDelete = this.refreshUsage(this.bulkDelete) + } + + 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 +82,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 +100,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: any) { + let mappedUsers: Omit[] = data.users.map((user: any) => { + const body: Omit = { email: user.email, password: user.password, roles: {}, @@ -113,41 +149,44 @@ export function createUsersStore() { const response = await API.createUsers(mappedUsers, data.groups) // 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)) } - async function getUserCountByApp(appId) { - return await API.getUserCountByApp(appId) - } - - async function bulkDelete(users) { + async bulkDelete( + users: Array<{ + userId: string + email: string + }> + ) { return API.deleteUsers(users) } - async function save(user) { + async save(user: 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 @@ -160,37 +199,16 @@ export function createUsersStore() { } } - const refreshUsage = - fn => - async (...args) => { + foo = this.refreshUsage(this.create) + bar = this.refreshUsage(this.save) + + refreshUsage(fn: (...args: T) => Promise) { + return async function (...args: T) { 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/frontend-core/src/api/user.ts b/packages/frontend-core/src/api/user.ts index 84ec68644d..7464b1ec4a 100644 --- a/packages/frontend-core/src/api/user.ts +++ b/packages/frontend-core/src/api/user.ts @@ -60,7 +60,7 @@ export interface UserEndpoints { getAccountHolder: () => Promise searchUsers: (data: SearchUsersRequest) => Promise createUsers: ( - users: User[], + users: Omit[], groups: any[] ) => Promise updateUserInvite: ( diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index a42449d550..8b0dfef34b 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -124,7 +124,7 @@ export interface AcceptUserInviteRequest { inviteCode: string password: string firstName: string - lastName: string + lastName?: string } export interface AcceptUserInviteResponse { From 6bd4cb47c204d1c2626c0d8d93c71b2b695679a4 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 8 Jan 2025 15:54:09 +0000 Subject: [PATCH 02/49] Add new UnsavedUser type and update controllers --- packages/builder/src/stores/portal/users.ts | 35 ++++++++++--------- packages/frontend-core/src/api/user.ts | 14 +++----- packages/types/src/api/web/user.ts | 4 ++- .../src/api/controllers/global/users.ts | 16 ++++++--- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/builder/src/stores/portal/users.ts b/packages/builder/src/stores/portal/users.ts index 7c0bec296e..9284ad2992 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -9,9 +9,18 @@ import { SearchUsersResponse, UpdateInviteRequest, User, + UserIdentifier, + UnsavedUser, } from "@budibase/types" import { BudiStore } from "../BudiStore" +interface UserInfo { + email: string + password: string + forceResetPassword?: boolean + role: keyof typeof Constants.BudibaseRoles +} + type UserState = SearchUsersResponse & SearchUsersRequest class UserStore extends BudiStore { @@ -116,9 +125,9 @@ class UserStore extends BudiStore { return await API.getUserCountByApp(appId) } - async create(data: any) { - let mappedUsers: Omit[] = data.users.map((user: any) => { - const body: Omit = { + 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: {}, @@ -128,17 +137,17 @@ class UserStore extends BudiStore { } 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 @@ -157,12 +166,7 @@ class UserStore extends BudiStore { await API.deleteUser(id) } - async bulkDelete( - users: Array<{ - userId: string - email: string - }> - ) { + async bulkDelete(users: UserIdentifier[]) { return API.deleteUsers(users) } @@ -199,9 +203,8 @@ class UserStore extends BudiStore { } } - foo = this.refreshUsage(this.create) - bar = this.refreshUsage(this.save) - + // Wrapper function to refresh quota usage after an operation, + // persisting argument and return types refreshUsage(fn: (...args: T) => Promise) { return async function (...args: T) { const response = await fn(...args) diff --git a/packages/frontend-core/src/api/user.ts b/packages/frontend-core/src/api/user.ts index 7464b1ec4a..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: Omit[], + users: UnsavedUser[], groups: any[] ) => Promise updateUserInvite: ( diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 8b0dfef34b..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[] } } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index a028f4fd33..4f9135e79e 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 const accountMetadata = await users.getExistingAccounts([requestUser.email]) @@ -149,7 +152,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) @@ -441,7 +449,6 @@ export const checkInvite = async (ctx: UserCtx) => { } catch (e) { console.warn("Error getting invite from code", e) ctx.throw(400, "There was a problem with the invite") - return } ctx.body = { email: invite.email, @@ -472,7 +479,6 @@ export const updateInvite = async ( invite = await cache.invite.getCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") - return } let updated = { From 45ee1570370c9e41fe503a12401ac5664081a832 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 11:24:37 +0100 Subject: [PATCH 03/49] Rename file --- packages/client/src/stores/index.js | 2 +- packages/client/src/stores/{notification.js => notification.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/client/src/stores/{notification.js => notification.ts} (100%) diff --git a/packages/client/src/stores/index.js b/packages/client/src/stores/index.js index f2b80ed732..e099434b3d 100644 --- a/packages/client/src/stores/index.js +++ b/packages/client/src/stores/index.js @@ -1,6 +1,6 @@ export { authStore } from "./auth" export { appStore } from "./app" -export { notificationStore } from "./notification" +export { notificationStore } from "./notification.ts" export { routeStore } from "./routes" export { screenStore } from "./screens" export { builderStore } from "./builder" diff --git a/packages/client/src/stores/notification.js b/packages/client/src/stores/notification.ts similarity index 100% rename from packages/client/src/stores/notification.js rename to packages/client/src/stores/notification.ts From 96cee21792413d638191e4a6872bf5e388d1786a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 11:32:10 +0100 Subject: [PATCH 04/49] Type store --- packages/client/src/stores/notification.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/client/src/stores/notification.ts b/packages/client/src/stores/notification.ts index 054117aaba..6f5fcc9ef4 100644 --- a/packages/client/src/stores/notification.ts +++ 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,17 +14,18 @@ const createNotificationStore = () => { } const send = ( - message, + message: string, type = "info", - icon, + icon: string, autoDismiss = true, - duration, + duration: number, count = 1 ) => { if (block) { return } + // @ts-expect-error if (get(routeStore).queryParams?.peek) { window.parent.postMessage({ type: "notification", @@ -66,7 +67,7 @@ const createNotificationStore = () => { } } - const dismiss = id => { + const dismiss = (id: string) => { store.update(state => { return state.filter(n => n.id !== id) }) @@ -76,13 +77,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, From f938eb3297e5dd3de249d41c36ef7c0565f1996a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 11:34:15 +0100 Subject: [PATCH 05/49] api to ts --- packages/client/src/api/{api.js => api.ts} | 6 +++++- packages/client/src/api/index.js | 2 +- packages/client/src/stores/notification.ts | 10 +++++----- 3 files changed, 11 insertions(+), 7 deletions(-) rename packages/client/src/api/{api.js => api.ts} (96%) diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.ts similarity index 96% rename from packages/client/src/api/api.js rename to packages/client/src/api/api.ts index d4c8faa4d2..59430e4ebd 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 { + notificationStore, + devToolsEnabled, + devToolsStore, +} from "../stores/index.js" 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..a63e19bfbb 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -1,4 +1,4 @@ -import { API } from "./api.js" +import { API } from "./api.ts" import { patchAPI } from "./patches.js" // Certain endpoints which return rows need patched so that they transform diff --git a/packages/client/src/stores/notification.ts b/packages/client/src/stores/notification.ts index 6f5fcc9ef4..5e1a4774c1 100644 --- a/packages/client/src/stores/notification.ts +++ b/packages/client/src/stores/notification.ts @@ -18,7 +18,7 @@ const createNotificationStore = () => { type = "info", icon: string, autoDismiss = true, - duration: number, + duration?: number, count = 1 ) => { if (block) { @@ -77,13 +77,13 @@ const createNotificationStore = () => { subscribe: store.subscribe, actions: { send, - info: (msg: string, autoDismiss: boolean, duration: number) => + info: (msg: string, autoDismiss?: boolean, duration?: number) => send(msg, "info", "Info", autoDismiss ?? true, duration), - success: (msg: string, autoDismiss: boolean, duration: number) => + success: (msg: string, autoDismiss?: boolean, duration?: number) => send(msg, "success", "CheckmarkCircle", autoDismiss ?? true, duration), - warning: (msg: string, autoDismiss: boolean, duration: number) => + warning: (msg: string, autoDismiss?: boolean, duration?: number) => send(msg, "warning", "Alert", autoDismiss ?? true, duration), - error: (msg: string, autoDismiss: boolean, duration: number) => + error: (msg: string, autoDismiss?: boolean, duration?: number) => send(msg, "error", "Alert", autoDismiss ?? false, duration), blockNotifications, dismiss, From a0950f15d9bb4d77dbdcc98d8f96fa93b78ef085 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 11:38:16 +0100 Subject: [PATCH 06/49] Add window typings --- packages/client/src/index.d.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/client/src/index.d.ts 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 +} From 60c23ae021614dee53eb62a08e0ae9c4da974ff9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 11:42:35 +0100 Subject: [PATCH 07/49] Type auth store --- packages/client/src/stores/{auth.js => auth.ts} | 4 +++- packages/types/src/api/web/global/self.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) rename packages/client/src/stores/{auth.js => auth.ts} (94%) 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/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 } From 6e816fb6a2ae3aa8ec13f8d5fc887bc88b3c25fe Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 11:51:27 +0100 Subject: [PATCH 08/49] Fixes --- packages/client/src/api/api.ts | 2 +- packages/client/src/stores/derived/currentRole.js | 2 +- packages/client/src/stores/notification.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/client/src/api/api.ts b/packages/client/src/api/api.ts index 59430e4ebd..93f59f6e9a 100644 --- a/packages/client/src/api/api.ts +++ b/packages/client/src/api/api.ts @@ -1,5 +1,5 @@ import { createAPIClient } from "@budibase/frontend-core" -import { authStore } from "../stores/auth.js" +import { authStore } from "../stores/auth" import { notificationStore, devToolsEnabled, 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.ts b/packages/client/src/stores/notification.ts index 5e1a4774c1..fa28b9f40a 100644 --- a/packages/client/src/stores/notification.ts +++ b/packages/client/src/stores/notification.ts @@ -25,7 +25,6 @@ const createNotificationStore = () => { return } - // @ts-expect-error if (get(routeStore).queryParams?.peek) { window.parent.postMessage({ type: "notification", From 1b6dc51f018c80938c8f45aaf2852734f3981076 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 11:51:38 +0100 Subject: [PATCH 09/49] Type routes --- .../src/stores/{routes.js => routes.ts} | 30 ++++++++++++++----- packages/types/src/documents/app/screen.ts | 1 + 2 files changed, 24 insertions(+), 7 deletions(-) rename packages/client/src/stores/{routes.js => routes.ts} (82%) 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/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts index b2cedf31a9..e85fd5953b 100644 --- a/packages/types/src/documents/app/screen.ts +++ b/packages/types/src/documents/app/screen.ts @@ -36,6 +36,7 @@ export type ScreenRoutingJson = Record< subpaths: Record< string, { + screenId: string screens: Record } > From 59bdd201585b90c1e87e6cfaa7b82becf2a3ef01 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 12:12:43 +0100 Subject: [PATCH 10/49] Type onError --- packages/frontend-core/src/api/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/api/types.ts b/packages/frontend-core/src/api/types.ts index 0db1049591..940927f499 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 } From e6c45d934b70ae496a8827b5f17d75cc5c8af5b8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 12:39:44 +0100 Subject: [PATCH 11/49] Stringify api errors --- packages/frontend-core/src/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/api/index.ts b/packages/frontend-core/src/api/index.ts index f7b05c338a..44e85e5a68 100644 --- a/packages/frontend-core/src/api/index.ts +++ b/packages/frontend-core/src/api/index.ts @@ -74,7 +74,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => { if (json?.message) { message = json.message } else if (json?.error) { - message = json.error + message = JSON.stringify(json.error) } } catch (error) { // Do nothing From 2cbf7a47c16194741d49dc654d1d0ea059c3d54d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 12:40:23 +0100 Subject: [PATCH 12/49] Type --- packages/frontend-core/src/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/api/index.ts b/packages/frontend-core/src/api/index.ts index 44e85e5a68..b0e1970e6f 100644 --- a/packages/frontend-core/src/api/index.ts +++ b/packages/frontend-core/src/api/index.ts @@ -226,7 +226,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 } From 2b90d972b21325608f0f1e4f51d0d1a850be2e26 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 12:50:19 +0100 Subject: [PATCH 13/49] Clean code --- packages/frontend-core/src/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/api/index.ts b/packages/frontend-core/src/api/index.ts index b0e1970e6f..92dc2d9ea3 100644 --- a/packages/frontend-core/src/api/index.ts +++ b/packages/frontend-core/src/api/index.ts @@ -68,7 +68,7 @@ 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) { From 71c6480c1b3ea08cd69bc8a0cfe015aadbaa773b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 13:00:02 +0100 Subject: [PATCH 14/49] Remove unused function --- packages/frontend-core/src/api/index.ts | 6 +----- packages/frontend-core/src/api/types.ts | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/frontend-core/src/api/index.ts b/packages/frontend-core/src/api/index.ts index 92dc2d9ea3..6efc90023a 100644 --- a/packages/frontend-core/src/api/index.ts +++ b/packages/frontend-core/src/api/index.ts @@ -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 { @@ -239,13 +239,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 940927f499..4819b4cd3b 100644 --- a/packages/frontend-core/src/api/types.ts +++ b/packages/frontend-core/src/api/types.ts @@ -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 From 70a8b9e4688249211f5e9d56463e3524a97b2539 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 13:55:00 +0100 Subject: [PATCH 15/49] Remove screen id --- packages/types/src/documents/app/screen.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts index e85fd5953b..b2cedf31a9 100644 --- a/packages/types/src/documents/app/screen.ts +++ b/packages/types/src/documents/app/screen.ts @@ -36,7 +36,6 @@ export type ScreenRoutingJson = Record< subpaths: Record< string, { - screenId: string screens: Record } > From 88e245a5bb7a97eedfc07ea6104e44c5d2dfbd2a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 14:01:07 +0100 Subject: [PATCH 16/49] Fix typing --- packages/types/src/documents/app/screen.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 } > From 26c0861e6899b196eed64e0396080a6a6e905d3f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Jan 2025 17:05:44 +0000 Subject: [PATCH 17/49] Fix date-only searching. --- .../src/middleware/errorHandling.ts | 6 +- packages/backend-core/src/sql/sql.ts | 54 ++++++++- packages/backend-core/src/sql/utils.ts | 12 +- .../src/api/routes/tests/search.spec.ts | 106 ++++++++++++++++++ .../src/utilities/rowProcessor/index.ts | 9 ++ packages/shared-core/src/filters.ts | 22 +++- 6 files changed, 196 insertions(+), 13 deletions(-) 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/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index e94e567b43..dca1bf9394 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1683,6 +1683,112 @@ if (descriptions.length) { }) }) + describe.only("datetime - date only", () => { + 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 }, + { date: `${JAN_10TH}T00:00:00.000Z` }, + ]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { date: JAN_1ST }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("successfully finds an ISO8601 row", async () => { + await expectQuery({ + equal: { date: JAN_10TH }, + }).toContainExactly([{ date: JAN_10TH }]) + }) + + it("finds a row with ISO8601 timestamp", async () => { + await expectQuery({ + equal: { date: `${JAN_1ST}T00:00:00.000Z` }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { date: UNEXISTING_DATE }, + }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { date: JAN_1ST }, + }).toContainExactly([ + { date: JAN_10TH }, + { dateid: NULL_DATE__ID }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notEqual: { date: JAN_30TH }, + }).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] }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { date: [UNEXISTING_DATE] }, + }).toFindNothing() + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { date: { low: JAN_1ST, high: JAN_1ST } }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { date: { low: JAN_1ST, high: JAN_10TH } }, + }).toContainExactly([{ date: JAN_1ST }, { date: JAN_10TH }]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { date: { low: JAN_30TH, high: JAN_30TH } }, + }).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 = From 7ad5879c3eb19783bd5a9d4beb36b835fe02fc3e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Jan 2025 17:16:47 +0000 Subject: [PATCH 18/49] Create search cross product on whether we save/search with timestamp. --- .../src/api/routes/tests/search.spec.ts | 219 +++++++++++------- 1 file changed, 129 insertions(+), 90 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index dca1bf9394..69c68aad97 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1684,109 +1684,148 @@ if (descriptions.length) { }) describe.only("datetime - date only", () => { - 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` + 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" + : "" - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - dateid: { name: "dateid", type: FieldType.STRING }, - date: { - name: "date", - type: FieldType.DATETIME, - dateOnly: true, - }, - }) + 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` - await createRows([ - { dateid: NULL_DATE__ID, date: null }, - { date: JAN_1ST }, - { date: `${JAN_10TH}T00:00:00.000Z` }, - ]) - }) + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + dateid: { name: "dateid", type: FieldType.STRING }, + date: { + name: "date", + type: FieldType.DATETIME, + dateOnly: true, + }, + }) - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ - equal: { date: JAN_1ST }, - }).toContainExactly([{ date: JAN_1ST }]) - }) + await createRows([ + { dateid: NULL_DATE__ID, date: null }, + { date: `${JAN_1ST}${SAVE_SUFFIX}` }, + { date: `${JAN_10TH}${SAVE_SUFFIX}` }, + ]) + }) - it("successfully finds an ISO8601 row", async () => { - await expectQuery({ - equal: { date: JAN_10TH }, - }).toContainExactly([{ date: JAN_10TH }]) - }) + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_1ST }]) + }) - it("finds a row with ISO8601 timestamp", async () => { - await expectQuery({ - equal: { date: `${JAN_1ST}T00:00:00.000Z` }, - }).toContainExactly([{ date: JAN_1ST }]) - }) + it("successfully finds an ISO8601 row", async () => { + await expectQuery({ + equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_10TH }]) + }) - it("fails to find nonexistent row", async () => { - await expectQuery({ - equal: { date: UNEXISTING_DATE }, - }).toFindNothing() - }) - }) + it("finds a row with ISO8601 timestamp", async () => { + await expectQuery({ + equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_1ST }]) + }) - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ - notEqual: { date: JAN_1ST }, - }).toContainExactly([ - { date: JAN_10TH }, - { dateid: NULL_DATE__ID }, - ]) - }) + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { + date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`, + }, + }).toFindNothing() + }) + }) - it("fails to find nonexistent row", async () => { - await expectQuery({ - notEqual: { date: JAN_30TH }, - }).toContainExactly([ - { date: JAN_1ST }, - { date: JAN_10TH }, - { dateid: NULL_DATE__ID }, - ]) - }) - }) + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + }).toContainExactly([ + { date: JAN_10TH }, + { dateid: NULL_DATE__ID }, + ]) + }) - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ - oneOf: { date: [JAN_1ST] }, - }).toContainExactly([{ date: JAN_1ST }]) - }) + 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 }, + ]) + }) + }) - it("fails to find nonexistent row", async () => { - await expectQuery({ - oneOf: { date: [UNEXISTING_DATE] }, - }).toFindNothing() - }) - }) + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] }, + }).toContainExactly([{ date: JAN_1ST }]) + }) - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { date: { low: JAN_1ST, high: JAN_1ST } }, - }).toContainExactly([{ date: JAN_1ST }]) - }) + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { + date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`], + }, + }).toFindNothing() + }) + }) - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { date: { low: JAN_1ST, high: JAN_10TH } }, - }).toContainExactly([{ date: JAN_1ST }, { date: JAN_10TH }]) - }) + 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 no rows", async () => { - await expectQuery({ - range: { date: { low: JAN_30TH, high: JAN_30TH } }, - }).toFindNothing() - }) - }) + 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 && From b9a5a71fd8e8be5282f459280c140effa812139b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Jan 2025 17:17:24 +0000 Subject: [PATCH 19/49] Remove focus test. --- packages/server/src/api/routes/tests/search.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 69c68aad97..4de92f21e5 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1683,7 +1683,7 @@ if (descriptions.length) { }) }) - describe.only("datetime - date only", () => { + describe("datetime - date only", () => { describe.each([true, false])( "saved with timestamp: %s", saveWithTimestamp => { From d15a5f330c4d8bb3c9b2dc9269b21554cf8145b6 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Jan 2025 17:25:31 +0000 Subject: [PATCH 20/49] Fix row.spec.ts --- packages/server/src/api/routes/tests/row.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From aadc4da0b124cbdeaae04f85a008f41160dcb67e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 10:15:34 +0100 Subject: [PATCH 21/49] Remove duplicated utils --- .../src/components/grid/lib/utils.js | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 packages/frontend-core/src/components/grid/lib/utils.js 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) -} From 96052679cb5bbdda0b082ee6c471b6ce7cbaff06 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 10:26:24 +0100 Subject: [PATCH 22/49] Convert constants.js --- .../src/components/grid/lib/{constants.js => constants.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/frontend-core/src/components/grid/lib/{constants.js => constants.ts} (100%) 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 From bb1811558570b1f29d3bd064640c6249df818e81 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 10:26:56 +0100 Subject: [PATCH 23/49] Convert renderer --- .../grid/lib/{renderers.js => renderers.ts} | 18 ++++++++++++++---- packages/types/src/ui/stores/grid/columns.ts | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) rename packages/frontend-core/src/components/grid/lib/{renderers.js => renderers.ts} (82%) 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/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 From e23be5acb8ccb0d0425b39d14180817355bc7e6c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 10:30:19 +0100 Subject: [PATCH 24/49] Websocket to ts --- .../components/grid/lib/{websocket.js => websocket.ts} | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) rename packages/frontend-core/src/components/grid/lib/{websocket.js => websocket.ts} (89%) diff --git a/packages/frontend-core/src/components/grid/lib/websocket.js b/packages/frontend-core/src/components/grid/lib/websocket.ts similarity index 89% rename from packages/frontend-core/src/components/grid/lib/websocket.js rename to packages/frontend-core/src/components/grid/lib/websocket.ts index e7b89ff58a..437b93270d 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 } 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 } @@ -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) } } From 535f2d68031efcb84f51493644e2d6c2fa8d7de7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 10:45:35 +0100 Subject: [PATCH 25/49] Type --- packages/frontend-core/src/components/grid/lib/websocket.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/lib/websocket.ts b/packages/frontend-core/src/components/grid/lib/websocket.ts index 437b93270d..bc41d594f4 100644 --- a/packages/frontend-core/src/components/grid/lib/websocket.ts +++ b/packages/frontend-core/src/components/grid/lib/websocket.ts @@ -2,7 +2,7 @@ import { get } from "svelte/store" import { createWebsocket } from "../../../utils" import { SocketEvent, GridSocketEvent } from "@budibase/shared-core" import { Store } from "../stores" -import { UIDatasource } from "@budibase/types" +import { UIDatasource, UIUser } from "@budibase/types" export const createGridWebsocket = (context: Store) => { const { rows, datasource, users, focusedCellId, definition, API } = context @@ -20,7 +20,7 @@ export const createGridWebsocket = (context: Store) => { datasource, appId, }, - ({ users: gridUsers }) => { + ({ users: gridUsers }: { users: UIUser[] }) => { users.set(gridUsers) } ) From 7d9debd3191668da83dac17c0f0b551a17cea0b5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 14:49:07 +0100 Subject: [PATCH 26/49] Export DataFetchMap --- packages/frontend-core/src/fetch/index.ts | 14 +++++++------- packages/frontend-core/src/index.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/frontend-core/src/fetch/index.ts b/packages/frontend-core/src/fetch/index.ts index c1f35abef2..e343b351bc 100644 --- a/packages/frontend-core/src/fetch/index.ts +++ b/packages/frontend-core/src/fetch/index.ts @@ -1,18 +1,18 @@ -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 const DataFetchMap = { table: TableFetch, view: ViewFetch, viewV2: ViewV2Fetch, diff --git a/packages/frontend-core/src/index.ts b/packages/frontend-core/src/index.ts index 37951dc776..5b24a9ac50 100644 --- a/packages/frontend-core/src/index.ts +++ b/packages/frontend-core/src/index.ts @@ -1,5 +1,5 @@ export { createAPIClient } from "./api" -export { fetchData } from "./fetch" +export { fetchData, DataFetchMap } from "./fetch" export * as Constants from "./constants" export * from "./stores" export * from "./utils" From 7df69f154a594a5250b511ef62de29a2063870ad Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 14:50:21 +0100 Subject: [PATCH 27/49] Basic ts conversion --- packages/client/src/sdk.js | 2 +- .../client/src/utils/{schema.js => schema.ts} | 24 +++---------------- 2 files changed, 4 insertions(+), 22 deletions(-) rename packages/client/src/utils/{schema.js => schema.ts} (73%) diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 40d066f2ee..40f35b2ba7 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.ts" 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/utils/schema.js b/packages/client/src/utils/schema.ts similarity index 73% rename from packages/client/src/utils/schema.js rename to packages/client/src/utils/schema.ts index 60800afc53..53b31b5495 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 } from "@budibase/frontend-core" /** * Constructs a fetch instance for a given datasource. @@ -16,18 +8,8 @@ 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 = (datasource: { type: string }) => { + const handler = DataFetchMap[datasource?.type] if (!handler) { return null } From e25f26d28d9d41bb2e46f7e0823e87b19c3c3986 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 15:04:56 +0100 Subject: [PATCH 28/49] Reuse type --- packages/frontend-core/src/fetch/index.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/frontend-core/src/fetch/index.ts b/packages/frontend-core/src/fetch/index.ts index e343b351bc..21832e3ede 100644 --- a/packages/frontend-core/src/fetch/index.ts +++ b/packages/frontend-core/src/fetch/index.ts @@ -12,6 +12,8 @@ import CustomFetch from "./CustomFetch" import QueryArrayFetch from "./QueryArrayFetch" import { APIClient } from "../api/types" +export type DataFetchType = keyof typeof DataFetchMap + export const DataFetchMap = { table: TableFetch, view: ViewFetch, @@ -31,8 +33,7 @@ export const DataFetchMap = { // 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,18 +44,14 @@ 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 } @@ -63,9 +60,7 @@ const createEmptyFetchInstance = < // 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 +74,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, From 4f06592685522e7d1d58af1a43e7549b24e9002a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 15:23:20 +0100 Subject: [PATCH 29/49] Use trimmed and typed datasources --- packages/frontend-core/src/fetch/DataFetch.ts | 3 ++- packages/frontend-core/src/fetch/FieldFetch.ts | 9 ++++++--- packages/frontend-core/src/fetch/GroupUserFetch.ts | 2 ++ packages/frontend-core/src/fetch/JSONArrayFetch.ts | 2 +- .../frontend-core/src/fetch/NestedProviderFetch.ts | 1 + packages/frontend-core/src/fetch/QueryArrayFetch.ts | 2 +- packages/frontend-core/src/fetch/QueryFetch.ts | 1 + .../frontend-core/src/fetch/RelationshipFetch.ts | 1 + packages/frontend-core/src/fetch/TableFetch.ts | 9 +++++++-- packages/frontend-core/src/fetch/UserFetch.ts | 8 ++++++-- packages/frontend-core/src/fetch/ViewFetch.ts | 11 +++++++++-- packages/frontend-core/src/fetch/ViewV2Fetch.ts | 12 ++++++++++-- packages/frontend-core/src/fetch/index.ts | 8 ++++++-- packages/frontend-core/src/index.ts | 1 + 14 files changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/frontend-core/src/fetch/DataFetch.ts b/packages/frontend-core/src/fetch/DataFetch.ts index 0079fec057..8f475339b4 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 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..54147fbccf 100644 --- a/packages/frontend-core/src/fetch/UserFetch.ts +++ b/packages/frontend-core/src/fetch/UserFetch.ts @@ -14,18 +14,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, }, }) diff --git a/packages/frontend-core/src/fetch/ViewFetch.ts b/packages/frontend-core/src/fetch/ViewFetch.ts index b6830e7118..c075d80ce0 100644 --- a/packages/frontend-core/src/fetch/ViewFetch.ts +++ b/packages/frontend-core/src/fetch/ViewFetch.ts @@ -1,7 +1,14 @@ -import { Table, View } from "@budibase/types" +import { Table } from "@budibase/types" import DataFetch from "./DataFetch" -type ViewV1 = View & { name: string } +type ViewV1 = { + type: "view" + name: string + tableId: string + calculation: string + field: string + groupBy: string +} export default class ViewFetch extends DataFetch { async getDefinition() { 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 21832e3ede..d80aa10df6 100644 --- a/packages/frontend-core/src/fetch/index.ts +++ b/packages/frontend-core/src/fetch/index.ts @@ -26,7 +26,7 @@ export const DataFetchMap = { // Client specific datasource types provider: NestedProviderFetch, - field: FieldFetch, + field: FieldFetch<"field">, jsonarray: JSONArrayFetch, queryarray: QueryArrayFetch, } @@ -55,7 +55,11 @@ const createEmptyFetchInstance = ({ 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 diff --git a/packages/frontend-core/src/index.ts b/packages/frontend-core/src/index.ts index 5b24a9ac50..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, DataFetchMap } from "./fetch" +export type { DataFetchType } from "./fetch" export * as Constants from "./constants" export * from "./stores" export * from "./utils" From eb73370460f9b3fb98303f086bf21e29a1ae9bc7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Jan 2025 15:35:16 +0100 Subject: [PATCH 30/49] Fix types --- packages/client/src/utils/schema.ts | 56 +++++++++++++------ packages/frontend-core/src/fetch/DataFetch.ts | 2 +- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/client/src/utils/schema.ts b/packages/client/src/utils/schema.ts index 53b31b5495..5400d62087 100644 --- a/packages/client/src/utils/schema.ts +++ b/packages/client/src/utils/schema.ts @@ -1,5 +1,5 @@ import { API } from "api" -import { DataFetchMap } from "@budibase/frontend-core" +import { DataFetchMap, DataFetchType } from "@budibase/frontend-core" /** * Constructs a fetch instance for a given datasource. @@ -8,12 +8,20 @@ import { DataFetchMap } from "@budibase/frontend-core" * @param datasource the datasource * @returns */ -const getDatasourceFetchInstance = (datasource: { type: string }) => { +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, + }) } /** @@ -21,21 +29,23 @@ const getDatasourceFetchInstance = (datasource: { type: string }) => { * @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" } @@ -55,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, @@ -71,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") { @@ -92,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/fetch/DataFetch.ts b/packages/frontend-core/src/fetch/DataFetch.ts index 8f475339b4..b10a8b0a69 100644 --- a/packages/frontend-core/src/fetch/DataFetch.ts +++ b/packages/frontend-core/src/fetch/DataFetch.ts @@ -369,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)) { From be41a2ae6c2c087deea46c2d8b1861c7448afec2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 10:07:45 +0100 Subject: [PATCH 31/49] Add type to customDatasource --- packages/frontend-core/src/fetch/CustomFetch.ts | 1 + 1 file changed, 1 insertion(+) 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 } From 8520ad2aeb9cc0424301950cebd0bc8a597bb2e4 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Fri, 10 Jan 2025 11:31:14 +0000 Subject: [PATCH 32/49] convert queries store to typescript --- .../builder/src/stores/builder/queries.js | 130 --------------- .../builder/src/stores/builder/queries.ts | 156 ++++++++++++++++++ 2 files changed, 156 insertions(+), 130 deletions(-) delete mode 100644 packages/builder/src/stores/builder/queries.js create mode 100644 packages/builder/src/stores/builder/queries.ts 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() From 616e89716c8ea89bcd1b34b8e16a72463b089924 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 12:47:49 +0100 Subject: [PATCH 33/49] Remove unneeded extension --- packages/client/src/sdk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 40f35b2ba7..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.ts" +} from "./utils/schema" import { getAPIKey } from "./utils/api.js" import { enrichButtonActions } from "./utils/buttonActions.js" import { processStringSync, makePropSafe } from "@budibase/string-templates" From b54eb876d81007c37b918e33f61ded9db8c4ef1f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 12:50:08 +0100 Subject: [PATCH 34/49] Renames --- packages/frontend-core/src/fetch/ViewFetch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/fetch/ViewFetch.ts b/packages/frontend-core/src/fetch/ViewFetch.ts index c075d80ce0..df00b9bbfc 100644 --- a/packages/frontend-core/src/fetch/ViewFetch.ts +++ b/packages/frontend-core/src/fetch/ViewFetch.ts @@ -1,7 +1,7 @@ import { Table } from "@budibase/types" import DataFetch from "./DataFetch" -type ViewV1 = { +type ViewV1Datasource = { type: "view" name: string tableId: string @@ -10,7 +10,7 @@ type ViewV1 = { groupBy: string } -export default class ViewFetch extends DataFetch { +export default class ViewFetch extends DataFetch { async getDefinition() { const { datasource } = this.options From ed6adbae08cb76f4278084b6d84fc9dcf0f65d45 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 10 Jan 2025 11:52:48 +0000 Subject: [PATCH 35/49] Bump version to 3.2.38 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 647c9f202d..ff69a18459 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.38", "npmClient": "yarn", "concurrency": 20, "command": { From 6ecb01ae830d7ea94fe14b4e2696cfbb29f82cb1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 14:47:09 +0100 Subject: [PATCH 36/49] Fix close modal on grid+modal generation --- packages/frontend-core/src/utils/utils.js | 3 +++ 1 file changed, 3 insertions(+) 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 ? [ From 23025f46396b828e27c356ae21c7bddb473c90c9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 15:19:55 +0100 Subject: [PATCH 37/49] Clean --- packages/client/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/api/api.ts b/packages/client/src/api/api.ts index 93f59f6e9a..b944f7bd7c 100644 --- a/packages/client/src/api/api.ts +++ b/packages/client/src/api/api.ts @@ -4,7 +4,7 @@ import { notificationStore, devToolsEnabled, devToolsStore, -} from "../stores/index.js" +} from "../stores/index" import { get } from "svelte/store" export const API = createAPIClient({ From 9d0e3a17222f303e1f3a952e7c5e298d7187f070 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 15:22:34 +0100 Subject: [PATCH 38/49] Remove unnecessary extensions --- packages/client/src/api/index.js | 4 ++-- packages/client/src/stores/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index a63e19bfbb..3c53045cfd 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -1,5 +1,5 @@ -import { API } from "./api.ts" -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/stores/index.js b/packages/client/src/stores/index.js index e099434b3d..f2b80ed732 100644 --- a/packages/client/src/stores/index.js +++ b/packages/client/src/stores/index.js @@ -1,6 +1,6 @@ export { authStore } from "./auth" export { appStore } from "./app" -export { notificationStore } from "./notification.ts" +export { notificationStore } from "./notification" export { routeStore } from "./routes" export { screenStore } from "./screens" export { builderStore } from "./builder" From 9e2915ff0fc0a7279b95b1b05f9dd8e8baec1cbc Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 16:24:55 +0100 Subject: [PATCH 39/49] Fix wrong conversion --- packages/frontend-core/src/fetch/UserFetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/fetch/UserFetch.ts b/packages/frontend-core/src/fetch/UserFetch.ts index 656cd840fe..58aa4f5a96 100644 --- a/packages/frontend-core/src/fetch/UserFetch.ts +++ b/packages/frontend-core/src/fetch/UserFetch.ts @@ -52,7 +52,7 @@ export default class UserFetch extends DataFetch< const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest) ? rest - : { [BasicOperator.EMPTY]: { email: null } } + : { [BasicOperator.STRING]: { email: null as any } } try { const opts: SearchUsersRequest = { From f54d917f3a1b375948491757298bc71fdb968192 Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:27:50 +0000 Subject: [PATCH 40/49] account-portal login v2 feature flag (#15343) --- packages/types/src/sdk/featureFlag.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 From e7400f982cc6bab2bada801e94a76b167332b84e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 16:29:42 +0100 Subject: [PATCH 41/49] Add comment --- packages/frontend-core/src/fetch/UserFetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/fetch/UserFetch.ts b/packages/frontend-core/src/fetch/UserFetch.ts index e8e7dc8721..1fe9c0a383 100644 --- a/packages/frontend-core/src/fetch/UserFetch.ts +++ b/packages/frontend-core/src/fetch/UserFetch.ts @@ -56,7 +56,7 @@ export default class UserFetch extends DataFetch< const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest) ? rest - : { [BasicOperator.STRING]: { email: null as any } } + : { [BasicOperator.STRING]: { email: null as any } } // TODO: chech. Left as any to not change the behaviour it had when it was js try { const opts: SearchUsersRequest = { From 337339d6e5366b36d437dc384f29963506cee6df Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Jan 2025 16:39:55 +0100 Subject: [PATCH 42/49] Remove confusing empty filter --- packages/frontend-core/src/fetch/UserFetch.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/frontend-core/src/fetch/UserFetch.ts b/packages/frontend-core/src/fetch/UserFetch.ts index 1fe9c0a383..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 @@ -56,7 +52,7 @@ export default class UserFetch extends DataFetch< const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest) ? rest - : { [BasicOperator.STRING]: { email: null as any } } // TODO: chech. Left as any to not change the behaviour it had when it was js + : {} try { const opts: SearchUsersRequest = { From 6ab389edde98138f90b85f216cea44b6af804875 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 10 Jan 2025 15:51:18 +0000 Subject: [PATCH 43/49] Bump version to 3.2.39 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index ff69a18459..0dc09b27be 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.38", + "version": "3.2.39", "npmClient": "yarn", "concurrency": 20, "command": { From fcb3a3a1986eae17208ecf44cfa0d9b3e513d09c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:24:23 +0000 Subject: [PATCH 44/49] Fix this reference --- packages/builder/src/stores/portal/users.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/builder/src/stores/portal/users.ts b/packages/builder/src/stores/portal/users.ts index 9284ad2992..1beb73a6c0 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -29,6 +29,8 @@ class UserStore extends BudiStore { data: [], }) + this.search = this.search.bind(this) + // Update quotas after any add or remove operation this.create = this.refreshUsage(this.create) this.save = this.refreshUsage(this.save) From a6ac76eb05f37c2402cd86c693334b36c6bee757 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:24:37 +0000 Subject: [PATCH 45/49] Fix group actions error --- .../builder/portal/users/groups/_components/GroupUsers.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() } From 1ec4b4c6b7fc3f0a7f91b52797669612c061761e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:38:00 +0000 Subject: [PATCH 46/49] Fix this reference --- .../src/pages/builder/portal/users/users/index.svelte | 1 + packages/builder/src/stores/portal/users.ts | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) 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/portal/users.ts b/packages/builder/src/stores/portal/users.ts index 1beb73a6c0..6503fc9280 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -29,13 +29,11 @@ class UserStore extends BudiStore { data: [], }) - this.search = this.search.bind(this) - // Update quotas after any add or remove operation - this.create = this.refreshUsage(this.create) - this.save = this.refreshUsage(this.save) - this.delete = this.refreshUsage(this.delete) - this.bulkDelete = this.refreshUsage(this.bulkDelete) + this.create = this.refreshUsage(this.create.bind(this)) + this.save = this.refreshUsage(this.save.bind(this)) + this.delete = this.refreshUsage(this.delete.bind(this)) + this.bulkDelete = this.refreshUsage(this.bulkDelete.bind(this)) } async search(opts: SearchUsersRequest = {}) { From bd378f0bd44f24bdd22c7fa55657ff96f4051d62 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:50:23 +0000 Subject: [PATCH 47/49] Simplify usage quota refreshing when doing user CRUD --- packages/builder/src/stores/portal/users.ts | 26 +++++++-------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/builder/src/stores/portal/users.ts b/packages/builder/src/stores/portal/users.ts index 6503fc9280..605f8612aa 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -28,12 +28,6 @@ class UserStore extends BudiStore { super({ data: [], }) - - // Update quotas after any add or remove operation - this.create = this.refreshUsage(this.create.bind(this)) - this.save = this.refreshUsage(this.save.bind(this)) - this.delete = this.refreshUsage(this.delete.bind(this)) - this.bulkDelete = this.refreshUsage(this.bulkDelete.bind(this)) } async search(opts: SearchUsersRequest = {}) { @@ -156,6 +150,7 @@ class UserStore extends BudiStore { return body }) const response = await API.createUsers(mappedUsers, data.groups) + licensing.setQuotaUsage() // re-search from first page await this.search() @@ -164,14 +159,19 @@ class UserStore extends BudiStore { async delete(id: string) { await API.deleteUser(id) + licensing.setQuotaUsage() } async bulkDelete(users: UserIdentifier[]) { - return API.deleteUsers(users) + const res = API.deleteUsers(users) + licensing.setQuotaUsage() + return res } async save(user: User) { - return await API.saveUser(user) + const res = await API.saveUser(user) + licensing.setQuotaUsage() + return res } async addAppBuilder(userId: string, appId: string) { @@ -202,16 +202,6 @@ class UserStore extends BudiStore { return Constants.BudibaseRoles.AppUser } } - - // Wrapper function to refresh quota usage after an operation, - // persisting argument and return types - refreshUsage(fn: (...args: T) => Promise) { - return async function (...args: T) { - const response = await fn(...args) - await licensing.setQuotaUsage() - return response - } - } } export const users = new UserStore() From 53b12075bb1b35bc7839f46384c6b7e907a92d15 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:57:10 +0000 Subject: [PATCH 48/49] Fix unrelated automation log error message --- .../src/pages/builder/portal/apps/index.svelte | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index f94bad2147..5c3ee674e9 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -176,6 +176,8 @@ notifications.error("Error getting init info") } }) + + $: console.log(automationErrors) @@ -191,8 +193,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)} /> From 79a74ff709706ed37aa3a3a9084f0ba7f4fcb133 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:58:31 +0000 Subject: [PATCH 49/49] Remove log --- packages/builder/src/pages/builder/portal/apps/index.svelte | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index 5c3ee674e9..bcd59cd948 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -176,8 +176,6 @@ notifications.error("Error getting init info") } }) - - $: console.log(automationErrors)