Merge branch 'develop' of github.com:Budibase/budibase into cheeks-fixes
This commit is contained in:
commit
cf178808bf
|
@ -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:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.8.18-alpha.1",
|
"version": "2.8.22-alpha.2",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(/-/, "")}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./platform"
|
|
@ -0,0 +1 @@
|
||||||
|
export * as installation from "./installation"
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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> </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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -2341,10 +2341,6 @@
|
||||||
"label": "Left",
|
"label": "Left",
|
||||||
"value": "left"
|
"value": "left"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "Right",
|
|
||||||
"value": "right"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Above",
|
"label": "Above",
|
||||||
"value": "above"
|
"value": "above"
|
||||||
|
|
|
@ -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
|
|
@ -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."
|
||||||
},
|
},
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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",
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./table"
|
||||||
|
export * from "./schema"
|
||||||
|
export * from "./constants"
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
|
|
@ -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[]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue