diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index d9d894c33e..9348706399 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -41,11 +41,12 @@ module.exports = { if ( /^@budibase\/[^/]+\/.*$/.test(importPath) && importPath !== "@budibase/backend-core/tests" && - importPath !== "@budibase/string-templates/test/utils" + importPath !== "@budibase/string-templates/test/utils" && + importPath !== "@budibase/client/manifest.json" ) { context.report({ node, - message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`, + message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`, }) } }, diff --git a/lerna.json b/lerna.json index d033c24518..13040cb50c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.46", + "version": "3.3.1", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index dbdce51c50..d4e6e9a1ec 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -1,6 +1,5 @@ export * as configs from "./configs" export * as events from "./events" -export * as migrations from "./migrations" export * as users from "./users" export * as userUtils from "./users/utils" export * as roles from "./security/roles" diff --git a/packages/backend-core/src/migrations/definitions.ts b/packages/backend-core/src/migrations/definitions.ts deleted file mode 100644 index 0dd57fe639..0000000000 --- a/packages/backend-core/src/migrations/definitions.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - MigrationType, - MigrationName, - MigrationDefinition, -} from "@budibase/types" - -export const DEFINITIONS: MigrationDefinition[] = [ - { - type: MigrationType.GLOBAL, - name: MigrationName.USER_EMAIL_VIEW_CASING, - }, - { - type: MigrationType.GLOBAL, - name: MigrationName.SYNC_QUOTAS, - }, - { - type: MigrationType.APP, - name: MigrationName.APP_URLS, - }, - { - type: MigrationType.APP, - name: MigrationName.EVENT_APP_BACKFILL, - }, - { - type: MigrationType.APP, - name: MigrationName.TABLE_SETTINGS_LINKS_TO_ACTIONS, - }, - { - type: MigrationType.GLOBAL, - name: MigrationName.EVENT_GLOBAL_BACKFILL, - }, - { - type: MigrationType.INSTALLATION, - name: MigrationName.EVENT_INSTALLATION_BACKFILL, - }, - { - type: MigrationType.GLOBAL, - name: MigrationName.GLOBAL_INFO_SYNC_USERS, - }, -] diff --git a/packages/backend-core/src/migrations/index.ts b/packages/backend-core/src/migrations/index.ts deleted file mode 100644 index bce0cfc75c..0000000000 --- a/packages/backend-core/src/migrations/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./migrations" -export * from "./definitions" diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts deleted file mode 100644 index c8320b5724..0000000000 --- a/packages/backend-core/src/migrations/migrations.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { DEFAULT_TENANT_ID } from "../constants" -import { - DocumentType, - StaticDatabases, - getAllApps, - getGlobalDBName, - getDB, -} from "../db" -import environment from "../environment" -import * as platform from "../platform" -import * as context from "../context" -import { DEFINITIONS } from "." -import { - Migration, - MigrationOptions, - MigrationType, - MigrationNoOpOptions, - App, -} from "@budibase/types" - -export const getMigrationsDoc = async (db: any) => { - // get the migrations doc - try { - return await db.get(DocumentType.MIGRATIONS) - } catch (err: any) { - if (err.status && err.status === 404) { - return { _id: DocumentType.MIGRATIONS } - } else { - throw err - } - } -} - -export const backPopulateMigrations = async (opts: MigrationNoOpOptions) => { - // filter migrations to the type and populate a no-op migration - const migrations: Migration[] = DEFINITIONS.filter( - def => def.type === opts.type - ).map(d => ({ ...d, fn: async () => {} })) - await runMigrations(migrations, { noOp: opts }) -} - -export const runMigration = async ( - migration: Migration, - options: MigrationOptions = {} -) => { - const migrationType = migration.type - const migrationName = migration.name - const silent = migration.silent - - const log = (message: string) => { - if (!silent) { - console.log(message) - } - } - - // get the db to store the migration in - let dbNames: string[] - if (migrationType === MigrationType.GLOBAL) { - dbNames = [getGlobalDBName()] - } else if (migrationType === MigrationType.APP) { - if (options.noOp) { - if (!options.noOp.appId) { - throw new Error("appId is required for noOp app migration") - } - dbNames = [options.noOp.appId] - } else { - const apps = (await getAllApps(migration.appOpts)) as App[] - dbNames = apps.map(app => app.appId) - } - } else if (migrationType === MigrationType.INSTALLATION) { - dbNames = [StaticDatabases.PLATFORM_INFO.name] - } else { - throw new Error(`Unrecognised migration type [${migrationType}]`) - } - - const length = dbNames.length - let count = 0 - - // run the migration against each db - for (const dbName of dbNames) { - count++ - const lengthStatement = length > 1 ? `[${count}/${length}]` : "" - - const db = getDB(dbName) - - try { - const doc = await getMigrationsDoc(db) - - // the migration has already been run - if (doc[migrationName]) { - // check for force - if ( - options.force && - options.force[migrationType] && - options.force[migrationType].includes(migrationName) - ) { - log(`[Migration: ${migrationName}] [DB: ${dbName}] Forcing`) - } else { - // no force, exit - return - } - } - - // check if the migration is not a no-op - if (!options.noOp) { - log( - `[Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}` - ) - - if (migration.preventRetry) { - // eagerly set the completion date - // so that we never run this migration twice even upon failure - doc[migrationName] = Date.now() - const response = await db.put(doc) - doc._rev = response.rev - } - - // run the migration - if (migrationType === MigrationType.APP) { - await context.doInAppContext(db.name, async () => { - await migration.fn(db) - }) - } else { - await migration.fn(db) - } - - log(`[Migration: ${migrationName}] [DB: ${dbName}] Complete`) - } - - // mark as complete - doc[migrationName] = Date.now() - await db.put(doc) - } catch (err) { - console.error( - `[Migration: ${migrationName}] [DB: ${dbName}] Error: `, - err - ) - throw err - } - } -} - -export const runMigrations = async ( - migrations: Migration[], - options: MigrationOptions = {} -) => { - let tenantIds - - if (environment.MULTI_TENANCY) { - if (options.noOp) { - tenantIds = [options.noOp.tenantId] - } else if (!options.tenantIds || !options.tenantIds.length) { - // run for all tenants - tenantIds = await platform.tenants.getTenantIds() - } else { - tenantIds = options.tenantIds - } - } else { - // single tenancy - tenantIds = [DEFAULT_TENANT_ID] - } - - if (tenantIds.length > 1) { - console.log(`Checking migrations for ${tenantIds.length} tenants`) - } else { - console.log("Checking migrations") - } - - let count = 0 - // for all tenants - for (const tenantId of tenantIds) { - count++ - if (tenantIds.length > 1) { - console.log(`Progress [${count}/${tenantIds.length}]`) - } - // for all migrations - for (const migration of migrations) { - // run the migration - await context.doInTenant( - tenantId, - async () => await runMigration(migration, options) - ) - } - } - console.log("Migrations complete") -} diff --git a/packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap b/packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap deleted file mode 100644 index 377900b5d5..0000000000 --- a/packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`migrations should match snapshot 1`] = ` -{ - "_id": "migrations", - "_rev": "1-2f64479842a0513aa8b97f356b0b9127", - "createdAt": "2020-01-01T00:00:00.000Z", - "test": 1577836800000, - "updatedAt": "2020-01-01T00:00:00.000Z", -} -`; diff --git a/packages/backend-core/src/migrations/tests/migrations.spec.ts b/packages/backend-core/src/migrations/tests/migrations.spec.ts deleted file mode 100644 index af2eb33cf5..0000000000 --- a/packages/backend-core/src/migrations/tests/migrations.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { testEnv, DBTestConfiguration } from "../../../tests/extra" -import * as migrations from "../index" -import * as context from "../../context" -import { MigrationType } from "@budibase/types" - -testEnv.multiTenant() - -describe("migrations", () => { - const config = new DBTestConfiguration() - - const migrationFunction = jest.fn() - - const MIGRATIONS = [ - { - type: MigrationType.GLOBAL, - name: "test" as any, - fn: migrationFunction, - }, - ] - - beforeEach(() => { - config.newTenant() - }) - - afterEach(async () => { - jest.clearAllMocks() - }) - - const migrate = () => { - return migrations.runMigrations(MIGRATIONS, { - tenantIds: [config.tenantId], - }) - } - - it("should run a new migration", async () => { - await config.doInTenant(async () => { - await migrate() - expect(migrationFunction).toHaveBeenCalled() - const db = context.getGlobalDB() - const doc = await migrations.getMigrationsDoc(db) - expect(doc.test).toBeDefined() - }) - }) - - it("should match snapshot", async () => { - await config.doInTenant(async () => { - await migrate() - const doc = await migrations.getMigrationsDoc(context.getGlobalDB()) - expect(doc).toMatchSnapshot() - }) - }) - - it("should skip a previously run migration", async () => { - await config.doInTenant(async () => { - const db = context.getGlobalDB() - await migrate() - const previousDoc = await migrations.getMigrationsDoc(db) - await migrate() - const currentDoc = await migrations.getMigrationsDoc(db) - expect(migrationFunction).toHaveBeenCalledTimes(1) - expect(currentDoc.test).toBe(previousDoc.test) - }) - }) -}) diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index c22bb3f918..7438fab5fd 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -1,23 +1,23 @@ - { try { expressionError = undefined - expressionResult = processStringSync( + const output = processStringWithLogsSync( expression || "", { ...context, @@ -167,6 +169,8 @@ noThrow: false, } ) + expressionResult = output.result + expressionLogs = output.logs } catch (err: any) { expressionResult = undefined expressionError = err @@ -421,6 +425,7 @@ diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index c8bf5529ad..c47840ea83 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -4,11 +4,13 @@ import { Helpers } from "@budibase/bbui" import { fade } from "svelte/transition" import { UserScriptError } from "@budibase/string-templates" + import type { Log } from "@budibase/string-templates" import type { JSONValue } from "@budibase/types" // this can be essentially any primitive response from the JS function export let expressionResult: JSONValue | undefined = undefined export let expressionError: string | undefined = undefined + export let expressionLogs: Log[] = [] export let evaluating = false export let expression: string | null = null @@ -16,6 +18,11 @@ $: empty = expression == null || expression?.trim() === "" $: success = !error && !empty $: highlightedResult = highlight(expressionResult) + $: highlightedLogs = expressionLogs.map(l => ({ + log: highlight(l.log.join(", ")), + line: l.line, + type: l.type, + })) const formatError = (err: any) => { if (err.code === UserScriptError.code) { @@ -25,14 +32,14 @@ } // json can be any primitive type - const highlight = (json?: any | null) => { + const highlight = (json?: JSONValue | null) => { if (json == null) { return "" } // Attempt to parse and then stringify, in case this is valid result try { - json = JSON.stringify(JSON.parse(json), null, 2) + json = JSON.stringify(JSON.parse(json as any), null, 2) } catch (err) { // couldn't parse/stringify, just treat it as the raw input } @@ -61,7 +68,7 @@
{#if error} - +
Error
{#if evaluating}
@@ -90,8 +97,36 @@ {:else if error} {formatError(expressionError)} {:else} - - {@html highlightedResult} +
+ {#each highlightedLogs as logLine} +
+
+ {#if logLine.type === "error"} + + {:else if logLine.type === "warn"} + + {/if} + + {@html logLine.log} +
+ {#if logLine.line} + :{logLine.line} + {/if} +
+ {/each} +
+ + {@html highlightedResult} +
+
{/if}
@@ -130,20 +165,37 @@ height: 100%; z-index: 1; position: absolute; - opacity: 10%; } .header.error::before { - background: var(--spectrum-global-color-red-400); + background: var(--error-bg); } .body { flex: 1 1 auto; padding: var(--spacing-m) var(--spacing-l); font-family: var(--font-mono); font-size: 12px; - overflow-y: scroll; + overflow-y: auto; overflow-x: hidden; - white-space: pre-wrap; + white-space: pre-line; word-wrap: break-word; height: 0; } + .output-lines { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + } + .line { + border-bottom: var(--border-light); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: end; + padding: var(--spacing-s); + } + .icon-log { + display: flex; + gap: var(--spacing-s); + align-items: start; + } diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte index f6d7cfc2c3..4ea8c63087 100644 --- a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte +++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte @@ -1,4 +1,5 @@ {#if dividerState} @@ -21,15 +37,16 @@ {#each dataSet as data}
  • onSelect(data)} > - {data.datasourceName ? `${data.datasourceName} - ` : ""}{data.label} + {data.datasourceName && displayDatasourceName + ? `${data.datasourceName} - ` + : ""}{data.label} format.table(table, $datasources.list)) - .sort((a, b) => { - // sort tables alphabetically, grouped by datasource - const dsA = a.datasourceName ?? "" - const dsB = b.datasourceName ?? "" - - const dsComparison = dsA.localeCompare(dsB) - if (dsComparison !== 0) { - return dsComparison - } - return a.label.localeCompare(b.label) - }) + $: tables = sortAndFormat.tables($tablesStore.list, $datasources.list) $: viewsV1 = $viewsStore.list.map(view => ({ ...view, label: view.name, type: "view", })) - $: viewsV2 = $viewsV2Store.list.map(format.viewV2) + $: viewsV2 = sortAndFormat.viewsV2($viewsV2Store.list, $datasources.list) $: views = [...(viewsV1 || []), ...(viewsV2 || [])] $: queries = $queriesStore.list .filter(q => showAllQueries || q.queryVerb === "read" || q.readable) @@ -303,6 +291,7 @@ dataSet={views} {value} onSelect={handleSelected} + identifiers={["tableId", "name"]} /> {/if} {#if queries?.length} @@ -312,6 +301,7 @@ dataSet={queries} {value} onSelect={handleSelected} + identifiers={["_id"]} /> {/if} {#if links?.length} @@ -321,6 +311,7 @@ dataSet={links} {value} onSelect={handleSelected} + identifiers={["tableId", "fieldName"]} /> {/if} {#if fields?.length} @@ -330,6 +321,7 @@ dataSet={fields} {value} onSelect={handleSelected} + identifiers={["providerId", "tableId", "fieldName"]} /> {/if} {#if jsonArrays?.length} @@ -339,6 +331,7 @@ dataSet={jsonArrays} {value} onSelect={handleSelected} + identifiers={["providerId", "tableId", "fieldName"]} /> {/if} {#if showDataProviders && dataProviders?.length} @@ -348,6 +341,7 @@ dataSet={dataProviders} {value} onSelect={handleSelected} + identifiers={["providerId"]} /> {/if} - import { Select } from "@budibase/bbui" + import { Popover, Select } from "@budibase/bbui" import { createEventDispatcher, onMount } from "svelte" - import { tables as tablesStore, viewsV2 } from "@/stores/builder" - import { tableSelect as format } from "@/helpers/data/format" + import { + tables as tableStore, + datasources as datasourceStore, + viewsV2 as viewsV2Store, + } from "@/stores/builder" + import DataSourceCategory from "./DataSourceSelect/DataSourceCategory.svelte" + import { sortAndFormat } from "@/helpers/data/format" export let value + let anchorRight, dropdownRight + const dispatch = createEventDispatcher() - $: tables = $tablesStore.list.map(format.table) - $: views = $viewsV2.list.map(format.viewV2) + $: tables = sortAndFormat.tables($tableStore.list, $datasourceStore.list) + $: views = sortAndFormat.viewsV2($viewsV2Store.list, $datasourceStore.list) $: options = [...(tables || []), ...(views || [])] + $: text = value?.label ?? "Choose an option" + const onChange = e => { dispatch( "change", - options.find(x => x.resourceId === e.detail) + options.find(x => x.resourceId === e.resourceId) ) + dropdownRight.hide() } onMount(() => { @@ -29,10 +39,47 @@ }) -
    +