diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index e4825ea5d0..c087627100 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -40,6 +40,24 @@ spec: - image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }} imagePullPolicy: Always name: proxy-service + livenessProbe: + httpGet: + path: /health + port: {{ .Values.services.proxy.port }} + initialDelaySeconds: 0 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 2 + timeoutSeconds: 3 + readinessProbe: + httpGet: + path: /health + port: {{ .Values.services.proxy.port }} + initialDelaySeconds: 0 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 2 + timeoutSeconds: 3 ports: - containerPort: {{ .Values.services.proxy.port }} env: diff --git a/lerna.json b/lerna.json index 3d9a4bb064..3746f256b5 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.8.18-alpha.1", + "version": "2.8.22-alpha.2", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/src/cache/appMetadata.ts b/packages/backend-core/src/cache/appMetadata.ts index 5b66c356d3..0c320ec776 100644 --- a/packages/backend-core/src/cache/appMetadata.ts +++ b/packages/backend-core/src/cache/appMetadata.ts @@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init" import { doWithDB, DocumentType } from "../db" import { Database, App } from "@budibase/types" -const AppState = { - INVALID: "invalid", +export enum AppState { + INVALID = "invalid", } + +export interface DeletedApp { + state: AppState +} + const EXPIRY_SECONDS = 3600 /** @@ -31,7 +36,7 @@ function isInvalid(metadata?: { state: string }) { * @param {string} appId the id of the app to get metadata from. * @returns {object} the app metadata. */ -export async function getAppMetadata(appId: string) { +export async function getAppMetadata(appId: string): Promise { const client = await getAppClient() // try cache let metadata = await client.get(appId) @@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) { } await client.store(appId, metadata, expiry) } - // we've stored in the cache an object to tell us that it is currently invalid - if (isInvalid(metadata)) { - throw { status: 404, message: "No app metadata found" } - } - return metadata as App + + return metadata } /** diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 6034296996..4ebf8392b5 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -2,7 +2,7 @@ import env from "../environment" import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants" import { getTenantId, getGlobalDBName } from "../context" import { doWithDB, directCouchAllDbs } from "./db" -import { getAppMetadata } from "../cache/appMetadata" +import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { App, Database } from "@budibase/types" import { getStartEndKeyURL } from "../docIds" @@ -101,7 +101,9 @@ export async function getAllApps({ const response = await Promise.allSettled(appPromises) const apps = response .filter( - (result: any) => result.status === "fulfilled" && result.value != null + (result: any) => + result.status === "fulfilled" && + result.value?.state !== AppState.INVALID ) .map(({ value }: any) => value) if (!all) { @@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) { ) // have to list the apps which exist, some may have been deleted return settled - .filter(promise => promise.status === "fulfilled") + .filter( + promise => + promise.status === "fulfilled" && + (promise.value as DeletedApp).state !== AppState.INVALID + ) .map(promise => (promise as PromiseFulfilledResult).value) } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 5076b7569b..b8d2eb2a54 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -163,6 +163,7 @@ const environment = { : false, ...getPackageJsonFields(), DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, + OFFLINE_MODE: process.env.OFFLINE_MODE, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/errors/errors.ts b/packages/backend-core/src/errors/errors.ts index 4e1f1abbb5..7d55d25e89 100644 --- a/packages/backend-core/src/errors/errors.ts +++ b/packages/backend-core/src/errors/errors.ts @@ -55,6 +55,18 @@ export class HTTPError extends BudibaseError { } } +export class NotFoundError extends HTTPError { + constructor(message: string) { + super(message, 404) + } +} + +export class BadRequestError extends HTTPError { + constructor(message: string) { + super(message, 400) + } +} + // LICENSING export class UsageLimitError extends HTTPError { diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 5eb11d1354..948d3b692b 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -264,7 +264,7 @@ const getEventTenantId = async (tenantId: string): Promise => { } } -const getUniqueTenantId = async (tenantId: string): Promise => { +export const getUniqueTenantId = async (tenantId: string): Promise => { // make sure this tenantId always matches the tenantId in context return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { diff --git a/packages/backend-core/src/logging/pino/logger.ts b/packages/backend-core/src/logging/pino/logger.ts index f2024db72b..7c444a3a59 100644 --- a/packages/backend-core/src/logging/pino/logger.ts +++ b/packages/backend-core/src/logging/pino/logger.ts @@ -12,29 +12,44 @@ import { localFileDestination } from "../system" let pinoInstance: pino.Logger | undefined if (!env.DISABLE_PINO_LOGGER) { + const level = env.LOG_LEVEL const pinoOptions: LoggerOptions = { - level: env.LOG_LEVEL, + level, formatters: { - level: label => { - return { level: label.toUpperCase() } + level: level => { + return { level: level.toUpperCase() } }, bindings: () => { - return { - service: env.SERVICE_NAME, + if (env.SELF_HOSTED) { + // "service" is being injected in datadog using the pod names, + // so we should leave it blank to allow the default behaviour if it's not running self-hosted + return { + service: env.SERVICE_NAME, + } + } else { + return {} } }, }, timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, } - const destinations: pino.DestinationStream[] = [] + const destinations: pino.StreamEntry[] = [] - if (env.isDev()) { - destinations.push(pinoPretty({ singleLine: true })) - } + destinations.push( + env.isDev() + ? { + stream: pinoPretty({ singleLine: true }), + level: level as pino.Level, + } + : { stream: process.stdout, level: level as pino.Level } + ) if (env.SELF_HOSTED) { - destinations.push(localFileDestination()) + destinations.push({ + stream: localFileDestination(), + level: level as pino.Level, + }) } pinoInstance = destinations.length diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 807153cd09..8476399aa3 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -13,7 +13,7 @@ import { } from "@budibase/types" import _ from "lodash" -export const account = (): Account => { +export const account = (partial: Partial = {}): Account => { return { accountId: uuid(), tenantId: generator.word(), @@ -29,6 +29,7 @@ export const account = (): Account => { size: "10+", profession: "Software Engineer", quotaUsage: quotas.usage(), + ...partial, } } diff --git a/packages/backend-core/tests/core/utilities/structures/db.ts b/packages/backend-core/tests/core/utilities/structures/db.ts index 31a52dce8b..87325573eb 100644 --- a/packages/backend-core/tests/core/utilities/structures/db.ts +++ b/packages/backend-core/tests/core/utilities/structures/db.ts @@ -1,4 +1,4 @@ -import { structures } from ".." +import { generator } from "./generator" import { newid } from "../../../../src/docIds/newid" export function id() { @@ -6,7 +6,7 @@ export function id() { } export function rev() { - return `${structures.generator.character({ + return `${generator.character({ numeric: true, - })}-${structures.uuid().replace(/-/, "")}` + })}-${generator.guid().replace(/-/, "")}` } diff --git a/packages/backend-core/tests/core/utilities/structures/documents/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/index.ts new file mode 100644 index 0000000000..c3bfba3597 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/index.ts @@ -0,0 +1 @@ +export * from "./platform" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts new file mode 100644 index 0000000000..98a6314999 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts @@ -0,0 +1 @@ +export * as installation from "./installation" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts new file mode 100644 index 0000000000..711c6cf14f --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts @@ -0,0 +1,12 @@ +import { generator } from "../../generator" +import { Installation } from "@budibase/types" +import * as db from "../../db" + +export function install(): Installation { + return { + _id: "install", + _rev: db.rev(), + installId: generator.guid(), + version: generator.string(), + } +} diff --git a/packages/backend-core/tests/core/utilities/structures/index.ts b/packages/backend-core/tests/core/utilities/structures/index.ts index 2c094f43a7..1a49e912fc 100644 --- a/packages/backend-core/tests/core/utilities/structures/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/index.ts @@ -2,6 +2,7 @@ export * from "./common" export * as accounts from "./accounts" export * as apps from "./apps" export * as db from "./db" +export * as docs from "./documents" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 22e73f2871..5cce84edfd 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -3,6 +3,8 @@ import { Customer, Feature, License, + OfflineIdentifier, + OfflineLicense, PlanModel, PlanType, PriceDuration, @@ -11,6 +13,7 @@ import { Quotas, Subscription, } from "@budibase/types" +import { generator } from "./generator" export function price(): PurchasedPrice { return { @@ -127,15 +130,15 @@ export function subscription(): Subscription { } } -export const license = ( - opts: { - quotas?: Quotas - plan?: PurchasedPlan - planType?: PlanType - features?: Feature[] - billing?: Billing - } = {} -): License => { +interface GenerateLicenseOpts { + quotas?: Quotas + plan?: PurchasedPlan + planType?: PlanType + features?: Feature[] + billing?: Billing +} + +export const license = (opts: GenerateLicenseOpts = {}): License => { return { features: opts.features || [], quotas: opts.quotas || quotas(), @@ -143,3 +146,22 @@ export const license = ( billing: opts.billing || billing(), } } + +export function offlineLicense(opts: GenerateLicenseOpts = {}): OfflineLicense { + const base = license(opts) + return { + ...base, + expireAt: new Date().toISOString(), + identifier: offlineIdentifier(), + } +} + +export function offlineIdentifier( + installId: string = generator.guid(), + tenantId: string = generator.guid() +): OfflineIdentifier { + return { + installId, + tenantId, + } +} diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index 452a8c74a1..11dc9963d5 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -47,7 +47,7 @@ {#if tooltip && showTooltip}
- +
{/if} @@ -80,15 +80,14 @@ position: absolute; pointer-events: none; left: 50%; - top: calc(100% + 4px); - width: 100vw; - max-width: 150px; + bottom: calc(100% + 4px); transform: translateX(-50%); text-align: center; + z-index: 1; } .spectrum-Icon--sizeXS { - width: 10px; - height: 10px; + width: var(--spectrum-global-dimension-size-150); + height: var(--spectrum-global-dimension-size-150); } diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 208739a540..4761ccee02 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -18,7 +18,7 @@ import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { FIELDS, - RelationshipTypes, + RelationshipType, ALLOWABLE_STRING_OPTIONS, ALLOWABLE_NUMBER_OPTIONS, ALLOWABLE_STRING_TYPES, @@ -33,6 +33,7 @@ import { getBindings } from "components/backend/DataTable/formula" import { getContext } from "svelte" import JSONSchemaModal from "./JSONSchemaModal.svelte" + import { ValidColumnNameRegex } from "@budibase/shared-core" const AUTO_TYPE = "auto" const FORMULA_TYPE = FIELDS.FORMULA.type @@ -183,7 +184,7 @@ dispatch("updatecolumns") if ( saveColumn.type === LINK_TYPE && - saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY + saveColumn.relationshipType === RelationshipType.MANY_TO_MANY ) { // Fetching the new tables tables.fetch() @@ -237,7 +238,7 @@ // Default relationships many to many if (editableColumn.type === LINK_TYPE) { - editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY + editableColumn.relationshipType = RelationshipType.MANY_TO_MANY } if (editableColumn.type === FORMULA_TYPE) { editableColumn.formulaType = "dynamic" @@ -285,17 +286,17 @@ { name: `Many ${thisName} rows → many ${linkName} rows`, alt: `Many ${table.name} rows → many ${linkTable.name} rows`, - value: RelationshipTypes.MANY_TO_MANY, + value: RelationshipType.MANY_TO_MANY, }, { name: `One ${linkName} row → many ${thisName} rows`, alt: `One ${linkTable.name} rows → many ${table.name} rows`, - value: RelationshipTypes.ONE_TO_MANY, + value: RelationshipType.ONE_TO_MANY, }, { name: `One ${thisName} row → many ${linkName} rows`, alt: `One ${table.name} rows → many ${linkTable.name} rows`, - value: RelationshipTypes.MANY_TO_ONE, + value: RelationshipType.MANY_TO_ONE, }, ] } @@ -375,7 +376,7 @@ const newError = {} if (!external && fieldInfo.name?.startsWith("_")) { newError.name = `Column name cannot start with an underscore.` - } else if (fieldInfo.name && !fieldInfo.name.match(/^[_a-zA-Z0-9\s]*$/g)) { + } else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) { newError.name = `Illegal character; must be alpha-numeric.` } else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) { newError.name = `${PROHIBITED_COLUMN_NAMES.join( diff --git a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte index 53fcf56e7f..36c6a32801 100644 --- a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte +++ b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte @@ -1,5 +1,5 @@
@@ -119,10 +127,8 @@ on:change={handleFile} />
{/each} @@ -167,7 +176,7 @@ + Installation identifier + Share this with support@budibase.com to obtain your offline license + + +
+ +
+
+ + + License + Upload your license to activate your plan + + +
+
- - - - {#if licenseInfo?.licenseKey} - - {/if} - -
+ {#if licenseKey} + + {/if} + + + {/if} - Plan - + Plan + You are currently on the {license.plan.type} plan +
+ If you purchase or update your plan on the account + portal, click the refresh button to sync those changes +
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { time: @@ -169,4 +293,7 @@ grid-gap: var(--spacing-l); align-items: center; } + .identifier-input { + width: 300px; + } diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index b9467fd037..2106acac27 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = { adminUser: { checked: false }, sso: { checked: false }, }, + offlineMode: false, } export function createAdminStore() { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index e336ad3817..285f045d08 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -2341,10 +2341,6 @@ "label": "Left", "value": "left" }, - { - "label": "Right", - "value": "right" - }, { "label": "Above", "value": "above" diff --git a/packages/frontend-core/src/api/licensing.js b/packages/frontend-core/src/api/licensing.js index c27d79d740..987fc34cf5 100644 --- a/packages/frontend-core/src/api/licensing.js +++ b/packages/frontend-core/src/api/licensing.js @@ -1,30 +1,58 @@ export const buildLicensingEndpoints = API => ({ - /** - * Activates a self hosted license key - */ + // LICENSE KEY + activateLicenseKey: async data => { return API.post({ - url: `/api/global/license/activate`, + url: `/api/global/license/key`, body: data, }) }, - - /** - * Delete a self hosted license key - */ deleteLicenseKey: async () => { return API.delete({ - url: `/api/global/license/info`, + url: `/api/global/license/key`, }) }, + getLicenseKey: async () => { + try { + return await API.get({ + url: "/api/global/license/key", + }) + } catch (e) { + if (e.status !== 404) { + throw e + } + } + }, - /** - * Get the license info - metadata about the license including the - * obfuscated license key. - */ - getLicenseInfo: async () => { - return API.get({ - url: "/api/global/license/info", + // OFFLINE LICENSE + + activateOfflineLicense: async ({ offlineLicenseToken }) => { + return API.post({ + url: "/api/global/license/offline", + body: { + offlineLicenseToken, + }, + }) + }, + deleteOfflineLicense: async () => { + return API.delete({ + url: "/api/global/license/offline", + }) + }, + getOfflineLicense: async () => { + try { + return await API.get({ + url: "/api/global/license/offline", + }) + } catch (e) { + if (e.status !== 404) { + throw e + } + } + }, + getOfflineLicenseIdentifier: async () => { + return await API.get({ + url: "/api/global/license/offline/identifier", }) }, @@ -36,7 +64,6 @@ export const buildLicensingEndpoints = API => ({ url: "/api/global/license/refresh", }) }, - /** * Retrieve the usage information for the tenant */ diff --git a/packages/pro b/packages/pro index 4d9840700e..347ee53268 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 4d9840700e7684581c39965b7cb6a2b2428c477c +Subproject commit 347ee5326812c01ef07f0e744f691ab4823e185a diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index bcff78e861..d97b09568c 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -841,7 +841,8 @@ "auto", "json", "internal", - "barcodeqr" + "barcodeqr", + "bigint" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1045,7 +1046,8 @@ "auto", "json", "internal", - "barcodeqr" + "barcodeqr", + "bigint" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1260,7 +1262,8 @@ "auto", "json", "internal", - "barcodeqr" + "barcodeqr", + "bigint" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index a9daed6c76..86807c9981 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -768,6 +768,7 @@ components: - json - internal - barcodeqr + - bigint description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -931,6 +932,7 @@ components: - json - internal - barcodeqr + - bigint description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -1101,6 +1103,7 @@ components: - json - internal - barcodeqr + - bigint description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: diff --git a/packages/server/specs/resources/table.ts b/packages/server/specs/resources/table.ts index 0fd063beb7..3a6a77e3e2 100644 --- a/packages/server/specs/resources/table.ts +++ b/packages/server/specs/resources/table.ts @@ -1,8 +1,4 @@ -import { - FieldTypes, - RelationshipTypes, - FormulaTypes, -} from "../../src/constants" +import { FieldTypes, RelationshipType, FormulaTypes } from "../../src/constants" import { object } from "./utils" import Resource from "./utils/Resource" @@ -100,7 +96,7 @@ const tableSchema = { }, relationshipType: { type: "string", - enum: Object.values(RelationshipTypes), + enum: Object.values(RelationshipType), description: "Defines the type of relationship that this column will be used for.", }, diff --git a/packages/server/src/api/controllers/datasource.ts b/packages/server/src/api/controllers/datasource.ts index 00ae2ea1d7..19a38206dc 100644 --- a/packages/server/src/api/controllers/datasource.ts +++ b/packages/server/src/api/controllers/datasource.ts @@ -1,34 +1,33 @@ import { - generateDatasourceID, - getDatasourceParams, - getQueryParams, DocumentType, - BudibaseInternalDB, + generateDatasourceID, + getQueryParams, getTableParams, } from "../../db/utils" import { destroy as tableDestroy } from "./table/internal" import { BuildSchemaErrors, InvalidColumns } from "../../constants" import { getIntegration } from "../../integrations" import { invalidateDynamicVariables } from "../../threads/utils" -import { db as dbCore, context, events } from "@budibase/backend-core" +import { context, db as dbCore, events } from "@budibase/backend-core" import { - UserCtx, - Datasource, - Row, - CreateDatasourceResponse, - UpdateDatasourceResponse, CreateDatasourceRequest, - VerifyDatasourceRequest, - VerifyDatasourceResponse, + CreateDatasourceResponse, + Datasource, + DatasourcePlus, FetchDatasourceInfoRequest, FetchDatasourceInfoResponse, IntegrationBase, - DatasourcePlus, + RestConfig, SourceName, + UpdateDatasourceResponse, + UserCtx, + VerifyDatasourceRequest, + VerifyDatasourceResponse, } from "@budibase/types" import sdk from "../../sdk" import { builderSocket } from "../../websockets" import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets" +import { areRESTVariablesValid } from "../../sdk/app/datasources/datasources" function getErrorTables(errors: any, errorType: string) { return Object.entries(errors) @@ -119,46 +118,7 @@ async function buildFilteredSchema(datasource: Datasource, filter?: string[]) { } export async function fetch(ctx: UserCtx) { - // Get internal tables - const db = context.getAppDB() - const internalTables = await db.allDocs( - getTableParams(null, { - include_docs: true, - }) - ) - - const internal = internalTables.rows.reduce((acc: any, row: Row) => { - const sourceId = row.doc.sourceId || "bb_internal" - acc[sourceId] = acc[sourceId] || [] - acc[sourceId].push(row.doc) - return acc - }, {}) - - const bbInternalDb = { - ...BudibaseInternalDB, - } - - // Get external datasources - const datasources = ( - await db.allDocs( - getDatasourceParams(null, { - include_docs: true, - }) - ) - ).rows.map(row => row.doc) - - const allDatasources: Datasource[] = await sdk.datasources.removeSecrets([ - bbInternalDb, - ...datasources, - ]) - - for (let datasource of allDatasources) { - if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) { - datasource.entities = internal[datasource._id!] - } - } - - ctx.body = [bbInternalDb, ...datasources] + ctx.body = await sdk.datasources.fetch() } export async function verify( @@ -290,6 +250,14 @@ export async function update(ctx: UserCtx) { datasource.config!.auth = auth } + // check all variables are unique + if ( + datasource.source === SourceName.REST && + !sdk.datasources.areRESTVariablesValid(datasource) + ) { + ctx.throw(400, "Duplicate dynamic/static variable names are invalid.") + } + const response = await db.put( sdk.tables.populateExternalTableSchemas(datasource) ) diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index a1f788ae75..ee8e1bb022 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -1,4 +1,4 @@ -import { generateQueryID, getQueryParams, isProdAppID } from "../../../db/utils" +import { generateQueryID } from "../../../db/utils" import { BaseQueryVerbs, FieldTypes } from "../../../constants" import { Thread, ThreadType } from "../../../threads" import { save as saveDatasource } from "../datasource" @@ -29,15 +29,7 @@ function enrichQueries(input: any) { } export async function fetch(ctx: any) { - const db = context.getAppDB() - - const body = await db.allDocs( - getQueryParams(null, { - include_docs: true, - }) - ) - - ctx.body = enrichQueries(body.rows.map((row: any) => row.doc)) + ctx.body = await sdk.queries.fetch() } const _import = async (ctx: any) => { @@ -109,14 +101,8 @@ export async function save(ctx: any) { } export async function find(ctx: any) { - const db = context.getAppDB() - const query = enrichQueries(await db.get(ctx.params.queryId)) - // remove properties that could be dangerous in real app - if (isProdAppID(ctx.appId)) { - delete query.fields - delete query.parameters - } - ctx.body = query + const queryId = ctx.params.queryId + ctx.body = await sdk.queries.find(queryId) } //Required to discern between OIDC OAuth config entries diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 9378eb534b..4b37d5a29e 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -7,7 +7,7 @@ import { Operation, PaginationJson, RelationshipsJson, - RelationshipTypes, + RelationshipType, Row, SearchFilters, SortJson, @@ -577,7 +577,7 @@ export class ExternalRequest { ) { continue } - const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY + const isMany = field.relationshipType === RelationshipType.MANY_TO_MANY const tableId = isMany ? field.through : field.tableId const { tableName: relatedTableName } = breakExternalTableId(tableId) // @ts-ignore diff --git a/packages/server/src/api/controllers/table/external.ts b/packages/server/src/api/controllers/table/external.ts index f008ff43fb..eb1eeb7256 100644 --- a/packages/server/src/api/controllers/table/external.ts +++ b/packages/server/src/api/controllers/table/external.ts @@ -20,7 +20,7 @@ import { FieldSchema, Operation, QueryJson, - RelationshipTypes, + RelationshipType, RenameColumn, Table, TableRequest, @@ -103,12 +103,12 @@ function getDatasourceId(table: Table) { } function otherRelationshipType(type?: string) { - if (type === RelationshipTypes.MANY_TO_MANY) { - return RelationshipTypes.MANY_TO_MANY + if (type === RelationshipType.MANY_TO_MANY) { + return RelationshipType.MANY_TO_MANY } - return type === RelationshipTypes.ONE_TO_MANY - ? RelationshipTypes.MANY_TO_ONE - : RelationshipTypes.ONE_TO_MANY + return type === RelationshipType.ONE_TO_MANY + ? RelationshipType.MANY_TO_ONE + : RelationshipType.ONE_TO_MANY } function generateManyLinkSchema( @@ -151,12 +151,12 @@ function generateLinkSchema( column: FieldSchema, table: Table, relatedTable: Table, - type: RelationshipTypes + type: RelationshipType ) { if (!table.primary || !relatedTable.primary) { throw new Error("Unable to generate link schema, no primary keys") } - const isOneSide = type === RelationshipTypes.ONE_TO_MANY + const isOneSide = type === RelationshipType.ONE_TO_MANY const primary = isOneSide ? relatedTable.primary[0] : table.primary[0] // generate a foreign key const foreignKey = generateForeignKey(column, relatedTable) @@ -251,7 +251,7 @@ export async function save(ctx: UserCtx) { } const relatedColumnName = schema.fieldName! const relationType = schema.relationshipType! - if (relationType === RelationshipTypes.MANY_TO_MANY) { + if (relationType === RelationshipType.MANY_TO_MANY) { const junctionTable = generateManyLinkSchema( datasource, schema, @@ -265,7 +265,7 @@ export async function save(ctx: UserCtx) { extraTablesToUpdate.push(junctionTable) } else { const fkTable = - relationType === RelationshipTypes.ONE_TO_MANY + relationType === RelationshipType.ONE_TO_MANY ? tableToSave : relatedTable const foreignKey = generateLinkSchema( diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index dfea8f89bc..c8c17e1d32 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -1,6 +1,6 @@ import { objectStore, roles, constants } from "@budibase/backend-core" import { FieldType as FieldTypes } from "@budibase/types" -export { FieldType as FieldTypes, RelationshipTypes } from "@budibase/types" +export { FieldType as FieldTypes, RelationshipType } from "@budibase/types" export enum FilterTypes { STRING = "string", diff --git a/packages/server/src/db/defaultData/datasource_bb_default.ts b/packages/server/src/db/defaultData/datasource_bb_default.ts index 6ffbf601c8..d01f598ce4 100644 --- a/packages/server/src/db/defaultData/datasource_bb_default.ts +++ b/packages/server/src/db/defaultData/datasource_bb_default.ts @@ -7,7 +7,7 @@ import { employeeImport } from "./employeeImport" import { jobsImport } from "./jobsImport" import { expensesImport } from "./expensesImport" import { db as dbCore } from "@budibase/backend-core" -import { Table, Row, RelationshipTypes } from "@budibase/types" +import { Table, Row, RelationshipType } from "@budibase/types" export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs" export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory" @@ -299,7 +299,7 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = { }, fieldName: "Assigned", name: "Jobs", - relationshipType: RelationshipTypes.MANY_TO_MANY, + relationshipType: RelationshipType.MANY_TO_MANY, tableId: DEFAULT_JOBS_TABLE_ID, }, "Start Date": { @@ -458,7 +458,7 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = { type: FieldTypes.LINK, tableId: DEFAULT_EMPLOYEE_TABLE_ID, fieldName: "Jobs", - relationshipType: RelationshipTypes.MANY_TO_MANY, + relationshipType: RelationshipType.MANY_TO_MANY, // sortable: true, }, "Works End": { diff --git a/packages/server/src/db/linkedRows/LinkController.ts b/packages/server/src/db/linkedRows/LinkController.ts index c3b3b324b5..5bfae49e8b 100644 --- a/packages/server/src/db/linkedRows/LinkController.ts +++ b/packages/server/src/db/linkedRows/LinkController.ts @@ -8,7 +8,7 @@ import { Database, FieldSchema, LinkDocumentValue, - RelationshipTypes, + RelationshipType, Row, Table, } from "@budibase/types" @@ -136,16 +136,16 @@ class LinkController { handleRelationshipType(linkerField: FieldSchema, linkedField: FieldSchema) { if ( !linkerField.relationshipType || - linkerField.relationshipType === RelationshipTypes.MANY_TO_MANY + linkerField.relationshipType === RelationshipType.MANY_TO_MANY ) { - linkedField.relationshipType = RelationshipTypes.MANY_TO_MANY + linkedField.relationshipType = RelationshipType.MANY_TO_MANY // make sure by default all are many to many (if not specified) - linkerField.relationshipType = RelationshipTypes.MANY_TO_MANY - } else if (linkerField.relationshipType === RelationshipTypes.MANY_TO_ONE) { + linkerField.relationshipType = RelationshipType.MANY_TO_MANY + } else if (linkerField.relationshipType === RelationshipType.MANY_TO_ONE) { // Ensure that the other side of the relationship is locked to one record - linkedField.relationshipType = RelationshipTypes.ONE_TO_MANY - } else if (linkerField.relationshipType === RelationshipTypes.ONE_TO_MANY) { - linkedField.relationshipType = RelationshipTypes.MANY_TO_ONE + linkedField.relationshipType = RelationshipType.ONE_TO_MANY + } else if (linkerField.relationshipType === RelationshipType.ONE_TO_MANY) { + linkedField.relationshipType = RelationshipType.MANY_TO_ONE } return { linkerField, linkedField } } @@ -200,9 +200,7 @@ class LinkController { // iterate through the link IDs in the row field, see if any don't exist already for (let linkId of rowField) { - if ( - linkedSchema?.relationshipType === RelationshipTypes.ONE_TO_MANY - ) { + if (linkedSchema?.relationshipType === RelationshipType.ONE_TO_MANY) { let links = ( (await getLinkDocuments({ tableId: field.tableId, diff --git a/packages/server/src/db/tests/linkController.spec.js b/packages/server/src/db/tests/linkController.spec.js index 866796fd56..1c50142871 100644 --- a/packages/server/src/db/tests/linkController.spec.js +++ b/packages/server/src/db/tests/linkController.spec.js @@ -2,7 +2,7 @@ const TestConfig = require("../../tests/utilities/TestConfiguration") const { basicRow, basicLinkedRow, basicTable } = require("../../tests/utilities/structures") const LinkController = require("../linkedRows/LinkController").default const { context } = require("@budibase/backend-core") -const { RelationshipTypes } = require("../../constants") +const { RelationshipType } = require("../../constants") const { cloneDeep } = require("lodash/fp") describe("test the link controller", () => { @@ -16,7 +16,7 @@ describe("test the link controller", () => { beforeEach(async () => { const { _id } = await config.createTable() - table2 = await config.createLinkedTable(RelationshipTypes.MANY_TO_MANY, ["link", "link2"]) + table2 = await config.createLinkedTable(RelationshipType.MANY_TO_MANY, ["link", "link2"]) // update table after creating link table1 = await config.getTable(_id) }) @@ -57,17 +57,17 @@ describe("test the link controller", () => { const controller = await createLinkController(table1) // empty case let output = controller.handleRelationshipType({}, {}) - expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) - expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) - output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.MANY_TO_MANY }, {}) - expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) - expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) - output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.MANY_TO_ONE }, {}) - expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.ONE_TO_MANY) - expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_ONE) - output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.ONE_TO_MANY }, {}) - expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_ONE) - expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.ONE_TO_MANY) + expect(output.linkedField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY) + expect(output.linkerField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY) + output = controller.handleRelationshipType({ relationshipType: RelationshipType.MANY_TO_MANY }, {}) + expect(output.linkedField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY) + expect(output.linkerField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY) + output = controller.handleRelationshipType({ relationshipType: RelationshipType.MANY_TO_ONE }, {}) + expect(output.linkedField.relationshipType).toEqual(RelationshipType.ONE_TO_MANY) + expect(output.linkerField.relationshipType).toEqual(RelationshipType.MANY_TO_ONE) + output = controller.handleRelationshipType({ relationshipType: RelationshipType.ONE_TO_MANY }, {}) + expect(output.linkedField.relationshipType).toEqual(RelationshipType.MANY_TO_ONE) + expect(output.linkerField.relationshipType).toEqual(RelationshipType.ONE_TO_MANY) }) it("should be able to delete a row", async () => { @@ -157,7 +157,7 @@ describe("test the link controller", () => { it("should throw an error when overwriting a link column", async () => { const update = cloneDeep(table1) - update.schema.link.relationshipType = RelationshipTypes.MANY_TO_ONE + update.schema.link.relationshipType = RelationshipType.MANY_TO_ONE let error try { const controller = await createLinkController(update) @@ -183,7 +183,7 @@ describe("test the link controller", () => { it("shouldn't allow one to many having many relationships against it", async () => { const firstTable = await config.createTable() - const { _id } = await config.createLinkedTable(RelationshipTypes.MANY_TO_ONE, ["link"]) + const { _id } = await config.createLinkedTable(RelationshipType.MANY_TO_ONE, ["link"]) const linkTable = await config.getTable(_id) // an initial row to link around const row = await createLinkedRow("link", linkTable, firstTable) diff --git a/packages/server/src/environment.ts b/packages/server/src/environment.ts index 79ee5d977c..64b342d577 100644 --- a/packages/server/src/environment.ts +++ b/packages/server/src/environment.ts @@ -81,7 +81,6 @@ const environment = { SELF_HOSTED: process.env.SELF_HOSTED, HTTP_MB_LIMIT: process.env.HTTP_MB_LIMIT, FORKED_PROCESS_NAME: process.env.FORKED_PROCESS_NAME || "main", - OFFLINE_MODE: process.env.OFFLINE_MODE, // old CLIENT_ID: process.env.CLIENT_ID, _set(key: string, value: any) { diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index f72cbda1ea..1b64c763e6 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -10,7 +10,7 @@ import * as setup from "../api/routes/tests/utilities" import { Datasource, FieldType, - RelationshipTypes, + RelationshipType, Row, SourceName, Table, @@ -101,17 +101,17 @@ describe("postgres integrations", () => { oneToManyRelationshipInfo = { table: await createAuxTable("o2m"), fieldName: "oneToManyRelation", - relationshipType: RelationshipTypes.ONE_TO_MANY, + relationshipType: RelationshipType.ONE_TO_MANY, } manyToOneRelationshipInfo = { table: await createAuxTable("m2o"), fieldName: "manyToOneRelation", - relationshipType: RelationshipTypes.MANY_TO_ONE, + relationshipType: RelationshipType.MANY_TO_ONE, } manyToManyRelationshipInfo = { table: await createAuxTable("m2m"), fieldName: "manyToManyRelation", - relationshipType: RelationshipTypes.MANY_TO_MANY, + relationshipType: RelationshipType.MANY_TO_MANY, } primaryPostgresTable = await config.createTable({ @@ -143,7 +143,7 @@ describe("postgres integrations", () => { }, fieldName: oneToManyRelationshipInfo.fieldName, name: "oneToManyRelation", - relationshipType: RelationshipTypes.ONE_TO_MANY, + relationshipType: RelationshipType.ONE_TO_MANY, tableId: oneToManyRelationshipInfo.table._id, main: true, }, @@ -154,7 +154,7 @@ describe("postgres integrations", () => { }, fieldName: manyToOneRelationshipInfo.fieldName, name: "manyToOneRelation", - relationshipType: RelationshipTypes.MANY_TO_ONE, + relationshipType: RelationshipType.MANY_TO_ONE, tableId: manyToOneRelationshipInfo.table._id, main: true, }, @@ -165,7 +165,7 @@ describe("postgres integrations", () => { }, fieldName: manyToManyRelationshipInfo.fieldName, name: "manyToManyRelation", - relationshipType: RelationshipTypes.MANY_TO_MANY, + relationshipType: RelationshipType.MANY_TO_MANY, tableId: manyToManyRelationshipInfo.table._id, main: true, }, @@ -193,12 +193,12 @@ describe("postgres integrations", () => { type ForeignTableInfo = { table: Table fieldName: string - relationshipType: RelationshipTypes + relationshipType: RelationshipType } type ForeignRowsInfo = { row: Row - relationshipType: RelationshipTypes + relationshipType: RelationshipType } async function createPrimaryRow(opts: { @@ -263,7 +263,7 @@ describe("postgres integrations", () => { rowData[manyToOneRelationshipInfo.fieldName].push(foreignRow._id) foreignRows.push({ row: foreignRow, - relationshipType: RelationshipTypes.MANY_TO_ONE, + relationshipType: RelationshipType.MANY_TO_ONE, }) } @@ -281,7 +281,7 @@ describe("postgres integrations", () => { rowData[manyToManyRelationshipInfo.fieldName].push(foreignRow._id) foreignRows.push({ row: foreignRow, - relationshipType: RelationshipTypes.MANY_TO_MANY, + relationshipType: RelationshipType.MANY_TO_MANY, }) } @@ -559,7 +559,7 @@ describe("postgres integrations", () => { expect(res.status).toBe(200) const one2ManyForeignRows = foreignRows.filter( - x => x.relationshipType === RelationshipTypes.ONE_TO_MANY + x => x.relationshipType === RelationshipType.ONE_TO_MANY ) expect(one2ManyForeignRows).toHaveLength(1) @@ -921,7 +921,7 @@ describe("postgres integrations", () => { (row: Row) => row.id === 2 ) expect(m2mRow1).toEqual({ - ...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][0].row, + ...foreignRowsByType[RelationshipType.MANY_TO_MANY][0].row, [m2mFieldName]: [ { _id: row._id, @@ -930,7 +930,7 @@ describe("postgres integrations", () => { ], }) expect(m2mRow2).toEqual({ - ...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][1].row, + ...foreignRowsByType[RelationshipType.MANY_TO_MANY][1].row, [m2mFieldName]: [ { _id: row._id, @@ -940,24 +940,24 @@ describe("postgres integrations", () => { }) expect(res.body[m2oFieldName]).toEqual([ { - ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][0].row, + ...foreignRowsByType[RelationshipType.MANY_TO_ONE][0].row, [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: row.id, }, { - ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][1].row, + ...foreignRowsByType[RelationshipType.MANY_TO_ONE][1].row, [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: row.id, }, { - ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][2].row, + ...foreignRowsByType[RelationshipType.MANY_TO_ONE][2].row, [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: row.id, }, ]) expect(res.body[o2mFieldName]).toEqual([ { - ...foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row, + ...foreignRowsByType[RelationshipType.ONE_TO_MANY][0].row, _id: expect.any(String), _rev: expect.any(String), }, diff --git a/packages/server/src/integrations/base/sqlTable.ts b/packages/server/src/integrations/base/sqlTable.ts index 62b13c4ef5..47b09dadee 100644 --- a/packages/server/src/integrations/base/sqlTable.ts +++ b/packages/server/src/integrations/base/sqlTable.ts @@ -3,7 +3,7 @@ import { Operation, QueryJson, RenameColumn, Table } from "@budibase/types" import { breakExternalTableId } from "../utils" import SchemaBuilder = Knex.SchemaBuilder import CreateTableBuilder = Knex.CreateTableBuilder -import { FieldTypes, RelationshipTypes } from "../../constants" +import { FieldTypes, RelationshipType } from "../../constants" function generateSchema( schema: CreateTableBuilder, @@ -70,8 +70,8 @@ function generateSchema( case FieldTypes.LINK: // this side of the relationship doesn't need any SQL work if ( - column.relationshipType !== RelationshipTypes.MANY_TO_ONE && - column.relationshipType !== RelationshipTypes.MANY_TO_MANY + column.relationshipType !== RelationshipType.MANY_TO_ONE && + column.relationshipType !== RelationshipType.MANY_TO_MANY ) { if (!column.foreignKey || !column.tableId) { throw "Invalid relationship schema" diff --git a/packages/server/src/sdk/app/datasources/datasources.ts b/packages/server/src/sdk/app/datasources/datasources.ts index 171ec42042..4145b1db63 100644 --- a/packages/server/src/sdk/app/datasources/datasources.ts +++ b/packages/server/src/sdk/app/datasources/datasources.ts @@ -1,4 +1,4 @@ -import { context } from "@budibase/backend-core" +import { context, db as dbCore } from "@budibase/backend-core" import { findHBSBlocks, processObjectSync } from "@budibase/string-templates" import { Datasource, @@ -8,15 +8,88 @@ import { RestAuthConfig, RestAuthType, RestBasicAuthConfig, + Row, + RestConfig, SourceName, } from "@budibase/types" import { cloneDeep } from "lodash/fp" import { getEnvironmentVariables } from "../../utils" import { getDefinitions, getDefinition } from "../../../integrations" import _ from "lodash" +import { + BudibaseInternalDB, + getDatasourceParams, + getTableParams, +} from "../../../db/utils" +import sdk from "../../index" const ENV_VAR_PREFIX = "env." +export async function fetch() { + // Get internal tables + const db = context.getAppDB() + const internalTables = await db.allDocs( + getTableParams(null, { + include_docs: true, + }) + ) + + const internal = internalTables.rows.reduce((acc: any, row: Row) => { + const sourceId = row.doc.sourceId || "bb_internal" + acc[sourceId] = acc[sourceId] || [] + acc[sourceId].push(row.doc) + return acc + }, {}) + + const bbInternalDb = { + ...BudibaseInternalDB, + } + + // Get external datasources + const datasources = ( + await db.allDocs( + getDatasourceParams(null, { + include_docs: true, + }) + ) + ).rows.map(row => row.doc) + + const allDatasources: Datasource[] = await sdk.datasources.removeSecrets([ + bbInternalDb, + ...datasources, + ]) + + for (let datasource of allDatasources) { + if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) { + datasource.entities = internal[datasource._id!] + } + } + + return [bbInternalDb, ...datasources] +} + +export function areRESTVariablesValid(datasource: Datasource) { + const restConfig = datasource.config as RestConfig + const varNames: string[] = [] + if (restConfig.dynamicVariables) { + for (let variable of restConfig.dynamicVariables) { + if (varNames.includes(variable.name)) { + return false + } + varNames.push(variable.name) + } + } + if (restConfig.staticVariables) { + for (let name of Object.keys(restConfig.staticVariables)) { + if (varNames.includes(name)) { + return false + } + varNames.push(name) + } + } + return true +} + export function checkDatasourceTypes(schema: Integration, config: any) { for (let key of Object.keys(config)) { if (!schema.datasource[key]) { diff --git a/packages/server/src/sdk/app/queries/queries.ts b/packages/server/src/sdk/app/queries/queries.ts index ca74eb44b5..408e393714 100644 --- a/packages/server/src/sdk/app/queries/queries.ts +++ b/packages/server/src/sdk/app/queries/queries.ts @@ -1,5 +1,49 @@ import { getEnvironmentVariables } from "../../utils" import { processStringSync } from "@budibase/string-templates" +import { context } from "@budibase/backend-core" +import { getQueryParams, isProdAppID } from "../../../db/utils" +import { BaseQueryVerbs } from "../../../constants" + +// simple function to append "readable" to all read queries +function enrichQueries(input: any) { + const wasArray = Array.isArray(input) + const queries = wasArray ? input : [input] + for (let query of queries) { + if (query.queryVerb === BaseQueryVerbs.READ) { + query.readable = true + } + } + return wasArray ? queries : queries[0] +} + +export async function find(queryId: string) { + const db = context.getAppDB() + const appId = context.getAppId() + const query = enrichQueries(await db.get(queryId)) + // remove properties that could be dangerous in real app + if (isProdAppID(appId)) { + delete query.fields + delete query.parameters + } + return query +} + +export async function fetch(opts: { enrich: boolean } = { enrich: true }) { + const db = context.getAppDB() + + const body = await db.allDocs( + getQueryParams(null, { + include_docs: true, + }) + ) + + const queries = body.rows.map((row: any) => row.doc) + if (opts.enrich) { + return enrichQueries(queries) + } else { + return queries + } +} export async function enrichContext( fields: Record, diff --git a/packages/server/src/sdk/app/tables/validation.ts b/packages/server/src/sdk/app/tables/validation.ts index e09380c309..8dc41107d3 100644 --- a/packages/server/src/sdk/app/tables/validation.ts +++ b/packages/server/src/sdk/app/tables/validation.ts @@ -3,7 +3,7 @@ import { Datasource, FieldSchema, FieldType, - RelationshipTypes, + RelationshipType, } from "@budibase/types" import { FieldTypes } from "../../../constants" @@ -19,14 +19,14 @@ function checkForeignKeysAreAutoColumns(datasource: Datasource) { column => column.type === FieldType.LINK ) relationships.forEach(relationship => { - if (relationship.relationshipType === RelationshipTypes.MANY_TO_MANY) { + if (relationship.relationshipType === RelationshipType.MANY_TO_MANY) { const tableId = relationship.through! foreignKeys.push({ key: relationship.throughTo!, tableId }) foreignKeys.push({ key: relationship.throughFrom!, tableId }) } else { const fk = relationship.foreignKey! const oneSide = - relationship.relationshipType === RelationshipTypes.ONE_TO_MANY + relationship.relationshipType === RelationshipType.ONE_TO_MANY foreignKeys.push({ tableId: oneSide ? table._id! : relationship.tableId!, key: fk, diff --git a/packages/server/src/utilities/schema.ts b/packages/server/src/utilities/schema.ts index 7b5de7898a..5ced82d8cf 100644 --- a/packages/server/src/utilities/schema.ts +++ b/packages/server/src/utilities/schema.ts @@ -1,4 +1,5 @@ import { FieldTypes } from "../constants" +import { ValidColumnNameRegex } from "@budibase/shared-core" interface SchemaColumn { readonly name: string @@ -27,6 +28,7 @@ interface ValidationResults { schemaValidation: SchemaValidation allValid: boolean invalidColumns: Array + errors: Record } const PARSERS: any = { @@ -69,6 +71,7 @@ export function validate(rows: Rows, schema: Schema): ValidationResults { schemaValidation: {}, allValid: false, invalidColumns: [], + errors: {}, } rows.forEach(row => { @@ -79,6 +82,11 @@ export function validate(rows: Rows, schema: Schema): ValidationResults { // If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array if (typeof columnType !== "string") { results.invalidColumns.push(columnName) + } else if (!columnName.match(ValidColumnNameRegex)) { + // Check for special characters in column names + results.schemaValidation[columnName] = false + results.errors[columnName] = + "Column names can't contain special characters" } else if ( columnData == null && !schema[columnName].constraints?.presence diff --git a/packages/shared-core/src/constants.ts b/packages/shared-core/src/constants.ts index fc18de255c..864009fbf2 100644 --- a/packages/shared-core/src/constants.ts +++ b/packages/shared-core/src/constants.ts @@ -95,3 +95,4 @@ export enum BuilderSocketEvent { export const SocketSessionTTL = 60 export const ValidQueryNameRegex = /^[^()]*$/ +export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g diff --git a/packages/types/src/api/account/license.ts b/packages/types/src/api/account/license.ts index a867358559..edb1267ecf 100644 --- a/packages/types/src/api/account/license.ts +++ b/packages/types/src/api/account/license.ts @@ -1,5 +1,6 @@ import { LicenseOverrides, QuotaUsage } from "../../documents" -import { PlanType } from "../../sdk" +import { OfflineLicense, PlanType } from "../../sdk" +import { ISO8601 } from "../../shared" export interface GetLicenseRequest { // All fields should be optional to cater for @@ -26,3 +27,13 @@ export interface UpdateLicenseRequest { planType?: PlanType overrides?: LicenseOverrides } + +export interface CreateOfflineLicenseRequest { + installationIdentifierBase64: string + expireAt: ISO8601 +} + +export interface GetOfflineLicenseResponse { + offlineLicenseToken: string + license: OfflineLicense +} diff --git a/packages/types/src/api/web/global/index.ts b/packages/types/src/api/web/global/index.ts index bf4b43f0ba..efcb6dc39c 100644 --- a/packages/types/src/api/web/global/index.ts +++ b/packages/types/src/api/web/global/index.ts @@ -3,3 +3,4 @@ export * from "./auditLogs" export * from "./events" export * from "./configs" export * from "./scim" +export * from "./license" diff --git a/packages/types/src/api/web/global/license.ts b/packages/types/src/api/web/global/license.ts new file mode 100644 index 0000000000..21d8876412 --- /dev/null +++ b/packages/types/src/api/web/global/license.ts @@ -0,0 +1,25 @@ +// LICENSE KEY + +export interface ActivateLicenseKeyRequest { + licenseKey: string +} + +export interface GetLicenseKeyResponse { + licenseKey: string +} + +// OFFLINE LICENSE + +export interface ActivateOfflineLicenseTokenRequest { + offlineLicenseToken: string +} + +export interface GetOfflineLicenseTokenResponse { + offlineLicenseToken: string +} + +// IDENTIFIER + +export interface GetOfflineIdentifierResponse { + identifierBase64: string +} diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index dad8abed30..5321aa7e08 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -51,6 +51,7 @@ export interface Account extends CreateAccount { licenseRequestedAt?: number licenseOverrides?: LicenseOverrides quotaUsage?: QuotaUsage + offlineLicenseToken?: string } export interface PasswordAccount extends Account { diff --git a/packages/types/src/documents/app/datasource.ts b/packages/types/src/documents/app/datasource.ts index 8dfdfe6d0f..855006ea4c 100644 --- a/packages/types/src/documents/app/datasource.ts +++ b/packages/types/src/documents/app/datasource.ts @@ -7,9 +7,7 @@ export interface Datasource extends Document { name?: string source: SourceName // the config is defined by the schema - config?: { - [key: string]: string | number | boolean | any[] - } + config?: Record plus?: boolean entities?: { [key: string]: Table diff --git a/packages/types/src/documents/app/table.ts b/packages/types/src/documents/app/table.ts deleted file mode 100644 index 18b415da5f..0000000000 --- a/packages/types/src/documents/app/table.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Document } from "../document" -import { View } from "./view" -import { RenameColumn } from "../../sdk" -import { FieldType } from "./row" - -export enum RelationshipTypes { - ONE_TO_MANY = "one-to-many", - MANY_TO_ONE = "many-to-one", - MANY_TO_MANY = "many-to-many", -} - -export enum AutoReason { - FOREIGN_KEY = "foreign_key", -} - -export interface FieldSchema { - type: FieldType - externalType?: string - fieldName?: string - name: string - sortable?: boolean - tableId?: string - relationshipType?: RelationshipTypes - through?: string - foreignKey?: string - icon?: string - autocolumn?: boolean - autoReason?: AutoReason - subtype?: string - throughFrom?: string - throughTo?: string - formula?: string - formulaType?: string - main?: boolean - ignoreTimezones?: boolean - timeOnly?: boolean - lastID?: number - useRichText?: boolean | null - order?: number - width?: number - meta?: { - toTable: string - toKey: string - } - constraints?: { - type?: string - email?: boolean - inclusion?: string[] - length?: { - minimum?: string | number | null - maximum?: string | number | null - } - numericality?: { - greaterThanOrEqualTo: string | null - lessThanOrEqualTo: string | null - } - presence?: - | boolean - | { - allowEmpty?: boolean - } - datetime?: { - latest: string - earliest: string - } - } -} - -export interface TableSchema { - [key: string]: FieldSchema -} - -export interface Table extends Document { - type?: string - views?: { [key: string]: View } - name: string - primary?: string[] - schema: TableSchema - primaryDisplay?: string - sourceId?: string - relatedFormula?: string[] - constrained?: string[] - sql?: boolean - indexes?: { [key: string]: any } - rows?: { [key: string]: any } - created?: boolean - rowHeight?: number -} - -export interface ExternalTable extends Table { - sourceId: string -} - -export interface TableRequest extends Table { - _rename?: RenameColumn - created?: boolean -} diff --git a/packages/types/src/documents/app/table/constants.ts b/packages/types/src/documents/app/table/constants.ts new file mode 100644 index 0000000000..9a0ea4d135 --- /dev/null +++ b/packages/types/src/documents/app/table/constants.ts @@ -0,0 +1,9 @@ +export enum RelationshipType { + ONE_TO_MANY = "one-to-many", + MANY_TO_ONE = "many-to-one", + MANY_TO_MANY = "many-to-many", +} + +export enum AutoReason { + FOREIGN_KEY = "foreign_key", +} diff --git a/packages/types/src/documents/app/table/index.ts b/packages/types/src/documents/app/table/index.ts new file mode 100644 index 0000000000..c22788d269 --- /dev/null +++ b/packages/types/src/documents/app/table/index.ts @@ -0,0 +1,3 @@ +export * from "./table" +export * from "./schema" +export * from "./constants" diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts new file mode 100644 index 0000000000..42a0838231 --- /dev/null +++ b/packages/types/src/documents/app/table/schema.ts @@ -0,0 +1,98 @@ +// all added by grid/table when defining the +// column size, position and whether it can be viewed +import { FieldType } from "../row" +import { AutoReason, RelationshipType } from "./constants" + +export interface UIFieldMetadata { + order?: number + width?: number + visible?: boolean + icon?: string +} + +export interface RelationshipFieldMetadata { + main?: boolean + fieldName?: string + tableId?: string + // below is used for SQL relationships, needed to define the foreign keys + // or the tables used for many-to-many relationships (through) + relationshipType?: RelationshipType + through?: string + foreignKey?: string + throughFrom?: string + throughTo?: string +} + +export interface AutoColumnFieldMetadata { + autocolumn?: boolean + subtype?: string + lastID?: number + // if the column was turned to an auto-column for SQL, explains why (primary, foreign etc) + autoReason?: AutoReason +} + +export interface NumberFieldMetadata { + // used specifically when Budibase generates external tables, this denotes if a number field + // is a foreign key used for a many-to-many relationship + meta?: { + toTable: string + toKey: string + } +} + +export interface DateFieldMetadata { + ignoreTimezones?: boolean + timeOnly?: boolean +} + +export interface StringFieldMetadata { + useRichText?: boolean | null +} + +export interface FormulaFieldMetadata { + formula?: string + formulaType?: string +} + +export interface FieldConstraints { + type?: string + email?: boolean + inclusion?: string[] + length?: { + minimum?: string | number | null + maximum?: string | number | null + } + numericality?: { + greaterThanOrEqualTo: string | null + lessThanOrEqualTo: string | null + } + presence?: + | boolean + | { + allowEmpty?: boolean + } + datetime?: { + latest: string + earliest: string + } +} + +export interface FieldSchema + extends UIFieldMetadata, + DateFieldMetadata, + RelationshipFieldMetadata, + AutoColumnFieldMetadata, + StringFieldMetadata, + FormulaFieldMetadata, + NumberFieldMetadata { + type: FieldType + name: string + sortable?: boolean + // only used by external databases, to denote the real type + externalType?: string + constraints?: FieldConstraints +} + +export interface TableSchema { + [key: string]: FieldSchema +} diff --git a/packages/types/src/documents/app/table/table.ts b/packages/types/src/documents/app/table/table.ts new file mode 100644 index 0000000000..f4dc790267 --- /dev/null +++ b/packages/types/src/documents/app/table/table.ts @@ -0,0 +1,30 @@ +import { Document } from "../../document" +import { View } from "../view" +import { RenameColumn } from "../../../sdk" +import { TableSchema } from "./schema" + +export interface Table extends Document { + type?: string + views?: { [key: string]: View } + name: string + primary?: string[] + schema: TableSchema + primaryDisplay?: string + sourceId?: string + relatedFormula?: string[] + constrained?: string[] + sql?: boolean + indexes?: { [key: string]: any } + rows?: { [key: string]: any } + created?: boolean + rowHeight?: number +} + +export interface ExternalTable extends Table { + sourceId: string +} + +export interface TableRequest extends Table { + _rename?: RenameColumn + created?: boolean +} diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index 1cbdd55bcf..f286a1cc44 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -9,6 +9,7 @@ export enum Feature { BRANDING = "branding", SCIM = "scim", SYNC_AUTOMATIONS = "syncAutomations", + OFFLINE = "offline", } export type PlanFeatures = { [key in PlanType]: Feature[] | undefined } diff --git a/packages/types/src/sdk/licensing/license.ts b/packages/types/src/sdk/licensing/license.ts index b287e67adf..105c3680a3 100644 --- a/packages/types/src/sdk/licensing/license.ts +++ b/packages/types/src/sdk/licensing/license.ts @@ -1,4 +1,15 @@ import { PurchasedPlan, Quotas, Feature, Billing } from "." +import { ISO8601 } from "../../shared" + +export interface OfflineIdentifier { + installId: string + tenantId: string +} + +export interface OfflineLicense extends License { + identifier: OfflineIdentifier + expireAt: ISO8601 +} export interface License { features: Feature[] diff --git a/packages/types/src/shared/typeUtils.ts b/packages/types/src/shared/typeUtils.ts index 71fadfc7aa..fbe215fdb9 100644 --- a/packages/types/src/shared/typeUtils.ts +++ b/packages/types/src/shared/typeUtils.ts @@ -1,3 +1,5 @@ export type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] } + +export type ISO8601 = string diff --git a/packages/worker/__mocks__/@budibase/pro.ts b/packages/worker/__mocks__/@budibase/pro.ts new file mode 100644 index 0000000000..59c7939111 --- /dev/null +++ b/packages/worker/__mocks__/@budibase/pro.ts @@ -0,0 +1,27 @@ +const actual = jest.requireActual("@budibase/pro") +const pro = { + ...actual, + licensing: { + keys: { + activateLicenseKey: jest.fn(), + getLicenseKey: jest.fn(), + deleteLicenseKey: jest.fn(), + }, + offline: { + activateOfflineLicenseToken: jest.fn(), + getOfflineLicenseToken: jest.fn(), + deleteOfflineLicenseToken: jest.fn(), + getIdentifierBase64: jest.fn(), + }, + cache: { + ...actual.licensing.cache, + refresh: jest.fn(), + }, + }, + quotas: { + ...actual.quotas, + getQuotaUsage: jest.fn(), + }, +} + +export = pro diff --git a/packages/worker/openapi-3.0.yaml b/packages/worker/openapi-3.0.yaml new file mode 100644 index 0000000000..c68654fffb --- /dev/null +++ b/packages/worker/openapi-3.0.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.0 +info: + title: Worker API Specification + version: 1.0.0 +servers: + - url: "http://localhost:10000" + description: localhost + - url: "https://budibaseqa.app" + description: QA + - url: "https://preprod.qa.budibase.net" + description: Preprod + - url: "https://budibase.app" + description: Production + +tags: + - name: license + description: License operations + +paths: + /api/global/license/key: + post: + tags: + - license + summary: Activate license key + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ActivateLicenseKeyRequest' + responses: + '200': + description: Success + get: + tags: + - license + summary: Get license key + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/GetLicenseKeyResponse' + delete: + tags: + - license + summary: Delete license key + responses: + '204': + description: No content + /api/global/license/offline: + post: + tags: + - license + summary: Activate offline license + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ActivateOfflineLicenseTokenRequest' + responses: + '200': + description: Success + get: + tags: + - license + summary: Get offline license + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/GetOfflineLicenseTokenResponse' + delete: + tags: + - license + summary: Delete offline license + responses: + '204': + description: No content + /api/global/license/offline/identifier: + get: + tags: + - license + summary: Get offline identifier + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/GetOfflineIdentifierResponse' + +components: + schemas: + ActivateOfflineLicenseTokenRequest: + type: object + properties: + offlineLicenseToken: + type: string + required: + - offlineLicenseToken + GetOfflineLicenseTokenResponse: + type: object + properties: + offlineLicenseToken: + type: string + required: + - offlineLicenseToken + ActivateLicenseKeyRequest: + type: object + properties: + licenseKey: + type: string + required: + - licenseKey + GetLicenseKeyResponse: + type: object + properties: + licenseKey: + type: string + required: + - licenseKey + GetOfflineIdentifierResponse: + type: object + properties: + identifierBase64: + type: string + required: + - identifierBase64 \ No newline at end of file diff --git a/packages/worker/src/api/controllers/global/license.ts b/packages/worker/src/api/controllers/global/license.ts index 2bd173010f..111cb5cea3 100644 --- a/packages/worker/src/api/controllers/global/license.ts +++ b/packages/worker/src/api/controllers/global/license.ts @@ -1,34 +1,83 @@ import { licensing, quotas } from "@budibase/pro" +import { + ActivateLicenseKeyRequest, + ActivateOfflineLicenseTokenRequest, + GetLicenseKeyResponse, + GetOfflineIdentifierResponse, + GetOfflineLicenseTokenResponse, + UserCtx, +} from "@budibase/types" -export const activate = async (ctx: any) => { +// LICENSE KEY + +export async function activateLicenseKey( + ctx: UserCtx +) { const { licenseKey } = ctx.request.body - if (!licenseKey) { - ctx.throw(400, "licenseKey is required") - } - - await licensing.activateLicenseKey(licenseKey) + await licensing.keys.activateLicenseKey(licenseKey) ctx.status = 200 } +export async function getLicenseKey(ctx: UserCtx) { + const licenseKey = await licensing.keys.getLicenseKey() + if (licenseKey) { + ctx.body = { licenseKey: "*" } + ctx.status = 200 + } else { + ctx.status = 404 + } +} + +export async function deleteLicenseKey(ctx: UserCtx) { + await licensing.keys.deleteLicenseKey() + ctx.status = 204 +} + +// OFFLINE LICENSE + +export async function activateOfflineLicenseToken( + ctx: UserCtx +) { + const { offlineLicenseToken } = ctx.request.body + await licensing.offline.activateOfflineLicenseToken(offlineLicenseToken) + ctx.status = 200 +} + +export async function getOfflineLicenseToken( + ctx: UserCtx +) { + const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken() + if (offlineLicenseToken) { + ctx.body = { offlineLicenseToken: "*" } + ctx.status = 200 + } else { + ctx.status = 404 + } +} + +export async function deleteOfflineLicenseToken(ctx: UserCtx) { + await licensing.offline.deleteOfflineLicenseToken() + ctx.status = 204 +} + +export async function getOfflineLicenseIdentifier( + ctx: UserCtx +) { + const identifierBase64 = await licensing.offline.getIdentifierBase64() + ctx.body = { identifierBase64 } + ctx.status = 200 +} + +// LICENSES + export const refresh = async (ctx: any) => { await licensing.cache.refresh() ctx.status = 200 } -export const getInfo = async (ctx: any) => { - const licenseInfo = await licensing.getLicenseInfo() - if (licenseInfo) { - licenseInfo.licenseKey = "*" - ctx.body = licenseInfo - } - ctx.status = 200 -} - -export const deleteInfo = async (ctx: any) => { - await licensing.deleteLicenseInfo() - ctx.status = 200 -} +// USAGE export const getQuotaUsage = async (ctx: any) => { ctx.body = await quotas.getQuotaUsage() + ctx.status = 200 } diff --git a/packages/worker/src/api/controllers/system/environment.ts b/packages/worker/src/api/controllers/system/environment.ts index f3f917c2dd..729a00fa88 100644 --- a/packages/worker/src/api/controllers/system/environment.ts +++ b/packages/worker/src/api/controllers/system/environment.ts @@ -1,10 +1,11 @@ import { Ctx } from "@budibase/types" import env from "../../../environment" +import { env as coreEnv } from "@budibase/backend-core" export const fetch = async (ctx: Ctx) => { ctx.body = { multiTenancy: !!env.MULTI_TENANCY, - offlineMode: !!env.OFFLINE_MODE, + offlineMode: !!coreEnv.OFFLINE_MODE, cloud: !env.SELF_HOSTED, accountPortalUrl: env.ACCOUNT_PORTAL_URL, disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL, diff --git a/packages/worker/src/api/routes/global/license.ts b/packages/worker/src/api/routes/global/license.ts index 0fb2a6e8bd..b0e474e4d5 100644 --- a/packages/worker/src/api/routes/global/license.ts +++ b/packages/worker/src/api/routes/global/license.ts @@ -1,13 +1,44 @@ import Router from "@koa/router" import * as controller from "../../controllers/global/license" +import { middleware } from "@budibase/backend-core" +import Joi from "joi" + +const activateLicenseKeyValidator = middleware.joiValidator.body( + Joi.object({ + licenseKey: Joi.string().required(), + }).required() +) + +const activateOfflineLicenseValidator = middleware.joiValidator.body( + Joi.object({ + offlineLicenseToken: Joi.string().required(), + }).required() +) const router: Router = new Router() router - .post("/api/global/license/activate", controller.activate) .post("/api/global/license/refresh", controller.refresh) - .get("/api/global/license/info", controller.getInfo) - .delete("/api/global/license/info", controller.deleteInfo) .get("/api/global/license/usage", controller.getQuotaUsage) + // LICENSE KEY + .post( + "/api/global/license/key", + activateLicenseKeyValidator, + controller.activateLicenseKey + ) + .get("/api/global/license/key", controller.getLicenseKey) + .delete("/api/global/license/key", controller.deleteLicenseKey) + // OFFLINE LICENSE + .post( + "/api/global/license/offline", + activateOfflineLicenseValidator, + controller.activateOfflineLicenseToken + ) + .get("/api/global/license/offline", controller.getOfflineLicenseToken) + .delete("/api/global/license/offline", controller.deleteOfflineLicenseToken) + .get( + "/api/global/license/offline/identifier", + controller.getOfflineLicenseIdentifier + ) export default router diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts index be0673729e..26e3b3dfb5 100644 --- a/packages/worker/src/api/routes/global/tests/license.spec.ts +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -1,4 +1,6 @@ -import { TestConfiguration } from "../../../../tests" +import { TestConfiguration, mocks, structures } from "../../../../tests" +const licensing = mocks.pro.licensing +const quotas = mocks.pro.quotas describe("/api/global/license", () => { const config = new TestConfiguration() @@ -12,18 +14,105 @@ describe("/api/global/license", () => { }) afterEach(() => { - jest.clearAllMocks() + jest.resetAllMocks() }) - describe("POST /api/global/license/activate", () => { - it("activates license", () => {}) + describe("POST /api/global/license/refresh", () => { + it("returns 200", async () => { + const res = await config.api.license.refresh() + expect(res.status).toBe(200) + expect(licensing.cache.refresh).toBeCalledTimes(1) + }) }) - describe("POST /api/global/license/refresh", () => {}) + describe("GET /api/global/license/usage", () => { + it("returns 200 + usage", async () => { + const usage = structures.quotas.usage() + quotas.getQuotaUsage.mockResolvedValue(usage) + const res = await config.api.license.getUsage() + expect(res.status).toBe(200) + expect(res.body).toEqual(usage) + }) + }) - describe("GET /api/global/license/info", () => {}) + describe("POST /api/global/license/key", () => { + it("returns 200", async () => { + const res = await config.api.license.activateLicenseKey({ + licenseKey: "licenseKey", + }) + expect(res.status).toBe(200) + expect(licensing.keys.activateLicenseKey).toBeCalledWith("licenseKey") + }) + }) - describe("DELETE /api/global/license/info", () => {}) + describe("GET /api/global/license/key", () => { + it("returns 404 when not found", async () => { + const res = await config.api.license.getLicenseKey() + expect(res.status).toBe(404) + }) + it("returns 200 + license key", async () => { + licensing.keys.getLicenseKey.mockResolvedValue("licenseKey") + const res = await config.api.license.getLicenseKey() + expect(res.status).toBe(200) + expect(res.body).toEqual({ + licenseKey: "*", + }) + }) + }) - describe("GET /api/global/license/usage", () => {}) + describe("DELETE /api/global/license/key", () => { + it("returns 204", async () => { + const res = await config.api.license.deleteLicenseKey() + expect(licensing.keys.deleteLicenseKey).toBeCalledTimes(1) + expect(res.status).toBe(204) + }) + }) + + describe("POST /api/global/license/offline", () => { + it("activates offline license", async () => { + const res = await config.api.license.activateOfflineLicense({ + offlineLicenseToken: "offlineLicenseToken", + }) + expect(licensing.offline.activateOfflineLicenseToken).toBeCalledWith( + "offlineLicenseToken" + ) + expect(res.status).toBe(200) + }) + }) + + describe("GET /api/global/license/offline", () => { + it("returns 404 when not found", async () => { + const res = await config.api.license.getOfflineLicense() + expect(res.status).toBe(404) + }) + it("returns 200 + offline license token", async () => { + licensing.offline.getOfflineLicenseToken.mockResolvedValue( + "offlineLicenseToken" + ) + const res = await config.api.license.getOfflineLicense() + expect(res.status).toBe(200) + expect(res.body).toEqual({ + offlineLicenseToken: "*", + }) + }) + }) + + describe("DELETE /api/global/license/offline", () => { + it("returns 204", async () => { + const res = await config.api.license.deleteOfflineLicense() + expect(res.status).toBe(204) + expect(licensing.offline.deleteOfflineLicenseToken).toBeCalledTimes(1) + }) + }) + + describe("GET /api/global/license/offline/identifier", () => { + it("returns 200 + identifier base64", async () => { + licensing.offline.getIdentifierBase64.mockResolvedValue("base64") + const res = await config.api.license.getOfflineLicenseIdentifier() + expect(res.status).toBe(200) + expect(res.body).toEqual({ + identifierBase64: "base64", + }) + }) + }) }) diff --git a/packages/worker/src/environment.ts b/packages/worker/src/environment.ts index 74d58831d9..678ffe7f14 100644 --- a/packages/worker/src/environment.ts +++ b/packages/worker/src/environment.ts @@ -61,7 +61,6 @@ const environment = { CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY, - OFFLINE_MODE: process.env.OFFLINE_MODE, /** * Mock the email service in use - links to ethereal hosted emails are logged instead. */ diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index a79ac0e189..b41b76efda 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -1,8 +1,7 @@ import mocks from "./mocks" // init the licensing mock -import * as pro from "@budibase/pro" -mocks.licenses.init(pro) +mocks.licenses.init(mocks.pro) // use unlimited license by default mocks.licenses.useUnlimited() @@ -238,21 +237,21 @@ class TestConfiguration { const db = context.getGlobalDB() - const id = dbCore.generateDevInfoID(this.user._id) + const id = dbCore.generateDevInfoID(this.user!._id) // TODO: dry this.apiKey = encryption.encrypt( `${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}` ) const devInfo = { _id: id, - userId: this.user._id, + userId: this.user!._id, apiKey: this.apiKey, } await db.put(devInfo) }) } - async getUser(email: string): Promise { + async getUser(email: string): Promise { return context.doInTenant(this.getTenantId(), () => { return users.getGlobalUserByEmail(email) }) @@ -264,7 +263,7 @@ class TestConfiguration { } const response = await this._req(user, null, controllers.users.save) const body = response as SaveUserResponse - return this.getUser(body.email) + return this.getUser(body.email) as Promise } // CONFIGS diff --git a/packages/worker/src/tests/api/license.ts b/packages/worker/src/tests/api/license.ts index 9d7745a80e..a6645226af 100644 --- a/packages/worker/src/tests/api/license.ts +++ b/packages/worker/src/tests/api/license.ts @@ -1,17 +1,62 @@ import TestConfiguration from "../TestConfiguration" import { TestAPI } from "./base" +import { + ActivateLicenseKeyRequest, + ActivateOfflineLicenseTokenRequest, +} from "@budibase/types" export class LicenseAPI extends TestAPI { constructor(config: TestConfiguration) { super(config) } - activate = async (licenseKey: string) => { + refresh = async () => { return this.request - .post("/api/global/license/activate") - .send({ licenseKey: licenseKey }) + .post("/api/global/license/refresh") + .set(this.config.defaultHeaders()) + } + getUsage = async () => { + return this.request + .get("/api/global/license/usage") .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) } + activateLicenseKey = async (body: ActivateLicenseKeyRequest) => { + return this.request + .post("/api/global/license/key") + .send(body) + .set(this.config.defaultHeaders()) + } + getLicenseKey = async () => { + return this.request + .get("/api/global/license/key") + .set(this.config.defaultHeaders()) + } + deleteLicenseKey = async () => { + return this.request + .delete("/api/global/license/key") + .set(this.config.defaultHeaders()) + } + activateOfflineLicense = async (body: ActivateOfflineLicenseTokenRequest) => { + return this.request + .post("/api/global/license/offline") + .send(body) + .set(this.config.defaultHeaders()) + } + getOfflineLicense = async () => { + return this.request + .get("/api/global/license/offline") + .set(this.config.defaultHeaders()) + } + deleteOfflineLicense = async () => { + return this.request + .delete("/api/global/license/offline") + .set(this.config.defaultHeaders()) + } + getOfflineLicenseIdentifier = async () => { + return this.request + .get("/api/global/license/offline/identifier") + .set(this.config.defaultHeaders()) + } } diff --git a/packages/worker/src/tests/mocks/index.ts b/packages/worker/src/tests/mocks/index.ts index 30bb4e1d09..cab019bb46 100644 --- a/packages/worker/src/tests/mocks/index.ts +++ b/packages/worker/src/tests/mocks/index.ts @@ -1,7 +1,11 @@ import * as email from "./email" import { mocks } from "@budibase/backend-core/tests" +import * as _pro from "@budibase/pro" +const pro = jest.mocked(_pro, true) + export default { email, + pro, ...mocks, } diff --git a/qa-core/scripts/testResultsWebhook.js b/qa-core/scripts/testResultsWebhook.js index 40cc42082d..5fbdd3a32e 100644 --- a/qa-core/scripts/testResultsWebhook.js +++ b/qa-core/scripts/testResultsWebhook.js @@ -42,7 +42,7 @@ async function discordResultsNotification(report) { Accept: "application/json", }, body: JSON.stringify({ - content: `**Nightly Tests Status**: ${OUTCOME}`, + content: `**Tests Status**: ${OUTCOME}`, embeds: [ { title: `Budi QA Bot - ${env}`, diff --git a/qa-core/src/account-api/tests/accounts/accounts.cloud.internal.spec.ts b/qa-core/src/account-api/tests/accounts/accounts.cloud.internal.spec.ts index fc5eb57de7..6c1d7eacac 100644 --- a/qa-core/src/account-api/tests/accounts/accounts.cloud.internal.spec.ts +++ b/qa-core/src/account-api/tests/accounts/accounts.cloud.internal.spec.ts @@ -15,7 +15,7 @@ describe("Account Internal Operations", () => { it("performs account deletion by ID", async () => { // Deleting by unknown id doesn't work - const accountId = generator.string() + const accountId = generator.guid() await config.api.accounts.delete(accountId, { status: 404 }) // Create new account