diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index b3385c2ccd..b37ff9cee8 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -68,16 +68,28 @@ jobs: ] env: KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' - - - name: Set the base64 kubeconfig - run: echo 'RELEASE_KUBECONFIG=${{ secrets.RELEASE_KUBECONFIG }}' | base64 - - name: Re roll the services + - name: Re roll app-service uses: actions-hub/kubectl@master env: - KUBE_CONFIG: ${{ env.RELEASE_KUBECONFIG }} + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} with: - args: rollout restart deployment proxy-service -n budibase && kubectl rollout restart deployment app-service -n budibase && kubectl rollout restart deployment worker-service -n budibase + args: rollout restart deployment app-service -n budibase + + - name: Re roll proxy-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment proxy-service -n budibase + + - name: Re roll worker-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment worker-service -n budibase + - name: Discord Webhook Action uses: tsickert/discord-webhook@v4.0.0 diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 8d3e9f4021..57e65c734e 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -121,15 +121,26 @@ jobs: env: KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' - - name: Set the base64 kubeconfig - run: echo 'RELEASE_KUBECONFIG=${{ secrets.RELEASE_KUBECONFIG }}' | base64 - - - name: Re roll the services + - name: Re roll app-service uses: actions-hub/kubectl@master env: - KUBE_CONFIG: ${{ env.RELEASE_KUBECONFIG }} + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} with: - args: rollout restart deployment proxy-service -n budibase && kubectl rollout restart deployment app-service -n budibase && kubectl rollout restart deployment worker-service -n budibase + args: rollout restart deployment app-service -n budibase + + - name: Re roll proxy-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment proxy-service -n budibase + + - name: Re roll worker-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment worker-service -n budibase - name: Discord Webhook Action uses: tsickert/discord-webhook@v4.0.0 diff --git a/.github/workflows/smoke_test.yaml b/.github/workflows/smoke_test.yaml index 7002c8335b..cffb914aaf 100644 --- a/.github/workflows/smoke_test.yaml +++ b/.github/workflows/smoke_test.yaml @@ -1,4 +1,4 @@ -name: Budibase Smoke Test +name: Budibase Nightly Tests on: workflow_dispatch: @@ -6,7 +6,7 @@ on: - cron: "0 5 * * *" # every day at 5AM jobs: - release: + nightly: runs-on: ubuntu-latest steps: @@ -43,6 +43,18 @@ jobs: name: Test Reports path: packages/builder/cypress/reports/testReport.html + # TODO: enable once running in QA test env + # - name: Configure AWS Credentials + # uses: aws-actions/configure-aws-credentials@v1 + # with: + # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # aws-region: eu-west-1 + + # - name: Upload test results HTML + # uses: aws-actions/configure-aws-credentials@v1 + # run: aws s3 cp packages/builder/cypress/reports/testReport.html s3://{{ secrets.BUDI_QA_REPORTS_BUCKET_NAME }}/$GITHUB_RUN_ID/index.html + - name: Cypress Discord Notify run: yarn test:e2e:ci:notify env: diff --git a/.prettierrc.json b/.prettierrc.json index 39654fd9f9..dae5906124 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,7 +4,7 @@ "singleQuote": false, "trailingComma": "es5", "arrowParens": "avoid", - "jsxBracketSameLine": false, + "bracketSameLine": false, "plugins": ["prettier-plugin-svelte"], "svelteSortOrder": "options-scripts-markup-styles" } diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index fd46e77647..74b98ac008 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -130,6 +130,22 @@ spec: - name: BB_ADMIN_USER_PASSWORD value: { { .Values.globals.bbAdminUserPassword | quote } } {{ end }} + {{ if .Values.services.apps.nodeDebug }} + - name: NODE_DEBUG + value: {{ .Values.services.apps.nodeDebug | quote }} + {{ end }} + {{ if .Values.globals.elasticApmEnabled }} + - name: ELASTIC_APM_ENABLED + value: {{ .Values.globals.elasticApmEnabled | quote }} + {{ end }} + {{ if .Values.globals.elasticApmSecretToken }} + - name: ELASTIC_APM_SECRET_TOKEN + value: {{ .Values.globals.elasticApmSecretToken | quote }} + {{ end }} + {{ if .Values.globals.elasticApmServerUrl }} + - name: ELASTIC_APM_SERVER_URL + value: {{ .Values.globals.elasticApmServerUrl | quote }} + {{ end }} image: budibase/apps:{{ .Values.globals.appVersion }} imagePullPolicy: Always diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index 918dab427b..083231eeff 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -27,6 +27,8 @@ spec: spec: containers: - env: + - name: BUDIBASE_ENVIRONMENT + value: {{ .Values.globals.budibaseEnv }} - name: DEPLOYMENT_ENVIRONMENT value: "kubernetes" - name: CLUSTER_PORT @@ -125,6 +127,19 @@ spec: value: {{ .Values.globals.google.secret | quote }} - name: TENANT_FEATURE_FLAGS value: {{ .Values.globals.tenantFeatureFlags | quote }} + {{ if .Values.globals.elasticApmEnabled }} + - name: ELASTIC_APM_ENABLED + value: {{ .Values.globals.elasticApmEnabled | quote }} + {{ end }} + {{ if .Values.globals.elasticApmSecretToken }} + - name: ELASTIC_APM_SECRET_TOKEN + value: {{ .Values.globals.elasticApmSecretToken | quote }} + {{ end }} + {{ if .Values.globals.elasticApmServerUrl }} + - name: ELASTIC_APM_SERVER_URL + value: {{ .Values.globals.elasticApmServerUrl | quote }} + {{ end }} + image: budibase/worker:{{ .Values.globals.appVersion }} imagePullPolicy: Always livenessProbe: diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 404e92c70f..9b5e76d0d7 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -114,6 +114,10 @@ globals: smtp: enabled: false +# elasticApmEnabled: +# elasticApmSecretToken: +# elasticApmServerUrl: + services: budibaseVersion: latest dns: cluster.local @@ -126,6 +130,7 @@ services: port: 4002 replicaCount: 1 logLevel: info +# nodeDebug: "" # set the value of NODE_DEBUG worker: port: 4003 diff --git a/hosting/nginx.dev.conf.hbs b/hosting/nginx.dev.conf.hbs index 9398b7e719..20c4d3d182 100644 --- a/hosting/nginx.dev.conf.hbs +++ b/hosting/nginx.dev.conf.hbs @@ -15,7 +15,10 @@ http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" ' + 'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr'; + + access_log /var/log/nginx/access.log main; map $http_upgrade $connection_upgrade { default "upgrade"; @@ -62,6 +65,10 @@ http { proxy_pass http://{{ address }}:4001; } + location /preview { + proxy_pass http://{{ address }}:4001; + } + location /builder { proxy_pass http://{{ address }}:3000; rewrite ^/builder(.*)$ /builder/$1 break; diff --git a/hosting/nginx.prod.conf.hbs b/hosting/nginx.prod.conf.hbs index 4213626309..5ecea67c42 100644 --- a/hosting/nginx.prod.conf.hbs +++ b/hosting/nginx.prod.conf.hbs @@ -33,7 +33,10 @@ http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" ' + 'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr'; + + access_log /var/log/nginx/access.log main; map $http_upgrade $connection_upgrade { default "upgrade"; @@ -85,6 +88,10 @@ http { proxy_pass http://$apps:4002; } + location /preview { + proxy_pass http://$apps:4002; + } + location = / { proxy_pass http://$apps:4002; } @@ -94,6 +101,7 @@ http { proxy_pass http://$watchtower:8080; } {{/if}} + location ~ ^/(builder|app_) { proxy_http_version 1.1; proxy_set_header Connection $connection_upgrade; diff --git a/lerna.json b/lerna.json index ba26b31fde..d726cbab9c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.2.41-alpha.5", + "version": "1.2.58-alpha.5", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 924fa4123c..e02ecc2c0c 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.2.41-alpha.5", + "version": "1.2.58-alpha.5", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "1.2.41-alpha.5", + "@budibase/types": "1.2.58-alpha.5", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.ts similarity index 73% rename from packages/backend-core/src/auth.js rename to packages/backend-core/src/auth.ts index d39b8426fb..23873b84e7 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.ts @@ -1,11 +1,11 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -const { getGlobalDB } = require("./tenancy") +import { getGlobalDB } from "./tenancy" const refresh = require("passport-oauth2-refresh") -const { Configs } = require("./constants") -const { getScopedConfig } = require("./db/utils") -const { +import { Configs } from "./constants" +import { getScopedConfig } from "./db/utils" +import { jwt, local, authenticated, @@ -13,7 +13,6 @@ const { oidc, auditLog, tenancy, - appTenancy, authError, ssoCallbackUrl, csrf, @@ -22,32 +21,36 @@ const { builderOnly, builderOrAdmin, joiValidator, -} = require("./middleware") - -const { invalidateUser } = require("./cache/user") +} from "./middleware" +import { invalidateUser } from "./cache/user" +import { User } from "@budibase/types" // Strategies passport.use(new LocalStrategy(local.options, local.authenticate)) passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) -passport.serializeUser((user, done) => done(null, user)) +passport.serializeUser((user: User, done: any) => done(null, user)) -passport.deserializeUser(async (user, done) => { +passport.deserializeUser(async (user: User, done: any) => { const db = getGlobalDB() try { - const user = await db.get(user._id) - return done(null, user) + const dbUser = await db.get(user._id) + return done(null, dbUser) } catch (err) { console.error(`User not found`, err) return done(null, false, { message: "User not found" }) } }) -async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) { +async function refreshOIDCAccessToken( + db: any, + chosenConfig: any, + refreshToken: string +) { const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) - let enrichedConfig - let strategy + let enrichedConfig: any + let strategy: any try { enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl) @@ -70,22 +73,28 @@ async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) { refresh.requestNewAccessToken( Configs.OIDC, refreshToken, - (err, accessToken, refreshToken, params) => { + (err: any, accessToken: string, refreshToken: any, params: any) => { resolve({ err, accessToken, refreshToken, params }) } ) }) } -async function refreshGoogleAccessToken(db, config, refreshToken) { +async function refreshGoogleAccessToken( + db: any, + config: any, + refreshToken: any +) { let callbackUrl = await google.getCallbackUrl(db, config) let strategy try { strategy = await google.strategyFactory(config, callbackUrl) - } catch (err) { + } catch (err: any) { console.error(err) - throw new Error("Error constructing OIDC refresh strategy", err) + throw new Error( + `Error constructing OIDC refresh strategy: message=${err.message}` + ) } refresh.use(strategy) @@ -94,14 +103,18 @@ async function refreshGoogleAccessToken(db, config, refreshToken) { refresh.requestNewAccessToken( Configs.GOOGLE, refreshToken, - (err, accessToken, refreshToken, params) => { + (err: any, accessToken: string, refreshToken: string, params: any) => { resolve({ err, accessToken, refreshToken, params }) } ) }) } -async function refreshOAuthToken(refreshToken, configType, configId) { +async function refreshOAuthToken( + refreshToken: string, + configType: string, + configId: string +) { const db = getGlobalDB() const config = await getScopedConfig(db, { @@ -113,7 +126,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) { let refreshResponse if (configType === Configs.OIDC) { // configId - retrieved from cookie. - chosenConfig = config.configs.filter(c => c.uuid === configId)[0] + chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] if (!chosenConfig) { throw new Error("Invalid OIDC configuration") } @@ -134,7 +147,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) { return refreshResponse } -async function updateUserOAuth(userId, oAuthConfig) { +async function updateUserOAuth(userId: string, oAuthConfig: any) { const details = { accessToken: oAuthConfig.accessToken, refreshToken: oAuthConfig.refreshToken, @@ -162,14 +175,13 @@ async function updateUserOAuth(userId, oAuthConfig) { } } -module.exports = { +export = { buildAuthMiddleware: authenticated, passport, google, oidc, jwt: require("jsonwebtoken"), buildTenancyMiddleware: tenancy, - buildAppTenancyMiddleware: appTenancy, auditLog, authError, buildCsrfMiddleware: csrf, diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts index b3bf3c7683..fd464ba5fb 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/db/constants.ts @@ -18,6 +18,7 @@ export enum ViewName { LINK = "by_link", ROUTING = "screen_routes", AUTOMATION_LOGS = "automation_logs", + ACCOUNT_BY_EMAIL = "account_by_email", } export const DeprecatedViews = { @@ -27,12 +28,6 @@ export const DeprecatedViews = { ], } -export type GlobalViewName = - | ViewName.USER_BY_EMAIL - | ViewName.BY_API_KEY - | ViewName.USER_BY_BUILDERS - | ViewName.USER_BY_APP - export enum DocumentType { USER = "us", GROUP = "gr", @@ -47,6 +42,7 @@ export enum DocumentType { MIGRATIONS = "migrations", DEV_INFO = "devinfo", AUTOMATION_LOG = "log_au", + ACCOUNT_METADATA = "acc_metadata", } export const StaticDatabases = { diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index d3ff0f66e8..23ea1fff34 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -1,13 +1,8 @@ -import { - DocumentType, - ViewName, - GlobalViewName, - DeprecatedViews, - SEPARATOR, -} from "./utils" +import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils" import { getGlobalDB } from "../tenancy" import PouchDB from "pouchdb" -import { Document } from "@budibase/types" +import { StaticDatabases } from "./constants" +import { doWithDB } from "./" const DESIGN_DB = "_design/database" @@ -65,11 +60,39 @@ export const createNewUserEmailView = async () => { await db.put(designDoc) } +export const createAccountEmailView = async () => { + await doWithDB( + StaticDatabases.PLATFORM_INFO.name, + async (db: PouchDB.Database) => { + let designDoc + try { + designDoc = await db.get(DESIGN_DB) + } catch (err) { + // no design doc, make one + designDoc = DesignDoc() + } + const view = { + // if using variables in a map function need to inject them before use + map: `function(doc) { + if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }`, + } + designDoc.views = { + ...designDoc.views, + [ViewName.ACCOUNT_BY_EMAIL]: view, + } + await db.put(designDoc) + } + ) +} + export const createUserAppView = async () => { - const db = getGlobalDB() + const db = getGlobalDB() as PouchDB.Database let designDoc try { - designDoc = await db.get("_design/database") + designDoc = await db.get("_design/database") } catch (err) { // no design doc, make one designDoc = DesignDoc() @@ -137,35 +160,73 @@ export const createUserBuildersView = async () => { await db.put(designDoc) } -export const queryGlobalView = async ( - viewName: GlobalViewName, - params: PouchDB.Query.Options, - db?: PouchDB.Database -): Promise => { - const CreateFuncByName = { - [ViewName.USER_BY_EMAIL]: createNewUserEmailView, - [ViewName.BY_API_KEY]: createApiKeyView, - [ViewName.USER_BY_BUILDERS]: createUserBuildersView, - [ViewName.USER_BY_APP]: createUserAppView, - } - // can pass DB in if working with something specific - if (!db) { - db = getGlobalDB() as PouchDB.Database - } +export interface QueryViewOptions { + arrayResponse?: boolean +} +export const queryView = async ( + viewName: ViewName, + params: PouchDB.Query.Options, + db: PouchDB.Database, + CreateFuncByName: any, + opts?: QueryViewOptions +): Promise => { try { - const response = await db.query(`database/${viewName}`, params) + let response = await db.query(`database/${viewName}`, params) const rows = response.rows const docs = rows.map(row => (params.include_docs ? row.doc : row.value)) - return docs.length <= 1 ? docs[0] : docs + + // if arrayResponse has been requested, always return array regardless of length + if (opts?.arrayResponse) { + return docs + } else { + // return the single document if there is only one + return docs.length <= 1 ? docs[0] : docs + } } catch (err: any) { if (err != null && err.name === "not_found") { const createFunc = CreateFuncByName[viewName] await removeDeprecated(db, viewName) await createFunc() - return queryGlobalView(viewName, params) + return queryView(viewName, params, db, CreateFuncByName, opts) } else { throw err } } } + +export const queryPlatformView = async ( + viewName: ViewName, + params: PouchDB.Query.Options, + opts?: QueryViewOptions +): Promise => { + const CreateFuncByName = { + [ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView, + } + + return doWithDB( + StaticDatabases.PLATFORM_INFO.name, + async (db: PouchDB.Database) => { + return queryView(viewName, params, db, CreateFuncByName, opts) + } + ) +} + +export const queryGlobalView = async ( + viewName: ViewName, + params: PouchDB.Query.Options, + db?: PouchDB.Database, + opts?: QueryViewOptions +): Promise => { + const CreateFuncByName = { + [ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView, + [ViewName.BY_API_KEY]: exports.createApiKeyView, + [ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView, + [ViewName.USER_BY_APP]: exports.createUserAppView, + } + // can pass DB in if working with something specific + if (!db) { + db = getGlobalDB() as PouchDB.Database + } + return queryView(viewName, params, db, CreateFuncByName, opts) +} diff --git a/packages/backend-core/src/events/index.ts b/packages/backend-core/src/events/index.ts index 814399655d..f94c8b0267 100644 --- a/packages/backend-core/src/events/index.ts +++ b/packages/backend-core/src/events/index.ts @@ -8,4 +8,5 @@ import { processors } from "./processors" export const shutdown = () => { processors.shutdown() + console.log("Events shutdown") } diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index b51ead46b9..062070785d 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -65,7 +65,7 @@ async function checkApiKey(apiKey: string, populateUser?: Function) { * The tenancy modules should not be used here and it should be assumed that the tenancy context * has not yet been populated. */ -module.exports = ( +export = ( noAuthPatterns = [], opts: { publicAllowed: boolean; populateUser?: Function } = { publicAllowed: false, diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.ts similarity index 96% rename from packages/backend-core/src/middleware/index.js rename to packages/backend-core/src/middleware/index.ts index 7e7b8a2931..998c231b3d 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.ts @@ -13,7 +13,8 @@ const adminOnly = require("./adminOnly") const builderOrAdmin = require("./builderOrAdmin") const builderOnly = require("./builderOnly") const joiValidator = require("./joi-validator") -module.exports = { + +const pkg = { google, oidc, jwt, @@ -33,3 +34,5 @@ module.exports = { builderOrAdmin, joiValidator, } + +export = pkg diff --git a/packages/backend-core/src/middleware/joi-validator.js b/packages/backend-core/src/middleware/joi-validator.js index 748ccebd89..6812dbdd54 100644 --- a/packages/backend-core/src/middleware/joi-validator.js +++ b/packages/backend-core/src/middleware/joi-validator.js @@ -13,10 +13,13 @@ function validate(schema, property) { params = ctx.request[property] } - schema = schema.append({ - createdAt: Joi.any().optional(), - updatedAt: Joi.any().optional(), - }) + // not all schemas have the append property e.g. array schemas + if (schema.append) { + schema = schema.append({ + createdAt: Joi.any().optional(), + updatedAt: Joi.any().optional(), + }) + } const { error } = schema.validate(params) if (error) { diff --git a/packages/backend-core/src/objectStore/index.ts b/packages/backend-core/src/objectStore/index.ts index 503ab9bca0..1b880ef7b2 100644 --- a/packages/backend-core/src/objectStore/index.ts +++ b/packages/backend-core/src/objectStore/index.ts @@ -66,15 +66,13 @@ const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL] * @constructor */ export const ObjectStore = (bucket: any) => { - AWS.config.update({ - accessKeyId: env.MINIO_ACCESS_KEY, - secretAccessKey: env.MINIO_SECRET_KEY, - region: env.AWS_REGION, - }) const config: any = { s3ForcePathStyle: true, signatureVersion: "v4", apiVersion: "2006-03-01", + accessKeyId: env.MINIO_ACCESS_KEY, + secretAccessKey: env.MINIO_SECRET_KEY, + region: env.AWS_REGION, } if (bucket) { config.params = { diff --git a/packages/backend-core/src/security/sessions.ts b/packages/backend-core/src/security/sessions.ts index 284adbcd1f..f621b99dc2 100644 --- a/packages/backend-core/src/security/sessions.ts +++ b/packages/backend-core/src/security/sessions.ts @@ -3,17 +3,27 @@ const { v4: uuidv4 } = require("uuid") const { logWarn } = require("../logging") const env = require("../environment") -interface Session { - key: string - userId: string +interface CreateSession { sessionId: string - lastAccessedAt: string - createdAt: string + tenantId: string csrfToken?: string - value: string } -type SessionKey = { key: string }[] +interface Session extends CreateSession { + userId: string + lastAccessedAt: string + createdAt: string + // make optional attributes required + csrfToken: string +} + +interface SessionKey { + key: string +} + +interface ScannedSession { + value: Session +} // a week in seconds const EXPIRY_SECONDS = 86400 * 7 @@ -22,14 +32,14 @@ function makeSessionID(userId: string, sessionId: string) { return `${userId}/${sessionId}` } -export async function getSessionsForUser(userId: string) { +export async function getSessionsForUser(userId: string): Promise { if (!userId) { console.trace("Cannot get sessions for undefined userId") return [] } const client = await redis.getSessionClient() - const sessions = await client.scan(userId) - return sessions.map((session: Session) => session.value) + const sessions: ScannedSession[] = await client.scan(userId) + return sessions.map(session => session.value) } export async function invalidateSessions( @@ -39,33 +49,32 @@ export async function invalidateSessions( try { const reason = opts?.reason || "unknown" let sessionIds: string[] = opts.sessionIds || [] - let sessions: SessionKey + let sessionKeys: SessionKey[] // If no sessionIds, get all the sessions for the user if (sessionIds.length === 0) { - sessions = await getSessionsForUser(userId) - sessions.forEach( - (session: any) => - (session.key = makeSessionID(session.userId, session.sessionId)) - ) + const sessions = await getSessionsForUser(userId) + sessionKeys = sessions.map(session => ({ + key: makeSessionID(session.userId, session.sessionId), + })) } else { // use the passed array of sessionIds sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds] - sessions = sessionIds.map((sessionId: string) => ({ + sessionKeys = sessionIds.map(sessionId => ({ key: makeSessionID(userId, sessionId), })) } - if (sessions && sessions.length > 0) { + if (sessionKeys && sessionKeys.length > 0) { const client = await redis.getSessionClient() const promises = [] - for (let session of sessions) { - promises.push(client.delete(session.key)) + for (let sessionKey of sessionKeys) { + promises.push(client.delete(sessionKey.key)) } if (!env.isTest()) { logWarn( - `Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions - .map(session => session.key) + `Invalidating sessions for ${userId} (reason: ${reason}) - ${sessionKeys + .map(sessionKey => sessionKey.key) .join(", ")}` ) } @@ -76,22 +85,26 @@ export async function invalidateSessions( } } -export async function createASession(userId: string, session: Session) { +export async function createASession( + userId: string, + createSession: CreateSession +) { // invalidate all other sessions await invalidateSessions(userId, { reason: "creation" }) const client = await redis.getSessionClient() - const sessionId = session.sessionId - if (!session.csrfToken) { - session.csrfToken = uuidv4() - } - session = { - ...session, + const sessionId = createSession.sessionId + const csrfToken = createSession.csrfToken ? createSession.csrfToken : uuidv4() + const key = makeSessionID(userId, sessionId) + + const session: Session = { + ...createSession, + csrfToken, createdAt: new Date().toISOString(), lastAccessedAt: new Date().toISOString(), userId, } - await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS) + await client.store(key, session, EXPIRY_SECONDS) } export async function updateSessionTTL(session: Session) { @@ -106,7 +119,10 @@ export async function endSession(userId: string, sessionId: string) { await client.delete(makeSessionID(userId, sessionId)) } -export async function getSession(userId: string, sessionId: string) { +export async function getSession( + userId: string, + sessionId: string +): Promise { if (!userId || !sessionId) { throw new Error(`Invalid session details - ${userId} - ${sessionId}`) } diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts new file mode 100644 index 0000000000..79436443db --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/accounts.ts @@ -0,0 +1,7 @@ +export const getAccount = jest.fn() +export const getAccountByTenantId = jest.fn() + +jest.mock("../../../src/cloud/accounts", () => ({ + getAccount, + getAccountByTenantId, +})) diff --git a/packages/backend-core/tests/utilities/mocks/date.js b/packages/backend-core/tests/utilities/mocks/date.js deleted file mode 100644 index 19248c6f11..0000000000 --- a/packages/backend-core/tests/utilities/mocks/date.js +++ /dev/null @@ -1,2 +0,0 @@ -exports.MOCK_DATE = new Date("2020-01-01T00:00:00.000Z") -exports.MOCK_DATE_TIMESTAMP = 1577836800000 diff --git a/packages/backend-core/tests/utilities/mocks/date.ts b/packages/backend-core/tests/utilities/mocks/date.ts new file mode 100644 index 0000000000..f580b68349 --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/date.ts @@ -0,0 +1,2 @@ +export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z") +export const MOCK_DATE_TIMESTAMP = 1577836800000 diff --git a/packages/backend-core/tests/utilities/mocks/events.js b/packages/backend-core/tests/utilities/mocks/events.ts similarity index 100% rename from packages/backend-core/tests/utilities/mocks/events.js rename to packages/backend-core/tests/utilities/mocks/events.ts diff --git a/packages/backend-core/tests/utilities/mocks/index.js b/packages/backend-core/tests/utilities/mocks/index.js deleted file mode 100644 index 6aa1c4a54f..0000000000 --- a/packages/backend-core/tests/utilities/mocks/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const posthog = require("./posthog") -const events = require("./events") -const date = require("./date") - -module.exports = { - posthog, - date, - events, -} diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts new file mode 100644 index 0000000000..7031b225ec --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -0,0 +1,4 @@ +import "./posthog" +import "./events" +export * as accounts from "./accounts" +export * as date from "./date" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 376cddf722..a35698c779 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.2.41-alpha.5", + "version": "1.2.58-alpha.5", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "1.2.41-alpha.5", + "@budibase/string-templates": "1.2.58-alpha.5", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index d75350d8e8..1a7ab59818 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -67,6 +67,13 @@ // If time only set date component to 2000-01-01 if (timeOnly) { + // Classic flackpickr causing issues. + // When selecting a value for the first time for a "time only" field, + // the time is always offset by 1 hour for some reason (regardless of time + // zone) so we need to correct it. + if (!value && newValue) { + newValue = new Date(dates[0].getTime() + 60 * 60 * 1000).toISOString() + } newValue = `2000-01-01T${newValue.split("T")[1]}` } diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index ffdac08402..3102972d1e 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -139,7 +139,13 @@
{#if selectedUrl} - {selectedImage.name} + + {selectedImage.name} + {:else} {selectedImage.name} {/if} diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte index 18a1e82ee8..a3952a9759 100644 --- a/packages/bbui/src/Form/Core/RadioGroup.svelte +++ b/packages/bbui/src/Form/Core/RadioGroup.svelte @@ -10,6 +10,7 @@ export let disabled = false export let getOptionLabel = option => option export let getOptionValue = option => option + export let getOptionTitle = option => option const dispatch = createEventDispatcher() const onChange = e => dispatch("change", e.target.value) @@ -19,7 +20,7 @@ {#if options && Array.isArray(options)} {#each options as option}
diff --git a/packages/bbui/src/Form/RadioGroup.svelte b/packages/bbui/src/Form/RadioGroup.svelte index 528f9f5eba..843a3657b4 100644 --- a/packages/bbui/src/Form/RadioGroup.svelte +++ b/packages/bbui/src/Form/RadioGroup.svelte @@ -12,6 +12,7 @@ export let direction = "vertical" export let getOptionLabel = option => extractProperty(option, "label") export let getOptionValue = option => extractProperty(option, "value") + export let getOptionTitle = option => extractProperty(option, "label") const dispatch = createEventDispatcher() const onChange = e => { @@ -35,6 +36,7 @@ {direction} {getOptionLabel} {getOptionValue} + {getOptionTitle} on:change={onChange} /> diff --git a/packages/bbui/src/Link/Link.svelte b/packages/bbui/src/Link/Link.svelte index f66554bd75..3bbfdd8282 100644 --- a/packages/bbui/src/Link/Link.svelte +++ b/packages/bbui/src/Link/Link.svelte @@ -8,12 +8,14 @@ export let secondary = false export let overBackground = false export let target + export let download +
{attachment.extension}
{:else}
- + {attachment.extension}
diff --git a/packages/builder/cypress/integration/adminAndManagement/authentication.spec.js b/packages/builder/cypress/integration/adminAndManagement/authentication.spec.js new file mode 100644 index 0000000000..5cc42cb59a --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/authentication.spec.js @@ -0,0 +1,178 @@ +import filterTests from "../../support/filterTests" +// const interact = require("../support/interact") + +filterTests(["smoke", "all"], () => { + context("Auth Configuration", () => { + before(() => { + cy.login() + }) + + after(() => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + + cy.get("[data-cy=new-scope-input]").clear() + + cy.get("div.content").scrollTo("bottom") + cy.get("[data-cy=oidc-active]").click() + + cy.get("[data-cy=oidc-active]").should('not.be.checked') + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.get(".spectrum-Toast-content") + .contains("Settings saved") + .should("be.visible") + }) + + it("Should allow updating of the OIDC config", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + cy.get(".spectrum-Toast .spectrum-ClearButton").click() + + cy.get("input[data-cy=configUrl]").type("http://budi-auth.com/v2") + cy.get("input[data-cy=clientID]").type("34ac6a13-f24a-4b52-c70d-fa544ffd11b2") + cy.get("input[data-cy=clientSecret]").type("12A8Q~4nS_DWhOOJ2vWIRsNyDVsdtXPD.Zxa9df_") + + cy.get("button[data-cy=oidc-save]").should("not.be.disabled"); + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.get(".spectrum-Toast-content") + .contains("Settings saved") + .should("be.visible") + }) + + it("Should display default scopes in advanced config.", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("openid") + cy.get(".spectrum-Tags-item").contains("openid").find(".spectrum-ClearButton").should("not.exist") + + cy.get(".spectrum-Tags-item").contains("offline_access") + cy.get(".spectrum-Tags-item").contains("email") + cy.get(".spectrum-Tags-item").contains("profile") + }) + + it("Add a new scopes", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + + cy.get("[data-cy=new-scope-input]").type("Sample{enter}") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 5) + cy.get(".spectrum-Tags-item").contains("Sample") + + cy.get(".auth-form input.spectrum-Textfield-input").type("Another ") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 6) + cy.get(".spectrum-Tags-item").contains("Another") + + cy.get("button[data-cy=oidc-save]").should("not.be.disabled"); + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.reload() + + cy.get("div.content").scrollTo("bottom") + + cy.get(".spectrum-Tags-item").contains("openid") + cy.get(".spectrum-Tags-item").contains("offline_access") + cy.get(".spectrum-Tags-item").contains("email") + cy.get(".spectrum-Tags-item").contains("profile") + cy.get(".spectrum-Tags-item").contains("Sample") + cy.get(".spectrum-Tags-item").contains("Another") + }) + + it("Should allow the removal of auth scopes", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + + cy.get(".spectrum-Tags-item").contains("offline_access").parent().find(".spectrum-ClearButton").click() + cy.get(".spectrum-Tags-item").contains("profile").parent().find(".spectrum-ClearButton").click() + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist") + cy.get(".spectrum-Tags-item").contains("profile").should("not.exist") + + cy.get("button[data-cy=oidc-save]").should("not.be.disabled"); + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.get(".spectrum-Toast-content") + .contains("Settings saved") + .should("be.visible") + + cy.reload() + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist") + cy.get(".spectrum-Tags-item").contains("profile").should("not.exist") + }) + + it("Should allow auth scopes to be reset to the core defaults.", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + + cy.get("div.content").scrollTo("bottom") + + cy.get("[data-cy=restore-oidc-default-scopes]").click({force: true}) + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("openid") + cy.get(".spectrum-Tags-item").contains("offline_access") + cy.get(".spectrum-Tags-item").contains("email") + cy.get(".spectrum-Tags-item").contains("profile") + }) + + it("Should not allow invalid characters in the auth scopes", () => { + cy.get("[data-cy=new-scope-input]").type("thisIsInvalid\\{enter}") + cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get("[data-cy=new-scope-input]").clear() + + cy.get("[data-cy=new-scope-input]").type("alsoInvalid\"{enter}") + cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get("[data-cy=new-scope-input]").clear() + }) + + it("Should not allow duplicate auth scopes", () => { + cy.get("[data-cy=new-scope-input]").type("offline_access{enter}") + cy.get(".spectrum-Form-itemField .error").contains("Auth scope already exists") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + }) + + }) +}) \ No newline at end of file diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index edca7ee3af..0e3fbb191b 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -102,7 +102,7 @@ filterTests(['all'], () => { cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 }) cy.wait(500) - cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished") + cy.get(interact.APP_TABLE_STATUS, { timeout: 10000 }).eq(0).contains("Unpublished") }) }) diff --git a/packages/builder/cypress/integration/datasources/mySql.spec.js b/packages/builder/cypress/integration/datasources/mySql.spec.js index 86b255ff58..654705a24e 100644 --- a/packages/builder/cypress/integration/datasources/mySql.spec.js +++ b/packages/builder/cypress/integration/datasources/mySql.spec.js @@ -175,7 +175,10 @@ filterTests(["all"], () => { cy.get("@query").its("response.statusCode").should("eq", 200) cy.get("@query").its("response.body").should("not.be.empty") // Save query + cy.intercept("POST", "**/queries").as("saveQuery") cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) + cy.wait("@saveQuery") + cy.get("@saveQuery").its("response.statusCode").should("eq", 200) cy.get(".nav-item").should("contain", queryName) }) diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js index feb583c83e..bfb312a0c8 100644 --- a/packages/builder/cypress/integration/datasources/postgreSql.spec.js +++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js @@ -252,7 +252,8 @@ filterTests(["all"], () => { .contains("Delete Query") .click({ force: true }) // Confirm deletion - cy.reload({ timeout: 5000 }) + cy.reload() + cy.get(".nav-item", { timeout: 30000 }).contains(datasource).click({ force: true }) cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename) }) diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js index 9a3d17f7c3..0fb58e89e9 100644 --- a/packages/builder/cypress/integration/revertApp.spec.js +++ b/packages/builder/cypress/integration/revertApp.spec.js @@ -48,6 +48,7 @@ filterTests(['smoke', 'all'], () => { cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + cy.get("input").type("Cypress Tests") // Click Revert cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true }) cy.wait(2000) // Wait for app to finish reverting diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index acb56a0bce..d4b0ec80e8 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -448,10 +448,7 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => { .contains("Continue") .click({ force: true }) }) - cy.get(".spectrum-Modal", { timeout: 10000 }).should( - "not.contain", - "Add data source" - ) + cy.get(".spectrum-Modal").contains("Create Table", { timeout: 10000 }) cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => { cy.get("input", { timeout: 2000 }).first().type(tableName).blur() cy.get(".spectrum-ButtonGroup").contains("Create").click() @@ -742,8 +739,15 @@ Cypress.Commands.add("deleteAllScreens", () => { Cypress.Commands.add("navigateToFrontend", () => { // Clicks on Design tab and then the Home nav item cy.wait(500) + cy.intercept("**/preview").as("preview") cy.contains("Design").click() - cy.get(".spectrum-Search", { timeout: 2000 }).type("/") + cy.wait("@preview") + cy.get("@preview").then(res => { + if (res.statusCode != 200) { + cy.reload() + } + }) + cy.get(".spectrum-Search", { timeout: 20000 }).type("/") cy.get(".nav-item", { timeout: 2000 }).contains("home").click({ force: true }) }) diff --git a/packages/builder/package.json b/packages/builder/package.json index 4690a82150..2ac398ecca 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.2.41-alpha.5", + "version": "1.2.58-alpha.5", "license": "GPL-3.0", "private": true, "scripts": { @@ -13,11 +13,11 @@ "cy:setup:ci": "node ./cypress/setup.js", "cy:open": "cypress open", "cy:run": "cypress run", - "cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js", + "cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js", "cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record", "cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run", "cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci", - "cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record && npm run cy:ci:report", + "cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record; npm run cy:ci:report", "cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline", "cy:ci:notify": "node scripts/cypressResultsWebhook", "cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open", @@ -69,10 +69,10 @@ } }, "dependencies": { - "@budibase/bbui": "1.2.41-alpha.5", - "@budibase/client": "1.2.41-alpha.5", - "@budibase/frontend-core": "1.2.41-alpha.5", - "@budibase/string-templates": "1.2.41-alpha.5", + "@budibase/bbui": "1.2.58-alpha.5", + "@budibase/client": "1.2.58-alpha.5", + "@budibase/frontend-core": "1.2.58-alpha.5", + "@budibase/string-templates": "1.2.58-alpha.5", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/scripts/cypressResultsWebhook.js b/packages/builder/scripts/cypressResultsWebhook.js index 457093e013..4de4c01cc7 100644 --- a/packages/builder/scripts/cypressResultsWebhook.js +++ b/packages/builder/scripts/cypressResultsWebhook.js @@ -5,7 +5,6 @@ const path = require("path") const fs = require("fs") const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL -const OUTCOME = process.env.CYPRESS_OUTCOME const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL const GIT_SHA = process.env.GITHUB_SHA const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL @@ -35,6 +34,8 @@ async function discordCypressResultsNotification(report) { skipped, } = report.stats + const OUTCOME = failures > 0 ? "failure" : "success" + const options = { method: "POST", headers: { @@ -114,7 +115,7 @@ async function discordCypressResultsNotification(report) { } const response = await fetch(WEBHOOK_URL, options) - if (response.status >= 400) { + if (response.status >= 201) { const text = await response.text() console.error( `Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}` diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index bebd06c6d7..13b749e19f 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -502,11 +502,11 @@ const getRoleBindings = () => { } /** - * Gets all bindable properties exposed in a button actions flow up until + * Gets all bindable properties exposed in an event action flow up until * the specified action ID, as well as context provided for the action * setting as a whole by the component. */ -export const getButtonContextBindings = ( +export const getEventContextBindings = ( asset, componentId, settingKey, @@ -520,10 +520,7 @@ export const getButtonContextBindings = ( const component = findComponent(asset.props, componentId) const settings = getComponentSettings(component?._component) const eventSetting = settings.find(setting => setting.key === settingKey) - if (!eventSetting) { - return bindings - } - if (eventSetting.context?.length) { + if (eventSetting?.context?.length) { eventSetting.context.forEach(contextEntry => { bindings.push({ readableBinding: contextEntry.label, diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index c7795d4b54..c5d953021f 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -19,7 +19,6 @@ import { makeComponentUnique, } from "../componentUtils" import { Helpers } from "@budibase/bbui" -import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants" import { Utils } from "@budibase/frontend-core" const INITIAL_FRONTEND_STATE = { @@ -40,6 +39,7 @@ const INITIAL_FRONTEND_STATE = { devicePreview: false, messagePassing: false, continueIfAction: false, + showNotificationAction: false, }, errors: [], hasAppPackage: false, @@ -124,35 +124,6 @@ export const getFrontendStore = () => { await integrations.init() await queries.init() await tables.init() - - // Add navigation settings to old apps - if (!application.navigation) { - const layout = layouts.find(x => x._id === LAYOUT_NAMES.MASTER.PRIVATE) - const customTheme = application.customTheme - let navigationSettings = { - navigation: "Top", - title: application.name, - navWidth: "Large", - navBackground: - customTheme?.navBackground || DefaultAppTheme.navBackground, - navTextColor: - customTheme?.navTextColor || DefaultAppTheme.navTextColor, - } - if (layout) { - navigationSettings.hideLogo = layout.props.hideLogo - navigationSettings.hideTitle = layout.props.hideTitle - navigationSettings.title = layout.props.title || application.name - navigationSettings.logoUrl = layout.props.logoUrl - navigationSettings.links = layout.props.links - navigationSettings.navigation = layout.props.navigation || "Top" - navigationSettings.sticky = layout.props.sticky - navigationSettings.navWidth = layout.props.width || "Large" - if (navigationSettings.navigation === "None") { - navigationSettings.navigation = "Top" - } - } - await store.actions.navigation.save(navigationSettings) - } }, theme: { save: async theme => { diff --git a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte index f510d961fb..3920885a2e 100644 --- a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte @@ -14,7 +14,7 @@ $: { let fields = {} - for (const [key, type] of Object.entries(block?.inputs?.fields)) { + for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) { fields = { ...fields, [key]: { diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index ce2b97bcba..21059b32dd 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -467,6 +467,7 @@ options={relationshipOptions} getOptionLabel={option => option.name} getOptionValue={option => option.value} + getOptionTitle={option => option.alt} /> {/if} + import { Select, Label, Checkbox } from "@budibase/bbui" + import { onMount } from "svelte" + import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" + + export let parameters + export let bindings = [] + + const types = [ + { + label: "Success", + value: "success", + }, + { + label: "Warning", + value: "warning", + }, + { + label: "Error", + value: "error", + }, + { + label: "Info", + value: "info", + }, + ] + + onMount(() => { + if (!parameters.type) { + parameters.type = "success" + } + if (parameters.autoDismiss == null) { + parameters.autoDismiss = true + } + }) + + +
+ + { // Delete unsupported fields delete element.createdAt @@ -199,7 +201,6 @@ } } }) - if (calls.length) { Promise.all(calls) .then(data => { @@ -215,6 +216,21 @@ } } + let defaultScopes = ["profile", "email", "offline_access"] + + const refreshScopes = idx => { + providers.oidc.config.configs[idx]["scopes"] = + providers.oidc.config.configs[idx]["scopes"] + } + + let scopesFields = [ + { + editing: true, + inputText: null, + error: null, + }, + ] + onMount(async () => { try { await organisation.init() @@ -276,7 +292,7 @@ if (!oidcDoc?._id) { providers.oidc = { type: ConfigTypes.OIDC, - config: { configs: [{ activated: true }] }, + config: { configs: [{ activated: true, scopes: defaultScopes }] }, } } else { originalOidcDoc = cloneDeep(oidcDoc) @@ -345,6 +361,7 @@ size="s" cta on:click={() => save([providers.oidc])} + dataCy={"oidc-save"} > Save @@ -362,6 +379,7 @@ bind:value={providers.oidc.config.configs[0][field.name]} readonly={field.readonly} placeholder={field.placeholder} + dataCy={field.name} />
{/each} @@ -392,15 +410,132 @@
+ + + +
+
Advanced
+ +
+
+ + Changes to your authentication scopes will only take effect when you + next log in. Please refer to your vendor documentation before + modification. + + +
+ + + { + if (!scopesFields[0].inputText) { + scopesFields[0].error = null + } + if ( + e.key === "Enter" || + e.keyCode === 13 || + e.code == "Space" || + e.keyCode == 32 + ) { + let scopes = providers.oidc.config.configs[0]["scopes"] + ? providers.oidc.config.configs[0]["scopes"] + : [...defaultScopes] + + let update = scopesFields[0].inputText.trim() + + if (HasSpacesRegex.test(update)) { + scopesFields[0].error = + "Auth scopes cannot contain spaces, double quotes or backslashes" + return + } else if (scopes.indexOf(update) > -1) { + scopesFields[0].error = "Auth scope already exists" + return + } else if (!update.length) { + scopesFields[0].inputText = null + scopesFields[0].error = null + return + } else { + scopesFields[0].error = null + scopes.push(update) + providers.oidc.config.configs[0]["scopes"] = scopes + scopesFields[0].inputText = null + } + } + }} + /> + +
+ + + openid + {#each providers.oidc.config.configs[0]["scopes"] || [...defaultScopes] as tag, idx} + { + let idxScopes = providers.oidc.config.configs[0]["scopes"] + if (idxScopes.length == 1) { + idxScopes.pop() + } else { + idxScopes.splice(idx, 1) + refreshScopes(0) + } + }} + > + {tag} + + {/each} + +
+
+
+
{/if} diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte index 1e7c579346..d6ea4275c9 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte @@ -62,7 +62,7 @@ csvString = e.target.result files = fileArray - userEmails = csvString.split("\n") + userEmails = csvString.split(/\r?\n/) }) reader.readAsText(fileArray[0]) } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/InvitedModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/InvitedModal.svelte new file mode 100644 index 0000000000..9cc66a1385 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/InvitedModal.svelte @@ -0,0 +1,75 @@ + + + + {#if hasSuccess} + + Your users should now receive an email invite to get access to their + Budibase account + + {/if} + {#if hasFailure} + + {failureMessage} + + + {/if} + + + diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte index 02501f2de0..990a54610c 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte @@ -2,24 +2,78 @@ import { Body, ModalContent, Table, Icon } from "@budibase/bbui" import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte" import { parseToCsv } from "helpers/data/utils" + import { onMount } from "svelte" export let userData + export let createUsersResponse - $: mappedData = userData.map(user => { - return { - email: user.email, - password: user.password, + let hasSuccess + let hasFailure + let title + let failureMessage + + let userDataIndex + let successfulUsers + let unsuccessfulUsers + + const setTitle = () => { + if (hasSuccess) { + title = "Users created!" + } else if (hasFailure) { + title = "Oops!" } + } + + const setFailureMessage = () => { + if (hasSuccess) { + failureMessage = "However there was a problem creating some users." + } else { + failureMessage = "There was a problem creating some users." + } + } + + const setUsers = () => { + userDataIndex = userData.reduce((prev, current) => { + prev[current.email] = current + return prev + }, {}) + + successfulUsers = createUsersResponse.successful.map(user => { + return { + email: user.email, + password: userDataIndex[user.email].password, + } + }) + + unsuccessfulUsers = createUsersResponse.unsuccessful.map(user => { + return { + email: user.email, + reason: user.reason, + } + }) + } + + onMount(() => { + hasSuccess = createUsersResponse.successful.length + hasFailure = createUsersResponse.unsuccessful.length + setTitle() + setFailureMessage() + setUsers() }) - const schema = { + const successSchema = { email: {}, password: {}, } + const failedSchema = { + email: {}, + reason: {}, + } + const downloadCsvFile = () => { const fileName = "passwords.csv" - const content = parseToCsv(["email", "password"], mappedData) + const content = parseToCsv(["email", "password"], successfulUsers) download(fileName, content) } @@ -42,36 +96,52 @@ - - All your new users can be accessed through the autogenerated passwords. Take - note of these passwords or download the CSV file. - + {#if hasFailure} + + {failureMessage} + +
+ {/if} + {#if hasSuccess} + + All your new users can be accessed through the autogenerated passwords. + Take note of these passwords or download the CSV file. + -
-
- +
+
+ -
- Passwords CSV +
+ Passwords CSV +
-
-
+
+ {/if} + + + + + \ No newline at end of file diff --git a/packages/server/src/api/routes/static.ts b/packages/server/src/api/routes/static.ts index c94ff54708..9a53486689 100644 --- a/packages/server/src/api/routes/static.ts +++ b/packages/server/src/api/routes/static.ts @@ -56,6 +56,7 @@ router authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), controller.deleteObjects ) + .get("/preview", authorized(BUILDER), controller.serveBuilderPreview) .get("/:appId/:path*", controller.serveApp) .get("/app/:appUrl/:path*", controller.serveApp) .post( diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js index 2620547298..dcfc2c6d9b 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.js @@ -17,6 +17,7 @@ const { checkBuilderEndpoint, } = require("./utilities/TestFunctions") const setup = require("./utilities") +const { basicScreen, basicLayout } = setup.structures const { AppStatus } = require("../../../db/utils") const { events } = require("@budibase/backend-core") @@ -81,6 +82,31 @@ describe("/applications", () => { body: { name: "My App" }, }) }) + + it("migrates navigation settings from old apps", async () => { + const res = await request + .post("/api/applications") + .field("name", "Old App") + .field("useTemplate", "true") + .set(config.defaultHeaders()) + .attach("templateFile", "src/api/routes/tests/data/old-app.txt") + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._id).toBeDefined() + expect(res.body.navigation).toBeDefined() + expect(res.body.navigation.hideLogo).toBe(true) + expect(res.body.navigation.title).toBe("Custom Title") + expect(res.body.navigation.hideLogo).toBe(true) + expect(res.body.navigation.navigation).toBe("Left") + expect(res.body.navigation.navBackground).toBe( + "var(--spectrum-global-color-blue-600)" + ) + expect(res.body.navigation.navTextColor).toBe( + "var(--spectrum-global-color-gray-50)" + ) + expect(events.app.created).toBeCalledTimes(1) + expect(events.app.fileImported).toBeCalledTimes(1) + }) }) describe("fetch", () => { diff --git a/packages/server/src/api/routes/tests/data/old-app.txt b/packages/server/src/api/routes/tests/data/old-app.txt new file mode 100644 index 0000000000..42732092e5 --- /dev/null +++ b/packages/server/src/api/routes/tests/data/old-app.txt @@ -0,0 +1,3 @@ +{"version":"1.2.9","db_type":"http","start_time":"2022-08-31T13:18:50.740Z","db_info":{"db_name":"app_dev_default_c45f987cfa274060840878557ff2f547","purge_seq":"0-g1AAAABXeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVB4LkGRoAFL_gSArkQGP2kSGpHqIoiwAtOgYRA","update_seq":"28-g1AAAABXeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVB4LkGRoAFL_gSArkQ2P2kSGpHqwIrEsALX-GGA","sizes":{"file":233890,"external":5856,"active":19326},"props":{},"doc_del_count":0,"doc_count":7,"disk_format_version":8,"compact_running":false,"cluster":{"q":2,"n":1,"w":1,"r":1},"instance_start_time":"0","host":"http://localhost:10000/db/app_dev_default_c45f987cfa274060840878557ff2f547/","auto_compaction":false,"adapter":"http"}} +{"docs":[{"_id":"_design/database","_rev":"4-afb0361e5ebb6b7e464c6c146ac3f559","views":{"by_link":{"map":"function (doc) {\n // everything in this must remain constant as its going to Pouch, no external variables\n if (doc.type === \"link\") {\n let doc1 = doc.doc1;\n let doc2 = doc.doc2;\n emit([doc1.tableId, doc1.rowId], {\n id: doc2.rowId,\n thisId: doc1.rowId,\n fieldName: doc1.fieldName,\n });\n // if linking to same table can't emit twice\n if (doc1.tableId !== doc2.tableId) {\n emit([doc2.tableId, doc2.rowId], {\n id: doc1.rowId,\n thisId: doc2.rowId,\n fieldName: doc2.fieldName,\n });\n }\n }\n }"},"screen_routes":{"map":"function(doc) {\n if (doc._id.startsWith(\"screen_\")) {\n emit(doc._id, {\n id: doc._id,\n routing: doc.routing,\n })\n }\n }"}},"indexes":{"rows":{"index":"function (doc) {\n function idx(input, prev) {\n for (let key of Object.keys(input)) {\n let idxKey = prev != null ? `${prev}.${key}` : key;\n idxKey = idxKey.replace(/ /g, \"_\");\n if (Array.isArray(input[key])) {\n for (let val of input[key]) {\n if (typeof val !== \"object\") {\n // eslint-disable-next-line no-undef\n index(idxKey, val, { store: true });\n }\n }\n }\n else if (key === \"_id\" || key === \"_rev\" || input[key] == null) {\n continue;\n }\n if (typeof input[key] === \"string\") {\n // eslint-disable-next-line no-undef\n index(idxKey, input[key].toLowerCase(), { store: true });\n }\n else if (typeof input[key] !== \"object\") {\n // eslint-disable-next-line no-undef\n index(idxKey, input[key], { store: true });\n }\n else {\n idx(input[key], idxKey);\n }\n }\n }\n if (doc._id.startsWith(\"ro_\")) {\n // eslint-disable-next-line no-undef\n index(\"default\", doc._id);\n idx(doc);\n }\n }","analyzer":"keyword"}},"_revisions":{"start":4,"ids":["afb0361e5ebb6b7e464c6c146ac3f559","d1eafe2832e6554b5d348e16d6a1b0d3","6e20795c5ba67179b478f0337a585b56","0b24e44a44af45e51e562fd124ce3007"]}},{"_id":"app_metadata","_rev":"8-9c8540f72e647e3ca7108978e862ff17","appId":"app_dev_default_c45f987cfa274060840878557ff2f547","type":"app","version":"1.0.79-alpha.7","componentLibraries":["@budibase/standard-components"],"name":"Old App","url":"/old%20app","instance":{"_id":"app_dev_default_c45f987cfa274060840878557ff2f547"},"tenantId":"default","updatedAt":"2022-08-31T13:18:05.273Z","createdAt":"2022-08-31T13:16:36.639Z","status":"development","customTheme":{"navBackground":"var(--spectrum-global-color-blue-600)","navTextColor":"var(--spectrum-global-color-gray-50)","buttonBorderRadius":"4px","primaryColor":"var(--spectrum-global-color-red-600)"},"_revisions":{"start":8,"ids":["9c8540f72e647e3ca7108978e862ff17","5c86ff8a96e1f3b4505e205f25a92656","75473d58cf92b61dbcdcd74157f9e2e1","ebc34a96a7ad678bae76d7d70880f4de","1c7397be4ef470d66bd0fd4b3fe4a002","89222c47e9d254b4885e1818cd0aac79","66bed1c391eabcea6ad3557d8ede5f27","0f1136445fd7c2cf73bb5cf41aaa19d2"]}},{"_id":"layout_private_master","_rev":"12-fa664c0538f1478b2a3695c8909a04e9","componentLibraries":["@budibase/standard-components"],"title":"Old App","favicon":"./_shared/favicon.png","stylesheets":[],"name":"Navigation Layout","props":{"_id":"4f569166-a4f3-47ea-a09e-6d218c75586f","_instanceName":"Navigation Layout","_component":"@budibase/standard-components/layout","_children":[{"_id":"7fcf11e4-6f5b-4085-8e0d-9f3d44c98967","_component":"@budibase/standard-components/screenslot","_instanceName":"Screen slot","_styles":{"normal":{"flex":"1 1 auto","display":"flex","flex-direction":"column","justify-content":"flex-start","align-items":"stretch"},"hover":{},"active":{},"selected":{}},"_children":[]}],"_styles":{"active":{},"hover":{},"normal":{},"selected":{}},"title":"Custom Title","navigation":"Left","width":"Small","links":[{"text":"Home","url":"/"}],"hideLogo":true,"hideTitle":false},"_revisions":{"start":12,"ids":["fa664c0538f1478b2a3695c8909a04e9","54b4084d5c10369ecd7a3b33d4cb7775","61e5d376e9f00d20d9905bf011680943","f222416fe5bcf0085ceb72bdc94327c1","b6d9abe1caa1a8ffec0b1e8cb7324c4b","85a347b8c8acb5ad4e31929384062e05","a7ce68d677f2fa7e9a0d54b2fb4275bb","83811e8befcdac752cdbe16cd8b33fa0","362cb9ad2c87ef9c92c58c0a41ed69d4","0a3bba6cdc98539abb3a4e98e13ef98a","d964020cc8907b37047bcd2bd1387735","156b45a34bbee17b6a4a97bfaaeb6f90"]}},{"_id":"layout_public_master","_rev":"1-c7d57c9c42e9e8f988d82f0e6cc7d32c","componentLibraries":["@budibase/standard-components"],"title":"Old App","favicon":"./_shared/favicon.png","stylesheets":[],"name":"Empty Layout","props":{"_id":"3723ffa1-f9e0-4c05-8013-98195c788ed6","_instanceName":"Empty Layout","_component":"@budibase/standard-components/layout","_children":[{"_id":"7fcf11e4-6f5b-4085-8e0d-9f3d44c98967","_component":"@budibase/standard-components/screenslot","_instanceName":"Screen slot","_styles":{"normal":{"flex":"1 1 auto","display":"flex","flex-direction":"column","justify-content":"flex-start","align-items":"stretch"},"hover":{},"active":{},"selected":{}},"_children":[]}],"_styles":{"active":{},"hover":{},"normal":{},"selected":{}},"navigation":"None","width":"Large","links":[{"text":"Home","url":"/"}]}},{"_id":"screen_c483ef802772479196a28c8a6eafe6a5","_rev":"1-db29ceb02c9c6c1ade847c8086d28a70","layoutId":"layout_private_master","props":{"_id":"cafd93b74e18b45a49a58c894d7ef29b4","_component":"@budibase/standard-components/container","_styles":{"normal":{},"hover":{},"active":{},"selected":{}},"_children":[],"_instanceName":"Home","direction":"column","hAlign":"stretch","vAlign":"top","size":"grow","gap":"M"},"routing":{"route":"/home","roleId":"BASIC"},"name":"cafd93b74e18b45a49a58c894d7ef29b4","template":"createFromScratch"},{"_id":"ta_users","_rev":"1-30a4344f056c24cf776d5736eb3c7ed5","type":"table","views":{},"name":"Users","schema":{"email":{"type":"string","constraints":{"type":"string","email":true,"length":{"maximum":""},"presence":true},"fieldName":"email","name":"email"},"firstName":{"name":"firstName","fieldName":"firstName","type":"string","constraints":{"type":"string","presence":false}},"lastName":{"name":"lastName","fieldName":"lastName","type":"string","constraints":{"type":"string","presence":false}},"roleId":{"fieldName":"roleId","name":"roleId","type":"options","constraints":{"type":"string","presence":false,"inclusion":["ADMIN","POWER","BASIC","PUBLIC"]}},"status":{"fieldName":"status","name":"status","type":"options","constraints":{"type":"string","presence":false,"inclusion":["active","inactive"]}}},"primaryDisplay":"email"}]} +{"seq":"28-g1AAAACbeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVCJDUv3___-zMpgTxXKBAuxJiZYGxgZG2DTgMSaPBUgyNACp_1DT2MCmGRmZWBglG2PTlwUAEaUn-w"} diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 86e47924d8..5cd282bb34 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -3,7 +3,12 @@ const setup = require("./utilities") const { basicRow } = setup.structures const { doInAppContext } = require("@budibase/backend-core/context") const { doInTenant } = require("@budibase/backend-core/tenancy") -const { quotas, QuotaUsageType, StaticQuotaName, MonthlyQuotaName } = require("@budibase/pro") +const { + quotas, + QuotaUsageType, + StaticQuotaName, + MonthlyQuotaName, +} = require("@budibase/pro") describe("/rows", () => { let request = setup.getRequest() @@ -23,23 +28,30 @@ describe("/rows", () => { await request .get(`/api/${table._id}/rows/${id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(status) const getRowUsage = async () => { - return config.doInContext(null, () => quotas.getCurrentUsageValue(QuotaUsageType.STATIC, StaticQuotaName.ROWS)) + return config.doInContext(null, () => + quotas.getCurrentUsageValue(QuotaUsageType.STATIC, StaticQuotaName.ROWS) + ) } const getQueryUsage = async () => { - return config.doInContext(null, () => quotas.getCurrentUsageValue(QuotaUsageType.MONTHLY, MonthlyQuotaName.QUERIES)) + return config.doInContext(null, () => + quotas.getCurrentUsageValue( + QuotaUsageType.MONTHLY, + MonthlyQuotaName.QUERIES + ) + ) } - const assertRowUsage = async (expected) => { + const assertRowUsage = async expected => { const usage = await getRowUsage() expect(usage).toBe(expected) } - const assertQueryUsage = async (expected) => { + const assertQueryUsage = async expected => { const usage = await getQueryUsage() expect(usage).toBe(expected) } @@ -76,10 +88,12 @@ describe("/rows", () => { name: "Updated Name", }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`) + expect(res.res.statusMessage).toEqual( + `${table.name} updated successfully.` + ) expect(res.body.name).toEqual("Updated Name") // await assertRowUsage(rowUsage) // await assertQueryUsage(queryUsage + 1) @@ -92,7 +106,7 @@ describe("/rows", () => { const res = await request .get(`/api/${table._id}/rows/${existing._id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body).toEqual({ @@ -110,7 +124,7 @@ describe("/rows", () => { const newRow = { tableId: table._id, name: "Second Contact", - status: "new" + status: "new", } await config.createRow() await config.createRow(newRow) @@ -119,7 +133,7 @@ describe("/rows", () => { const res = await request .get(`/api/${table._id}/rows`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toBe(2) @@ -135,17 +149,36 @@ describe("/rows", () => { await request .get(`/api/${table._id}/rows/not-a-valid-id`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(404) await assertQueryUsage(queryUsage) // no change }) it("row values are coerced", async () => { - const str = {type:"string", constraints: { type: "string", presence: false }} - const attachment = {type:"attachment", constraints: { type: "array", presence: false }} - const bool = {type:"boolean", constraints: { type: "boolean", presence: false }} - const number = {type:"number", constraints: { type: "number", presence: false }} - const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }} + const str = { + type: "string", + constraints: { type: "string", presence: false }, + } + const attachment = { + type: "attachment", + constraints: { type: "array", presence: false }, + } + const bool = { + type: "boolean", + constraints: { type: "boolean", presence: false }, + } + const number = { + type: "number", + constraints: { type: "number", presence: false }, + } + const datetime = { + type: "datetime", + constraints: { + type: "string", + presence: false, + datetime: { earliest: "", latest: "" }, + }, + } table = await config.createTable({ name: "TestTable2", @@ -171,9 +204,9 @@ describe("/rows", () => { boolUndefined: bool, boolString: bool, boolBool: bool, - attachmentNull : attachment, - attachmentUndefined : attachment, - attachmentEmpty : attachment, + attachmentNull: attachment, + attachmentUndefined: attachment, + attachmentEmpty: attachment, }, }) @@ -198,9 +231,9 @@ describe("/rows", () => { boolString: "true", boolBool: true, tableId: table._id, - attachmentNull : null, - attachmentUndefined : undefined, - attachmentEmpty : "", + attachmentNull: null, + attachmentUndefined: undefined, + attachmentEmpty: "", } const id = (await config.createRow(row))._id @@ -218,7 +251,9 @@ describe("/rows", () => { expect(saved.datetimeEmptyString).toBe(null) expect(saved.datetimeNull).toBe(null) expect(saved.datetimeUndefined).toBe(undefined) - expect(saved.datetimeString).toBe(new Date(row.datetimeString).toISOString()) + expect(saved.datetimeString).toBe( + new Date(row.datetimeString).toISOString() + ) expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString()) expect(saved.boolNull).toBe(null) expect(saved.boolEmpty).toBe(null) @@ -247,10 +282,12 @@ describe("/rows", () => { name: "Updated Name", }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - - expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`) + + expect(res.res.statusMessage).toEqual( + `${table.name} updated successfully.` + ) expect(res.body.name).toEqual("Updated Name") expect(res.body.description).toEqual(existing.description) @@ -292,16 +329,14 @@ describe("/rows", () => { const res = await request .delete(`/api/${table._id}/rows`) .send({ - rows: [ - createdRow - ] + rows: [createdRow], }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body[0]._id).toEqual(createdRow._id) - await assertRowUsage(rowUsage -1) - await assertQueryUsage(queryUsage +1) + await assertRowUsage(rowUsage - 1) + await assertQueryUsage(queryUsage + 1) }) }) @@ -314,9 +349,9 @@ describe("/rows", () => { .post(`/api/${table._id}/rows/validate`) .send({ name: "ivan" }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - + expect(res.body.valid).toBe(true) expect(Object.keys(res.body.errors)).toEqual([]) await assertRowUsage(rowUsage) @@ -331,9 +366,9 @@ describe("/rows", () => { .post(`/api/${table._id}/rows/validate`) .send({ name: 1 }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) - + expect(res.body.valid).toBe(false) expect(Object.keys(res.body.errors)).toEqual(["name"]) await assertRowUsage(rowUsage) @@ -351,19 +386,16 @@ describe("/rows", () => { const res = await request .delete(`/api/${table._id}/rows`) .send({ - rows: [ - row1, - row2, - ] + rows: [row1, row2], }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toEqual(2) await loadRow(row1._id, 404) await assertRowUsage(rowUsage - 2) - await assertQueryUsage(queryUsage +1) + await assertQueryUsage(queryUsage + 1) }) }) @@ -376,12 +408,12 @@ describe("/rows", () => { const res = await request .get(`/api/views/${table._id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toEqual(1) expect(res.body[0]._id).toEqual(row._id) await assertRowUsage(rowUsage) - await assertQueryUsage(queryUsage +1) + await assertQueryUsage(queryUsage + 1) }) it("should throw an error if view doesn't exist", async () => { @@ -406,7 +438,7 @@ describe("/rows", () => { const res = await request .get(`/api/views/${view.name}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toEqual(1) expect(res.body[0]._id).toEqual(row._id) @@ -418,21 +450,24 @@ describe("/rows", () => { describe("fetchEnrichedRows", () => { it("should allow enriching some linked rows", async () => { - const { table, firstRow, secondRow } = await doInTenant(setup.structures.TENANT_ID, async () => { - const table = await config.createLinkedTable() - const firstRow = await config.createRow({ - name: "Test Contact", - description: "original description", - tableId: table._id - }) - const secondRow = await config.createRow({ - name: "Test 2", - description: "og desc", - link: [{_id: firstRow._id}], - tableId: table._id, - }) - return { table, firstRow, secondRow } - }) + const { table, firstRow, secondRow } = await doInTenant( + setup.structures.TENANT_ID, + async () => { + const table = await config.createLinkedTable() + const firstRow = await config.createRow({ + name: "Test Contact", + description: "original description", + tableId: table._id, + }) + const secondRow = await config.createRow({ + name: "Test 2", + description: "og desc", + link: [{ _id: firstRow._id }], + tableId: table._id, + }) + return { table, firstRow, secondRow } + } + ) const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() @@ -440,7 +475,7 @@ describe("/rows", () => { const resBasic = await request .get(`/api/${table._id}/rows/${secondRow._id}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(resBasic.body.link[0]._id).toBe(firstRow._id) expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact") @@ -449,14 +484,14 @@ describe("/rows", () => { const resEnriched = await request .get(`/api/${table._id}/${secondRow._id}/enrich`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(resEnriched.body.link.length).toBe(1) expect(resEnriched.body.link[0]._id).toBe(firstRow._id) expect(resEnriched.body.link[0].name).toBe("Test Contact") expect(resEnriched.body.link[0].description).toBe("original description") await assertRowUsage(rowUsage) - await assertQueryUsage(queryUsage +2) + await assertQueryUsage(queryUsage + 2) }) }) @@ -466,9 +501,11 @@ describe("/rows", () => { const row = await config.createRow({ name: "test", description: "test", - attachment: [{ - key: `${config.getAppId()}/attachments/test/thing.csv`, - }], + attachment: [ + { + key: `${config.getAppId()}/attachments/test/thing.csv`, + }, + ], tableId: table._id, }) // the environment needs configured for this @@ -482,4 +519,49 @@ describe("/rows", () => { }) }) }) + + describe("exportData", () => { + it("should allow exporting all columns", async () => { + const existing = await config.createRow() + const res = await request + .post(`/api/${table._id}/rows/exportRows?format=json`) + .set(config.defaultHeaders()) + .send({ + rows: [existing._id], + }) + .expect("Content-Type", /json/) + .expect(200) + const results = JSON.parse(res.text) + expect(results.length).toEqual(1) + const row = results[0] + + // Ensure all original columns were exported + expect(Object.keys(row).length).toBeGreaterThanOrEqual( + Object.keys(existing).length + ) + Object.keys(existing).forEach(key => { + expect(row[key]).toEqual(existing[key]) + }) + }) + + it("should allow exporting only certain columns", async () => { + const existing = await config.createRow() + const res = await request + .post(`/api/${table._id}/rows/exportRows?format=json`) + .set(config.defaultHeaders()) + .send({ + rows: [existing._id], + columns: ["_id"], + }) + .expect("Content-Type", /json/) + .expect(200) + const results = JSON.parse(res.text) + expect(results.length).toEqual(1) + const row = results[0] + + // Ensure only the _id column was exported + expect(Object.keys(row).length).toEqual(1) + expect(row._id).toEqual(existing._id) + }) + }) }) diff --git a/packages/server/src/api/routes/tests/static.spec.js b/packages/server/src/api/routes/tests/static.spec.js index 37176f5cf5..812b88329b 100644 --- a/packages/server/src/api/routes/tests/static.spec.js +++ b/packages/server/src/api/routes/tests/static.spec.js @@ -40,7 +40,6 @@ describe("/static", () => { }) describe("/app", () => { - beforeEach(() => { jest.clearAllMocks() }) @@ -60,7 +59,7 @@ describe("/static", () => { it("should serve the app by url", async () => { const headers = config.defaultHeaders() delete headers[constants.Headers.APP_ID] - + const res = await request .get(`/app${config.prodApp.url}`) .set(headers) @@ -82,7 +81,7 @@ describe("/static", () => { describe("/attachments", () => { describe("generateSignedUrls", () => { let datasource - + beforeEach(async () => { datasource = await config.createDatasource({ datasource: { @@ -93,7 +92,7 @@ describe("/static", () => { }, }) }) - + it("should be able to generate a signed upload URL", async () => { const bucket = "foo" const key = "bar" @@ -108,7 +107,7 @@ describe("/static", () => { `https://${bucket}.s3.eu-west-1.amazonaws.com/${key}` ) }) - + it("should handle an invalid datasource ID", async () => { const res = await request .post(`/api/attachments/foo/url`) @@ -123,7 +122,7 @@ describe("/static", () => { "The specified datasource could not be found" ) }) - + it("should require a bucket parameter", async () => { const res = await request .post(`/api/attachments/${datasource._id}/url`) @@ -136,7 +135,7 @@ describe("/static", () => { .expect(400) expect(res.body.message).toEqual("bucket and key values are required") }) - + it("should require a key parameter", async () => { const res = await request .post(`/api/attachments/${datasource._id}/url`) @@ -151,4 +150,17 @@ describe("/static", () => { }) }) + describe("/preview", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should serve the builder preview", async () => { + const headers = config.defaultHeaders() + const res = await request.get(`/preview`).set(headers).expect(200) + + expect(res.body.appId).toBe(config.appId) + expect(res.body.builderPreview).toBe(true) + }) + }) }) diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 62301d57ca..831a9b1046 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -1,6 +1,15 @@ // need to load environment first -import { ExtendableContext } from "koa" import * as env from "./environment" + +// enable APM if configured +if (process.env.ELASTIC_APM_ENABLED) { + const apm = require("elastic-apm-node").start({ + serviceName: process.env.SERVICE, + environment: process.env.BUDIBASE_ENVIRONMENT, + }) +} + +import { ExtendableContext } from "koa" import db from "./db" db.init() const Koa = require("koa") @@ -74,9 +83,7 @@ server.on("close", async () => { return } shuttingDown = true - if (!env.isTest()) { - console.log("Server Closed") - } + console.log("Server Closed") await automations.shutdown() await redis.shutdown() await events.shutdown() @@ -158,3 +165,7 @@ process.on("uncaughtException", err => { process.on("SIGTERM", () => { shutdown() }) + +process.on("SIGINT", () => { + shutdown() +}) diff --git a/packages/server/src/automations/bullboard.js b/packages/server/src/automations/bullboard.js index cba6594ae7..3aac6c4fed 100644 --- a/packages/server/src/automations/bullboard.js +++ b/packages/server/src/automations/bullboard.js @@ -8,12 +8,14 @@ const Queue = env.isTest() const { JobQueues } = require("../constants") const { utils } = require("@budibase/backend-core/redis") const { opts, redisProtocolUrl } = utils.getRedisOptions() +const listeners = require("./listeners") const CLEANUP_PERIOD_MS = 60 * 1000 const queueConfig = redisProtocolUrl || { redis: opts } let cleanupInternal = null let automationQueue = new Queue(JobQueues.AUTOMATIONS, queueConfig) +listeners.addListeners(automationQueue) async function cleanup() { await automationQueue.clean(CLEANUP_PERIOD_MS, "completed") @@ -51,6 +53,7 @@ exports.shutdown = async () => { await automationQueue.close() automationQueue = null } + console.log("Bull shutdown") } exports.queue = automationQueue diff --git a/packages/server/src/automations/listeners.ts b/packages/server/src/automations/listeners.ts new file mode 100644 index 0000000000..9f8667bd29 --- /dev/null +++ b/packages/server/src/automations/listeners.ts @@ -0,0 +1,78 @@ +import { Queue, Job, JobId } from "bull" +import { AutomationEvent } from "../definitions/automations" +import * as automation from "../threads/automation" + +export const addListeners = (queue: Queue) => { + logging(queue) + handleStalled(queue) +} + +const handleStalled = (queue: Queue) => { + queue.on("stalled", async (job: Job) => { + await automation.removeStalled(job as AutomationEvent) + }) +} + +const logging = (queue: Queue) => { + if (process.env.NODE_DEBUG?.includes("bull")) { + queue + .on("error", (error: any) => { + // An error occurred. + console.error(`automation-event=error error=${JSON.stringify(error)}`) + }) + .on("waiting", (jobId: JobId) => { + // A Job is waiting to be processed as soon as a worker is idling. + console.log(`automation-event=waiting jobId=${jobId}`) + }) + .on("active", (job: Job, jobPromise: any) => { + // A job has started. You can use `jobPromise.cancel()`` to abort it. + console.log(`automation-event=active jobId=${job.id}`) + }) + .on("stalled", (job: Job) => { + // A job has been marked as stalled. This is useful for debugging job + // workers that crash or pause the event loop. + console.error( + `automation-event=stalled jobId=${job.id} job=${JSON.stringify(job)}` + ) + }) + .on("progress", (job: Job, progress: any) => { + // A job's progress was updated! + console.log( + `automation-event=progress jobId=${job.id} progress=${progress}` + ) + }) + .on("completed", (job: Job, result) => { + // A job successfully completed with a `result`. + console.log( + `automation-event=completed jobId=${job.id} result=${result}` + ) + }) + .on("failed", (job, err: any) => { + // A job failed with reason `err`! + console.log(`automation-event=failed jobId=${job.id} error=${err}`) + }) + .on("paused", () => { + // The queue has been paused. + console.log(`automation-event=paused`) + }) + .on("resumed", (job: Job) => { + // The queue has been resumed. + console.log(`automation-event=paused jobId=${job.id}`) + }) + .on("cleaned", (jobs: Job[], type: string) => { + // Old jobs have been cleaned from the queue. `jobs` is an array of cleaned + // jobs, and `type` is the type of jobs cleaned. + console.log( + `automation-event=cleaned length=${jobs.length} type=${type}` + ) + }) + .on("drained", () => { + // Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed) + console.log(`automation-event=drained`) + }) + .on("removed", (job: Job) => { + // A job successfully removed. + console.log(`automation-event=removed jobId=${job.id}`) + }) + } +} diff --git a/packages/server/src/automations/steps/queryRows.js b/packages/server/src/automations/steps/queryRows.js index b02f31b1ec..cfff039221 100644 --- a/packages/server/src/automations/steps/queryRows.js +++ b/packages/server/src/automations/steps/queryRows.js @@ -125,6 +125,14 @@ const hasNullFilters = filters => exports.run = async function ({ inputs, appId }) { const { tableId, filters, sortColumn, sortOrder, limit } = inputs + if (!tableId) { + return { + success: false, + response: { + message: "You must select a table to query.", + }, + } + } const table = await getTable(appId, tableId) let sortType = FieldTypes.STRING if (table && table.schema && table.schema[sortColumn] && sortColumn) { diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 1799b4d74d..e0979ac0d9 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -21,11 +21,13 @@ const WH_STEP_ID = definitions.WEBHOOK.stepId const CRON_STEP_ID = definitions.CRON.stepId const Runner = new Thread(ThreadType.AUTOMATION) +const jobMessage = (job: any, message: string) => { + return `app=${job.data.event.appId} automation=${job.data.automation._id} jobId=${job.id} trigger=${job.data.automation.definition.trigger.event} : ${message}` +} + export async function processEvent(job: any) { try { - console.log( - `${job.data.automation.appId} automation ${job.data.automation._id} running` - ) + console.log(jobMessage(job, "running")) // need to actually await these so that an error can be captured properly const tenantId = tenancy.getTenantIDFromAppID(job.data.event.appId) return await tenancy.doInTenant(tenantId, async () => { @@ -34,9 +36,7 @@ export async function processEvent(job: any) { }) } catch (err) { const errJson = JSON.stringify(err) - console.error( - `${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${errJson}` - ) + console.error(jobMessage(job, `was unable to run - ${errJson}`)) console.trace(err) return { err } } @@ -91,6 +91,7 @@ export async function disableAllCrons(appId: any) { export async function disableCron(jobId: string, jobKey: string) { await queue.removeRepeatableByKey(jobKey) await queue.removeJobs(jobId) + console.log(`jobId=${jobId} disabled`) } export async function clearMetadata() { diff --git a/packages/server/src/db/dynamoClient.js b/packages/server/src/db/dynamoClient.js index 58e469f80d..12e53ff1fd 100644 --- a/packages/server/src/db/dynamoClient.js +++ b/packages/server/src/db/dynamoClient.js @@ -103,11 +103,9 @@ class Table { exports.init = endpoint => { let AWS = require("aws-sdk") - AWS.config.update({ - region: AWS_REGION, - }) let docClientParams = { correctClockSkew: true, + region: AWS_REGION, } if (endpoint) { docClientParams.endpoint = endpoint diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js index c2e2815e00..7f929f79e1 100644 --- a/packages/server/src/environment.js +++ b/packages/server/src/environment.js @@ -13,10 +13,7 @@ function isJest() { } function isDev() { - return ( - process.env.NODE_ENV !== "production" && - process.env.BUDIBASE_ENVIRONMENT !== "production" - ) + return process.env.NODE_ENV !== "production" } function isCypress() { diff --git a/packages/server/src/events/AutomationEmitter.js b/packages/server/src/events/AutomationEmitter.js index fbfc445e2c..99345228ff 100644 --- a/packages/server/src/events/AutomationEmitter.js +++ b/packages/server/src/events/AutomationEmitter.js @@ -1,8 +1,10 @@ const { rowEmission, tableEmission } = require("./utils") const mainEmitter = require("./index") +const env = require("../environment") // max number of automations that can chain on top of each other -const MAX_AUTOMATION_CHAIN = 5 +// TODO: in future make this configurable at the automation level +const MAX_AUTOMATION_CHAIN = env.SELF_HOSTED ? 5 : 0 /** * Special emitter which takes the count of automation runs which have occurred and blocks an diff --git a/packages/server/src/integrations/dynamodb.ts b/packages/server/src/integrations/dynamodb.ts index 5321da4791..78d4ff1447 100644 --- a/packages/server/src/integrations/dynamodb.ts +++ b/packages/server/src/integrations/dynamodb.ts @@ -13,7 +13,8 @@ module DynamoModule { region: string accessKeyId: string secretAccessKey: string - endpoint: string + endpoint?: string + currentClockSkew?: boolean } const SCHEMA: Integration = { @@ -132,31 +133,20 @@ module DynamoModule { constructor(config: DynamoDBConfig) { this.config = config - if (this.config.endpoint && !this.config.endpoint.includes("localhost")) { - this.connect() + + // User is using a local dynamoDB endpoint, don't auth with remote + if (this.config?.endpoint?.includes("localhost")) { + // @ts-ignore + this.config = {} } - let options = { - correctClockSkew: true, - region: this.config.region || AWS_REGION, - endpoint: config.endpoint ? config.endpoint : undefined, + + this.config = { + ...this.config, + currentClockSkew: true, + region: config.region || AWS_REGION, + endpoint: config.endpoint || undefined, } - this.client = new AWS.DynamoDB.DocumentClient(options) - } - - end() { - this.disconnect() - } - - connect() { - AWS.config.update(this.config) - } - - disconnect() { - AWS.config.update({ - secretAccessKey: undefined, - accessKeyId: undefined, - region: AWS_REGION, - }) + this.client = new AWS.DynamoDB.DocumentClient(this.config) } async create(query: { table: string; json: object }) { @@ -197,7 +187,7 @@ module DynamoModule { const params = { TableName: query.table, } - return new AWS.DynamoDB().describeTable(params).promise() + return new AWS.DynamoDB(this.config).describeTable(params).promise() } async get(query: { table: string; json: object }) { diff --git a/packages/server/src/integrations/mongodb.ts b/packages/server/src/integrations/mongodb.ts index 4f2a901259..9f1d41d2ec 100644 --- a/packages/server/src/integrations/mongodb.ts +++ b/packages/server/src/integrations/mongodb.ts @@ -93,8 +93,8 @@ module MongoDBModule { json[field] = self.createObjectIds(json[field]) } if ( - (field === "_id" || field?.startsWith("$")) && - typeof json[field] === "string" + typeof json[field] === "string" && + json[field].toLowerCase().startsWith("objectid") ) { const id = json[field].match( /(?<=objectid\(['"]).*(?=['"]\))/gi diff --git a/packages/server/src/integrations/tests/dynamodb.spec.js b/packages/server/src/integrations/tests/dynamodb.spec.js index 4c6b931090..198ed6a4b4 100644 --- a/packages/server/src/integrations/tests/dynamodb.spec.js +++ b/packages/server/src/integrations/tests/dynamodb.spec.js @@ -100,4 +100,52 @@ describe("DynamoDB Integration", () => { Name: "John" }) }) + + it("configures the dynamoDB constructor based on an empty endpoint parameter", async () => { + const config = { + region: "us-east-1", + accessKeyId: "test", + secretAccessKeyId: "test" + } + + const integration = new DynamoDBIntegration.integration(config) + + expect(integration.config).toEqual({ + currentClockSkew: true, + ...config + }) + }) + + it("configures the dynamoDB constructor based on a localhost endpoint parameter", async () => { + const config = { + region: "us-east-1", + accessKeyId: "test", + secretAccessKeyId: "test", + endpoint: "localhost:8080" + } + + const integration = new DynamoDBIntegration.integration(config) + + expect(integration.config).toEqual({ + region: "us-east-1", + currentClockSkew: true, + endpoint: "localhost:8080" + }) + }) + + it("configures the dynamoDB constructor based on a remote endpoint parameter", async () => { + const config = { + region: "us-east-1", + accessKeyId: "test", + secretAccessKeyId: "test", + endpoint: "dynamodb.aws.foo.net" + } + + const integration = new DynamoDBIntegration.integration(config) + + expect(integration.config).toEqual({ + currentClockSkew: true, + ...config + }) + }) }) \ No newline at end of file diff --git a/packages/server/src/integrations/tests/mongo.spec.js b/packages/server/src/integrations/tests/mongo.spec.js index 9687723528..40aa6dbb58 100644 --- a/packages/server/src/integrations/tests/mongo.spec.js +++ b/packages/server/src/integrations/tests/mongo.spec.js @@ -103,16 +103,16 @@ describe("MongoDB Integration", () => { restore() }) - it("creates ObjectIds if the _id fields contains a match on ObjectId", async () => { + it("creates ObjectIds if the field contains a match on ObjectId", async () => { const query = { json: { filter: { _id: "ObjectId('ACBD12345678ABCD12345678')", - name: "ObjectId('name')" + name: "ObjectId('BBBB12345678ABCD12345678')" }, update: { _id: "ObjectId('FFFF12345678ABCD12345678')", - name: "ObjectId('updatedName')", + name: "ObjectId('CCCC12345678ABCD12345678')", }, options: { upsert: false, @@ -126,11 +126,11 @@ describe("MongoDB Integration", () => { const args = config.integration.client.updateOne.mock.calls[0] expect(args[0]).toEqual({ _id: mongo.ObjectID.createFromHexString("ACBD12345678ABCD12345678"), - name: "ObjectId('name')", + name: mongo.ObjectID.createFromHexString("BBBB12345678ABCD12345678"), }) expect(args[1]).toEqual({ _id: mongo.ObjectID.createFromHexString("FFFF12345678ABCD12345678"), - name: "ObjectId('updatedName')", + name: mongo.ObjectID.createFromHexString("CCCC12345678ABCD12345678"), }) expect(args[2]).toEqual({ upsert: false diff --git a/packages/server/src/middleware/joi-validator.js b/packages/server/src/middleware/joi-validator.js index 748ccebd89..6812dbdd54 100644 --- a/packages/server/src/middleware/joi-validator.js +++ b/packages/server/src/middleware/joi-validator.js @@ -13,10 +13,13 @@ function validate(schema, property) { params = ctx.request[property] } - schema = schema.append({ - createdAt: Joi.any().optional(), - updatedAt: Joi.any().optional(), - }) + // not all schemas have the append property e.g. array schemas + if (schema.append) { + schema = schema.append({ + createdAt: Joi.any().optional(), + updatedAt: Joi.any().optional(), + }) + } const { error } = schema.validate(params) if (error) { diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index d04c49ce79..3136155869 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -133,27 +133,34 @@ class Orchestrator { return metadata } + async stopCron(reason: string) { + if (!this._repeat) { + return + } + logWarn( + `CRON disabled reason=${reason} - ${this._appId}/${this._automation._id}` + ) + const automation = this._automation + const trigger = automation.definition.trigger + await disableCron(this._repeat?.jobId, this._repeat?.jobKey) + this.updateExecutionOutput( + trigger.id, + trigger.stepId, + {}, + { + status: AutomationStatus.STOPPED_ERROR, + success: false, + } + ) + await storeLog(automation, this.executionOutput) + } + async checkIfShouldStop(metadata: AutomationMetadata): Promise { if (!metadata.errorCount || !this._repeat) { return false } - const automation = this._automation - const trigger = automation.definition.trigger if (metadata.errorCount >= MAX_AUTOMATION_RECURRING_ERRORS) { - logWarn( - `CRON disabled due to errors - ${this._appId}/${this._automation._id}` - ) - await disableCron(this._repeat?.jobId, this._repeat?.jobKey) - this.updateExecutionOutput( - trigger.id, - trigger.stepId, - {}, - { - status: AutomationStatus.STOPPED_ERROR, - success: false, - } - ) - await storeLog(automation, this.executionOutput) + await this.stopCron("errors") return true } return false @@ -465,3 +472,15 @@ export function execute(input: AutomationEvent, callback: WorkerCallback) { } }) } + +export const removeStalled = async (input: AutomationEvent) => { + const appId = input.data.event.appId + await doInAppContext(appId, async () => { + const automationOrchestrator = new Orchestrator( + input.data.automation, + input.data.event, + input.opts + ) + await automationOrchestrator.stopCron("stalled") + }) +} diff --git a/packages/server/src/threads/index.ts b/packages/server/src/threads/index.ts index f112fdca5e..cee85e2815 100644 --- a/packages/server/src/threads/index.ts +++ b/packages/server/src/threads/index.ts @@ -106,5 +106,6 @@ export class Thread { static async shutdown() { await Thread.stopThreads() + console.log("Threads shutdown") } } diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index 1223ea55f0..02aa2e5c2f 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -287,13 +287,18 @@ exports.getComponentLibraryManifest = async library => { } let resp + let path try { // Try to load the manifest from the new file location - const path = join(appId, filename) + path = join(appId, filename) resp = await retrieve(ObjectStoreBuckets.APPS, path) } catch (error) { + console.error( + `component-manifest-objectstore=failed appId=${appId} path=${path}`, + error + ) // Fallback to loading it from the old location for old apps - const path = join(appId, "node_modules", library, "package", filename) + path = join(appId, "node_modules", library, "package", filename) resp = await retrieve(ObjectStoreBuckets.APPS, path) } if (typeof resp !== "string") { diff --git a/packages/server/src/utilities/queue/inMemoryQueue.js b/packages/server/src/utilities/queue/inMemoryQueue.js index 620b65cf38..79781f9283 100644 --- a/packages/server/src/utilities/queue/inMemoryQueue.js +++ b/packages/server/src/utilities/queue/inMemoryQueue.js @@ -113,6 +113,10 @@ class InMemoryQueue { async getJob() { return {} } + + on() { + // do nothing + } } module.exports = InMemoryQueue diff --git a/packages/server/src/utilities/redis.js b/packages/server/src/utilities/redis.js index 0c25bab27a..4eddca6e4a 100644 --- a/packages/server/src/utilities/redis.js +++ b/packages/server/src/utilities/redis.js @@ -20,6 +20,7 @@ exports.shutdown = async () => { if (devAppClient) await devAppClient.finish() if (debounceClient) await debounceClient.finish() if (flagClient) await flagClient.finish() + console.log("Redis shutdown") } exports.doesUserHaveLock = async (devAppId, user) => { diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index b433988f24..19c3977f67 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1094,12 +1094,12 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.2.41-alpha.5": - version "1.2.41-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.41-alpha.5.tgz#e03c9970f9eadcdfe9146ffe4d7f29174648675a" - integrity sha512-kB+S3pu4F9wNMwzdBSF4auZEAqQ05VlhEA/cjLyb54zjxrJmpGv761I6vAM4BptvjpSNCGtiv6HrpGaOILRDDQ== +"@budibase/backend-core@1.2.58-alpha.5": + version "1.2.58-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.58-alpha.5.tgz#430699334629b3bd05a5066aa6660c1f68f047b2" + integrity sha512-Z926rAp0eskXUr5UXGwh0S1OMDA3qzIi8o0GHLlO7KZAzk49RYzhRT/yo1fIst8Eq/9ZCaXLFxIeRkDmCJgILg== dependencies: - "@budibase/types" "1.2.41-alpha.5" + "@budibase/types" "1.2.58-alpha.5" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" bcrypt "5.0.1" @@ -1178,13 +1178,13 @@ svelte-flatpickr "^3.2.3" svelte-portal "^1.0.0" -"@budibase/pro@1.2.41-alpha.5": - version "1.2.41-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.41-alpha.5.tgz#43362d0e9bca4dbb8ab93cfe3daf821b04f1ee95" - integrity sha512-p/Vvs2xOWlQlLYJ8sF/PWI2ki/B0uy0sQPwUUld4qQRHXa+YevXJgnjZPa3v7q2NWQ1vxov2K8g8y6Z5mv9XAw== +"@budibase/pro@1.2.58-alpha.5": + version "1.2.58-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.58-alpha.5.tgz#1f739653e641ac32d20818517d35bef489ec2ae6" + integrity sha512-owT7ipgZ588KAz6aHPJie5bLJX6EkhTRUvUQlyOJuLWt/hdXXnvbu+agxBQNaXE4gVzn916JJP0L0tbiMctjNA== dependencies: - "@budibase/backend-core" "1.2.41-alpha.5" - "@budibase/types" "1.2.41-alpha.5" + "@budibase/backend-core" "1.2.58-alpha.5" + "@budibase/types" "1.2.58-alpha.5" "@koa/router" "8.0.8" joi "17.6.0" node-fetch "^2.6.1" @@ -1207,10 +1207,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@1.2.41-alpha.5": - version "1.2.41-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.41-alpha.5.tgz#0ade282c2e4ec3e4a1a4212403a1d0c3da80521b" - integrity sha512-Awya83t8bDeavc0XSglYVc4sStDheUKsAQWzTpgcRaB5l7NwhHQG5xlWsbFT8glolYY96ogLZJjLFTzIur3iDQ== +"@budibase/types@1.2.58-alpha.5": + version "1.2.58-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.58-alpha.5.tgz#8ea99745f296a1076f878a06b7c228bc60886cd3" + integrity sha512-0JhHDGvfcMcjhBckP3RqbKCDsMhx7ZVTV8wvaqsOaG7PnBSst9dxyX7ETVVzfvYolIK1eV11itsgx0FpbYID2w== "@bull-board/api@3.7.0": version "3.7.0" @@ -1304,6 +1304,20 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@elastic/ecs-helpers@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@elastic/ecs-helpers/-/ecs-helpers-1.1.0.tgz#ee7e6f870f75a2222c5d7179b36a628f1db4779e" + integrity sha512-MDLb2aFeGjg46O5mLpdCzT5yOUDnXToJSrco2ShqGIXxNJaM8uJjX+4nd+hRYV4Vex8YJyDtOFEVBldQct6ndg== + dependencies: + fast-json-stringify "^2.4.1" + +"@elastic/ecs-pino-format@^1.2.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@elastic/ecs-pino-format/-/ecs-pino-format-1.3.0.tgz#6e349a7da342b3c370d15361ba7f850bc2f783bc" + integrity sha512-U8D57gPECYoRCcwREsrXKBtqeyFFF/KAwHi4rG1u/oQhAg91Kzw8ZtUQJXD/DMDieLOqtbItFr2FRBWI3t3wog== + dependencies: + "@elastic/ecs-helpers" "^1.1.0" + "@elastic/elasticsearch@7.10.0": version "7.10.0" resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0.tgz#da105a9c1f14146f9f2cab4e7026cb7949121b8d" @@ -2026,6 +2040,36 @@ semver "^7.3.5" tar "^6.1.11" +"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz#9571b87be3a3f2c46de05585470bc4f3af2f6f00" + integrity sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.1.2.tgz#bfbc6936ede2955218f5621a675679a5fe8e6f4c" + integrity sha512-YPXtcVkhmVNoMGlqp81ZHW4dMxK09msWgnxtsDpSiZwTzUBG2N+No2bsr7WMtBKCVJMSD6mbAl7YhKUqkp/Few== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.1.2.tgz#22555e28382af2922e7450634c8a2f240bb9eb82" + integrity sha512-vHZ2JiOWF2+DN9lzltGbhtQNzDo8fKFGrf37UJrgqxU0yvtERrzUugnfnX1wmVfFhSsF8OxrfqiNOUc5hko1Zg== + +"@msgpackr-extract/msgpackr-extract-linux-arm@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.1.2.tgz#ffb6ae1beea7ac572b6be6bf2a8e8162ebdd8be7" + integrity sha512-42R4MAFeIeNn+L98qwxAt360bwzX2Kf0ZQkBBucJ2Ircza3asoY4CDbgiu9VWklq8gWJVSJSJBwDI+c/THiWkA== + +"@msgpackr-extract/msgpackr-extract-linux-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.1.2.tgz#7caf62eebbfb1345de40f75e89666b3d4194755f" + integrity sha512-RjRoRxg7Q3kPAdUSC5EUUPlwfMkIVhmaRTIe+cqHbKrGZ4M6TyCA/b5qMaukQ/1CHWrqYY2FbKOAU8Hg0pQFzg== + +"@msgpackr-extract/msgpackr-extract-win32-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz#f2d8b9ddd8d191205ed26ce54aba3dfc5ae3e7c9" + integrity sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2047,7 +2091,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@opentelemetry/api@^1.0.1": +"@opentelemetry/api@^1.0.1", "@opentelemetry/api@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.1.0.tgz#563539048255bbe1a5f4f586a4a10a1bb737f44a" integrity sha512-hf+3bwuBwtXsugA2ULBc95qxrOqP2pOekLz34BJhcAKawt94vfeNyUKpYc0lZQ/3sCP6LqRa7UAdHA7i5UODzQ== @@ -3327,6 +3371,11 @@ adal-node@^0.2.2: uuid "^3.1.0" xpath.js "~1.1.0" +after-all-results@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/after-all-results/-/after-all-results-2.0.0.tgz#6ac2fc202b500f88da8f4f5530cfa100f4c6a2d0" + integrity sha512-2zHEyuhSJOuCrmas9YV0YL/MFCWLxe1dS6k/ENhgYrb/JqyMnadLN4iIAc9kkZrbElMDyyAGH/0J18OPErOWLg== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -3334,6 +3383,15 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" +agentkeepalive@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + airtable@0.10.1: version "0.10.1" resolved "https://registry.yarnpkg.com/airtable/-/airtable-0.10.1.tgz#0b311002bb44b39f19bf7c4bd2d47d75c733bf87" @@ -3357,7 +3415,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.5: +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.3, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3666,11 +3724,30 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async-cache@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/async-cache/-/async-cache-1.1.0.tgz#4a9a5a89d065ec5d8e5254bd9ee96ba76c532b5a" + integrity sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g== + dependencies: + lru-cache "^4.0.0" + async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-value-promise@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/async-value-promise/-/async-value-promise-1.1.1.tgz#68957819e3eace804f3b4b69477e2bd276c15378" + integrity sha512-c2RFDKjJle1rHa0YxN9Ysu97/QBu3Wa+NOejJxsX+1qVDJrkD3JL/GN1B3gaILAEXJXbu/4Z1lcoCHFESe/APA== + dependencies: + async-value "^1.2.2" + +async-value@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/async-value/-/async-value-1.2.2.tgz#84517a1e7cb6b1a5b5e181fa31be10437b7fb125" + integrity sha512-8rwtYe32OAS1W9CTwvknoyts+mc3ta8N7Pi0h7AjkMaKvsFbr39K+gEfZ7Z81aPXQ1sK5M23lgLy1QfZpcpadQ== + async@^2.6.3: version "2.6.4" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" @@ -3961,6 +4038,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -4006,6 +4090,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binary-search@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" + integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== + binascii@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/binascii/-/binascii-0.0.2.tgz#a7f8a8801dbccf8b1756b743daa0fee9e2d9e0ee" @@ -4127,6 +4216,13 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +breadth-filter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/breadth-filter/-/breadth-filter-2.0.0.tgz#7b3f8737f46ba1946aec19355ecf5df2bdb7e47c" + integrity sha512-thQShDXnFWSk2oVBixRCyrWsFoV5tfOpWKHmxwafHQDNxCfDBk539utpvytNjmlFrTMqz41poLwJvA1MW3z0MQ== + dependencies: + object.entries "^1.0.4" + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -4246,20 +4342,19 @@ buffer@^5.1.0, buffer@^5.2.0, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" -bull@3.29.3: - version "3.29.3" - resolved "https://registry.yarnpkg.com/bull/-/bull-3.29.3.tgz#5b0059b172685b0d6f011d56214e1898ff3a7a0b" - integrity sha512-MOqV1dKLy1YQgP9m3lFolyMxaU+1+o4afzYYf0H4wNM+x/S0I1QPQfkgGlLiH00EyFrvSmeubeCYFP47rTfpjg== +bull@4.8.5: + version "4.8.5" + resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.5.tgz#eebafddc3249d6d5e8ced1c42b8bfa8efcc274aa" + integrity sha512-2Z630e4f6VsLJnWMAtfEHwIqJYmND4W3dcG48RIbXeWpvb4UnYtpe/zxEdslJu0PKrltB4IkFj5YtBsdeQRn8w== dependencies: - cron-parser "^2.13.0" + cron-parser "^4.2.1" debuglog "^1.0.0" get-port "^5.1.1" - ioredis "^4.27.0" + ioredis "^4.28.5" lodash "^4.17.21" + msgpackr "^1.5.2" p-timeout "^3.2.0" - promise.prototype.finally "^3.1.2" semver "^7.3.2" - util.promisify "^1.0.1" uuid "^8.3.0" bytes@3.1.2, bytes@^3.0.0: @@ -4726,6 +4821,11 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== +console-log-level@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" + integrity sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ== + consolidate@^0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" @@ -4733,6 +4833,11 @@ consolidate@^0.16.0: dependencies: bluebird "^3.7.2" +container-info@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/container-info/-/container-info-1.1.0.tgz#6fcb94e93eacd397c6316ca2834491ede44e55ee" + integrity sha512-eD2zLAmxGS2kmL4f1jY8BdOqnmpL6X70kvzTBW/9FIQnxoxiBJ4htMsTmtPLPWRs7NHYFvqKQ1VtppV08mdsQA== + content-disposition@^0.5.2, content-disposition@~0.5.2: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -4757,6 +4862,11 @@ cookie@^0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cookiejar@^2.1.0: version "2.1.3" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" @@ -4826,13 +4936,12 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cron-parser@^2.13.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf" - integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg== +cron-parser@^4.2.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.6.0.tgz#404c3fdbff10ae80eef6b709555d577ef2fd2e0d" + integrity sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA== dependencies: - is-nan "^1.3.0" - moment-timezone "^0.5.31" + luxon "^3.0.1" cross-spawn@^4.0.0: version "4.0.2" @@ -5380,6 +5489,60 @@ ejs@^3.1.6: dependencies: jake "^10.8.5" +elastic-apm-http-client@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-11.0.1.tgz#15dbe99d56d62b3f732d1bd2b51bef6094b78801" + integrity sha512-5AOWlhs2WlZpI+DfgGqY/8Rk7KF8WeevaO8R961eBylavU6GWhLRNiJncohn5jsvrqhmeT19azBvy/oYRN7bJw== + dependencies: + agentkeepalive "^4.2.1" + breadth-filter "^2.0.0" + container-info "^1.0.1" + end-of-stream "^1.4.4" + fast-safe-stringify "^2.0.7" + fast-stream-to-buffer "^1.0.0" + object-filter-sequence "^1.0.0" + readable-stream "^3.4.0" + semver "^6.3.0" + stream-chopper "^3.0.1" + +elastic-apm-node@3.38.0: + version "3.38.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.38.0.tgz#4d0dc9279c0e23e09b3b30aa4a9f9aeccfa59cc0" + integrity sha512-/d6YuWFtsfkVRpFD0YJ2rYJVq0rI0PGqG/C+cW1JpMZ4IOU8dA9xzUkxbT0G3B8gpHNug07Xo6bJdQa2oUaFbQ== + dependencies: + "@elastic/ecs-pino-format" "^1.2.0" + "@opentelemetry/api" "^1.1.0" + after-all-results "^2.0.0" + async-cache "^1.1.0" + async-value-promise "^1.1.1" + basic-auth "^2.0.1" + cookie "^0.5.0" + core-util-is "^1.0.2" + elastic-apm-http-client "11.0.1" + end-of-stream "^1.4.4" + error-callsites "^2.0.4" + error-stack-parser "^2.0.6" + escape-string-regexp "^4.0.0" + fast-safe-stringify "^2.0.7" + http-headers "^3.0.2" + is-native "^1.0.1" + lru-cache "^6.0.0" + measured-reporting "^1.51.1" + monitor-event-loop-delay "^1.0.0" + object-filter-sequence "^1.0.0" + object-identity-map "^1.0.2" + original-url "^1.2.3" + pino "^6.11.2" + relative-microtime "^2.0.0" + require-in-the-middle "^5.0.3" + semver "^6.3.0" + set-cookie-serde "^1.0.0" + shallow-clone-shim "^2.0.0" + source-map "^0.8.0-beta.0" + sql-summary "^1.0.1" + traverse "^0.6.6" + unicode-byte-truncate "^1.0.0" + electron-to-chromium@^1.4.147: version "1.4.150" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.150.tgz#89f0e12505462d5df7e56c5b91aff7e1dfdd33ec" @@ -5432,7 +5595,7 @@ encoding-down@^6.3.0: level-codec "^9.0.0" level-errors "^2.0.0" -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -5471,6 +5634,11 @@ errno@~0.1.1, errno@~0.1.7: dependencies: prr "~1.0.1" +error-callsites@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.4.tgz#44f09e6a201e9a1603ead81eacac5ba258fca76e" + integrity sha512-V877Ch4FC4FN178fDK1fsrHN4I1YQIBdtjKrHh3BUHMnh3SMvwUVrqkaOgDpUuevgSNna0RBq6Ox9SGlxYrigA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -5483,6 +5651,13 @@ error-inject@^1.0.0: resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" integrity sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg== +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + es-abstract@^1.17.5, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0, es-abstract@^1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" @@ -6109,6 +6284,16 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-json-stringify@^2.4.1: + version "2.7.13" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz#277aa86c2acba4d9851bd6108ed657aa327ed8c0" + integrity sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA== + dependencies: + ajv "^6.11.0" + deepmerge "^4.2.2" + rfdc "^1.2.0" + string-similarity "^4.0.1" + fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -6124,6 +6309,13 @@ fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-stream-to-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stream-to-buffer/-/fast-stream-to-buffer-1.0.0.tgz#793340cc753e7ec9c7fb6d57a53a0b911cb0f588" + integrity sha512-bI/544WUQlD2iXBibQbOMSmG07Hay7YrpXlKaeGTPT7H7pC0eitt3usak5vUwEvCGK/O7rUAM3iyQValGU22TQ== + dependencies: + end-of-stream "^1.4.1" + fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" @@ -6411,6 +6603,11 @@ formidable@^1.1.1, formidable@^1.2.0: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +forwarded-parse@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" + integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -7156,6 +7353,13 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" +http-headers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/http-headers/-/http-headers-3.0.2.tgz#5147771292f0b39d6778d930a3a59a76fc7ef44d" + integrity sha512-87E1I+2Wg4dxxz4rcxElo3dxO/w1ZtgL1yA0Sb6vH3qU16vRKq1NjWQv9SCY3ly2OQROcoxHZOUpmelS+k6wOw== + dependencies: + next-line "^1.1.0" + http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -7187,6 +7391,13 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.5: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -7397,7 +7608,7 @@ ioredis@4.28.0: redis-parser "^3.0.0" standard-as-callback "^2.1.0" -ioredis@^4.27.0: +ioredis@^4.28.5: version "4.28.5" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f" integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== @@ -7502,6 +7713,13 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -7563,6 +7781,11 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -7605,13 +7828,20 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-nan@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== +is-integer@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" + integrity sha512-RPQc/s9yBHSvpi+hs9dYiJ2cuFeU6x3TyyIp8O2H6SKEltIvJOzRj9ToyvcStDvPR/pS4rxgr1oBFajQjZ2Szg== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" + is-finite "^1.0.0" + +is-native@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-native/-/is-native-1.0.1.tgz#cd18cc162e8450d683b5babe79ac99c145449675" + integrity sha512-I4z9hx+4u3/zyvpvGtAR+n7SodJugE+i2jiS8yfq1A9QAZY0KldLQz0SBptLC9ti7kBlpghWUwTKE2BA62eCcw== + dependencies: + is-nil "^1.0.0" + to-source-code "^1.0.0" is-natural-number@^4.0.1: version "4.0.1" @@ -7623,6 +7853,11 @@ is-negative-zero@^2.0.2: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-nil@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-nil/-/is-nil-1.0.1.tgz#2daba29e0b585063875e7b539d071f5b15937969" + integrity sha512-m2Rm8PhUFDNNhgvwZJjJG74a9h5CHU0fkA8WT+WGlCjyEbZ2jPwgb+ZxHu4np284EqNVyOsgppReK4qy/TwEwg== + is-npm@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" @@ -9628,7 +9863,7 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^4.0.1, lru-cache@^4.1.3, lru-cache@^4.1.5: +lru-cache@^4.0.0, lru-cache@^4.0.1, lru-cache@^4.1.3, lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -9653,6 +9888,11 @@ ltgt@2.2.1, ltgt@^2.1.2, ltgt@~2.2.0: resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA== +luxon@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.0.1.tgz#6901111d10ad06fd267ad4e4128a84bef8a77299" + integrity sha512-hF3kv0e5gwHQZKz4wtm4c+inDtyc7elkanAsBq+fundaCdUBNJB1dHEGUZIM6SfSBUlbVFduPwEtNjFK8wLtcw== + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -9706,6 +9946,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +mapcap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mapcap/-/mapcap-1.0.0.tgz#e8e29d04a160eaf8c92ec4bcbd2c5d07ed037e5a" + integrity sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g== + markdown-it@^12.2.0: version "12.3.2" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" @@ -9738,6 +9983,24 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== +measured-core@^1.51.1: + version "1.51.1" + resolved "https://registry.yarnpkg.com/measured-core/-/measured-core-1.51.1.tgz#98989705c00bfb0d8a20e665a9f8d6e246a40518" + integrity sha512-DZQP9SEwdqqYRvT2slMK81D/7xwdxXosZZBtLVfPSo6y5P672FBTbzHVdN4IQyUkUpcVOR9pIvtUy5Ryl7NKyg== + dependencies: + binary-search "^1.3.3" + optional-js "^2.0.0" + +measured-reporting@^1.51.1: + version "1.51.1" + resolved "https://registry.yarnpkg.com/measured-reporting/-/measured-reporting-1.51.1.tgz#6aeb209ad55edf3940e8afa75c8f97f541216b31" + integrity sha512-JCt+2u6XT1I5lG3SuYqywE0e62DJuAzBcfMzWGUhIYtPQV2Vm4HiYt/durqmzsAbZV181CEs+o/jMKWJKkYIWw== + dependencies: + console-log-level "^1.4.1" + mapcap "^1.0.0" + measured-core "^1.51.1" + optional-js "^2.0.0" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -9935,19 +10198,19 @@ mock-require@^3.0.3: get-caller-file "^1.0.2" normalize-path "^2.1.1" -moment-timezone@^0.5.15, moment-timezone@^0.5.31: - version "0.5.34" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" - integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + +moment-timezone@^0.5.15: + version "0.5.37" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.37.tgz#adf97f719c4e458fdb12e2b4e87b8bec9f4eef1e" + integrity sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg== dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0": - version "2.29.3" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" - integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== - -moment@^2.29.3: +"moment@>= 2.9.0", moment@^2.29.3: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== @@ -9965,6 +10228,11 @@ mongodb@3.6.3: optionalDependencies: saslprep "^1.0.0" +monitor-event-loop-delay@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/monitor-event-loop-delay/-/monitor-event-loop-delay-1.0.0.tgz#b5ab78165a3bb93f2b275c50d01430c7f155d1f7" + integrity sha512-YRIr1exCIfBDLZle8WHOfSo7Xg3M+phcZfq9Fx1L6Abo+atGp7cge5pM7PjyBn4s1oZI/BRD4EMrzQBbPpVb5Q== + mri@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" @@ -9980,11 +10248,32 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1, ms@^2.1.3: +ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract@^2.0.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.1.2.tgz#56272030f3e163e1b51964ef8b1cd5e7240c03ed" + integrity sha512-cmrmERQFb19NX2JABOGtrKdHMyI6RUyceaPBQ2iRz9GnDkjBWFjNJC0jyyoOfZl2U/LZE3tQCCQc4dlRyA8mcA== + dependencies: + node-gyp-build-optional-packages "5.0.3" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-win32-x64" "2.1.2" + +msgpackr@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.6.2.tgz#176cd9f6b4437dad87a839b37f23c2dfee408d9a" + integrity sha512-bqSQ0DYJbXbrJcrZFmMygUZmqQiDfI2ewFVWcrZY12w5XHWtPuW4WppDT/e63Uu311ajwkRRXSoF0uILroBeTA== + optionalDependencies: + msgpackr-extract "^2.0.2" + mssql@6.2.3: version "6.2.3" resolved "https://registry.yarnpkg.com/mssql/-/mssql-6.2.3.tgz#1d15bbe8c3057e32ee6e98596b6c323b097a6cba" @@ -10099,6 +10388,11 @@ neo-async@^2.6.0, neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-line@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603" + integrity sha512-+I10J3wKNoKddNxn0CNpoZ3eTZuqxjNM3b1GImVx22+ePI+Y15P8g/j3WsbP0fhzzrFzrtjOAoq5NCCucswXOQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -10131,6 +10425,11 @@ node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-gyp-build-optional-packages@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" + integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== + node-gyp-build@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" @@ -10303,11 +10602,23 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-filter-sequence@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object-filter-sequence/-/object-filter-sequence-1.0.0.tgz#10bb05402fff100082b80d7e83991b10db411692" + integrity sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ== + object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== +object-identity-map@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-identity-map/-/object-identity-map-1.0.2.tgz#2b4213a4285ca3a8cd2e696782c9964f887524e7" + integrity sha512-a2XZDGyYTngvGS67kWnqVdpoaJWsY7C1GhPJvejWAFCsUioTAaiTu8oBad7c6cI4McZxr4CmvnZeycK05iav5A== + dependencies: + object.entries "^1.1.0" + object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" @@ -10335,6 +10646,15 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.entries@^1.0.4, object.entries@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" + integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + object.getownpropertydescriptors@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz#7965e6437a57278b587383831a9b829455a4bc37" @@ -10458,6 +10778,11 @@ openapi-validator@^0.14.2: path-parser "^6.1.0" typeof "^1.0.0" +optional-js@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.3.0.tgz#81d54c4719afa8845b988143643a5148f9d89490" + integrity sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw== + optionator@^0.8.1, optionator@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -10475,6 +10800,13 @@ oracledb@5.3.0: resolved "https://registry.yarnpkg.com/oracledb/-/oracledb-5.3.0.tgz#a15e6cd16757d8711a2c006a28bd7ecd3b8466f7" integrity sha512-HMJzQ6lCf287ztvvehTEmjCWA21FQ3RMvM+mgoqd4i8pkREuqFWO+y3ovsGR9moJUg4T0xjcwS8rl4mggWPxmg== +original-url@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/original-url/-/original-url-1.2.3.tgz#133aff4b2d27e38a98d736f7629c56262b7153e1" + integrity sha512-BYm+pKYLtS4mVe/mgT3YKGtWV5HzN/XKiaIu1aK4rsxyjuHeTW9N+xVBEpJcY1onB3nccfH0RbzUEoimMqFUHQ== + dependencies: + forwarded-parse "^2.1.0" + os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" @@ -10958,7 +11290,7 @@ pino-std-serializers@^4.0.0: resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1791ccd2539c091ae49ce9993205e2cd5dbba1e2" integrity sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q== -pino@^6.13.0: +pino@^6.11.2, pino@^6.13.0: version "6.14.0" resolved "https://registry.yarnpkg.com/pino/-/pino-6.14.0.tgz#b745ea87a99a6c4c9b374e4f29ca7910d4c69f78" integrity sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg== @@ -11354,15 +11686,6 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise.prototype.finally@^3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.3.tgz#d3186e58fcf4df1682a150f934ccc27b7893389c" - integrity sha512-EXRF3fC9/0gz4qkt/f5EP5iW4kj9oFpBICNpCNOb/52+8nlHIX07FPLbi/q4qYBQ1xZqivMzTpNQSnArVASolQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -11579,7 +11902,7 @@ readable-stream@1.1.14, readable-stream@^1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.0.0, readable-stream@^3.0.1, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@^3.0.0, readable-stream@^3.0.1, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -11796,6 +12119,11 @@ regjsparser@^0.8.2: dependencies: jsesc "~0.5.0" +relative-microtime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/relative-microtime/-/relative-microtime-2.0.0.tgz#cceed2af095ecd72ea32011279c79e5fcc7de29b" + integrity sha512-l18ha6HEZc+No/uK4GyAnNxgKW7nvEe35IaeN54sShMojtqik2a6GbTyuiezkjpPaqP874Z3lW5ysBo5irz4NA== + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -11876,6 +12204,15 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-in-the-middle@^5.0.3: + version "5.2.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz#4b71e3cc7f59977100af9beb76bf2d056a5a6de2" + integrity sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.22.1" + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -11948,6 +12285,15 @@ resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.9.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -11981,6 +12327,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -12036,7 +12387,7 @@ safe-buffer@*, safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-b resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -12191,6 +12542,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-serde@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-cookie-serde/-/set-cookie-serde-1.0.0.tgz#bcf9c260ed2212ac4005a53eacbaaa37c07ac452" + integrity sha512-Vq8e5GsupfJ7okHIvEPcfs5neCo7MZ1ZuWrO3sllYi3DOWt6bSSCpADzqXjz3k0fXehnoFIrmmhty9IN6U6BXQ== + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -12211,6 +12567,11 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +shallow-clone-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone-shim/-/shallow-clone-shim-2.0.0.tgz#b62bf55aed79f4c1430ea1dc4d293a193f52cf91" + integrity sha512-YRNymdiL3KGOoS67d73TEmk4tdPTO9GSMCoiphQsTcC9EtC+AOmMPjkyBkRoCJfW9ASsaZw1craaiw1dPN2D3Q== + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -12480,6 +12841,13 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + spark-md5@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.1.tgz#83a0e255734f2ab4e5c466e5a2cfc9ba2aa2124d" @@ -12564,6 +12932,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sql-summary@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sql-summary/-/sql-summary-1.0.1.tgz#a2dddb5435bae294eb11424a7330dc5bafe09c2b" + integrity sha512-IpCr2tpnNkP3Jera4ncexsZUp0enJBLr+pHCyTweMUBrbJsTgQeLWx1FXLhoBj/MvcnUQpkgOn2EY8FKOkUzww== + sqlstring@^2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" @@ -12603,6 +12976,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" @@ -12636,6 +13014,13 @@ step@0.0.x: resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2" integrity sha512-qSSeQinUJk2w38vUFobjFoE307GqsozMC8VisOCkJLpklvKPT0ptPHwWOrENoag8rgLudvTkfP3bancwP93/Jw== +stream-chopper@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/stream-chopper/-/stream-chopper-3.0.1.tgz#73791ae7bf954c297d6683aec178648efc61dd75" + integrity sha512-f7h+ly8baAE26iIjcp3VbnBkbIRGtrvV0X0xxFM/d7fwLTYnLzDPTXRKNxa2HZzohOrc96NTrR+FaV3mzOelNA== + dependencies: + readable-stream "^3.0.6" + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" @@ -12659,7 +13044,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-similarity@^4.0.4: +string-similarity@^4.0.1, string-similarity@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== @@ -13314,6 +13699,13 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-source-code@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-source-code/-/to-source-code-1.0.2.tgz#dd136bdb1e1dbd80bbeacf088992678e9070bfea" + integrity sha512-YzWtjmNIf3E75eZYa7m1SCyl0vgOGoTzdpH3svfa8SUm5rqTgl9hnDolrAGOghCF9P2gsITXQoMrlujOoz+RPw== + dependencies: + is-nil "^1.0.0" + toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -13371,6 +13763,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +traverse@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + integrity sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw== + trim-repeated@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" @@ -13569,6 +13966,14 @@ undici@^4.14.1: resolved "https://registry.yarnpkg.com/undici/-/undici-4.16.0.tgz#469bb87b3b918818d3d7843d91a1d08da357d5ff" integrity sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw== +unicode-byte-truncate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unicode-byte-truncate/-/unicode-byte-truncate-1.0.0.tgz#aa6f0f3475193fe20c320ac9213e36e62e8764a7" + integrity sha512-GQgHk6DodEoKddKQdjnv7xKS9G09XCfHWX0R4RKht+EbUMSiVEmtWHGFO8HUm+6NvWik3E2/DG4MxTitOLL64A== + dependencies: + is-integer "^1.0.6" + unicode-substring "^0.1.0" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -13592,6 +13997,11 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== +unicode-substring@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicode-substring/-/unicode-substring-0.1.0.tgz#6120ce3c390385dbcd0f60c32b9065c4181d4b36" + integrity sha512-36Xaw9wXi7MB/3/EQZZHkZyyiRNa9i3k9YtPAz2KfqMVH2xutdXyMHn4Igarmnvr+wOrfWa/6njhY+jPpXN2EQ== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -13733,7 +14143,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -util.promisify@^1.0.0, util.promisify@^1.0.1: +util.promisify@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.1.1.tgz#77832f57ced2c9478174149cae9b96e9918cd54b" integrity sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw== diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 626be8c83d..7cd3887fc8 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.2.41-alpha.5", + "version": "1.2.58-alpha.5", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 2e8b022017..18930c6dfa 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "1.2.41-alpha.5", + "version": "1.2.58-alpha.5", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/types/src/api/web/errors.ts b/packages/types/src/api/web/errors.ts new file mode 100644 index 0000000000..65870d6a29 --- /dev/null +++ b/packages/types/src/api/web/errors.ts @@ -0,0 +1,5 @@ +export interface APIError { + message: string + status: number + error?: any +} diff --git a/packages/types/src/api/web/index.ts b/packages/types/src/api/web/index.ts index b2258fe18e..0129fb38d9 100644 --- a/packages/types/src/api/web/index.ts +++ b/packages/types/src/api/web/index.ts @@ -1 +1,3 @@ export * from "./analytics" +export * from "./user" +export * from "./errors" diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts new file mode 100644 index 0000000000..b2c17575c2 --- /dev/null +++ b/packages/types/src/api/web/user.ts @@ -0,0 +1,43 @@ +import { User } from "../../documents" + +export interface CreateUserResponse { + _id: string + _rev: string + email: string +} + +export interface BulkCreateUsersRequest { + users: User[] + groups: any[] +} + +export interface UserDetails { + _id: string + email: string +} + +export interface BulkCreateUsersResponse { + successful: UserDetails[] + unsuccessful: { email: string; reason: string }[] +} + +export interface BulkDeleteUsersRequest { + userIds: string[] +} + +export interface BulkDeleteUsersResponse { + successful: UserDetails[] + unsuccessful: { _id: string; email: string; reason: string }[] +} + +export interface InviteUserRequest { + email: string + userInfo: any +} + +export type InviteUsersRequest = InviteUserRequest[] + +export interface InviteUsersResponse { + successful: { email: string }[] + unsuccessful: { email: string; reason: string }[] +} diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts index 1def994697..a03875aa50 100644 --- a/packages/types/src/documents/app/app.ts +++ b/packages/types/src/documents/app/app.ts @@ -14,14 +14,11 @@ export interface App extends Document { tenantId: string status: string theme?: string - customTheme?: { - buttonBorderRadius?: string - primaryColor?: string - primaryColorHover?: string - } + customTheme?: AppCustomTheme revertableVersion?: string navigation?: AppNavigation automationErrors?: AppMetadataErrors + icon?: AppIcon } export interface AppInstance { @@ -47,3 +44,18 @@ export interface AppNavigationLink { id?: string roleId?: string } + +export interface AppCustomTheme { + buttonBorderRadius?: string + primaryColor?: string + primaryColorHover?: string + + // Used to exist before new design UI + navTextColor?: string + navBackground?: string +} + +export interface AppIcon { + name: string + color: string +} diff --git a/packages/types/src/documents/app/layout.ts b/packages/types/src/documents/app/layout.ts index 85ca4b7e94..db046e3d92 100644 --- a/packages/types/src/documents/app/layout.ts +++ b/packages/types/src/documents/app/layout.ts @@ -1,3 +1,5 @@ import { Document } from "../document" -export interface Layout extends Document {} +export interface Layout extends Document { + props: any +} diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts index 1eead16aa0..98db658aa6 100644 --- a/packages/types/src/documents/app/screen.ts +++ b/packages/types/src/documents/app/screen.ts @@ -1,7 +1,7 @@ import { Document } from "../document" export interface Screen extends Document { - layoutId: string + layoutId?: string showNavigation?: boolean width?: string routing: { diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index b900dac5c1..ac16194a21 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -21,3 +21,20 @@ export interface User extends Document { export interface UserRoles { [key: string]: string } + +// utility types + +export interface BuilderUser extends User { + builder: { + global: boolean + } +} + +export interface AdminUser extends User { + admin: { + global: boolean + } + builder: { + global: boolean + } +} diff --git a/packages/types/src/documents/global/userGroup.ts b/packages/types/src/documents/global/userGroup.ts index b822e5bb7b..86010d118b 100644 --- a/packages/types/src/documents/global/userGroup.ts +++ b/packages/types/src/documents/global/userGroup.ts @@ -1,19 +1,20 @@ import { Document } from "../document" -import { User } from "./user" + export interface UserGroup extends Document { name: string icon: string color: string - users: groupUser[] + users: GroupUser[] apps: string[] roles: UserGroupRoles createdAt?: number } -export interface groupUser { +export interface GroupUser { _id: string - email: string[] + email: string } + export interface UserGroupRoles { [key: string]: string } diff --git a/packages/types/src/documents/index.ts b/packages/types/src/documents/index.ts index 4f5b278a4b..47ec48f49c 100644 --- a/packages/types/src/documents/index.ts +++ b/packages/types/src/documents/index.ts @@ -3,3 +3,4 @@ export * from "./app" export * from "./global" export * from "./platform" export * from "./document" +export * from "./pouch" diff --git a/packages/types/src/documents/platform/accounts.ts b/packages/types/src/documents/platform/accounts.ts new file mode 100644 index 0000000000..ac6a027448 --- /dev/null +++ b/packages/types/src/documents/platform/accounts.ts @@ -0,0 +1,5 @@ +import { Document } from "../document" + +export interface AccountMetadata extends Document { + email: string +} diff --git a/packages/types/src/documents/platform/index.ts b/packages/types/src/documents/platform/index.ts index ba329f1bd0..1a7cef91cf 100644 --- a/packages/types/src/documents/platform/index.ts +++ b/packages/types/src/documents/platform/index.ts @@ -1 +1,3 @@ export * from "./info" +export * from "./users" +export * from "./accounts" diff --git a/packages/types/src/documents/platform/users.ts b/packages/types/src/documents/platform/users.ts new file mode 100644 index 0000000000..1b65ea42f2 --- /dev/null +++ b/packages/types/src/documents/platform/users.ts @@ -0,0 +1,9 @@ +import { Document } from "../document" + +/** + * doc id is user email + */ +export interface PlatformUserByEmail extends Document { + tenantId: string + userId: string +} diff --git a/packages/types/src/documents/pouch.ts b/packages/types/src/documents/pouch.ts new file mode 100644 index 0000000000..f9ed43b32f --- /dev/null +++ b/packages/types/src/documents/pouch.ts @@ -0,0 +1,26 @@ +export interface RowValue { + rev: string + deleted: boolean +} + +export interface RowResponse { + id: string + key: string + error: string + value: RowValue + doc: T +} + +export interface AllDocsResponse { + offset: number + total_rows: number + rows: RowResponse[] +} + +export type BulkDocsResponse = BulkDocResponse[] + +interface BulkDocResponse { + ok: boolean + id: string + rev: string +} diff --git a/packages/types/src/sdk/auth.ts b/packages/types/src/sdk/auth.ts new file mode 100644 index 0000000000..dd3c2124b5 --- /dev/null +++ b/packages/types/src/sdk/auth.ts @@ -0,0 +1,5 @@ +export interface AuthToken { + userId: string + tenantId: string + sessionId: string +} diff --git a/packages/types/src/sdk/events/screen.ts b/packages/types/src/sdk/events/screen.ts index 0d9f8cb32c..23c468b7ad 100644 --- a/packages/types/src/sdk/events/screen.ts +++ b/packages/types/src/sdk/events/screen.ts @@ -2,12 +2,12 @@ import { BaseEvent } from "./event" export interface ScreenCreatedEvent extends BaseEvent { screenId: string - layoutId: string + layoutId?: string roleId: string } export interface ScreenDeletedEvent extends BaseEvent { screenId: string - layoutId: string + layoutId?: string roleId: string } diff --git a/packages/types/src/sdk/index.ts b/packages/types/src/sdk/index.ts index 58d4593472..bae566b42e 100644 --- a/packages/types/src/sdk/index.ts +++ b/packages/types/src/sdk/index.ts @@ -6,3 +6,4 @@ export * from "./migrations" export * from "./datasources" export * from "./search" export * from "./koa" +export * from "./auth" diff --git a/packages/worker/package.json b/packages/worker/package.json index ae71c3971b..4601c1cd43 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.2.41-alpha.5", + "version": "1.2.58-alpha.5", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -35,10 +35,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "1.2.41-alpha.5", - "@budibase/pro": "1.2.41-alpha.5", - "@budibase/string-templates": "1.2.41-alpha.5", - "@budibase/types": "1.2.41-alpha.5", + "@budibase/backend-core": "1.2.58-alpha.5", + "@budibase/pro": "1.2.58-alpha.5", + "@budibase/string-templates": "1.2.58-alpha.5", + "@budibase/types": "1.2.58-alpha.5", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", @@ -46,6 +46,7 @@ "aws-sdk": "2.1030.0", "bcryptjs": "2.4.3", "dotenv": "8.6.0", + "elastic-apm-node": "3.38.0", "global-agent": "3.0.0", "got": "11.8.3", "joi": "17.6.0", @@ -73,6 +74,7 @@ "@types/koa-router": "7.4.4", "@types/koa__router": "8.0.11", "@types/node": "14.18.20", + "@types/uuid": "8.3.4", "@typescript-eslint/parser": "5.12.0", "copyfiles": "2.4.1", "eslint": "6.8.0", diff --git a/packages/worker/scripts/jestSetup.js b/packages/worker/scripts/jestSetup.js index 765befd153..8ee2d33d70 100644 --- a/packages/worker/scripts/jestSetup.js +++ b/packages/worker/scripts/jestSetup.js @@ -14,3 +14,9 @@ const tk = require("timekeeper") tk.freeze(mocks.date.MOCK_DATE) global.console.log = jest.fn() // console.log are ignored in tests + +if (!process.env.CI) { + // set a longer timeout in dev for debugging + // 100 seconds + jest.setTimeout(100000) +} diff --git a/packages/worker/scripts/load/users.js b/packages/worker/scripts/load/users.js deleted file mode 100644 index 6caf67ade4..0000000000 --- a/packages/worker/scripts/load/users.js +++ /dev/null @@ -1,97 +0,0 @@ -// get the JWT secret etc -require("../../src/environment") -require("@budibase/backend-core").init() -const { - getProdAppID, - generateGlobalUserID, -} = require("@budibase/backend-core/db") -const { doInTenant, getGlobalDB } = require("@budibase/backend-core/tenancy") -const users = require("../../src/sdk/users") -const { publicApiUserFix } = require("../../src/utilities/users") -const { hash } = require("@budibase/backend-core/utils") - -const USER_LOAD_NUMBER = 10000 -const BATCH_SIZE = 200 -const PASSWORD = "test" -const TENANT_ID = "default" - -const APP_ID = process.argv[2] - -const words = [ - "test", - "testing", - "budi", - "mail", - "age", - "risk", - "load", - "uno", - "arm", - "leg", - "pen", - "glass", - "box", - "chicken", - "bottle", -] - -if (!APP_ID) { - console.error("Must supply app ID as first CLI option!") - process.exit(-1) -} - -const WORD_1 = words[Math.floor(Math.random() * words.length)] -const WORD_2 = words[Math.floor(Math.random() * words.length)] -let HASHED_PASSWORD - -function generateUser(count) { - return { - _id: generateGlobalUserID(), - password: HASHED_PASSWORD, - email: `${WORD_1}${count}@${WORD_2}.com`, - roles: { - [getProdAppID(APP_ID)]: "BASIC", - }, - status: "active", - forceResetPassword: false, - firstName: "John", - lastName: "Smith", - } -} - -async function run() { - HASHED_PASSWORD = await hash(PASSWORD) - return doInTenant(TENANT_ID, async () => { - const db = getGlobalDB() - for (let i = 0; i < USER_LOAD_NUMBER; i += BATCH_SIZE) { - let userSavePromises = [] - for (let j = 0; j < BATCH_SIZE; j++) { - // like the public API - const ctx = publicApiUserFix({ - request: { - body: generateUser(i + j), - }, - }) - userSavePromises.push( - users.save(ctx.request.body, { - hashPassword: false, - requirePassword: true, - bulkCreate: true, - }) - ) - } - const allUsers = await Promise.all(userSavePromises) - await db.bulkDocs(allUsers) - console.log(`${i + BATCH_SIZE} users have been created.`) - } - }) -} - -run() - .then(() => { - console.log(`Generated ${USER_LOAD_NUMBER} users!`) - }) - .catch(err => { - console.error("Failed for reason: ", err) - process.exit(-1) - }) diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index dc96554cb2..834531cd78 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -227,9 +227,22 @@ export const oidcPreAuth = async (ctx: any, next: any) => { setCookie(ctx, configId, Cookies.OIDC_CONFIG) + const db = getGlobalDB() + const config = await core.db.getScopedConfig(db, { + type: Configs.OIDC, + group: ctx.query.group, + }) + + const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] + + let authScopes = + chosenConfig.scopes?.length > 0 + ? chosenConfig.scopes + : ["profile", "email", "offline_access"] + return passport.authenticate(strategy, { // required 'openid' scope is added by oidc strategy factory - scope: ["profile", "email", "offline_access"], //auth0 offline_access scope required for the refresh token behaviour. + scope: authScopes, })(ctx, next) } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 1f9af3514b..d5e8eb8e62 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,9 +1,13 @@ -import { EmailTemplatePurpose } from "../../../constants" import { checkInviteCode } from "../../../utilities/redis" -import { sendEmail } from "../../../utilities/email" import { users } from "../../../sdk" import env from "../../../environment" -import { CloudAccount, User } from "@budibase/types" +import { + BulkDeleteUsersRequest, + CloudAccount, + InviteUserRequest, + InviteUsersRequest, + User, +} from "@budibase/types" import { accounts, cache, @@ -46,8 +50,8 @@ export const bulkCreate = async (ctx: any) => { } try { - let response = await users.bulkCreate(newUsersRequested, groups) - await groupUtils.bulkSaveGroupUsers(groupsToSave, response) + const response = await users.bulkCreate(newUsersRequested, groups) + await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful) ctx.body = response } catch (err: any) { @@ -138,17 +142,15 @@ export const destroy = async (ctx: any) => { } export const bulkDelete = async (ctx: any) => { - const { userIds } = ctx.request.body + const { userIds } = ctx.request.body as BulkDeleteUsersRequest if (userIds?.indexOf(ctx.user._id) !== -1) { ctx.throw(400, "Unable to delete self.") } try { - let usersResponse = await users.bulkDelete(userIds) + let response = await users.bulkDelete(userIds) - ctx.body = { - message: `${usersResponse.length} user(s) deleted`, - } + ctx.body = response } catch (err) { ctx.throw(err) } @@ -193,58 +195,27 @@ export const tenantUserLookup = async (ctx: any) => { } export const invite = async (ctx: any) => { - let { email, userInfo } = ctx.request.body - const existing = await usersCore.getGlobalUserByEmail(email) - if (existing) { - ctx.throw(400, "Email address already in use.") + const request = ctx.request.body as InviteUserRequest + const response = await users.invite([request]) + + // explicitly throw for single user invite + if (response.unsuccessful.length) { + const reason = response.unsuccessful[0].reason + if (reason === "Unavailable") { + ctx.throw(400, reason) + } else { + ctx.throw(500, reason) + } } - if (!userInfo) { - userInfo = {} - } - userInfo.tenantId = tenancy.getTenantId() - const opts: any = { - subject: "{{ company }} platform invitation", - info: userInfo, - } - await sendEmail(email, EmailTemplatePurpose.INVITATION, opts) + ctx.body = { message: "Invitation has been sent.", } - await events.user.invited() } export const inviteMultiple = async (ctx: any) => { - let users = ctx.request.body - let existing = false - let existingEmail - for (let user of users) { - if (await usersCore.getGlobalUserByEmail(user.email)) { - existing = true - existingEmail = user.email - break - } - } - - if (existing) { - ctx.throw(400, `${existingEmail} already exists`) - } - - for (let i = 0; i < users.length; i++) { - let userInfo = users[i].userInfo - if (!userInfo) { - userInfo = {} - } - userInfo.tenantId = tenancy.getTenantId() - const opts: any = { - subject: "{{ company }} platform invitation", - info: userInfo, - } - await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts) - } - - ctx.body = { - message: "Invitations have been sent.", - } + const request = ctx.request.body as InviteUsersRequest + ctx.body = await users.invite(request) } export const inviteAccept = async (ctx: any) => { diff --git a/packages/worker/src/api/controllers/system/accounts.ts b/packages/worker/src/api/controllers/system/accounts.ts new file mode 100644 index 0000000000..5e72f35bab --- /dev/null +++ b/packages/worker/src/api/controllers/system/accounts.ts @@ -0,0 +1,21 @@ +import { Account, AccountMetadata } from "@budibase/types" +import { accounts } from "../../../sdk" + +export const save = async (ctx: any) => { + const account = ctx.request.body as Account + let metadata: AccountMetadata = { + _id: accounts.formatAccountMetadataId(account.accountId), + email: account.email, + } + + metadata = await accounts.saveMetadata(metadata) + + ctx.body = metadata + ctx.status = 200 +} + +export const destroy = async (ctx: any) => { + const accountId = accounts.formatAccountMetadataId(ctx.params.accountId) + await accounts.destroyMetadata(accountId) + ctx.status = 204 +} diff --git a/packages/worker/src/api/index.js b/packages/worker/src/api/index.ts similarity index 82% rename from packages/worker/src/api/index.js rename to packages/worker/src/api/index.ts index 202471c78f..692eff685c 100644 --- a/packages/worker/src/api/index.js +++ b/packages/worker/src/api/index.ts @@ -1,15 +1,10 @@ -const Router = require("@koa/router") +import Router from "@koa/router" const compress = require("koa-compress") const zlib = require("zlib") -const { routes } = require("./routes") -const { - buildAuthMiddleware, - auditLog, - buildTenancyMiddleware, - buildCsrfMiddleware, -} = require("@budibase/backend-core/auth") -const { middleware: pro } = require("@budibase/pro") -const { errors } = require("@budibase/backend-core") +import { routes } from "./routes" +import { middleware as pro } from "@budibase/pro" +import { errors, auth, middleware } from "@budibase/backend-core" +import { APIError } from "@budibase/types" const PUBLIC_ENDPOINTS = [ // old deprecated endpoints kept for backwards compat @@ -97,9 +92,9 @@ router }) ) .use("/health", ctx => (ctx.status = 200)) - .use(buildAuthMiddleware(PUBLIC_ENDPOINTS)) - .use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS)) - .use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS })) + .use(auth.buildAuthMiddleware(PUBLIC_ENDPOINTS)) + .use(auth.buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS)) + .use(auth.buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS })) .use(pro.licensing()) .use(pro.activity()) // for now no public access is allowed to worker (bar health check) @@ -115,21 +110,22 @@ router } return next() }) - .use(auditLog) + .use(middleware.auditLog) // error handling middleware - TODO: This could be moved to backend-core router.use(async (ctx, next) => { try { await next() - } catch (err) { + } catch (err: any) { ctx.log.error(err) ctx.status = err.status || err.statusCode || 500 const error = errors.getPublicError(err) - ctx.body = { + const body: APIError = { message: err.message, status: ctx.status, error, } + ctx.body = body } }) diff --git a/packages/worker/src/api/routes/global/configs.js b/packages/worker/src/api/routes/global/configs.js index c4beb57600..e08611b73a 100644 --- a/packages/worker/src/api/routes/global/configs.js +++ b/packages/worker/src/api/routes/global/configs.js @@ -53,6 +53,7 @@ function oidcValidation() { name: Joi.string().allow("", null), uuid: Joi.string().required(), activated: Joi.boolean().required(), + scopes: Joi.array().optional() }) ).required(true) }).unknown(true) diff --git a/packages/worker/src/api/routes/tests/auth.spec.js b/packages/worker/src/api/routes/global/tests/auth.spec.ts similarity index 51% rename from packages/worker/src/api/routes/tests/auth.spec.js rename to packages/worker/src/api/routes/global/tests/auth.spec.ts index 165ecd0f4a..69fa1b223c 100644 --- a/packages/worker/src/api/routes/tests/auth.spec.js +++ b/packages/worker/src/api/routes/global/tests/auth.spec.ts @@ -1,11 +1,11 @@ jest.mock("nodemailer") -const { config, request, mocks, structures } = require("../../../tests") +import { TestConfiguration, mocks, API } from "../../../../tests" const sendMailMock = mocks.email.mock() -const { events } = require("@budibase/backend-core") - -const TENANT_ID = structures.TENANT_ID +import { events } from "@budibase/backend-core" describe("/api/global/auth", () => { + const config = new TestConfiguration() + const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -19,56 +19,32 @@ describe("/api/global/auth", () => { jest.clearAllMocks() }) - const requestPasswordReset = async () => { - await config.saveSmtpConfig() - await config.saveSettingsConfig() - await config.createUser() - const res = await request - .post(`/api/global/auth/${TENANT_ID}/reset`) - .send({ - email: "test@test.com", - }) - .expect("Content-Type", /json/) - .expect(200) - const emailCall = sendMailMock.mock.calls[0][0] - const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`) - const code = parts[1].split("\"")[0].split("&")[0] - return { code, res } - } - it("should logout", async () => { - await request - .post("/api/global/auth/logout") - .set(config.defaultHeaders()) - .expect(200) + await api.auth.logout() expect(events.auth.logout).toBeCalledTimes(1) }) it("should be able to generate password reset email", async () => { - const { res, code } = await requestPasswordReset() + const { res, code } = await api.auth.requestPasswordReset(sendMailMock) const user = await config.getUser("test@test.com") - expect(res.body).toEqual({ message: "Please check your email for a reset link." }) + expect(res.body).toEqual({ + message: "Please check your email for a reset link.", + }) expect(sendMailMock).toHaveBeenCalled() - + expect(code).toBeDefined() expect(events.user.passwordResetRequested).toBeCalledTimes(1) expect(events.user.passwordResetRequested).toBeCalledWith(user) }) it("should allow resetting user password with code", async () => { - const { code } = await requestPasswordReset() + const { code } = await api.auth.requestPasswordReset(sendMailMock) const user = await config.getUser("test@test.com") - delete user.password + delete user.password + + const res = await api.auth.updatePassword(code) - const res = await request - .post(`/api/global/auth/${TENANT_ID}/reset/update`) - .send({ - password: "newpassword", - resetCode: code, - }) - .expect("Content-Type", /json/) - .expect(200) expect(res.body).toEqual({ message: "password reset successfully." }) expect(events.user.passwordReset).toBeCalledTimes(1) expect(events.user.passwordReset).toBeCalledWith(user) @@ -79,15 +55,15 @@ describe("/api/global/auth", () => { const passportSpy = jest.spyOn(auth.passport, "authenticate") let oidcConf - let chosenConfig - let configId + let chosenConfig: any + let configId: string // mock the oidc strategy implementation and return value let strategyFactory = jest.fn() let mockStrategyReturn = jest.fn() let mockStrategyConfig = jest.fn() auth.oidc.fetchStrategyConfig = mockStrategyConfig - + strategyFactory.mockReturnValue(mockStrategyReturn) auth.oidc.strategyFactory = strategyFactory @@ -99,34 +75,34 @@ describe("/api/global/auth", () => { }) afterEach(() => { - expect(strategyFactory).toBeCalledWith( - chosenConfig, - expect.any(Function) - ) + expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function)) }) describe("oidc configs", () => { it("should load strategy and delegate to passport", async () => { - await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`) + await api.configs.getOIDCConfig(configId) expect(passportSpy).toBeCalledWith(mockStrategyReturn, { - scope: ["profile", "email", "offline_access"] + scope: ["profile", "email", "offline_access"], }) - expect(passportSpy.mock.calls.length).toBe(1); + expect(passportSpy.mock.calls.length).toBe(1) }) }) describe("oidc callback", () => { it("should load strategy and delegate to passport", async () => { - await request.get(`/api/global/auth/${TENANT_ID}/oidc/callback`) - .set(config.getOIDConfigCookie(configId)) - - expect(passportSpy).toBeCalledWith(mockStrategyReturn, { - successRedirect: "/", failureRedirect: "/error" - }, expect.anything()) - expect(passportSpy.mock.calls.length).toBe(1); + await api.configs.OIDCCallback(configId) + + expect(passportSpy).toBeCalledWith( + mockStrategyReturn, + { + successRedirect: "/", + failureRedirect: "/error", + }, + expect.anything() + ) + expect(passportSpy.mock.calls.length).toBe(1) }) }) - }) }) diff --git a/packages/worker/src/api/routes/tests/configs.spec.js b/packages/worker/src/api/routes/global/tests/configs.spec.ts similarity index 72% rename from packages/worker/src/api/routes/tests/configs.spec.js rename to packages/worker/src/api/routes/global/tests/configs.spec.ts index b2dc8124da..31510c03dd 100644 --- a/packages/worker/src/api/routes/tests/configs.spec.js +++ b/packages/worker/src/api/routes/global/tests/configs.spec.ts @@ -1,11 +1,12 @@ // mock the email system jest.mock("nodemailer") -const { config, structures, mocks, request } = require("../../../tests") +import { TestConfiguration, structures, mocks, API } from "../../../../tests" mocks.email.mock() -const { Configs } = require("@budibase/backend-core/constants") -const { events } = require("@budibase/backend-core") +import { Configs, events } from "@budibase/backend-core" describe("configs", () => { + const config = new TestConfiguration() + const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -20,35 +21,33 @@ describe("configs", () => { }) describe("post /api/global/configs", () => { - - const saveConfig = async (conf, _id, _rev) => { + const saveConfig = async (conf: any, _id?: string, _rev?: string) => { const data = { ...conf, _id, - _rev + _rev, } - const res = await request - .post(`/api/global/configs`) - .send(data) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await api.configs.saveConfig(data) return { ...data, - ...res.body + ...res.body, } } describe("google", () => { - const saveGoogleConfig = async (conf, _id, _rev) => { + const saveGoogleConfig = async ( + conf?: any, + _id?: string, + _rev?: string + ) => { const googleConfig = structures.configs.google(conf) return saveConfig(googleConfig, _id, _rev) } - + describe("create", () => { - it ("should create activated google config", async () => { + it("should create activated google config", async () => { await saveGoogleConfig() expect(events.auth.SSOCreated).toBeCalledTimes(1) expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE) @@ -58,7 +57,7 @@ describe("configs", () => { await config.deleteConfig(Configs.GOOGLE) }) - it ("should create deactivated google config", async () => { + it("should create deactivated google config", async () => { await saveGoogleConfig({ activated: false }) expect(events.auth.SSOCreated).toBeCalledTimes(1) expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE) @@ -69,10 +68,14 @@ describe("configs", () => { }) describe("update", () => { - it ("should update google config to deactivated", async () => { + it("should update google config to deactivated", async () => { const googleConf = await saveGoogleConfig() jest.clearAllMocks() - await saveGoogleConfig({ ...googleConf.config, activated: false }, googleConf._id, googleConf._rev) + await saveGoogleConfig( + { ...googleConf.config, activated: false }, + googleConf._id, + googleConf._rev + ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE) expect(events.auth.SSOActivated).not.toBeCalled() @@ -81,10 +84,14 @@ describe("configs", () => { await config.deleteConfig(Configs.GOOGLE) }) - it ("should update google config to activated", async () => { + it("should update google config to activated", async () => { const googleConf = await saveGoogleConfig({ activated: false }) jest.clearAllMocks() - await saveGoogleConfig({ ...googleConf.config, activated: true}, googleConf._id, googleConf._rev) + await saveGoogleConfig( + { ...googleConf.config, activated: true }, + googleConf._id, + googleConf._rev + ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE) expect(events.auth.SSODeactivated).not.toBeCalled() @@ -92,17 +99,21 @@ describe("configs", () => { expect(events.auth.SSOActivated).toBeCalledWith(Configs.GOOGLE) await config.deleteConfig(Configs.GOOGLE) }) - }) + }) }) describe("oidc", () => { - const saveOIDCConfig = async (conf, _id, _rev) => { + const saveOIDCConfig = async ( + conf?: any, + _id?: string, + _rev?: string + ) => { const oidcConfig = structures.configs.oidc(conf) return saveConfig(oidcConfig, _id, _rev) } describe("create", () => { - it ("should create activated OIDC config", async () => { + it("should create activated OIDC config", async () => { await saveOIDCConfig() expect(events.auth.SSOCreated).toBeCalledTimes(1) expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC) @@ -112,7 +123,7 @@ describe("configs", () => { await config.deleteConfig(Configs.OIDC) }) - it ("should create deactivated OIDC config", async () => { + it("should create deactivated OIDC config", async () => { await saveOIDCConfig({ activated: false }) expect(events.auth.SSOCreated).toBeCalledTimes(1) expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC) @@ -123,10 +134,14 @@ describe("configs", () => { }) describe("update", () => { - it ("should update OIDC config to deactivated", async () => { + it("should update OIDC config to deactivated", async () => { const oidcConf = await saveOIDCConfig() jest.clearAllMocks() - await saveOIDCConfig({ ...oidcConf.config.configs[0], activated: false }, oidcConf._id, oidcConf._rev) + await saveOIDCConfig( + { ...oidcConf.config.configs[0], activated: false }, + oidcConf._id, + oidcConf._rev + ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC) expect(events.auth.SSOActivated).not.toBeCalled() @@ -135,10 +150,14 @@ describe("configs", () => { await config.deleteConfig(Configs.OIDC) }) - it ("should update OIDC config to activated", async () => { + it("should update OIDC config to activated", async () => { const oidcConf = await saveOIDCConfig({ activated: false }) jest.clearAllMocks() - await saveOIDCConfig({ ...oidcConf.config.configs[0], activated: true}, oidcConf._id, oidcConf._rev) + await saveOIDCConfig( + { ...oidcConf.config.configs[0], activated: true }, + oidcConf._id, + oidcConf._rev + ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC) expect(events.auth.SSODeactivated).not.toBeCalled() @@ -147,17 +166,20 @@ describe("configs", () => { await config.deleteConfig(Configs.OIDC) }) }) - }) describe("smtp", () => { - const saveSMTPConfig = async (conf, _id, _rev) => { + const saveSMTPConfig = async ( + conf?: any, + _id?: string, + _rev?: string + ) => { const smtpConfig = structures.configs.smtp(conf) return saveConfig(smtpConfig, _id, _rev) } describe("create", () => { - it ("should create SMTP config", async () => { + it("should create SMTP config", async () => { await config.deleteConfig(Configs.SMTP) await saveSMTPConfig() expect(events.email.SMTPUpdated).not.toBeCalled() @@ -167,7 +189,7 @@ describe("configs", () => { }) describe("update", () => { - it ("should update SMTP config", async () => { + it("should update SMTP config", async () => { const smtpConf = await saveSMTPConfig() jest.clearAllMocks() await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev) @@ -179,15 +201,19 @@ describe("configs", () => { }) describe("settings", () => { - const saveSettingsConfig = async (conf, _id, _rev) => { + const saveSettingsConfig = async ( + conf?: any, + _id?: string, + _rev?: string + ) => { const settingsConfig = structures.configs.settings(conf) return saveConfig(settingsConfig, _id, _rev) } describe("create", () => { - it ("should create settings config with default settings", async () => { + it("should create settings config with default settings", async () => { await config.deleteConfig(Configs.SETTINGS) - + await saveSettingsConfig() expect(events.org.nameUpdated).not.toBeCalled() @@ -195,35 +221,43 @@ describe("configs", () => { expect(events.org.platformURLUpdated).not.toBeCalled() }) - it ("should create settings config with non-default settings", async () => { + it("should create settings config with non-default settings", async () => { + config.modeSelf() await config.deleteConfig(Configs.SETTINGS) const conf = { company: "acme", logoUrl: "http://example.com", - platformUrl: "http://example.com" + platformUrl: "http://example.com", } await saveSettingsConfig(conf) - + expect(events.org.nameUpdated).toBeCalledTimes(1) expect(events.org.logoUpdated).toBeCalledTimes(1) expect(events.org.platformURLUpdated).toBeCalledTimes(1) + config.modeAccount() }) }) describe("update", () => { - it ("should update settings config", async () => { + it("should update settings config", async () => { + config.modeSelf() await config.deleteConfig(Configs.SETTINGS) const settingsConfig = await saveSettingsConfig() settingsConfig.config.company = "acme" settingsConfig.config.logoUrl = "http://example.com" settingsConfig.config.platformUrl = "http://example.com" - await saveSettingsConfig(settingsConfig.config, settingsConfig._id, settingsConfig._rev) + await saveSettingsConfig( + settingsConfig.config, + settingsConfig._id, + settingsConfig._rev + ) expect(events.org.nameUpdated).toBeCalledTimes(1) expect(events.org.logoUpdated).toBeCalledTimes(1) expect(events.org.platformURLUpdated).toBeCalledTimes(1) + config.modeAccount() }) }) }) @@ -232,12 +266,7 @@ describe("configs", () => { it("should return the correct checklist status based on the state of the budibase installation", async () => { await config.saveSmtpConfig() - const res = await request - .get(`/api/global/configs/checklist`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - + const res = await api.configs.getConfigChecklist() const checklist = res.body expect(checklist.apps.checked).toBeFalsy() diff --git a/packages/worker/src/api/routes/tests/email.spec.js b/packages/worker/src/api/routes/global/tests/email.spec.ts similarity index 58% rename from packages/worker/src/api/routes/tests/email.spec.js rename to packages/worker/src/api/routes/global/tests/email.spec.ts index e90d953880..608f4094f8 100644 --- a/packages/worker/src/api/routes/tests/email.spec.js +++ b/packages/worker/src/api/routes/global/tests/email.spec.ts @@ -1,12 +1,11 @@ jest.mock("nodemailer") -const { config, mocks, structures, request } = require("../../../tests") +import { TestConfiguration, mocks, API } from "../../../../tests" const sendMailMock = mocks.email.mock() - -const { EmailTemplatePurpose } = require("../../../constants") - -const TENANT_ID = structures.TENANT_ID +import { EmailTemplatePurpose } from "../../../../constants" describe("/api/global/email", () => { + const config = new TestConfiguration() + const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -20,16 +19,9 @@ describe("/api/global/email", () => { // initially configure settings await config.saveSmtpConfig() await config.saveSettingsConfig() - const res = await request - .post(`/api/global/email/send`) - .send({ - email: "test@test.com", - purpose: EmailTemplatePurpose.INVITATION, - tenantId: TENANT_ID, - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + + const res = await api.emails.sendEmail(EmailTemplatePurpose.INVITATION) + expect(res.body.message).toBeDefined() expect(sendMailMock).toHaveBeenCalled() const emailCall = sendMailMock.mock.calls[0][0] diff --git a/packages/worker/src/api/routes/tests/realEmail.spec.js b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts similarity index 75% rename from packages/worker/src/api/routes/tests/realEmail.spec.js rename to packages/worker/src/api/routes/global/tests/realEmail.spec.ts index 548c6ee59e..135367e0d8 100644 --- a/packages/worker/src/api/routes/tests/realEmail.spec.js +++ b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts @@ -1,5 +1,5 @@ -const { config, request } = require("../../../tests") -const { EmailTemplatePurpose } = require("../../../constants") +import { TestConfiguration, API } from "../../../../tests" +import { EmailTemplatePurpose } from "../../../../constants" const nodemailer = require("nodemailer") const fetch = require("node-fetch") @@ -7,6 +7,8 @@ const fetch = require("node-fetch") jest.setTimeout(30000) describe("/api/global/email", () => { + const config = new TestConfiguration() + const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -16,27 +18,24 @@ describe("/api/global/email", () => { await config.afterAll() }) - async function sendRealEmail(purpose) { + async function sendRealEmail(purpose: string) { let response, text try { - const timeout = () => new Promise((resolve, reject) => - setTimeout(() => reject({ - status: 301, - errno: "ETIME" - }), 20000) - ) + const timeout = () => + new Promise((resolve, reject) => + setTimeout( + () => + reject({ + status: 301, + errno: "ETIME", + }), + 20000 + ) + ) await Promise.race([config.saveEtherealSmtpConfig(), timeout()]) await Promise.race([config.saveSettingsConfig(), timeout()]) - const user = await config.getUser("test@test.com") - const res = await request - .post(`/api/global/email/send`) - .send({ - email: "test@test.com", - purpose, - userId: user._id, - }) - .set(config.defaultHeaders()) - .timeout(20000) + + const res = await api.emails.sendEmail(purpose).timeout(20000) // ethereal hiccup, can't test right now if (res.status >= 300) { return @@ -47,7 +46,7 @@ describe("/api/global/email", () => { expect(testUrl).toBeDefined() response = await fetch(testUrl) text = await response.text() - } catch (err) { + } catch (err: any) { // ethereal hiccup, can't test right now if (parseInt(err.status) >= 300 || (err && err.errno === "ETIME")) { return @@ -81,4 +80,4 @@ describe("/api/global/email", () => { it("should be able to send a password recovery email", async () => { await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY) }) -}) \ No newline at end of file +}) diff --git a/packages/worker/src/api/routes/tests/self.spec.js b/packages/worker/src/api/routes/global/tests/self.spec.ts similarity index 67% rename from packages/worker/src/api/routes/tests/self.spec.js rename to packages/worker/src/api/routes/global/tests/self.spec.ts index 64fce5ae1d..b3c91a9306 100644 --- a/packages/worker/src/api/routes/tests/self.spec.js +++ b/packages/worker/src/api/routes/global/tests/self.spec.ts @@ -1,8 +1,10 @@ jest.mock("nodemailer") -const { config, request } = require("../../../tests") -const { events } = require("@budibase/backend-core") +import { TestConfiguration, API } from "../../../../tests" +import { events } from "@budibase/backend-core" describe("/api/global/self", () => { + const config = new TestConfiguration() + const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -16,23 +18,13 @@ describe("/api/global/self", () => { jest.clearAllMocks() }) - const updateSelf = async (user) => { - const res = await request - .post(`/api/global/self`) - .send(user) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - return res - } - describe("update", () => { - it("should update self", async () => { const user = await config.createUser() + await config.createSession(user) delete user.password - const res = await updateSelf(user) + const res = await api.self.updateSelf(user) expect(res.body._id).toBe(user._id) expect(events.user.updated).toBeCalledTimes(1) @@ -42,10 +34,10 @@ describe("/api/global/self", () => { it("should update password", async () => { const user = await config.createUser() - const password = "newPassword" - user.password = password + await config.createSession(user) - const res = await updateSelf(user) + user.password = "newPassword" + const res = await api.self.updateSelf(user) delete user.password expect(res.body._id).toBe(user._id) @@ -55,4 +47,4 @@ describe("/api/global/self", () => { expect(events.user.passwordUpdated).toBeCalledWith(user) }) }) -}) \ No newline at end of file +}) diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts new file mode 100644 index 0000000000..7762c2e7e2 --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -0,0 +1,512 @@ +import { InviteUsersResponse } from "@budibase/types" + +jest.mock("nodemailer") +import { + TestConfiguration, + mocks, + structures, + TENANT_1, + API, +} from "../../../../tests" +const sendMailMock = mocks.email.mock() +import { events, tenancy } from "@budibase/backend-core" + +describe("/api/global/users", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("invite", () => { + it("should be able to generate an invitation", async () => { + const email = structures.users.newEmail() + const { code, res } = await api.users.sendUserInvite(sendMailMock, email) + + expect(res.body).toEqual({ message: "Invitation has been sent." }) + expect(sendMailMock).toHaveBeenCalled() + expect(code).toBeDefined() + expect(events.user.invited).toBeCalledTimes(1) + }) + + it("should not be able to generate an invitation for existing user", async () => { + const { code, res } = await api.users.sendUserInvite( + sendMailMock, + config.defaultUser!.email, + 400 + ) + + expect(res.body.message).toBe("Unavailable") + expect(sendMailMock).toHaveBeenCalledTimes(0) + expect(code).toBeUndefined() + expect(events.user.invited).toBeCalledTimes(0) + }) + + it("should be able to create new user from invite", async () => { + const email = structures.users.newEmail() + const { code } = await api.users.sendUserInvite(sendMailMock, email) + + const res = await api.users.acceptInvite(code) + + expect(res.body._id).toBeDefined() + const user = await config.getUser(email) + expect(user).toBeDefined() + expect(user._id).toEqual(res.body._id) + expect(events.user.inviteAccepted).toBeCalledTimes(1) + expect(events.user.inviteAccepted).toBeCalledWith(user) + }) + }) + + describe("inviteMultiple", () => { + it("should be able to generate an invitation", async () => { + const newUserInvite = () => ({ + email: structures.users.newEmail(), + userInfo: {}, + }) + const request = [newUserInvite(), newUserInvite()] + + const res = await api.users.sendMultiUserInvite(request) + + const body = res.body as InviteUsersResponse + expect(body.successful.length).toBe(2) + expect(body.unsuccessful.length).toBe(0) + expect(sendMailMock).toHaveBeenCalledTimes(2) + expect(events.user.invited).toBeCalledTimes(2) + }) + + it("should not be able to generate an invitation for existing user", async () => { + const request = [{ email: config.defaultUser!.email, userInfo: {} }] + + const res = await api.users.sendMultiUserInvite(request) + + const body = res.body as InviteUsersResponse + expect(body.successful.length).toBe(0) + expect(body.unsuccessful.length).toBe(1) + expect(body.unsuccessful[0].reason).toBe("Unavailable") + expect(sendMailMock).toHaveBeenCalledTimes(0) + expect(events.user.invited).toBeCalledTimes(0) + }) + }) + + describe("bulkCreate", () => { + it("should ignore users existing in the same tenant", async () => { + const user = await config.createUser() + jest.clearAllMocks() + + const response = await api.users.bulkCreateUsers([user]) + + expect(response.successful.length).toBe(0) + expect(response.unsuccessful.length).toBe(1) + expect(response.unsuccessful[0].email).toBe(user.email) + expect(events.user.created).toBeCalledTimes(0) + }) + + it("should ignore users existing in other tenants", async () => { + const user = await config.createUser() + jest.resetAllMocks() + + await tenancy.doInTenant(TENANT_1, async () => { + const response = await api.users.bulkCreateUsers([user]) + + expect(response.successful.length).toBe(0) + expect(response.unsuccessful.length).toBe(1) + expect(response.unsuccessful[0].email).toBe(user.email) + expect(events.user.created).toBeCalledTimes(0) + }) + }) + + it("should ignore accounts using the same email", async () => { + const account = structures.accounts.account() + const resp = await api.accounts.saveMetadata(account) + const user = structures.users.user({ email: resp.email }) + jest.clearAllMocks() + + const response = await api.users.bulkCreateUsers([user]) + + expect(response.successful.length).toBe(0) + expect(response.unsuccessful.length).toBe(1) + expect(response.unsuccessful[0].email).toBe(user.email) + expect(events.user.created).toBeCalledTimes(0) + }) + + it("should be able to bulkCreate users", async () => { + const builder = structures.users.builderUser() + const admin = structures.users.adminUser() + const user = structures.users.user() + + const response = await api.users.bulkCreateUsers([builder, admin, user]) + + expect(response.successful.length).toBe(3) + expect(response.successful[0].email).toBe(builder.email) + expect(response.successful[1].email).toBe(admin.email) + expect(response.successful[2].email).toBe(user.email) + expect(response.unsuccessful.length).toBe(0) + expect(events.user.created).toBeCalledTimes(3) + expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) + expect(events.user.permissionBuilderAssigned).toBeCalledTimes(2) + }) + }) + + describe("create", () => { + it("should be able to create a basic user", async () => { + const user = structures.users.user() + + await api.users.saveUser(user) + + expect(events.user.created).toBeCalledTimes(1) + expect(events.user.updated).not.toBeCalled() + expect(events.user.permissionBuilderAssigned).not.toBeCalled() + expect(events.user.permissionAdminAssigned).not.toBeCalled() + }) + + it("should be able to create an admin user", async () => { + const user = structures.users.adminUser() + + await api.users.saveUser(user) + + expect(events.user.created).toBeCalledTimes(1) + expect(events.user.updated).not.toBeCalled() + expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) + expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) + }) + + it("should be able to create a builder user", async () => { + const user = structures.users.builderUser() + + await api.users.saveUser(user) + + expect(events.user.created).toBeCalledTimes(1) + expect(events.user.updated).not.toBeCalled() + expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) + expect(events.user.permissionAdminAssigned).not.toBeCalled() + }) + + it("should be able to assign app roles", async () => { + const user = structures.users.user() + user.roles = { + app_123: "role1", + app_456: "role2", + } + + await api.users.saveUser(user) + + const savedUser = await config.getUser(user.email) + expect(events.user.created).toBeCalledTimes(1) + expect(events.user.updated).not.toBeCalled() + expect(events.role.assigned).toBeCalledTimes(2) + expect(events.role.assigned).toBeCalledWith(savedUser, "role1") + expect(events.role.assigned).toBeCalledWith(savedUser, "role2") + }) + + it("should not be able to create user that exists in same tenant", async () => { + const user = await config.createUser() + jest.clearAllMocks() + delete user._id + delete user._rev + + const response = await api.users.saveUser(user, 400) + + expect(response.body.message).toBe(`Unavailable`) + expect(events.user.created).toBeCalledTimes(0) + }) + + it("should not be able to create user that exists in other tenant", async () => { + const user = await config.createUser() + jest.resetAllMocks() + + await tenancy.doInTenant(TENANT_1, async () => { + delete user._id + const response = await api.users.saveUser(user, 400) + + expect(response.body.message).toBe(`Unavailable`) + expect(events.user.created).toBeCalledTimes(0) + }) + }) + + it("should not be able to create user with the same email as an account", async () => { + const user = structures.users.user() + const account = structures.accounts.cloudAccount() + mocks.accounts.getAccount.mockReturnValueOnce(account) + + const response = await api.users.saveUser(user, 400) + + expect(response.body.message).toBe(`Unavailable`) + expect(events.user.created).toBeCalledTimes(0) + }) + }) + + describe("update", () => { + it("should be able to update a basic user", async () => { + const user = await config.createUser() + jest.clearAllMocks() + + await api.users.saveUser(user) + + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.user.permissionBuilderAssigned).not.toBeCalled() + expect(events.user.permissionAdminAssigned).not.toBeCalled() + expect(events.user.passwordForceReset).not.toBeCalled() + }) + + it("should be able to force reset password", async () => { + const user = await config.createUser() + jest.clearAllMocks() + + user.forceResetPassword = true + user.password = "tempPassword" + await api.users.saveUser(user) + + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.user.permissionBuilderAssigned).not.toBeCalled() + expect(events.user.permissionAdminAssigned).not.toBeCalled() + expect(events.user.passwordForceReset).toBeCalledTimes(1) + }) + + it("should be able to update a basic user to an admin user", async () => { + const user = await config.createUser() + jest.clearAllMocks() + + await api.users.saveUser(structures.users.adminUser(user)) + + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) + expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) + }) + + it("should be able to update a basic user to a builder user", async () => { + const user = await config.createUser() + jest.clearAllMocks() + + await api.users.saveUser(structures.users.builderUser(user)) + + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) + expect(events.user.permissionAdminAssigned).not.toBeCalled() + }) + + it("should be able to update an admin user to a basic user", async () => { + const user = await config.createUser(structures.users.adminUser()) + jest.clearAllMocks() + user.admin!.global = false + user.builder!.global = false + + await api.users.saveUser(user) + + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) + expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) + }) + + it("should be able to update an builder user to a basic user", async () => { + const user = await config.createUser(structures.users.builderUser()) + jest.clearAllMocks() + user.builder!.global = false + + await api.users.saveUser(user) + + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) + expect(events.user.permissionAdminRemoved).not.toBeCalled() + }) + + it("should be able to assign app roles", async () => { + const user = await config.createUser() + jest.clearAllMocks() + user.roles = { + app_123: "role1", + app_456: "role2", + } + + await api.users.saveUser(user) + + const savedUser = await config.getUser(user.email) + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.role.assigned).toBeCalledTimes(2) + expect(events.role.assigned).toBeCalledWith(savedUser, "role1") + expect(events.role.assigned).toBeCalledWith(savedUser, "role2") + }) + + it("should be able to unassign app roles", async () => { + let user = structures.users.user() + user.roles = { + app_123: "role1", + app_456: "role2", + } + user = await config.createUser(user) + jest.clearAllMocks() + user.roles = {} + + await api.users.saveUser(user) + + const savedUser = await config.getUser(user.email) + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.role.unassigned).toBeCalledTimes(2) + expect(events.role.unassigned).toBeCalledWith(savedUser, "role1") + expect(events.role.unassigned).toBeCalledWith(savedUser, "role2") + }) + + it("should be able to update existing app roles", async () => { + let user = structures.users.user() + user.roles = { + app_123: "role1", + app_456: "role2", + } + user = await config.createUser(user) + jest.clearAllMocks() + user.roles = { + app_123: "role1", + app_456: "role2-edit", + } + + await api.users.saveUser(user) + + const savedUser = await config.getUser(user.email) + expect(events.user.created).not.toBeCalled() + expect(events.user.updated).toBeCalledTimes(1) + expect(events.role.unassigned).toBeCalledTimes(1) + expect(events.role.unassigned).toBeCalledWith(savedUser, "role2") + expect(events.role.assigned).toBeCalledTimes(1) + expect(events.role.assigned).toBeCalledWith(savedUser, "role2-edit") + }) + + it("should not be able to update email address", async () => { + const email = "email@test.com" + const user = await config.createUser(structures.users.user({ email })) + user.email = "new@test.com" + + const response = await api.users.saveUser(user, 400) + + const dbUser = await config.getUser(email) + user.email = email + expect(user).toStrictEqual(dbUser) + expect(response.body.message).toBe("Email address cannot be changed") + }) + }) + + describe("bulkDelete", () => { + it("should not be able to bulkDelete current user", async () => { + const user = await config.defaultUser! + const request = { userIds: [user._id!] } + + const response = await api.users.bulkDeleteUsers(request, 400) + + expect(response.body.message).toBe("Unable to delete self.") + expect(events.user.deleted).not.toBeCalled() + }) + + it("should not be able to bulkDelete account owner", async () => { + const user = await config.createUser() + const account = structures.accounts.cloudAccount() + account.budibaseUserId = user._id! + mocks.accounts.getAccountByTenantId.mockReturnValue(account) + + const request = { userIds: [user._id!] } + + const response = await api.users.bulkDeleteUsers(request) + + expect(response.body.successful.length).toBe(0) + expect(response.body.unsuccessful.length).toBe(1) + expect(response.body.unsuccessful[0].reason).toBe( + "Account holder cannot be deleted" + ) + expect(response.body.unsuccessful[0]._id).toBe(user._id) + expect(events.user.deleted).not.toBeCalled() + }) + + it("should be able to bulk delete users", async () => { + const account = structures.accounts.cloudAccount() + mocks.accounts.getAccountByTenantId.mockReturnValue(account) + + const builder = structures.users.builderUser() + const admin = structures.users.adminUser() + const user = structures.users.user() + const createdUsers = await api.users.bulkCreateUsers([ + builder, + admin, + user, + ]) + const request = { userIds: createdUsers.successful.map(u => u._id!) } + + const response = await api.users.bulkDeleteUsers(request) + + expect(response.body.successful.length).toBe(3) + expect(response.body.unsuccessful.length).toBe(0) + expect(events.user.deleted).toBeCalledTimes(3) + expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) + expect(events.user.permissionBuilderRemoved).toBeCalledTimes(2) + }) + }) + + describe("destroy", () => { + it("should be able to destroy a basic user", async () => { + const user = await config.createUser() + jest.clearAllMocks() + + await api.users.deleteUser(user._id!) + + expect(events.user.deleted).toBeCalledTimes(1) + expect(events.user.permissionBuilderRemoved).not.toBeCalled() + expect(events.user.permissionAdminRemoved).not.toBeCalled() + }) + + it("should be able to destroy an admin user", async () => { + const user = await config.createUser(structures.users.adminUser()) + jest.clearAllMocks() + + await api.users.deleteUser(user._id!) + + expect(events.user.deleted).toBeCalledTimes(1) + expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) + expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) + }) + + it("should be able to destroy a builder user", async () => { + const user = await config.createUser(structures.users.builderUser()) + jest.clearAllMocks() + + await api.users.deleteUser(user._id!) + + expect(events.user.deleted).toBeCalledTimes(1) + expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) + expect(events.user.permissionAdminRemoved).not.toBeCalled() + }) + + it("should not be able to destroy account owner", async () => { + const user = await config.createUser() + const account = structures.accounts.cloudAccount() + mocks.accounts.getAccount.mockReturnValueOnce(account) + + const response = await api.users.deleteUser(user._id!, 400) + + expect(response.body.message).toBe("Account holder cannot be deleted") + }) + + it("should not be able to destroy account owner as account owner", async () => { + const user = await config.defaultUser! + const account = structures.accounts.cloudAccount() + account.email = user.email + mocks.accounts.getAccount.mockReturnValueOnce(account) + + const response = await api.users.deleteUser(user._id!, 400) + + expect(response.body.message).toBe("Unable to delete self.") + }) + }) +}) diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index 550d14a9a3..7f5c783caa 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -12,6 +12,7 @@ const statusRoutes = require("./system/status") const selfRoutes = require("./global/self") const licenseRoutes = require("./global/license") const migrationRoutes = require("./system/migrations") +const accountRoutes = require("./system/accounts") let userGroupRoutes = api.groups exports.routes = [ @@ -29,4 +30,5 @@ exports.routes = [ licenseRoutes, userGroupRoutes, migrationRoutes, + accountRoutes, ] diff --git a/packages/worker/src/api/routes/system/accounts.ts b/packages/worker/src/api/routes/system/accounts.ts new file mode 100644 index 0000000000..61a46ae437 --- /dev/null +++ b/packages/worker/src/api/routes/system/accounts.ts @@ -0,0 +1,19 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/system/accounts" +import { middleware } from "@budibase/backend-core" + +const router = new Router() + +router + .put( + "/api/system/accounts/:accountId/metadata", + middleware.internalApi, + controller.save + ) + .delete( + "/api/system/accounts/:accountId/metadata", + middleware.internalApi, + controller.destroy + ) + +export = router diff --git a/packages/worker/src/api/routes/system/tests/accounts.spec.ts b/packages/worker/src/api/routes/system/tests/accounts.spec.ts new file mode 100644 index 0000000000..b20b7a6472 --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/accounts.spec.ts @@ -0,0 +1,57 @@ +import { accounts } from "../../../../sdk" +import { TestConfiguration, structures, API } from "../../../../tests" +import { v4 as uuid } from "uuid" + +describe("accounts", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("metadata", () => { + describe("saveMetadata", () => { + it("saves account metadata", async () => { + let account = structures.accounts.account() + + const response = await api.accounts.saveMetadata(account) + + const id = accounts.formatAccountMetadataId(account.accountId) + const metadata = await accounts.getMetadata(id) + expect(response).toStrictEqual(metadata) + }) + }) + + describe("destroyMetadata", () => { + it("destroys account metadata", async () => { + const account = structures.accounts.account() + await api.accounts.saveMetadata(account) + + await api.accounts.destroyMetadata(account.accountId) + + const deleted = await accounts.getMetadata(account.accountId) + expect(deleted).toBe(undefined) + }) + + it("destroys account metadata that does not exist", async () => { + const id = uuid() + + const response = await api.accounts.destroyMetadata(id) + + expect(response.status).toBe(404) + expect(response.body.message).toBe( + `id=${accounts.formatAccountMetadataId(id)} does not exist` + ) + }) + }) + }) +}) diff --git a/packages/worker/src/api/routes/tests/users.spec.js b/packages/worker/src/api/routes/tests/users.spec.js deleted file mode 100644 index 5194823615..0000000000 --- a/packages/worker/src/api/routes/tests/users.spec.js +++ /dev/null @@ -1,391 +0,0 @@ -jest.mock("nodemailer") -const { config, request, mocks, structures } = require("../../../tests") -const sendMailMock = mocks.email.mock() -const { events } = require("@budibase/backend-core") - -describe("/api/global/users", () => { - - beforeAll(async () => { - await config.beforeAll() - }) - - afterAll(async () => { - await config.afterAll() - }) - - const sendUserInvite = async () => { - await config.saveSmtpConfig() - await config.saveSettingsConfig() - const res = await request - .post(`/api/global/users/invite`) - .send({ - email: "invite@test.com", - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - - const emailCall = sendMailMock.mock.calls[0][0] - // after this URL there should be a code - const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=") - const code = parts[1].split("\"")[0].split("&")[0] - return { code, res } - } - - it("should be able to generate an invitation", async () => { - const { code, res } = await sendUserInvite() - - expect(res.body).toEqual({ message: "Invitation has been sent." }) - expect(sendMailMock).toHaveBeenCalled() - expect(code).toBeDefined() - expect(events.user.invited).toBeCalledTimes(1) - }) - - it("should be able to create new user from invite", async () => { - const { code } = await sendUserInvite() - - const res = await request - .post(`/api/global/users/invite/accept`) - .send({ - password: "newpassword", - inviteCode: code, - }) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body._id).toBeDefined() - const user = await config.getUser("invite@test.com") - expect(user).toBeDefined() - expect(user._id).toEqual(res.body._id) - expect(events.user.inviteAccepted).toBeCalledTimes(1) - expect(events.user.inviteAccepted).toBeCalledWith(user) - }) - - const createUser = async (user) => { - const existing = await config.getUser(user.email) - if (existing) { - await deleteUser(existing._id) - } - return saveUser(user) - } - - const updateUser = async (user) => { - const existing = await config.getUser(user.email) - user._id = existing._id - return saveUser(user) - } - - const saveUser = async (user) => { - const res = await request - .post(`/api/global/users`) - .send(user) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - return res.body - } - - - const bulkCreateUsers = async (users) => { - const res = await request - .post(`/api/global/users/bulkCreate`) - .send(users) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - return res.body - } - - const bulkDeleteUsers = async (users) => { - const res = await request - .post(`/api/global/users/bulkDelete`) - .send(users) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - return res.body - } - - - - const deleteUser = async (email) => { - const user = await config.getUser(email) - if (user) { - await request - .delete(`/api/global/users/${user._id}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - } - } - - describe("create", () => { - it("should be able to create a basic user", async () => { - jest.clearAllMocks() - const user = structures.users.user({ email: "basic@test.com" }) - await createUser(user) - - expect(events.user.created).toBeCalledTimes(1) - expect(events.user.updated).not.toBeCalled() - expect(events.user.permissionBuilderAssigned).not.toBeCalled() - expect(events.user.permissionAdminAssigned).not.toBeCalled() - }) - - it("should be able to bulkCreate users with different permissions", async () => { - jest.clearAllMocks() - const builder = structures.users.builderUser({ email: "bulkbasic@test.com" }) - const admin = structures.users.adminUser({ email: "bulkadmin@test.com" }) - const user = structures.users.user({ email: "bulkuser@test.com" }) - - let toCreate = { users: [builder, admin, user], groups: [] } - await bulkCreateUsers(toCreate) - - expect(events.user.created).toBeCalledTimes(3) - expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) - expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) - }) - - - it("should be able to create an admin user", async () => { - jest.clearAllMocks() - const user = structures.users.adminUser({ email: "admin@test.com" }) - await createUser(user) - - expect(events.user.created).toBeCalledTimes(1) - expect(events.user.updated).not.toBeCalled() - expect(events.user.permissionBuilderAssigned).not.toBeCalled() - expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) - }) - - it("should be able to create a builder user", async () => { - jest.clearAllMocks() - const user = structures.users.builderUser({ email: "builder@test.com" }) - await createUser(user) - - expect(events.user.created).toBeCalledTimes(1) - expect(events.user.updated).not.toBeCalled() - expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) - expect(events.user.permissionAdminAssigned).not.toBeCalled() - }) - - it("should be able to assign app roles", async () => { - jest.clearAllMocks() - const user = structures.users.user({ email: "assign-roles@test.com" }) - user.roles = { - "app_123": "role1", - "app_456": "role2", - } - - await createUser(user) - const savedUser = await config.getUser(user.email) - - expect(events.user.created).toBeCalledTimes(1) - expect(events.user.updated).not.toBeCalled() - expect(events.role.assigned).toBeCalledTimes(2) - expect(events.role.assigned).toBeCalledWith(savedUser, "role1") - expect(events.role.assigned).toBeCalledWith(savedUser, "role2") - }) - }) - - describe("update", () => { - it("should be able to update a basic user", async () => { - let user = structures.users.user({ email: "basic-update@test.com" }) - await createUser(user) - jest.clearAllMocks() - - await updateUser(user) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.permissionBuilderAssigned).not.toBeCalled() - expect(events.user.permissionAdminAssigned).not.toBeCalled() - expect(events.user.passwordForceReset).not.toBeCalled() - }) - - it("should be able to force reset password", async () => { - let user = structures.users.user({ email: "basic-password-update@test.com" }) - await createUser(user) - jest.clearAllMocks() - - user.forceResetPassword = true - user.password = "tempPassword" - await updateUser(user) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.permissionBuilderAssigned).not.toBeCalled() - expect(events.user.permissionAdminAssigned).not.toBeCalled() - expect(events.user.passwordForceReset).toBeCalledTimes(1) - }) - - it("should be able to update a basic user to an admin user", async () => { - let user = structures.users.user({ email: "basic-update-admin@test.com" }) - await createUser(user) - jest.clearAllMocks() - - await updateUser(structures.users.adminUser(user)) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.permissionBuilderAssigned).not.toBeCalled() - expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) - }) - - it("should be able to update a basic user to a builder user", async () => { - let user = structures.users.user({ email: "basic-update-builder@test.com" }) - await createUser(user) - jest.clearAllMocks() - - await updateUser(structures.users.builderUser(user)) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) - expect(events.user.permissionAdminAssigned).not.toBeCalled() - }) - - it("should be able to update an admin user to a basic user", async () => { - let user = structures.users.adminUser({ email: "admin-update-basic@test.com" }) - await createUser(user) - jest.clearAllMocks() - - user.admin.global = false - await updateUser(user) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) - expect(events.user.permissionBuilderRemoved).not.toBeCalled() - }) - - it("should be able to update an builder user to a basic user", async () => { - let user = structures.users.builderUser({ email: "builder-update-basic@test.com" }) - await createUser(user) - jest.clearAllMocks() - - user.builder.global = false - await updateUser(user) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) - expect(events.user.permissionAdminRemoved).not.toBeCalled() - }) - - it("should be able to assign app roles", async () => { - const user = structures.users.user({ email: "assign-roles-update@test.com" }) - await createUser(user) - jest.clearAllMocks() - - user.roles = { - "app_123": "role1", - "app_456": "role2", - } - await updateUser(user) - const savedUser = await config.getUser(user.email) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.role.assigned).toBeCalledTimes(2) - expect(events.role.assigned).toBeCalledWith(savedUser, "role1") - expect(events.role.assigned).toBeCalledWith(savedUser, "role2") - }) - - it("should be able to unassign app roles", async () => { - const user = structures.users.user({ email: "unassign-roles@test.com" }) - user.roles = { - "app_123": "role1", - "app_456": "role2", - } - await createUser(user) - jest.clearAllMocks() - - user.roles = {} - await updateUser(user) - const savedUser = await config.getUser(user.email) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.role.unassigned).toBeCalledTimes(2) - expect(events.role.unassigned).toBeCalledWith(savedUser, "role1") - expect(events.role.unassigned).toBeCalledWith(savedUser, "role2") - }) - - it("should be able to update existing app roles", async () => { - const user = structures.users.user({ email: "update-roles@test.com" }) - user.roles = { - "app_123": "role1", - "app_456": "role2", - } - await createUser(user) - jest.clearAllMocks() - - user.roles = { - "app_123": "role1", - "app_456": "role2-edit", - } - await updateUser(user) - const savedUser = await config.getUser(user.email) - - expect(events.user.created).not.toBeCalled() - expect(events.user.updated).toBeCalledTimes(1) - expect(events.role.unassigned).toBeCalledTimes(1) - expect(events.role.unassigned).toBeCalledWith(savedUser, "role2") - expect(events.role.assigned).toBeCalledTimes(1) - expect(events.role.assigned).toBeCalledWith(savedUser, "role2-edit") - }) - }) - - describe("destroy", () => { - it("should be able to destroy a basic user", async () => { - let user = structures.users.user({ email: "destroy@test.com" }) - await createUser(user) - jest.clearAllMocks() - - await deleteUser(user.email) - - expect(events.user.deleted).toBeCalledTimes(1) - expect(events.user.permissionBuilderRemoved).not.toBeCalled() - expect(events.user.permissionAdminRemoved).not.toBeCalled() - }) - - it("should be able to destroy an admin user", async () => { - let user = structures.users.adminUser({ email: "destroy-admin@test.com" }) - await createUser(user) - jest.clearAllMocks() - - await deleteUser(user.email) - - expect(events.user.deleted).toBeCalledTimes(1) - expect(events.user.permissionBuilderRemoved).not.toBeCalled() - expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) - }) - - it("should be able to destroy a builder user", async () => { - let user = structures.users.builderUser({ email: "destroy-admin@test.com" }) - await createUser(user) - jest.clearAllMocks() - - await deleteUser(user.email) - - expect(events.user.deleted).toBeCalledTimes(1) - expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) - expect(events.user.permissionAdminRemoved).not.toBeCalled() - }) - - it("should be able to bulk delete users with different permissions", async () => { - jest.clearAllMocks() - const builder = structures.users.builderUser({ email: "basic@test.com" }) - const admin = structures.users.adminUser({ email: "admin@test.com" }) - const user = structures.users.user({ email: "user@test.com" }) - - let toCreate = { users: [builder, admin, user], groups: [] } - let createdUsers = await bulkCreateUsers(toCreate) - await bulkDeleteUsers({ userIds: [createdUsers[0]._id, createdUsers[1]._id, createdUsers[2]._id] }) - expect(events.user.deleted).toBeCalledTimes(3) - expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) - expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) - - }) - - }) -}) \ No newline at end of file diff --git a/packages/worker/src/environment.js b/packages/worker/src/environment.ts similarity index 88% rename from packages/worker/src/environment.js rename to packages/worker/src/environment.ts index bb45c1dd78..fd6749a0f7 100644 --- a/packages/worker/src/environment.js +++ b/packages/worker/src/environment.ts @@ -20,13 +20,13 @@ if (!LOADED && isDev() && !isTest()) { LOADED = true } -function parseIntSafe(number) { +function parseIntSafe(number: any) { if (number) { return parseInt(number) } } -module.exports = { +const env = { // auth MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, @@ -47,7 +47,7 @@ module.exports = { CLUSTER_PORT: process.env.CLUSTER_PORT, // flags NODE_ENV: process.env.NODE_ENV, - SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), + SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), LOG_LEVEL: process.env.LOG_LEVEL, MULTI_TENANCY: process.env.MULTI_TENANCY, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, @@ -62,7 +62,7 @@ module.exports = { // other CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, - _set(key, value) { + _set(key: any, value: any) { process.env[key] = value module.exports[key] = value }, @@ -74,16 +74,17 @@ module.exports = { } // if some var haven't been set, define them -if (!module.exports.APPS_URL) { - module.exports.APPS_URL = isDev() - ? "http://localhost:4001" - : "http://app-service:4002" +if (!env.APPS_URL) { + env.APPS_URL = isDev() ? "http://localhost:4001" : "http://app-service:4002" } // clean up any environment variable edge cases for (let [key, value] of Object.entries(module.exports)) { // handle the edge case of "0" to disable an environment variable if (value === "0") { - module.exports[key] = 0 + // @ts-ignore + env[key] = 0 } } + +export = env diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 6fb954a1b5..7b8c6e68d8 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -1,10 +1,18 @@ // need to load environment first +const env = require("./environment") + +// enable APM if configured +if (process.env.ELASTIC_APM_ENABLED) { + const apm = require("elastic-apm-node").start({ + serviceName: process.env.SERVICE, + environment: process.env.BUDIBASE_ENVIRONMENT, + }) +} + import { Scope } from "@sentry/node" import { Event } from "@sentry/types/dist/event" import Application from "koa" import { bootstrap } from "global-agent" - -const env = require("./environment") import db from "./db" db.init() const Koa = require("koa") @@ -63,9 +71,7 @@ server.on("close", async () => { return } shuttingDown = true - if (!env.isTest()) { - console.log("Server Closed") - } + console.log("Server Closed") await redis.shutdown() await events.shutdown() if (!env.isTest()) { @@ -78,7 +84,7 @@ const shutdown = () => { server.destroy() } -module.exports = server.listen(parseInt(env.PORT || 4002), async () => { +export = server.listen(parseInt(env.PORT || 4002), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) await redis.init() }) @@ -92,3 +98,7 @@ process.on("uncaughtException", err => { process.on("SIGTERM", () => { shutdown() }) + +process.on("SIGINT", () => { + shutdown() +}) diff --git a/packages/worker/src/sdk/accounts/accounts.ts b/packages/worker/src/sdk/accounts/accounts.ts new file mode 100644 index 0000000000..d18317aeb2 --- /dev/null +++ b/packages/worker/src/sdk/accounts/accounts.ts @@ -0,0 +1,53 @@ +import { AccountMetadata } from "@budibase/types" +import { + db, + StaticDatabases, + HTTPError, + DocumentType, + SEPARATOR, +} from "@budibase/backend-core" + +export const formatAccountMetadataId = (accountId: string) => { + return `${DocumentType.ACCOUNT_METADATA}${SEPARATOR}${accountId}` +} + +export const saveMetadata = async ( + metadata: AccountMetadata +): Promise => { + return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { + const existing = await getMetadata(metadata._id!) + if (existing) { + metadata._rev = existing._rev + } + const res = await db.put(metadata) + metadata._rev = res.rev + return metadata + }) +} + +export const getMetadata = async ( + accountId: string +): Promise => { + return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { + try { + return await db.get(accountId) + } catch (e: any) { + if (e.status === 404) { + // do nothing + return + } else { + throw e + } + } + }) +} + +export const destroyMetadata = async (accountId: string) => { + await db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { + const metadata = await getMetadata(accountId) + if (!metadata) { + throw new HTTPError(`id=${accountId} does not exist`, 404) + } + await db.remove(accountId, metadata._rev) + }) +} diff --git a/packages/worker/src/sdk/accounts/index.ts b/packages/worker/src/sdk/accounts/index.ts new file mode 100644 index 0000000000..f2ae03040e --- /dev/null +++ b/packages/worker/src/sdk/accounts/index.ts @@ -0,0 +1 @@ +export * from "./accounts" diff --git a/packages/worker/src/sdk/index.ts b/packages/worker/src/sdk/index.ts index 1b1c47b924..fdc1098361 100644 --- a/packages/worker/src/sdk/index.ts +++ b/packages/worker/src/sdk/index.ts @@ -1 +1,2 @@ export * as users from "./users" +export * as accounts from "./accounts" diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index f55ddb3b54..d1c076443d 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -2,20 +2,39 @@ import env from "../../environment" import * as apps from "../../utilities/appService" import * as eventHelpers from "./events" import { - tenancy, - utils, - db as dbUtils, - constants, - cache, - users as usersCore, - deprovisioning, - sessions, - HTTPError, accounts, + cache, + constants, + db as dbUtils, + deprovisioning, + events, + HTTPError, migrations, + sessions, + StaticDatabases, + tenancy, + users as usersCore, + utils, + ViewName, } from "@budibase/backend-core" -import { MigrationType, User } from "@budibase/types" +import { + AccountMetadata, + AllDocsResponse, + BulkCreateUsersResponse, + BulkDeleteUsersResponse, + BulkDocsResponse, + CloudAccount, + CreateUserResponse, + InviteUsersRequest, + InviteUsersResponse, + MigrationType, + PlatformUserByEmail, + RowResponse, + User, +} from "@budibase/types" import { groups as groupUtils } from "@budibase/pro" +import { sendEmail } from "../../utilities/email" +import { EmailTemplatePurpose } from "../../constants" const PAGE_LIMIT = 8 @@ -97,7 +116,6 @@ export const getUser = async (userId: string) => { interface SaveUserOpts { hashPassword?: boolean requirePassword?: boolean - bulkCreate?: boolean } const buildUser = async ( @@ -108,7 +126,7 @@ const buildUser = async ( }, tenantId: string, dbUser?: any -) => { +): Promise => { let { password, _id } = user let hashedPassword @@ -142,62 +160,63 @@ const buildUser = async ( return user } +const validateUniqueUser = async (email: string, tenantId: string) => { + // check budibase users in other tenants + if (env.MULTI_TENANCY) { + const tenantUser = await tenancy.getTenantUser(email) + if (tenantUser != null && tenantUser.tenantId !== tenantId) { + throw `Unavailable` + } + } + + // check root account users in account portal + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + const account = await accounts.getAccount(email) + if (account && account.verified && account.tenantId !== tenantId) { + throw `Unavailable` + } + } +} + export const save = async ( user: User, opts: SaveUserOpts = { hashPassword: true, requirePassword: true, - bulkCreate: false, } -) => { +): Promise => { const tenantId = tenancy.getTenantId() const db = tenancy.getGlobalDB() let { email, _id } = user - // make sure another user isn't using the same email + let dbUser: User | undefined - if (opts.bulkCreate) { - dbUser = undefined + if (_id) { + // try to get existing user from db + dbUser = (await db.get(_id)) as User + if (email && dbUser.email !== email) { + throw "Email address cannot be changed" + } + email = dbUser.email } else if (email) { - // check budibase users inside the tenant + // no id was specified - load from email instead dbUser = await usersCore.getGlobalUserByEmail(email) - if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { - throw `Email address ${email} already in use.` + if (dbUser && dbUser._id !== _id) { + throw `Unavailable` } - - // check budibase users in other tenants - if (env.MULTI_TENANCY) { - const tenantUser = await tenancy.getTenantUser(email) - if (tenantUser != null && tenantUser.tenantId !== tenantId) { - throw `Email address ${email} already in use.` - } - } - - // check root account users in account portal - if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { - const account = await accounts.getAccount(email) - if (account && account.verified && account.tenantId !== tenantId) { - throw `Email address ${email} already in use.` - } - } - } else if (_id) { - dbUser = await db.get(_id) + } else { + throw new Error("_id or email is required") } + await validateUniqueUser(email, tenantId) + let builtUser = await buildUser(user, opts, tenantId, dbUser) // make sure we set the _id field for a new user if (!_id) { - _id = builtUser._id as string + _id = builtUser._id! } try { - const putOpts = { - password: builtUser.password, - ...user, - } - if (opts.bulkCreate) { - return putOpts - } // save the user to db let response = await db.put(builtUser) builtUser._rev = response.rev @@ -237,29 +256,102 @@ export const addTenant = async ( } } +const getExistingTenantUsers = async (emails: string[]): Promise => { + const params = { + keys: emails, + include_docs: true, + } + const opts = { + arrayResponse: true, + } + return dbUtils.queryGlobalView( + ViewName.USER_BY_EMAIL, + params, + undefined, + opts + ) as Promise +} + +const getExistingPlatformUsers = async ( + emails: string[] +): Promise => { + return dbUtils.doWithDB( + StaticDatabases.PLATFORM_INFO.name, + async (infoDb: any) => { + const response: AllDocsResponse = + await infoDb.allDocs({ + keys: emails, + include_docs: true, + }) + return response.rows + .filter(row => row.doc && (row.error !== "not_found") !== null) + .map((row: any) => row.doc) + } + ) +} + +const getExistingAccounts = async ( + emails: string[] +): Promise => { + const params = { + keys: emails, + include_docs: true, + } + const opts = { + arrayResponse: true, + } + return dbUtils.queryPlatformView( + ViewName.ACCOUNT_BY_EMAIL, + params, + opts + ) as Promise +} + +/** + * Apply a system-wide search on emails: + * - in tenant + * - cross tenant + * - accounts + * return an array of emails that match the supplied emails. + */ +const searchExistingEmails = async (emails: string[]) => { + let matchedEmails: string[] = [] + + const existingTenantUsers = await getExistingTenantUsers(emails) + matchedEmails.push(...existingTenantUsers.map(user => user.email)) + + const existingPlatformUsers = await getExistingPlatformUsers(emails) + matchedEmails.push(...existingPlatformUsers.map(user => user._id!)) + + const existingAccounts = await getExistingAccounts(emails) + matchedEmails.push(...existingAccounts.map(account => account.email)) + + return [...new Set(matchedEmails)] +} + export const bulkCreate = async ( newUsersRequested: User[], groups: string[] -) => { +): Promise => { const db = tenancy.getGlobalDB() const tenantId = tenancy.getTenantId() let usersToSave: any[] = [] let newUsers: any[] = [] - const allUsers = await db.allDocs( - dbUtils.getGlobalUserParams(null, { - include_docs: true, - }) - ) - let mapped = allUsers.rows.map((row: any) => row.id) + const emails = newUsersRequested.map((user: User) => user.email) + const existingEmails = await searchExistingEmails(emails) + const unsuccessful: { email: string; reason: string }[] = [] - const currentUserEmails = mapped.map((x: any) => x.email) || [] for (const newUser of newUsersRequested) { if ( newUsers.find((x: any) => x.email === newUser.email) || - currentUserEmails.includes(newUser.email) + existingEmails.includes(newUser.email) ) { + unsuccessful.push({ + email: newUser.email, + reason: `Unavailable`, + }) continue } newUser.userGroups = groups @@ -292,62 +384,129 @@ export const bulkCreate = async ( await apps.syncUserInApps(user._id) } - return usersToBulkSave.map(user => { + const saved = usersToBulkSave.map(user => { return { _id: user._id, email: user.email, } }) + + return { + successful: saved, + unsuccessful, + } } -export const bulkDelete = async (userIds: any) => { +/** + * For the given user id's, return the account holder if it is in the ids. + */ +const getAccountHolderFromUserIds = async ( + userIds: string[] +): Promise => { + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + const tenantId = tenancy.getTenantId() + const account = await accounts.getAccountByTenantId(tenantId) + if (!account) { + throw new Error(`Account not found for tenantId=${tenantId}`) + } + + const budibaseUserId = account.budibaseUserId + if (userIds.includes(budibaseUserId)) { + return account + } + } +} + +export const bulkDelete = async ( + userIds: string[] +): Promise => { const db = tenancy.getGlobalDB() + const response: BulkDeleteUsersResponse = { + successful: [], + unsuccessful: [], + } + + // remove the account holder from the delete request if present + const account = await getAccountHolderFromUserIds(userIds) + if (account) { + userIds = userIds.filter(u => u !== account.budibaseUserId) + // mark user as unsuccessful + response.unsuccessful.push({ + _id: account.budibaseUserId, + email: account.email, + reason: "Account holder cannot be deleted", + }) + } + let groupsToModify: any = {} // Get users and delete - let usersToDelete = ( - await db.allDocs({ - include_docs: true, - keys: userIds, - }) - ).rows.map((user: any) => { - // if we find a user that has an associated group, add it to - // an array so we can easily use allDocs on them later. - // This prevents us having to re-loop over all the users - if (user.doc.userGroups) { - for (let groupId of user.doc.userGroups) { - if (!Object.keys(groupsToModify).includes(groupId)) { - groupsToModify[groupId] = [user.id] - } else { - groupsToModify[groupId] = [...groupsToModify[groupId], user.id] + const allDocsResponse: AllDocsResponse = await db.allDocs({ + include_docs: true, + keys: userIds, + }) + const usersToDelete: User[] = allDocsResponse.rows.map( + (user: RowResponse) => { + // if we find a user that has an associated group, add it to + // an array so we can easily use allDocs on them later. + // This prevents us having to re-loop over all the users + if (user.doc.userGroups) { + for (let groupId of user.doc.userGroups) { + if (!Object.keys(groupsToModify).includes(groupId)) { + groupsToModify[groupId] = [user.id] + } else { + groupsToModify[groupId] = [...groupsToModify[groupId], user.id] + } } } + + return user.doc } + ) - return user.doc - }) - - const response = await db.bulkDocs( - usersToDelete.map((user: any) => ({ + // Delete from DB + const dbResponse: BulkDocsResponse = await db.bulkDocs( + usersToDelete.map(user => ({ ...user, _deleted: true, })) ) + // Deletion post processing await groupUtils.bulkDeleteGroupUsers(groupsToModify) - - //Deletion post processing for (let user of usersToDelete) { await bulkDeleteProcessing(user) } + // Build Response + // index users by id + const userIndex: { [key: string]: User } = {} + usersToDelete.reduce((prev, current) => { + prev[current._id!] = current + return prev + }, userIndex) + + // add the successful and unsuccessful users to response + dbResponse.forEach(item => { + const email = userIndex[item.id].email + if (item.ok) { + response.successful.push({ _id: item.id, email }) + } else { + response.unsuccessful.push({ + _id: item.id, + email, + reason: "Database error", + }) + } + }) + return response } export const destroy = async (id: string, currentUser: any) => { const db = tenancy.getGlobalDB() const dbUser = await db.get(id) - const userId = dbUser._id + const userId = dbUser._id as string let groups = dbUser.userGroups if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { @@ -387,3 +546,53 @@ const bulkDeleteProcessing = async (dbUser: User) => { // let server know to sync user await apps.syncUserInApps(userId) } + +export const invite = async ( + users: InviteUsersRequest +): Promise => { + const response: InviteUsersResponse = { + successful: [], + unsuccessful: [], + } + + const matchedEmails = await searchExistingEmails(users.map(u => u.email)) + const newUsers = [] + + // separate duplicates from new users + for (let user of users) { + if (matchedEmails.includes(user.email)) { + response.unsuccessful.push({ email: user.email, reason: "Unavailable" }) + } else { + newUsers.push(user) + } + } + // overwrite users with new only + users = newUsers + + // send the emails for new users + const tenantId = tenancy.getTenantId() + for (let user of users) { + try { + let userInfo = user.userInfo + if (!userInfo) { + userInfo = {} + } + userInfo.tenantId = tenantId + const opts: any = { + subject: "{{ company }} platform invitation", + info: userInfo, + } + await sendEmail(user.email, EmailTemplatePurpose.INVITATION, opts) + response.successful.push({ email: user.email }) + await events.user.invited() + } catch (e) { + console.error(`Failed to send email invitation email=${user.email}`, e) + response.unsuccessful.push({ + email: user.email, + reason: "Failed to send email", + }) + } + } + + return response +} diff --git a/packages/worker/src/tests/TestConfiguration.js b/packages/worker/src/tests/TestConfiguration.js deleted file mode 100644 index 694c56bfcd..0000000000 --- a/packages/worker/src/tests/TestConfiguration.js +++ /dev/null @@ -1,231 +0,0 @@ -require("./mocks") -require("../db").init() -const env = require("../environment") -const controllers = require("./controllers") -const supertest = require("supertest") -const { jwt } = require("@budibase/backend-core/auth") -const { Cookies, Headers } = require("@budibase/backend-core/constants") -const { Configs } = require("../constants") -const { users } = require("@budibase/backend-core") -const { createASession } = require("@budibase/backend-core/sessions") -const { TENANT_ID, CSRF_TOKEN } = require("./structures") -const structures = require("./structures") -const { doInTenant } = require("@budibase/backend-core/tenancy") -const { groups } = require("@budibase/pro") -class TestConfiguration { - constructor(openServer = true) { - if (openServer) { - env.PORT = "0" // random port - this.server = require("../index") - // we need the request for logging in, involves cookies, hard to fake - this.request = supertest(this.server) - } - } - - getRequest() { - return this.request - } - - // UTILS - - async _req(config, params, controlFunc) { - const request = {} - // fake cookies, we don't need them - request.cookies = { set: () => {}, get: () => {} } - request.config = { jwtSecret: env.JWT_SECRET } - request.appId = this.appId - request.user = { appId: this.appId, tenantId: TENANT_ID } - request.query = {} - request.request = { - body: config, - } - request.throw = (status, err) => { - throw { status, message: err } - } - if (params) { - request.params = params - } - await doInTenant(TENANT_ID, () => { - return controlFunc(request) - }) - return request.body - } - - // SETUP / TEARDOWN - - async beforeAll() { - await this.login() - } - - async afterAll() { - if (this.server) { - await this.server.close() - } - } - - // USER / AUTH - - async login() { - // create a test user - await this._req( - { - email: "test@test.com", - password: "test", - _id: "us_uuid1", - builder: { - global: true, - }, - admin: { - global: true, - }, - }, - null, - controllers.users.save - ) - await createASession("us_uuid1", { - sessionId: "sessionid", - tenantId: TENANT_ID, - csrfToken: CSRF_TOKEN, - }) - } - - cookieHeader(cookies) { - return { - Cookie: [cookies], - } - } - - defaultHeaders() { - const user = { - _id: "us_uuid1", - userId: "us_uuid1", - sessionId: "sessionid", - tenantId: TENANT_ID, - } - const authToken = jwt.sign(user, env.JWT_SECRET) - return { - Accept: "application/json", - ...this.cookieHeader([`${Cookies.Auth}=${authToken}`]), - [Headers.CSRF_TOKEN]: CSRF_TOKEN, - } - } - - async getUser(email) { - return doInTenant(TENANT_ID, () => { - return users.getGlobalUserByEmail(email) - }) - } - - async getGroup(id) { - return doInTenant(TENANT_ID, () => { - return groups.get(id) - }) - } - - async saveGroup(group) { - const res = await this.getRequest() - .post(`/api/global/groups`) - .send(group) - .set(this.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - return res.body - } - - async createUser(email, password) { - const user = await this.getUser(structures.users.email) - if (user) { - return user - } - await this._req( - structures.users.user({ email, password }), - null, - controllers.users.save - ) - } - - async saveAdminUser() { - await this._req( - structures.users.user({ tenantId: TENANT_ID }), - null, - controllers.users.adminUser - ) - } - - // CONFIGS - - async deleteConfig(type) { - try { - const cfg = await this._req( - null, - { - type, - }, - controllers.config.find - ) - if (cfg) { - await this._req( - null, - { - id: cfg._id, - rev: cfg._rev, - }, - controllers.config.destroy - ) - } - } catch (err) { - // don't need to handle error - } - } - - // CONFIGS - SETTINGS - - async saveSettingsConfig() { - await this.deleteConfig(Configs.SETTINGS) - await this._req( - structures.configs.settings(), - null, - controllers.config.save - ) - } - - // CONFIGS - GOOGLE - - async saveGoogleConfig() { - await this.deleteConfig(Configs.GOOGLE) - await this._req(structures.configs.google(), null, controllers.config.save) - } - - // CONFIGS - OIDC - - getOIDConfigCookie(configId) { - const token = jwt.sign(configId, env.JWT_SECRET) - return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]]) - } - - async saveOIDCConfig() { - await this.deleteConfig(Configs.OIDC) - const config = structures.configs.oidc() - - await this._req(config, null, controllers.config.save) - return config - } - - // CONFIGS - SMTP - - async saveSmtpConfig() { - await this.deleteConfig(Configs.SMTP) - await this._req(structures.configs.smtp(), null, controllers.config.save) - } - - async saveEtherealSmtpConfig() { - await this.deleteConfig(Configs.SMTP) - await this._req( - structures.configs.smtpEthereal(), - null, - controllers.config.save - ) - } -} - -module.exports = TestConfiguration diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts new file mode 100644 index 0000000000..746e1ccf1b --- /dev/null +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -0,0 +1,273 @@ +import "./mocks" +import dbConfig from "../db" +dbConfig.init() +import env from "../environment" +import controllers from "./controllers" +const supertest = require("supertest") +import { Configs } from "../constants" +import { + users, + tenancy, + Cookies, + Headers, + sessions, + auth, +} from "@budibase/backend-core" +import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures" +import structures from "./structures" +import { CreateUserResponse, User, AuthToken } from "@budibase/types" + +enum Mode { + ACCOUNT = "account", + SELF = "self", +} + +class TestConfiguration { + server: any + request: any + defaultUser?: User + tenant1User?: User + + constructor( + opts: { openServer: boolean; mode: Mode } = { + openServer: true, + mode: Mode.ACCOUNT, + } + ) { + if (opts.mode === Mode.ACCOUNT) { + this.modeAccount() + } else if (opts.mode === Mode.SELF) { + this.modeSelf() + } + + if (opts.openServer) { + env.PORT = "0" // random port + this.server = require("../index") + // we need the request for logging in, involves cookies, hard to fake + this.request = supertest(this.server) + } + } + + getRequest() { + return this.request + } + + // MODES + + modeAccount = () => { + env.SELF_HOSTED = false + // @ts-ignore + env.MULTI_TENANCY = true + // @ts-ignore + env.DISABLE_ACCOUNT_PORTAL = false + } + + modeSelf = () => { + env.SELF_HOSTED = true + // @ts-ignore + env.MULTI_TENANCY = false + // @ts-ignore + env.DISABLE_ACCOUNT_PORTAL = true + } + + // UTILS + + async _req(config: any, params: any, controlFunc: any) { + const request: any = {} + // fake cookies, we don't need them + request.cookies = { set: () => {}, get: () => {} } + request.config = { jwtSecret: env.JWT_SECRET } + request.user = { tenantId: this.getTenantId() } + request.query = {} + request.request = { + body: config, + } + request.throw = (status: any, err: any) => { + throw { status, message: err } + } + if (params) { + request.params = params + } + await tenancy.doInTenant(this.getTenantId(), () => { + return controlFunc(request) + }) + return request.body + } + + // SETUP / TEARDOWN + + async beforeAll() { + await this.createDefaultUser() + await this.createSession(this.defaultUser!) + + await tenancy.doInTenant(TENANT_1, async () => { + await this.createTenant1User() + await this.createSession(this.tenant1User!) + }) + } + + async afterAll() { + if (this.server) { + await this.server.close() + } + } + + // TENANCY + + getTenantId() { + try { + return tenancy.getTenantId() + } catch (e: any) { + return TENANT_ID + } + } + + // USER / AUTH + + async createDefaultUser() { + const user = structures.users.adminUser({ + email: "test@test.com", + password: "test", + }) + this.defaultUser = await this.createUser(user) + } + + async createTenant1User() { + const user = structures.users.adminUser({ + email: "tenant1@test.com", + password: "test", + }) + this.tenant1User = await this.createUser(user) + } + + async createSession(user: User) { + await sessions.createASession(user._id!, { + sessionId: "sessionid", + tenantId: user.tenantId, + csrfToken: CSRF_TOKEN, + }) + } + + cookieHeader(cookies: any) { + return { + Cookie: [cookies], + } + } + + authHeaders(user: User) { + const authToken: AuthToken = { + userId: user._id!, + sessionId: "sessionid", + tenantId: user.tenantId, + } + const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET) + return { + Accept: "application/json", + ...this.cookieHeader([`${Cookies.Auth}=${authCookie}`]), + [Headers.CSRF_TOKEN]: CSRF_TOKEN, + } + } + + defaultHeaders() { + const tenantId = this.getTenantId() + if (tenantId === TENANT_ID) { + return this.authHeaders(this.defaultUser!) + } else if (tenantId === TENANT_1) { + return this.authHeaders(this.tenant1User!) + } else { + throw new Error("could not determine auth headers to use") + } + } + + async getUser(email: string): Promise { + return tenancy.doInTenant(this.getTenantId(), () => { + return users.getGlobalUserByEmail(email) + }) + } + + async createUser(user?: User) { + if (!user) { + user = structures.users.user() + } + const response = await this._req(user, null, controllers.users.save) + const body = response as CreateUserResponse + return this.getUser(body.email) + } + + // CONFIGS + + async deleteConfig(type: any) { + try { + const cfg = await this._req( + null, + { + type, + }, + controllers.config.find + ) + if (cfg) { + await this._req( + null, + { + id: cfg._id, + rev: cfg._rev, + }, + controllers.config.destroy + ) + } + } catch (err) { + // don't need to handle error + } + } + + // CONFIGS - SETTINGS + + async saveSettingsConfig() { + await this.deleteConfig(Configs.SETTINGS) + await this._req( + structures.configs.settings(), + null, + controllers.config.save + ) + } + + // CONFIGS - GOOGLE + + async saveGoogleConfig() { + await this.deleteConfig(Configs.GOOGLE) + await this._req(structures.configs.google(), null, controllers.config.save) + } + + // CONFIGS - OIDC + + getOIDConfigCookie(configId: string) { + const token = auth.jwt.sign(configId, env.JWT_SECRET) + return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]]) + } + + async saveOIDCConfig() { + await this.deleteConfig(Configs.OIDC) + const config = structures.configs.oidc() + + await this._req(config, null, controllers.config.save) + return config + } + + // CONFIGS - SMTP + + async saveSmtpConfig() { + await this.deleteConfig(Configs.SMTP) + await this._req(structures.configs.smtp(), null, controllers.config.save) + } + + async saveEtherealSmtpConfig() { + await this.deleteConfig(Configs.SMTP) + await this._req( + structures.configs.smtpEthereal(), + null, + controllers.config.save + ) + } +} + +export = TestConfiguration diff --git a/packages/worker/src/tests/api/accounts.ts b/packages/worker/src/tests/api/accounts.ts new file mode 100644 index 0000000000..fe6bf31192 --- /dev/null +++ b/packages/worker/src/tests/api/accounts.ts @@ -0,0 +1,28 @@ +import { Account, AccountMetadata } from "@budibase/types" +import TestConfiguration from "../TestConfiguration" + +export class AccountAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + saveMetadata = async (account: Account) => { + const res = await this.request + .put(`/api/system/accounts/${account.accountId}/metadata`) + .send(account) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + return res.body as AccountMetadata + } + + destroyMetadata = (accountId: string) => { + return this.request + .del(`/api/system/accounts/${accountId}/metadata`) + .set(this.config.defaultHeaders()) + } +} diff --git a/packages/worker/src/tests/api/auth.ts b/packages/worker/src/tests/api/auth.ts new file mode 100644 index 0000000000..204ae9f5dd --- /dev/null +++ b/packages/worker/src/tests/api/auth.ts @@ -0,0 +1,48 @@ +import TestConfiguration from "../TestConfiguration" + +export class AuthAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + updatePassword = (code: string) => { + return this.request + .post(`/api/global/auth/${this.config.getTenantId()}/reset/update`) + .send({ + password: "newpassword", + resetCode: code, + }) + .expect("Content-Type", /json/) + .expect(200) + } + + logout = () => { + return this.request + .post("/api/global/auth/logout") + .set(this.config.defaultHeaders()) + .expect(200) + } + + requestPasswordReset = async (sendMailMock: any) => { + await this.config.saveSmtpConfig() + await this.config.saveSettingsConfig() + await this.config.createUser() + const res = await this.request + .post(`/api/global/auth/${this.config.getTenantId()}/reset`) + .send({ + email: "test@test.com", + }) + .expect("Content-Type", /json/) + .expect(200) + const emailCall = sendMailMock.mock.calls[0][0] + const parts = emailCall.html.split( + `http://localhost:10000/builder/auth/reset?code=` + ) + const code = parts[1].split('"')[0].split("&")[0] + return { code, res } + } +} diff --git a/packages/worker/src/tests/api/configs.ts b/packages/worker/src/tests/api/configs.ts new file mode 100644 index 0000000000..3a3c433fa0 --- /dev/null +++ b/packages/worker/src/tests/api/configs.ts @@ -0,0 +1,40 @@ +import TestConfiguration from "../TestConfiguration" + +export class ConfigAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + getConfigChecklist = () => { + return this.request + .get(`/api/global/configs/checklist`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } + + saveConfig = (data: any) => { + return this.request + .post(`/api/global/configs`) + .send(data) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } + + OIDCCallback = (configId: string) => { + return this.request + .get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`) + .set(this.config.getOIDConfigCookie(configId)) + } + + getOIDCConfig = (configId: string) => { + return this.request.get( + `/api/global/auth/${this.config.getTenantId()}/oidc/configs/${configId}` + ) + } +} diff --git a/packages/worker/src/tests/api/email.ts b/packages/worker/src/tests/api/email.ts new file mode 100644 index 0000000000..ea026c22ac --- /dev/null +++ b/packages/worker/src/tests/api/email.ts @@ -0,0 +1,24 @@ +import TestConfiguration from "../TestConfiguration" + +export class EmailAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + sendEmail = (purpose: string) => { + return this.request + .post(`/api/global/email/send`) + .send({ + email: "test@test.com", + purpose, + tenantId: this.config.getTenantId(), + }) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } +} diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts new file mode 100644 index 0000000000..a12e489a78 --- /dev/null +++ b/packages/worker/src/tests/api/index.ts @@ -0,0 +1,25 @@ +import TestConfiguration from "../TestConfiguration" +import { AccountAPI } from "./accounts" +import { AuthAPI } from "./auth" +import { ConfigAPI } from "./configs" +import { EmailAPI } from "./email" +import { SelfAPI } from "./self" +import { UserAPI } from "./users" + +export default class API { + accounts: AccountAPI + auth: AuthAPI + configs: ConfigAPI + emails: EmailAPI + self: SelfAPI + users: UserAPI + + constructor(config: TestConfiguration) { + this.accounts = new AccountAPI(config) + this.auth = new AuthAPI(config) + this.configs = new ConfigAPI(config) + this.emails = new EmailAPI(config) + this.self = new SelfAPI(config) + this.users = new UserAPI(config) + } +} diff --git a/packages/worker/src/tests/api/self.ts b/packages/worker/src/tests/api/self.ts new file mode 100644 index 0000000000..b1cd4a48c6 --- /dev/null +++ b/packages/worker/src/tests/api/self.ts @@ -0,0 +1,21 @@ +import TestConfiguration from "../TestConfiguration" +import { User } from "@budibase/types" + +export class SelfAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + updateSelf = (user: User) => { + return this.request + .post(`/api/global/self`) + .send(user) + .set(this.config.authHeaders(user)) + .expect("Content-Type", /json/) + .expect(200) + } +} diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts new file mode 100644 index 0000000000..986a26ad5f --- /dev/null +++ b/packages/worker/src/tests/api/users.ts @@ -0,0 +1,110 @@ +import { + BulkCreateUsersRequest, + BulkCreateUsersResponse, + BulkDeleteUsersRequest, + BulkDeleteUsersResponse, + InviteUsersRequest, + User, +} from "@budibase/types" +import TestConfiguration from "../TestConfiguration" + +export class UserAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + // INVITE + + sendUserInvite = async (sendMailMock: any, email: string, status = 200) => { + await this.config.saveSmtpConfig() + await this.config.saveSettingsConfig() + const res = await this.request + .post(`/api/global/users/invite`) + .send({ + email, + }) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status) + + if (status !== 200) { + return { code: undefined, res } + } + + const emailCall = sendMailMock.mock.calls[0][0] + // after this URL there should be a code + const parts = emailCall.html.split( + "http://localhost:10000/builder/invite?code=" + ) + const code = parts[1].split('"')[0].split("&")[0] + return { code, res } + } + + acceptInvite = (code: string) => { + return this.request + .post(`/api/global/users/invite/accept`) + .send({ + password: "newpassword", + inviteCode: code, + }) + .expect("Content-Type", /json/) + .expect(200) + } + + sendMultiUserInvite = async (request: InviteUsersRequest, status = 200) => { + await this.config.saveSmtpConfig() + await this.config.saveSettingsConfig() + return this.request + .post(`/api/global/users/multi/invite`) + .send(request) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status) + } + + // BULK + + bulkCreateUsers = async (users: User[], groups: any[] = []) => { + const body: BulkCreateUsersRequest = { users, groups } + const res = await this.request + .post(`/api/global/users/bulkCreate`) + .send(body) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + return res.body as BulkCreateUsersResponse + } + + bulkDeleteUsers = (body: BulkDeleteUsersRequest, status?: number) => { + return this.request + .post(`/api/global/users/bulkDelete`) + .send(body) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status ? status : 200) + } + + // USER + + saveUser = (user: User, status?: number) => { + return this.request + .post(`/api/global/users`) + .send(user) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status ? status : 200) + } + + deleteUser = (userId: string, status?: number) => { + return this.request + .delete(`/api/global/users/${userId}`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status ? status : 200) + } +} diff --git a/packages/worker/src/tests/index.js b/packages/worker/src/tests/index.js deleted file mode 100644 index 9aa88dc444..0000000000 --- a/packages/worker/src/tests/index.js +++ /dev/null @@ -1,12 +0,0 @@ -const TestConfiguration = require("./TestConfiguration") -const structures = require("./structures") -const mocks = require("./mocks") -const config = new TestConfiguration() -const request = config.getRequest() - -module.exports = { - structures, - mocks, - config, - request, -} diff --git a/packages/worker/src/tests/index.ts b/packages/worker/src/tests/index.ts new file mode 100644 index 0000000000..6ab1e83955 --- /dev/null +++ b/packages/worker/src/tests/index.ts @@ -0,0 +1,14 @@ +import TestConfiguration from "./TestConfiguration" +import structures from "./structures" +import mocks from "./mocks" +import API from "./api" + +const pkg = { + structures, + TENANT_1: structures.TENANT_1, + mocks, + TestConfiguration, + API, +} + +export = pkg diff --git a/packages/worker/src/tests/mocks/index.js b/packages/worker/src/tests/mocks/index.js deleted file mode 100644 index 5a85348350..0000000000 --- a/packages/worker/src/tests/mocks/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const email = require("./email") - -module.exports = { - email, -} diff --git a/packages/worker/src/tests/mocks/index.ts b/packages/worker/src/tests/mocks/index.ts new file mode 100644 index 0000000000..e4b68bbfd4 --- /dev/null +++ b/packages/worker/src/tests/mocks/index.ts @@ -0,0 +1,7 @@ +const email = require("./email") +import { mocks as coreMocks } from "@budibase/backend-core/tests" + +export = { + email, + ...coreMocks, +} diff --git a/packages/worker/src/tests/structures/accounts.ts b/packages/worker/src/tests/structures/accounts.ts new file mode 100644 index 0000000000..df6b993684 --- /dev/null +++ b/packages/worker/src/tests/structures/accounts.ts @@ -0,0 +1,24 @@ +import { Account, AuthType, Hosting, CloudAccount } from "@budibase/types" +import { v4 as uuid } from "uuid" +import { utils } from "@budibase/backend-core" + +export const account = (): Account => { + return { + email: `${uuid()}@test.com`, + tenantId: utils.newid(), + hosting: Hosting.SELF, + authType: AuthType.SSO, + accountId: uuid(), + createdAt: Date.now(), + verified: true, + verificationSent: true, + tier: "FREE", + } +} + +export const cloudAccount = (): CloudAccount => { + return { + ...account(), + budibaseUserId: uuid(), + } +} diff --git a/packages/worker/src/tests/structures/index.js b/packages/worker/src/tests/structures/index.js deleted file mode 100644 index 3212ae606d..0000000000 --- a/packages/worker/src/tests/structures/index.js +++ /dev/null @@ -1,14 +0,0 @@ -const configs = require("./configs") -const users = require("./users") -const groups = require("./groups") - -const TENANT_ID = "default" -const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" - -module.exports = { - configs, - users, - TENANT_ID, - CSRF_TOKEN, - groups, -} diff --git a/packages/worker/src/tests/structures/index.ts b/packages/worker/src/tests/structures/index.ts new file mode 100644 index 0000000000..a3029b0105 --- /dev/null +++ b/packages/worker/src/tests/structures/index.ts @@ -0,0 +1,20 @@ +import configs from "./configs" +import * as users from "./users" +import * as groups from "./groups" +import * as accounts from "./accounts" + +const TENANT_ID = "default" +const TENANT_1 = "tenant1" +const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" + +const pkg = { + configs, + users, + accounts, + TENANT_ID, + TENANT_1, + CSRF_TOKEN, + groups, +} + +export = pkg diff --git a/packages/worker/src/tests/structures/users.ts b/packages/worker/src/tests/structures/users.ts index dce771aaa7..4bf24ec780 100644 --- a/packages/worker/src/tests/structures/users.ts +++ b/packages/worker/src/tests/structures/users.ts @@ -1,24 +1,32 @@ export const email = "test@test.com" +import { AdminUser, BuilderUser, User } from "@budibase/types" +import { v4 as uuid } from "uuid" -export const user = (userProps: any) => { +export const newEmail = () => { + return `${uuid()}@test.com` +} +export const user = (userProps?: any): User => { return { - email: "test@test.com", + email: newEmail(), password: "test", roles: {}, ...userProps, } } -export const adminUser = (userProps: any) => { +export const adminUser = (userProps?: any): AdminUser => { return { ...user(userProps), admin: { global: true, }, + builder: { + global: true, + }, } } -export const builderUser = (userProps: any) => { +export const builderUser = (userProps?: any): BuilderUser => { return { ...user(userProps), builder: { diff --git a/packages/worker/src/utilities/redis.js b/packages/worker/src/utilities/redis.js index 97adafa195..7e474b2c28 100644 --- a/packages/worker/src/utilities/redis.js +++ b/packages/worker/src/utilities/redis.js @@ -55,6 +55,7 @@ exports.init = async () => { exports.shutdown = async () => { if (pwResetClient) await pwResetClient.finish() if (invitationClient) await invitationClient.finish() + console.log("Redis shutdown") } /** diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index c027efab72..580f586a63 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -291,12 +291,12 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.2.41-alpha.5": - version "1.2.41-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.41-alpha.5.tgz#e03c9970f9eadcdfe9146ffe4d7f29174648675a" - integrity sha512-kB+S3pu4F9wNMwzdBSF4auZEAqQ05VlhEA/cjLyb54zjxrJmpGv761I6vAM4BptvjpSNCGtiv6HrpGaOILRDDQ== +"@budibase/backend-core@1.2.58-alpha.5": + version "1.2.58-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.58-alpha.5.tgz#430699334629b3bd05a5066aa6660c1f68f047b2" + integrity sha512-Z926rAp0eskXUr5UXGwh0S1OMDA3qzIi8o0GHLlO7KZAzk49RYzhRT/yo1fIst8Eq/9ZCaXLFxIeRkDmCJgILg== dependencies: - "@budibase/types" "1.2.41-alpha.5" + "@budibase/types" "1.2.58-alpha.5" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" bcrypt "5.0.1" @@ -325,21 +325,21 @@ uuid "8.3.2" zlib "1.0.5" -"@budibase/pro@1.2.41-alpha.5": - version "1.2.41-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.41-alpha.5.tgz#43362d0e9bca4dbb8ab93cfe3daf821b04f1ee95" - integrity sha512-p/Vvs2xOWlQlLYJ8sF/PWI2ki/B0uy0sQPwUUld4qQRHXa+YevXJgnjZPa3v7q2NWQ1vxov2K8g8y6Z5mv9XAw== +"@budibase/pro@1.2.58-alpha.5": + version "1.2.58-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.58-alpha.5.tgz#1f739653e641ac32d20818517d35bef489ec2ae6" + integrity sha512-owT7ipgZ588KAz6aHPJie5bLJX6EkhTRUvUQlyOJuLWt/hdXXnvbu+agxBQNaXE4gVzn916JJP0L0tbiMctjNA== dependencies: - "@budibase/backend-core" "1.2.41-alpha.5" - "@budibase/types" "1.2.41-alpha.5" + "@budibase/backend-core" "1.2.58-alpha.5" + "@budibase/types" "1.2.58-alpha.5" "@koa/router" "8.0.8" joi "17.6.0" node-fetch "^2.6.1" -"@budibase/types@1.2.41-alpha.5": - version "1.2.41-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.41-alpha.5.tgz#0ade282c2e4ec3e4a1a4212403a1d0c3da80521b" - integrity sha512-Awya83t8bDeavc0XSglYVc4sStDheUKsAQWzTpgcRaB5l7NwhHQG5xlWsbFT8glolYY96ogLZJjLFTzIur3iDQ== +"@budibase/types@1.2.58-alpha.5": + version "1.2.58-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.58-alpha.5.tgz#8ea99745f296a1076f878a06b7c228bc60886cd3" + integrity sha512-0JhHDGvfcMcjhBckP3RqbKCDsMhx7ZVTV8wvaqsOaG7PnBSst9dxyX7ETVVzfvYolIK1eV11itsgx0FpbYID2w== "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" @@ -353,6 +353,20 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@elastic/ecs-helpers@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@elastic/ecs-helpers/-/ecs-helpers-1.1.0.tgz#ee7e6f870f75a2222c5d7179b36a628f1db4779e" + integrity sha512-MDLb2aFeGjg46O5mLpdCzT5yOUDnXToJSrco2ShqGIXxNJaM8uJjX+4nd+hRYV4Vex8YJyDtOFEVBldQct6ndg== + dependencies: + fast-json-stringify "^2.4.1" + +"@elastic/ecs-pino-format@^1.2.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@elastic/ecs-pino-format/-/ecs-pino-format-1.3.0.tgz#6e349a7da342b3c370d15361ba7f850bc2f783bc" + integrity sha512-U8D57gPECYoRCcwREsrXKBtqeyFFF/KAwHi4rG1u/oQhAg91Kzw8ZtUQJXD/DMDieLOqtbItFr2FRBWI3t3wog== + dependencies: + "@elastic/ecs-helpers" "^1.1.0" + "@hapi/bourne@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.1.0.tgz#66aff77094dc3080bd5df44ec63881f2676eb020" @@ -654,6 +668,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@opentelemetry/api@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.1.0.tgz#563539048255bbe1a5f4f586a4a10a1bb737f44a" + integrity sha512-hf+3bwuBwtXsugA2ULBc95qxrOqP2pOekLz34BJhcAKawt94vfeNyUKpYc0lZQ/3sCP6LqRa7UAdHA7i5UODzQ== + "@sentry/core@6.17.7": version "6.17.7" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.17.7.tgz#f591235c06b1a4e75d748b15c539e071bd3f5cf5" @@ -1075,6 +1094,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -1230,6 +1254,11 @@ acorn@^8.2.4, acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +after-all-results@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/after-all-results/-/after-all-results-2.0.0.tgz#6ac2fc202b500f88da8f4f5530cfa100f4c6a2d0" + integrity sha512-2zHEyuhSJOuCrmas9YV0YL/MFCWLxe1dS6k/ENhgYrb/JqyMnadLN4iIAc9kkZrbElMDyyAGH/0J18OPErOWLg== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1237,7 +1266,16 @@ agent-base@6: dependencies: debug "4" -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3: +agentkeepalive@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.3: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1375,6 +1413,25 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async-cache@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/async-cache/-/async-cache-1.1.0.tgz#4a9a5a89d065ec5d8e5254bd9ee96ba76c532b5a" + integrity sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g== + dependencies: + lru-cache "^4.0.0" + +async-value-promise@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/async-value-promise/-/async-value-promise-1.1.1.tgz#68957819e3eace804f3b4b69477e2bd276c15378" + integrity sha512-c2RFDKjJle1rHa0YxN9Ysu97/QBu3Wa+NOejJxsX+1qVDJrkD3JL/GN1B3gaILAEXJXbu/4Z1lcoCHFESe/APA== + dependencies: + async-value "^1.2.2" + +async-value@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/async-value/-/async-value-1.2.2.tgz#84517a1e7cb6b1a5b5e181fa31be10437b7fb125" + integrity sha512-8rwtYe32OAS1W9CTwvknoyts+mc3ta8N7Pi0h7AjkMaKvsFbr39K+gEfZ7Z81aPXQ1sK5M23lgLy1QfZpcpadQ== + async@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" @@ -1513,6 +1570,13 @@ base64url@3.x.x, base64url@^3.0.1: resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -1538,6 +1602,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binary-search@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" + integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1586,6 +1655,13 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +breadth-filter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/breadth-filter/-/breadth-filter-2.0.0.tgz#7b3f8737f46ba1946aec19355ecf5df2bdb7e47c" + integrity sha512-thQShDXnFWSk2oVBixRCyrWsFoV5tfOpWKHmxwafHQDNxCfDBk539utpvytNjmlFrTMqz41poLwJvA1MW3z0MQ== + dependencies: + object.entries "^1.0.4" + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -1692,7 +1768,7 @@ cacheable-request@^7.0.2: normalize-url "^6.0.1" responselike "^2.0.0" -call-bind@^1.0.0: +call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== @@ -1968,6 +2044,16 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== +console-log-level@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" + integrity sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ== + +container-info@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/container-info/-/container-info-1.1.0.tgz#6fcb94e93eacd397c6316ca2834491ede44e55ee" + integrity sha512-eD2zLAmxGS2kmL4f1jY8BdOqnmpL6X70kvzTBW/9FIQnxoxiBJ4htMsTmtPLPWRs7NHYFvqKQ1VtppV08mdsQA== + content-disposition@~0.5.2: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -1992,6 +2078,11 @@ cookie@^0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cookiejar@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" @@ -2184,7 +2275,7 @@ deferred-leveldown@~5.3.0: abstract-leveldown "~6.2.1" inherits "^2.0.3" -define-properties@^1.1.3: +define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== @@ -2217,7 +2308,7 @@ depd@2.0.0, depd@^2.0.0, depd@~2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== @@ -2341,6 +2432,60 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +elastic-apm-http-client@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-11.0.1.tgz#15dbe99d56d62b3f732d1bd2b51bef6094b78801" + integrity sha512-5AOWlhs2WlZpI+DfgGqY/8Rk7KF8WeevaO8R961eBylavU6GWhLRNiJncohn5jsvrqhmeT19azBvy/oYRN7bJw== + dependencies: + agentkeepalive "^4.2.1" + breadth-filter "^2.0.0" + container-info "^1.0.1" + end-of-stream "^1.4.4" + fast-safe-stringify "^2.0.7" + fast-stream-to-buffer "^1.0.0" + object-filter-sequence "^1.0.0" + readable-stream "^3.4.0" + semver "^6.3.0" + stream-chopper "^3.0.1" + +elastic-apm-node@3.38.0: + version "3.38.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.38.0.tgz#4d0dc9279c0e23e09b3b30aa4a9f9aeccfa59cc0" + integrity sha512-/d6YuWFtsfkVRpFD0YJ2rYJVq0rI0PGqG/C+cW1JpMZ4IOU8dA9xzUkxbT0G3B8gpHNug07Xo6bJdQa2oUaFbQ== + dependencies: + "@elastic/ecs-pino-format" "^1.2.0" + "@opentelemetry/api" "^1.1.0" + after-all-results "^2.0.0" + async-cache "^1.1.0" + async-value-promise "^1.1.1" + basic-auth "^2.0.1" + cookie "^0.5.0" + core-util-is "^1.0.2" + elastic-apm-http-client "11.0.1" + end-of-stream "^1.4.4" + error-callsites "^2.0.4" + error-stack-parser "^2.0.6" + escape-string-regexp "^4.0.0" + fast-safe-stringify "^2.0.7" + http-headers "^3.0.2" + is-native "^1.0.1" + lru-cache "^6.0.0" + measured-reporting "^1.51.1" + monitor-event-loop-delay "^1.0.0" + object-filter-sequence "^1.0.0" + object-identity-map "^1.0.2" + original-url "^1.2.3" + pino "^6.11.2" + relative-microtime "^2.0.0" + require-in-the-middle "^5.0.3" + semver "^6.3.0" + set-cookie-serde "^1.0.0" + shallow-clone-shim "^2.0.0" + source-map "^0.8.0-beta.0" + sql-summary "^1.0.1" + traverse "^0.6.6" + unicode-byte-truncate "^1.0.0" + electron-to-chromium@^1.4.147: version "1.4.150" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.150.tgz#89f0e12505462d5df7e56c5b91aff7e1dfdd33ec" @@ -2383,7 +2528,7 @@ encoding-down@^6.3.0: level-codec "^9.0.0" level-errors "^2.0.0" -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2404,6 +2549,11 @@ errno@~0.1.1: dependencies: prr "~1.0.1" +error-callsites@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.4.tgz#44f09e6a201e9a1603ead81eacac5ba258fca76e" + integrity sha512-V877Ch4FC4FN178fDK1fsrHN4I1YQIBdtjKrHh3BUHMnh3SMvwUVrqkaOgDpUuevgSNna0RBq6Ox9SGlxYrigA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2411,6 +2561,51 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.5: + version "1.20.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" + integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es3ify@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/es3ify/-/es3ify-0.2.2.tgz#5dae3e650e5be3684b88066513d528d092629862" @@ -2678,6 +2873,16 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-json-stringify@^2.4.1: + version "2.7.13" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz#277aa86c2acba4d9851bd6108ed657aa327ed8c0" + integrity sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA== + dependencies: + ajv "^6.11.0" + deepmerge "^4.2.2" + rfdc "^1.2.0" + string-similarity "^4.0.1" + fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -2693,6 +2898,13 @@ fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8, fast-safe-stringify@^2.1 resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-stream-to-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stream-to-buffer/-/fast-stream-to-buffer-1.0.0.tgz#793340cc753e7ec9c7fb6d57a53a0b911cb0f588" + integrity sha512-bI/544WUQlD2iXBibQbOMSmG07Hay7YrpXlKaeGTPT7H7pC0eitt3usak5vUwEvCGK/O7rUAM3iyQValGU22TQ== + dependencies: + end-of-stream "^1.4.1" + fast-url-parser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" @@ -2828,6 +3040,11 @@ formidable@^2.0.1: once "1.4.0" qs "6.9.3" +forwarded-parse@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" + integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw== + fresh@~0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -2860,11 +3077,26 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + gauge@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" @@ -2890,7 +3122,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== @@ -2923,6 +3155,14 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -3098,6 +3338,11 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -3206,6 +3451,13 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" +http-headers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/http-headers/-/http-headers-3.0.2.tgz#5147771292f0b39d6778d930a3a59a76fc7ef44d" + integrity sha512-87E1I+2Wg4dxxz4rcxElo3dxO/w1ZtgL1yA0Sb6vH3qU16vRKq1NjWQv9SCY3ly2OQROcoxHZOUpmelS+k6wOw== + dependencies: + next-line "^1.1.0" + http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -3245,6 +3497,13 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.5: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3365,6 +3624,15 @@ inquirer@^7.0.0: strip-ansi "^6.0.0" through "^2.3.6" +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + ioredis@4.28.0: version "4.28.0" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.0.tgz#5a2be3f37ff2075e2332f280eaeb02ab4d9ff0d3" @@ -3387,6 +3655,13 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -3394,11 +3669,24 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -3418,11 +3706,30 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -3460,11 +3767,43 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" +is-integer@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" + integrity sha512-RPQc/s9yBHSvpi+hs9dYiJ2cuFeU6x3TyyIp8O2H6SKEltIvJOzRj9ToyvcStDvPR/pS4rxgr1oBFajQjZ2Szg== + dependencies: + is-finite "^1.0.0" + +is-native@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-native/-/is-native-1.0.1.tgz#cd18cc162e8450d683b5babe79ac99c145449675" + integrity sha512-I4z9hx+4u3/zyvpvGtAR+n7SodJugE+i2jiS8yfq1A9QAZY0KldLQz0SBptLC9ti7kBlpghWUwTKE2BA62eCcw== + dependencies: + is-nil "^1.0.0" + to-source-code "^1.0.0" + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-nil@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-nil/-/is-nil-1.0.1.tgz#2daba29e0b585063875e7b539d071f5b15937969" + integrity sha512-m2Rm8PhUFDNNhgvwZJjJG74a9h5CHU0fkA8WT+WGlCjyEbZ2jPwgb+ZxHu4np284EqNVyOsgppReK4qy/TwEwg== + is-npm@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -3485,16 +3824,45 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-retry-allowed@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-type-of@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/is-type-of/-/is-type-of-1.2.1.tgz#e263ec3857aceb4f28c47130ec78db09a920f8c5" @@ -3509,6 +3877,13 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" @@ -4505,6 +4880,11 @@ lodash.pick@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + lodash@4.17.21, lodash@^4.14.0, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -4520,6 +4900,14 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^4.0.0: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -4556,6 +4944,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +mapcap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mapcap/-/mapcap-1.0.0.tgz#e8e29d04a160eaf8c92ec4bcbd2c5d07ed037e5a" + integrity sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g== + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -4572,6 +4965,24 @@ md5@^2.3.0: crypt "0.0.2" is-buffer "~1.1.6" +measured-core@^1.51.1: + version "1.51.1" + resolved "https://registry.yarnpkg.com/measured-core/-/measured-core-1.51.1.tgz#98989705c00bfb0d8a20e665a9f8d6e246a40518" + integrity sha512-DZQP9SEwdqqYRvT2slMK81D/7xwdxXosZZBtLVfPSo6y5P672FBTbzHVdN4IQyUkUpcVOR9pIvtUy5Ryl7NKyg== + dependencies: + binary-search "^1.3.3" + optional-js "^2.0.0" + +measured-reporting@^1.51.1: + version "1.51.1" + resolved "https://registry.yarnpkg.com/measured-reporting/-/measured-reporting-1.51.1.tgz#6aeb209ad55edf3940e8afa75c8f97f541216b31" + integrity sha512-JCt+2u6XT1I5lG3SuYqywE0e62DJuAzBcfMzWGUhIYtPQV2Vm4HiYt/durqmzsAbZV181CEs+o/jMKWJKkYIWw== + dependencies: + console-log-level "^1.4.1" + mapcap "^1.0.0" + measured-core "^1.51.1" + optional-js "^2.0.0" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -4693,6 +5104,16 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + +monitor-event-loop-delay@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/monitor-event-loop-delay/-/monitor-event-loop-delay-1.0.0.tgz#b5ab78165a3bb93f2b275c50d01430c7f155d1f7" + integrity sha512-YRIr1exCIfBDLZle8WHOfSo7Xg3M+phcZfq9Fx1L6Abo+atGp7cge5pM7PjyBn4s1oZI/BRD4EMrzQBbPpVb5Q== + mri@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" @@ -4703,7 +5124,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1, ms@^2.1.3: +ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4738,6 +5159,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +next-line@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603" + integrity sha512-+I10J3wKNoKddNxn0CNpoZ3eTZuqxjNM3b1GImVx22+ePI+Y15P8g/j3WsbP0fhzzrFzrtjOAoq5NCCucswXOQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4880,7 +5306,19 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.9.0: +object-filter-sequence@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object-filter-sequence/-/object-filter-sequence-1.0.0.tgz#10bb05402fff100082b80d7e83991b10db411692" + integrity sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ== + +object-identity-map@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-identity-map/-/object-identity-map-1.0.2.tgz#2b4213a4285ca3a8cd2e696782c9964f887524e7" + integrity sha512-a2XZDGyYTngvGS67kWnqVdpoaJWsY7C1GhPJvejWAFCsUioTAaiTu8oBad7c6cI4McZxr4CmvnZeycK05iav5A== + dependencies: + object.entries "^1.1.0" + +object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== @@ -4890,6 +5328,25 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +object.assign@^4.1.2: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.0.4, object.entries@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" + integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -4916,6 +5373,11 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== +optional-js@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.3.0.tgz#81d54c4719afa8845b988143643a5148f9d89490" + integrity sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw== + optionator@^0.8.1, optionator@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -4928,6 +5390,13 @@ optionator@^0.8.1, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +original-url@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/original-url/-/original-url-1.2.3.tgz#133aff4b2d27e38a98d736f7629c56262b7153e1" + integrity sha512-BYm+pKYLtS4mVe/mgT3YKGtWV5HzN/XKiaIu1aK4rsxyjuHeTW9N+xVBEpJcY1onB3nccfH0RbzUEoimMqFUHQ== + dependencies: + forwarded-parse "^2.1.0" + os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5181,7 +5650,7 @@ pino-std-serializers@^4.0.0: resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1791ccd2539c091ae49ce9993205e2cd5dbba1e2" integrity sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q== -pino@^6.13.0: +pino@^6.11.2, pino@^6.13.0: version "6.14.0" resolved "https://registry.yarnpkg.com/pino/-/pino-6.14.0.tgz#b745ea87a99a6c4c9b374e4f29ca7910d4c69f78" integrity sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg== @@ -5505,6 +5974,11 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -5622,7 +6096,7 @@ readable-stream@1.1.14, readable-stream@^1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -5705,6 +6179,15 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -5724,6 +6207,11 @@ registry-url@^5.0.0: dependencies: rc "^1.2.8" +relative-microtime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/relative-microtime/-/relative-microtime-2.0.0.tgz#cceed2af095ecd72ea32011279c79e5fcc7de29b" + integrity sha512-l18ha6HEZc+No/uK4GyAnNxgKW7nvEe35IaeN54sShMojtqik2a6GbTyuiezkjpPaqP874Z3lW5ysBo5irz4NA== + remove-trailing-slash@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" @@ -5760,6 +6248,15 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-in-the-middle@^5.0.3: + version "5.2.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz#4b71e3cc7f59977100af9beb76bf2d056a5a6de2" + integrity sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.22.1" + resolve-alpn@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" @@ -5804,6 +6301,15 @@ resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -5831,7 +6337,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.3.0: +rfdc@^1.2.0, rfdc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== @@ -5881,16 +6387,16 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -5964,6 +6470,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-serde@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-cookie-serde/-/set-cookie-serde-1.0.0.tgz#bcf9c260ed2212ac4005a53eacbaaa37c07ac452" + integrity sha512-Vq8e5GsupfJ7okHIvEPcfs5neCo7MZ1ZuWrO3sllYi3DOWt6bSSCpADzqXjz3k0fXehnoFIrmmhty9IN6U6BXQ== + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -5974,6 +6485,11 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +shallow-clone-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone-shim/-/shallow-clone-shim-2.0.0.tgz#b62bf55aed79f4c1430ea1dc4d293a193f52cf91" + integrity sha512-YRNymdiL3KGOoS67d73TEmk4tdPTO9GSMCoiphQsTcC9EtC+AOmMPjkyBkRoCJfW9ASsaZw1craaiw1dPN2D3Q== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -6069,6 +6585,13 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + source-map@~0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6108,6 +6631,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sql-summary@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sql-summary/-/sql-summary-1.0.1.tgz#a2dddb5435bae294eb11424a7330dc5bafe09c2b" + integrity sha512-IpCr2tpnNkP3Jera4ncexsZUp0enJBLr+pHCyTweMUBrbJsTgQeLWx1FXLhoBj/MvcnUQpkgOn2EY8FKOkUzww== + sshpk@^1.7.0: version "1.17.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" @@ -6130,6 +6658,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" @@ -6150,6 +6683,13 @@ step@0.0.x: resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2" integrity sha512-qSSeQinUJk2w38vUFobjFoE307GqsozMC8VisOCkJLpklvKPT0ptPHwWOrENoag8rgLudvTkfP3bancwP93/Jw== +stream-chopper@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/stream-chopper/-/stream-chopper-3.0.1.tgz#73791ae7bf954c297d6683aec178648efc61dd75" + integrity sha512-f7h+ly8baAE26iIjcp3VbnBkbIRGtrvV0X0xxFM/d7fwLTYnLzDPTXRKNxa2HZzohOrc96NTrR+FaV3mzOelNA== + dependencies: + readable-stream "^3.0.6" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -6158,6 +6698,11 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-similarity@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" + integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== + string-template@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" @@ -6181,6 +6726,24 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -6443,6 +7006,13 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +to-source-code@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-source-code/-/to-source-code-1.0.2.tgz#dd136bdb1e1dbd80bbeacf088992678e9070bfea" + integrity sha512-YzWtjmNIf3E75eZYa7m1SCyl0vgOGoTzdpH3svfa8SUm5rqTgl9hnDolrAGOghCF9P2gsITXQoMrlujOoz+RPw== + dependencies: + is-nil "^1.0.0" + toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -6472,6 +7042,13 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -6484,6 +7061,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +traverse@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + integrity sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw== + ts-jest@27.1.3: version "27.1.3" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.3.tgz#1f723e7e74027c4da92c0ffbd73287e8af2b2957" @@ -6611,11 +7193,34 @@ uid2@0.0.x: resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +unicode-byte-truncate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unicode-byte-truncate/-/unicode-byte-truncate-1.0.0.tgz#aa6f0f3475193fe20c320ac9213e36e62e8764a7" + integrity sha512-GQgHk6DodEoKddKQdjnv7xKS9G09XCfHWX0R4RKht+EbUMSiVEmtWHGFO8HUm+6NvWik3E2/DG4MxTitOLL64A== + dependencies: + is-integer "^1.0.6" + unicode-substring "^0.1.0" + +unicode-substring@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicode-substring/-/unicode-substring-0.1.0.tgz#6120ce3c390385dbcd0f60c32b9065c4181d4b36" + integrity sha512-36Xaw9wXi7MB/3/EQZZHkZyyiRNa9i3k9YtPAz2KfqMVH2xutdXyMHn4Igarmnvr+wOrfWa/6njhY+jPpXN2EQ== + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" @@ -6787,6 +7392,11 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -6817,6 +7427,15 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" @@ -6826,6 +7445,17 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -6947,6 +7577,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"