Merge branch 'master' of github.com:budibase/budibase into reorganise-row-tests-3
This commit is contained in:
commit
07c6dcc0c0
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.22.0",
|
||||
"version": "2.22.7",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,7 +142,8 @@ 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" } }
|
||||
|
@ -150,6 +151,7 @@ export async function refreshOAuthToken(
|
|||
return refreshGoogleAccessToken(googleConfig, refreshToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Refactor to use user save function instead to prevent the need for
|
||||
// manually saving and invalidating on callback
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,7 +12,12 @@ interface ProcessDocMessage {
|
|||
const PERSIST_MAX_ATTEMPTS = 100
|
||||
let processor: DocWritethroughProcessor | undefined
|
||||
|
||||
export const docWritethroughProcessorQueue = createQueue<ProcessDocMessage>(
|
||||
export class DocWritethroughProcessor {
|
||||
private static _queue: Queue
|
||||
|
||||
public static get queue() {
|
||||
if (!DocWritethroughProcessor._queue) {
|
||||
DocWritethroughProcessor._queue = createQueue<ProcessDocMessage>(
|
||||
JobQueue.DOC_WRITETHROUGH_QUEUE,
|
||||
{
|
||||
jobOptions: {
|
||||
|
@ -20,10 +25,13 @@ export const docWritethroughProcessorQueue = createQueue<ProcessDocMessage>(
|
|||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }))
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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".`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -197,7 +197,9 @@
|
|||
>
|
||||
<Icon name="ChevronRight" />
|
||||
</div>
|
||||
{#if maximum !== 1}
|
||||
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if value?.length}
|
||||
{#each value as 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
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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`)) {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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)})`
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -11,6 +11,7 @@ export const INITIAL_NAVIGATION_STATE = {
|
|||
hideLogo: null,
|
||||
logoUrl: null,
|
||||
hideTitle: null,
|
||||
textAlign: "Left",
|
||||
navBackground: null,
|
||||
navWidth: null,
|
||||
navTextColor: null,
|
||||
|
|
|
@ -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
|
|
@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/**/*"],
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module FirebaseMock {
|
||||
const firebase: any = {}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module SendgridMock {
|
||||
class Email {
|
||||
constructor() {
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
module AirtableMock {
|
||||
function Airtable() {
|
||||
// @ts-ignore
|
||||
this.base = jest.fn()
|
||||
class Airtable {
|
||||
base = jest.fn()
|
||||
}
|
||||
|
||||
module.exports = Airtable
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module ArangoMock {
|
||||
const arangodb: any = {}
|
||||
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
import fs from "fs"
|
||||
import { join } from "path"
|
||||
|
||||
module AwsMock {
|
||||
const aws: any = {}
|
||||
|
||||
const response = (body: any, extra?: any) => () => ({
|
||||
promise: () => body,
|
||||
...extra,
|
||||
})
|
||||
|
||||
function DocumentClient() {
|
||||
// @ts-ignore
|
||||
this.put = jest.fn(response({}))
|
||||
// @ts-ignore
|
||||
this.query = jest.fn(
|
||||
class DocumentClient {
|
||||
put = jest.fn(response({}))
|
||||
query = jest.fn(
|
||||
response({
|
||||
Items: [],
|
||||
})
|
||||
)
|
||||
// @ts-ignore
|
||||
this.scan = jest.fn(
|
||||
scan = jest.fn(
|
||||
response({
|
||||
Items: [
|
||||
{
|
||||
|
@ -28,57 +22,41 @@ module AwsMock {
|
|||
],
|
||||
})
|
||||
)
|
||||
// @ts-ignore
|
||||
this.get = jest.fn(response({}))
|
||||
// @ts-ignore
|
||||
this.update = jest.fn(response({}))
|
||||
// @ts-ignore
|
||||
this.delete = jest.fn(response({}))
|
||||
get = jest.fn(response({}))
|
||||
update = jest.fn(response({}))
|
||||
delete = jest.fn(response({}))
|
||||
}
|
||||
|
||||
function S3() {
|
||||
// @ts-ignore
|
||||
this.listObjects = jest.fn(
|
||||
class S3 {
|
||||
listObjects = jest.fn(
|
||||
response({
|
||||
Contents: [],
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
this.createBucket = jest.fn(
|
||||
createBucket = jest.fn(
|
||||
response({
|
||||
Contents: {},
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
this.deleteObjects = jest.fn(
|
||||
deleteObjects = jest.fn(
|
||||
response({
|
||||
Contents: {},
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
this.getSignedUrl = (operation, params) => {
|
||||
getSignedUrl = jest.fn((operation, params) => {
|
||||
return `http://example.com/${params.Bucket}/${params.Key}`
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
this.headBucket = jest.fn(
|
||||
})
|
||||
headBucket = jest.fn(
|
||||
response({
|
||||
Contents: {},
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
this.upload = jest.fn(
|
||||
upload = jest.fn(
|
||||
response({
|
||||
Contents: {},
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
this.getObject = jest.fn(
|
||||
getObject = jest.fn(
|
||||
response(
|
||||
{
|
||||
Body: "",
|
||||
|
@ -86,17 +64,18 @@ module AwsMock {
|
|||
{
|
||||
createReadStream: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
fs.createReadStream(join(__dirname, "aws-sdk.ts"))
|
||||
),
|
||||
.mockReturnValue(fs.createReadStream(join(__dirname, "aws-sdk.ts"))),
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
aws.DynamoDB = { DocumentClient }
|
||||
aws.S3 = S3
|
||||
aws.config = { update: jest.fn() }
|
||||
|
||||
module.exports = aws
|
||||
module.exports = {
|
||||
DynamoDB: {
|
||||
DocumentClient,
|
||||
},
|
||||
S3,
|
||||
config: {
|
||||
update: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module MongoMock {
|
||||
const mongodb: any = {}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
module MySQLMock {
|
||||
const mysql: any = {}
|
||||
|
||||
const client = {
|
||||
connect: jest.fn(),
|
||||
query: jest.fn((query, bindings, fn) => {
|
||||
|
@ -8,7 +5,7 @@ module MySQLMock {
|
|||
}),
|
||||
}
|
||||
|
||||
mysql.createConnection = jest.fn(() => client)
|
||||
|
||||
module.exports = mysql
|
||||
module.exports = {
|
||||
createConnection: jest.fn(() => client),
|
||||
client,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module MySQLMock {
|
||||
const mysql: any = {}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
module OracleDbMock {
|
||||
// mock execute
|
||||
const execute = jest.fn(() => ({
|
||||
const executeMock = jest.fn(() => ({
|
||||
rows: [
|
||||
{
|
||||
a: "string",
|
||||
|
@ -9,23 +7,15 @@ module OracleDbMock {
|
|||
],
|
||||
}))
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
module PgMock {
|
||||
const pg: any = {}
|
||||
|
||||
const query = jest.fn(() => ({
|
||||
rows: [
|
||||
{
|
||||
|
@ -10,21 +7,19 @@ module PgMock {
|
|||
],
|
||||
}))
|
||||
|
||||
// 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()
|
||||
connect = jest.fn()
|
||||
release = jest.fn()
|
||||
}
|
||||
|
||||
const on = jest.fn()
|
||||
|
||||
pg.Client = Client
|
||||
pg.queryMock = query
|
||||
pg.on = on
|
||||
|
||||
module.exports = pg
|
||||
module.exports = {
|
||||
Client,
|
||||
queryMock: query,
|
||||
on,
|
||||
}
|
||||
|
|
|
@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -175,6 +175,10 @@
|
|||
]
|
||||
},
|
||||
"build": {
|
||||
"inputs": [
|
||||
"{projectRoot}/builder",
|
||||
"{projectRoot}/client"
|
||||
],
|
||||
"outputs": [
|
||||
"{projectRoot}/builder",
|
||||
"{projectRoot}/client",
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
env as envCore,
|
||||
ErrorCode,
|
||||
events,
|
||||
HTTPError,
|
||||
migrations,
|
||||
objectStore,
|
||||
roles,
|
||||
|
|
|
@ -39,19 +39,21 @@ 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)
|
||||
|
@ -59,6 +61,7 @@ export async function create(ctx: any) {
|
|||
directory = directoryUrl
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pluginCore.validate(metadata?.schema)
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
@ -136,6 +137,7 @@ export class OpenAPI2 extends OpenAPISource {
|
|||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// add the parameter if it can be bound in our config
|
||||
if (["query", "header", "path"].includes(param.in)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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_")
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { quotas } from "@budibase/pro"
|
||||
import {
|
||||
UserCtx,
|
||||
ViewV2,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue