Merge branch 'develop' of github.com:Budibase/budibase into cheeks-fixes

This commit is contained in:
Andrew Kingston 2023-07-24 12:03:28 +01:00
commit cf178808bf
73 changed files with 1307 additions and 506 deletions

View File

@ -40,6 +40,24 @@ spec:
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }} - image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always imagePullPolicy: Always
name: proxy-service 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: ports:
- containerPort: {{ .Values.services.proxy.port }} - containerPort: {{ .Values.services.proxy.port }}
env: env:

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8.18-alpha.1", "version": "2.8.22-alpha.2",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init"
import { doWithDB, DocumentType } from "../db" import { doWithDB, DocumentType } from "../db"
import { Database, App } from "@budibase/types" import { Database, App } from "@budibase/types"
const AppState = { export enum AppState {
INVALID: "invalid", INVALID = "invalid",
} }
export interface DeletedApp {
state: AppState
}
const EXPIRY_SECONDS = 3600 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. * @param {string} appId the id of the app to get metadata from.
* @returns {object} the app metadata. * @returns {object} the app metadata.
*/ */
export async function getAppMetadata(appId: string) { export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
const client = await getAppClient() const client = await getAppClient()
// try cache // try cache
let metadata = await client.get(appId) let metadata = await client.get(appId)
@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) {
} }
await client.store(appId, metadata, expiry) 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)) { return metadata
throw { status: 404, message: "No app metadata found" }
}
return metadata as App
} }
/** /**

View File

@ -2,7 +2,7 @@ import env from "../environment"
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants" import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
import { getTenantId, getGlobalDBName } from "../context" import { getTenantId, getGlobalDBName } from "../context"
import { doWithDB, directCouchAllDbs } from "./db" 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 { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
import { App, Database } from "@budibase/types" import { App, Database } from "@budibase/types"
import { getStartEndKeyURL } from "../docIds" import { getStartEndKeyURL } from "../docIds"
@ -101,7 +101,9 @@ export async function getAllApps({
const response = await Promise.allSettled(appPromises) const response = await Promise.allSettled(appPromises)
const apps = response const apps = response
.filter( .filter(
(result: any) => result.status === "fulfilled" && result.value != null (result: any) =>
result.status === "fulfilled" &&
result.value?.state !== AppState.INVALID
) )
.map(({ value }: any) => value) .map(({ value }: any) => value)
if (!all) { if (!all) {
@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) {
) )
// have to list the apps which exist, some may have been deleted // have to list the apps which exist, some may have been deleted
return settled return settled
.filter(promise => promise.status === "fulfilled") .filter(
promise =>
promise.status === "fulfilled" &&
(promise.value as DeletedApp).state !== AppState.INVALID
)
.map(promise => (promise as PromiseFulfilledResult<App>).value) .map(promise => (promise as PromiseFulfilledResult<App>).value)
} }

View File

@ -163,6 +163,7 @@ const environment = {
: false, : false,
...getPackageJsonFields(), ...getPackageJsonFields(),
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
OFFLINE_MODE: process.env.OFFLINE_MODE,
_set(key: any, value: any) { _set(key: any, value: any) {
process.env[key] = value process.env[key] = value
// @ts-ignore // @ts-ignore

View File

@ -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 // LICENSING
export class UsageLimitError extends HTTPError { export class UsageLimitError extends HTTPError {

View File

@ -264,7 +264,7 @@ const getEventTenantId = async (tenantId: string): Promise<string> => {
} }
} }
const getUniqueTenantId = async (tenantId: string): Promise<string> => { export const getUniqueTenantId = async (tenantId: string): Promise<string> => {
// make sure this tenantId always matches the tenantId in context // make sure this tenantId always matches the tenantId in context
return context.doInTenant(tenantId, () => { return context.doInTenant(tenantId, () => {
return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => {

View File

@ -12,29 +12,44 @@ import { localFileDestination } from "../system"
let pinoInstance: pino.Logger | undefined let pinoInstance: pino.Logger | undefined
if (!env.DISABLE_PINO_LOGGER) { if (!env.DISABLE_PINO_LOGGER) {
const level = env.LOG_LEVEL
const pinoOptions: LoggerOptions = { const pinoOptions: LoggerOptions = {
level: env.LOG_LEVEL, level,
formatters: { formatters: {
level: label => { level: level => {
return { level: label.toUpperCase() } return { level: level.toUpperCase() }
}, },
bindings: () => { bindings: () => {
return { if (env.SELF_HOSTED) {
service: env.SERVICE_NAME, // "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()}"`, timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
} }
const destinations: pino.DestinationStream[] = [] const destinations: pino.StreamEntry[] = []
if (env.isDev()) { destinations.push(
destinations.push(pinoPretty({ singleLine: true })) env.isDev()
} ? {
stream: pinoPretty({ singleLine: true }),
level: level as pino.Level,
}
: { stream: process.stdout, level: level as pino.Level }
)
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
destinations.push(localFileDestination()) destinations.push({
stream: localFileDestination(),
level: level as pino.Level,
})
} }
pinoInstance = destinations.length pinoInstance = destinations.length

View File

@ -13,7 +13,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import _ from "lodash" import _ from "lodash"
export const account = (): Account => { export const account = (partial: Partial<Account> = {}): Account => {
return { return {
accountId: uuid(), accountId: uuid(),
tenantId: generator.word(), tenantId: generator.word(),
@ -29,6 +29,7 @@ export const account = (): Account => {
size: "10+", size: "10+",
profession: "Software Engineer", profession: "Software Engineer",
quotaUsage: quotas.usage(), quotaUsage: quotas.usage(),
...partial,
} }
} }

View File

@ -1,4 +1,4 @@
import { structures } from ".." import { generator } from "./generator"
import { newid } from "../../../../src/docIds/newid" import { newid } from "../../../../src/docIds/newid"
export function id() { export function id() {
@ -6,7 +6,7 @@ export function id() {
} }
export function rev() { export function rev() {
return `${structures.generator.character({ return `${generator.character({
numeric: true, numeric: true,
})}-${structures.uuid().replace(/-/, "")}` })}-${generator.guid().replace(/-/, "")}`
} }

View File

@ -0,0 +1 @@
export * from "./platform"

View File

@ -0,0 +1 @@
export * as installation from "./installation"

View File

@ -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(),
}
}

View File

@ -2,6 +2,7 @@ export * from "./common"
export * as accounts from "./accounts" export * as accounts from "./accounts"
export * as apps from "./apps" export * as apps from "./apps"
export * as db from "./db" export * as db from "./db"
export * as docs from "./documents"
export * as koa from "./koa" export * as koa from "./koa"
export * as licenses from "./licenses" export * as licenses from "./licenses"
export * as plugins from "./plugins" export * as plugins from "./plugins"

View File

@ -3,6 +3,8 @@ import {
Customer, Customer,
Feature, Feature,
License, License,
OfflineIdentifier,
OfflineLicense,
PlanModel, PlanModel,
PlanType, PlanType,
PriceDuration, PriceDuration,
@ -11,6 +13,7 @@ import {
Quotas, Quotas,
Subscription, Subscription,
} from "@budibase/types" } from "@budibase/types"
import { generator } from "./generator"
export function price(): PurchasedPrice { export function price(): PurchasedPrice {
return { return {
@ -127,15 +130,15 @@ export function subscription(): Subscription {
} }
} }
export const license = ( interface GenerateLicenseOpts {
opts: { quotas?: Quotas
quotas?: Quotas plan?: PurchasedPlan
plan?: PurchasedPlan planType?: PlanType
planType?: PlanType features?: Feature[]
features?: Feature[] billing?: Billing
billing?: Billing }
} = {}
): License => { export const license = (opts: GenerateLicenseOpts = {}): License => {
return { return {
features: opts.features || [], features: opts.features || [],
quotas: opts.quotas || quotas(), quotas: opts.quotas || quotas(),
@ -143,3 +146,22 @@ export const license = (
billing: opts.billing || billing(), 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,
}
}

View File

@ -47,7 +47,7 @@
</svg> </svg>
{#if tooltip && showTooltip} {#if tooltip && showTooltip}
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}> <div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
<Tooltip textWrapping direction="bottom" text={tooltip} /> <Tooltip textWrapping direction="top" text={tooltip} />
</div> </div>
{/if} {/if}
</div> </div>
@ -80,15 +80,14 @@
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
left: 50%; left: 50%;
top: calc(100% + 4px); bottom: calc(100% + 4px);
width: 100vw;
max-width: 150px;
transform: translateX(-50%); transform: translateX(-50%);
text-align: center; text-align: center;
z-index: 1;
} }
.spectrum-Icon--sizeXS { .spectrum-Icon--sizeXS {
width: 10px; width: var(--spectrum-global-dimension-size-150);
height: 10px; height: var(--spectrum-global-dimension-size-150);
} }
</style> </style>

View File

@ -18,7 +18,7 @@
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { import {
FIELDS, FIELDS,
RelationshipTypes, RelationshipType,
ALLOWABLE_STRING_OPTIONS, ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS, ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES, ALLOWABLE_STRING_TYPES,
@ -33,6 +33,7 @@
import { getBindings } from "components/backend/DataTable/formula" import { getBindings } from "components/backend/DataTable/formula"
import { getContext } from "svelte" import { getContext } from "svelte"
import JSONSchemaModal from "./JSONSchemaModal.svelte" import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core"
const AUTO_TYPE = "auto" const AUTO_TYPE = "auto"
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
@ -183,7 +184,7 @@
dispatch("updatecolumns") dispatch("updatecolumns")
if ( if (
saveColumn.type === LINK_TYPE && saveColumn.type === LINK_TYPE &&
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
) { ) {
// Fetching the new tables // Fetching the new tables
tables.fetch() tables.fetch()
@ -237,7 +238,7 @@
// Default relationships many to many // Default relationships many to many
if (editableColumn.type === LINK_TYPE) { if (editableColumn.type === LINK_TYPE) {
editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
} }
if (editableColumn.type === FORMULA_TYPE) { if (editableColumn.type === FORMULA_TYPE) {
editableColumn.formulaType = "dynamic" editableColumn.formulaType = "dynamic"
@ -285,17 +286,17 @@
{ {
name: `Many ${thisName} rows → many ${linkName} rows`, name: `Many ${thisName} rows → many ${linkName} rows`,
alt: `Many ${table.name} rows → many ${linkTable.name} 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`, name: `One ${linkName} row → many ${thisName} rows`,
alt: `One ${linkTable.name} rows → many ${table.name} 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`, name: `One ${thisName} row → many ${linkName} rows`,
alt: `One ${table.name} rows → many ${linkTable.name} 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 = {} const newError = {}
if (!external && fieldInfo.name?.startsWith("_")) { if (!external && fieldInfo.name?.startsWith("_")) {
newError.name = `Column name cannot start with an underscore.` 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.` newError.name = `Illegal character; must be alpha-numeric.`
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) { } else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
newError.name = `${PROHIBITED_COLUMN_NAMES.join( newError.name = `${PROHIBITED_COLUMN_NAMES.join(

View File

@ -1,5 +1,5 @@
<script> <script>
import { RelationshipTypes } from "constants/backend" import { RelationshipType } from "constants/backend"
import { import {
keepOpen, keepOpen,
Button, Button,
@ -25,11 +25,11 @@
const relationshipTypes = [ const relationshipTypes = [
{ {
label: "One to Many", label: "One to Many",
value: RelationshipTypes.MANY_TO_ONE, value: RelationshipType.MANY_TO_ONE,
}, },
{ {
label: "Many to Many", label: "Many to Many",
value: RelationshipTypes.MANY_TO_MANY, value: RelationshipType.MANY_TO_MANY,
}, },
] ]
@ -58,8 +58,8 @@
value: table._id, value: table._id,
})) }))
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet() $: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE $: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
function getTable(id) { function getTable(id) {
return plusTables.find(table => table._id === id) return plusTables.find(table => table._id === id)
@ -116,7 +116,7 @@
function allRequiredAttributesSet() { function allRequiredAttributesSet() {
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
if (relationshipType === RelationshipTypes.MANY_TO_ONE) { if (relationshipType === RelationshipType.MANY_TO_ONE) {
return base && fromPrimary && fromForeign return base && fromPrimary && fromForeign
} else { } else {
return base && getTable(throughId) && throughFromKey && throughToKey return base && getTable(throughId) && throughFromKey && throughToKey
@ -181,12 +181,12 @@
} }
function otherRelationshipType(type) { function otherRelationshipType(type) {
if (type === RelationshipTypes.MANY_TO_ONE) { if (type === RelationshipType.MANY_TO_ONE) {
return RelationshipTypes.ONE_TO_MANY return RelationshipType.ONE_TO_MANY
} else if (type === RelationshipTypes.ONE_TO_MANY) { } else if (type === RelationshipType.ONE_TO_MANY) {
return RelationshipTypes.MANY_TO_ONE return RelationshipType.MANY_TO_ONE
} else if (type === RelationshipTypes.MANY_TO_MANY) { } else if (type === RelationshipType.MANY_TO_MANY) {
return RelationshipTypes.MANY_TO_MANY return RelationshipType.MANY_TO_MANY
} }
} }
@ -218,7 +218,7 @@
// if any to many only need to check from // if any to many only need to check from
const manyToMany = const manyToMany =
relateFrom.relationshipType === RelationshipTypes.MANY_TO_MANY relateFrom.relationshipType === RelationshipType.MANY_TO_MANY
if (!manyToMany) { if (!manyToMany) {
delete relateFrom.through delete relateFrom.through
@ -253,7 +253,7 @@
} }
relateTo = { relateTo = {
...relateTo, ...relateTo,
relationshipType: RelationshipTypes.ONE_TO_MANY, relationshipType: RelationshipType.ONE_TO_MANY,
foreignKey: relateFrom.fieldName, foreignKey: relateFrom.fieldName,
fieldName: fromPrimary, fieldName: fromPrimary,
} }
@ -321,7 +321,7 @@
fromColumn = toRelationship.name fromColumn = toRelationship.name
} }
relationshipType = relationshipType =
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE fromRelationship.relationshipType || RelationshipType.MANY_TO_ONE
if (selectedFromTable) { if (selectedFromTable) {
fromId = selectedFromTable._id fromId = selectedFromTable._id
fromColumn = selectedFromTable.name fromColumn = selectedFromTable.name

View File

@ -1,4 +1,4 @@
import { RelationshipTypes } from "constants/backend" import { RelationshipType } from "constants/backend"
const typeMismatch = "Column type of the foreign key must match the primary key" const typeMismatch = "Column type of the foreign key must match the primary key"
const columnBeingUsed = "Column name cannot be an existing column" const columnBeingUsed = "Column name cannot be an existing column"
@ -40,7 +40,7 @@ export class RelationshipErrorChecker {
} }
isMany() { isMany() {
return this.type === RelationshipTypes.MANY_TO_MANY return this.type === RelationshipType.MANY_TO_MANY
} }
relationshipTypeSet(type) { relationshipTypeSet(type) {

View File

@ -1,17 +1,9 @@
<script> <script>
import { Select } from "@budibase/bbui" import { Select, Icon } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import { API } from "api" import { API } from "api"
import { parseFile } from "./utils" import { parseFile } from "./utils"
let fileInput
let error = null
let fileName = null
let loading = false
let validation = {}
let validateHash = ""
export let rows = [] export let rows = []
export let schema = {} export let schema = {}
export let allValid = true export let allValid = true
@ -49,6 +41,27 @@
}, },
] ]
let fileInput
let error = null
let fileName = null
let loading = false
let validation = {}
let validateHash = ""
let errors = {}
$: displayColumnOptions = Object.keys(schema || {}).filter(column => {
return validation[column]
})
$: {
// binding in consumer is causing double renders here
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
if (newValidateHash !== validateHash) {
validate(rows, schema)
}
validateHash = newValidateHash
}
$: openFileUpload(promptUpload, fileInput)
async function handleFile(e) { async function handleFile(e) {
loading = true loading = true
error = null error = null
@ -67,34 +80,23 @@
async function validate(rows, schema) { async function validate(rows, schema) {
loading = true loading = true
error = null
validation = {}
allValid = false
try { try {
if (rows.length > 0) { if (rows.length > 0) {
const response = await API.validateNewTableImport({ rows, schema }) const response = await API.validateNewTableImport({ rows, schema })
validation = response.schemaValidation validation = response.schemaValidation
allValid = response.allValid allValid = response.allValid
errors = response.errors
error = null
} }
} catch (e) { } catch (e) {
error = e.message error = e.message
validation = {}
allValid = false
errors = {}
} }
loading = false loading = false
} }
$: {
// binding in consumer is causing double renders here
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
if (newValidateHash !== validateHash) {
validate(rows, schema)
}
validateHash = newValidateHash
}
const handleChange = (name, e) => { const handleChange = (name, e) => {
schema[name].type = e.detail schema[name].type = e.detail
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
@ -106,7 +108,13 @@
} }
} }
$: openFileUpload(promptUpload, fileInput) const deleteColumn = name => {
if (loading) {
return
}
delete schema[name]
schema = schema
}
</script> </script>
<div class="dropzone"> <div class="dropzone">
@ -119,10 +127,8 @@
on:change={handleFile} on:change={handleFile}
/> />
<label for="file-upload" class:uploaded={rows.length > 0}> <label for="file-upload" class:uploaded={rows.length > 0}>
{#if loading} {#if error}
loading... Error: {error}
{:else if error}
error: {error}
{:else if fileName} {:else if fileName}
{fileName} {fileName}
{:else} {:else}
@ -142,23 +148,26 @@
placeholder={null} placeholder={null}
getOptionLabel={option => option.label} getOptionLabel={option => option.label}
getOptionValue={option => option.value} getOptionValue={option => option.value}
disabled={loading}
/> />
<span <span
class={loading || validation[column.name] class={validation[column.name]
? "fieldStatusSuccess" ? "fieldStatusSuccess"
: "fieldStatusFailure"} : "fieldStatusFailure"}
> >
{validation[column.name] ? "Success" : "Failure"} {#if validation[column.name]}
Success
{:else}
Failure
{#if errors[column.name]}
<Icon name="Help" tooltip={errors[column.name]} />
{/if}
{/if}
</span> </span>
<i <Icon
class={`omit-button ri-close-circle-fill ${ size="S"
loading ? "omit-button-disabled" : "" name="Close"
}`} hoverable
on:click={() => { on:click={() => deleteColumn(column.name)}
delete schema[column.name]
schema = schema
}}
/> />
</div> </div>
{/each} {/each}
@ -167,7 +176,7 @@
<Select <Select
label="Display Column" label="Display Column"
bind:value={displayColumn} bind:value={displayColumn}
options={Object.keys(schema)} options={displayColumnOptions}
sort sort
/> />
</div> </div>
@ -235,23 +244,16 @@
justify-self: center; justify-self: center;
font-weight: 600; font-weight: 600;
} }
.fieldStatusFailure { .fieldStatusFailure {
color: var(--red); color: var(--red);
justify-self: center; justify-self: center;
font-weight: 600; font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
} }
.fieldStatusFailure :global(.spectrum-Icon) {
.omit-button { width: 12px;
font-size: 1.2em;
color: var(--grey-7);
cursor: pointer;
justify-self: flex-end;
}
.omit-button-disabled {
pointer-events: none;
opacity: 70%;
} }
.display-column { .display-column {

View File

@ -4,7 +4,8 @@
import { licensing } from "stores/portal" import { licensing } from "stores/portal"
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags" import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
$: isPremiumUser = $licensing.license && !$licensing.isFreePlan $: isBusinessAndAbove =
$licensing.isBusinessPlan || $licensing.isEnterprisePlan
let show let show
let hide let hide
@ -55,22 +56,22 @@
<div class="divider" /> <div class="divider" />
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)} {#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
<a <a
href={isPremiumUser href={isBusinessAndAbove
? "mailto:support@budibase.com" ? "mailto:support@budibase.com"
: "/builder/portal/account/usage"} : "/builder/portal/account/usage"}
> >
<div class="premiumLinkContent" class:disabled={!isPremiumUser}> <div class="premiumLinkContent" class:disabled={!isBusinessAndAbove}>
<div class="icon"> <div class="icon">
<FontAwesomeIcon name="fa-solid fa-envelope" /> <FontAwesomeIcon name="fa-solid fa-envelope" />
</div> </div>
<Body size="S">Email support</Body> <Body size="S">Email support</Body>
</div> </div>
{#if !isPremiumUser} {#if !isBusinessAndAbove}
<div class="premiumBadge"> <div class="premiumBadge">
<div class="icon"> <div class="icon">
<FontAwesomeIcon name="fa-solid fa-lock" /> <FontAwesomeIcon name="fa-solid fa-lock" />
</div> </div>
<Body size="XS">Premium</Body> <Body size="XS">Business</Body>
</div> </div>
{/if} {/if}
</a> </a>

View File

@ -419,16 +419,22 @@
if (query && !query.fields.pagination) { if (query && !query.fields.pagination) {
query.fields.pagination = {} query.fields.pagination = {}
} }
dynamicVariables = getDynamicVariables( // if query doesn't have ID then its new - don't try to copy existing dynamic variables
datasource, if (!queryId) {
query._id, dynamicVariables = []
(variable, queryId) => variable.queryId === queryId globalDynamicBindings = getDynamicVariables(datasource)
) } else {
globalDynamicBindings = getDynamicVariables( dynamicVariables = getDynamicVariables(
datasource, datasource,
query._id, query._id,
(variable, queryId) => variable.queryId !== queryId (variable, queryId) => variable.queryId === queryId
) )
globalDynamicBindings = getDynamicVariables(
datasource,
query._id,
(variable, queryId) => variable.queryId !== queryId
)
}
prettifyQueryRequestBody( prettifyQueryRequestBody(
query, query,

View File

@ -151,7 +151,7 @@ export function isAutoColumnUserRelationship(subtype) {
) )
} }
export const RelationshipTypes = { export const RelationshipType = {
MANY_TO_MANY: "many-to-many", MANY_TO_MANY: "many-to-many",
ONE_TO_MANY: "one-to-many", ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one", MANY_TO_ONE: "many-to-one",

View File

@ -18,7 +18,7 @@
const onClick = dynamicVariable => { const onClick = dynamicVariable => {
const queryId = dynamicVariable.queryId const queryId = dynamicVariable.queryId
queries.select({ _id: queryId }) queries.select({ _id: queryId })
$goto(`./${queryId}`) $goto(`../../query/${queryId}`)
} }
/** /**

View File

@ -10,6 +10,8 @@
Label, Label,
ButtonGroup, ButtonGroup,
notifications, notifications,
CopyInput,
File,
} from "@budibase/bbui" } from "@budibase/bbui"
import { auth, admin } from "stores/portal" import { auth, admin } from "stores/portal"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
@ -21,15 +23,20 @@
$: license = $auth.user.license $: license = $auth.user.license
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade` $: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
// LICENSE KEY
$: activateDisabled = !licenseKey || licenseKeyDisabled $: activateDisabled = !licenseKey || licenseKeyDisabled
let licenseInfo
let licenseKeyDisabled = false let licenseKeyDisabled = false
let licenseKeyType = "text" let licenseKeyType = "text"
let licenseKey = "" let licenseKey = ""
let deleteLicenseKeyModal let deleteLicenseKeyModal
// OFFLINE
let offlineLicenseIdentifier = ""
let offlineLicense = undefined
const offlineLicenseExtensions = [".txt"]
// Make sure page can't be visited directly in cloud // Make sure page can't be visited directly in cloud
$: { $: {
if ($admin.cloud) { if ($admin.cloud) {
@ -37,28 +44,115 @@
} }
} }
const activate = async () => { // LICENSE KEY
const getLicenseKey = async () => {
try { try {
await API.activateLicenseKey({ licenseKey }) licenseKey = await API.getLicenseKey()
await auth.getSelf() if (licenseKey) {
await setLicenseInfo() licenseKey = "**********************************************"
notifications.success("Successfully activated") licenseKeyType = "password"
licenseKeyDisabled = true
activateDisabled = true
}
} catch (e) { } catch (e) {
notifications.error(e.message) console.error(e)
notifications.error("Error retrieving license key")
} }
} }
const destroy = async () => { const activateLicenseKey = async () => {
try {
await API.activateLicenseKey({ licenseKey })
await auth.getSelf()
await getLicenseKey()
notifications.success("Successfully activated")
} catch (e) {
console.error(e)
notifications.error("Error activating license key")
}
}
const deleteLicenseKey = async () => {
try { try {
await API.deleteLicenseKey({ licenseKey }) await API.deleteLicenseKey({ licenseKey })
await auth.getSelf() await auth.getSelf()
await setLicenseInfo() await getLicenseKey()
// reset the form // reset the form
licenseKey = "" licenseKey = ""
licenseKeyDisabled = false licenseKeyDisabled = false
notifications.success("Successfully deleted") notifications.success("Offline license removed")
} catch (e) { } catch (e) {
notifications.error(e.message) console.error(e)
notifications.error("Error deleting license key")
}
}
// OFFLINE LICENSE
const getOfflineLicense = async () => {
try {
const license = await API.getOfflineLicense()
if (license) {
offlineLicense = {
name: "license",
}
} else {
offlineLicense = undefined
}
} catch (e) {
console.error(e)
notifications.error("Error loading offline license")
}
}
const getOfflineLicenseIdentifier = async () => {
try {
const res = await API.getOfflineLicenseIdentifier()
offlineLicenseIdentifier = res.identifierBase64
} catch (e) {
console.error(e)
notifications.error("Error loading installation identifier")
}
}
async function activateOfflineLicense(offlineLicenseToken) {
try {
await API.activateOfflineLicense({ offlineLicenseToken })
await auth.getSelf()
await getOfflineLicense()
notifications.success("Successfully activated")
} catch (e) {
console.error(e)
notifications.error("Error activating offline license")
}
}
async function deleteOfflineLicense() {
try {
await API.deleteOfflineLicense()
await auth.getSelf()
await getOfflineLicense()
notifications.success("Successfully removed ofline license")
} catch (e) {
console.error(e)
notifications.error("Error upload offline license")
}
}
async function onOfflineLicenseChange(event) {
if (event.detail) {
// prevent file preview jitter by assigning constant
// as soon as possible
offlineLicense = {
name: "license",
}
const reader = new FileReader()
reader.readAsText(event.detail)
reader.onload = () => activateOfflineLicense(reader.result)
} else {
offlineLicense = undefined
await deleteOfflineLicense()
} }
} }
@ -73,29 +167,19 @@
} }
} }
// deactivate the license key field if there is a license key set
$: {
if (licenseInfo?.licenseKey) {
licenseKey = "**********************************************"
licenseKeyType = "password"
licenseKeyDisabled = true
activateDisabled = true
}
}
const setLicenseInfo = async () => {
licenseInfo = await API.getLicenseInfo()
}
onMount(async () => { onMount(async () => {
await setLicenseInfo() if ($admin.offlineMode) {
await Promise.all([getOfflineLicense(), getOfflineLicenseIdentifier()])
} else {
await getLicenseKey()
}
}) })
</script> </script>
{#if $auth.isAdmin} {#if $auth.isAdmin}
<DeleteLicenseKeyModal <DeleteLicenseKeyModal
bind:this={deleteLicenseKeyModal} bind:this={deleteLicenseKeyModal}
onConfirm={destroy} onConfirm={deleteLicenseKey}
/> />
<Layout noPadding> <Layout noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
@ -108,42 +192,82 @@
{:else} {:else}
To manage your plan visit your To manage your plan visit your
<Link size="L" href={upgradeUrl}>account</Link> <Link size="L" href={upgradeUrl}>account</Link>
<div>&nbsp</div>
{/if} {/if}
</Body> </Body>
</Layout> </Layout>
<Divider /> <Divider />
<Layout gap="XS" noPadding> {#if $admin.offlineMode}
<Heading size="S">Activate</Heading> <Layout gap="XS" noPadding>
<Body size="S">Enter your license key below to activate your plan</Body> <Heading size="XS">Installation identifier</Heading>
</Layout> <Body size="S"
<Layout noPadding> >Share this with support@budibase.com to obtain your offline license</Body
<div class="fields"> >
<div class="field"> </Layout>
<Label size="L">License key</Label> <Layout noPadding>
<Input <div class="identifier-input">
thin <CopyInput value={offlineLicenseIdentifier} />
bind:value={licenseKey} </div>
type={licenseKeyType} </Layout>
disabled={licenseKeyDisabled} <Divider />
<Layout gap="XS" noPadding>
<Heading size="XS">License</Heading>
<Body size="S">Upload your license to activate your plan</Body>
</Layout>
<Layout noPadding>
<div>
<File
title="Upload license"
extensions={offlineLicenseExtensions}
value={offlineLicense}
on:change={onOfflineLicenseChange}
allowClear={true}
disabled={!!offlineLicense}
/> />
</div> </div>
</div> </Layout>
<ButtonGroup gap="M"> {:else}
<Button cta on:click={activate} disabled={activateDisabled}> <Layout gap="XS" noPadding>
Activate <Heading size="XS">Activate</Heading>
</Button> <Body size="S">Enter your license key below to activate your plan</Body>
{#if licenseInfo?.licenseKey} </Layout>
<Button warning on:click={() => deleteLicenseKeyModal.show()}> <Layout noPadding>
Delete <div class="fields">
<div class="field">
<Label size="L">License key</Label>
<Input
thin
bind:value={licenseKey}
type={licenseKeyType}
disabled={licenseKeyDisabled}
/>
</div>
</div>
<ButtonGroup gap="M">
<Button cta on:click={activateLicenseKey} disabled={activateDisabled}>
Activate
</Button> </Button>
{/if} {#if licenseKey}
</ButtonGroup> <Button warning on:click={() => deleteLicenseKeyModal.show()}>
</Layout> Delete
</Button>
{/if}
</ButtonGroup>
</Layout>
{/if}
<Divider /> <Divider />
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="S">Plan</Heading> <Heading size="XS">Plan</Heading>
<Layout noPadding gap="XXS"> <Layout noPadding gap="S">
<Body size="S">You are currently on the {license.plan.type} plan</Body> <Body size="S">You are currently on the {license.plan.type} plan</Body>
<div>
<Body size="S"
>If you purchase or update your plan on the account</Body
>
<Body size="S"
>portal, click the refresh button to sync those changes</Body
>
</div>
<Body size="XS"> <Body size="XS">
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { {processStringSync("Updated {{ duration time 'millisecond' }} ago", {
time: time:
@ -169,4 +293,7 @@
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
align-items: center; align-items: center;
} }
.identifier-input {
width: 300px;
}
</style> </style>

View File

@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = {
adminUser: { checked: false }, adminUser: { checked: false },
sso: { checked: false }, sso: { checked: false },
}, },
offlineMode: false,
} }
export function createAdminStore() { export function createAdminStore() {

View File

@ -2341,10 +2341,6 @@
"label": "Left", "label": "Left",
"value": "left" "value": "left"
}, },
{
"label": "Right",
"value": "right"
},
{ {
"label": "Above", "label": "Above",
"value": "above" "value": "above"

View File

@ -1,30 +1,58 @@
export const buildLicensingEndpoints = API => ({ export const buildLicensingEndpoints = API => ({
/** // LICENSE KEY
* Activates a self hosted license key
*/
activateLicenseKey: async data => { activateLicenseKey: async data => {
return API.post({ return API.post({
url: `/api/global/license/activate`, url: `/api/global/license/key`,
body: data, body: data,
}) })
}, },
/**
* Delete a self hosted license key
*/
deleteLicenseKey: async () => { deleteLicenseKey: async () => {
return API.delete({ 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
}
}
},
/** // OFFLINE LICENSE
* Get the license info - metadata about the license including the
* obfuscated license key. activateOfflineLicense: async ({ offlineLicenseToken }) => {
*/ return API.post({
getLicenseInfo: async () => { url: "/api/global/license/offline",
return API.get({ body: {
url: "/api/global/license/info", 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", url: "/api/global/license/refresh",
}) })
}, },
/** /**
* Retrieve the usage information for the tenant * Retrieve the usage information for the tenant
*/ */

@ -1 +1 @@
Subproject commit 4d9840700e7684581c39965b7cb6a2b2428c477c Subproject commit 347ee5326812c01ef07f0e744f691ab4823e185a

View File

@ -841,7 +841,8 @@
"auto", "auto",
"json", "json",
"internal", "internal",
"barcodeqr" "barcodeqr",
"bigint"
], ],
"description": "Defines the type of the column, most explain themselves, a link column is a relationship." "description": "Defines the type of the column, most explain themselves, a link column is a relationship."
}, },
@ -1045,7 +1046,8 @@
"auto", "auto",
"json", "json",
"internal", "internal",
"barcodeqr" "barcodeqr",
"bigint"
], ],
"description": "Defines the type of the column, most explain themselves, a link column is a relationship." "description": "Defines the type of the column, most explain themselves, a link column is a relationship."
}, },
@ -1260,7 +1262,8 @@
"auto", "auto",
"json", "json",
"internal", "internal",
"barcodeqr" "barcodeqr",
"bigint"
], ],
"description": "Defines the type of the column, most explain themselves, a link column is a relationship." "description": "Defines the type of the column, most explain themselves, a link column is a relationship."
}, },

View File

@ -768,6 +768,7 @@ components:
- json - json
- internal - internal
- barcodeqr - barcodeqr
- bigint
description: Defines the type of the column, most explain themselves, a link description: Defines the type of the column, most explain themselves, a link
column is a relationship. column is a relationship.
constraints: constraints:
@ -931,6 +932,7 @@ components:
- json - json
- internal - internal
- barcodeqr - barcodeqr
- bigint
description: Defines the type of the column, most explain themselves, a link description: Defines the type of the column, most explain themselves, a link
column is a relationship. column is a relationship.
constraints: constraints:
@ -1101,6 +1103,7 @@ components:
- json - json
- internal - internal
- barcodeqr - barcodeqr
- bigint
description: Defines the type of the column, most explain themselves, a link description: Defines the type of the column, most explain themselves, a link
column is a relationship. column is a relationship.
constraints: constraints:

View File

@ -1,8 +1,4 @@
import { import { FieldTypes, RelationshipType, FormulaTypes } from "../../src/constants"
FieldTypes,
RelationshipTypes,
FormulaTypes,
} from "../../src/constants"
import { object } from "./utils" import { object } from "./utils"
import Resource from "./utils/Resource" import Resource from "./utils/Resource"
@ -100,7 +96,7 @@ const tableSchema = {
}, },
relationshipType: { relationshipType: {
type: "string", type: "string",
enum: Object.values(RelationshipTypes), enum: Object.values(RelationshipType),
description: description:
"Defines the type of relationship that this column will be used for.", "Defines the type of relationship that this column will be used for.",
}, },

View File

@ -1,34 +1,33 @@
import { import {
generateDatasourceID,
getDatasourceParams,
getQueryParams,
DocumentType, DocumentType,
BudibaseInternalDB, generateDatasourceID,
getQueryParams,
getTableParams, getTableParams,
} from "../../db/utils" } from "../../db/utils"
import { destroy as tableDestroy } from "./table/internal" import { destroy as tableDestroy } from "./table/internal"
import { BuildSchemaErrors, InvalidColumns } from "../../constants" import { BuildSchemaErrors, InvalidColumns } from "../../constants"
import { getIntegration } from "../../integrations" import { getIntegration } from "../../integrations"
import { invalidateDynamicVariables } from "../../threads/utils" 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 { import {
UserCtx,
Datasource,
Row,
CreateDatasourceResponse,
UpdateDatasourceResponse,
CreateDatasourceRequest, CreateDatasourceRequest,
VerifyDatasourceRequest, CreateDatasourceResponse,
VerifyDatasourceResponse, Datasource,
DatasourcePlus,
FetchDatasourceInfoRequest, FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse, FetchDatasourceInfoResponse,
IntegrationBase, IntegrationBase,
DatasourcePlus, RestConfig,
SourceName, SourceName,
UpdateDatasourceResponse,
UserCtx,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets" import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
import { areRESTVariablesValid } from "../../sdk/app/datasources/datasources"
function getErrorTables(errors: any, errorType: string) { function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors) return Object.entries(errors)
@ -119,46 +118,7 @@ async function buildFilteredSchema(datasource: Datasource, filter?: string[]) {
} }
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx) {
// Get internal tables ctx.body = await sdk.datasources.fetch()
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]
} }
export async function verify( export async function verify(
@ -290,6 +250,14 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
datasource.config!.auth = auth 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( const response = await db.put(
sdk.tables.populateExternalTableSchemas(datasource) sdk.tables.populateExternalTableSchemas(datasource)
) )

View File

@ -1,4 +1,4 @@
import { generateQueryID, getQueryParams, isProdAppID } from "../../../db/utils" import { generateQueryID } from "../../../db/utils"
import { BaseQueryVerbs, FieldTypes } from "../../../constants" import { BaseQueryVerbs, FieldTypes } from "../../../constants"
import { Thread, ThreadType } from "../../../threads" import { Thread, ThreadType } from "../../../threads"
import { save as saveDatasource } from "../datasource" import { save as saveDatasource } from "../datasource"
@ -29,15 +29,7 @@ function enrichQueries(input: any) {
} }
export async function fetch(ctx: any) { export async function fetch(ctx: any) {
const db = context.getAppDB() ctx.body = await sdk.queries.fetch()
const body = await db.allDocs(
getQueryParams(null, {
include_docs: true,
})
)
ctx.body = enrichQueries(body.rows.map((row: any) => row.doc))
} }
const _import = async (ctx: any) => { const _import = async (ctx: any) => {
@ -109,14 +101,8 @@ export async function save(ctx: any) {
} }
export async function find(ctx: any) { export async function find(ctx: any) {
const db = context.getAppDB() const queryId = ctx.params.queryId
const query = enrichQueries(await db.get(ctx.params.queryId)) ctx.body = await sdk.queries.find(queryId)
// remove properties that could be dangerous in real app
if (isProdAppID(ctx.appId)) {
delete query.fields
delete query.parameters
}
ctx.body = query
} }
//Required to discern between OIDC OAuth config entries //Required to discern between OIDC OAuth config entries

View File

@ -7,7 +7,7 @@ import {
Operation, Operation,
PaginationJson, PaginationJson,
RelationshipsJson, RelationshipsJson,
RelationshipTypes, RelationshipType,
Row, Row,
SearchFilters, SearchFilters,
SortJson, SortJson,
@ -577,7 +577,7 @@ export class ExternalRequest {
) { ) {
continue 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 tableId = isMany ? field.through : field.tableId
const { tableName: relatedTableName } = breakExternalTableId(tableId) const { tableName: relatedTableName } = breakExternalTableId(tableId)
// @ts-ignore // @ts-ignore

View File

@ -20,7 +20,7 @@ import {
FieldSchema, FieldSchema,
Operation, Operation,
QueryJson, QueryJson,
RelationshipTypes, RelationshipType,
RenameColumn, RenameColumn,
Table, Table,
TableRequest, TableRequest,
@ -103,12 +103,12 @@ function getDatasourceId(table: Table) {
} }
function otherRelationshipType(type?: string) { function otherRelationshipType(type?: string) {
if (type === RelationshipTypes.MANY_TO_MANY) { if (type === RelationshipType.MANY_TO_MANY) {
return RelationshipTypes.MANY_TO_MANY return RelationshipType.MANY_TO_MANY
} }
return type === RelationshipTypes.ONE_TO_MANY return type === RelationshipType.ONE_TO_MANY
? RelationshipTypes.MANY_TO_ONE ? RelationshipType.MANY_TO_ONE
: RelationshipTypes.ONE_TO_MANY : RelationshipType.ONE_TO_MANY
} }
function generateManyLinkSchema( function generateManyLinkSchema(
@ -151,12 +151,12 @@ function generateLinkSchema(
column: FieldSchema, column: FieldSchema,
table: Table, table: Table,
relatedTable: Table, relatedTable: Table,
type: RelationshipTypes type: RelationshipType
) { ) {
if (!table.primary || !relatedTable.primary) { if (!table.primary || !relatedTable.primary) {
throw new Error("Unable to generate link schema, no primary keys") 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] const primary = isOneSide ? relatedTable.primary[0] : table.primary[0]
// generate a foreign key // generate a foreign key
const foreignKey = generateForeignKey(column, relatedTable) const foreignKey = generateForeignKey(column, relatedTable)
@ -251,7 +251,7 @@ export async function save(ctx: UserCtx) {
} }
const relatedColumnName = schema.fieldName! const relatedColumnName = schema.fieldName!
const relationType = schema.relationshipType! const relationType = schema.relationshipType!
if (relationType === RelationshipTypes.MANY_TO_MANY) { if (relationType === RelationshipType.MANY_TO_MANY) {
const junctionTable = generateManyLinkSchema( const junctionTable = generateManyLinkSchema(
datasource, datasource,
schema, schema,
@ -265,7 +265,7 @@ export async function save(ctx: UserCtx) {
extraTablesToUpdate.push(junctionTable) extraTablesToUpdate.push(junctionTable)
} else { } else {
const fkTable = const fkTable =
relationType === RelationshipTypes.ONE_TO_MANY relationType === RelationshipType.ONE_TO_MANY
? tableToSave ? tableToSave
: relatedTable : relatedTable
const foreignKey = generateLinkSchema( const foreignKey = generateLinkSchema(

View File

@ -1,6 +1,6 @@
import { objectStore, roles, constants } from "@budibase/backend-core" import { objectStore, roles, constants } from "@budibase/backend-core"
import { FieldType as FieldTypes } from "@budibase/types" 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 { export enum FilterTypes {
STRING = "string", STRING = "string",

View File

@ -7,7 +7,7 @@ import { employeeImport } from "./employeeImport"
import { jobsImport } from "./jobsImport" import { jobsImport } from "./jobsImport"
import { expensesImport } from "./expensesImport" import { expensesImport } from "./expensesImport"
import { db as dbCore } from "@budibase/backend-core" 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_JOBS_TABLE_ID = "ta_bb_jobs"
export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory" export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory"
@ -299,7 +299,7 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
}, },
fieldName: "Assigned", fieldName: "Assigned",
name: "Jobs", name: "Jobs",
relationshipType: RelationshipTypes.MANY_TO_MANY, relationshipType: RelationshipType.MANY_TO_MANY,
tableId: DEFAULT_JOBS_TABLE_ID, tableId: DEFAULT_JOBS_TABLE_ID,
}, },
"Start Date": { "Start Date": {
@ -458,7 +458,7 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
type: FieldTypes.LINK, type: FieldTypes.LINK,
tableId: DEFAULT_EMPLOYEE_TABLE_ID, tableId: DEFAULT_EMPLOYEE_TABLE_ID,
fieldName: "Jobs", fieldName: "Jobs",
relationshipType: RelationshipTypes.MANY_TO_MANY, relationshipType: RelationshipType.MANY_TO_MANY,
// sortable: true, // sortable: true,
}, },
"Works End": { "Works End": {

View File

@ -8,7 +8,7 @@ import {
Database, Database,
FieldSchema, FieldSchema,
LinkDocumentValue, LinkDocumentValue,
RelationshipTypes, RelationshipType,
Row, Row,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
@ -136,16 +136,16 @@ class LinkController {
handleRelationshipType(linkerField: FieldSchema, linkedField: FieldSchema) { handleRelationshipType(linkerField: FieldSchema, linkedField: FieldSchema) {
if ( if (
!linkerField.relationshipType || !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) // make sure by default all are many to many (if not specified)
linkerField.relationshipType = RelationshipTypes.MANY_TO_MANY linkerField.relationshipType = RelationshipType.MANY_TO_MANY
} else if (linkerField.relationshipType === RelationshipTypes.MANY_TO_ONE) { } else if (linkerField.relationshipType === RelationshipType.MANY_TO_ONE) {
// Ensure that the other side of the relationship is locked to one record // Ensure that the other side of the relationship is locked to one record
linkedField.relationshipType = RelationshipTypes.ONE_TO_MANY linkedField.relationshipType = RelationshipType.ONE_TO_MANY
} else if (linkerField.relationshipType === RelationshipTypes.ONE_TO_MANY) { } else if (linkerField.relationshipType === RelationshipType.ONE_TO_MANY) {
linkedField.relationshipType = RelationshipTypes.MANY_TO_ONE linkedField.relationshipType = RelationshipType.MANY_TO_ONE
} }
return { linkerField, linkedField } 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 // iterate through the link IDs in the row field, see if any don't exist already
for (let linkId of rowField) { for (let linkId of rowField) {
if ( if (linkedSchema?.relationshipType === RelationshipType.ONE_TO_MANY) {
linkedSchema?.relationshipType === RelationshipTypes.ONE_TO_MANY
) {
let links = ( let links = (
(await getLinkDocuments({ (await getLinkDocuments({
tableId: field.tableId, tableId: field.tableId,

View File

@ -2,7 +2,7 @@ const TestConfig = require("../../tests/utilities/TestConfiguration")
const { basicRow, basicLinkedRow, basicTable } = require("../../tests/utilities/structures") const { basicRow, basicLinkedRow, basicTable } = require("../../tests/utilities/structures")
const LinkController = require("../linkedRows/LinkController").default const LinkController = require("../linkedRows/LinkController").default
const { context } = require("@budibase/backend-core") const { context } = require("@budibase/backend-core")
const { RelationshipTypes } = require("../../constants") const { RelationshipType } = require("../../constants")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
describe("test the link controller", () => { describe("test the link controller", () => {
@ -16,7 +16,7 @@ describe("test the link controller", () => {
beforeEach(async () => { beforeEach(async () => {
const { _id } = await config.createTable() 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 // update table after creating link
table1 = await config.getTable(_id) table1 = await config.getTable(_id)
}) })
@ -57,17 +57,17 @@ describe("test the link controller", () => {
const controller = await createLinkController(table1) const controller = await createLinkController(table1)
// empty case // empty case
let output = controller.handleRelationshipType({}, {}) let output = controller.handleRelationshipType({}, {})
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) expect(output.linkedField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY)
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) expect(output.linkerField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY)
output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.MANY_TO_MANY }, {}) output = controller.handleRelationshipType({ relationshipType: RelationshipType.MANY_TO_MANY }, {})
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) expect(output.linkedField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY)
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) expect(output.linkerField.relationshipType).toEqual(RelationshipType.MANY_TO_MANY)
output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.MANY_TO_ONE }, {}) output = controller.handleRelationshipType({ relationshipType: RelationshipType.MANY_TO_ONE }, {})
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.ONE_TO_MANY) expect(output.linkedField.relationshipType).toEqual(RelationshipType.ONE_TO_MANY)
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_ONE) expect(output.linkerField.relationshipType).toEqual(RelationshipType.MANY_TO_ONE)
output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.ONE_TO_MANY }, {}) output = controller.handleRelationshipType({ relationshipType: RelationshipType.ONE_TO_MANY }, {})
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_ONE) expect(output.linkedField.relationshipType).toEqual(RelationshipType.MANY_TO_ONE)
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.ONE_TO_MANY) expect(output.linkerField.relationshipType).toEqual(RelationshipType.ONE_TO_MANY)
}) })
it("should be able to delete a row", async () => { 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 () => { it("should throw an error when overwriting a link column", async () => {
const update = cloneDeep(table1) const update = cloneDeep(table1)
update.schema.link.relationshipType = RelationshipTypes.MANY_TO_ONE update.schema.link.relationshipType = RelationshipType.MANY_TO_ONE
let error let error
try { try {
const controller = await createLinkController(update) 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 () => { it("shouldn't allow one to many having many relationships against it", async () => {
const firstTable = await config.createTable() 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) const linkTable = await config.getTable(_id)
// an initial row to link around // an initial row to link around
const row = await createLinkedRow("link", linkTable, firstTable) const row = await createLinkedRow("link", linkTable, firstTable)

View File

@ -81,7 +81,6 @@ const environment = {
SELF_HOSTED: process.env.SELF_HOSTED, SELF_HOSTED: process.env.SELF_HOSTED,
HTTP_MB_LIMIT: process.env.HTTP_MB_LIMIT, HTTP_MB_LIMIT: process.env.HTTP_MB_LIMIT,
FORKED_PROCESS_NAME: process.env.FORKED_PROCESS_NAME || "main", FORKED_PROCESS_NAME: process.env.FORKED_PROCESS_NAME || "main",
OFFLINE_MODE: process.env.OFFLINE_MODE,
// old // old
CLIENT_ID: process.env.CLIENT_ID, CLIENT_ID: process.env.CLIENT_ID,
_set(key: string, value: any) { _set(key: string, value: any) {

View File

@ -10,7 +10,7 @@ import * as setup from "../api/routes/tests/utilities"
import { import {
Datasource, Datasource,
FieldType, FieldType,
RelationshipTypes, RelationshipType,
Row, Row,
SourceName, SourceName,
Table, Table,
@ -101,17 +101,17 @@ describe("postgres integrations", () => {
oneToManyRelationshipInfo = { oneToManyRelationshipInfo = {
table: await createAuxTable("o2m"), table: await createAuxTable("o2m"),
fieldName: "oneToManyRelation", fieldName: "oneToManyRelation",
relationshipType: RelationshipTypes.ONE_TO_MANY, relationshipType: RelationshipType.ONE_TO_MANY,
} }
manyToOneRelationshipInfo = { manyToOneRelationshipInfo = {
table: await createAuxTable("m2o"), table: await createAuxTable("m2o"),
fieldName: "manyToOneRelation", fieldName: "manyToOneRelation",
relationshipType: RelationshipTypes.MANY_TO_ONE, relationshipType: RelationshipType.MANY_TO_ONE,
} }
manyToManyRelationshipInfo = { manyToManyRelationshipInfo = {
table: await createAuxTable("m2m"), table: await createAuxTable("m2m"),
fieldName: "manyToManyRelation", fieldName: "manyToManyRelation",
relationshipType: RelationshipTypes.MANY_TO_MANY, relationshipType: RelationshipType.MANY_TO_MANY,
} }
primaryPostgresTable = await config.createTable({ primaryPostgresTable = await config.createTable({
@ -143,7 +143,7 @@ describe("postgres integrations", () => {
}, },
fieldName: oneToManyRelationshipInfo.fieldName, fieldName: oneToManyRelationshipInfo.fieldName,
name: "oneToManyRelation", name: "oneToManyRelation",
relationshipType: RelationshipTypes.ONE_TO_MANY, relationshipType: RelationshipType.ONE_TO_MANY,
tableId: oneToManyRelationshipInfo.table._id, tableId: oneToManyRelationshipInfo.table._id,
main: true, main: true,
}, },
@ -154,7 +154,7 @@ describe("postgres integrations", () => {
}, },
fieldName: manyToOneRelationshipInfo.fieldName, fieldName: manyToOneRelationshipInfo.fieldName,
name: "manyToOneRelation", name: "manyToOneRelation",
relationshipType: RelationshipTypes.MANY_TO_ONE, relationshipType: RelationshipType.MANY_TO_ONE,
tableId: manyToOneRelationshipInfo.table._id, tableId: manyToOneRelationshipInfo.table._id,
main: true, main: true,
}, },
@ -165,7 +165,7 @@ describe("postgres integrations", () => {
}, },
fieldName: manyToManyRelationshipInfo.fieldName, fieldName: manyToManyRelationshipInfo.fieldName,
name: "manyToManyRelation", name: "manyToManyRelation",
relationshipType: RelationshipTypes.MANY_TO_MANY, relationshipType: RelationshipType.MANY_TO_MANY,
tableId: manyToManyRelationshipInfo.table._id, tableId: manyToManyRelationshipInfo.table._id,
main: true, main: true,
}, },
@ -193,12 +193,12 @@ describe("postgres integrations", () => {
type ForeignTableInfo = { type ForeignTableInfo = {
table: Table table: Table
fieldName: string fieldName: string
relationshipType: RelationshipTypes relationshipType: RelationshipType
} }
type ForeignRowsInfo = { type ForeignRowsInfo = {
row: Row row: Row
relationshipType: RelationshipTypes relationshipType: RelationshipType
} }
async function createPrimaryRow(opts: { async function createPrimaryRow(opts: {
@ -263,7 +263,7 @@ describe("postgres integrations", () => {
rowData[manyToOneRelationshipInfo.fieldName].push(foreignRow._id) rowData[manyToOneRelationshipInfo.fieldName].push(foreignRow._id)
foreignRows.push({ foreignRows.push({
row: foreignRow, 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) rowData[manyToManyRelationshipInfo.fieldName].push(foreignRow._id)
foreignRows.push({ foreignRows.push({
row: foreignRow, row: foreignRow,
relationshipType: RelationshipTypes.MANY_TO_MANY, relationshipType: RelationshipType.MANY_TO_MANY,
}) })
} }
@ -559,7 +559,7 @@ describe("postgres integrations", () => {
expect(res.status).toBe(200) expect(res.status).toBe(200)
const one2ManyForeignRows = foreignRows.filter( const one2ManyForeignRows = foreignRows.filter(
x => x.relationshipType === RelationshipTypes.ONE_TO_MANY x => x.relationshipType === RelationshipType.ONE_TO_MANY
) )
expect(one2ManyForeignRows).toHaveLength(1) expect(one2ManyForeignRows).toHaveLength(1)
@ -921,7 +921,7 @@ describe("postgres integrations", () => {
(row: Row) => row.id === 2 (row: Row) => row.id === 2
) )
expect(m2mRow1).toEqual({ expect(m2mRow1).toEqual({
...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][0].row, ...foreignRowsByType[RelationshipType.MANY_TO_MANY][0].row,
[m2mFieldName]: [ [m2mFieldName]: [
{ {
_id: row._id, _id: row._id,
@ -930,7 +930,7 @@ describe("postgres integrations", () => {
], ],
}) })
expect(m2mRow2).toEqual({ expect(m2mRow2).toEqual({
...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][1].row, ...foreignRowsByType[RelationshipType.MANY_TO_MANY][1].row,
[m2mFieldName]: [ [m2mFieldName]: [
{ {
_id: row._id, _id: row._id,
@ -940,24 +940,24 @@ describe("postgres integrations", () => {
}) })
expect(res.body[m2oFieldName]).toEqual([ 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}`]: [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
row.id, row.id,
}, },
{ {
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][1].row, ...foreignRowsByType[RelationshipType.MANY_TO_ONE][1].row,
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
row.id, row.id,
}, },
{ {
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][2].row, ...foreignRowsByType[RelationshipType.MANY_TO_ONE][2].row,
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
row.id, row.id,
}, },
]) ])
expect(res.body[o2mFieldName]).toEqual([ expect(res.body[o2mFieldName]).toEqual([
{ {
...foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row, ...foreignRowsByType[RelationshipType.ONE_TO_MANY][0].row,
_id: expect.any(String), _id: expect.any(String),
_rev: expect.any(String), _rev: expect.any(String),
}, },

View File

@ -3,7 +3,7 @@ import { Operation, QueryJson, RenameColumn, Table } from "@budibase/types"
import { breakExternalTableId } from "../utils" import { breakExternalTableId } from "../utils"
import SchemaBuilder = Knex.SchemaBuilder import SchemaBuilder = Knex.SchemaBuilder
import CreateTableBuilder = Knex.CreateTableBuilder import CreateTableBuilder = Knex.CreateTableBuilder
import { FieldTypes, RelationshipTypes } from "../../constants" import { FieldTypes, RelationshipType } from "../../constants"
function generateSchema( function generateSchema(
schema: CreateTableBuilder, schema: CreateTableBuilder,
@ -70,8 +70,8 @@ function generateSchema(
case FieldTypes.LINK: case FieldTypes.LINK:
// this side of the relationship doesn't need any SQL work // this side of the relationship doesn't need any SQL work
if ( if (
column.relationshipType !== RelationshipTypes.MANY_TO_ONE && column.relationshipType !== RelationshipType.MANY_TO_ONE &&
column.relationshipType !== RelationshipTypes.MANY_TO_MANY column.relationshipType !== RelationshipType.MANY_TO_MANY
) { ) {
if (!column.foreignKey || !column.tableId) { if (!column.foreignKey || !column.tableId) {
throw "Invalid relationship schema" throw "Invalid relationship schema"

View File

@ -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 { findHBSBlocks, processObjectSync } from "@budibase/string-templates"
import { import {
Datasource, Datasource,
@ -8,15 +8,88 @@ import {
RestAuthConfig, RestAuthConfig,
RestAuthType, RestAuthType,
RestBasicAuthConfig, RestBasicAuthConfig,
Row,
RestConfig,
SourceName, SourceName,
} from "@budibase/types" } from "@budibase/types"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { getEnvironmentVariables } from "../../utils" import { getEnvironmentVariables } from "../../utils"
import { getDefinitions, getDefinition } from "../../../integrations" import { getDefinitions, getDefinition } from "../../../integrations"
import _ from "lodash" import _ from "lodash"
import {
BudibaseInternalDB,
getDatasourceParams,
getTableParams,
} from "../../../db/utils"
import sdk from "../../index"
const ENV_VAR_PREFIX = "env." 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) { export function checkDatasourceTypes(schema: Integration, config: any) {
for (let key of Object.keys(config)) { for (let key of Object.keys(config)) {
if (!schema.datasource[key]) { if (!schema.datasource[key]) {

View File

@ -1,5 +1,49 @@
import { getEnvironmentVariables } from "../../utils" import { getEnvironmentVariables } from "../../utils"
import { processStringSync } from "@budibase/string-templates" 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( export async function enrichContext(
fields: Record<string, any>, fields: Record<string, any>,

View File

@ -3,7 +3,7 @@ import {
Datasource, Datasource,
FieldSchema, FieldSchema,
FieldType, FieldType,
RelationshipTypes, RelationshipType,
} from "@budibase/types" } from "@budibase/types"
import { FieldTypes } from "../../../constants" import { FieldTypes } from "../../../constants"
@ -19,14 +19,14 @@ function checkForeignKeysAreAutoColumns(datasource: Datasource) {
column => column.type === FieldType.LINK column => column.type === FieldType.LINK
) )
relationships.forEach(relationship => { relationships.forEach(relationship => {
if (relationship.relationshipType === RelationshipTypes.MANY_TO_MANY) { if (relationship.relationshipType === RelationshipType.MANY_TO_MANY) {
const tableId = relationship.through! const tableId = relationship.through!
foreignKeys.push({ key: relationship.throughTo!, tableId }) foreignKeys.push({ key: relationship.throughTo!, tableId })
foreignKeys.push({ key: relationship.throughFrom!, tableId }) foreignKeys.push({ key: relationship.throughFrom!, tableId })
} else { } else {
const fk = relationship.foreignKey! const fk = relationship.foreignKey!
const oneSide = const oneSide =
relationship.relationshipType === RelationshipTypes.ONE_TO_MANY relationship.relationshipType === RelationshipType.ONE_TO_MANY
foreignKeys.push({ foreignKeys.push({
tableId: oneSide ? table._id! : relationship.tableId!, tableId: oneSide ? table._id! : relationship.tableId!,
key: fk, key: fk,

View File

@ -1,4 +1,5 @@
import { FieldTypes } from "../constants" import { FieldTypes } from "../constants"
import { ValidColumnNameRegex } from "@budibase/shared-core"
interface SchemaColumn { interface SchemaColumn {
readonly name: string readonly name: string
@ -27,6 +28,7 @@ interface ValidationResults {
schemaValidation: SchemaValidation schemaValidation: SchemaValidation
allValid: boolean allValid: boolean
invalidColumns: Array<string> invalidColumns: Array<string>
errors: Record<string, string>
} }
const PARSERS: any = { const PARSERS: any = {
@ -69,6 +71,7 @@ export function validate(rows: Rows, schema: Schema): ValidationResults {
schemaValidation: {}, schemaValidation: {},
allValid: false, allValid: false,
invalidColumns: [], invalidColumns: [],
errors: {},
} }
rows.forEach(row => { 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 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") { if (typeof columnType !== "string") {
results.invalidColumns.push(columnName) 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 ( } else if (
columnData == null && columnData == null &&
!schema[columnName].constraints?.presence !schema[columnName].constraints?.presence

View File

@ -95,3 +95,4 @@ export enum BuilderSocketEvent {
export const SocketSessionTTL = 60 export const SocketSessionTTL = 60
export const ValidQueryNameRegex = /^[^()]*$/ export const ValidQueryNameRegex = /^[^()]*$/
export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g

View File

@ -1,5 +1,6 @@
import { LicenseOverrides, QuotaUsage } from "../../documents" import { LicenseOverrides, QuotaUsage } from "../../documents"
import { PlanType } from "../../sdk" import { OfflineLicense, PlanType } from "../../sdk"
import { ISO8601 } from "../../shared"
export interface GetLicenseRequest { export interface GetLicenseRequest {
// All fields should be optional to cater for // All fields should be optional to cater for
@ -26,3 +27,13 @@ export interface UpdateLicenseRequest {
planType?: PlanType planType?: PlanType
overrides?: LicenseOverrides overrides?: LicenseOverrides
} }
export interface CreateOfflineLicenseRequest {
installationIdentifierBase64: string
expireAt: ISO8601
}
export interface GetOfflineLicenseResponse {
offlineLicenseToken: string
license: OfflineLicense
}

View File

@ -3,3 +3,4 @@ export * from "./auditLogs"
export * from "./events" export * from "./events"
export * from "./configs" export * from "./configs"
export * from "./scim" export * from "./scim"
export * from "./license"

View File

@ -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
}

View File

@ -51,6 +51,7 @@ export interface Account extends CreateAccount {
licenseRequestedAt?: number licenseRequestedAt?: number
licenseOverrides?: LicenseOverrides licenseOverrides?: LicenseOverrides
quotaUsage?: QuotaUsage quotaUsage?: QuotaUsage
offlineLicenseToken?: string
} }
export interface PasswordAccount extends Account { export interface PasswordAccount extends Account {

View File

@ -7,9 +7,7 @@ export interface Datasource extends Document {
name?: string name?: string
source: SourceName source: SourceName
// the config is defined by the schema // the config is defined by the schema
config?: { config?: Record<string, any>
[key: string]: string | number | boolean | any[]
}
plus?: boolean plus?: boolean
entities?: { entities?: {
[key: string]: Table [key: string]: Table

View File

@ -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
}

View File

@ -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",
}

View File

@ -0,0 +1,3 @@
export * from "./table"
export * from "./schema"
export * from "./constants"

View File

@ -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
}

View File

@ -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
}

View File

@ -9,6 +9,7 @@ export enum Feature {
BRANDING = "branding", BRANDING = "branding",
SCIM = "scim", SCIM = "scim",
SYNC_AUTOMATIONS = "syncAutomations", SYNC_AUTOMATIONS = "syncAutomations",
OFFLINE = "offline",
} }
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined } export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }

View File

@ -1,4 +1,15 @@
import { PurchasedPlan, Quotas, Feature, Billing } from "." 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 { export interface License {
features: Feature[] features: Feature[]

View File

@ -1,3 +1,5 @@
export type DeepPartial<T> = { export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
} }
export type ISO8601 = string

View File

@ -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

View File

@ -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

View File

@ -1,34 +1,83 @@
import { licensing, quotas } from "@budibase/pro" 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<ActivateLicenseKeyRequest>
) {
const { licenseKey } = ctx.request.body const { licenseKey } = ctx.request.body
if (!licenseKey) { await licensing.keys.activateLicenseKey(licenseKey)
ctx.throw(400, "licenseKey is required")
}
await licensing.activateLicenseKey(licenseKey)
ctx.status = 200 ctx.status = 200
} }
export async function getLicenseKey(ctx: UserCtx<void, GetLicenseKeyResponse>) {
const licenseKey = await licensing.keys.getLicenseKey()
if (licenseKey) {
ctx.body = { licenseKey: "*" }
ctx.status = 200
} else {
ctx.status = 404
}
}
export async function deleteLicenseKey(ctx: UserCtx<void, void>) {
await licensing.keys.deleteLicenseKey()
ctx.status = 204
}
// OFFLINE LICENSE
export async function activateOfflineLicenseToken(
ctx: UserCtx<ActivateOfflineLicenseTokenRequest>
) {
const { offlineLicenseToken } = ctx.request.body
await licensing.offline.activateOfflineLicenseToken(offlineLicenseToken)
ctx.status = 200
}
export async function getOfflineLicenseToken(
ctx: UserCtx<void, GetOfflineLicenseTokenResponse>
) {
const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken()
if (offlineLicenseToken) {
ctx.body = { offlineLicenseToken: "*" }
ctx.status = 200
} else {
ctx.status = 404
}
}
export async function deleteOfflineLicenseToken(ctx: UserCtx<void, void>) {
await licensing.offline.deleteOfflineLicenseToken()
ctx.status = 204
}
export async function getOfflineLicenseIdentifier(
ctx: UserCtx<void, GetOfflineIdentifierResponse>
) {
const identifierBase64 = await licensing.offline.getIdentifierBase64()
ctx.body = { identifierBase64 }
ctx.status = 200
}
// LICENSES
export const refresh = async (ctx: any) => { export const refresh = async (ctx: any) => {
await licensing.cache.refresh() await licensing.cache.refresh()
ctx.status = 200 ctx.status = 200
} }
export const getInfo = async (ctx: any) => { // USAGE
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
}
export const getQuotaUsage = async (ctx: any) => { export const getQuotaUsage = async (ctx: any) => {
ctx.body = await quotas.getQuotaUsage() ctx.body = await quotas.getQuotaUsage()
ctx.status = 200
} }

View File

@ -1,10 +1,11 @@
import { Ctx } from "@budibase/types" import { Ctx } from "@budibase/types"
import env from "../../../environment" import env from "../../../environment"
import { env as coreEnv } from "@budibase/backend-core"
export const fetch = async (ctx: Ctx) => { export const fetch = async (ctx: Ctx) => {
ctx.body = { ctx.body = {
multiTenancy: !!env.MULTI_TENANCY, multiTenancy: !!env.MULTI_TENANCY,
offlineMode: !!env.OFFLINE_MODE, offlineMode: !!coreEnv.OFFLINE_MODE,
cloud: !env.SELF_HOSTED, cloud: !env.SELF_HOSTED,
accountPortalUrl: env.ACCOUNT_PORTAL_URL, accountPortalUrl: env.ACCOUNT_PORTAL_URL,
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL, disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,

View File

@ -1,13 +1,44 @@
import Router from "@koa/router" import Router from "@koa/router"
import * as controller from "../../controllers/global/license" 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() const router: Router = new Router()
router router
.post("/api/global/license/activate", controller.activate)
.post("/api/global/license/refresh", controller.refresh) .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) .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 export default router

View File

@ -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", () => { describe("/api/global/license", () => {
const config = new TestConfiguration() const config = new TestConfiguration()
@ -12,18 +14,105 @@ describe("/api/global/license", () => {
}) })
afterEach(() => { afterEach(() => {
jest.clearAllMocks() jest.resetAllMocks()
}) })
describe("POST /api/global/license/activate", () => { describe("POST /api/global/license/refresh", () => {
it("activates license", () => {}) 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",
})
})
})
}) })

View File

@ -61,7 +61,6 @@ const environment = {
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600, CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY, 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. * Mock the email service in use - links to ethereal hosted emails are logged instead.
*/ */

View File

@ -1,8 +1,7 @@
import mocks from "./mocks" import mocks from "./mocks"
// init the licensing mock // init the licensing mock
import * as pro from "@budibase/pro" mocks.licenses.init(mocks.pro)
mocks.licenses.init(pro)
// use unlimited license by default // use unlimited license by default
mocks.licenses.useUnlimited() mocks.licenses.useUnlimited()
@ -238,21 +237,21 @@ class TestConfiguration {
const db = context.getGlobalDB() const db = context.getGlobalDB()
const id = dbCore.generateDevInfoID(this.user._id) const id = dbCore.generateDevInfoID(this.user!._id)
// TODO: dry // TODO: dry
this.apiKey = encryption.encrypt( this.apiKey = encryption.encrypt(
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}` `${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`
) )
const devInfo = { const devInfo = {
_id: id, _id: id,
userId: this.user._id, userId: this.user!._id,
apiKey: this.apiKey, apiKey: this.apiKey,
} }
await db.put(devInfo) await db.put(devInfo)
}) })
} }
async getUser(email: string): Promise<User> { async getUser(email: string): Promise<User | undefined> {
return context.doInTenant(this.getTenantId(), () => { return context.doInTenant(this.getTenantId(), () => {
return users.getGlobalUserByEmail(email) return users.getGlobalUserByEmail(email)
}) })
@ -264,7 +263,7 @@ class TestConfiguration {
} }
const response = await this._req(user, null, controllers.users.save) const response = await this._req(user, null, controllers.users.save)
const body = response as SaveUserResponse const body = response as SaveUserResponse
return this.getUser(body.email) return this.getUser(body.email) as Promise<User>
} }
// CONFIGS // CONFIGS

View File

@ -1,17 +1,62 @@
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { TestAPI } from "./base"
import {
ActivateLicenseKeyRequest,
ActivateOfflineLicenseTokenRequest,
} from "@budibase/types"
export class LicenseAPI extends TestAPI { export class LicenseAPI extends TestAPI {
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
super(config) super(config)
} }
activate = async (licenseKey: string) => { refresh = async () => {
return this.request return this.request
.post("/api/global/license/activate") .post("/api/global/license/refresh")
.send({ licenseKey: licenseKey }) .set(this.config.defaultHeaders())
}
getUsage = async () => {
return this.request
.get("/api/global/license/usage")
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .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())
}
} }

View File

@ -1,7 +1,11 @@
import * as email from "./email" import * as email from "./email"
import { mocks } from "@budibase/backend-core/tests" import { mocks } from "@budibase/backend-core/tests"
import * as _pro from "@budibase/pro"
const pro = jest.mocked(_pro, true)
export default { export default {
email, email,
pro,
...mocks, ...mocks,
} }

View File

@ -42,7 +42,7 @@ async function discordResultsNotification(report) {
Accept: "application/json", Accept: "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
content: `**Nightly Tests Status**: ${OUTCOME}`, content: `**Tests Status**: ${OUTCOME}`,
embeds: [ embeds: [
{ {
title: `Budi QA Bot - ${env}`, title: `Budi QA Bot - ${env}`,

View File

@ -15,7 +15,7 @@ describe("Account Internal Operations", () => {
it("performs account deletion by ID", async () => { it("performs account deletion by ID", async () => {
// Deleting by unknown id doesn't work // Deleting by unknown id doesn't work
const accountId = generator.string() const accountId = generator.guid()
await config.api.accounts.delete(accountId, { status: 404 }) await config.api.accounts.delete(accountId, { status: 404 })
// Create new account // Create new account