From b07382fa2e0efb0009bf98f14f3140ebd07da591 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Sat, 4 Nov 2023 11:04:09 +0100 Subject: [PATCH 01/67] WIP - Migrating worker --- packages/types/src/sdk/licensing/plan.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/types/src/sdk/licensing/plan.ts b/packages/types/src/sdk/licensing/plan.ts index 1604dfb8af..5ac8b1c9f6 100644 --- a/packages/types/src/sdk/licensing/plan.ts +++ b/packages/types/src/sdk/licensing/plan.ts @@ -7,7 +7,9 @@ export enum PlanType { /** @deprecated */ PREMIUM = "premium", PREMIUM_PLUS = "premium_plus", + /** @deprecated */ BUSINESS = "business", + ENTERPRISE_BASIC = "enterprise_basic", ENTERPRISE = "enterprise", } From 5088caf0f944c82cad3b292df3c06896a0583893 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Mon, 6 Nov 2023 08:37:53 +0100 Subject: [PATCH 02/67] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 3820c0c93a..34cce46d9c 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 3820c0c93a3e448e10a60a9feb5396844b537ca8 +Subproject commit 34cce46d9c67f03aa66272e079e1d460a731225e From 47292b8ab4fd95a7d1ffaada5baf28bd394b977d Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 8 Nov 2023 14:37:19 +0000 Subject: [PATCH 03/67] Make DB name non-optional. --- packages/backend-core/src/context/mainContext.ts | 3 +++ packages/backend-core/src/db/couch/DatabaseImpl.ts | 5 +---- packages/backend-core/src/db/db.ts | 12 +----------- packages/server/src/api/controllers/datasource.ts | 2 +- packages/server/src/db/utils.ts | 12 +++++++++--- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 609c18abb5..52ab17d31e 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -276,6 +276,9 @@ export function getAuditLogsDB(): Database { */ export function getAppDB(opts?: any): Database { const appId = getAppId() + if (!appId) { + throw new Error("Unable to retrieve app DB - no app ID.") + } return getDB(appId, opts) } diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 330b15e680..1ace60ed5b 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -48,10 +48,7 @@ export class DatabaseImpl implements Database { private readonly couchInfo = getCouchInfo() - constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) { - if (dbName == null) { - throw new Error("Database name cannot be undefined.") - } + constructor(dbName: string, opts?: DatabaseOpts, connection?: string) { this.name = dbName this.pouchOpts = opts || {} if (connection) { diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts index 9aae64b892..54c4d3ae0f 100644 --- a/packages/backend-core/src/db/db.ts +++ b/packages/backend-core/src/db/db.ts @@ -1,10 +1,7 @@ -import env from "../environment" import { directCouchQuery, DatabaseImpl } from "./couch" import { CouchFindOptions, Database } from "@budibase/types" -const dbList = new Set() - -export function getDB(dbName?: string, opts?: any): Database { +export function getDB(dbName: string, opts?: any): Database { return new DatabaseImpl(dbName, opts) } @@ -22,13 +19,6 @@ export async function doWithDB( return await cb(db) } -export function allDbs() { - if (!env.isTest()) { - throw new Error("Cannot be used outside test environment.") - } - return [...dbList] -} - export async function directCouchAllDbs(queryString?: string) { let couchPath = "/_all_dbs" if (queryString) { diff --git a/packages/server/src/api/controllers/datasource.ts b/packages/server/src/api/controllers/datasource.ts index 5d024d51b6..39bc612b32 100644 --- a/packages/server/src/api/controllers/datasource.ts +++ b/packages/server/src/api/controllers/datasource.ts @@ -337,7 +337,7 @@ export async function destroy(ctx: UserCtx) { if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) { await destroyInternalTablesBySourceId(datasourceId) } else { - const queries = await db.allDocs(getQueryParams(datasourceId, null)) + const queries = await db.allDocs(getQueryParams(datasourceId)) await db.bulkDocs( queries.rows.map((row: any) => ({ _id: row.id, diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index d532d8a8b2..715db552c9 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -6,6 +6,7 @@ import { RelationshipFieldMetadata, VirtualDocumentType, INTERNAL_TABLE_SOURCE_ID, + DatabaseQueryOpts, } from "@budibase/types" import { FieldTypes } from "../constants" export { DocumentType, VirtualDocumentType } from "@budibase/types" @@ -229,7 +230,10 @@ export function getAutomationMetadataParams(otherProps: any = {}) { /** * Gets parameters for retrieving a query, this is a utility function for the getDocParams function. */ -export function getQueryParams(datasourceId?: Optional, otherProps: any = {}) { +export function getQueryParams( + datasourceId?: Optional, + otherProps: Partial = {} +) { if (datasourceId == null) { return getDocParams(DocumentType.QUERY, null, otherProps) } @@ -256,7 +260,7 @@ export function generateMetadataID(type: string, entityId: string) { export function getMetadataParams( type: string, entityId?: Optional, - otherProps: any = {} + otherProps: Partial = {} ) { let docId = `${type}${SEPARATOR}` if (entityId != null) { @@ -269,7 +273,9 @@ export function generateMemoryViewID(viewName: string) { return `${DocumentType.MEM_VIEW}${SEPARATOR}${viewName}` } -export function getMemoryViewParams(otherProps: any = {}) { +export function getMemoryViewParams( + otherProps: Partial = {} +) { return getDocParams(DocumentType.MEM_VIEW, null, otherProps) } From e3a4c34f8d2b7094965b1ff10f23fb3a8cd8144f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 8 Nov 2023 16:17:24 +0000 Subject: [PATCH 04/67] Pass types through context callbacks. --- .../backend-core/src/cache/appMetadata.ts | 2 +- packages/backend-core/src/context/Context.ts | 2 +- .../backend-core/src/context/mainContext.ts | 20 +++++++++---------- packages/backend-core/src/db/db.ts | 6 +++--- packages/server/src/automations/triggers.ts | 2 +- .../src/tests/utilities/TestConfiguration.ts | 11 +++++----- packages/server/src/threads/automation.ts | 2 +- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/backend-core/src/cache/appMetadata.ts b/packages/backend-core/src/cache/appMetadata.ts index bd3efc20db..d442511fb8 100644 --- a/packages/backend-core/src/cache/appMetadata.ts +++ b/packages/backend-core/src/cache/appMetadata.ts @@ -19,7 +19,7 @@ async function populateFromDB(appId: string) { return doWithDB( appId, (db: Database) => { - return db.get(DocumentType.APP_METADATA) + return db.get(DocumentType.APP_METADATA) }, { skip_setup: true } ) diff --git a/packages/backend-core/src/context/Context.ts b/packages/backend-core/src/context/Context.ts index d29b6935a8..a59f5c6503 100644 --- a/packages/backend-core/src/context/Context.ts +++ b/packages/backend-core/src/context/Context.ts @@ -4,7 +4,7 @@ import { ContextMap } from "./types" export default class Context { static storage = new AsyncLocalStorage() - static run(context: ContextMap, func: any) { + static run(context: ContextMap, func: () => T) { return Context.storage.run(context, () => func()) } diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 52ab17d31e..d2259cfcab 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -98,17 +98,17 @@ function updateContext(updates: ContextMap): ContextMap { return context } -async function newContext(updates: ContextMap, task: any) { +async function newContext(updates: ContextMap, task: () => T) { // see if there already is a context setup let context: ContextMap = updateContext(updates) return Context.run(context, task) } -export async function doInAutomationContext(params: { +export async function doInAutomationContext(params: { appId: string automationId: string - task: any -}): Promise { + task: () => T +}): Promise { const tenantId = getTenantIDFromAppID(params.appId) return newContext( { @@ -144,10 +144,10 @@ export async function doInTenant( return newContext(updates, task) } -export async function doInAppContext( +export async function doInAppContext( appId: string | null, - task: any -): Promise { + task: () => T +): Promise { if (!appId && !env.isTest()) { throw new Error("appId is required") } @@ -165,10 +165,10 @@ export async function doInAppContext( return newContext(updates, task) } -export async function doInIdentityContext( +export async function doInIdentityContext( identity: IdentityContext, - task: any -): Promise { + task: () => T +): Promise { if (!identity) { throw new Error("identity is required") } diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts index 54c4d3ae0f..3e69d49f0e 100644 --- a/packages/backend-core/src/db/db.ts +++ b/packages/backend-core/src/db/db.ts @@ -1,7 +1,7 @@ import { directCouchQuery, DatabaseImpl } from "./couch" -import { CouchFindOptions, Database } from "@budibase/types" +import { CouchFindOptions, Database, DatabaseOpts } from "@budibase/types" -export function getDB(dbName: string, opts?: any): Database { +export function getDB(dbName: string, opts?: DatabaseOpts): Database { return new DatabaseImpl(dbName, opts) } @@ -11,7 +11,7 @@ export function getDB(dbName: string, opts?: any): Database { export async function doWithDB( dbName: string, cb: (db: Database) => Promise, - opts = {} + opts?: DatabaseOpts ) { const db = getDB(dbName, opts) // need this to be async so that we can correctly close DB after all diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index f0eca759f5..ac977bbefb 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -94,7 +94,7 @@ export async function externalTrigger( automation: Automation, params: { fields: Record; timeout?: number }, { getResponses }: { getResponses?: boolean } = {} -) { +): Promise { if ( automation.definition != null && automation.definition.trigger != null && diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 6877561fcb..3a14a87d2a 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -510,13 +510,14 @@ class TestConfiguration { // create dev app // clear any old app this.appId = null - await context.doInAppContext(null, async () => { - this.app = await this._req( + this.app = await context.doInAppContext(null, async () => { + const app = await this._req( { name: appName }, null, controllers.app.create ) - this.appId = this.app?.appId! + this.appId = app.appId! + return app }) return await context.doInAppContext(this.appId, async () => { // create production app @@ -525,7 +526,7 @@ class TestConfiguration { this.allApps.push(this.prodApp) this.allApps.push(this.app) - return this.app + return this.app! }) } @@ -537,7 +538,7 @@ class TestConfiguration { return context.doInAppContext(prodAppId, async () => { const db = context.getProdAppDB() - return await db.get(dbCore.DocumentType.APP_METADATA) + return await db.get(dbCore.DocumentType.APP_METADATA) }) } diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 9241289e86..d1fcc2be72 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -241,7 +241,7 @@ class Orchestrator { }) } - async execute() { + async execute(): Promise { // this will retrieve from context created at start of thread this._context.env = await sdkUtils.getEnvironmentVariables() let automation = this._automation From b03719ff1c72509e7edd75f7a1eb82695c658c80 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Wed, 8 Nov 2023 17:34:28 +0100 Subject: [PATCH 05/67] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 34cce46d9c..cbd3c7e333 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 34cce46d9c67f03aa66272e079e1d460a731225e +Subproject commit cbd3c7e33389c976dc73934667beb65d60c605f3 From f35daee07f43120554991be35456a31a804df2c1 Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 8 Nov 2023 16:59:00 +0000 Subject: [PATCH 06/67] Added toggle-all functionality to the formblock and update context to the ComponentSettingsSection --- .../controls/EditComponentPopover.svelte | 2 + .../FieldConfiguration/CellEditor.svelte | 26 --- .../FieldConfiguration/ColumnDrawer.svelte | 202 ------------------ .../FieldConfiguration.svelte | 58 ++++- .../FieldConfiguration/FieldSetting.svelte | 10 +- .../Component/ComponentSettingsSection.svelte | 19 ++ 6 files changed, 84 insertions(+), 233 deletions(-) delete mode 100644 packages/builder/src/components/design/settings/controls/FieldConfiguration/CellEditor.svelte delete mode 100644 packages/builder/src/components/design/settings/controls/FieldConfiguration/ColumnDrawer.svelte diff --git a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte index 26c1ced502..d0f6f24362 100644 --- a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte +++ b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte @@ -11,6 +11,7 @@ export let componentBindings export let bindings export let parseSettings + export let disabled const draggable = getContext("draggable") const dispatch = createEventDispatcher() @@ -90,6 +91,7 @@ open = true } }} + {disabled} /> - import { DrawerContent, Drawer, Button, Icon } from "@budibase/bbui" - import ValidationDrawer from "components/design/settings/controls/ValidationEditor/ValidationDrawer.svelte" - export let column - export let type - - let drawer - - - - - - "{column.name}" field validation - - - -
- -
-
-
diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/ColumnDrawer.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/ColumnDrawer.svelte deleted file mode 100644 index 316bf56da3..0000000000 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/ColumnDrawer.svelte +++ /dev/null @@ -1,202 +0,0 @@ - - - -
- - {#if columns?.length} - -
-
- - -
-
-
-
- {#each columns as column (column.id)} -
-
(dragDisabled = false)} - > - -
- - - removeColumn(column.id)} - disabled={columns.length === 1} - /> -
- {/each} -
- - {:else} -
-
- Add columns to be included in your form below. -
-
- {/if} -
-
- - - {#if columns?.length} - - {/if} -
-
- -
- - - diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte index 6c74705ab0..2211f4a41b 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte @@ -1,4 +1,5 @@
+
+ + { + let update = fieldList.map(field => ({ + ...field, + active: selectAll, + })) + toggleAll(update) + }} + text="" + bind:value={selectAll} + thin + disabled={updating} + /> +
{#if fieldList?.length} listUpdated(e.detail)} on:itemChange={processItemUpdate} items={fieldList} listItemKey={"_id"} @@ -162,7 +193,9 @@ listTypeProps={{ componentBindings, bindings, + updating, }} + draggable={!updating} /> {/if}
@@ -171,4 +204,21 @@ .field-configuration :global(.spectrum-ActionButton) { width: 100%; } + .toggle-all { + display: flex; + justify-content: space-between; + } + .toggle-all :global(.spectrum-Switch) { + margin-right: 0px; + padding-right: calc(var(--spacing-s) - 1px); + min-height: unset; + } + .toggle-all :global(.spectrum-Switch .spectrum-Switch-switch) { + margin-top: 0px; + } + .toggle-all span { + color: var(--spectrum-global-color-gray-700); + font-size: 12px; + margin-left: calc(var(--spacing-s) - 1px); + } diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte index 1d9ce733b8..75e6599a6c 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte @@ -11,6 +11,7 @@ export let componentBindings export let bindings export let anchor + export let updating //or disabled const dispatch = createEventDispatcher() const onToggle = item => { @@ -49,6 +50,7 @@ {bindings} {parseSettings} on:change + disabled={updating} >
@@ -58,7 +60,13 @@
{readableText}
- +
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte index 65f010e4ec..3483f6d26f 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte @@ -8,6 +8,8 @@ import { getComponentForSetting } from "components/design/settings/componentSettings" import InfoDisplay from "./InfoDisplay.svelte" import analytics, { Events } from "analytics" + import { setContext } from "svelte" + import { writable } from "svelte/store" export let componentDefinition export let componentInstance @@ -19,6 +21,21 @@ export let includeHidden = false export let tag + let status = writable({ + status: null, + lastUpdate: null, + }) + + const updateStatus = resp => { + status.update(state => ({ + ...state, + resp, + lastUpdate: new Date().getTime(), + })) + } + + setContext("settings", status) + $: sections = getSections( componentInstance, componentDefinition, @@ -76,6 +93,7 @@ } else { await store.actions.components.updateSetting(setting.key, value) } + updateStatus("success") // Send event if required if (setting.sendEvents) { analytics.captureEvent(Events.COMPONENT_UPDATED, { @@ -85,6 +103,7 @@ }) } } catch (error) { + updateStatus("failed") notifications.error("Error updating component prop") } } From 8436a8d22055d47cf5f331cc4e65259ae5d87e5d Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 9 Nov 2023 09:54:05 +0100 Subject: [PATCH 07/67] Day passes are optional --- packages/types/src/sdk/licensing/quota.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/sdk/licensing/quota.ts b/packages/types/src/sdk/licensing/quota.ts index 85700f167b..2d33c14182 100644 --- a/packages/types/src/sdk/licensing/quota.ts +++ b/packages/types/src/sdk/licensing/quota.ts @@ -61,7 +61,7 @@ export type PlanQuotas = { [key in PlanType]: Quotas | undefined } export type MonthlyQuotas = { [MonthlyQuotaName.QUERIES]: Quota [MonthlyQuotaName.AUTOMATIONS]: Quota - [MonthlyQuotaName.DAY_PASSES]: Quota + [MonthlyQuotaName.DAY_PASSES]?: Quota } export type StaticQuotas = { From e1ca1d67322779eae0857028ea715a899933da14 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 9 Nov 2023 09:55:09 +0100 Subject: [PATCH 08/67] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index cbd3c7e333..ab17824872 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit cbd3c7e33389c976dc73934667beb65d60c605f3 +Subproject commit ab17824872a08010886abdc5893b14d3430efe00 From 311ad6b6a5010435fd56778151f85356e4ee01aa Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 9 Nov 2023 10:10:04 +0100 Subject: [PATCH 09/67] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index ab17824872..18f558efb0 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit ab17824872a08010886abdc5893b14d3430efe00 +Subproject commit 18f558efb0f2d4a021a46d6236d12d93f6ce474f From 7afcfbac93fc172e3b0a754e101f06de90d600bc Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 9 Nov 2023 09:27:06 +0000 Subject: [PATCH 10/67] Bump version to 2.13.6 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 5605642877..ce3128165f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.5", + "version": "2.13.6", "npmClient": "yarn", "packages": [ "packages/*" From 63b3bee311e4f329b6b4bb28e114944e3963ca49 Mon Sep 17 00:00:00 2001 From: Mel O'Hagan Date: Thu, 9 Nov 2023 09:52:07 +0000 Subject: [PATCH 11/67] fix --- packages/client/src/components/app/Text.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/app/Text.svelte b/packages/client/src/components/app/Text.svelte index 6c16db25fd..1037725ff8 100644 --- a/packages/client/src/components/app/Text.svelte +++ b/packages/client/src/components/app/Text.svelte @@ -94,7 +94,7 @@ .align--right { text-align: right; } - .align-justify { + .align--justify { text-align: justify; } From 1f85b56a99400b3cda5735379a98a359279180a5 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 9 Nov 2023 11:44:36 +0100 Subject: [PATCH 12/67] Revert "Day passes are optional" This reverts commit 8436a8d22055d47cf5f331cc4e65259ae5d87e5d. --- packages/types/src/sdk/licensing/quota.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/sdk/licensing/quota.ts b/packages/types/src/sdk/licensing/quota.ts index 2d33c14182..85700f167b 100644 --- a/packages/types/src/sdk/licensing/quota.ts +++ b/packages/types/src/sdk/licensing/quota.ts @@ -61,7 +61,7 @@ export type PlanQuotas = { [key in PlanType]: Quotas | undefined } export type MonthlyQuotas = { [MonthlyQuotaName.QUERIES]: Quota [MonthlyQuotaName.AUTOMATIONS]: Quota - [MonthlyQuotaName.DAY_PASSES]?: Quota + [MonthlyQuotaName.DAY_PASSES]: Quota } export type StaticQuotas = { From 5ddc3faf22a7100cf83a286ae372967b83a268bb Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 9 Nov 2023 11:45:38 +0100 Subject: [PATCH 13/67] Upgrade pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 18f558efb0..837bbc7b33 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 18f558efb0f2d4a021a46d6236d12d93f6ce474f +Subproject commit 837bbc7b3317c5681fa22f2b92a8d1a7d39c3c95 From 14c6e1bc4a4cf9c3f68cda4b300e8e511e15e73d Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 9 Nov 2023 12:00:21 +0100 Subject: [PATCH 14/67] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 837bbc7b33..e202f415d9 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 837bbc7b3317c5681fa22f2b92a8d1a7d39c3c95 +Subproject commit e202f415d9fa540d08cc2ba6e27394fbc22f357b From 3f69b17c94a6ca45f88764d1ecfb1306b1afc07f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 11:05:42 +0000 Subject: [PATCH 15/67] Fully type the worker redis utils file. --- .../tests/core/utilities/structures/users.ts | 2 +- .../src/api/controllers/global/users.ts | 15 +- .../src/api/routes/global/tests/users.spec.ts | 21 ++- packages/worker/src/sdk/auth/auth.ts | 2 +- packages/worker/src/sdk/users/users.ts | 2 + packages/worker/src/utilities/email.ts | 6 +- packages/worker/src/utilities/redis.ts | 162 ++++++++++-------- 7 files changed, 124 insertions(+), 86 deletions(-) diff --git a/packages/backend-core/tests/core/utilities/structures/users.ts b/packages/backend-core/tests/core/utilities/structures/users.ts index 66d23696e0..68ee29686c 100644 --- a/packages/backend-core/tests/core/utilities/structures/users.ts +++ b/packages/backend-core/tests/core/utilities/structures/users.ts @@ -12,7 +12,7 @@ import { generator } from "./generator" import { tenant } from "." export const newEmail = () => { - return `${uuid()}@test.com` + return `${uuid()}@example.com` } export const user = (userProps?: Partial>): User => { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index de1a605890..1f4168c00b 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,5 +1,6 @@ import { - checkInviteCode, + getInviteCode, + deleteInviteCode, getInviteCodes, updateInviteCode, } from "../../../utilities/redis" @@ -336,10 +337,11 @@ export const checkInvite = async (ctx: any) => { const { code } = ctx.params let invite try { - invite = await checkInviteCode(code, false) + invite = await getInviteCode(code) } 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, @@ -365,12 +367,10 @@ export const updateInvite = async (ctx: any) => { let invite try { - invite = await checkInviteCode(code, false) - if (!invite) { - throw new Error("The invite could not be retrieved") - } + invite = await getInviteCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") + return } let updated = { @@ -405,7 +405,8 @@ export const inviteAccept = async ( const { inviteCode, password, firstName, lastName } = ctx.request.body try { // info is an extension of the user object that was stored by global - const { email, info }: any = await checkInviteCode(inviteCode) + const { email, info }: any = await getInviteCode(inviteCode) + await deleteInviteCode(inviteCode) const user = await tenancy.doInTenant(info.tenantId, async () => { let request: any = { firstName, diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 846b98a7ae..adcbca9d29 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -1,11 +1,12 @@ import { InviteUsersResponse, User } from "@budibase/types" -jest.mock("nodemailer") import { TestConfiguration, mocks, structures } from "../../../../tests" -const sendMailMock = mocks.email.mock() import { events, tenancy, accounts as _accounts } from "@budibase/backend-core" import * as userSdk from "../../../../sdk/users" +jest.mock("nodemailer") +const sendMailMock = mocks.email.mock() + const accounts = jest.mocked(_accounts) describe("/api/global/users", () => { @@ -54,6 +55,22 @@ describe("/api/global/users", () => { expect(events.user.invited).toBeCalledTimes(0) }) + it("should not invite the same user twice", async () => { + const email = structures.users.newEmail() + await config.api.users.sendUserInvite(sendMailMock, email) + + const { code, res } = await config.api.users.sendUserInvite( + sendMailMock, + email, + 400 + ) + + expect(res.body.message).toBe(`Unavailable`) + expect(sendMailMock).toHaveBeenCalledTimes(0) + expect(code).toBeUndefined() + expect(events.user.invited).toBeCalledTimes(0) + }) + it("should be able to create new user from invite", async () => { const email = structures.users.newEmail() const { code } = await config.api.users.sendUserInvite( diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index d989113c3d..e25d34fa5e 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.checkResetPasswordCode(resetCode) + const { userId } = await redis.getResetPasswordCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 230a8fa146..8f04bb1941 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -2,6 +2,7 @@ import { events, tenancy, users as usersCore } from "@budibase/backend-core" import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" +import { getInviteCodes } from "../..//utilities/redis" export async function invite( users: InviteUsersRequest @@ -14,6 +15,7 @@ export async function invite( const matchedEmails = await usersCore.searchExistingEmails( users.map(u => u.email) ) + const existingInvites = await getInviteCodes() const newUsers = [] // separate duplicates from new users diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index c5b1d9d8ab..a0c02d335c 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -3,7 +3,7 @@ import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" -import { getResetPasswordCode, getInviteCode } from "./redis" +import { createResetPasswordCode, createInviteCode } from "./redis" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" import ical from "ical-generator" @@ -61,9 +61,9 @@ async function getLinkCode( ) { switch (purpose) { case EmailTemplatePurpose.PASSWORD_RECOVERY: - return getResetPasswordCode(user._id!, info) + return createResetPasswordCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return getInviteCode(email, info) + return createInviteCode(email, info) default: return null } diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts index 993cdf97ce..9852fb0467 100644 --- a/packages/worker/src/utilities/redis.ts +++ b/packages/worker/src/utilities/redis.ts @@ -1,60 +1,24 @@ import { redis, utils, tenancy } from "@budibase/backend-core" import env from "../environment" -function getExpirySecondsForDB(db: string) { - switch (db) { - case redis.utils.Databases.PW_RESETS: - // a hour - return 3600 - case redis.utils.Databases.INVITATIONS: - // a week - return 604800 - } +interface Invite { + email: string + info: any } -let pwResetClient: any, invitationClient: any - -function getClient(db: string) { - switch (db) { - case redis.utils.Databases.PW_RESETS: - return pwResetClient - case redis.utils.Databases.INVITATIONS: - return invitationClient - } +interface InviteWithCode extends Invite { + code: string } -async function writeACode(db: string, value: any) { - const client = await getClient(db) - const code = utils.newid() - await client.store(code, value, getExpirySecondsForDB(db)) - return code +interface PasswordReset { + userId: string + info: any } -async function updateACode(db: string, code: string, value: any) { - const client = await getClient(db) - await client.store(code, value, getExpirySecondsForDB(db)) -} - -/** - * Given an invite code and invite body, allow the update an existing/valid invite in redis - * @param inviteCode The invite code for an invite in redis - * @param value The body of the updated user invitation - */ -export async function updateInviteCode(inviteCode: string, value: string) { - await updateACode(redis.utils.Databases.INVITATIONS, inviteCode, value) -} - -async function getACode(db: string, code: string, deleteCode = true) { - const client = await getClient(db) - const value = await client.get(code) - if (!value) { - throw new Error("Invalid code.") - } - if (deleteCode) { - await client.delete(code) - } - return value -} +type RedisDBName = + | redis.utils.Databases.PW_RESETS + | redis.utils.Databases.INVITATIONS +let pwResetClient: redis.Client, invitationClient: redis.Client export async function init() { pwResetClient = new redis.Client(redis.utils.Databases.PW_RESETS) @@ -63,9 +27,6 @@ export async function init() { await invitationClient.init() } -/** - * make sure redis connection is closed. - */ export async function shutdown() { if (pwResetClient) await pwResetClient.finish() if (invitationClient) await invitationClient.finish() @@ -74,6 +35,65 @@ export async function shutdown() { console.log("Redis shutdown") } +function getExpirySecondsForDB(db: RedisDBName) { + switch (db) { + case redis.utils.Databases.PW_RESETS: + // a hour + return 3600 + case redis.utils.Databases.INVITATIONS: + // a week + return 604800 + default: + throw new Error(`Unknown redis database: ${db}`) + } +} + +function getClient(db: RedisDBName): redis.Client { + switch (db) { + case redis.utils.Databases.PW_RESETS: + return pwResetClient + case redis.utils.Databases.INVITATIONS: + return invitationClient + default: + throw new Error(`Unknown redis database: ${db}`) + } +} + +async function writeCode(db: RedisDBName, value: Invite | PasswordReset) { + const client = getClient(db) + const code = utils.newid() + await client.store(code, value, getExpirySecondsForDB(db)) + return code +} + +async function updateCode(db: RedisDBName, code: string, value: any) { + const client = getClient(db) + await client.store(code, value, getExpirySecondsForDB(db)) +} + +/** + * Given an invite code and invite body, allow the update an existing/valid invite in redis + * @param inviteCode The invite code for an invite in redis + * @param value The body of the updated user invitation + */ +export async function updateInviteCode(code: string, value: Invite) { + await updateCode(redis.utils.Databases.INVITATIONS, code, value) +} + +async function deleteCode(db: RedisDBName, code: string) { + const client = getClient(db) + await client.delete(code) +} + +async function getCode(db: RedisDBName, code: string) { + const client = getClient(db) + const value = await client.get(code) + if (!value) { + throw new Error(`Could not find code: ${code}`) + } + return value +} + /** * Given a user ID this will store a code (that is returned) for an hour in redis. * The user can then return this code for resetting their password (through their reset link). @@ -81,22 +101,20 @@ export async function shutdown() { * @param info Info about the user/the reset process. * @return returns the code that was stored to redis. */ -export async function getResetPasswordCode(userId: string, info: any) { - return writeACode(redis.utils.Databases.PW_RESETS, { userId, info }) +export async function createResetPasswordCode(userId: string, info: any) { + return writeCode(redis.utils.Databases.PW_RESETS, { userId, info }) } /** - * Given a reset code this will lookup to redis, check if the code is valid and delete if required. + * Given a reset code this will lookup to redis, check if the code is valid. * @param resetCode The code provided via the email link. - * @param deleteCode If the code is used/finished with this will delete it - defaults to true. * @return returns the user ID if it is found */ -export async function checkResetPasswordCode( - resetCode: string, - deleteCode = true -) { +export async function getResetPasswordCode( + code: string +): Promise { try { - return getACode(redis.utils.Databases.PW_RESETS, resetCode, deleteCode) + return getCode(redis.utils.Databases.PW_RESETS, code) } catch (err) { throw "Provided information is not valid, cannot reset password - please try again." } @@ -108,35 +126,35 @@ export async function checkResetPasswordCode( * @param info Information to be carried along with the invitation. * @return returns the code that was stored to redis. */ -export async function getInviteCode(email: string, info: any) { - return writeACode(redis.utils.Databases.INVITATIONS, { email, info }) +export async function createInviteCode(email: string, info: any) { + return writeCode(redis.utils.Databases.INVITATIONS, { email, info }) } /** * Checks that the provided invite code is valid - will return the email address of user that was invited. * @param inviteCode the invite code that was provided as part of the link. - * @param deleteCode whether or not the code should be deleted after retrieval - defaults to true. * @return If the code is valid then an email address will be returned. */ -export async function checkInviteCode( - inviteCode: string, - deleteCode: boolean = true -) { +export async function getInviteCode(code: string): Promise { try { - return getACode(redis.utils.Databases.INVITATIONS, inviteCode, deleteCode) + return getCode(redis.utils.Databases.INVITATIONS, code) } catch (err) { throw "Invitation is not valid or has expired, please request a new one." } } +export async function deleteInviteCode(code: string) { + return deleteCode(redis.utils.Databases.INVITATIONS, code) +} + /** Get all currently available user invitations for the current tenant. **/ -export async function getInviteCodes() { - const client = await getClient(redis.utils.Databases.INVITATIONS) - const invites: any[] = await client.scan() +export async function getInviteCodes(): Promise { + const client = getClient(redis.utils.Databases.INVITATIONS) + const invites: { key: string; value: Invite }[] = await client.scan() - const results = invites.map(invite => { + const results: InviteWithCode[] = invites.map(invite => { return { ...invite.value, code: invite.key, From a6a75b533c0ea587516200691d75afb3b33c387c Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 11:15:44 +0000 Subject: [PATCH 16/67] Reject inviting the same user twice. --- .../worker/src/api/controllers/global/users.ts | 4 +--- .../src/api/routes/global/tests/users.spec.ts | 2 ++ packages/worker/src/sdk/users/users.ts | 17 +++++++++++++---- packages/worker/src/utilities/redis.ts | 6 +++++- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 1f4168c00b..fae42acdfe 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -349,14 +349,12 @@ export const checkInvite = async (ctx: any) => { } export const getUserInvites = async (ctx: any) => { - let invites try { // Restricted to the currently authenticated tenant - invites = await getInviteCodes() + ctx.body = await getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } - ctx.body = invites } export const updateInvite = async (ctx: any) => { diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index adcbca9d29..09cd4d358f 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -59,6 +59,8 @@ describe("/api/global/users", () => { const email = structures.users.newEmail() await config.api.users.sendUserInvite(sendMailMock, email) + jest.clearAllMocks() + const { code, res } = await config.api.users.sendUserInvite( sendMailMock, email, diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 8f04bb1941..4cca0b8fa6 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -1,5 +1,9 @@ import { events, tenancy, users as usersCore } from "@budibase/backend-core" -import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types" +import { + InviteUserRequest, + InviteUsersRequest, + InviteUsersResponse, +} from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" import { getInviteCodes } from "../..//utilities/redis" @@ -15,12 +19,17 @@ export async function invite( const matchedEmails = await usersCore.searchExistingEmails( users.map(u => u.email) ) - const existingInvites = await getInviteCodes() - const newUsers = [] + const invitedEmails = (await getInviteCodes()).map(invite => invite.email) + const newUsers: InviteUserRequest[] = [] // separate duplicates from new users for (let user of users) { - if (matchedEmails.includes(user.email)) { + if ( + matchedEmails.includes(user.email) || + invitedEmails.includes(user.email) + ) { + // This "Unavailable" is load bearing. The tests and frontend both check for it + // specifically response.unsuccessful.push({ email: user.email, reason: "Unavailable" }) } else { newUsers.push(user) diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts index 9852fb0467..337d34e376 100644 --- a/packages/worker/src/utilities/redis.ts +++ b/packages/worker/src/utilities/redis.ts @@ -66,7 +66,11 @@ async function writeCode(db: RedisDBName, value: Invite | PasswordReset) { return code } -async function updateCode(db: RedisDBName, code: string, value: any) { +async function updateCode( + db: RedisDBName, + code: string, + value: Invite | PasswordReset +) { const client = getClient(db) await client.store(code, value, getExpirySecondsForDB(db)) } From b2841b30b23270139cd9560b9520758d78671f44 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 11:17:30 +0000 Subject: [PATCH 17/67] Add a test for the multi-invite endpoint. --- .../src/api/routes/global/tests/users.spec.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 09cd4d358f..ce2c9347b4 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -120,6 +120,23 @@ describe("/api/global/users", () => { expect(sendMailMock).toHaveBeenCalledTimes(0) expect(events.user.invited).toBeCalledTimes(0) }) + + it("should not be able to generate an invitation for user that has already been invited", async () => { + const email = structures.users.newEmail() + await config.api.users.sendUserInvite(sendMailMock, email) + + jest.clearAllMocks() + + const request = [{ email: email, userInfo: {} }] + const res = await config.api.users.sendMultiUserInvite(request) + + const body = res.body as InviteUsersResponse + expect(body.successful.length).toBe(0) + expect(body.unsuccessful.length).toBe(1) + expect(body.unsuccessful[0].reason).toBe("Unavailable") + expect(sendMailMock).toHaveBeenCalledTimes(0) + expect(events.user.invited).toBeCalledTimes(0) + }) }) describe("POST /api/global/users/bulk", () => { From ee91d6753c438229affc681e13cf54b4d8ab1760 Mon Sep 17 00:00:00 2001 From: Mel O'Hagan Date: Thu, 9 Nov 2023 11:55:10 +0000 Subject: [PATCH 18/67] Encode query string --- .../builder/src/components/integration/RestQueryViewer.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/integration/RestQueryViewer.svelte b/packages/builder/src/components/integration/RestQueryViewer.svelte index e6913b0953..9634cd9746 100644 --- a/packages/builder/src/components/integration/RestQueryViewer.svelte +++ b/packages/builder/src/components/integration/RestQueryViewer.svelte @@ -404,7 +404,7 @@ datasource = $datasources.list.find(ds => ds._id === query?.datasourceId) const datasourceUrl = datasource?.config.url const qs = query?.fields.queryString - breakQs = restUtils.breakQueryString(qs) + breakQs = restUtils.breakQueryString(encodeURI(qs)) breakQs = runtimeToReadableMap(mergedBindings, breakQs) const path = query.fields.path @@ -652,7 +652,7 @@
- {#if !response && Object.keys(schema).length === 0} + {#if !response && Object.keys(schema || {}).length === 0} Response
From 08b96ad2131dacd72e7042cd5c98110afc8f1fe0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 9 Nov 2023 12:55:50 +0100 Subject: [PATCH 19/67] Remove unnused pipelines --- .github/workflows/deploy-cloud.yaml | 48 ------ .github/workflows/release-master.yml | 178 ---------------------- .github/workflows/release-selfhost.yml | 125 --------------- .github/workflows/release-singleimage.yml | 86 ----------- 4 files changed, 437 deletions(-) delete mode 100644 .github/workflows/deploy-cloud.yaml delete mode 100644 .github/workflows/release-master.yml delete mode 100644 .github/workflows/release-selfhost.yml delete mode 100644 .github/workflows/release-singleimage.yml diff --git a/.github/workflows/deploy-cloud.yaml b/.github/workflows/deploy-cloud.yaml deleted file mode 100644 index 389b10f7d3..0000000000 --- a/.github/workflows/deploy-cloud.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: Budibase Deploy Production - -on: - workflow_dispatch: - inputs: - version: - description: Budibase release version. For example - 1.0.0 - required: false - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - name: Fail if not a tag - run: | - if [[ $GITHUB_REF != refs/tags/* ]]; then - echo "Workflow Dispatch can only be run on tags" - exit 1 - fi - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Fail if tag is not in master - run: | - if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then - echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" - exit 1 - fi - - - name: Get the latest budibase release version - id: version - run: | - if [ -z "${{ github.event.inputs.version }}" ]; then - release_version=$(cat lerna.json | jq -r '.version') - else - release_version=${{ github.event.inputs.version }} - fi - echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - - uses: passeidireto/trigger-external-workflow-action@main - env: - PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} - with: - repository: budibase/budibase-deploys - event: budicloud-prod-deploy - github_pat: ${{ secrets.GH_ACCESS_TOKEN }} diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml deleted file mode 100644 index 2edb470405..0000000000 --- a/.github/workflows/release-master.yml +++ /dev/null @@ -1,178 +0,0 @@ -name: Budibase Release -concurrency: - group: release - cancel-in-progress: false - -on: - push: - tags: - - "[0-9]+.[0-9]+.[0-9]+" - # Exclude all pre-releases - - "!*[0-9]+.[0-9]+.[0-9]+-*" - -env: - # Posthog token used by ui at build time - POSTHOG_TOKEN: phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU - INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} - PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - -jobs: - release-images: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: true - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - fetch-depth: 0 - - - name: Fail if tag is not in master - run: | - if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then - echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" - exit 1 - fi - - - uses: actions/setup-node@v1 - with: - node-version: 18.x - cache: yarn - - - run: yarn install --frozen-lockfile - - name: Update versions - run: ./scripts/updateVersions.sh - - run: yarn lint - - run: yarn build - - run: yarn build:sdk - - - name: Publish budibase packages to NPM - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - # setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default - git config --global user.name "Budibase Release Bot" - git config --global user.email "<>" - git submodule foreach git commit -a -m 'Release process' - git commit -a -m 'Release process' - echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc - yarn release - - - name: "Get Current tag" - id: currenttag - run: | - version=$(./scripts/getCurrentVersion.sh) - echo "Using tag $version" - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - - name: Docker login - run: | - docker login -u $DOCKER_USER -p $DOCKER_PASSWORD - env: - DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - - - name: Build worker docker - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }} - tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - file: ./packages/worker/Dockerfile.v2 - cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest - cache-to: type=inline - env: - IMAGE_NAME: budibase/worker - IMAGE_TAG: ${{ steps.currenttag.outputs.version }} - BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} - - - name: Build server docker - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }} - tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - file: ./packages/server/Dockerfile.v2 - cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest - cache-to: type=inline - env: - IMAGE_NAME: budibase/apps - IMAGE_TAG: ${{ steps.currenttag.outputs.version }} - BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} - - - name: Build proxy docker - uses: docker/build-push-action@v5 - with: - context: ./hosting/proxy - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - file: ./hosting/proxy/Dockerfile - cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest - cache-to: type=inline - env: - IMAGE_NAME: budibase/proxy - IMAGE_TAG: ${{ steps.currenttag.outputs.version }} - - release-helm-chart: - needs: [release-images] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup Helm - uses: azure/setup-helm@v1 - id: helm-install - - - name: Get the latest budibase release version - id: version - run: | - release_version=$(cat lerna.json | jq -r '.version') - echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - # due to helm repo index issue: https://github.com/helm/helm/issues/7363 - # we need to create new package in a different dir, merge the index and move the package back - - name: Build and release helm chart - run: | - git config user.name "Budibase Helm Bot" - git config user.email "<>" - git reset --hard - git fetch - mkdir sync - echo "Packaging chart to sync dir" - helm package charts/budibase --version 0.0.0-master --app-version "$RELEASE_VERSION" --destination sync - echo "Packaging successful" - git checkout gh-pages - echo "Indexing helm repo" - helm repo index --merge docs/index.yaml sync - mv -f sync/* docs - rm -rf sync - echo "Pushing new helm release" - git add -A - git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}" - git push - - trigger-deploy-to-qa-env: - needs: [release-helm-chart] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: peter-evans/repository-dispatch@v2 - with: - repository: budibase/budibase-deploys - event-type: budicloud-qa-deploy - token: ${{ secrets.GH_ACCESS_TOKEN }} - client-payload: |- - { - "VERSION": "${{ github.ref_name }}", - "REF_NAME": "${{ github.ref_name}}" - } diff --git a/.github/workflows/release-selfhost.yml b/.github/workflows/release-selfhost.yml deleted file mode 100644 index d2689a0ea0..0000000000 --- a/.github/workflows/release-selfhost.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: Budibase Release Selfhost - -on: - workflow_dispatch: - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - name: Fail if not a tag - run: | - if [[ $GITHUB_REF != refs/tags/* ]]; then - echo "Workflow Dispatch can only be run on tags" - exit 1 - fi - - - uses: actions/checkout@v2 - with: - submodules: true - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - fetch-depth: 0 - - - name: Fail if tag is not in master - run: | - if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then - echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" - exit 1 - fi - - - name: Use Node.js 18.x - uses: actions/setup-node@v1 - with: - node-version: 18.x - - - name: Get the latest budibase release version - id: version - run: | - release_version=$(cat lerna.json | jq -r '.version') - echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - - name: Tag and release Docker images (Self Host) - run: | - docker login -u $DOCKER_USER -p $DOCKER_PASSWORD - - release_tag=${{ env.RELEASE_VERSION }} - - # Pull apps and worker images - docker pull budibase/apps:$release_tag - docker pull budibase/worker:$release_tag - docker pull budibase/proxy:$release_tag - - # Tag apps and worker images - docker tag budibase/apps:$release_tag budibase/apps:$SELFHOST_TAG - docker tag budibase/worker:$release_tag budibase/worker:$SELFHOST_TAG - docker tag budibase/proxy:$release_tag budibase/proxy:$SELFHOST_TAG - - # Push images - docker push budibase/apps:$SELFHOST_TAG - docker push budibase/worker:$SELFHOST_TAG - docker push budibase/proxy:$SELFHOST_TAG - env: - DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - SELFHOST_TAG: latest - - - name: Bootstrap and build (CLI) - run: | - yarn - yarn build - - - name: Build OpenAPI spec - run: | - pushd packages/server - yarn - yarn specs - popd - - - name: Setup Helm - uses: azure/setup-helm@v1 - id: helm-install - - # due to helm repo index issue: https://github.com/helm/helm/issues/7363 - # we need to create new package in a different dir, merge the index and move the package back - - name: Build and release helm chart - run: | - git config user.name "Budibase Helm Bot" - git config user.email "<>" - git reset --hard - git fetch - mkdir sync - echo "Packaging chart to sync dir" - helm package charts/budibase --version "$RELEASE_VERSION" --app-version "$RELEASE_VERSION" --destination sync - echo "Packaging successful" - git checkout gh-pages - echo "Indexing helm repo" - helm repo index --merge docs/index.yaml sync - mv -f sync/* docs - rm -rf sync - echo "Pushing new helm release" - git add -A - git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}" - git push - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Perform Github Release - uses: softprops/action-gh-release@v1 - with: - name: ${{ env.RELEASE_VERSION }} - tag_name: ${{ env.RELEASE_VERSION }} - generate_release_notes: true - files: | - packages/cli/build/cli-win.exe - packages/cli/build/cli-linux - packages/cli/build/cli-macos - packages/server/specs/openapi.yaml - packages/server/specs/openapi.json - - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v4.0.0 - with: - webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} - content: "Self Host Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Self Host." - embed-title: ${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/release-singleimage.yml b/.github/workflows/release-singleimage.yml deleted file mode 100644 index 16b1da186a..0000000000 --- a/.github/workflows/release-singleimage.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Deploy Budibase Single Container Image to DockerHub - -on: - workflow_dispatch: - -env: - CI: true - PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - REGISTRY_URL: registry.hub.docker.com -jobs: - build: - name: "build" - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] - steps: - - name: Maximize build space - uses: easimon/maximize-build-space@master - with: - root-reserve-mb: 30000 - swap-size-mb: 1024 - remove-android: "true" - remove-dotnet: "true" - - name: Fail if not a tag - run: | - if [[ $GITHUB_REF != refs/tags/* ]]; then - echo "Workflow Dispatch can only be run on tags" - exit 1 - fi - - name: "Checkout" - uses: actions/checkout@v2 - with: - submodules: true - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Setup QEMU - uses: docker/setup-qemu-action@v1 - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - name: Run Yarn - run: yarn - - name: Update versions - run: ./scripts/updateVersions.sh - - name: Run Yarn Build - run: yarn build - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_API_KEY }} - - name: Get the latest release version - id: version - run: | - release_version=$(cat lerna.json | jq -r '.version') - echo $release_version - echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - name: Tag and release Budibase service docker image - uses: docker/build-push-action@v2 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - build-args: BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }} - tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }} - file: ./hosting/single/Dockerfile.v2 - env: - BUDIBASE_VERSION: ${{ env.RELEASE_VERSION }} - - name: Tag and release Budibase Azure App Service docker image - uses: docker/build-push-action@v2 - with: - context: . - push: true - platforms: linux/amd64 - build-args: | - TARGETBUILD=aas - BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }} - tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }} - file: ./hosting/single/Dockerfile.v2 - env: - BUDIBASE_VERSION: ${{ env.RELEASE_VERSION }} From a26f2e83e49a1acc24f3b262666ec273e7c4d470 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 9 Nov 2023 14:45:23 +0000 Subject: [PATCH 20/67] Some of the new table getters did not account for table IDs not being found, adding a new function which properly accounts for IDs being missing and manages it correctly, rather than leaving tables in an undefined state. --- packages/backend-core/src/db/couch/DatabaseImpl.ts | 13 ++++++++++++- .../server/src/api/controllers/row/internal.ts | 11 ++++------- packages/server/src/api/controllers/row/utils.ts | 14 +------------- packages/server/src/db/linkedRows/index.ts | 6 ++---- packages/server/src/db/utils.ts | 10 ---------- packages/server/src/sdk/app/tables/getters.ts | 14 +++++++++----- packages/server/src/utilities/global.ts | 6 ++---- packages/types/src/sdk/db.ts | 3 ++- 8 files changed, 32 insertions(+), 45 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 1ace60ed5b..96b946fe16 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -109,7 +109,7 @@ export class DatabaseImpl implements Database { } } - async get(id?: string): Promise { + async get(id?: string): Promise { const db = await this.checkSetup() if (!id) { throw new Error("Unable to get doc without a valid _id.") @@ -117,6 +117,17 @@ export class DatabaseImpl implements Database { return this.updateOutput(() => db.get(id)) } + async getMultiple(ids: string[]): Promise { + // get unique + ids = [...new Set(ids)] + const response = await this.allDocs({ + keys: ids, + include_docs: true, + }) + const rows = response.rows.filter(row => row.error !== "not_found") + return rows.map(row => row.doc!) + } + async remove(idOrDoc: string | Document, rev?: string) { const db = await this.checkSetup() let _id: string diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts index fe7d94547a..0edbe4ea86 100644 --- a/packages/server/src/api/controllers/row/internal.ts +++ b/packages/server/src/api/controllers/row/internal.ts @@ -1,9 +1,5 @@ import * as linkRows from "../../../db/linkedRows" -import { - generateRowID, - getMultiIDParams, - InternalTables, -} from "../../../db/utils" +import { generateRowID, InternalTables } from "../../../db/utils" import * as userController from "../user" import { cleanupAttachments, @@ -240,8 +236,9 @@ export async function fetchEnrichedRow(ctx: UserCtx) { const linkVals = links as LinkDocumentValue[] // look up the actual rows based on the ids - const params = getMultiIDParams(linkVals.map(linkVal => linkVal.id)) - let linkedRows = (await db.allDocs(params)).rows.map(row => row.doc!) + let linkedRows = await db.getMultiple( + linkVals.map(linkVal => linkVal.id) + ) // get the linked tables const linkTableIds = getLinkedTableIDs(table as Table) diff --git a/packages/server/src/api/controllers/row/utils.ts b/packages/server/src/api/controllers/row/utils.ts index cd311fdf0f..ed6ccd4c53 100644 --- a/packages/server/src/api/controllers/row/utils.ts +++ b/packages/server/src/api/controllers/row/utils.ts @@ -1,21 +1,9 @@ import { InternalTables } from "../../../db/utils" import * as userController from "../user" import { context } from "@budibase/backend-core" -import { - Ctx, - FieldType, - ManyToOneRelationshipFieldMetadata, - OneToManyRelationshipFieldMetadata, - Row, - SearchFilters, - Table, - UserCtx, -} from "@budibase/types" -import { FieldTypes, NoEmptyFilterStrings } from "../../../constants" -import sdk from "../../../sdk" +import { Ctx, Row, UserCtx } from "@budibase/types" import validateJs from "validate.js" -import { cloneDeep } from "lodash/fp" validateJs.extend(validateJs.validators.datetime, { parse: function (value: string) { diff --git a/packages/server/src/db/linkedRows/index.ts b/packages/server/src/db/linkedRows/index.ts index 7324fa1d94..24ff47ead3 100644 --- a/packages/server/src/db/linkedRows/index.ts +++ b/packages/server/src/db/linkedRows/index.ts @@ -8,7 +8,7 @@ import { getLinkedTable, } from "./linkUtils" import flatten from "lodash/flatten" -import { getMultiIDParams, USER_METDATA_PREFIX } from "../utils" +import { USER_METDATA_PREFIX } from "../utils" import partition from "lodash/partition" import { getGlobalUsersFromMetadata } from "../../utilities/global" import { processFormulas } from "../../utilities/rowProcessor" @@ -79,9 +79,7 @@ async function getFullLinkedDocs(links: LinkDocumentValue[]) { const db = context.getAppDB() const linkedRowIds = links.map(link => link.id) const uniqueRowIds = [...new Set(linkedRowIds)] - let dbRows = (await db.allDocs(getMultiIDParams(uniqueRowIds))).rows.map( - row => row.doc! - ) + let dbRows = await db.getMultiple(uniqueRowIds) // convert the unique db rows back to a full list of linked rows const linked = linkedRowIds .map(id => dbRows.find(row => row && row._id === id)) diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index 715db552c9..a5569f8166 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -283,16 +283,6 @@ export function generatePluginID(name: string) { return `${DocumentType.PLUGIN}${SEPARATOR}${name}` } -/** - * This can be used with the db.allDocs to get a list of IDs - */ -export function getMultiIDParams(ids: string[]) { - return { - keys: ids, - include_docs: true, - } -} - /** * Generates a new view ID. * @returns The new view ID which the view doc can be stored under. diff --git a/packages/server/src/sdk/app/tables/getters.ts b/packages/server/src/sdk/app/tables/getters.ts index a7074f95b2..a09d686b6d 100644 --- a/packages/server/src/sdk/app/tables/getters.ts +++ b/packages/server/src/sdk/app/tables/getters.ts @@ -1,5 +1,5 @@ import { context } from "@budibase/backend-core" -import { getMultiIDParams, getTableParams } from "../../../db/utils" +import { getTableParams } from "../../../db/utils" import { breakExternalTableId, isExternalTableID, @@ -17,6 +17,9 @@ import datasources from "../datasources" import sdk from "../../../sdk" export function processTable(table: Table): Table { + if (!table) { + return table + } if (table._id && isExternalTableID(table._id)) { return { ...table, @@ -73,6 +76,9 @@ export async function getExternalTable( tableName: string ): Promise { const entities = await getExternalTablesInDatasource(datasourceId) + if (!entities[tableName]) { + throw new Error(`Unable to find table named "${tableName}"`) + } return processTable(entities[tableName]) } @@ -124,10 +130,8 @@ export async function getTables(tableIds: string[]): Promise { } if (internalTableIds.length) { const db = context.getAppDB() - const internalTableDocs = await db.allDocs
( - getMultiIDParams(internalTableIds) - ) - tables = tables.concat(internalTableDocs.rows.map(row => row.doc!)) + const internalTables = await db.getMultiple
(internalTableIds) + tables = tables.concat(internalTables) } return processTables(tables) } diff --git a/packages/server/src/utilities/global.ts b/packages/server/src/utilities/global.ts index cdc2d84513..762d1fb7a0 100644 --- a/packages/server/src/utilities/global.ts +++ b/packages/server/src/utilities/global.ts @@ -1,4 +1,4 @@ -import { getMultiIDParams, getGlobalIDFromUserMetadataID } from "../db/utils" +import { getGlobalIDFromUserMetadataID } from "../db/utils" import { roles, db as dbCore, @@ -96,9 +96,7 @@ export async function getRawGlobalUsers(userIds?: string[]): Promise { const db = tenancy.getGlobalDB() let globalUsers: User[] if (userIds) { - globalUsers = (await db.allDocs(getMultiIDParams(userIds))).rows.map( - row => row.doc! - ) + globalUsers = await db.getMultiple(userIds) } else { globalUsers = ( await db.allDocs( diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index 26807d99ce..9ea854b3aa 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -122,7 +122,8 @@ export interface Database { exists(): Promise checkSetup(): Promise> - get(id?: string): Promise + get(id?: string): Promise + getMultiple(ids: string[]): Promise remove( id: string | Document, rev?: string From b29cfc600cf628fae8b4740bdabc530cbb8f2e21 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 14:51:07 +0000 Subject: [PATCH 21/67] Move Invite and PasswordReset code into backend-core. --- packages/backend-core/src/redis/index.ts | 2 + packages/backend-core/src/redis/invite.ts | 96 ++++++++++ .../backend-core/src/redis/passwordReset.ts | 51 ++++++ packages/backend-core/src/users/lookup.ts | 4 + .../src/api/controllers/global/users.ts | 2 +- packages/worker/src/index.ts | 3 - packages/worker/src/sdk/auth/auth.ts | 4 +- packages/worker/src/sdk/users/users.ts | 7 +- packages/worker/src/utilities/email.ts | 3 +- packages/worker/src/utilities/redis.ts | 172 ------------------ 10 files changed, 159 insertions(+), 185 deletions(-) create mode 100644 packages/backend-core/src/redis/invite.ts create mode 100644 packages/backend-core/src/redis/passwordReset.ts delete mode 100644 packages/worker/src/utilities/redis.ts diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 6585d6e4fa..8d153ea5a1 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,3 +4,5 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" +export * as invite from "./invite" +export * as passwordReset from "./passwordReset" diff --git a/packages/backend-core/src/redis/invite.ts b/packages/backend-core/src/redis/invite.ts new file mode 100644 index 0000000000..db36d3dfa6 --- /dev/null +++ b/packages/backend-core/src/redis/invite.ts @@ -0,0 +1,96 @@ +import { redis, utils, tenancy } from "../" +import env from "../environment" + +const TTL_SECONDS = 60 * 60 * 24 * 7 + +interface Invite { + email: string + info: any +} + +interface InviteWithCode extends Invite { + code: string +} + +let client: redis.Client + +async function getClient(): Promise { + if (!client) { + client = new redis.Client(redis.utils.Databases.INVITATIONS) + await client.init() + } + return client +} + +/** + * Given an invite code and invite body, allow the update an existing/valid invite in redis + * @param inviteCode The invite code for an invite in redis + * @param value The body of the updated user invitation + */ +export async function updateInviteCode(code: string, value: Invite) { + const client = await getClient() + await client.store(code, value, TTL_SECONDS) +} + +/** + * Generates an invitation code and writes it to redis - which can later be checked for user creation. + * @param email the email address which the code is being sent to (for use later). + * @param info Information to be carried along with the invitation. + * @return returns the code that was stored to redis. + */ +export async function createInviteCode( + email: string, + info: any +): Promise { + const client = await getClient() + const code = utils.newid() + await client.store(code, { email, info }, TTL_SECONDS) + return code +} + +/** + * Checks that the provided invite code is valid - will return the email address of user that was invited. + * @param inviteCode the invite code that was provided as part of the link. + * @return If the code is valid then an email address will be returned. + */ +export async function getInviteCode(code: string): Promise { + const client = await getClient() + const value = (await client.get(code)) as Invite | undefined + if (!value) { + throw "Invitation is not valid or has expired, please request a new one." + } + return value +} + +export async function deleteInviteCode(code: string) { + const client = await getClient() + await client.delete(code) +} + +/** + Get all currently available user invitations for the current tenant. +**/ +export async function getInviteCodes(): Promise { + const client = await getClient() + const invites: { key: string; value: Invite }[] = await client.scan() + + const results: InviteWithCode[] = invites.map(invite => { + return { + ...invite.value, + code: invite.key, + } + }) + if (!env.MULTI_TENANCY) { + return results + } + const tenantId = tenancy.getTenantId() + return results.filter(invite => tenantId === invite.info.tenantId) +} + +export async function getExistingInvites( + emails: string[] +): Promise { + return (await getInviteCodes()).filter(invite => + emails.includes(invite.email) + ) +} diff --git a/packages/backend-core/src/redis/passwordReset.ts b/packages/backend-core/src/redis/passwordReset.ts new file mode 100644 index 0000000000..63c3371bba --- /dev/null +++ b/packages/backend-core/src/redis/passwordReset.ts @@ -0,0 +1,51 @@ +import { redis, utils } from "../" + +const TTL_SECONDS = 60 * 60 + +interface PasswordReset { + userId: string + info: any +} + +let client: redis.Client + +async function getClient(): Promise { + if (!client) { + client = new redis.Client(redis.utils.Databases.PW_RESETS) + await client.init() + } + return client +} + +/** + * Given a user ID this will store a code (that is returned) for an hour in redis. + * The user can then return this code for resetting their password (through their reset link). + * @param userId the ID of the user which is to be reset. + * @param info Info about the user/the reset process. + * @return returns the code that was stored to redis. + */ +export async function createResetPasswordCode( + userId: string, + info: any +): Promise { + const client = await getClient() + const code = utils.newid() + await client.store(code, { userId, info }, TTL_SECONDS) + return code +} + +/** + * Given a reset code this will lookup to redis, check if the code is valid. + * @param code The code provided via the email link. + * @return returns the user ID if it is found + */ +export async function getResetPasswordCode( + code: string +): Promise { + const client = await getClient() + const value = (await client.get(code)) as PasswordReset | undefined + if (!value) { + throw "Provided information is not valid, cannot reset password - please try again." + } + return value +} diff --git a/packages/backend-core/src/users/lookup.ts b/packages/backend-core/src/users/lookup.ts index 17d0e91d88..2c0c66276a 100644 --- a/packages/backend-core/src/users/lookup.ts +++ b/packages/backend-core/src/users/lookup.ts @@ -6,6 +6,7 @@ import { } from "@budibase/types" import * as dbUtils from "../db" import { ViewName } from "../constants" +import { getExistingInvites } from "../redis/invite" /** * Apply a system-wide search on emails: @@ -26,6 +27,9 @@ export async function searchExistingEmails(emails: string[]) { const existingAccounts = await getExistingAccounts(emails) matchedEmails.push(...existingAccounts.map(account => account.email)) + const invitedEmails = await getExistingInvites(emails) + matchedEmails.push(...invitedEmails.map(invite => invite.email)) + return [...new Set(matchedEmails.map(email => email.toLowerCase()))] } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index fae42acdfe..8fb2b5675d 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -3,7 +3,7 @@ import { deleteInviteCode, getInviteCodes, updateInviteCode, -} from "../../../utilities/redis" +} from "@budibase/backend-core/src/redis/invite" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 4b1d11ecf7..1c101983df 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -22,7 +22,6 @@ import Koa from "koa" import koaBody from "koa-body" import http from "http" import api from "./api" -import * as redis from "./utilities/redis" const koaSession = require("koa-session") import { userAgent } from "koa-useragent" @@ -72,7 +71,6 @@ server.on("close", async () => { shuttingDown = true console.log("Server Closed") timers.cleanup() - await redis.shutdown() await events.shutdown() await queue.shutdown() if (!env.isTest()) { @@ -88,7 +86,6 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) await initPro() - await redis.init() // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues await events.processors.init(proSdk.auditLogs.write) diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index e25d34fa5e..57131294b3 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -6,12 +6,12 @@ import { sessions, tenancy, utils as coreUtils, + redis, } from "@budibase/backend-core" import { PlatformLogoutOpts, User } from "@budibase/types" import jwt from "jsonwebtoken" import * as userSdk from "../users" import * as emails from "../../utilities/email" -import * as redis from "../../utilities/redis" import { EmailTemplatePurpose } from "../../constants" // LOGIN / LOGOUT @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.getResetPasswordCode(resetCode) + const { userId } = await redis.passwordReset.getResetPasswordCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 4cca0b8fa6..9dc1bac0e9 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -6,7 +6,6 @@ import { } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" -import { getInviteCodes } from "../..//utilities/redis" export async function invite( users: InviteUsersRequest @@ -19,15 +18,11 @@ export async function invite( const matchedEmails = await usersCore.searchExistingEmails( users.map(u => u.email) ) - const invitedEmails = (await getInviteCodes()).map(invite => invite.email) const newUsers: InviteUserRequest[] = [] // separate duplicates from new users for (let user of users) { - if ( - matchedEmails.includes(user.email) || - invitedEmails.includes(user.email) - ) { + if (matchedEmails.includes(user.email)) { // This "Unavailable" is load bearing. The tests and frontend both check for it // specifically response.unsuccessful.push({ email: user.email, reason: "Unavailable" }) diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index a0c02d335c..f3f1fd40c4 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -3,7 +3,8 @@ import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" -import { createResetPasswordCode, createInviteCode } from "./redis" +import { createResetPasswordCode } from "@budibase/backend-core/src/redis/passwordReset" +import { createInviteCode } from "@budibase/backend-core/src/redis/invite" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" import ical from "ical-generator" diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts deleted file mode 100644 index 337d34e376..0000000000 --- a/packages/worker/src/utilities/redis.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { redis, utils, tenancy } from "@budibase/backend-core" -import env from "../environment" - -interface Invite { - email: string - info: any -} - -interface InviteWithCode extends Invite { - code: string -} - -interface PasswordReset { - userId: string - info: any -} - -type RedisDBName = - | redis.utils.Databases.PW_RESETS - | redis.utils.Databases.INVITATIONS -let pwResetClient: redis.Client, invitationClient: redis.Client - -export async function init() { - pwResetClient = new redis.Client(redis.utils.Databases.PW_RESETS) - invitationClient = new redis.Client(redis.utils.Databases.INVITATIONS) - await pwResetClient.init() - await invitationClient.init() -} - -export async function shutdown() { - if (pwResetClient) await pwResetClient.finish() - if (invitationClient) await invitationClient.finish() - // shutdown core clients - await redis.clients.shutdown() - console.log("Redis shutdown") -} - -function getExpirySecondsForDB(db: RedisDBName) { - switch (db) { - case redis.utils.Databases.PW_RESETS: - // a hour - return 3600 - case redis.utils.Databases.INVITATIONS: - // a week - return 604800 - default: - throw new Error(`Unknown redis database: ${db}`) - } -} - -function getClient(db: RedisDBName): redis.Client { - switch (db) { - case redis.utils.Databases.PW_RESETS: - return pwResetClient - case redis.utils.Databases.INVITATIONS: - return invitationClient - default: - throw new Error(`Unknown redis database: ${db}`) - } -} - -async function writeCode(db: RedisDBName, value: Invite | PasswordReset) { - const client = getClient(db) - const code = utils.newid() - await client.store(code, value, getExpirySecondsForDB(db)) - return code -} - -async function updateCode( - db: RedisDBName, - code: string, - value: Invite | PasswordReset -) { - const client = getClient(db) - await client.store(code, value, getExpirySecondsForDB(db)) -} - -/** - * Given an invite code and invite body, allow the update an existing/valid invite in redis - * @param inviteCode The invite code for an invite in redis - * @param value The body of the updated user invitation - */ -export async function updateInviteCode(code: string, value: Invite) { - await updateCode(redis.utils.Databases.INVITATIONS, code, value) -} - -async function deleteCode(db: RedisDBName, code: string) { - const client = getClient(db) - await client.delete(code) -} - -async function getCode(db: RedisDBName, code: string) { - const client = getClient(db) - const value = await client.get(code) - if (!value) { - throw new Error(`Could not find code: ${code}`) - } - return value -} - -/** - * Given a user ID this will store a code (that is returned) for an hour in redis. - * The user can then return this code for resetting their password (through their reset link). - * @param userId the ID of the user which is to be reset. - * @param info Info about the user/the reset process. - * @return returns the code that was stored to redis. - */ -export async function createResetPasswordCode(userId: string, info: any) { - return writeCode(redis.utils.Databases.PW_RESETS, { userId, info }) -} - -/** - * Given a reset code this will lookup to redis, check if the code is valid. - * @param resetCode The code provided via the email link. - * @return returns the user ID if it is found - */ -export async function getResetPasswordCode( - code: string -): Promise { - try { - return getCode(redis.utils.Databases.PW_RESETS, code) - } catch (err) { - throw "Provided information is not valid, cannot reset password - please try again." - } -} - -/** - * Generates an invitation code and writes it to redis - which can later be checked for user creation. - * @param email the email address which the code is being sent to (for use later). - * @param info Information to be carried along with the invitation. - * @return returns the code that was stored to redis. - */ -export async function createInviteCode(email: string, info: any) { - return writeCode(redis.utils.Databases.INVITATIONS, { email, info }) -} - -/** - * Checks that the provided invite code is valid - will return the email address of user that was invited. - * @param inviteCode the invite code that was provided as part of the link. - * @return If the code is valid then an email address will be returned. - */ -export async function getInviteCode(code: string): Promise { - try { - return getCode(redis.utils.Databases.INVITATIONS, code) - } catch (err) { - throw "Invitation is not valid or has expired, please request a new one." - } -} - -export async function deleteInviteCode(code: string) { - return deleteCode(redis.utils.Databases.INVITATIONS, code) -} - -/** - Get all currently available user invitations for the current tenant. -**/ -export async function getInviteCodes(): Promise { - const client = getClient(redis.utils.Databases.INVITATIONS) - const invites: { key: string; value: Invite }[] = await client.scan() - - const results: InviteWithCode[] = invites.map(invite => { - return { - ...invite.value, - code: invite.key, - } - }) - if (!env.MULTI_TENANCY) { - return results - } - const tenantId = tenancy.getTenantId() - return results.filter(invite => tenantId === invite.info.tenantId) -} From 37e34c8ed2e79c1c5981a95e6402736643d94d1e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 9 Nov 2023 14:53:14 +0000 Subject: [PATCH 22/67] Adding the ability to fail on getMultiple if needed. --- packages/backend-core/src/db/couch/DatabaseImpl.ts | 14 ++++++++++++-- packages/types/src/sdk/db.ts | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 96b946fe16..fa505847af 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -117,14 +117,24 @@ export class DatabaseImpl implements Database { return this.updateOutput(() => db.get(id)) } - async getMultiple(ids: string[]): Promise { + async getMultiple( + ids: string[], + opts?: { failIfMissing?: boolean } + ): Promise { // get unique ids = [...new Set(ids)] const response = await this.allDocs({ keys: ids, include_docs: true, }) - const rows = response.rows.filter(row => row.error !== "not_found") + const NOT_FOUND = "not_found" + const rows = response.rows.filter(row => row.error !== NOT_FOUND) + // some were filtered out - means some missing + if (opts?.failIfMissing && rows.length !== response.rows.length) { + const missing = response.rows.filter(row => row.error === NOT_FOUND) + const missingIds = missing.map(row => row.key).join(", ") + throw new Error(`Unable to get documents: ${missingIds}`) + } return rows.map(row => row.doc!) } diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index 9ea854b3aa..ff5742bcfc 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -123,7 +123,10 @@ export interface Database { exists(): Promise checkSetup(): Promise> get(id?: string): Promise - getMultiple(ids: string[]): Promise + getMultiple( + ids: string[], + opts?: { failIfMissing?: boolean } + ): Promise remove( id: string | Document, rev?: string From 822c03b0ef2d2bbbba996831a4c5cedf19e0d0bc Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 15:02:44 +0000 Subject: [PATCH 23/67] Refactor onboardUsers endpoint. --- packages/types/src/api/web/user.ts | 1 + .../src/api/controllers/global/users.ts | 79 ++++++------------- 2 files changed, 27 insertions(+), 53 deletions(-) diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 3a5bd16bdf..6db70f20d0 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -10,6 +10,7 @@ export interface SaveUserResponse { export interface UserDetails { _id: string email: string + password?: string } export interface BulkUserRequest { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 8fb2b5675d..6608fcde05 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -251,58 +251,32 @@ export const tenantUserLookup = async (ctx: any) => { Encapsulate the app user onboarding flows here. */ export const onboardUsers = async (ctx: Ctx) => { - const request = ctx.request.body - const isBulkCreate = "create" in request - - const emailConfigured = await isEmailConfigured() - - let onboardingResponse - - if (isBulkCreate) { - // @ts-ignore - const { users, groups, roles } = request.create - const assignUsers = users.map((user: User) => (user.roles = roles)) - onboardingResponse = await userSdk.db.bulkCreate(assignUsers, groups) - ctx.body = onboardingResponse - } else if (emailConfigured) { - onboardingResponse = await inviteMultiple(ctx) - } else if (!emailConfigured) { - const inviteRequest = ctx.request.body - - let createdPasswords: any = {} - - const users: User[] = inviteRequest.map(invite => { - let password = Math.random().toString(36).substring(2, 22) - - // Temp password to be passed to the user. - createdPasswords[invite.email] = password - - return { - email: invite.email, - password, - forceResetPassword: true, - roles: invite.userInfo.apps, - admin: invite.userInfo.admin, - builder: invite.userInfo.builder, - tenantId: tenancy.getTenantId(), - } - }) - let bulkCreateReponse = await userSdk.db.bulkCreate(users, []) - - // Apply temporary credentials - ctx.body = { - ...bulkCreateReponse, - successful: bulkCreateReponse?.successful.map(user => { - return { - ...user, - password: createdPasswords[user.email], - } - }), - created: true, - } - } else { - ctx.throw(400, "User onboarding failed") + if (await isEmailConfigured()) { + await inviteMultiple(ctx) + return } + + let createdPasswords: Record = {} + const users: User[] = ctx.request.body.map(invite => { + let password = Math.random().toString(36).substring(2, 22) + createdPasswords[invite.email] = password + + return { + email: invite.email, + password, + forceResetPassword: true, + roles: invite.userInfo.apps, + admin: invite.userInfo.admin, + builder: invite.userInfo.builder, + tenantId: tenancy.getTenantId(), + } + }) + + let resp = await userSdk.db.bulkCreate(users, []) + resp.successful.forEach(user => { + user.password = createdPasswords[user.email] + }) + ctx.body = { ...resp, created: true } } export const invite = async (ctx: Ctx) => { @@ -329,8 +303,7 @@ export const invite = async (ctx: Ctx) => { } export const inviteMultiple = async (ctx: Ctx) => { - const request = ctx.request.body - ctx.body = await userSdk.invite(request) + ctx.body = await userSdk.invite(ctx.request.body) } export const checkInvite = async (ctx: any) => { From 7f530eeab5970feda052576ad3c786b1f25673cf Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 15:13:59 +0000 Subject: [PATCH 24/67] Add tests for the onboarding endpoint. --- packages/backend-core/src/users/db.ts | 4 ++-- packages/types/src/api/web/user.ts | 1 + .../src/api/controllers/global/users.ts | 11 ++++++---- .../src/api/routes/global/tests/users.spec.ts | 21 +++++++++++++++++++ packages/worker/src/tests/api/users.ts | 21 +++++++++++++++++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 59f698d99c..bd85097bbd 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -303,7 +303,7 @@ export class UserDB { static async bulkCreate( newUsersRequested: User[], - groups: string[] + groups?: string[] ): Promise { const tenantId = getTenantId() @@ -328,7 +328,7 @@ export class UserDB { }) continue } - newUser.userGroups = groups + newUser.userGroups = groups || [] newUsers.push(newUser) if (isCreator(newUser)) { newCreators.push(newUser) diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 6db70f20d0..0de42622e6 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -50,6 +50,7 @@ export type InviteUsersRequest = InviteUserRequest[] export interface InviteUsersResponse { successful: { email: string }[] unsuccessful: { email: string; reason: string }[] + created?: boolean } export interface SearchUsersRequest { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 6608fcde05..e904567674 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -17,6 +17,7 @@ import { Ctx, InviteUserRequest, InviteUsersRequest, + InviteUsersResponse, MigrationType, SaveUserResponse, SearchUsersRequest, @@ -250,7 +251,9 @@ export const tenantUserLookup = async (ctx: any) => { /* Encapsulate the app user onboarding flows here. */ -export const onboardUsers = async (ctx: Ctx) => { +export const onboardUsers = async ( + ctx: Ctx +) => { if (await isEmailConfigured()) { await inviteMultiple(ctx) return @@ -272,10 +275,10 @@ export const onboardUsers = async (ctx: Ctx) => { } }) - let resp = await userSdk.db.bulkCreate(users, []) - resp.successful.forEach(user => { + let resp = await userSdk.db.bulkCreate(users) + for (const user of resp.successful) { user.password = createdPasswords[user.email] - }) + } ctx.body = { ...resp, created: true } } diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index ce2c9347b4..a85933255a 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -669,4 +669,25 @@ describe("/api/global/users", () => { expect(response.body.message).toBe("Unable to delete self.") }) }) + + describe("POST /api/global/users/onboard", () => { + it("should successfully onboard a user", async () => { + const response = await config.api.users.onboardUser([ + { email: structures.users.newEmail(), userInfo: {} }, + ]) + expect(response.successful.length).toBe(1) + expect(response.unsuccessful.length).toBe(0) + }) + + it("should not onboard a user who has been invited", async () => { + const email = structures.users.newEmail() + await config.api.users.sendUserInvite(sendMailMock, email) + + const response = await config.api.users.onboardUser([ + { email, userInfo: {} }, + ]) + expect(response.successful.length).toBe(0) + expect(response.unsuccessful.length).toBe(1) + }) + }) }) diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index ca25e2f9ca..5ecd1447ca 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -5,6 +5,7 @@ import { User, CreateAdminUserRequest, SearchQuery, + InviteUsersResponse, } from "@budibase/types" import structures from "../structures" import { generator } from "@budibase/backend-core/tests" @@ -176,4 +177,24 @@ export class UserAPI extends TestAPI { .expect("Content-Type", /json/) .expect(200) } + + onboardUser = async ( + req: InviteUsersRequest + ): Promise => { + const resp = await this.request + .post(`/api/global/users/onboard`) + .send(req) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + + if (resp.status !== 200) { + throw new Error( + `request failed with status ${resp.status} and body ${JSON.stringify( + resp.body + )}` + ) + } + + return resp.body as InviteUsersResponse + } } From 419c5ced70d9f25cc037132b2c6737976b59af5f Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 9 Nov 2023 15:31:20 +0000 Subject: [PATCH 25/67] fix issue with relationship selection --- .../Datasources/CreateEditRelationship.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte index f6621c1508..abdc95a6da 100644 --- a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte +++ b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte @@ -33,6 +33,10 @@ part1: PrettyRelationshipDefinitions.MANY, part2: PrettyRelationshipDefinitions.ONE, }, + [RelationshipType.MANY_TO_ONE]: { + part1: PrettyRelationshipDefinitions.ONE, + part2: PrettyRelationshipDefinitions.MANY, + }, } let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions) let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions) @@ -58,7 +62,7 @@ let fromPrimary, fromForeign, fromColumn, toColumn let throughId, throughToKey, throughFromKey - let isManyToMany, isManyToOne, relationshipType + let relationshipType let hasValidated = false $: fromId = null @@ -85,8 +89,10 @@ $: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType) $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY - $: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE - + $: isManyToOne = + relationshipType === RelationshipType.MANY_TO_ONE || + relationshipType === RelationshipType.ONE_TO_MANY + $: console.log(relationshipType) function getTable(id) { return plusTables.find(table => table._id === id) } From dde446286de6922d79953881b11abb1033d33e0e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 9 Nov 2023 17:08:14 +0000 Subject: [PATCH 26/67] Switching getMultiple to default to failure if not all entries found, then updating usages. --- packages/backend-core/src/db/couch/DatabaseImpl.ts | 5 +++-- packages/server/src/api/controllers/row/internal.ts | 3 ++- packages/server/src/db/linkedRows/index.ts | 2 +- packages/server/src/sdk/app/tables/getters.ts | 4 +++- packages/server/src/utilities/global.ts | 2 +- packages/types/src/sdk/db.ts | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index fa505847af..6a1e575ac9 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -119,7 +119,7 @@ export class DatabaseImpl implements Database { async getMultiple( ids: string[], - opts?: { failIfMissing?: boolean } + opts?: { allowMissing?: boolean } ): Promise { // get unique ids = [...new Set(ids)] @@ -129,8 +129,9 @@ export class DatabaseImpl implements Database { }) const NOT_FOUND = "not_found" const rows = response.rows.filter(row => row.error !== NOT_FOUND) + const someMissing = rows.length !== response.rows.length // some were filtered out - means some missing - if (opts?.failIfMissing && rows.length !== response.rows.length) { + if (!opts?.allowMissing && someMissing) { const missing = response.rows.filter(row => row.error === NOT_FOUND) const missingIds = missing.map(row => row.key).join(", ") throw new Error(`Unable to get documents: ${missingIds}`) diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts index 0edbe4ea86..0907c22f0e 100644 --- a/packages/server/src/api/controllers/row/internal.ts +++ b/packages/server/src/api/controllers/row/internal.ts @@ -237,7 +237,8 @@ export async function fetchEnrichedRow(ctx: UserCtx) { // look up the actual rows based on the ids let linkedRows = await db.getMultiple( - linkVals.map(linkVal => linkVal.id) + linkVals.map(linkVal => linkVal.id), + { allowMissing: true } ) // get the linked tables diff --git a/packages/server/src/db/linkedRows/index.ts b/packages/server/src/db/linkedRows/index.ts index 24ff47ead3..7af3f9392f 100644 --- a/packages/server/src/db/linkedRows/index.ts +++ b/packages/server/src/db/linkedRows/index.ts @@ -79,7 +79,7 @@ async function getFullLinkedDocs(links: LinkDocumentValue[]) { const db = context.getAppDB() const linkedRowIds = links.map(link => link.id) const uniqueRowIds = [...new Set(linkedRowIds)] - let dbRows = await db.getMultiple(uniqueRowIds) + let dbRows = await db.getMultiple(uniqueRowIds, { allowMissing: true }) // convert the unique db rows back to a full list of linked rows const linked = linkedRowIds .map(id => dbRows.find(row => row && row._id === id)) diff --git a/packages/server/src/sdk/app/tables/getters.ts b/packages/server/src/sdk/app/tables/getters.ts index a09d686b6d..72a6ab61f1 100644 --- a/packages/server/src/sdk/app/tables/getters.ts +++ b/packages/server/src/sdk/app/tables/getters.ts @@ -130,7 +130,9 @@ export async function getTables(tableIds: string[]): Promise { } if (internalTableIds.length) { const db = context.getAppDB() - const internalTables = await db.getMultiple
(internalTableIds) + const internalTables = await db.getMultiple
(internalTableIds, { + allowMissing: true, + }) tables = tables.concat(internalTables) } return processTables(tables) diff --git a/packages/server/src/utilities/global.ts b/packages/server/src/utilities/global.ts index 762d1fb7a0..bbb84c1882 100644 --- a/packages/server/src/utilities/global.ts +++ b/packages/server/src/utilities/global.ts @@ -96,7 +96,7 @@ export async function getRawGlobalUsers(userIds?: string[]): Promise { const db = tenancy.getGlobalDB() let globalUsers: User[] if (userIds) { - globalUsers = await db.getMultiple(userIds) + globalUsers = await db.getMultiple(userIds, { allowMissing: true }) } else { globalUsers = ( await db.allDocs( diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index ff5742bcfc..7613ac6aeb 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -125,7 +125,7 @@ export interface Database { get(id?: string): Promise getMultiple( ids: string[], - opts?: { failIfMissing?: boolean } + opts?: { allowMissing?: boolean } ): Promise remove( id: string | Document, From 77f0eebdfdb5afec83bf1fc75d49b19209c81ed0 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 9 Nov 2023 17:30:40 +0000 Subject: [PATCH 27/67] Removing stray console.log. --- .../components/backend/Datasources/CreateEditRelationship.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte index abdc95a6da..3f2253c754 100644 --- a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte +++ b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte @@ -92,7 +92,6 @@ $: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE || relationshipType === RelationshipType.ONE_TO_MANY - $: console.log(relationshipType) function getTable(id) { return plusTables.find(table => table._id === id) } From b68607b0485844e004a2a481300048421d0ef33d Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:21:28 +0000 Subject: [PATCH 28/67] Redis query command doesn't accept spaced values (#12357) * Handle string phrase with spaces value * Unit test --- packages/server/src/integrations/redis.ts | 14 +++++++++++++- .../server/src/integrations/tests/redis.spec.ts | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/server/src/integrations/redis.ts b/packages/server/src/integrations/redis.ts index 879a790550..6a6331ccd4 100644 --- a/packages/server/src/integrations/redis.ts +++ b/packages/server/src/integrations/redis.ts @@ -165,10 +165,22 @@ class RedisIntegration { // commands split line by line const commands = query.json.trim().split("\n") let pipelineCommands = [] + let tokenised // process each command separately for (let command of commands) { - const tokenised = command.trim().split(" ") + const valueToken = command.trim().match(/".*"/) + if (valueToken?.[0]) { + tokenised = [ + ...command + .substring(0, command.indexOf(valueToken[0]) - 1) + .trim() + .split(" "), + valueToken?.[0], + ] + } else { + tokenised = command.trim().split(" ") + } // Pipeline only accepts lower case commands tokenised[0] = tokenised[0].toLowerCase() pipelineCommands.push(tokenised) diff --git a/packages/server/src/integrations/tests/redis.spec.ts b/packages/server/src/integrations/tests/redis.spec.ts index 9521d58a51..942da99530 100644 --- a/packages/server/src/integrations/tests/redis.spec.ts +++ b/packages/server/src/integrations/tests/redis.spec.ts @@ -85,4 +85,21 @@ describe("Redis Integration", () => { ["get", "foo"], ]) }) + + it("calls the pipeline method with double quoted phrase values", async () => { + const body = { + json: 'SET foo "What a wonderful world!"\nGET foo', + } + + // ioredis-mock doesn't support pipelines + config.integration.client.pipeline = jest.fn(() => ({ + exec: jest.fn(() => [[]]), + })) + + await config.integration.command(body) + expect(config.integration.client.pipeline).toHaveBeenCalledWith([ + ["set", "foo", '"What a wonderful world!"'], + ["get", "foo"], + ]) + }) }) From 20895cf4266f3accc30aba8bfd5678793195de04 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 9 Nov 2023 18:22:06 +0000 Subject: [PATCH 29/67] Adding test case. --- packages/server/src/sdk/tests/tables.spec.ts | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/server/src/sdk/tests/tables.spec.ts diff --git a/packages/server/src/sdk/tests/tables.spec.ts b/packages/server/src/sdk/tests/tables.spec.ts new file mode 100644 index 0000000000..0e3cd73cfd --- /dev/null +++ b/packages/server/src/sdk/tests/tables.spec.ts @@ -0,0 +1,39 @@ +import TestConfig from "../../tests/utilities/TestConfiguration" +import { basicTable } from "../../tests/utilities/structures" +import { Table } from "@budibase/types" +import sdk from "../" + +describe("tables", () => { + const config = new TestConfig() + let table: Table + + beforeAll(async () => { + await config.init() + table = await config.api.table.create(basicTable()) + }) + + describe("getTables", () => { + it("should be able to retrieve tables", async () => { + await config.doInContext(config.appId, async () => { + const tables = await sdk.tables.getTables([table._id!]) + expect(tables.length).toBe(1) + expect(tables[0]._id).toBe(table._id) + expect(tables[0].name).toBe(table.name) + }) + }) + + it("shouldn't fail when retrieving tables that don't exist", async () => { + await config.doInContext(config.appId, async () => { + const tables = await sdk.tables.getTables(["unknown"]) + expect(tables.length).toBe(0) + }) + }) + + it("should de-duplicate the IDs", async () => { + await config.doInContext(config.appId, async () => { + const tables = await sdk.tables.getTables([table._id!, table._id!]) + expect(tables.length).toBe(1) + }) + }) + }) +}) From bcfab2d0a30655f6a959e584f003557ecd85783d Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 10 Nov 2023 08:03:07 +0000 Subject: [PATCH 30/67] Bump version to 2.13.7 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index ce3128165f..c0559d8346 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.6", + "version": "2.13.7", "npmClient": "yarn", "packages": [ "packages/*" From 4fd9d2262ec0433580e81b55b838ea12646e1179 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 10 Nov 2023 09:48:19 +0000 Subject: [PATCH 31/67] PR Feedback --- .../FieldConfiguration.svelte | 13 +------------ .../Component/ComponentSettingsSection.svelte | 19 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte index 2211f4a41b..965c1c2881 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte @@ -9,7 +9,7 @@ } from "builderStore/dataBinding" import { currentAsset } from "builderStore" import DraggableList from "../DraggableList/DraggableList.svelte" - import { createEventDispatcher, getContext } from "svelte" + import { createEventDispatcher } from "svelte" import { store, selectedScreen } from "builderStore" import FieldSetting from "./FieldSetting.svelte" import { convertOldFieldFormat, getComponentForField } from "./utils" @@ -17,8 +17,6 @@ export let componentInstance export let value - const updates = getContext("settings") - const dispatch = createEventDispatcher() let sanitisedFields let fieldList @@ -29,7 +27,6 @@ let unconfigured let selectAll = true - let updating = false $: bindings = getBindableProperties($selectedScreen, componentInstance._id) $: actionType = componentInstance.actionType @@ -49,10 +46,6 @@ cachedValue = cloneDeep(value) } - $: if (typeof $updates.resp == "string") { - updating = false - } - const updateState = value => { schema = getSchema($currentAsset, datasource) options = Object.keys(schema || {}) @@ -161,7 +154,6 @@ } const toggleAll = update => { - updating = true listUpdated(update) } @@ -180,7 +172,6 @@ text="" bind:value={selectAll} thin - disabled={updating} /> {#if fieldList?.length} @@ -193,9 +184,7 @@ listTypeProps={{ componentBindings, bindings, - updating, }} - draggable={!updating} /> {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte index 3483f6d26f..65f010e4ec 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte @@ -8,8 +8,6 @@ import { getComponentForSetting } from "components/design/settings/componentSettings" import InfoDisplay from "./InfoDisplay.svelte" import analytics, { Events } from "analytics" - import { setContext } from "svelte" - import { writable } from "svelte/store" export let componentDefinition export let componentInstance @@ -21,21 +19,6 @@ export let includeHidden = false export let tag - let status = writable({ - status: null, - lastUpdate: null, - }) - - const updateStatus = resp => { - status.update(state => ({ - ...state, - resp, - lastUpdate: new Date().getTime(), - })) - } - - setContext("settings", status) - $: sections = getSections( componentInstance, componentDefinition, @@ -93,7 +76,6 @@ } else { await store.actions.components.updateSetting(setting.key, value) } - updateStatus("success") // Send event if required if (setting.sendEvents) { analytics.captureEvent(Events.COMPONENT_UPDATED, { @@ -103,7 +85,6 @@ }) } } catch (error) { - updateStatus("failed") notifications.error("Error updating component prop") } } From 6282582bb0426b7e473db02287df4b21da9e68a9 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 10 Nov 2023 11:02:50 +0000 Subject: [PATCH 32/67] PR comments. --- .../backend/Datasources/CreateEditRelationship.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte index 3f2253c754..af678a88ba 100644 --- a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte +++ b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte @@ -33,7 +33,7 @@ part1: PrettyRelationshipDefinitions.MANY, part2: PrettyRelationshipDefinitions.ONE, }, - [RelationshipType.MANY_TO_ONE]: { + [RelationshipType.ONE_TO_MANY]: { part1: PrettyRelationshipDefinitions.ONE, part2: PrettyRelationshipDefinitions.MANY, }, From d98e217c6c1ba90ccddfc9833609e145ed6785f5 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 11:21:36 +0000 Subject: [PATCH 33/67] Fix backend-core redis imports. --- packages/backend-core/src/redis/index.ts | 5 +++-- .../src/api/controllers/global/users.ts | 19 +++++++------------ packages/worker/src/sdk/auth/auth.ts | 2 +- packages/worker/src/utilities/email.ts | 4 ++-- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 8d153ea5a1..419f1db700 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,5 +4,6 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" -export * as invite from "./invite" -export * as passwordReset from "./passwordReset" + +export * from "./invite" +export * from "./passwordReset" diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index e904567674..cd11fc74b6 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,9 +1,4 @@ -import { - getInviteCode, - deleteInviteCode, - getInviteCodes, - updateInviteCode, -} from "@budibase/backend-core/src/redis/invite" +import { redis } from "@budibase/backend-core" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { @@ -313,7 +308,7 @@ export const checkInvite = async (ctx: any) => { const { code } = ctx.params let invite try { - invite = await getInviteCode(code) + invite = await redis.getInviteCode(code) } catch (e) { console.warn("Error getting invite from code", e) ctx.throw(400, "There was a problem with the invite") @@ -327,7 +322,7 @@ export const checkInvite = async (ctx: any) => { export const getUserInvites = async (ctx: any) => { try { // Restricted to the currently authenticated tenant - ctx.body = await getInviteCodes() + ctx.body = await redis.getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } @@ -341,7 +336,7 @@ export const updateInvite = async (ctx: any) => { let invite try { - invite = await getInviteCode(code) + invite = await redis.getInviteCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") return @@ -369,7 +364,7 @@ export const updateInvite = async (ctx: any) => { } } - await updateInviteCode(code, updated) + await redis.updateInviteCode(code, updated) ctx.body = { ...invite } } @@ -379,8 +374,8 @@ export const inviteAccept = async ( const { inviteCode, password, firstName, lastName } = ctx.request.body try { // info is an extension of the user object that was stored by global - const { email, info }: any = await getInviteCode(inviteCode) - await deleteInviteCode(inviteCode) + const { email, info }: any = await redis.getInviteCode(inviteCode) + await redis.deleteInviteCode(inviteCode) const user = await tenancy.doInTenant(info.tenantId, async () => { let request: any = { firstName, diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 57131294b3..2140b89ce3 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.passwordReset.getResetPasswordCode(resetCode) + const { userId } = await redis.getResetPasswordCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index f3f1fd40c4..a4d2c296e5 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -4,7 +4,7 @@ import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" import { createResetPasswordCode } from "@budibase/backend-core/src/redis/passwordReset" -import { createInviteCode } from "@budibase/backend-core/src/redis/invite" +import { redis } from "@budibase/backend-core" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" import ical from "ical-generator" @@ -64,7 +64,7 @@ async function getLinkCode( case EmailTemplatePurpose.PASSWORD_RECOVERY: return createResetPasswordCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return createInviteCode(email, info) + return redis.createInviteCode(email, info) default: return null } From dd2f68d0990b7019ac0faa8caf4c74f09dcae07b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 11:24:55 +0000 Subject: [PATCH 34/67] Hook new Redis clients into init/shutdown flow. --- packages/backend-core/src/redis/init.ts | 22 +++++++++++++++++- packages/backend-core/src/redis/invite.ts | 23 ++++++------------- .../backend-core/src/redis/passwordReset.ts | 17 ++++---------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index 55ffe3dd12..a4f1fecc17 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -7,7 +7,9 @@ let userClient: Client, cacheClient: Client, writethroughClient: Client, lockClient: Client, - socketClient: Client + socketClient: Client, + inviteClient: Client, + passwordResetClient: Client async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() @@ -20,6 +22,8 @@ async function init() { utils.Databases.SOCKET_IO, utils.SelectableDatabase.SOCKET_IO ).init() + inviteClient = await new Client(utils.Databases.INVITATIONS).init() + passwordResetClient = await new Client(utils.Databases.PW_RESETS).init() } export async function shutdown() { @@ -30,6 +34,8 @@ export async function shutdown() { if (writethroughClient) await writethroughClient.finish() if (lockClient) await lockClient.finish() if (socketClient) await socketClient.finish() + if (inviteClient) await inviteClient.finish() + if (passwordResetClient) await passwordResetClient.finish() } process.on("exit", async () => { @@ -84,3 +90,17 @@ export async function getSocketClient() { } return socketClient } + +export async function getInviteClient() { + if (!inviteClient) { + await init() + } + return inviteClient +} + +export async function getPasswordResetClient() { + if (!passwordResetClient) { + await init() + } + return passwordResetClient +} diff --git a/packages/backend-core/src/redis/invite.ts b/packages/backend-core/src/redis/invite.ts index db36d3dfa6..6e6a1fd9e9 100644 --- a/packages/backend-core/src/redis/invite.ts +++ b/packages/backend-core/src/redis/invite.ts @@ -1,5 +1,6 @@ -import { redis, utils, tenancy } from "../" +import { utils, tenancy } from "../" import env from "../environment" +import { getInviteClient } from "./init" const TTL_SECONDS = 60 * 60 * 24 * 7 @@ -12,23 +13,13 @@ interface InviteWithCode extends Invite { code: string } -let client: redis.Client - -async function getClient(): Promise { - if (!client) { - client = new redis.Client(redis.utils.Databases.INVITATIONS) - await client.init() - } - return client -} - /** * Given an invite code and invite body, allow the update an existing/valid invite in redis * @param inviteCode The invite code for an invite in redis * @param value The body of the updated user invitation */ export async function updateInviteCode(code: string, value: Invite) { - const client = await getClient() + const client = await getInviteClient() await client.store(code, value, TTL_SECONDS) } @@ -42,7 +33,7 @@ export async function createInviteCode( email: string, info: any ): Promise { - const client = await getClient() + const client = await getInviteClient() const code = utils.newid() await client.store(code, { email, info }, TTL_SECONDS) return code @@ -54,7 +45,7 @@ export async function createInviteCode( * @return If the code is valid then an email address will be returned. */ export async function getInviteCode(code: string): Promise { - const client = await getClient() + const client = await getInviteClient() const value = (await client.get(code)) as Invite | undefined if (!value) { throw "Invitation is not valid or has expired, please request a new one." @@ -63,7 +54,7 @@ export async function getInviteCode(code: string): Promise { } export async function deleteInviteCode(code: string) { - const client = await getClient() + const client = await getInviteClient() await client.delete(code) } @@ -71,7 +62,7 @@ export async function deleteInviteCode(code: string) { Get all currently available user invitations for the current tenant. **/ export async function getInviteCodes(): Promise { - const client = await getClient() + const client = await getInviteClient() const invites: { key: string; value: Invite }[] = await client.scan() const results: InviteWithCode[] = invites.map(invite => { diff --git a/packages/backend-core/src/redis/passwordReset.ts b/packages/backend-core/src/redis/passwordReset.ts index 63c3371bba..243b73c529 100644 --- a/packages/backend-core/src/redis/passwordReset.ts +++ b/packages/backend-core/src/redis/passwordReset.ts @@ -1,4 +1,5 @@ -import { redis, utils } from "../" +import { utils } from "../" +import { getPasswordResetClient } from "./init" const TTL_SECONDS = 60 * 60 @@ -7,16 +8,6 @@ interface PasswordReset { info: any } -let client: redis.Client - -async function getClient(): Promise { - if (!client) { - client = new redis.Client(redis.utils.Databases.PW_RESETS) - await client.init() - } - return client -} - /** * Given a user ID this will store a code (that is returned) for an hour in redis. * The user can then return this code for resetting their password (through their reset link). @@ -28,7 +19,7 @@ export async function createResetPasswordCode( userId: string, info: any ): Promise { - const client = await getClient() + const client = await getPasswordResetClient() const code = utils.newid() await client.store(code, { userId, info }, TTL_SECONDS) return code @@ -42,7 +33,7 @@ export async function createResetPasswordCode( export async function getResetPasswordCode( code: string ): Promise { - const client = await getClient() + const client = await getPasswordResetClient() const value = (await client.get(code)) as PasswordReset | undefined if (!value) { throw "Provided information is not valid, cannot reset password - please try again." From 94983c289fc578033e2d20466388adf05858dfa3 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 11:39:26 +0000 Subject: [PATCH 35/67] Hook redis init flow into overall worker init flow. --- packages/backend-core/src/index.ts | 5 ++++- packages/backend-core/src/redis/index.ts | 1 + packages/backend-core/src/redis/init.ts | 2 +- packages/worker/src/db/index.ts | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index c7cf9f56cc..d41f2b9384 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -37,6 +37,7 @@ export { SearchParams } from "./db" // circular dependencies import * as context from "./context" import * as _tenancy from "./tenancy" +import * as redis from "./redis" export const tenancy = { ..._tenancy, ...context, @@ -50,6 +51,8 @@ export * from "./constants" // expose package init function import * as db from "./db" -export const init = (opts: any = {}) => { + +export const init = async (opts: any = {}) => { db.init(opts.db) + await redis.init() } diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 419f1db700..cc9eb854b4 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,6 +4,7 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" +export * from "./init" export * from "./invite" export * from "./passwordReset" diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index a4f1fecc17..8f2d2914b5 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -11,7 +11,7 @@ let userClient: Client, inviteClient: Client, passwordResetClient: Client -async function init() { +export async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() sessionClient = await new Client(utils.Databases.SESSIONS).init() appClient = await new Client(utils.Databases.APP_METADATA).init() diff --git a/packages/worker/src/db/index.ts b/packages/worker/src/db/index.ts index 157c2f4fb3..19f8f8acee 100644 --- a/packages/worker/src/db/index.ts +++ b/packages/worker/src/db/index.ts @@ -1,7 +1,7 @@ import * as core from "@budibase/backend-core" import env from "../environment" -export function init() { +export async function init() { const dbConfig: any = { replication: true, find: true, @@ -12,5 +12,5 @@ export function init() { dbConfig.allDbs = true } - core.init({ db: dbConfig }) + await core.init({ db: dbConfig }) } From a427d990a11eea5910526b378e4cd70b8a5e5aba Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 10 Nov 2023 11:58:07 +0000 Subject: [PATCH 36/67] Quick addition - if the object has been deleted but the key is still known, then CouchDB will alert us to the fact that it is deleted, leaving the response in a weird state. --- packages/backend-core/src/db/couch/DatabaseImpl.ts | 14 +++++++++++--- packages/backend-core/src/redis/redis.ts | 1 - 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 6a1e575ac9..8588a7157a 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -10,6 +10,7 @@ import { DatabaseDeleteIndexOpts, Document, isDocument, + RowResponse, } from "@budibase/types" import { getCouchInfo } from "./connections" import { directCouchUrlCall } from "./utils" @@ -127,12 +128,19 @@ export class DatabaseImpl implements Database { keys: ids, include_docs: true, }) - const NOT_FOUND = "not_found" - const rows = response.rows.filter(row => row.error !== NOT_FOUND) + const rowUnavailable = (row: RowResponse) => { + // row is deleted - key lookup can return this + if (row.doc == null || ("deleted" in row.value && row.value.deleted)) { + return true + } + return row.error === "not_found" + } + + const rows = response.rows.filter(row => !rowUnavailable(row)) const someMissing = rows.length !== response.rows.length // some were filtered out - means some missing if (!opts?.allowMissing && someMissing) { - const missing = response.rows.filter(row => row.error === NOT_FOUND) + const missing = response.rows.filter(row => rowUnavailable(row)) const missingIds = missing.map(row => row.key).join(", ") throw new Error(`Unable to get documents: ${missingIds}`) } diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 6f1b573718..701e262091 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -28,7 +28,6 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT // for testing just generate the client once let CLOSED = false let CLIENTS: { [key: number]: any } = {} -0 let CONNECTED = false // mock redis always connected From f808b3e1117ee78f8d4a4cac5ee871d0b69985e3 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 10 Nov 2023 12:02:11 +0000 Subject: [PATCH 37/67] PR Feedback --- .../settings/controls/EditComponentPopover.svelte | 1 - .../FieldConfiguration/FieldConfiguration.svelte | 6 +----- .../controls/FieldConfiguration/FieldSetting.svelte | 10 +--------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte index d0f6f24362..0ba502bbcb 100644 --- a/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte +++ b/packages/builder/src/components/design/settings/controls/EditComponentPopover.svelte @@ -91,7 +91,6 @@ open = true } }} - {disabled} /> { - listUpdated(update) - }
@@ -167,7 +163,7 @@ ...field, active: selectAll, })) - toggleAll(update) + listUpdated(update) }} text="" bind:value={selectAll} diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte index 75e6599a6c..1d9ce733b8 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte @@ -11,7 +11,6 @@ export let componentBindings export let bindings export let anchor - export let updating //or disabled const dispatch = createEventDispatcher() const onToggle = item => { @@ -50,7 +49,6 @@ {bindings} {parseSettings} on:change - disabled={updating} >
@@ -60,13 +58,7 @@
{readableText}
- +
From 90aaa1d9c19762fc0993957b7631e6c532f1af79 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 10 Nov 2023 12:13:45 +0000 Subject: [PATCH 38/67] Bump version to 2.13.8 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index c0559d8346..e06cae80fd 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.7", + "version": "2.13.8", "npmClient": "yarn", "packages": [ "packages/*" From e1f8b575689a730a9d38146b1b19e09107cea846 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 10 Nov 2023 12:54:49 +0000 Subject: [PATCH 39/67] Bump version to 2.13.9 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index e06cae80fd..aafb6b22ce 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.8", + "version": "2.13.9", "npmClient": "yarn", "packages": [ "packages/*" From ac57cdbf028b207a4ce943508de2da7fa0f33129 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Nov 2023 16:16:54 +0100 Subject: [PATCH 40/67] Encode view ids on paths --- packages/frontend-core/src/api/viewsV2.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/frontend-core/src/api/viewsV2.js b/packages/frontend-core/src/api/viewsV2.js index 3580d08229..a2072c2e1d 100644 --- a/packages/frontend-core/src/api/viewsV2.js +++ b/packages/frontend-core/src/api/viewsV2.js @@ -5,7 +5,7 @@ export const buildViewV2Endpoints = API => ({ */ fetchDefinition: async viewId => { return await API.get({ - url: `/api/v2/views/${viewId}`, + url: `/api/v2/views/${encodeURIComponent(viewId)}`, }) }, /** @@ -24,7 +24,7 @@ export const buildViewV2Endpoints = API => ({ */ update: async view => { return await API.put({ - url: `/api/v2/views/${view.id}`, + url: `/api/v2/views/${encodeURIComponent(view.id)}`, body: view, }) }, @@ -50,7 +50,7 @@ export const buildViewV2Endpoints = API => ({ sortType, }) => { return await API.post({ - url: `/api/v2/views/${viewId}/search`, + url: `/api/v2/views/${encodeURIComponent(viewId)}/search`, body: { query, paginate, @@ -67,6 +67,8 @@ export const buildViewV2Endpoints = API => ({ * @param viewId the id of the view */ delete: async viewId => { - return await API.delete({ url: `/api/v2/views/${viewId}` }) + return await API.delete({ + url: `/api/v2/views/${encodeURIComponent(viewId)}`, + }) }, }) From 24774f0836f6f9c9723f0cc6356d629474cab641 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 10 Nov 2023 16:33:59 +0100 Subject: [PATCH 41/67] Fix navigation --- .../src/components/backend/TableNavigator/TableNavigator.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte index 056a36c4a7..712d74889c 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte @@ -53,7 +53,7 @@ selected={isViewActive(view, $isActive, $views, $viewsV2)} on:click={() => { if (view.version === 2) { - $goto(`./view/v2/${view.id}`) + $goto(`./view/v2/${encodeURIComponent(view.id)}`) } else { $goto(`./view/v1/${encodeURIComponent(name)}`) } From d6eb2b945223c5ef72113b2db87d30d795c02a3b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 15:43:06 +0000 Subject: [PATCH 42/67] Attempting to get integration tests passing again. --- packages/worker/src/index.ts | 3 +- .../worker/src/tests/TestConfiguration.ts | 2 +- qa-core/yarn.lock | 28 +++++++++---------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 1c101983df..d40c7f0668 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -17,7 +17,7 @@ import { env as coreEnv, timers, } from "@budibase/backend-core" -db.init() + import Koa from "koa" import koaBody from "koa-body" import http from "http" @@ -85,6 +85,7 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) + await db.init() await initPro() // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index d4fcbeebd6..1961d22c34 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -7,7 +7,6 @@ mocks.licenses.init(mocks.pro) mocks.licenses.useUnlimited() import * as dbConfig from "../db" -dbConfig.init() import env from "../environment" import * as controllers from "./controllers" const supertest = require("supertest") @@ -109,6 +108,7 @@ class TestConfiguration { // SETUP / TEARDOWN async beforeAll() { + await dbConfig.init() try { await this.createDefaultUser() await this.createSession(this.user!) diff --git a/qa-core/yarn.lock b/qa-core/yarn.lock index d2cde27530..1de9d75e60 100644 --- a/qa-core/yarn.lock +++ b/qa-core/yarn.lock @@ -983,10 +983,10 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/node-fetch@2.6.2": - version "2.6.2" - resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz" - integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== +"@types/node-fetch@2.6.4": + version "2.6.4" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" + integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== dependencies: "@types/node" "*" form-data "^3.0.0" @@ -3587,18 +3587,18 @@ node-duration@^1.0.4: resolved "https://registry.npmjs.org/node-duration/-/node-duration-1.0.4.tgz" integrity sha512-eUXYNSY7DL53vqfTosggWkvyIW3bhAcqBDIlolgNYlZhianXTrCL50rlUJWD1eRqkIxMppXTfiFbp+9SjpPrgA== -node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.7: +node-fetch@2.6.0: + version "2.6.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-fetch@2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== - node-gyp-build-optional-packages@5.0.7: version "5.0.7" resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz" @@ -4893,10 +4893,10 @@ type-is@^1.6.16, type-is@^1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typescript@4.7.3: - version "4.7.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz" - integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== +typescript@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== uid2@0.0.x: version "0.0.4" From 4c7c10b121682ad53acbd8ae3aec1765d08060c9 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 16:17:18 +0000 Subject: [PATCH 43/67] Set Redis initialisation back to how it was before I started messing with it. --- packages/backend-core/src/index.ts | 4 +-- packages/backend-core/src/redis/index.ts | 6 ++-- packages/backend-core/src/redis/init.ts | 24 ++------------ packages/backend-core/src/redis/invite.ts | 32 +++++++++++-------- .../backend-core/src/redis/passwordReset.ts | 27 +++++++++------- .../src/api/controllers/global/users.ts | 12 +++---- packages/worker/src/db/index.ts | 4 +-- packages/worker/src/index.ts | 8 +++-- packages/worker/src/sdk/auth/auth.ts | 2 +- .../worker/src/tests/TestConfiguration.ts | 2 +- packages/worker/src/utilities/email.ts | 5 ++- 11 files changed, 57 insertions(+), 69 deletions(-) diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index d41f2b9384..f6c091d679 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -37,7 +37,6 @@ export { SearchParams } from "./db" // circular dependencies import * as context from "./context" import * as _tenancy from "./tenancy" -import * as redis from "./redis" export const tenancy = { ..._tenancy, ...context, @@ -52,7 +51,6 @@ export * from "./constants" // expose package init function import * as db from "./db" -export const init = async (opts: any = {}) => { +export const init = (opts: any = {}) => { db.init(opts.db) - await redis.init() } diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index cc9eb854b4..8d153ea5a1 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,7 +4,5 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" -export * from "./init" - -export * from "./invite" -export * from "./passwordReset" +export * as invite from "./invite" +export * as passwordReset from "./passwordReset" diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index 8f2d2914b5..55ffe3dd12 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -7,11 +7,9 @@ let userClient: Client, cacheClient: Client, writethroughClient: Client, lockClient: Client, - socketClient: Client, - inviteClient: Client, - passwordResetClient: Client + socketClient: Client -export async function init() { +async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() sessionClient = await new Client(utils.Databases.SESSIONS).init() appClient = await new Client(utils.Databases.APP_METADATA).init() @@ -22,8 +20,6 @@ export async function init() { utils.Databases.SOCKET_IO, utils.SelectableDatabase.SOCKET_IO ).init() - inviteClient = await new Client(utils.Databases.INVITATIONS).init() - passwordResetClient = await new Client(utils.Databases.PW_RESETS).init() } export async function shutdown() { @@ -34,8 +30,6 @@ export async function shutdown() { if (writethroughClient) await writethroughClient.finish() if (lockClient) await lockClient.finish() if (socketClient) await socketClient.finish() - if (inviteClient) await inviteClient.finish() - if (passwordResetClient) await passwordResetClient.finish() } process.on("exit", async () => { @@ -90,17 +84,3 @@ export async function getSocketClient() { } return socketClient } - -export async function getInviteClient() { - if (!inviteClient) { - await init() - } - return inviteClient -} - -export async function getPasswordResetClient() { - if (!passwordResetClient) { - await init() - } - return passwordResetClient -} diff --git a/packages/backend-core/src/redis/invite.ts b/packages/backend-core/src/redis/invite.ts index 6e6a1fd9e9..006a06fe26 100644 --- a/packages/backend-core/src/redis/invite.ts +++ b/packages/backend-core/src/redis/invite.ts @@ -1,6 +1,5 @@ -import { utils, tenancy } from "../" +import { utils, tenancy, redis } from "../" import env from "../environment" -import { getInviteClient } from "./init" const TTL_SECONDS = 60 * 60 * 24 * 7 @@ -13,13 +12,25 @@ interface InviteWithCode extends Invite { code: string } +let client: redis.Client + +export async function init() { + if (!client) { + client = new redis.Client(redis.utils.Databases.INVITATIONS) + } + return client +} + +export async function shutdown() { + if (client) await client.finish() +} + /** * Given an invite code and invite body, allow the update an existing/valid invite in redis * @param inviteCode The invite code for an invite in redis * @param value The body of the updated user invitation */ -export async function updateInviteCode(code: string, value: Invite) { - const client = await getInviteClient() +export async function updateCode(code: string, value: Invite) { await client.store(code, value, TTL_SECONDS) } @@ -29,11 +40,7 @@ export async function updateInviteCode(code: string, value: Invite) { * @param info Information to be carried along with the invitation. * @return returns the code that was stored to redis. */ -export async function createInviteCode( - email: string, - info: any -): Promise { - const client = await getInviteClient() +export async function createCode(email: string, info: any): Promise { const code = utils.newid() await client.store(code, { email, info }, TTL_SECONDS) return code @@ -44,8 +51,7 @@ export async function createInviteCode( * @param inviteCode the invite code that was provided as part of the link. * @return If the code is valid then an email address will be returned. */ -export async function getInviteCode(code: string): Promise { - const client = await getInviteClient() +export async function getCode(code: string): Promise { const value = (await client.get(code)) as Invite | undefined if (!value) { throw "Invitation is not valid or has expired, please request a new one." @@ -53,8 +59,7 @@ export async function getInviteCode(code: string): Promise { return value } -export async function deleteInviteCode(code: string) { - const client = await getInviteClient() +export async function deleteCode(code: string) { await client.delete(code) } @@ -62,7 +67,6 @@ export async function deleteInviteCode(code: string) { Get all currently available user invitations for the current tenant. **/ export async function getInviteCodes(): Promise { - const client = await getInviteClient() const invites: { key: string; value: Invite }[] = await client.scan() const results: InviteWithCode[] = invites.map(invite => { diff --git a/packages/backend-core/src/redis/passwordReset.ts b/packages/backend-core/src/redis/passwordReset.ts index 243b73c529..13c1b1d2e6 100644 --- a/packages/backend-core/src/redis/passwordReset.ts +++ b/packages/backend-core/src/redis/passwordReset.ts @@ -1,5 +1,4 @@ -import { utils } from "../" -import { getPasswordResetClient } from "./init" +import { redis, utils } from "../" const TTL_SECONDS = 60 * 60 @@ -8,6 +7,19 @@ interface PasswordReset { info: any } +let client: redis.Client + +export async function init() { + if (!client) { + client = new redis.Client(redis.utils.Databases.PW_RESETS) + } + return client +} + +export async function shutdown() { + if (client) await client.finish() +} + /** * Given a user ID this will store a code (that is returned) for an hour in redis. * The user can then return this code for resetting their password (through their reset link). @@ -15,11 +27,7 @@ interface PasswordReset { * @param info Info about the user/the reset process. * @return returns the code that was stored to redis. */ -export async function createResetPasswordCode( - userId: string, - info: any -): Promise { - const client = await getPasswordResetClient() +export async function createCode(userId: string, info: any): Promise { const code = utils.newid() await client.store(code, { userId, info }, TTL_SECONDS) return code @@ -30,10 +38,7 @@ export async function createResetPasswordCode( * @param code The code provided via the email link. * @return returns the user ID if it is found */ -export async function getResetPasswordCode( - code: string -): Promise { - const client = await getPasswordResetClient() +export async function getCode(code: string): Promise { const value = (await client.get(code)) as PasswordReset | undefined if (!value) { throw "Provided information is not valid, cannot reset password - please try again." diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index cd11fc74b6..affdaa9938 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -308,7 +308,7 @@ export const checkInvite = async (ctx: any) => { const { code } = ctx.params let invite try { - invite = await redis.getInviteCode(code) + invite = await redis.invite.getCode(code) } catch (e) { console.warn("Error getting invite from code", e) ctx.throw(400, "There was a problem with the invite") @@ -322,7 +322,7 @@ export const checkInvite = async (ctx: any) => { export const getUserInvites = async (ctx: any) => { try { // Restricted to the currently authenticated tenant - ctx.body = await redis.getInviteCodes() + ctx.body = await redis.invite.getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } @@ -336,7 +336,7 @@ export const updateInvite = async (ctx: any) => { let invite try { - invite = await redis.getInviteCode(code) + invite = await redis.invite.getCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") return @@ -364,7 +364,7 @@ export const updateInvite = async (ctx: any) => { } } - await redis.updateInviteCode(code, updated) + await redis.invite.updateCode(code, updated) ctx.body = { ...invite } } @@ -374,8 +374,8 @@ export const inviteAccept = async ( const { inviteCode, password, firstName, lastName } = ctx.request.body try { // info is an extension of the user object that was stored by global - const { email, info }: any = await redis.getInviteCode(inviteCode) - await redis.deleteInviteCode(inviteCode) + const { email, info }: any = await redis.invite.getCode(inviteCode) + await redis.invite.deleteCode(inviteCode) const user = await tenancy.doInTenant(info.tenantId, async () => { let request: any = { firstName, diff --git a/packages/worker/src/db/index.ts b/packages/worker/src/db/index.ts index 19f8f8acee..157c2f4fb3 100644 --- a/packages/worker/src/db/index.ts +++ b/packages/worker/src/db/index.ts @@ -1,7 +1,7 @@ import * as core from "@budibase/backend-core" import env from "../environment" -export async function init() { +export function init() { const dbConfig: any = { replication: true, find: true, @@ -12,5 +12,5 @@ export async function init() { dbConfig.allDbs = true } - await core.init({ db: dbConfig }) + core.init({ db: dbConfig }) } diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index d40c7f0668..e486a67433 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -16,8 +16,9 @@ import { queue, env as coreEnv, timers, + redis, } from "@budibase/backend-core" - +db.init() import Koa from "koa" import koaBody from "koa-body" import http from "http" @@ -71,6 +72,8 @@ server.on("close", async () => { shuttingDown = true console.log("Server Closed") timers.cleanup() + await redis.invite.shutdown() + await redis.passwordReset.shutdown() await events.shutdown() await queue.shutdown() if (!env.isTest()) { @@ -85,8 +88,9 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) - await db.init() await initPro() + await redis.invite.init() + await redis.passwordReset.init() // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues await events.processors.init(proSdk.auditLogs.write) diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 2140b89ce3..704de9e4b2 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.getResetPasswordCode(resetCode) + const { userId } = await redis.passwordReset.getCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 1961d22c34..d4fcbeebd6 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -7,6 +7,7 @@ mocks.licenses.init(mocks.pro) mocks.licenses.useUnlimited() import * as dbConfig from "../db" +dbConfig.init() import env from "../environment" import * as controllers from "./controllers" const supertest = require("supertest") @@ -108,7 +109,6 @@ class TestConfiguration { // SETUP / TEARDOWN async beforeAll() { - await dbConfig.init() try { await this.createDefaultUser() await this.createSession(this.user!) diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index a4d2c296e5..530b6ce87f 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -3,7 +3,6 @@ import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" -import { createResetPasswordCode } from "@budibase/backend-core/src/redis/passwordReset" import { redis } from "@budibase/backend-core" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" @@ -62,9 +61,9 @@ async function getLinkCode( ) { switch (purpose) { case EmailTemplatePurpose.PASSWORD_RECOVERY: - return createResetPasswordCode(user._id!, info) + return redis.passwordReset.createCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return redis.createInviteCode(email, info) + return redis.invite.createCode(email, info) default: return null } From a4cac14914df968c241f487f530b1bef515526f6 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 17 Nov 2023 10:59:55 +0000 Subject: [PATCH 44/67] Bump version to 2.13.10 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index aafb6b22ce..a12b1238b3 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.9", + "version": "2.13.10", "npmClient": "yarn", "packages": [ "packages/*" From 051551f876bb5f0f7f29b8d472d336e09c2ee25a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 Nov 2023 12:51:22 +0100 Subject: [PATCH 45/67] Unify .eslintrc --- packages/cli/.eslintrc | 12 ------------ packages/string-templates/.eslintrc | 12 ------------ 2 files changed, 24 deletions(-) delete mode 100644 packages/cli/.eslintrc delete mode 100644 packages/string-templates/.eslintrc diff --git a/packages/cli/.eslintrc b/packages/cli/.eslintrc deleted file mode 100644 index 3431bf04fb..0000000000 --- a/packages/cli/.eslintrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "globals": { - "emit": true, - "key": true - }, - "env": { - "node": true - }, - "extends": ["eslint:recommended"], - "rules": { - } -} \ No newline at end of file diff --git a/packages/string-templates/.eslintrc b/packages/string-templates/.eslintrc deleted file mode 100644 index 3431bf04fb..0000000000 --- a/packages/string-templates/.eslintrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "globals": { - "emit": true, - "key": true - }, - "env": { - "node": true - }, - "extends": ["eslint:recommended"], - "rules": { - } -} \ No newline at end of file From ab56228192f1e307f49a95ff1bea932237056050 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 Nov 2023 13:03:28 +0100 Subject: [PATCH 46/67] Use eslint-plugin-import and import/no-relative-packages --- .eslintrc.json | 12 +- package.json | 13 +- yarn.lock | 412 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 426 insertions(+), 11 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 75584b8163..783a212f11 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,6 +19,7 @@ "bundle.js" ], "extends": ["eslint:recommended"], + "plugins": ["import"], "overrides": [ { "files": ["**/*.svelte"], @@ -30,7 +31,6 @@ "sourceType": "module", "allowImportExportEverywhere": true } - }, { "files": ["**/*.ts"], @@ -48,7 +48,15 @@ ], "rules": { "no-self-assign": "off", - "no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }] + "no-unused-vars": [ + "error", + { + "varsIgnorePattern": "^_", + "argsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_" + } + ], + "import/no-relative-packages": "error" }, "globals": { "GeolocationPositionError": true diff --git a/package.json b/package.json index 8a27cde104..29d5560955 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,16 @@ "name": "root", "private": true, "devDependencies": { + "@babel/core": "^7.22.5", + "@babel/eslint-parser": "^7.22.5", + "@babel/preset-env": "^7.22.5", "@esbuild-plugins/tsconfig-paths": "^0.1.2", "@typescript-eslint/parser": "6.7.2", "esbuild": "^0.18.17", "esbuild-node-externals": "^1.8.0", "eslint": "^8.44.0", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-svelte": "^2.32.2", "husky": "^8.0.3", "kill-port": "^1.6.1", "lerna": "7.1.1", @@ -17,12 +22,8 @@ "prettier": "2.8.8", "prettier-plugin-svelte": "^2.3.0", "svelte": "3.49.0", - "typescript": "5.2.2", - "@babel/core": "^7.22.5", - "@babel/eslint-parser": "^7.22.5", - "@babel/preset-env": "^7.22.5", - "eslint-plugin-svelte": "^2.32.2", - "svelte-eslint-parser": "^0.32.0" + "svelte-eslint-parser": "^0.32.0", + "typescript": "5.2.2" }, "scripts": { "preinstall": "node scripts/syncProPackage.js", diff --git a/yarn.lock b/yarn.lock index f573046394..50c497c6c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6799,6 +6799,14 @@ array-back@^6.2.0, array-back@^6.2.2: resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.2.tgz#f567d99e9af88a6d3d2f9dfcc21db6f9ba9fd157" integrity sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw== +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-differ@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" @@ -6809,6 +6817,17 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== +array-includes@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" + integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-string "^1.0.7" + array-sort@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" @@ -6833,6 +6852,50 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== +array.prototype.findlastindex@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" + integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -7727,6 +7790,15 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -9087,6 +9159,15 @@ deferred-leveldown@~5.3.0: abstract-leveldown "~6.2.1" inherits "^2.0.3" +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -9100,6 +9181,15 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -9465,6 +9555,13 @@ doctrine@3.0.0, doctrine@^3.0.0: dependencies: esutils "^2.0.2" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" @@ -9890,6 +9987,51 @@ es-abstract@^1.17.5, es-abstract@^1.19.0, es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-abstract@^1.22.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.5" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.2" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.13" + es-aggregate-error@^1.0.8: version "1.0.9" resolved "https://registry.yarnpkg.com/es-aggregate-error/-/es-aggregate-error-1.0.9.tgz#b50925cdf78c8a634bd766704f6f7825902be3d9" @@ -9932,6 +10074,13 @@ es-set-tostringtag@^2.0.1: has "^1.0.3" has-tostringtag "^1.0.0" +es-shim-unscopables@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -10141,6 +10290,45 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz#8133232e4329ee344f2f612885ac3073b0b7e155" + integrity sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.14.2" + eslint-plugin-svelte@^2.32.2: version "2.32.2" resolved "https://registry.yarnpkg.com/eslint-plugin-svelte/-/eslint-plugin-svelte-2.32.2.tgz#d8f1352b55967445ee8d57aaee55f99712696a30" @@ -11068,6 +11256,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -11078,6 +11271,16 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -11234,6 +11437,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-object@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/get-object/-/get-object-0.2.0.tgz#d92ff7d5190c64530cda0543dac63a3d47fe8c0c" @@ -11937,6 +12150,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + help-me@^4.0.1: version "4.2.0" resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563" @@ -12407,6 +12627,15 @@ internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" + integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== + dependencies: + get-intrinsic "^1.2.2" + hasown "^2.0.0" + side-channel "^1.0.4" + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -12508,7 +12737,7 @@ is-arguments@^1.1.1: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-array-buffer@^3.0.1: +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== @@ -12585,6 +12814,13 @@ is-core-module@2.9.0, is-core-module@^2.5.0, is-core-module@^2.8.1, is-core-modu dependencies: has "^1.0.3" +is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -12935,6 +13171,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" +is-typed-array@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -13794,10 +14037,10 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1: +json5@^1.0.1, json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity "sha1-Y9mNYPIbMTt3xNbaGL+mnYDh1ZM= sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -16298,6 +16541,11 @@ object-inspect@^1.12.2, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -16349,6 +16597,25 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" +object.fromentries@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" + integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.groupby@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" + integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -16356,6 +16623,15 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +object.values@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" + integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + octal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/octal/-/octal-1.0.0.tgz#63e7162a68efbeb9e213588d58e989d1e5c4530b" @@ -18668,6 +18944,15 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" +regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + regexparam@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-2.0.1.tgz#c912f5dae371e3798100b3c9ce22b7414d0889fa" @@ -18879,6 +19164,15 @@ resolve@^1.10.0, resolve@^1.11.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.1 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -19153,6 +19447,16 @@ rxjs@^7.5.5: dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -19334,6 +19638,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" @@ -19387,6 +19696,25 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -20109,6 +20437,15 @@ string.prototype.startswith@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -20118,6 +20455,15 @@ string.prototype.trimend@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" @@ -20127,6 +20473,15 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -21079,6 +21434,16 @@ tsconfig-paths@^3.10.1: minimist "^1.2.6" strip-bom "^3.0.0" +tsconfig-paths@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" @@ -21203,6 +21568,36 @@ type-is@^1.6.14, type-is@^1.6.16, type-is@^1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -21978,6 +22373,17 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.11, which-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.4" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" From e8abb5cb46b071d813731b8390cd31ecd6759818 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 Nov 2023 14:39:52 +0100 Subject: [PATCH 47/67] Detect non-barrel workspace usages --- .eslintrc.json | 5 +++-- eslint-local-rules/index.js | 21 +++++++++++++++++++++ package.json | 1 + packages/backend-core/src/index.ts | 1 + yarn.lock | 5 +++++ 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 eslint-local-rules/index.js diff --git a/.eslintrc.json b/.eslintrc.json index 783a212f11..f6f03c6523 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,7 +19,7 @@ "bundle.js" ], "extends": ["eslint:recommended"], - "plugins": ["import"], + "plugins": ["import", "eslint-plugin-local-rules"], "overrides": [ { "files": ["**/*.svelte"], @@ -42,7 +42,8 @@ "no-case-declarations": "off", "no-useless-escape": "off", "no-undef": "off", - "no-prototype-builtins": "off" + "no-prototype-builtins": "off", + "local-rules/no-budibase-imports": "error" } } ], diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js new file mode 100644 index 0000000000..af02599c90 --- /dev/null +++ b/eslint-local-rules/index.js @@ -0,0 +1,21 @@ +module.exports = { + "no-budibase-imports": { + create: function (context) { + return { + ImportDeclaration(node) { + const importPath = node.source.value + + if ( + /^@budibase\/[^/]+\/.*$/.test(importPath) && + importPath !== "@budibase/backend-core/tests" + ) { + context.report({ + node, + message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests.`, + }) + } + }, + } + }, + }, +} diff --git a/package.json b/package.json index 29d5560955..2978483448 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "esbuild-node-externals": "^1.8.0", "eslint": "^8.44.0", "eslint-plugin-import": "^2.29.0", + "eslint-plugin-local-rules": "^2.0.0", "eslint-plugin-svelte": "^2.32.2", "husky": "^8.0.3", "kill-port": "^1.6.1", diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index c7cf9f56cc..e569f27b88 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -32,6 +32,7 @@ export * as blacklist from "./blacklist" export * as docUpdates from "./docUpdates" export * from "./utils/Duration" export { SearchParams } from "./db" +export * as docIds from "./docIds" // Add context to tenancy for backwards compatibility // only do this for external usages to prevent internal // circular dependencies diff --git a/yarn.lock b/yarn.lock index 50c497c6c2..700c3f9456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10329,6 +10329,11 @@ eslint-plugin-import@^2.29.0: semver "^6.3.1" tsconfig-paths "^3.14.2" +eslint-plugin-local-rules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-local-rules/-/eslint-plugin-local-rules-2.0.0.tgz#cda95d7616cc0e2609d76c347c187ca2be1e252e" + integrity sha512-sWueme0kUcP0JC1+6OBDQ9edBDVFJR92WJHSRbhiRExlenMEuUisdaVBPR+ItFBFXo2Pdw6FD2UfGZWkz8e93g== + eslint-plugin-svelte@^2.32.2: version "2.32.2" resolved "https://registry.yarnpkg.com/eslint-plugin-svelte/-/eslint-plugin-svelte-2.32.2.tgz#d8f1352b55967445ee8d57aaee55f99712696a30" From d0e40afbcb9cb8bfe82e2d5865ff4a310e02dae8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 Nov 2023 14:42:37 +0100 Subject: [PATCH 48/67] Fix lint issues --- packages/client/src/components/app/forms/S3Upload.svelte | 2 +- packages/pro | 2 +- packages/server/src/sdk/app/links/links.ts | 8 ++++---- packages/server/src/sdk/app/tables/migration.ts | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/client/src/components/app/forms/S3Upload.svelte b/packages/client/src/components/app/forms/S3Upload.svelte index 9985c83bb8..5ead872863 100644 --- a/packages/client/src/components/app/forms/S3Upload.svelte +++ b/packages/client/src/components/app/forms/S3Upload.svelte @@ -2,7 +2,7 @@ import Field from "./Field.svelte" import { CoreDropzone, ProgressCircle } from "@budibase/bbui" import { getContext, onMount, onDestroy } from "svelte" - import { cloneDeep } from "../../../../../bbui/src/helpers" + import { cloneDeep } from "@budibase/bbui/src/helpers" export let datasourceId export let bucket diff --git a/packages/pro b/packages/pro index e202f415d9..d017f81efd 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit e202f415d9fa540d08cc2ba6e27394fbc22f357b +Subproject commit d017f81efdfc5fef3ec6c185cbccba54213be0b1 diff --git a/packages/server/src/sdk/app/links/links.ts b/packages/server/src/sdk/app/links/links.ts index fda07568f9..5d3420341f 100644 --- a/packages/server/src/sdk/app/links/links.ts +++ b/packages/server/src/sdk/app/links/links.ts @@ -1,5 +1,5 @@ -import { context } from "@budibase/backend-core" -import { isTableId } from "@budibase/backend-core/src/docIds" +import { context, docIds } from "@budibase/backend-core" + import { DatabaseQueryOpts, LinkDocument, @@ -8,7 +8,7 @@ import { import { ViewName, getQueryIndex } from "../../../../src/db/utils" export async function fetch(tableId: string): Promise { - if (!isTableId(tableId)) { + if (!docIds.isTableId(tableId)) { throw new Error(`Invalid tableId: ${tableId}`) } @@ -24,7 +24,7 @@ export async function fetch(tableId: string): Promise { export async function fetchWithDocument( tableId: string ): Promise { - if (!isTableId(tableId)) { + if (!docIds.isTableId(tableId)) { throw new Error(`Invalid tableId: ${tableId}`) } diff --git a/packages/server/src/sdk/app/tables/migration.ts b/packages/server/src/sdk/app/tables/migration.ts index 718223dbeb..e282251bfb 100644 --- a/packages/server/src/sdk/app/tables/migration.ts +++ b/packages/server/src/sdk/app/tables/migration.ts @@ -17,7 +17,6 @@ import sdk from "../../../sdk" import { isExternalTableID } from "../../../integrations/utils" import { EventType, updateLinks } from "../../../db/linkedRows" import { cloneDeep } from "lodash" -import { isInternalColumnName } from "@budibase/backend-core/src/db" export interface MigrationResult { tablesUpdated: Table[] @@ -36,7 +35,7 @@ export async function migrate( throw new BadRequestError(`Column name cannot be empty`) } - if (isInternalColumnName(newColumn.name)) { + if (dbCore.isInternalColumnName(newColumn.name)) { throw new BadRequestError(`Column name cannot be a reserved column name`) } From 9a097c37f2f12cb06ff4d80406a86cdec0aa22c5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 Nov 2023 14:59:25 +0100 Subject: [PATCH 49/67] Update master ref --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index d017f81efd..2cf6f28380 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit d017f81efdfc5fef3ec6c185cbccba54213be0b1 +Subproject commit 2cf6f28380d3ab22128b8a889d622fd5adfa31fc From f70bb967e69211d5c62b6aaa461a87ea3920918c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 17 Nov 2023 15:40:26 +0100 Subject: [PATCH 50/67] Fix references --- packages/client/src/components/app/forms/S3Upload.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/app/forms/S3Upload.svelte b/packages/client/src/components/app/forms/S3Upload.svelte index 5ead872863..fd9af0489c 100644 --- a/packages/client/src/components/app/forms/S3Upload.svelte +++ b/packages/client/src/components/app/forms/S3Upload.svelte @@ -1,8 +1,7 @@