diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 1258bddcca..2151e1e342 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -200,6 +200,20 @@ jobs: - run: yarn --frozen-lockfile + - name: Set up PostgreSQL 16 + if: matrix.datasource == 'postgres' + run: | + sudo systemctl stop postgresql + sudo apt-get remove --purge -y postgresql* libpq-dev + sudo rm -rf /etc/postgresql /var/lib/postgresql + sudo apt-get autoremove -y + sudo apt-get autoclean + + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get update + sudo apt-get install -y postgresql-16 + - name: Test server env: DATASOURCE: ${{ matrix.datasource }} @@ -281,6 +295,7 @@ jobs: check-lockfile: runs-on: ubuntu-latest + if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/examples/nextjs-api-sales/package.json b/examples/nextjs-api-sales/package.json index f1ef4843a1..21c548c6cc 100644 --- a/examples/nextjs-api-sales/package.json +++ b/examples/nextjs-api-sales/package.json @@ -22,6 +22,6 @@ "@types/react": "17.0.39", "eslint": "8.10.0", "eslint-config-next": "12.1.0", - "typescript": "5.5.2" + "typescript": "5.7.2" } } diff --git a/globalSetup.ts b/globalSetup.ts index 07a0cec5e2..ec4f38b388 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -6,6 +6,26 @@ import { import { ContainerInfo } from "dockerode" import path from "path" import lockfile from "proper-lockfile" +import { execSync } from "child_process" + +interface DockerContext { + Name: string + Description: string + DockerEndpoint: string + ContextType: string + Error: string +} + +function getCurrentDockerContext(): DockerContext { + const out = execSync("docker context ls --format json") + for (const line of out.toString().split("\n")) { + const parsed = JSON.parse(line) + if (parsed.Current) { + return parsed as DockerContext + } + } + throw new Error("No current Docker context") +} async function getBudibaseContainers() { const client = await getContainerRuntimeClient() @@ -27,6 +47,16 @@ async function killContainers(containers: ContainerInfo[]) { } export default async function setup() { + process.env.TESTCONTAINERS_RYUK_DISABLED = "true" + + // For whatever reason, testcontainers doesn't always use the correct current + // docker context. This bit of code forces the issue by finding the current + // context and setting it as the DOCKER_HOST environment + if (!process.env.DOCKER_HOST) { + const dockerContext = getCurrentDockerContext() + process.env.DOCKER_HOST = dockerContext.DockerEndpoint + } + const lockPath = path.resolve(__dirname, "globalSetup.ts") // If you run multiple tests at the same time, it's possible for the CouchDB // shared container to get started multiple times despite having an @@ -47,6 +77,7 @@ export default async function setup() { try { const couchdb = new GenericContainer("budibase/couchdb:v3.3.3-sqs-v2.1.1") + .withName("couchdb_testcontainer") .withExposedPorts(5984, 4984) .withEnvironment({ COUCHDB_PASSWORD: "budibase", @@ -71,6 +102,7 @@ export default async function setup() { ) const minio = new GenericContainer("minio/minio") + .withName("minio_testcontainer") .withExposedPorts(9000) .withCommand(["server", "/data"]) .withTmpFs({ "/data": "rw" }) diff --git a/hosting/single/nginx/nginx-default-site.conf b/hosting/single/nginx/nginx-default-site.conf index d2f8e229c6..f72d33e236 100644 --- a/hosting/single/nginx/nginx-default-site.conf +++ b/hosting/single/nginx/nginx-default-site.conf @@ -46,6 +46,11 @@ server { } location ~ ^/api/(system|admin|global)/ { + # Enable buffering for potentially large OIDC configs + proxy_buffering on; + proxy_buffer_size 16k; + proxy_buffers 4 32k; + proxy_pass http://127.0.0.1:4002; } diff --git a/lerna.json b/lerna.json index be6673e7ae..4192189369 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.14", + "version": "3.2.25", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/package.json b/package.json index e354f36d2a..69eeaaa681 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@types/node": "20.10.0", "@types/proper-lockfile": "^4.1.4", "@typescript-eslint/parser": "6.9.0", + "cross-spawn": "7.0.6", "depcheck": "^1.4.7", "esbuild": "^0.18.17", "esbuild-node-externals": "^1.14.0", @@ -27,10 +28,9 @@ "proper-lockfile": "^4.1.2", "svelte": "4.2.19", "svelte-eslint-parser": "^0.33.1", - "typescript": "5.5.2", + "typescript": "5.7.2", "typescript-eslint": "^7.3.1", - "yargs": "^17.7.2", - "cross-spawn": "7.0.6" + "yargs": "^17.7.2" }, "scripts": { "get-past-client-version": "node scripts/getPastClientVersion.js", @@ -76,7 +76,6 @@ "build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting", "publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.3.3 -t budibase/couchdb:v3.3.3-sqs-v2.1.1 --push ./hosting/couchdb", "publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting", - "release:helm": "node scripts/releaseHelmChart", "env:multi:enable": "lerna run --stream env:multi:enable", "env:multi:disable": "lerna run --stream env:multi:disable", "env:selfhost:enable": "lerna run --stream env:selfhost:enable", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index a4381b4200..ba052a5fda 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -83,6 +83,7 @@ "@types/semver": "7.3.7", "@types/tar-fs": "2.0.1", "@types/uuid": "8.3.4", + "@types/koa": "2.13.4", "chance": "1.1.8", "ioredis-mock": "8.9.0", "jest": "29.7.0", @@ -90,9 +91,9 @@ "nock": "^13.5.6", "pino-pretty": "10.0.0", "pouchdb-adapter-memory": "7.2.2", - "testcontainers": "^10.7.2", + "testcontainers": "10.16.0", "timekeeper": "2.2.0", - "typescript": "5.5.2" + "typescript": "5.7.2" }, "nx": { "targets": { diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 69bf7009b2..6117f4b485 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -121,7 +121,7 @@ const identifyInstallationGroup = async ( const identifyTenantGroup = async ( tenantId: string, - account: Account | undefined, + hosting: Hosting, timestamp?: string | number ): Promise => { const id = await getEventTenantId(tenantId) @@ -129,26 +129,12 @@ const identifyTenantGroup = async ( const installationId = await getInstallationId() const environment = getDeploymentEnvironment() - let hosting: Hosting - let profession: string | undefined - let companySize: string | undefined - - if (account) { - profession = account.profession - companySize = account.size - hosting = account.hosting - } else { - hosting = getHostingFromEnv() - } - const group: TenantGroup = { id, type, hosting, environment, installationId, - profession, - companySize, } await identifyGroup(group, timestamp) diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts index b3f016e88a..650254fcb2 100644 --- a/packages/backend-core/src/features/features.ts +++ b/packages/backend-core/src/features/features.ts @@ -266,12 +266,14 @@ export class FlagSet, T extends { [key: string]: V }> { // new flag, add it here and use the `fetch` and `get` functions to access it. // All of the machinery in this file is to make sure that flags have their // default values set correctly and their types flow through the system. -export const flags = new FlagSet({ +const flagsConfig: Record> = { [FeatureFlag.DEFAULT_VALUES]: Flag.boolean(true), [FeatureFlag.AUTOMATION_BRANCHING]: Flag.boolean(true), [FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(true), [FeatureFlag.BUDIBASE_AI]: Flag.boolean(true), -}) + [FeatureFlag.USE_ZOD_VALIDATOR]: Flag.boolean(env.isDev()), +} +export const flags = new FlagSet(flagsConfig) type UnwrapPromise = T extends Promise ? U : T export type FeatureFlags = UnwrapPromise> diff --git a/packages/backend-core/src/middleware/auditLog.ts b/packages/backend-core/src/middleware/auditLog.ts index 9b76bb10b7..d529e8f908 100644 --- a/packages/backend-core/src/middleware/auditLog.ts +++ b/packages/backend-core/src/middleware/auditLog.ts @@ -1,6 +1,10 @@ -import { BBContext } from "@budibase/types" +import { Ctx } from "@budibase/types" +import type { Middleware, Next } from "koa" -export default async (ctx: BBContext | any, next: any) => { +// this middleware exists purely to be overridden by middlewares supplied by the @budibase/pro library +const middleware = (async (ctx: Ctx, next: Next) => { // Placeholder for audit log middleware return next() -} +}) as Middleware + +export default middleware diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index b713f509e0..6713cc7687 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -22,6 +22,7 @@ import { } from "@budibase/types" import { ErrorCode, InvalidAPIKeyError } from "../errors" import tracer from "dd-trace" +import type { Middleware, Next } from "koa" const ONE_MINUTE = env.SESSION_UPDATE_PERIOD ? parseInt(env.SESSION_UPDATE_PERIOD) @@ -94,6 +95,14 @@ async function checkApiKey( }) } +function getHeader(ctx: Ctx, header: Header): string | undefined { + const contents = ctx.request.headers[header] + if (Array.isArray(contents)) { + throw new Error("Unexpected header format") + } + return contents +} + /** * This middleware is tenancy aware, so that it does not depend on other middlewares being used. * The tenancy modules should not be used here and it should be assumed that the tenancy context @@ -106,9 +115,9 @@ export default function ( } ) { const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] - return async (ctx: Ctx | any, next: any) => { + return (async (ctx: Ctx, next: Next) => { let publicEndpoint = false - const version = ctx.request.headers[Header.API_VER] + const version = getHeader(ctx, Header.API_VER) // the path is not authenticated const found = matches(ctx, noAuthOptions) if (found) { @@ -116,18 +125,18 @@ export default function ( } try { // check the actual user is authenticated first, try header or cookie - let headerToken = ctx.request.headers[Header.TOKEN] + let headerToken = getHeader(ctx, Header.TOKEN) const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken) - let apiKey = ctx.request.headers[Header.API_KEY] + let apiKey = getHeader(ctx, Header.API_KEY) if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) { apiKey = ctx.request.headers[Header.AUTHORIZATION].split(" ")[1] } - const tenantId = ctx.request.headers[Header.TENANT_ID] + const tenantId = getHeader(ctx, Header.TENANT_ID) let authenticated: boolean = false, user: User | { tenantId: string } | undefined = undefined, internal: boolean = false, @@ -243,5 +252,5 @@ export default function ( ctx.throw(err.status || 403, err) } } - } + }) as Middleware } diff --git a/packages/backend-core/src/middleware/csrf.ts b/packages/backend-core/src/middleware/csrf.ts index cced4d5f7d..907c53a87d 100644 --- a/packages/backend-core/src/middleware/csrf.ts +++ b/packages/backend-core/src/middleware/csrf.ts @@ -1,6 +1,7 @@ import { Header } from "../constants" import { buildMatcherRegex, matches } from "./matchers" -import { BBContext, EndpointMatcher } from "@budibase/types" +import { Ctx, EndpointMatcher } from "@budibase/types" +import type { Middleware, Next } from "koa" /** * GET, HEAD and OPTIONS methods are considered safe operations @@ -36,7 +37,7 @@ export default function ( opts: { noCsrfPatterns: EndpointMatcher[] } = { noCsrfPatterns: [] } ) { const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns) - return async (ctx: BBContext | any, next: any) => { + return (async (ctx: Ctx, next: Next) => { // don't apply for excluded paths const found = matches(ctx, noCsrfOptions) if (found) { @@ -77,5 +78,5 @@ export default function ( } return next() - } + }) as Middleware } diff --git a/packages/backend-core/src/middleware/internalApi.ts b/packages/backend-core/src/middleware/internalApi.ts index dc73cd6b66..b240738aee 100644 --- a/packages/backend-core/src/middleware/internalApi.ts +++ b/packages/backend-core/src/middleware/internalApi.ts @@ -1,11 +1,11 @@ import { Header } from "../constants" -import { BBContext } from "@budibase/types" +import { Ctx } from "@budibase/types" import { isValidInternalAPIKey } from "../utils" /** * API Key only endpoint. */ -export default async (ctx: BBContext, next: any) => { +export default async (ctx: Ctx, next: any) => { const apiKey = ctx.request.headers[Header.API_KEY] if (!apiKey) { ctx.throw(403, "Unauthorized") diff --git a/packages/backend-core/src/middleware/matchers.ts b/packages/backend-core/src/middleware/matchers.ts index 757d93a60d..8ed0b4d608 100644 --- a/packages/backend-core/src/middleware/matchers.ts +++ b/packages/backend-core/src/middleware/matchers.ts @@ -1,4 +1,4 @@ -import { BBContext, EndpointMatcher, RegexMatcher } from "@budibase/types" +import { Ctx, EndpointMatcher, RegexMatcher } from "@budibase/types" const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g @@ -27,7 +27,7 @@ export const buildMatcherRegex = ( }) } -export const matches = (ctx: BBContext, options: RegexMatcher[]) => { +export const matches = (ctx: Ctx, options: RegexMatcher[]) => { return options.find(({ regex, method }) => { const urlMatch = regex.test(ctx.request.url) const methodMatch = diff --git a/packages/backend-core/src/middleware/passport/local.ts b/packages/backend-core/src/middleware/passport/local.ts index f1d72cab7a..40e6d294c1 100644 --- a/packages/backend-core/src/middleware/passport/local.ts +++ b/packages/backend-core/src/middleware/passport/local.ts @@ -2,7 +2,7 @@ import { UserStatus } from "../../constants" import { compare } from "../../utils" import * as users from "../../users" import { authError } from "./utils" -import { BBContext } from "@budibase/types" +import { Ctx } from "@budibase/types" const INVALID_ERR = "Invalid credentials" const EXPIRED = "This account has expired. Please reset your password" @@ -20,7 +20,7 @@ export const options = { * @returns The authenticated user, or errors if they occur */ export async function authenticate( - ctx: BBContext, + ctx: Ctx, email: string, password: string, done: Function diff --git a/packages/backend-core/src/middleware/tenancy.ts b/packages/backend-core/src/middleware/tenancy.ts index 22b7cc213d..de756c0af2 100644 --- a/packages/backend-core/src/middleware/tenancy.ts +++ b/packages/backend-core/src/middleware/tenancy.ts @@ -3,11 +3,12 @@ import { getTenantIDFromCtx } from "../tenancy" import { buildMatcherRegex, matches } from "./matchers" import { Header } from "../constants" import { - BBContext, + Ctx, EndpointMatcher, GetTenantIdOptions, TenantResolutionStrategy, } from "@budibase/types" +import type { Next, Middleware } from "koa" export default function ( allowQueryStringPatterns: EndpointMatcher[], @@ -17,7 +18,7 @@ export default function ( const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) - return async function (ctx: BBContext | any, next: any) { + return async function (ctx: Ctx, next: Next) { const allowNoTenant = opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) const tenantOpts: GetTenantIdOptions = { @@ -32,5 +33,5 @@ export default function ( const tenantId = getTenantIDFromCtx(ctx, tenantOpts) ctx.set(Header.TENANT_ID, tenantId as string) return doInTenant(tenantId, next) - } + } as Middleware } diff --git a/packages/backend-core/src/redis/tests/redis.spec.ts b/packages/backend-core/src/redis/tests/redis.spec.ts index 4ea41fdb7f..1ac33e1434 100644 --- a/packages/backend-core/src/redis/tests/redis.spec.ts +++ b/packages/backend-core/src/redis/tests/redis.spec.ts @@ -11,7 +11,7 @@ describe("redis", () => { let container: StartedTestContainer beforeAll(async () => { - const container = await new GenericContainer("redis") + container = await new GenericContainer("redis") .withExposedPorts(6379) .start() diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts index 929ae92909..263f042a9d 100644 --- a/packages/backend-core/src/security/permissions.ts +++ b/packages/backend-core/src/security/permissions.ts @@ -2,6 +2,8 @@ import { PermissionLevel, PermissionType, BuiltinPermissionID, + Permission, + BuiltinPermissions, } from "@budibase/types" import flatten from "lodash/flatten" import cloneDeep from "lodash/fp/cloneDeep" @@ -12,7 +14,7 @@ export type RoleHierarchy = { permissionId: string }[] -export class Permission { +export class PermissionImpl implements Permission { type: PermissionType level: PermissionLevel @@ -61,68 +63,62 @@ export function getAllowedLevels(userPermLevel: PermissionLevel): string[] { } } -export const BUILTIN_PERMISSIONS: { - [key in keyof typeof BuiltinPermissionID]: { - _id: (typeof BuiltinPermissionID)[key] - name: string - permissions: Permission[] - } -} = { +export const BUILTIN_PERMISSIONS: BuiltinPermissions = { PUBLIC: { _id: BuiltinPermissionID.PUBLIC, name: "Public", permissions: [ - new Permission(PermissionType.WEBHOOK, PermissionLevel.EXECUTE), + new PermissionImpl(PermissionType.WEBHOOK, PermissionLevel.EXECUTE), ], }, READ_ONLY: { _id: BuiltinPermissionID.READ_ONLY, name: "Read only", permissions: [ - new Permission(PermissionType.QUERY, PermissionLevel.READ), - new Permission(PermissionType.TABLE, PermissionLevel.READ), - new Permission(PermissionType.APP, PermissionLevel.READ), + new PermissionImpl(PermissionType.QUERY, PermissionLevel.READ), + new PermissionImpl(PermissionType.TABLE, PermissionLevel.READ), + new PermissionImpl(PermissionType.APP, PermissionLevel.READ), ], }, WRITE: { _id: BuiltinPermissionID.WRITE, name: "Read/Write", permissions: [ - new Permission(PermissionType.QUERY, PermissionLevel.WRITE), - new Permission(PermissionType.TABLE, PermissionLevel.WRITE), - new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), - new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), - new Permission(PermissionType.APP, PermissionLevel.READ), + new PermissionImpl(PermissionType.QUERY, PermissionLevel.WRITE), + new PermissionImpl(PermissionType.TABLE, PermissionLevel.WRITE), + new PermissionImpl(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), + new PermissionImpl(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new PermissionImpl(PermissionType.APP, PermissionLevel.READ), ], }, POWER: { _id: BuiltinPermissionID.POWER, name: "Power", permissions: [ - new Permission(PermissionType.TABLE, PermissionLevel.WRITE), - new Permission(PermissionType.USER, PermissionLevel.READ), - new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), - new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), - new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), - new Permission(PermissionType.APP, PermissionLevel.READ), + new PermissionImpl(PermissionType.TABLE, PermissionLevel.WRITE), + new PermissionImpl(PermissionType.USER, PermissionLevel.READ), + new PermissionImpl(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), + new PermissionImpl(PermissionType.WEBHOOK, PermissionLevel.READ), + new PermissionImpl(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new PermissionImpl(PermissionType.APP, PermissionLevel.READ), ], }, ADMIN: { _id: BuiltinPermissionID.ADMIN, name: "Admin", permissions: [ - new Permission(PermissionType.TABLE, PermissionLevel.ADMIN), - new Permission(PermissionType.USER, PermissionLevel.ADMIN), - new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN), - new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), - new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), - new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), - new Permission(PermissionType.APP, PermissionLevel.READ), + new PermissionImpl(PermissionType.TABLE, PermissionLevel.ADMIN), + new PermissionImpl(PermissionType.USER, PermissionLevel.ADMIN), + new PermissionImpl(PermissionType.AUTOMATION, PermissionLevel.ADMIN), + new PermissionImpl(PermissionType.WEBHOOK, PermissionLevel.READ), + new PermissionImpl(PermissionType.QUERY, PermissionLevel.ADMIN), + new PermissionImpl(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new PermissionImpl(PermissionType.APP, PermissionLevel.READ), ], }, } -export function getBuiltinPermissions() { +export function getBuiltinPermissions(): BuiltinPermissions { return cloneDeep(BUILTIN_PERMISSIONS) } diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index 4076be93a0..2c424a4900 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -592,7 +592,10 @@ export class AccessController { ) } - async checkScreensAccess(screens: Screen[], userRoleId: string) { + async checkScreensAccess( + screens: Screen[], + userRoleId: string + ): Promise { let accessibleScreens = [] // don't want to handle this with Promise.all as this would mean all custom roles would be // retrieved at same time, it is likely a custom role will be re-used and therefore want diff --git a/packages/backend-core/src/security/tests/permissions.spec.ts b/packages/backend-core/src/security/tests/permissions.spec.ts index f98833c7cd..d268a56a1f 100644 --- a/packages/backend-core/src/security/tests/permissions.spec.ts +++ b/packages/backend-core/src/security/tests/permissions.spec.ts @@ -133,7 +133,7 @@ describe("getBuiltinPermissionByID", () => { _id: BuiltinPermissionID.PUBLIC, name: "Public", permissions: [ - new permissions.Permission( + new permissions.PermissionImpl( permissions.PermissionType.WEBHOOK, permissions.PermissionLevel.EXECUTE ), diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index e4b2b843af..5f462ee144 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -18,6 +18,7 @@ import { BasicOperator, BBReferenceFieldMetadata, CalculationType, + EnrichedQueryJson, FieldSchema, FieldType, INTERNAL_TABLE_SOURCE_ID, @@ -27,7 +28,6 @@ import { LogicalOperator, Operation, prefixed, - QueryJson, QueryOptions, RangeOperator, RelationshipsJson, @@ -134,18 +134,18 @@ const allowEmptyRelationships: Record = { class InternalBuilder { private readonly client: SqlClient - private readonly query: QueryJson + private readonly query: EnrichedQueryJson private readonly splitter: dataFilters.ColumnSplitter private readonly knex: Knex - constructor(client: SqlClient, knex: Knex, query: QueryJson) { + constructor(client: SqlClient, knex: Knex, query: EnrichedQueryJson) { this.client = client this.query = query this.knex = knex this.splitter = new dataFilters.ColumnSplitter([this.table], { aliases: this.query.tableAliases, - columnPrefix: this.query.meta.columnPrefix, + columnPrefix: this.query.meta?.columnPrefix, }) } @@ -167,7 +167,7 @@ class InternalBuilder { } get table(): Table { - return this.query.meta.table + return this.query.table } get knexClient(): Knex.Client { @@ -273,8 +273,7 @@ class InternalBuilder { } private isFullSelectStatementRequired(): boolean { - const { meta } = this.query - for (let column of Object.values(meta.table.schema)) { + for (let column of Object.values(this.table.schema)) { if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) { return true } else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) { @@ -285,14 +284,14 @@ class InternalBuilder { } private generateSelectStatement(): (string | Knex.Raw)[] | "*" { - const { meta, endpoint, resource } = this.query + const { table, resource } = this.query if (!resource || !resource.fields || resource.fields.length === 0) { return "*" } - const alias = this.getTableName(endpoint.entityId) - const schema = meta.table.schema + const alias = this.getTableName(table) + const schema = this.table.schema if (!this.isFullSelectStatementRequired()) { return [this.knex.raw("??", [`${alias}.*`])] } @@ -497,9 +496,8 @@ class InternalBuilder { filterKey: string, whereCb: (filterKey: string, query: Knex.QueryBuilder) => Knex.QueryBuilder ): Knex.QueryBuilder { - const { relationships, endpoint, tableAliases: aliases } = this.query - const tableName = endpoint.entityId - const fromAlias = aliases?.[tableName] || tableName + const { relationships, schema, tableAliases: aliases, table } = this.query + const fromAlias = aliases?.[table.name] || table.name const matches = (value: string) => filterKey.match(new RegExp(`^${value}\\.`)) if (!relationships) { @@ -539,7 +537,7 @@ class InternalBuilder { aliases?.[manyToMany.through] || relationship.through let throughTable = this.tableNameWithSchema(manyToMany.through, { alias: throughAlias, - schema: endpoint.schema, + schema, }) subQuery = subQuery // add a join through the junction table @@ -1012,28 +1010,10 @@ class InternalBuilder { return isSqs(this.table) } - getTableName(tableOrName?: Table | string): string { - let table: Table - if (typeof tableOrName === "string") { - const name = tableOrName - if (this.query.table?.name === name) { - table = this.query.table - } else if (this.query.meta.table?.name === name) { - table = this.query.meta.table - } else if (!this.query.meta.tables?.[name]) { - // This can legitimately happen in custom queries, where the user is - // querying against a table that may not have been imported into - // Budibase. - return name - } else { - table = this.query.meta.tables[name] - } - } else if (tableOrName) { - table = tableOrName - } else { + getTableName(table?: Table): string { + if (!table) { table = this.table } - let name = table.name if (isSqs(table) && table._id) { // SQS uses the table ID rather than the table name @@ -1191,8 +1171,9 @@ class InternalBuilder { return withSchema } - private buildJsonField(field: string): string { + private buildJsonField(table: Table, field: string): [string, Knex.Raw] { const parts = field.split(".") + let baseName = parts[parts.length - 1] let unaliased: string let tableField: string @@ -1205,10 +1186,19 @@ class InternalBuilder { tableField = unaliased } - const separator = this.client === SqlClient.ORACLE ? " VALUE " : "," - return this.knex - .raw(`?${separator}??`, [unaliased, this.rawQuotedIdentifier(tableField)]) - .toString() + if (this.query.meta?.columnPrefix) { + baseName = baseName.replace(this.query.meta.columnPrefix, "") + } + + let identifier = this.rawQuotedIdentifier(tableField) + // Internal tables have special _id, _rev, createdAt, and updatedAt fields + // that do not appear in the schema, meaning schema could actually be + // undefined. + const schema: FieldSchema | undefined = table.schema[baseName] + if (schema && schema.type === FieldType.BIGINT) { + identifier = this.castIntToString(identifier) + } + return [unaliased, identifier] } maxFunctionParameters() { @@ -1234,7 +1224,7 @@ class InternalBuilder { ): Knex.QueryBuilder { const sqlClient = this.client const knex = this.knex - const { resource, tableAliases: aliases, endpoint, meta } = this.query + const { resource, tableAliases: aliases, schema, tables } = this.query const fields = resource?.fields || [] for (let relationship of relationships) { const { @@ -1249,13 +1239,16 @@ class InternalBuilder { if (!toTable || !fromTable) { continue } - const relatedTable = meta.tables?.[toTable] + const relatedTable = tables[toTable] + if (!relatedTable) { + throw new Error(`related table "${toTable}" not found in datasource`) + } const toAlias = aliases?.[toTable] || toTable, fromAlias = aliases?.[fromTable] || fromTable, throughAlias = (throughTable && aliases?.[throughTable]) || throughTable let toTableWithSchema = this.tableNameWithSchema(toTable, { alias: toAlias, - schema: endpoint.schema, + schema, }) const requiredFields = [ ...(relatedTable?.primary || []), @@ -1271,8 +1264,14 @@ class InternalBuilder { 0, Math.floor(this.maxFunctionParameters() / 2) ) - const fieldList: string = relationshipFields - .map(field => this.buildJsonField(field)) + const fieldList = relationshipFields.map(field => + this.buildJsonField(relatedTable, field) + ) + const fieldListFormatted = fieldList + .map(f => { + const separator = this.client === SqlClient.ORACLE ? " VALUE " : "," + return this.knex.raw(`?${separator}??`, [f[0], f[1]]).toString() + }) .join(",") // SQL Server uses TOP - which performs a little differently to the normal LIMIT syntax // it reduces the result set rather than limiting how much data it filters over @@ -1293,7 +1292,7 @@ class InternalBuilder { if (isManyToMany) { let throughTableWithSchema = this.tableNameWithSchema(throughTable, { alias: throughAlias, - schema: endpoint.schema, + schema, }) subQuery = subQuery.join(throughTableWithSchema, function () { this.on(`${toAlias}.${toPrimary}`, "=", `${throughAlias}.${toKey}`) @@ -1320,35 +1319,42 @@ class InternalBuilder { // need to check the junction table document is to the right column, this is just for SQS subQuery = this.addJoinFieldCheck(subQuery, relationship) wrapperQuery = standardWrap( - this.knex.raw(`json_group_array(json_object(${fieldList}))`) + this.knex.raw( + `json_group_array(json_object(${fieldListFormatted}))` + ) ) break case SqlClient.POSTGRES: wrapperQuery = standardWrap( - this.knex.raw(`json_agg(json_build_object(${fieldList}))`) + this.knex.raw(`json_agg(json_build_object(${fieldListFormatted}))`) ) break case SqlClient.MARIADB: // can't use the standard wrap due to correlated sub-query limitations in MariaDB wrapperQuery = subQuery.select( knex.raw( - `json_arrayagg(json_object(${fieldList}) LIMIT ${getRelationshipLimit()})` + `json_arrayagg(json_object(${fieldListFormatted}) LIMIT ${getRelationshipLimit()})` ) ) break case SqlClient.MY_SQL: case SqlClient.ORACLE: wrapperQuery = standardWrap( - this.knex.raw(`json_arrayagg(json_object(${fieldList}))`) + this.knex.raw(`json_arrayagg(json_object(${fieldListFormatted}))`) ) break case SqlClient.MS_SQL: { const comparatorQuery = knex - .select(`${fromAlias}.*`) + .select(`*`) // @ts-ignore - from alias syntax not TS supported .from({ [fromAlias]: subQuery - .select(`${toAlias}.*`) + .select( + fieldList.map(f => { + // @ts-expect-error raw is fine here, knex types are wrong + return knex.ref(f[1]).as(f[0]) + }) + ) .limit(getRelationshipLimit()), }) @@ -1377,8 +1383,7 @@ class InternalBuilder { toPrimary?: string }[] ): Knex.QueryBuilder { - const { tableAliases: aliases, endpoint } = this.query - const schema = endpoint.schema + const { tableAliases: aliases, schema } = this.query const toTable = tables.to, fromTable = tables.from, throughTable = tables.through @@ -1429,16 +1434,16 @@ class InternalBuilder { } qualifiedKnex(opts?: { alias?: string | boolean }): Knex.QueryBuilder { - let alias = this.query.tableAliases?.[this.query.endpoint.entityId] + let alias = this.query.tableAliases?.[this.query.table.name] if (opts?.alias === false) { alias = undefined } else if (typeof opts?.alias === "string") { alias = opts.alias } return this.knex( - this.tableNameWithSchema(this.query.endpoint.entityId, { + this.tableNameWithSchema(this.query.table.name, { alias, - schema: this.query.endpoint.schema, + schema: this.query.schema, }) ) } @@ -1455,9 +1460,7 @@ class InternalBuilder { if (this.client === SqlClient.ORACLE) { // Oracle doesn't seem to automatically insert nulls // if we don't specify them, so we need to do that here - for (const [column, schema] of Object.entries( - this.query.meta.table.schema - )) { + for (const [column, schema] of Object.entries(this.query.table.schema)) { if ( schema.constraints?.presence === true || schema.type === FieldType.FORMULA || @@ -1534,11 +1537,9 @@ class InternalBuilder { limits?: { base: number; query: number } } = {} ): Knex.QueryBuilder { - let { endpoint, filters, paginate, relationships } = this.query + let { operation, filters, paginate, relationships, table } = this.query const { limits } = opts - const counting = endpoint.operation === Operation.COUNT - const tableName = endpoint.entityId // start building the query let query = this.qualifiedKnex() // handle pagination @@ -1557,7 +1558,7 @@ class InternalBuilder { foundLimit = paginate.limit } // counting should not sort, limit or offset - if (!counting) { + if (operation !== Operation.COUNT) { // add the found limit if supplied if (foundLimit != null) { query = query.limit(foundLimit) @@ -1569,7 +1570,7 @@ class InternalBuilder { } const aggregations = this.query.resource?.aggregations || [] - if (counting) { + if (operation === Operation.COUNT) { query = this.addDistinctCount(query) } else if (aggregations.length > 0) { query = this.addAggregations(query, aggregations) @@ -1578,7 +1579,7 @@ class InternalBuilder { } // have to add after as well (this breaks MS-SQL) - if (!counting) { + if (operation !== Operation.COUNT) { query = this.addSorting(query) } @@ -1586,9 +1587,7 @@ class InternalBuilder { // handle relationships with a CTE for all others if (relationships?.length && aggregations.length === 0) { - const mainTable = - this.query.tableAliases?.[this.query.endpoint.entityId] || - this.query.endpoint.entityId + const mainTable = this.query.tableAliases?.[table.name] || table.name const cte = this.addSorting( this.knex .with("paginated", query) @@ -1598,7 +1597,7 @@ class InternalBuilder { }) ) // add JSON aggregations attached to the CTE - return this.addJsonRelationships(cte, tableName, relationships) + return this.addJsonRelationships(cte, table.name, relationships) } return query @@ -1661,7 +1660,10 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { * which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes. * @return the query ready to be passed to the driver. */ - _query(json: QueryJson, opts: QueryOptions = {}): SqlQuery | SqlQuery[] { + _query( + json: EnrichedQueryJson, + opts: QueryOptions = {} + ): SqlQuery | SqlQuery[] { const sqlClient = this.getSqlClient() const config: Knex.Config = { client: this.getBaseSqlClient(), @@ -1711,34 +1713,30 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { return this.convertToNative(query, opts) } - async getReturningRow(queryFn: QueryFunction, json: QueryJson) { + async getReturningRow(queryFn: QueryFunction, json: EnrichedQueryJson) { if (!json.extra || !json.extra.idFilter) { return {} } const input = this._query({ - endpoint: { - ...json.endpoint, - operation: Operation.READ, - }, - resource: { - fields: [], - }, + operation: Operation.READ, + datasource: json.datasource, + schema: json.schema, + table: json.table, + tables: json.tables, + resource: { fields: [] }, filters: json.extra?.idFilter, - paginate: { - limit: 1, - }, - meta: json.meta, + paginate: { limit: 1 }, }) return queryFn(input, Operation.READ) } // when creating if an ID has been inserted need to make sure // the id filter is enriched with it before trying to retrieve the row - checkLookupKeys(id: any, json: QueryJson) { - if (!id || !json.meta.table || !json.meta.table.primary) { + checkLookupKeys(id: any, json: EnrichedQueryJson) { + if (!id || !json.table.primary) { return json } - const primaryKey = json.meta.table.primary?.[0] + const primaryKey = json.table.primary[0] json.extra = { idFilter: { equal: { @@ -1751,7 +1749,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { // this function recreates the returning functionality of postgres async queryWithReturning( - json: QueryJson, + json: EnrichedQueryJson, queryFn: QueryFunction, processFn: Function = (result: any) => result ) { diff --git a/packages/backend-core/src/sql/sqlTable.ts b/packages/backend-core/src/sql/sqlTable.ts index 84f4e290aa..8e15d3d4ef 100644 --- a/packages/backend-core/src/sql/sqlTable.ts +++ b/packages/backend-core/src/sql/sqlTable.ts @@ -3,13 +3,13 @@ import { FieldType, NumberFieldMetadata, Operation, - QueryJson, RelationshipType, RenameColumn, SqlQuery, Table, TableSourceType, SqlClient, + EnrichedQueryJson, } from "@budibase/types" import { breakExternalTableId, getNativeSql } from "./utils" import { helpers, utils } from "@budibase/shared-core" @@ -25,7 +25,7 @@ function generateSchema( schema: CreateTableBuilder, table: Table, tables: Record, - oldTable: null | Table = null, + oldTable?: Table, renamed?: RenameColumn ) { let primaryKeys = table && table.primary ? table.primary : [] @@ -55,7 +55,7 @@ function generateSchema( ) for (let [key, column] of Object.entries(table.schema)) { // skip things that are already correct - const oldColumn = oldTable ? oldTable.schema[key] : null + const oldColumn = oldTable?.schema[key] if ( (oldColumn && oldColumn.type) || columnTypeSet.includes(key) || @@ -199,8 +199,8 @@ function buildUpdateTable( knex: SchemaBuilder, table: Table, tables: Record, - oldTable: Table, - renamed: RenameColumn + oldTable?: Table, + renamed?: RenameColumn ): SchemaBuilder { return knex.alterTable(table.name, schema => { generateSchema(schema, table, tables, oldTable, renamed) @@ -238,19 +238,18 @@ class SqlTableQueryBuilder { * @param json the input JSON structure from which an SQL query will be built. * @return the operation that was found in the JSON. */ - _operation(json: QueryJson): Operation { - return json.endpoint.operation + _operation(json: EnrichedQueryJson): Operation { + return json.operation } - _tableQuery(json: QueryJson): SqlQuery | SqlQuery[] { + _tableQuery(json: EnrichedQueryJson): SqlQuery | SqlQuery[] { let client = knex({ client: this.sqlClient }).schema - let schemaName = json?.endpoint?.schema - if (schemaName) { - client = client.withSchema(schemaName) + if (json?.schema) { + client = client.withSchema(json.schema) } let query: Knex.SchemaBuilder - if (!json.table || !json.meta || !json.meta.tables) { + if (!json.table || !json.tables) { throw new Error("Cannot execute without table being specified") } if (json.table.sourceType === TableSourceType.INTERNAL) { @@ -259,17 +258,17 @@ class SqlTableQueryBuilder { switch (this._operation(json)) { case Operation.CREATE_TABLE: - query = buildCreateTable(client, json.table, json.meta.tables) + query = buildCreateTable(client, json.table, json.tables) break case Operation.UPDATE_TABLE: - if (!json.meta || !json.meta.table) { + if (!json.table) { throw new Error("Must specify old table for update") } // renameColumn does not work for MySQL, so return a raw query - if (this.sqlClient === SqlClient.MY_SQL && json.meta.renamed) { + if (this.sqlClient === SqlClient.MY_SQL && json.meta?.renamed) { const updatedColumn = json.meta.renamed.updated - const tableName = schemaName - ? `\`${schemaName}\`.\`${json.table.name}\`` + const tableName = json?.schema + ? `\`${json.schema}\`.\`${json.table.name}\`` : `\`${json.table.name}\`` return { sql: `alter table ${tableName} rename column \`${json.meta.renamed.old}\` to \`${updatedColumn}\`;`, @@ -280,18 +279,18 @@ class SqlTableQueryBuilder { query = buildUpdateTable( client, json.table, - json.meta.tables, - json.meta.table, - json.meta.renamed! + json.tables, + json.meta?.oldTable, + json.meta?.renamed ) // renameColumn for SQL Server returns a parameterised `sp_rename` query, // which is not supported by SQL Server and gives a syntax error. - if (this.sqlClient === SqlClient.MS_SQL && json.meta.renamed) { + if (this.sqlClient === SqlClient.MS_SQL && json.meta?.renamed) { const oldColumn = json.meta.renamed.old const updatedColumn = json.meta.renamed.updated - const tableName = schemaName - ? `${schemaName}.${json.table.name}` + const tableName = json?.schema + ? `${json.schema}.${json.table.name}` : `${json.table.name}` const sql = getNativeSql(query) if (Array.isArray(sql)) { diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 8835960ca5..6096b45800 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -6,7 +6,7 @@ import { getPlatformURL, } from "../context" import { - BBContext, + Ctx, TenantResolutionStrategy, GetTenantIdOptions, } from "@budibase/types" @@ -37,7 +37,7 @@ export const isUserInAppTenant = (appId: string, user?: any) => { const ALL_STRATEGIES = Object.values(TenantResolutionStrategy) export const getTenantIDFromCtx = ( - ctx: BBContext, + ctx: Ctx, opts: GetTenantIdOptions ): string | undefined => { // exit early if not multi-tenant diff --git a/packages/backend-core/src/utils/tests/utils.spec.ts b/packages/backend-core/src/utils/tests/utils.spec.ts index 4dc3855c35..b5c6283ce2 100644 --- a/packages/backend-core/src/utils/tests/utils.spec.ts +++ b/packages/backend-core/src/utils/tests/utils.spec.ts @@ -5,7 +5,7 @@ import * as db from "../../db" import { Header } from "../../constants" import { newid } from "../../utils" import env from "../../environment" -import { BBContext } from "@budibase/types" +import { Ctx } from "@budibase/types" describe("utils", () => { const config = new DBTestConfiguration() @@ -109,7 +109,7 @@ describe("utils", () => { }) describe("isServingBuilder", () => { - let ctx: BBContext + let ctx: Ctx const expectResult = (result: boolean) => expect(utils.isServingBuilder(ctx)).toBe(result) @@ -133,7 +133,7 @@ describe("utils", () => { }) describe("isServingBuilderPreview", () => { - let ctx: BBContext + let ctx: Ctx const expectResult = (result: boolean) => expect(utils.isServingBuilderPreview(ctx)).toBe(result) @@ -157,7 +157,7 @@ describe("utils", () => { }) describe("isPublicAPIRequest", () => { - let ctx: BBContext + let ctx: Ctx const expectResult = (result: boolean) => expect(utils.isPublicApiRequest(ctx)).toBe(result) diff --git a/packages/backend-core/tests/core/utilities/structures/koa.ts b/packages/backend-core/tests/core/utilities/structures/koa.ts index f9883dc1b9..c804d3b5f2 100644 --- a/packages/backend-core/tests/core/utilities/structures/koa.ts +++ b/packages/backend-core/tests/core/utilities/structures/koa.ts @@ -1,8 +1,8 @@ import { createMockContext, createMockCookies } from "@shopify/jest-koa-mocks" -import { BBContext } from "@budibase/types" +import { Ctx } from "@budibase/types" -export const newContext = (): BBContext => { - const ctx = createMockContext() as any +export const newContext = (): Ctx => { + const ctx = createMockContext() as Ctx return { ...ctx, path: "/", diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index 71d7fa32db..7a8a6262cc 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -25,7 +25,7 @@ function getTestcontainers(): ContainerInfo[] { // We use --format json to make sure the output is nice and machine-readable, // and we use --no-trunc so that the command returns full container IDs so we // can filter on them correctly. - return execSync("docker ps --format json --no-trunc") + return execSync("docker ps --all --format json --no-trunc") .toString() .split("\n") .filter(x => x.length > 0) @@ -70,7 +70,36 @@ export function getExposedV4Port(container: ContainerInfo, port: number) { return getExposedV4Ports(container).find(x => x.container === port)?.host } +interface DockerContext { + Name: string + Description: string + DockerEndpoint: string + ContextType: string + Error: string +} + +function getCurrentDockerContext(): DockerContext { + const out = execSync("docker context ls --format json") + for (const line of out.toString().split("\n")) { + const parsed = JSON.parse(line) + if (parsed.Current) { + return parsed as DockerContext + } + } + throw new Error("No current Docker context") +} + export function setupEnv(...envs: any[]) { + process.env.TESTCONTAINERS_RYUK_DISABLED = "true" + + // For whatever reason, testcontainers doesn't always use the correct current + // docker context. This bit of code forces the issue by finding the current + // context and setting it as the DOCKER_HOST environment + if (!process.env.DOCKER_HOST) { + const dockerContext = getCurrentDockerContext() + process.env.DOCKER_HOST = dockerContext.DockerEndpoint + } + // We start couchdb in globalSetup.ts, in the root of the monorepo, so it // should be relatively safe to look for it by its image name. const couch = getContainerByImage("budibase/couchdb") @@ -116,11 +145,12 @@ export async function startContainer(container: GenericContainer) { key = imageName.split("@")[0] } key = key.replace(/\//g, "-").replace(/:/g, "-") + const name = `${key}_testcontainer` container = container .withReuse() .withLabels({ "com.budibase": "true" }) - .withName(`${key}_testcontainer`) + .withName(name) let startedContainer: StartedTestContainer | undefined = undefined let lastError = undefined diff --git a/packages/bbui/package.json b/packages/bbui/package.json index aeb7418526..35f6676fdc 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -4,27 +4,21 @@ "version": "0.0.0", "license": "MPL-2.0", "svelte": "src/index.js", - "module": "dist/bbui.es.js", + "module": "dist/bbui.mjs", "exports": { ".": { - "import": "./dist/bbui.es.js" + "import": "./dist/bbui.mjs" }, "./package.json": "./package.json", "./spectrum-icons-rollup.js": "./src/spectrum-icons-rollup.js", "./spectrum-icons-vite.js": "./src/spectrum-icons-vite.js" }, "scripts": { - "build": "rollup -c" + "build": "vite build" }, "devDependencies": { - "@rollup/plugin-commonjs": "^16.0.0", - "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "postcss": "^8.2.9", - "rollup": "^2.45.2", - "rollup-plugin-postcss": "^4.0.0", - "rollup-plugin-svelte": "^7.1.0", - "rollup-plugin-terser": "^7.0.2" + "@sveltejs/vite-plugin-svelte": "1.4.0", + "vite-plugin-css-injected-by-js": "3.5.2" }, "keywords": [ "svelte" @@ -96,8 +90,7 @@ "dependsOn": [ { "projects": [ - "@budibase/string-templates", - "@budibase/shared-core" + "@budibase/string-templates" ], "target": "build" } diff --git a/packages/bbui/rollup.config.js b/packages/bbui/rollup.config.js deleted file mode 100644 index da274e0ba5..0000000000 --- a/packages/bbui/rollup.config.js +++ /dev/null @@ -1,32 +0,0 @@ -import svelte from "rollup-plugin-svelte" -import resolve from "@rollup/plugin-node-resolve" -import commonjs from "@rollup/plugin-commonjs" -import json from "@rollup/plugin-json" -import { terser } from "rollup-plugin-terser" -import postcss from "rollup-plugin-postcss" - -export default { - input: "src/index.js", - output: { - sourcemap: true, - format: "esm", - file: "dist/bbui.es.js", - }, - onwarn(warning, warn) { - // suppress eval warnings - if (warning.code === "EVAL") { - return - } - warn(warning) - }, - plugins: [ - resolve(), - commonjs(), - svelte({ - emitCss: true, - }), - postcss(), - terser(), - json(), - ], -} diff --git a/packages/bbui/vite.config.js b/packages/bbui/vite.config.js new file mode 100644 index 0000000000..bf0f9fc26d --- /dev/null +++ b/packages/bbui/vite.config.js @@ -0,0 +1,29 @@ +import { defineConfig } from "vite" +import { svelte } from "@sveltejs/vite-plugin-svelte" +import path from "path" +import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js" + +export default defineConfig(({ mode }) => { + const isProduction = mode === "production" + return { + build: { + sourcemap: !isProduction, + lib: { + entry: "src/index.js", + formats: ["es"], + }, + }, + plugins: [ + svelte({ + emitCss: true, + }), + cssInjectedByJsPlugin(), + ], + resolve: { + alias: { + "@budibase/shared-core": path.resolve(__dirname, "../shared-core/src"), + "@budibase/types": path.resolve(__dirname, "../types/src"), + }, + }, + } +}) diff --git a/packages/builder/src/components/common/bindings/SnippetDrawer.svelte b/packages/builder/src/components/common/bindings/SnippetDrawer.svelte index d6b6f92b17..d8e56b059a 100644 --- a/packages/builder/src/components/common/bindings/SnippetDrawer.svelte +++ b/packages/builder/src/components/common/bindings/SnippetDrawer.svelte @@ -63,7 +63,7 @@ if (!name?.length) { return "Name is required" } - if (snippets.some(snippet => snippet.name === name)) { + if (!snippet?.name && snippets.some(snippet => snippet.name === name)) { return "That name is already in use" } if (firstCharNumberRegex.test(name)) { @@ -106,11 +106,7 @@ Delete {/if} - diff --git a/packages/builder/src/components/common/bindings/SnippetSidePanel.svelte b/packages/builder/src/components/common/bindings/SnippetSidePanel.svelte index c68699fc0f..2cc27b91cd 100644 --- a/packages/builder/src/components/common/bindings/SnippetSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/SnippetSidePanel.svelte @@ -186,7 +186,7 @@
{#key hoveredSnippet} diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte index 410af53648..9eef8c1c06 100644 --- a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte @@ -52,9 +52,16 @@ let modal $: text = value?.label ?? "Choose an option" - $: tables = $tablesStore.list.map(table => - format.table(table, $datasources.list) - ) + $: tables = $tablesStore.list + .map(table => format.table(table, $datasources.list)) + .sort((a, b) => { + // sort tables alphabetically, grouped by datasource + const dsComparison = a.datasourceName.localeCompare(b.datasourceName) + if (dsComparison !== 0) { + return dsComparison + } + return a.label.localeCompare(b.label) + }) $: viewsV1 = $viewsStore.list.map(view => ({ ...view, label: view.name, diff --git a/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte index acc1b8e2d9..c5c28d1e1e 100644 --- a/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/automation/_layout.svelte @@ -1,5 +1,5 @@