1
0
Fork 0
mirror of synced 2024-06-27 02:20:35 +12:00

Merge branch 'master' of github.com:budibase/budibase into reorganise-row-tests-3

This commit is contained in:
Sam Rose 2024-03-20 16:01:29 +00:00
commit 07c6dcc0c0
No known key found for this signature in database
262 changed files with 2157 additions and 2185 deletions

View file

@ -34,18 +34,43 @@
},
{
"files": ["**/*.ts"],
"excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended"],
"globals": {
"NodeJS": true
},
"rules": {
"no-unused-vars": "off",
"no-inner-declarations": "off",
"no-case-declarations": "off",
"no-useless-escape": "off",
"no-undef": "off",
"no-prototype-builtins": "off",
"local-rules/no-budibase-imports": "error",
"@typescript-eslint/no-unused-vars": "error",
"local-rules/no-budibase-imports": "error"
}
},
{
"files": ["**/*.spec.ts"],
"excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser",
"plugins": ["jest", "@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:jest/recommended"],
"env": {
"jest/globals": true
},
"globals": {
"NodeJS": true
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"local-rules/no-test-com": "error",
"local-rules/email-domain-example-com": "error"
"local-rules/email-domain-example-com": "error",
"no-console": "warn",
// We have a lot of tests that don't have assertions, they use our test
// API client that does the assertions for them
"jest/expect-expect": "off",
// We do this in some tests where the behaviour of internal tables
// differs to external, but the API is broadly the same
"jest/no-conditional-expect": "off"
}
},
{

View file

@ -140,7 +140,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
| ingress.className | string | `""` | What ingress class to use. |
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
| ingress.hosts | list | `[]` | Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy. |
| nameOverride | string | `""` | Override the name of the deploymen. Defaults to {{ .Chart.Name }}. |
| nameOverride | string | `""` | Override the name of the deployment. Defaults to {{ .Chart.Name }}. |
| service.port | int | `10000` | Port to expose on the service. |
| service.type | string | `"ClusterIP"` | Service type for the service that points to the main Budibase proxy pod. |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |

View file

@ -1,6 +1,6 @@
# -- Passed to all pods created by this chart. Should not ordinarily need to be changed.
imagePullSecrets: []
# -- Override the name of the deploymen. Defaults to {{ .Chart.Name }}.
# -- Override the name of the deployment. Defaults to {{ .Chart.Name }}.
nameOverride: ""
serviceAccount:

View file

@ -7,11 +7,12 @@ module.exports = {
if (
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
importPath !== "@budibase/backend-core/tests"
importPath !== "@budibase/backend-core/tests" &&
importPath !== "@budibase/string-templates/test/utils"
) {
context.report({
node,
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests.`,
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
})
}
},
@ -24,11 +25,9 @@ module.exports = {
docs: {
description:
"disallow the use of 'test.com' in strings and replace it with 'example.com'",
category: "Possible Errors",
recommended: false,
},
schema: [], // no options
fixable: "code", // Indicates that this rule supports automatic fixing
schema: [],
fixable: "code",
},
create: function (context) {
return {
@ -57,8 +56,6 @@ module.exports = {
docs: {
description:
"enforce using the example.com domain for generator.email calls",
category: "Possible Errors",
recommended: false,
},
fixable: "code",
schema: [],

View file

@ -12,8 +12,6 @@ COPY .yarnrc .
COPY packages/server/package.json packages/server/package.json
COPY packages/worker/package.json packages/worker/package.json
# string-templates does not get bundled during the esbuild process, so we want to use the local version
COPY packages/string-templates/package.json packages/string-templates/package.json
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
@ -26,7 +24,7 @@ RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
RUN echo '' > scripts/syncProPackage.js
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
RUN ./scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production --frozen-lockfile
# copy the actual code
COPY packages/server/dist packages/server/dist
@ -35,7 +33,6 @@ COPY packages/server/client packages/server/client
COPY packages/server/builder packages/server/builder
COPY packages/worker/dist packages/worker/dist
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
COPY packages/string-templates packages/string-templates
FROM budibase/couchdb:v3.3.3 as runner
@ -52,11 +49,11 @@ RUN apt-get update && \
# Install postgres client for pg_dump utils
RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https gpg -y
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https gpg -y
# We use pm2 in order to run multiple node processes in a single container
RUN npm install --global pm2
@ -100,9 +97,6 @@ COPY --from=build /app/node_modules /node_modules
COPY --from=build /app/package.json /package.json
COPY --from=build /app/packages/server /app
COPY --from=build /app/packages/worker /worker
COPY --from=build /app/packages/string-templates /string-templates
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
EXPOSE 80

View file

@ -1,5 +1,5 @@
{
"version": "2.22.0",
"version": "2.22.7",
"npmClient": "yarn",
"packages": [
"packages/*",

View file

@ -12,6 +12,7 @@
"esbuild-node-externals": "^1.8.0",
"eslint": "^8.52.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-local-rules": "^2.0.0",
"eslint-plugin-svelte": "^2.34.0",
"husky": "^8.0.3",
@ -25,6 +26,7 @@
"svelte": "^4.2.10",
"svelte-eslint-parser": "^0.33.1",
"typescript": "5.2.2",
"typescript-eslint": "^7.3.1",
"yargs": "^17.7.2"
},
"scripts": {

@ -1 +1 @@
Subproject commit 0c050591c21d3b67dc0c9225d60cc9e2324c8dac
Subproject commit f5b467b6b1c55c48847545db41be7b1c035e167a

View file

@ -4,10 +4,10 @@ set -e
if [[ -n $CI ]]
then
# --runInBand performs better in ci where resources are limited
echo "jest --coverage --runInBand --forceExit"
jest --coverage --runInBand --forceExit
echo "jest --coverage --runInBand --forceExit $@"
jest --coverage --runInBand --forceExit $@
else
# --maxWorkers performs better in development
echo "jest --coverage --detectOpenHandles"
jest --coverage --detectOpenHandles
echo "jest --coverage --forceExit --detectOpenHandles $@"
jest --coverage --forceExit --detectOpenHandles $@
fi

View file

@ -133,7 +133,7 @@ export async function refreshOAuthToken(
configId?: string
): Promise<RefreshResponse> {
switch (providerType) {
case SSOProviderType.OIDC:
case SSOProviderType.OIDC: {
if (!configId) {
return { err: { data: "OIDC config id not provided" } }
}
@ -142,12 +142,14 @@ export async function refreshOAuthToken(
return { err: { data: "OIDC configuration not found" } }
}
return refreshOIDCAccessToken(oidcConfig, refreshToken)
case SSOProviderType.GOOGLE:
}
case SSOProviderType.GOOGLE: {
let googleConfig = await configs.getGoogleConfig()
if (!googleConfig) {
return { err: { data: "Google configuration not found" } }
}
return refreshGoogleAccessToken(googleConfig, refreshToken)
}
}
}

View file

@ -8,7 +8,7 @@ describe("platformLogout", () => {
await testEnv.withTenant(async () => {
const ctx = structures.koa.newContext()
await auth.platformLogout({ ctx, userId: "test" })
expect(events.auth.logout).toBeCalledTimes(1)
expect(events.auth.logout).toHaveBeenCalledTimes(1)
})
})
})

View file

@ -129,7 +129,7 @@ export default class BaseCache {
}
}
async bustCache(key: string, opts = { client: null }) {
async bustCache(key: string) {
const client = await this.getClient()
try {
await client.delete(generateTenantKey(key))

View file

@ -1,6 +1,6 @@
import { AnyDocument, Database } from "@budibase/types"
import { JobQueue, createQueue } from "../queue"
import { JobQueue, Queue, createQueue } from "../queue"
import * as dbUtils from "../db"
interface ProcessDocMessage {
@ -12,18 +12,26 @@ interface ProcessDocMessage {
const PERSIST_MAX_ATTEMPTS = 100
let processor: DocWritethroughProcessor | undefined
export const docWritethroughProcessorQueue = createQueue<ProcessDocMessage>(
JobQueue.DOC_WRITETHROUGH_QUEUE,
{
jobOptions: {
attempts: PERSIST_MAX_ATTEMPTS,
},
}
)
export class DocWritethroughProcessor {
private static _queue: Queue
public static get queue() {
if (!DocWritethroughProcessor._queue) {
DocWritethroughProcessor._queue = createQueue<ProcessDocMessage>(
JobQueue.DOC_WRITETHROUGH_QUEUE,
{
jobOptions: {
attempts: PERSIST_MAX_ATTEMPTS,
},
}
)
}
return DocWritethroughProcessor._queue
}
class DocWritethroughProcessor {
init() {
docWritethroughProcessorQueue.process(async message => {
DocWritethroughProcessor.queue.process(async message => {
try {
await this.persistToDb(message.data)
} catch (err: any) {
@ -76,7 +84,7 @@ export class DocWritethrough {
}
async patch(data: Record<string, any>) {
await docWritethroughProcessorQueue.add({
await DocWritethroughProcessor.queue.add({
dbName: this.db.name,
docId: this.docId,
data,

View file

@ -1,5 +1,5 @@
import * as utils from "../utils"
import { Duration, DurationType } from "../utils"
import { Duration } from "../utils"
import env from "../environment"
import { getTenantId } from "../context"
import * as redis from "../redis/init"

View file

@ -6,7 +6,7 @@ import { getDB } from "../../db"
import {
DocWritethrough,
docWritethroughProcessorQueue,
DocWritethroughProcessor,
init,
} from "../docWritethrough"
@ -15,7 +15,7 @@ import InMemoryQueue from "../../queue/inMemoryQueue"
const initialTime = Date.now()
async function waitForQueueCompletion() {
const queue: InMemoryQueue = docWritethroughProcessorQueue as never
const queue: InMemoryQueue = DocWritethroughProcessor.queue as never
await queue.waitForCompletion()
}
@ -235,11 +235,11 @@ describe("docWritethrough", () => {
return acc
}, {})
}
const queueMessageSpy = jest.spyOn(docWritethroughProcessorQueue, "add")
const queueMessageSpy = jest.spyOn(DocWritethroughProcessor.queue, "add")
await config.doInTenant(async () => {
let patches = await parallelPatch(5)
expect(queueMessageSpy).toBeCalledTimes(5)
expect(queueMessageSpy).toHaveBeenCalledTimes(5)
await waitForQueueCompletion()
expect(await db.get(documentId)).toEqual(
@ -247,7 +247,7 @@ describe("docWritethrough", () => {
)
patches = { ...patches, ...(await parallelPatch(40)) }
expect(queueMessageSpy).toBeCalledTimes(45)
expect(queueMessageSpy).toHaveBeenCalledTimes(45)
await waitForQueueCompletion()
expect(await db.get(documentId)).toEqual(
@ -255,7 +255,7 @@ describe("docWritethrough", () => {
)
patches = { ...patches, ...(await parallelPatch(10)) }
expect(queueMessageSpy).toBeCalledTimes(55)
expect(queueMessageSpy).toHaveBeenCalledTimes(55)
await waitForQueueCompletion()
expect(await db.get(documentId)).toEqual(
@ -265,6 +265,7 @@ describe("docWritethrough", () => {
})
// This is not yet supported
// eslint-disable-next-line jest/no-disabled-tests
it.skip("patches will execute in order", async () => {
let incrementalValue = 0
const keyToOverride = generator.word()

View file

@ -55,8 +55,8 @@ describe("user cache", () => {
})),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest)
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
expect(UserDB.bulkGet).toHaveBeenCalledWith(userIdsToRequest)
})
it("on a second all, all of them are retrieved from cache", async () => {
@ -82,7 +82,7 @@ describe("user cache", () => {
),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
})
it("when some users are cached, only the missing ones are retrieved from db", async () => {
@ -110,8 +110,8 @@ describe("user cache", () => {
),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toBeCalledWith([
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
expect(UserDB.bulkGet).toHaveBeenCalledWith([
userIdsToRequest[1],
userIdsToRequest[2],
userIdsToRequest[4],

View file

@ -8,7 +8,7 @@ const DEFAULT_WRITE_RATE_MS = 10000
let CACHE: BaseCache | null = null
interface CacheItem<T extends Document> {
doc: any
doc: T
lastWrite: number
}

View file

@ -246,7 +246,7 @@ describe("context", () => {
context.doInAppMigrationContext(db.generateAppID(), async () => {
await otherContextCall()
})
).rejects.toThrowError(
).rejects.toThrow(
"The context cannot be changed, a migration is currently running"
)
}

View file

@ -10,10 +10,6 @@ interface SearchResponse<T> {
totalRows: number
}
interface PaginatedSearchResponse<T> extends SearchResponse<T> {
hasNextPage: boolean
}
export type SearchParams<T> = {
tableId?: string
sort?: string
@ -247,7 +243,7 @@ export class QueryBuilder<T> {
}
// Escape characters
if (!this.#noEscaping && escape && originalType === "string") {
value = `${value}`.replace(/[ \/#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
value = `${value}`.replace(/[ /#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
}
// Wrap in quotes

View file

@ -34,12 +34,12 @@ export async function createUserIndex() {
}
let idxKey = prev != null ? `${prev}.${key}` : key
if (typeof input[key] === "string") {
// @ts-expect-error index is available in a CouchDB map function
// eslint-disable-next-line no-undef
// @ts-ignore
index(idxKey, input[key].toLowerCase(), { facet: true })
} else if (typeof input[key] !== "object") {
// @ts-expect-error index is available in a CouchDB map function
// eslint-disable-next-line no-undef
// @ts-ignore
index(idxKey, input[key], { facet: true })
} else {
idx(input[key], idxKey)

View file

@ -17,13 +17,8 @@ export function init(processors: ProcessorMap) {
// if not processing in this instance, kick it off
if (!processingPromise) {
processingPromise = asyncEventQueue.process(async job => {
const { event, identity, properties, timestamp } = job.data
await documentProcessor.processEvent(
event,
identity,
properties,
timestamp
)
const { event, identity, properties } = job.data
await documentProcessor.processEvent(event, identity, properties)
})
}
}

View file

@ -1,7 +1,6 @@
import {
Event,
Identity,
Group,
IdentityType,
AuditLogQueueEvent,
AuditLogFn,
@ -79,11 +78,11 @@ export default class AuditLogsProcessor implements EventProcessor {
}
}
async identify(identity: Identity, timestamp?: string | number) {
async identify() {
// no-op
}
async identifyGroup(group: Group, timestamp?: string | number) {
async identifyGroup() {
// no-op
}

View file

@ -8,8 +8,7 @@ export default class LoggingProcessor implements EventProcessor {
async processEvent(
event: Event,
identity: Identity,
properties: any,
timestamp?: string
properties: any
): Promise<void> {
if (skipLogging) {
return
@ -17,14 +16,14 @@ export default class LoggingProcessor implements EventProcessor {
console.log(`[audit] [identityType=${identity.type}] ${event}`, properties)
}
async identify(identity: Identity, timestamp?: string | number) {
async identify(identity: Identity) {
if (skipLogging) {
return
}
console.log(`[audit] identified`, identity)
}
async identifyGroup(group: Group, timestamp?: string | number) {
async identifyGroup(group: Group) {
if (skipLogging) {
return
}

View file

@ -14,12 +14,7 @@ export default class DocumentUpdateProcessor implements EventProcessor {
this.processors = processors
}
async processEvent(
event: Event,
identity: Identity,
properties: any,
timestamp?: string | number
) {
async processEvent(event: Event, identity: Identity, properties: any) {
const tenantId = identity.realTenantId
const docId = getDocumentId(event, properties)
if (!tenantId || !docId) {

View file

@ -10,6 +10,18 @@ import { formats } from "dd-trace/ext"
import { localFileDestination } from "../system"
function isPlainObject(obj: any) {
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
}
function isError(obj: any) {
return obj instanceof Error
}
function isMessage(obj: any) {
return typeof obj === "string"
}
// LOGGER
let pinoInstance: pino.Logger | undefined
@ -71,23 +83,11 @@ if (!env.DISABLE_PINO_LOGGER) {
err?: Error
}
function isPlainObject(obj: any) {
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
}
function isError(obj: any) {
return obj instanceof Error
}
function isMessage(obj: any) {
return typeof obj === "string"
}
/**
* Backwards compatibility between console logging statements
* and pino logging requirements.
*/
function getLogParams(args: any[]): [MergingObject, string] {
const getLogParams = (args: any[]): [MergingObject, string] => {
let error = undefined
let objects: any[] = []
let message = ""

View file

@ -11,7 +11,6 @@ export const buildMatcherRegex = (
return patterns.map(pattern => {
let route = pattern.route
const method = pattern.method
const strict = pattern.strict ? pattern.strict : false
// if there is a param in the route
// use a wildcard pattern
@ -24,24 +23,17 @@ export const buildMatcherRegex = (
}
}
return { regex: new RegExp(route), method, strict, route }
return { regex: new RegExp(route), method, route }
})
}
export const matches = (ctx: BBContext, options: RegexMatcher[]) => {
return options.find(({ regex, method, strict, route }) => {
let urlMatch
if (strict) {
urlMatch = ctx.request.url === route
} else {
urlMatch = regex.test(ctx.request.url)
}
return options.find(({ regex, method }) => {
const urlMatch = regex.test(ctx.request.url)
const methodMatch =
method === "ALL"
? true
: ctx.request.method.toLowerCase() === method.toLowerCase()
return urlMatch && methodMatch
})
}

View file

@ -3,7 +3,7 @@ import { Cookie } from "../../../constants"
import * as configs from "../../../configs"
import * as cache from "../../../cache"
import * as utils from "../../../utils"
import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types"
import { UserCtx, SSOProfile } from "@budibase/types"
import { ssoSaveUserNoOp } from "../sso/sso"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy

View file

@ -5,7 +5,6 @@ import * as context from "../../../context"
import fetch from "node-fetch"
import {
SaveSSOUserFunction,
SaveUserOpts,
SSOAuthDetails,
SSOUser,
User,
@ -14,10 +13,8 @@ import {
// no-op function for user save
// - this allows datasource auth and access token refresh to work correctly
// - prefer no-op over an optional argument to ensure function is provided to login flows
export const ssoSaveUserNoOp: SaveSSOUserFunction = (
user: SSOUser,
opts: SaveUserOpts
) => Promise.resolve(user)
export const ssoSaveUserNoOp: SaveSSOUserFunction = (user: SSOUser) =>
Promise.resolve(user)
/**
* Common authentication logic for third parties. e.g. OAuth, OIDC.

View file

@ -114,11 +114,11 @@ describe("sso", () => {
// tenant id added
ssoUser.tenantId = context.getTenantId()
expect(mockSaveUser).toBeCalledWith(ssoUser, {
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
hashPassword: false,
requirePassword: false,
})
expect(mockDone).toBeCalledWith(null, ssoUser)
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
})
})
})
@ -159,11 +159,11 @@ describe("sso", () => {
// existing id preserved
ssoUser._id = existingUser._id
expect(mockSaveUser).toBeCalledWith(ssoUser, {
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
hashPassword: false,
requirePassword: false,
})
expect(mockDone).toBeCalledWith(null, ssoUser)
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
})
})
@ -187,11 +187,11 @@ describe("sso", () => {
// existing id preserved
ssoUser._id = existingUser._id
expect(mockSaveUser).toBeCalledWith(ssoUser, {
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
hashPassword: false,
requirePassword: false,
})
expect(mockDone).toBeCalledWith(null, ssoUser)
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
})
})
})

View file

@ -24,13 +24,13 @@ function buildUserCtx(user: ContextUser) {
}
function passed(throwFn: jest.Func, nextFn: jest.Func) {
expect(throwFn).not.toBeCalled()
expect(nextFn).toBeCalled()
expect(throwFn).not.toHaveBeenCalled()
expect(nextFn).toHaveBeenCalled()
}
function threw(throwFn: jest.Func) {
// cant check next, the throw function doesn't actually throw - so it still continues
expect(throwFn).toBeCalled()
expect(throwFn).toHaveBeenCalled()
}
describe("adminOnly middleware", () => {

View file

@ -34,23 +34,6 @@ describe("matchers", () => {
expect(!!matchers.matches(ctx, built)).toBe(true)
})
it("doesn't wildcard path with strict", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
strict: true,
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id/something/else"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("matches with param", () => {
const pattern = [
{
@ -67,23 +50,6 @@ describe("matchers", () => {
expect(!!matchers.matches(ctx, built)).toBe(true)
})
// TODO: Support the below behaviour
// Strict does not work when a param is present
// it("matches with param with strict", () => {
// const pattern = [{
// route: "/api/tests/:testId",
// method: "GET",
// strict: true
// }]
// const ctx = structures.koa.newContext()
// ctx.request.url = "/api/tests/id"
// ctx.request.method = "GET"
//
// const built = matchers.buildMatcherRegex(pattern)
//
// expect(!!matchers.matches(ctx, built)).toBe(true)
// })
it("doesn't match by path", () => {
const pattern = [
{

View file

@ -45,10 +45,6 @@ export const runMigration = async (
options: MigrationOptions = {}
) => {
const migrationType = migration.type
let tenantId: string | undefined
if (migrationType !== MigrationType.INSTALLATION) {
tenantId = context.getTenantId()
}
const migrationName = migration.name
const silent = migration.silent

View file

@ -126,7 +126,7 @@ describe("app", () => {
it("gets url with embedded minio", async () => {
testEnv.withMinio()
await testEnv.withTenant(tenantId => {
await testEnv.withTenant(() => {
const url = getAppFileUrl()
expect(url).toBe(
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
@ -136,7 +136,7 @@ describe("app", () => {
it("gets url with custom S3", async () => {
testEnv.withS3()
await testEnv.withTenant(tenantId => {
await testEnv.withTenant(() => {
const url = getAppFileUrl()
expect(url).toBe(
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
@ -146,7 +146,7 @@ describe("app", () => {
it("gets url with cloudfront + s3", async () => {
testEnv.withCloudfront()
await testEnv.withTenant(tenantId => {
await testEnv.withTenant(() => {
const url = getAppFileUrl()
// omit rest of signed params
expect(

View file

@ -3,7 +3,7 @@ import { DBTestConfiguration } from "../../../tests/extra"
import * as tenants from "../tenants"
describe("tenants", () => {
const config = new DBTestConfiguration()
new DBTestConfiguration()
describe("addTenant", () => {
it("concurrently adds multiple tenants safely", async () => {

View file

@ -39,7 +39,7 @@ class InMemoryQueue implements Partial<Queue> {
_opts?: QueueOptions
_messages: JobMessage[]
_queuedJobIds: Set<string>
_emitter: EventEmitter
_emitter: NodeJS.EventEmitter
_runCount: number
_addCount: number
@ -166,7 +166,7 @@ class InMemoryQueue implements Partial<Queue> {
return []
}
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async removeJobs(pattern: string) {
// no-op
}

View file

@ -132,7 +132,7 @@ function logging(queue: Queue, jobQueue: JobQueue) {
// A Job is waiting to be processed as soon as a worker is idling.
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
})
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
.on(BullEvent.ACTIVE, async (job: Job) => {
// A job has started. You can use `jobPromise.cancel()`` to abort it.
await doInJobContext(job, () => {
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))

View file

@ -40,6 +40,7 @@ export async function shutdown() {
if (inviteClient) await inviteClient.finish()
if (passwordResetClient) await passwordResetClient.finish()
if (socketClient) await socketClient.finish()
if (docWritethroughClient) await docWritethroughClient.finish()
}
process.on("exit", async () => {

View file

@ -120,7 +120,7 @@ describe("redis", () => {
await redis.bulkStore(data, ttl)
for (const [key, value] of Object.entries(data)) {
for (const key of Object.keys(data)) {
expect(await redis.get(key)).toBe(null)
}
@ -147,17 +147,6 @@ describe("redis", () => {
expect(results).toEqual([1, 2, 3, 4, 5])
})
it("can increment on a new key", async () => {
const key1 = structures.uuid()
const key2 = structures.uuid()
const result1 = await redis.increment(key1)
expect(result1).toBe(1)
const result2 = await redis.increment(key2)
expect(result2).toBe(1)
})
it("can increment multiple times in parallel", async () => {
const key = structures.uuid()
const results = await Promise.all(
@ -184,7 +173,7 @@ describe("redis", () => {
const key = structures.uuid()
await redis.store(key, value)
await expect(redis.increment(key)).rejects.toThrowError(
await expect(redis.increment(key)).rejects.toThrow(
"ERR value is not an integer or out of range"
)
})

View file

@ -96,8 +96,8 @@ describe("redlockImpl", () => {
task: mockTask,
executionTimeMs: lockTtl * 2,
})
).rejects.toThrowError(
`Unable to fully release the lock on resource \"lock:${config.tenantId}_persist_writethrough\".`
).rejects.toThrow(
`Unable to fully release the lock on resource "lock:${config.tenantId}_persist_writethrough".`
)
}
)

View file

@ -158,8 +158,8 @@ describe("getTenantIDFromCtx", () => {
],
}
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeUndefined()
expect(ctx.throw).toBeCalledTimes(1)
expect(ctx.throw).toBeCalledWith(403, "Tenant id not set")
expect(ctx.throw).toHaveBeenCalledTimes(1)
expect(ctx.throw).toHaveBeenCalledWith(403, "Tenant id not set")
})
it("returns undefined if allowNoTenant is true", () => {

View file

@ -45,7 +45,7 @@ describe("Users", () => {
...{ _id: groupId, roles: { app1: "ADMIN" } },
}
const users: User[] = []
for (const _ of Array.from({ length: usersInGroup })) {
for (let i = 0; i < usersInGroup; i++) {
const userId = `us_${generator.guid()}`
const user: User = structures.users.user({
_id: userId,

View file

@ -3,7 +3,7 @@ import { generator } from "./generator"
export function userGroup(): UserGroup {
return {
name: generator.word(),
name: generator.guid(),
icon: generator.word(),
color: generator.word(),
}

View file

@ -39,19 +39,23 @@ const handleClick = event => {
return
}
if (handler.allowedType && event.type !== handler.allowedType) {
return
}
handler.callback?.(event)
})
}
document.documentElement.addEventListener("click", handleClick, true)
document.documentElement.addEventListener("contextmenu", handleClick, true)
document.documentElement.addEventListener("mousedown", handleClick, true)
/**
* Adds or updates a click handler
*/
const updateHandler = (id, element, anchor, callback) => {
const updateHandler = (id, element, anchor, callback, allowedType) => {
let existingHandler = clickHandlers.find(x => x.id === id)
if (!existingHandler) {
clickHandlers.push({ id, element, anchor, callback })
clickHandlers.push({ id, element, anchor, callback, allowedType })
} else {
existingHandler.callback = callback
}
@ -77,7 +81,8 @@ export default (element, opts) => {
const update = newOpts => {
const callback = newOpts?.callback || newOpts
const anchor = newOpts?.anchor || element
updateHandler(id, element, anchor, callback)
const allowedType = newOpts?.allowedType || "click"
updateHandler(id, element, anchor, callback, allowedType)
}
update(opts)
return {

View file

@ -197,7 +197,9 @@
>
<Icon name="ChevronRight" />
</div>
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
{#if maximum !== 1}
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
{/if}
</div>
{:else if value?.length}
{#each value as file}

View file

@ -470,7 +470,7 @@
newError.name = `Column name already in use.`
}
if (fieldInfo.type === "auto" && !fieldInfo.subtype) {
if (fieldInfo.type === FieldType.AUTO && !fieldInfo.subtype) {
newError.subtype = `Auto Column requires a type`
}
@ -531,18 +531,18 @@
}}
/>
{#if editableColumn.type === "string"}
{#if editableColumn.type === FieldType.STRING}
<Input
type="number"
label="Max Length"
bind:value={editableColumn.constraints.length.maximum}
/>
{:else if editableColumn.type === "options"}
{:else if editableColumn.type === FieldType.OPTIONS}
<OptionSelectDnD
bind:constraints={editableColumn.constraints}
bind:optionColors={editableColumn.optionColors}
/>
{:else if editableColumn.type === "longform"}
{:else if editableColumn.type === FieldType.LONGFORM}
<div>
<div class="tooltip-alignment">
<Label size="M">Formatting</Label>
@ -560,12 +560,12 @@
text="Enable rich text support (markdown)"
/>
</div>
{:else if editableColumn.type === "array"}
{:else if editableColumn.type === FieldType.ARRAY}
<OptionSelectDnD
bind:constraints={editableColumn.constraints}
bind:optionColors={editableColumn.optionColors}
/>
{:else if editableColumn.type === "datetime" && !editableColumn.autocolumn}
{:else if editableColumn.type === FieldType.DATETIME && !editableColumn.autocolumn}
<div class="split-label">
<div class="label-length">
<Label size="M">Earliest</Label>
@ -604,7 +604,7 @@
</div>
{/if}
<Toggle bind:value={editableColumn.dateOnly} text="Date only" />
{:else if editableColumn.type === "number" && !editableColumn.autocolumn}
{:else if editableColumn.type === FieldType.NUMBER && !editableColumn.autocolumn}
<div class="split-label">
<div class="label-length">
<Label size="M">Min Value</Label>
@ -629,7 +629,7 @@
/>
</div>
</div>
{:else if editableColumn.type === "link"}
{:else if editableColumn.type === FieldType.LINK}
<RelationshipSelector
bind:relationshipPart1
bind:relationshipPart2
@ -703,6 +703,24 @@
thin
text="Allow multiple users"
/>
{:else if editableColumn.type === FieldType.ATTACHMENT}
<Toggle
value={editableColumn.constraints?.length?.maximum !== 1}
on:change={e => {
if (!e.detail) {
editableColumn.constraints ??= { length: {} }
editableColumn.constraints.length ??= {}
editableColumn.constraints.length.maximum = 1
editableColumn.constraints.length.message =
"cannot contain multiple files"
} else {
delete editableColumn.constraints?.length?.maximum
delete editableColumn.constraints?.length?.message
}
}}
thin
text="Allow multiple"
/>
{/if}
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
<Select

View file

@ -279,3 +279,11 @@ export const buildContextTreeLookupMap = rootComponent => {
})
return map
}
// Get a flat list of ids for all descendants of a component
export const getChildIdsForComponent = component => {
return [
component._id,
...(component?._children ?? []).map(getChildIdsForComponent).flat(1),
]
}

View file

@ -129,10 +129,7 @@
filteredUsers = $usersFetch.rows
.filter(user => user.email !== $auth.user.email)
.map(user => {
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
user,
prodAppId
)
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(user)
const isAppBuilder = user.builder?.apps?.includes(prodAppId)
let role
if (isAdminOrGlobalBuilder) {

View file

@ -24,6 +24,13 @@
navigationStore,
} from "stores/builder"
import { DefaultAppTheme } from "constants"
import BarButtonList from "/src/components/design/settings/controls/BarButtonList.svelte"
$: alignmentOptions = [
{ value: "Left", barIcon: "TextAlignLeft" },
{ value: "Center", barIcon: "TextAlignCenter" },
{ value: "Right", barIcon: "TextAlignRight" },
]
$: screenRouteOptions = $screenStore.screens
.map(screen => screen.routing?.route)
@ -46,6 +53,10 @@
notifications.error("Error updating navigation settings")
}
}
const updateTextAlign = textAlignValue => {
navigationStore.syncAppNavigation({ textAlign: textAlignValue })
}
</script>
<Panel
@ -133,6 +144,15 @@
on:change={e => update("title", e.detail)}
updateOnChange={false}
/>
<div class="label">
<Label size="M">Text align</Label>
</div>
<BarButtonList
options={alignmentOptions}
value={$navigationStore.textAlign}
onChange={updateTextAlign}
/>
{/if}
<div class="label">
<Label>Background</Label>

View file

@ -10,6 +10,7 @@
navigationStore,
selectedScreen,
hoverStore,
componentTreeNodesStore,
snippets,
} from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -132,6 +133,7 @@
error = event.error || "An unknown error occurred"
} else if (type === "select-component" && data.id) {
componentStore.select(data.id)
componentTreeNodesStore.makeNodeVisible(data.id)
} else if (type === "hover-component") {
hoverStore.hover(data.id, false)
} else if (type === "update-prop") {

View file

@ -4,12 +4,12 @@
selectedScreen,
componentStore,
selectedComponent,
componentTreeNodesStore,
} from "stores/builder"
import { findComponent } from "helpers/components"
import { findComponent, getChildIdsForComponent } from "helpers/components"
import { goto, isActive } from "@roxi/routify"
import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
let confirmDeleteDialog
let confirmEjectDialog
@ -63,38 +63,25 @@
componentStore.selectNext()
},
["ArrowRight"]: component => {
componentTreeNodesStore.expandNode(component._id)
componentTreeNodesStore.expandNodes([component._id])
},
["ArrowLeft"]: component => {
componentTreeNodesStore.collapseNode(component._id)
// Select the collapsing root component to ensure the currently selected component is not
// hidden in a collapsed node
componentStore.select(component._id)
componentTreeNodesStore.collapseNodes([component._id])
},
["Ctrl+ArrowRight"]: component => {
componentTreeNodesStore.expandNode(component._id)
const expandChildren = component => {
const children = component._children ?? []
children.forEach(child => {
componentTreeNodesStore.expandNode(child._id)
expandChildren(child)
})
}
expandChildren(component)
const childIds = getChildIdsForComponent(component)
componentTreeNodesStore.expandNodes(childIds)
},
["Ctrl+ArrowLeft"]: component => {
componentTreeNodesStore.collapseNode(component._id)
// Select the collapsing root component to ensure the currently selected component is not
// hidden in a collapsed node
componentStore.select(component._id)
const collapseChildren = component => {
const children = component._children ?? []
children.forEach(child => {
componentTreeNodesStore.collapseNode(child._id)
collapseChildren(child)
})
}
collapseChildren(component)
const childIds = getChildIdsForComponent(component)
componentTreeNodesStore.collapseNodes(childIds)
},
["Escape"]: () => {
if ($isActive(`./:componentId/new`)) {

View file

@ -7,8 +7,8 @@
componentStore,
userSelectedResourceMap,
selectedComponent,
selectedComponentPath,
hoverStore,
componentTreeNodesStore,
} from "stores/builder"
import {
findComponentPath,
@ -17,7 +17,6 @@
} from "helpers/components"
import { get } from "svelte/store"
import { dndStore } from "./dndStore"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
export let components = []
export let level = 0
@ -64,14 +63,11 @@
}
}
const isOpen = (component, selectedComponentPath, openNodes) => {
const isOpen = component => {
if (!component?._children?.length) {
return false
}
if (selectedComponentPath.slice(0, -1).includes(component._id)) {
return true
}
return openNodes[`nodeOpen-${component._id}`]
return componentTreeNodesStore.isNodeExpanded(component._id)
}
const isChildOfSelectedComponent = component => {
@ -83,6 +79,11 @@
return findComponentPath($selectedComponent, component._id)?.length > 0
}
const handleIconClick = componentId => {
componentStore.select(componentId)
componentTreeNodesStore.toggleNode(componentId)
}
const hover = hoverStore.hover
</script>
@ -90,7 +91,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<ul>
{#each filteredComponents || [] as component, index (component._id)}
{@const opened = isOpen(component, $selectedComponentPath, openNodes)}
{@const opened = isOpen(component, openNodes)}
<li
on:click|stopPropagation={() => {
componentStore.select(component._id)
@ -104,7 +105,7 @@
on:dragend={dndStore.actions.reset}
on:dragstart={() => dndStore.actions.dragstart(component)}
on:dragover={dragover(component, index)}
on:iconClick={() => componentTreeNodesStore.toggleNode(component._id)}
on:iconClick={() => handleIconClick(component._id)}
on:drop={onDrop}
hovering={$hoverStore.componentId === component._id}
on:mouseenter={() => hover(component._id)}

View file

@ -85,7 +85,7 @@
}
const automationErrorMessage = appId => {
const app = enrichedApps.find(app => app.devId === appId)
const app = $enrichedApps.find(app => app.devId === appId)
const errors = automationErrors[appId]
return `${app.name} - Automation error (${errorCount(errors)})`
}

View file

@ -0,0 +1,67 @@
import { get } from "svelte/store"
import { createSessionStorageStore } from "@budibase/frontend-core"
import { selectedScreen as selectedScreenStore } from "./screens"
import { findComponentPath } from "helpers/components"
const baseStore = createSessionStorageStore("openNodes", {})
const toggleNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`]
return openNodes
})
}
const expandNodes = componentIds => {
baseStore.update(openNodes => {
const newNodes = Object.fromEntries(
componentIds.map(id => [`nodeOpen-${id}`, true])
)
return { ...openNodes, ...newNodes }
})
}
const collapseNodes = componentIds => {
baseStore.update(openNodes => {
const newNodes = Object.fromEntries(
componentIds.map(id => [`nodeOpen-${id}`, false])
)
return { ...openNodes, ...newNodes }
})
}
// Will ensure all parents of a node are expanded so that it is visible in the tree
const makeNodeVisible = componentId => {
const selectedScreen = get(selectedScreenStore)
const path = findComponentPath(selectedScreen.props, componentId)
const componentIds = path.map(component => component._id)
baseStore.update(openNodes => {
const newNodes = Object.fromEntries(
componentIds.map(id => [`nodeOpen-${id}`, true])
)
return { ...openNodes, ...newNodes }
})
}
const isNodeExpanded = componentId => {
const openNodes = get(baseStore)
return !!openNodes[`nodeOpen-${componentId}`]
}
const store = {
subscribe: baseStore.subscribe,
toggleNode,
expandNodes,
makeNodeVisible,
collapseNodes,
isNodeExpanded,
}
export default store

View file

@ -19,6 +19,7 @@ import {
appStore,
previewStore,
tables,
componentTreeNodesStore,
} from "stores/builder/index"
import { buildFormSchema, getSchemaForDatasource } from "dataBinding"
import {
@ -29,7 +30,6 @@ import {
} from "constants/backend"
import BudiStore from "../BudiStore"
import { Utils } from "@budibase/frontend-core"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
export const INITIAL_COMPONENTS_STATE = {
components: {},
@ -653,8 +653,11 @@ export class ComponentStore extends BudiStore {
this.update(state => {
state.selectedScreenId = targetScreenId
state.selectedComponentId = newComponentId
return state
})
componentTreeNodesStore.makeNodeVisible(newComponentId)
}
getPrevious() {
@ -663,7 +666,6 @@ export class ComponentStore extends BudiStore {
const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex(x => x._id === componentId)
const componentTreeNodes = get(componentTreeNodesStore)
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
@ -680,16 +682,16 @@ export class ComponentStore extends BudiStore {
// If we have siblings above us, choose the sibling or a descendant
if (index > 0) {
// If sibling before us accepts children, select a descendant
// If sibling before us accepts children, and is not collapsed, select a descendant
const previousSibling = parent._children[index - 1]
if (
previousSibling._children?.length &&
componentTreeNodes[`nodeOpen-${previousSibling._id}`]
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
) {
let target = previousSibling
while (
target._children?.length &&
componentTreeNodes[`nodeOpen-${target._id}`]
componentTreeNodesStore.isNodeExpanded(target._id)
) {
target = target._children[target._children.length - 1]
}
@ -711,7 +713,6 @@ export class ComponentStore extends BudiStore {
const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex(x => x._id === componentId)
const componentTreeNodes = get(componentTreeNodesStore)
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
@ -720,11 +721,11 @@ export class ComponentStore extends BudiStore {
return navComponentId
}
// If we have children, select first child
// If we have children, select first child, and the node is not collapsed
if (
component._children?.length &&
(state.selectedComponentId === navComponentId ||
componentTreeNodes[`nodeOpen-${component._id}`])
componentTreeNodesStore.isNodeExpanded(component._id))
) {
return component._children[0]._id
} else if (!parent) {
@ -803,7 +804,10 @@ export class ComponentStore extends BudiStore {
// sibling
const previousSibling = parent._children[index - 1]
const definition = this.getDefinition(previousSibling._component)
if (definition.hasChildren) {
if (
definition.hasChildren &&
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
) {
previousSibling._children.push(originalComponent)
}
@ -852,10 +856,13 @@ export class ComponentStore extends BudiStore {
// Move below the next sibling if we are not the last sibling
if (index < parent._children.length) {
// If the next sibling has children, become the first child
// If the next sibling has children, and is not collapsed, become the first child
const nextSibling = parent._children[index]
const definition = this.getDefinition(nextSibling._component)
if (definition.hasChildren) {
if (
definition.hasChildren &&
componentTreeNodesStore.isNodeExpanded(nextSibling._id)
) {
nextSibling._children.splice(0, 0, originalComponent)
}
@ -1151,13 +1158,3 @@ export const selectedComponent = derived(
return clone
}
)
export const selectedComponentPath = derived(
[componentStore, selectedScreen],
([$store, $selectedScreen]) => {
return findComponentPath(
$selectedScreen?.props,
$store.selectedComponentId
).map(component => component._id)
}
)

View file

@ -1,10 +1,6 @@
import { layoutStore } from "./layouts.js"
import { appStore } from "./app.js"
import {
componentStore,
selectedComponent,
selectedComponentPath,
} from "./components"
import { componentStore, selectedComponent } from "./components"
import { navigationStore } from "./navigation.js"
import { themeStore } from "./theme.js"
import { screenStore, selectedScreen, sortedScreens } from "./screens.js"
@ -31,8 +27,10 @@ import { integrations } from "./integrations"
import { sortedIntegrations } from "./sortedIntegrations"
import { queries } from "./queries"
import { flags } from "./flags"
import componentTreeNodesStore from "./componentTreeNodes"
export {
componentTreeNodesStore,
layoutStore,
appStore,
componentStore,
@ -51,7 +49,6 @@ export {
isOnlyUser,
deploymentStore,
selectedComponent,
selectedComponentPath,
tables,
views,
viewsV2,

View file

@ -11,6 +11,7 @@ export const INITIAL_NAVIGATION_STATE = {
hideLogo: null,
logoUrl: null,
hideTitle: null,
textAlign: "Left",
navBackground: null,
navWidth: null,
navTextColor: null,

View file

@ -1,36 +0,0 @@
import { createSessionStorageStore } from "@budibase/frontend-core"
const baseStore = createSessionStorageStore("openNodes", {})
const toggleNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`]
return openNodes
})
}
const expandNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = true
return openNodes
})
}
const collapseNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = false
return openNodes
})
}
const store = {
subscribe: baseStore.subscribe,
toggleNode,
expandNode,
collapseNode,
}
export default store

View file

@ -4,6 +4,16 @@
"composite": true,
"declaration": true,
"sourceMap": true,
"baseUrl": "."
"baseUrl": ".",
"paths": {
"assets/*": ["./assets/*"],
"@budibase/*": [
"../*/src/index.ts",
"../*/src/index.js",
"../*",
"../../node_modules/@budibase/*"
],
"*": ["./src/*"]
}
}
}

View file

@ -11,11 +11,13 @@
"types": ["node", "jest"],
"outDir": "dist",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"],
"@budibase/backend-core/*": ["../backend-core/*"],
"@budibase/shared-core": ["../shared-core/src"]
"@budibase/shared-core": ["../shared-core/src"],
"@budibase/string-templates": ["../string-templates/src"]
}
},
"include": ["src/**/*"],

View file

@ -1,16 +1,8 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
"resolveJsonModule": true
},
"ts-node": {
"require": ["tsconfig-paths/register"],
"swc": true
},
"include": ["src/**/*", "package.json"],
"exclude": ["node_modules", "dist"]
}

View file

@ -36,6 +36,7 @@
export let pageWidth
export let logoLinkUrl
export let openLogoLinkInNewTab
export let textAlign
export let embedded = false
@ -226,7 +227,7 @@
{/if}
{/if}
{#if !hideTitle && title}
<Heading size="S">{title}</Heading>
<Heading size="S" {textAlign}>{title}</Heading>
{/if}
</div>
{#if !embedded}
@ -290,7 +291,10 @@
<div
id="side-panel-container"
class:open={$sidePanelStore.open}
use:clickOutside={autoCloseSidePanel ? sidePanelStore.actions.close : null}
use:clickOutside={{
callback: autoCloseSidePanel ? sidePanelStore.actions.close : null,
allowedType: "mousedown",
}}
class:builder={$builderStore.inBuilder}
>
<div class="side-panel-header">

View file

@ -0,0 +1,86 @@
<script>
import BlockComponent from "components/BlockComponent.svelte"
import { FieldType } from "@budibase/types"
export let field
export let schema
export let order
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
bigint: "bigintfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
bb_reference: "bbreferencefield",
}
const getFieldSchema = field => {
const fieldSchemaName = field.field || field.name
if (!fieldSchemaName || !schema?.[fieldSchemaName]) {
return null
}
return schema[fieldSchemaName]
}
const getComponentForField = field => {
const fieldSchema = getFieldSchema(field)
if (!fieldSchema) {
return null
}
const { type } = fieldSchema
return FieldTypeToComponentMap[type]
}
const getPropsForField = field => {
let fieldProps = field._component
? {
...field,
}
: {
field: field.name,
label: field.name,
placeholder: field.name,
_instanceName: field.name,
}
fieldProps = {
...getPropsByType(field),
...fieldProps,
}
return fieldProps
}
function getPropsByType(field) {
const propsMapByType = {
[FieldType.ATTACHMENT]: (_field, schema) => {
return {
maximum: schema?.constraints?.length?.maximum,
}
},
}
const fieldSchema = getFieldSchema(field)
const mapper = propsMapByType[fieldSchema.type]
if (mapper) {
return mapper(field, fieldSchema)
}
}
</script>
{#if getComponentForField(field) && field.active}
<BlockComponent
type={getComponentForField(field)}
props={getPropsForField(field)}
{order}
interactive
name={field?.field}
/>
{/if}

View file

@ -6,6 +6,7 @@
import { Utils } from "@budibase/frontend-core"
import FormBlockWrapper from "./form/FormBlockWrapper.svelte"
import { get, writable } from "svelte/store"
import FormBlockComponent from "./FormBlockComponent.svelte"
export let actionType
export let rowId
@ -23,22 +24,6 @@
const currentStep = writable(1)
setContext("current-step", currentStep)
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
bigint: "bigintfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
bb_reference: "bbreferencefield",
}
let schema
$: fetchSchema(dataSource)
@ -78,27 +63,6 @@
currentStep.set(newStep + 1)
}
const getPropsForField = field => {
if (field._component) {
return field
}
return {
field: field.name,
label: field.name,
placeholder: field.name,
_instanceName: field.name,
}
}
const getComponentForField = field => {
const fieldSchemaName = field.field || field.name
if (!fieldSchemaName || !schema?.[fieldSchemaName]) {
return null
}
const type = schema[fieldSchemaName].type
return FieldTypeToComponentMap[type]
}
const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {}
}
@ -111,6 +75,7 @@
.filter(field => !field.autocolumn)
.map(field => ({
name: field.name,
active: true,
}))
}
@ -205,15 +170,7 @@
class:mobile={$context.device.mobile}
>
{#each step.fields as field, fieldIdx (`${field.field || field.name}_${fieldIdx}`)}
{#if getComponentForField(field)}
<BlockComponent
type={getComponentForField(field)}
props={getPropsForField(field)}
order={fieldIdx}
interactive
name={field.field}
/>
{/if}
<FormBlockComponent {field} {schema} order={fieldIdx} />
{/each}
</div>
</BlockComponent>

View file

@ -2,6 +2,7 @@
import BlockComponent from "components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte"
import { getContext } from "svelte"
import FormBlockComponent from "../FormBlockComponent.svelte"
export let dataSource
export let actionType
@ -14,49 +15,11 @@
export let buttonPosition = "bottom"
export let schema
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
bigint: "bigintfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
bb_reference: "bbreferencefield",
}
const context = getContext("context")
let formId
$: renderHeader = buttons || title
const getComponentForField = field => {
const fieldSchemaName = field.field || field.name
if (!fieldSchemaName || !schema?.[fieldSchemaName]) {
return null
}
const type = schema[fieldSchemaName].type
return FieldTypeToComponentMap[type]
}
const getPropsForField = field => {
let fieldProps = field._component
? {
...field,
}
: {
field: field.name,
label: field.name,
placeholder: field.name,
_instanceName: field.name,
}
return fieldProps
}
</script>
{#if fields?.length}
@ -132,15 +95,7 @@
<BlockComponent type="container">
<div class="form-block fields" class:mobile={$context.device.mobile}>
{#each fields as field, idx}
{#if getComponentForField(field) && field.active}
<BlockComponent
type={getComponentForField(field)}
props={getPropsForField(field)}
order={idx}
interactive
name={field?.field}
/>
{/if}
<FormBlockComponent {field} {schema} order={idx} />
{/each}
</div>
</BlockComponent>

View file

@ -9,6 +9,7 @@
export let api
export let invertX = false
export let invertY = false
export let schema
const { API, notifications } = getContext("grid")
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
@ -97,6 +98,7 @@
{value}
compact
on:change={e => onChange(e.detail)}
maximum={schema.constraints?.length?.maximum}
{processFiles}
{deleteAttachments}
{handleFileTooLarge}

@ -1 +1 @@
Subproject commit c4c98ae70f2e936009250893898ecf11f4ddf2c3
Subproject commit dd748e045ffdbc6662c5d2b76075f01d65a96a2f

View file

@ -41,17 +41,9 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates
COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
COPY packages/string-templates .
WORKDIR /app
COPY packages/server/package.json .
COPY packages/server/dist/yarn.lock .
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module FirebaseMock {
const firebase: any = {}

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module SendgridMock {
class Email {
constructor() {

View file

@ -1,8 +1,5 @@
module AirtableMock {
function Airtable() {
// @ts-ignore
this.base = jest.fn()
}
module.exports = Airtable
class Airtable {
base = jest.fn()
}
module.exports = Airtable

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module ArangoMock {
const arangodb: any = {}

View file

@ -1,102 +1,81 @@
import fs from "fs"
import { join } from "path"
module AwsMock {
const aws: any = {}
const response = (body: any, extra?: any) => () => ({
promise: () => body,
...extra,
})
const response = (body: any, extra?: any) => () => ({
promise: () => body,
...extra,
})
function DocumentClient() {
// @ts-ignore
this.put = jest.fn(response({}))
// @ts-ignore
this.query = jest.fn(
response({
Items: [],
})
)
// @ts-ignore
this.scan = jest.fn(
response({
Items: [
{
Name: "test",
},
],
})
)
// @ts-ignore
this.get = jest.fn(response({}))
// @ts-ignore
this.update = jest.fn(response({}))
// @ts-ignore
this.delete = jest.fn(response({}))
}
function S3() {
// @ts-ignore
this.listObjects = jest.fn(
response({
Contents: [],
})
)
// @ts-ignore
this.createBucket = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.deleteObjects = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.getSignedUrl = (operation, params) => {
return `http://example.com/${params.Bucket}/${params.Key}`
}
// @ts-ignore
this.headBucket = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.upload = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.getObject = jest.fn(
response(
class DocumentClient {
put = jest.fn(response({}))
query = jest.fn(
response({
Items: [],
})
)
scan = jest.fn(
response({
Items: [
{
Body: "",
Name: "test",
},
{
createReadStream: jest
.fn()
.mockReturnValue(
fs.createReadStream(join(__dirname, "aws-sdk.ts"))
),
}
)
)
}
aws.DynamoDB = { DocumentClient }
aws.S3 = S3
aws.config = { update: jest.fn() }
module.exports = aws
],
})
)
get = jest.fn(response({}))
update = jest.fn(response({}))
delete = jest.fn(response({}))
}
class S3 {
listObjects = jest.fn(
response({
Contents: [],
})
)
createBucket = jest.fn(
response({
Contents: {},
})
)
deleteObjects = jest.fn(
response({
Contents: {},
})
)
getSignedUrl = jest.fn((operation, params) => {
return `http://example.com/${params.Bucket}/${params.Key}`
})
headBucket = jest.fn(
response({
Contents: {},
})
)
upload = jest.fn(
response({
Contents: {},
})
)
getObject = jest.fn(
response(
{
Body: "",
},
{
createReadStream: jest
.fn()
.mockReturnValue(fs.createReadStream(join(__dirname, "aws-sdk.ts"))),
}
)
)
}
module.exports = {
DynamoDB: {
DocumentClient,
},
S3,
config: {
update: jest.fn(),
},
}

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module MongoMock {
const mongodb: any = {}

View file

@ -1,24 +1,17 @@
module MsSqlMock {
const mssql: any = {}
mssql.query = jest.fn(() => ({
module.exports = {
ConnectionPool: jest.fn(() => ({
connect: jest.fn(() => ({
request: jest.fn(() => ({
query: jest.fn(sql => ({ recordset: [sql] })),
})),
})),
})),
query: jest.fn(() => ({
recordset: [
{
a: "string",
b: 1,
},
],
}))
// mssql.connect = jest.fn(() => ({ recordset: [] }))
mssql.ConnectionPool = jest.fn(() => ({
connect: jest.fn(() => ({
request: jest.fn(() => ({
query: jest.fn(sql => ({ recordset: [sql] })),
})),
})),
}))
module.exports = mssql
})),
}

View file

@ -1,14 +1,11 @@
module MySQLMock {
const mysql: any = {}
const client = {
connect: jest.fn(),
query: jest.fn((query, bindings, fn) => {
fn(null, [])
}),
}
mysql.createConnection = jest.fn(() => client)
module.exports = mysql
const client = {
connect: jest.fn(),
query: jest.fn((query, bindings, fn) => {
fn(null, [])
}),
}
module.exports = {
createConnection: jest.fn(() => client),
client,
}

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module MySQLMock {
const mysql: any = {}

View file

@ -1,6 +1,7 @@
// @ts-ignore
import fs from "fs"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module FetchMock {
// @ts-ignore
const fetch = jest.requireActual("node-fetch")

View file

@ -1,31 +1,21 @@
module OracleDbMock {
// mock execute
const execute = jest.fn(() => ({
rows: [
{
a: "string",
b: 1,
},
],
}))
const executeMock = jest.fn(() => ({
rows: [
{
a: "string",
b: 1,
},
],
}))
const close = jest.fn()
const closeMock = jest.fn()
// mock connection
function Connection() {}
Connection.prototype.execute = execute
Connection.prototype.close = close
// mock oracledb
const oracleDb: any = {}
oracleDb.getConnection = jest.fn(() => {
// @ts-ignore
return new Connection()
})
// expose mocks
oracleDb.executeMock = execute
oracleDb.closeMock = close
module.exports = oracleDb
class Connection {
execute = executeMock
close = closeMock
}
module.exports = {
getConnection: jest.fn(() => new Connection()),
executeMock,
closeMock,
}

View file

@ -1,30 +1,25 @@
module PgMock {
const pg: any = {}
const query = jest.fn(() => ({
rows: [
{
a: "string",
b: 1,
},
],
}))
const query = jest.fn(() => ({
rows: [
{
a: "string",
b: 1,
},
],
}))
// constructor
function Client() {}
Client.prototype.query = query
Client.prototype.end = jest.fn(cb => {
class Client {
query = query
end = jest.fn(cb => {
if (cb) cb()
})
Client.prototype.connect = jest.fn()
Client.prototype.release = jest.fn()
const on = jest.fn()
pg.Client = Client
pg.queryMock = query
pg.on = on
module.exports = pg
connect = jest.fn()
release = jest.fn()
}
const on = jest.fn()
module.exports = {
Client,
queryMock: query,
on,
}

View file

@ -30,6 +30,8 @@ const baseConfig: Config.InitialProjectOptions = {
"@budibase/backend-core": "<rootDir>/../backend-core/src",
"@budibase/shared-core": "<rootDir>/../shared-core/src",
"@budibase/types": "<rootDir>/../types/src",
"@budibase/string-templates/(.*)": ["<rootDir>/../string-templates/$1"],
"@budibase/string-templates": ["<rootDir>/../string-templates/src"],
},
}

View file

@ -175,6 +175,10 @@
]
},
"build": {
"inputs": [
"{projectRoot}/builder",
"{projectRoot}/client"
],
"outputs": [
"{projectRoot}/builder",
"{projectRoot}/client",

View file

@ -26,7 +26,6 @@ import {
env as envCore,
ErrorCode,
events,
HTTPError,
migrations,
objectStore,
roles,

View file

@ -39,25 +39,28 @@ export async function create(ctx: any) {
let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000)
switch (source) {
case PluginSource.NPM:
case PluginSource.NPM: {
const { metadata: metadataNpm, directory: directoryNpm } =
await npmUpload(url, name)
metadata = metadataNpm
directory = directoryNpm
break
case PluginSource.GITHUB:
}
case PluginSource.GITHUB: {
const { metadata: metadataGithub, directory: directoryGithub } =
await githubUpload(url, name, githubToken)
metadata = metadataGithub
directory = directoryGithub
break
case PluginSource.URL:
}
case PluginSource.URL: {
const headersObj = headers || {}
const { metadata: metadataUrl, directory: directoryUrl } =
await urlUpload(url, name, headersObj)
metadata = metadataUrl
directory = directoryUrl
break
}
}
pluginCore.validate(metadata?.schema)

View file

@ -109,13 +109,14 @@ export class OpenAPI2 extends OpenAPISource {
for (let param of allParams) {
if (parameterNotRef(param)) {
switch (param.in) {
case "query":
case "query": {
let prefix = ""
if (queryString) {
prefix = "&"
}
queryString = `${queryString}${prefix}${param.name}={{${param.name}}}`
break
}
case "header":
headers[param.name] = `{{${param.name}}}`
break
@ -125,7 +126,7 @@ export class OpenAPI2 extends OpenAPISource {
case "formData":
// future enhancement
break
case "body":
case "body": {
// set the request body to the example provided
// future enhancement: generate an example from the schema
let bodyParam: OpenAPIV2.InBodyParameterObject =
@ -135,6 +136,7 @@ export class OpenAPI2 extends OpenAPISource {
requestBody = schema.example
}
break
}
}
// add the parameter if it can be bound in our config

View file

@ -161,13 +161,14 @@ export class OpenAPI3 extends OpenAPISource {
for (let param of allParams) {
if (parameterNotRef(param)) {
switch (param.in) {
case "query":
case "query": {
let prefix = ""
if (queryString) {
prefix = "&"
}
queryString = `${queryString}${prefix}${param.name}={{${param.name}}}`
break
}
case "header":
headers[param.name] = `{{${param.name}}}`
break

View file

@ -14,22 +14,35 @@ import {
SessionCookie,
JsonFieldSubType,
QueryResponse,
QueryPreview,
QuerySchema,
FieldType,
ExecuteQueryRequest,
ExecuteQueryResponse,
Row,
QueryParameter,
PreviewQueryRequest,
PreviewQueryResponse,
} from "@budibase/types"
import { ValidQueryNameRegex, utils as JsonUtils } from "@budibase/shared-core"
import { findHBSBlocks } from "@budibase/string-templates"
const Runner = new Thread(ThreadType.QUERY, {
timeoutMs: env.QUERY_THREAD_TIMEOUT,
})
function validateQueryInputs(parameters: Record<string, string>) {
for (let entry of Object.entries(parameters)) {
const [key, value] = entry
if (typeof value !== "string") {
continue
}
if (findHBSBlocks(value).length !== 0) {
throw new Error(
`Parameter '${key}' input contains a handlebars binding - this is not allowed.`
)
}
}
}
export async function fetch(ctx: UserCtx) {
ctx.body = await sdk.queries.fetch()
}
@ -123,10 +136,10 @@ function getAuthConfig(ctx: UserCtx) {
function enrichParameters(
queryParameters: QueryParameter[],
requestParameters: { [key: string]: string } = {}
): {
[key: string]: string
} {
requestParameters: Record<string, string> = {}
): Record<string, string> {
// first check parameters are all valid
validateQueryInputs(requestParameters)
// make sure parameters are fully enriched with defaults
for (let parameter of queryParameters) {
if (!requestParameters[parameter.name]) {

View file

@ -116,7 +116,7 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
target: prodDb.name,
})
await replication.replicate({
filter: (doc: any, params: any) => {
filter: (doc: any) => {
return doc._id && doc._id.startsWith("role_")
},
})

View file

@ -7,13 +7,11 @@ import {
FilterType,
IncludeRelationship,
ManyToManyRelationshipFieldMetadata,
ManyToOneRelationshipFieldMetadata,
OneToManyRelationshipFieldMetadata,
Operation,
PaginationJson,
RelationshipFieldMetadata,
RelationshipsJson,
RelationshipType,
Row,
SearchFilters,
SortJson,
@ -717,7 +715,7 @@ export class ExternalRequest<T extends Operation> {
const rows = related[key]?.rows || []
function relationshipMatchPredicate({
const relationshipMatchPredicate = ({
row,
linkPrimary,
linkSecondary,
@ -725,7 +723,7 @@ export class ExternalRequest<T extends Operation> {
row: Row
linkPrimary: string
linkSecondary?: string
}) {
}) => {
const matchesPrimaryLink =
row[linkPrimary] === relationship.id ||
row[linkPrimary] === body?.[linkPrimary]

View file

@ -23,6 +23,12 @@ const DISABLED_WRITE_CLIENTS: SqlClient[] = [
SqlClient.ORACLE,
]
const DISABLED_OPERATIONS: Operation[] = [
Operation.CREATE_TABLE,
Operation.UPDATE_TABLE,
Operation.DELETE_TABLE,
]
class CharSequence {
static alphabet = "abcdefghijklmnopqrstuvwxyz"
counters: number[]
@ -59,13 +65,18 @@ export default class AliasTables {
}
isAliasingEnabled(json: QueryJson, datasource: Datasource) {
const operation = json.endpoint.operation
const fieldLength = json.resource?.fields?.length
if (!fieldLength || fieldLength <= 0) {
if (
!fieldLength ||
fieldLength <= 0 ||
DISABLED_OPERATIONS.includes(operation)
) {
return false
}
try {
const sqlClient = getSQLClient(datasource)
const isWrite = WRITE_OPERATIONS.includes(json.endpoint.operation)
const isWrite = WRITE_OPERATIONS.includes(operation)
const isDisabledClient = DISABLED_WRITE_CLIENTS.includes(sqlClient)
if (isWrite && isDisabledClient) {
return false

View file

@ -1,4 +1,3 @@
import { quotas } from "@budibase/pro"
import {
UserCtx,
ViewV2,

View file

@ -1,6 +1,6 @@
import { generateUserFlagID, InternalTables } from "../../db/utils"
import { getFullUser } from "../../utilities/users"
import { cache, context } from "@budibase/backend-core"
import { context } from "@budibase/backend-core"
import {
ContextUserMetadata,
Ctx,

View file

@ -24,7 +24,7 @@ async function parseSchema(view: CreateViewRequest) {
icon: schemaValue.icon,
}
Object.entries(fieldSchema)
.filter(([_, val]) => val === undefined)
.filter(([, val]) => val === undefined)
.forEach(([key]) => {
delete fieldSchema[key as keyof UIFieldMetadata]
})

View file

@ -33,7 +33,6 @@ export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public"
const appBackupRoutes = pro.appBackups
const scheduleRoutes = pro.schedules
const environmentVariableRoutes = pro.environmentVariables
export const mainRoutes: Router[] = [
@ -65,7 +64,6 @@ export const mainRoutes: Router[] = [
pluginRoutes,
opsRoutes,
debugRoutes,
scheduleRoutes,
environmentVariableRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this

View file

@ -81,6 +81,7 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
{
"config": {},
"createdAt": "2020-01-01T00:00:00.000Z",
"isSQL": true,
"name": "Test",
"source": "POSTGRES",
"type": "datasource",

View file

@ -16,7 +16,7 @@ describe("/applications/:appId/import", () => {
it("should be able to perform import", async () => {
const appId = config.getAppId()
const res = await request
await request
.post(`/api/applications/${appId}/import`)
.field("encryptionPassword", PASSWORD)
.attach("appExport", path.join(__dirname, "assets", "export.tar.gz"))
@ -25,8 +25,8 @@ describe("/applications/:appId/import", () => {
.expect(200)
const appPackage = await config.api.application.get(appId!)
expect(appPackage.navigation?.links?.length).toBe(2)
expect(expect(appPackage.navigation?.links?.[0].url).toBe("/blank"))
expect(expect(appPackage.navigation?.links?.[1].url).toBe("/derp"))
expect(appPackage.navigation?.links?.[0].url).toBe("/blank")
expect(appPackage.navigation?.links?.[1].url).toBe("/derp")
const screens = await config.api.screen.list()
expect(screens.length).toBe(2)
expect(screens[0].routing.route).toBe("/derp")

View file

@ -2,7 +2,6 @@ import * as setup from "./utilities"
import { roles, db as dbCore } from "@budibase/backend-core"
describe("/api/applications/:appId/sync", () => {
let request = setup.getRequest()
let config = setup.getConfig()
let app

View file

@ -19,6 +19,7 @@ import env from "../../../environment"
import { type App } from "@budibase/types"
import tk from "timekeeper"
import * as uuid from "uuid"
import { structures } from "@budibase/backend-core/tests"
describe("/applications", () => {
let config = setup.getConfig()
@ -30,7 +31,9 @@ describe("/applications", () => {
beforeEach(async () => {
app = await config.api.application.create({ name: utils.newid() })
const deployment = await config.api.application.publish(app.appId)
expect(deployment.status).toBe("SUCCESS")
if (deployment.status !== "SUCCESS") {
throw new Error("Failed to publish app")
}
jest.clearAllMocks()
})
@ -128,7 +131,7 @@ describe("/applications", () => {
it("creates empty app", async () => {
const app = await config.api.application.create({ name: utils.newid() })
expect(app._id).toBeDefined()
expect(events.app.created).toBeCalledTimes(1)
expect(events.app.created).toHaveBeenCalledTimes(1)
})
it("creates app from template", async () => {
@ -139,8 +142,8 @@ describe("/applications", () => {
templateString: "{}",
})
expect(app._id).toBeDefined()
expect(events.app.created).toBeCalledTimes(1)
expect(events.app.templateImported).toBeCalledTimes(1)
expect(events.app.created).toHaveBeenCalledTimes(1)
expect(events.app.templateImported).toHaveBeenCalledTimes(1)
})
it("creates app from file", async () => {
@ -150,8 +153,8 @@ describe("/applications", () => {
templateFile: "src/api/routes/tests/data/export.txt",
})
expect(app._id).toBeDefined()
expect(events.app.created).toBeCalledTimes(1)
expect(events.app.fileImported).toBeCalledTimes(1)
expect(events.app.created).toHaveBeenCalledTimes(1)
expect(events.app.fileImported).toHaveBeenCalledTimes(1)
})
it("should apply authorization to endpoint", async () => {
@ -181,8 +184,8 @@ describe("/applications", () => {
expect(app.navigation!.navTextColor).toBe(
"var(--spectrum-global-color-gray-50)"
)
expect(events.app.created).toBeCalledTimes(1)
expect(events.app.fileImported).toBeCalledTimes(1)
expect(events.app.created).toHaveBeenCalledTimes(1)
expect(events.app.fileImported).toHaveBeenCalledTimes(1)
})
it("should reject with a known name", async () => {
@ -228,32 +231,32 @@ describe("/applications", () => {
name: "TEST_APP",
})
expect(updatedApp._rev).toBeDefined()
expect(events.app.updated).toBeCalledTimes(1)
expect(events.app.updated).toHaveBeenCalledTimes(1)
})
})
describe("publish", () => {
it("should publish app with dev app ID", async () => {
await config.api.application.publish(app.appId)
expect(events.app.published).toBeCalledTimes(1)
expect(events.app.published).toHaveBeenCalledTimes(1)
})
it("should publish app with prod app ID", async () => {
await config.api.application.publish(app.appId.replace("_dev", ""))
expect(events.app.published).toBeCalledTimes(1)
expect(events.app.published).toHaveBeenCalledTimes(1)
})
})
describe("manage client library version", () => {
it("should be able to update the app client library version", async () => {
await config.api.application.updateClient(app.appId)
expect(events.app.versionUpdated).toBeCalledTimes(1)
expect(events.app.versionUpdated).toHaveBeenCalledTimes(1)
})
it("should be able to revert the app client library version", async () => {
await config.api.application.updateClient(app.appId)
await config.api.application.revertClient(app.appId)
expect(events.app.versionReverted).toBeCalledTimes(1)
expect(events.app.versionReverted).toHaveBeenCalledTimes(1)
})
})
@ -310,26 +313,26 @@ describe("/applications", () => {
describe("unpublish", () => {
it("should unpublish app with dev app ID", async () => {
await config.api.application.unpublish(app.appId)
expect(events.app.unpublished).toBeCalledTimes(1)
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
})
it("should unpublish app with prod app ID", async () => {
await config.api.application.unpublish(app.appId.replace("_dev", ""))
expect(events.app.unpublished).toBeCalledTimes(1)
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
})
})
describe("delete", () => {
it("should delete published app and dev apps with dev app ID", async () => {
await config.api.application.delete(app.appId)
expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1)
expect(events.app.deleted).toHaveBeenCalledTimes(1)
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
})
it("should delete published app and dev app with prod app ID", async () => {
await config.api.application.delete(app.appId.replace("_dev", ""))
expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1)
expect(events.app.deleted).toHaveBeenCalledTimes(1)
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
})
})
@ -346,7 +349,7 @@ describe("/applications", () => {
}
)
expect(events.app.duplicated).toBeCalled()
expect(events.app.duplicated).toHaveBeenCalled()
expect(resp.duplicateAppId).toBeDefined()
expect(resp.sourceAppId).toEqual(app.appId)
expect(resp.duplicateAppId).not.toEqual(app.appId)
@ -354,7 +357,7 @@ describe("/applications", () => {
it("should reject an unknown app id with a 404", async () => {
await config.api.application.duplicateApp(
app.appId.slice(0, -1) + "a",
structures.db.id(),
{
name: "to-dupe 123",
url: "/to-dupe-123",
@ -366,7 +369,7 @@ describe("/applications", () => {
})
it("should reject with a known name", async () => {
const resp = await config.api.application.duplicateApp(
await config.api.application.duplicateApp(
app.appId,
{
name: app.name,
@ -374,11 +377,11 @@ describe("/applications", () => {
},
{ body: { message: "App name is already in use." }, status: 400 }
)
expect(events.app.duplicated).not.toBeCalled()
expect(events.app.duplicated).not.toHaveBeenCalled()
})
it("should reject with a known url", async () => {
const resp = await config.api.application.duplicateApp(
await config.api.application.duplicateApp(
app.appId,
{
name: "this is fine",
@ -386,7 +389,7 @@ describe("/applications", () => {
},
{ body: { message: "App URL is already in use." }, status: 400 }
)
expect(events.app.duplicated).not.toBeCalled()
expect(events.app.duplicated).not.toHaveBeenCalled()
})
})

View file

@ -95,8 +95,8 @@ describe("/automations", () => {
expect(res.body.message).toEqual("Automation created successfully")
expect(res.body.automation.name).toEqual("My Automation")
expect(res.body.automation._id).not.toEqual(null)
expect(events.automation.created).toBeCalledTimes(1)
expect(events.automation.stepCreated).not.toBeCalled()
expect(events.automation.created).toHaveBeenCalledTimes(1)
expect(events.automation.stepCreated).not.toHaveBeenCalled()
})
it("creates an automation with steps", async () => {
@ -114,8 +114,8 @@ describe("/automations", () => {
expect(res.body.message).toEqual("Automation created successfully")
expect(res.body.automation.name).toEqual("My Automation")
expect(res.body.automation._id).not.toEqual(null)
expect(events.automation.created).toBeCalledTimes(1)
expect(events.automation.stepCreated).toBeCalledTimes(2)
expect(events.automation.created).toHaveBeenCalledTimes(1)
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
})
it("should apply authorization to endpoint", async () => {
@ -158,7 +158,7 @@ describe("/automations", () => {
automation = await config.createAutomation(automation)
await setup.delay(500)
const res = await testAutomation(config, automation)
expect(events.automation.tested).toBeCalledTimes(1)
expect(events.automation.tested).toHaveBeenCalledTimes(1)
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to
// know that it has finished all of its actions - this is currently the best way
// also when this runs in CI it is very temper-mental so for now trying to make run stable by repeating until it works
@ -265,10 +265,10 @@ describe("/automations", () => {
`Automation ${automation._id} updated successfully.`
)
// events
expect(events.automation.created).not.toBeCalled()
expect(events.automation.stepCreated).not.toBeCalled()
expect(events.automation.stepDeleted).not.toBeCalled()
expect(events.automation.triggerUpdated).not.toBeCalled()
expect(events.automation.created).not.toHaveBeenCalled()
expect(events.automation.stepCreated).not.toHaveBeenCalled()
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
expect(events.automation.triggerUpdated).not.toHaveBeenCalled()
})
it("updates a automations name using POST request", async () => {
@ -293,10 +293,10 @@ describe("/automations", () => {
`Automation ${automation._id} updated successfully.`
)
// events
expect(events.automation.created).not.toBeCalled()
expect(events.automation.stepCreated).not.toBeCalled()
expect(events.automation.stepDeleted).not.toBeCalled()
expect(events.automation.triggerUpdated).not.toBeCalled()
expect(events.automation.created).not.toHaveBeenCalled()
expect(events.automation.stepCreated).not.toHaveBeenCalled()
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
expect(events.automation.triggerUpdated).not.toHaveBeenCalled()
})
it("updates an automation trigger", async () => {
@ -310,10 +310,10 @@ describe("/automations", () => {
await update(automation)
// events
expect(events.automation.created).not.toBeCalled()
expect(events.automation.stepCreated).not.toBeCalled()
expect(events.automation.stepDeleted).not.toBeCalled()
expect(events.automation.triggerUpdated).toBeCalledTimes(1)
expect(events.automation.created).not.toHaveBeenCalled()
expect(events.automation.stepCreated).not.toHaveBeenCalled()
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
expect(events.automation.triggerUpdated).toHaveBeenCalledTimes(1)
})
it("adds automation steps", async () => {
@ -327,10 +327,10 @@ describe("/automations", () => {
await update(automation)
// events
expect(events.automation.stepCreated).toBeCalledTimes(2)
expect(events.automation.created).not.toBeCalled()
expect(events.automation.stepDeleted).not.toBeCalled()
expect(events.automation.triggerUpdated).not.toBeCalled()
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
expect(events.automation.created).not.toHaveBeenCalled()
expect(events.automation.stepDeleted).not.toHaveBeenCalled()
expect(events.automation.triggerUpdated).not.toHaveBeenCalled()
})
it("removes automation steps", async () => {
@ -344,10 +344,10 @@ describe("/automations", () => {
await update(automation)
// events
expect(events.automation.stepDeleted).toBeCalledTimes(2)
expect(events.automation.stepCreated).not.toBeCalled()
expect(events.automation.created).not.toBeCalled()
expect(events.automation.triggerUpdated).not.toBeCalled()
expect(events.automation.stepDeleted).toHaveBeenCalledTimes(2)
expect(events.automation.stepCreated).not.toHaveBeenCalled()
expect(events.automation.created).not.toHaveBeenCalled()
expect(events.automation.triggerUpdated).not.toHaveBeenCalled()
})
it("adds and removes automation steps", async () => {
@ -360,10 +360,10 @@ describe("/automations", () => {
await update(automation)
// events
expect(events.automation.stepCreated).toBeCalledTimes(2)
expect(events.automation.stepDeleted).toBeCalledTimes(1)
expect(events.automation.created).not.toBeCalled()
expect(events.automation.triggerUpdated).not.toBeCalled()
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
expect(events.automation.stepDeleted).toHaveBeenCalledTimes(1)
expect(events.automation.created).not.toHaveBeenCalled()
expect(events.automation.triggerUpdated).not.toHaveBeenCalled()
})
})
@ -400,7 +400,7 @@ describe("/automations", () => {
.expect(200)
expect(res.body.id).toEqual(automation._id)
expect(events.automation.deleted).toBeCalledTimes(1)
expect(events.automation.deleted).toHaveBeenCalledTimes(1)
})
it("should apply authorization to endpoint", async () => {

View file

@ -21,7 +21,7 @@ describe("/backups", () => {
it("should be able to export app", async () => {
const body = await config.api.backup.exportBasicBackup(config.getAppId()!)
expect(body instanceof Buffer).toBe(true)
expect(events.app.exported).toBeCalledTimes(1)
expect(events.app.exported).toHaveBeenCalledTimes(1)
})
it("should apply authorization to endpoint", async () => {

View file

@ -40,7 +40,7 @@ describe("/datasources", () => {
expect(res.body.datasource.name).toEqual("Test")
expect(res.body.errors).toEqual({})
expect(events.datasource.created).toBeCalledTimes(1)
expect(events.datasource.created).toHaveBeenCalledTimes(1)
})
})
@ -56,7 +56,7 @@ describe("/datasources", () => {
expect(res.body.datasource.name).toEqual("Updated Test")
expect(res.body.errors).toBeUndefined()
expect(events.datasource.updated).toBeCalledTimes(1)
expect(events.datasource.updated).toHaveBeenCalledTimes(1)
})
describe("dynamic variables", () => {
@ -196,7 +196,7 @@ describe("/datasources", () => {
.expect(200)
expect(res.body.length).toEqual(1)
expect(events.datasource.deleted).toBeCalledTimes(1)
expect(events.datasource.deleted).toHaveBeenCalledTimes(1)
})
it("should apply authorization to endpoint", async () => {

Some files were not shown because too many files have changed in this diff Show more