diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index e0263546ff..c64adb010f 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -10,7 +10,7 @@ on: pull_request: branches: - master - - develop + - develop workflow_dispatch: env: @@ -64,6 +64,20 @@ jobs: name: codecov-umbrella verbose: true + test-pro: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn test:pro + integration-test: runs-on: ubuntu-latest services: diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 16c6c37bbd..e986179cfc 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -68,83 +68,6 @@ jobs: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - deploy-to-release-env: - needs: [release-images] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Get the current budibase release version - id: version - run: | - release_version=$(cat lerna.json | jq -r '.version') - echo "RELEASE_VERSION=$release_version" >> $GITHUB_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: Pull values.yaml from budibase-infra - run: | - curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ - -H 'Accept: application/vnd.github.v3.raw' \ - -o values.release.yaml \ - -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml - wc -l values.release.yaml - - - name: Deploy to Release Environment - uses: glopezep/helm@v1.7.1 - with: - release: budibase-release - namespace: budibase - chart: charts/budibase - token: ${{ github.token }} - helm: helm3 - values: | - globals: - appVersion: develop - ingress: - enabled: true - nginx: true - value-files: >- - [ - "values.release.yaml" - ] - env: - KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' - - - name: Re roll app-service - uses: actions-hub/kubectl@master - env: - KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} - with: - 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 - with: - webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} - content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env." - embed-title: ${{ env.RELEASE_VERSION }} - release-helm-chart: needs: [release-images] runs-on: ubuntu-latest diff --git a/.tool-versions b/.tool-versions index 8a1af3c071..6ee8cc60be 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ nodejs 14.19.3 -python 3.11.1 \ No newline at end of file +python 3.10.0 \ No newline at end of file diff --git a/hosting/docker-compose.test.yaml b/hosting/docker-compose.test.yaml index dfd78621c5..f059173d2d 100644 --- a/hosting/docker-compose.test.yaml +++ b/hosting/docker-compose.test.yaml @@ -8,8 +8,8 @@ services: # Last version that supports the "fs" backend image: minio/minio:RELEASE.2022-10-24T18-35-07Z ports: - - 9000 - - 9001 + - "9000" + - "9001" environment: MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} @@ -28,9 +28,9 @@ services: - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} - COUCHDB_USER=${COUCH_DB_USER} ports: - - 5984 - - 4369 - - 9100 + - "5984" + - "4369" + - "9100" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5984/_up"] interval: 30s @@ -42,6 +42,6 @@ services: image: redis command: redis-server --requirepass ${REDIS_PASSWORD} ports: - - 6379 + - "6379" healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD", "redis-cli", "ping"] \ No newline at end of file diff --git a/lerna.json b/lerna.json index 8f5c3f058f..01659623bb 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 3ead7d5553..815e470916 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "js-yaml": "^4.1.0", "kill-port": "^1.6.1", "lerna": "3.14.1", - "madge": "^5.0.1", + "madge": "^6.0.0", "prettier": "^2.3.1", "prettier-plugin-svelte": "^2.3.0", "rimraf": "^3.0.2", @@ -44,7 +44,7 @@ "dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1", "dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server", - "test": "lerna run test && yarn test:pro", + "test": "lerna run test", "test:pro": "bash scripts/pro/test.sh", "lint:eslint": "eslint packages && eslint qa-core", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"", @@ -84,4 +84,4 @@ "install:pro": "bash scripts/pro/install.sh", "dep:clean": "yarn clean && yarn bootstrap" } -} \ No newline at end of file +} diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 0d7c5b1079..de2017ecd6 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.18-alpha.8", + "@budibase/types": "2.3.18-alpha.15", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index bee245a3ae..936d06ddff 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -2,25 +2,34 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy import { getGlobalDB } from "../context" -const refresh = require("passport-oauth2-refresh") -import { Config, Cookie } from "../constants" -import { getScopedConfig } from "../db" +import { Cookie } from "../constants" import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { + authenticated, + csrf, + google, jwt as jwtPassport, local, - authenticated, - tenancy, - csrf, oidc, - google, + tenancy, } from "../middleware" +import * as userCache from "../cache/user" import { invalidateUser } from "../cache/user" -import { PlatformLogoutOpts, User } from "@budibase/types" +import { + ConfigType, + GoogleInnerConfig, + OIDCInnerConfig, + PlatformLogoutOpts, + SSOProviderType, + User, +} from "@budibase/types" import { logAlert } from "../logging" import * as events from "../events" -import * as userCache from "../cache/user" +import * as configs from "../configs" import { clearCookie, getCookie } from "../utils" +import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" + +const refresh = require("passport-oauth2-refresh") export { auditLog, authError, @@ -33,7 +42,6 @@ export { google, oidc, } from "../middleware" -import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" export const buildAuthMiddleware = authenticated export const buildTenancyMiddleware = tenancy export const buildCsrfMiddleware = csrf @@ -63,11 +71,10 @@ _passport.deserializeUser(async (user: User, done: any) => { }) async function refreshOIDCAccessToken( - db: any, - chosenConfig: any, + chosenConfig: OIDCInnerConfig, refreshToken: string -) { - const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) +): Promise { + const callbackUrl = await oidc.getCallbackUrl() let enrichedConfig: any let strategy: any @@ -90,7 +97,7 @@ async function refreshOIDCAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Config.OIDC, + ConfigType.OIDC, refreshToken, (err: any, accessToken: string, refreshToken: any, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -100,11 +107,10 @@ async function refreshOIDCAccessToken( } async function refreshGoogleAccessToken( - db: any, - config: any, + config: GoogleInnerConfig, refreshToken: any -) { - let callbackUrl = await google.getCallbackUrl(db, config) +): Promise { + let callbackUrl = await google.getCallbackUrl(config) let strategy try { @@ -124,7 +130,7 @@ async function refreshGoogleAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Config.GOOGLE, + ConfigType.GOOGLE, refreshToken, (err: any, accessToken: string, refreshToken: string, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -133,41 +139,37 @@ async function refreshGoogleAccessToken( }) } +interface RefreshResponse { + err?: { + data?: string + } + accessToken?: string + refreshToken?: string + params?: any +} + export async function refreshOAuthToken( refreshToken: string, - configType: string, - configId: string -) { - const db = getGlobalDB() - - const config = await getScopedConfig(db, { - type: configType, - group: {}, - }) - - let chosenConfig = {} - let refreshResponse - if (configType === Config.OIDC) { - // configId - retrieved from cookie. - chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] - if (!chosenConfig) { - throw new Error("Invalid OIDC configuration") - } - refreshResponse = await refreshOIDCAccessToken( - db, - chosenConfig, - refreshToken - ) - } else { - chosenConfig = config - refreshResponse = await refreshGoogleAccessToken( - db, - chosenConfig, - refreshToken - ) + providerType: SSOProviderType, + configId?: string +): Promise { + switch (providerType) { + case SSOProviderType.OIDC: + if (!configId) { + return { err: { data: "OIDC config id not provided" } } + } + const oidcConfig = await configs.getOIDCConfigById(configId) + if (!oidcConfig) { + return { err: { data: "OIDC configuration not found" } } + } + return refreshOIDCAccessToken(oidcConfig, refreshToken) + case SSOProviderType.GOOGLE: + let googleConfig = await configs.getGoogleConfig() + if (!googleConfig) { + return { err: { data: "Google configuration not found" } } + } + return refreshGoogleAccessToken(googleConfig, refreshToken) } - - return refreshResponse } // TODO: Refactor to use user save function instead to prevent the need for @@ -225,6 +227,6 @@ export async function platformLogout(opts: PlatformLogoutOpts) { const sessionIds = sessions.map(({ sessionId }) => sessionId) await invalidateSessions(userId, { sessionIds, reason: "logout" }) - await events.auth.logout() + await events.auth.logout(ctx.user?.email) await userCache.invalidateUser(userId) } diff --git a/packages/backend-core/src/configs/configs.ts b/packages/backend-core/src/configs/configs.ts new file mode 100644 index 0000000000..38e7bcbb29 --- /dev/null +++ b/packages/backend-core/src/configs/configs.ts @@ -0,0 +1,242 @@ +import { + Config, + ConfigType, + GoogleConfig, + GoogleInnerConfig, + OIDCConfig, + OIDCInnerConfig, + SettingsConfig, + SettingsInnerConfig, + SMTPConfig, + SMTPInnerConfig, +} from "@budibase/types" +import { DocumentType, SEPARATOR } from "../constants" +import { CacheKey, TTL, withCache } from "../cache" +import * as context from "../context" +import env from "../environment" +import environment from "../environment" + +// UTILS + +/** + * Generates a new configuration ID. + * @returns {string} The new configuration ID which the config doc can be stored under. + */ +export function generateConfigID(type: ConfigType) { + return `${DocumentType.CONFIG}${SEPARATOR}${type}` +} + +export async function getConfig( + type: ConfigType +): Promise { + const db = context.getGlobalDB() + try { + // await to catch error + const config = (await db.get(generateConfigID(type))) as T + return config + } catch (e: any) { + if (e.status === 404) { + return + } + throw e + } +} + +export async function save(config: Config) { + const db = context.getGlobalDB() + return db.put(config) +} + +// SETTINGS + +export async function getSettingsConfigDoc(): Promise { + let config = await getConfig(ConfigType.SETTINGS) + + if (!config) { + config = { + _id: generateConfigID(ConfigType.GOOGLE), + type: ConfigType.SETTINGS, + config: {}, + } + } + + // overridden fields + config.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + config: config.config, + }) + config.config.analyticsEnabled = await analyticsEnabled({ + config: config.config, + }) + + return config +} + +export async function getSettingsConfig(): Promise { + return (await getSettingsConfigDoc()).config +} + +export async function getPlatformUrl( + opts: { tenantAware: boolean; config?: SettingsInnerConfig } = { + tenantAware: true, + } +) { + let platformUrl = env.PLATFORM_URL || "http://localhost:10000" + + if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { + // cloud and multi tenant - add the tenant to the default platform url + const tenantId = context.getTenantId() + if (!platformUrl.includes("localhost:")) { + platformUrl = platformUrl.replace("://", `://${tenantId}.`) + } + } else if (env.SELF_HOSTED) { + const config = opts?.config + ? opts.config + : // direct to db to prevent infinite loop + (await getConfig(ConfigType.SETTINGS))?.config + if (config?.platformUrl) { + platformUrl = config.platformUrl + } + } + + return platformUrl +} + +export const analyticsEnabled = async (opts?: { + config?: SettingsInnerConfig +}) => { + // cloud - always use the environment variable + if (!env.SELF_HOSTED) { + return !!env.ENABLE_ANALYTICS + } + + // self host - prefer the settings doc + // use cache as events have high throughput + const enabledInDB = await withCache( + CacheKey.ANALYTICS_ENABLED, + TTL.ONE_DAY, + async () => { + const config = opts?.config + ? opts.config + : // direct to db to prevent infinite loop + (await getConfig(ConfigType.SETTINGS))?.config + + // need to do explicit checks in case the field is not set + if (config?.analyticsEnabled === false) { + return false + } else if (config?.analyticsEnabled === true) { + return true + } + } + ) + + if (enabledInDB !== undefined) { + return enabledInDB + } + + // fallback to the environment variable + // explicitly check for 0 or false here, undefined or otherwise is treated as true + const envEnabled: any = env.ENABLE_ANALYTICS + if (envEnabled === 0 || envEnabled === false) { + return false + } else { + return true + } +} + +// GOOGLE + +async function getGoogleConfigDoc(): Promise { + return await getConfig(ConfigType.GOOGLE) +} + +export async function getGoogleConfig(): Promise< + GoogleInnerConfig | undefined +> { + const config = await getGoogleConfigDoc() + return config?.config +} + +export async function getGoogleDatasourceConfig(): Promise< + GoogleInnerConfig | undefined +> { + if (!env.SELF_HOSTED) { + // always use the env vars in cloud + return getDefaultGoogleConfig() + } + + // prefer the config in self-host + let config = await getGoogleConfig() + + // fallback to env vars + if (!config || !config.activated) { + config = getDefaultGoogleConfig() + } + + return config +} + +export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined { + if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) { + return { + clientID: environment.GOOGLE_CLIENT_ID!, + clientSecret: environment.GOOGLE_CLIENT_SECRET!, + activated: true, + } + } +} + +// OIDC + +async function getOIDCConfigDoc(): Promise { + return getConfig(ConfigType.OIDC) +} + +export async function getOIDCConfig(): Promise { + const config = (await getOIDCConfigDoc())?.config + // default to the 0th config + return config?.configs && config.configs[0] +} + +/** + * @param configId The config id of the inner config to retrieve + */ +export async function getOIDCConfigById( + configId: string +): Promise { + const config = (await getConfig(ConfigType.OIDC))?.config + return config && config.configs.filter((c: any) => c.uuid === configId)[0] +} + +// SMTP + +export async function getSMTPConfigDoc(): Promise { + return getConfig(ConfigType.SMTP) +} + +export async function getSMTPConfig( + isAutomation?: boolean +): Promise { + const config = await getSMTPConfigDoc() + if (config) { + return config.config + } + + // always allow fallback in self host + // in cloud don't allow for automations + const allowFallback = env.SELF_HOSTED || !isAutomation + + // Use an SMTP fallback configuration from env variables + if (env.SMTP_FALLBACK_ENABLED && allowFallback) { + return { + port: env.SMTP_PORT, + host: env.SMTP_HOST!, + secure: false, + from: env.SMTP_FROM_ADDRESS!, + auth: { + user: env.SMTP_USER!, + pass: env.SMTP_PASSWORD!, + }, + } + } +} diff --git a/packages/backend-core/src/configs/index.ts b/packages/backend-core/src/configs/index.ts new file mode 100644 index 0000000000..783f22a0b9 --- /dev/null +++ b/packages/backend-core/src/configs/index.ts @@ -0,0 +1 @@ +export * from "./configs" diff --git a/packages/backend-core/src/configs/tests/configs.spec.ts b/packages/backend-core/src/configs/tests/configs.spec.ts new file mode 100644 index 0000000000..079f2ab681 --- /dev/null +++ b/packages/backend-core/src/configs/tests/configs.spec.ts @@ -0,0 +1,116 @@ +import { DBTestConfiguration, generator, testEnv } from "../../../tests" +import { ConfigType } from "@budibase/types" +import env from "../../environment" +import * as configs from "../configs" + +const DEFAULT_URL = "http://localhost:10000" +const ENV_URL = "http://env.com" + +describe("configs", () => { + const config = new DBTestConfiguration() + + const setDbPlatformUrl = async (dbUrl: string) => { + const settingsConfig = { + _id: configs.generateConfigID(ConfigType.SETTINGS), + type: ConfigType.SETTINGS, + config: { + platformUrl: dbUrl, + }, + } + await configs.save(settingsConfig) + } + + beforeEach(async () => { + config.newTenant() + }) + + describe("getPlatformUrl", () => { + describe("self host", () => { + beforeEach(async () => { + testEnv.selfHosted() + }) + + it("gets the default url", async () => { + await config.doInTenant(async () => { + const url = await configs.getPlatformUrl() + expect(url).toBe(DEFAULT_URL) + }) + }) + + it("gets the platform url from the environment", async () => { + await config.doInTenant(async () => { + env._set("PLATFORM_URL", ENV_URL) + const url = await configs.getPlatformUrl() + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the database", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const url = await configs.getPlatformUrl() + expect(url).toBe(dbUrl) + }) + }) + }) + + describe("cloud", () => { + function getTenantAwareUrl() { + return `http://${config.tenantId}.env.com` + } + + beforeEach(async () => { + testEnv.cloudHosted() + testEnv.multiTenant() + + env._set("PLATFORM_URL", ENV_URL) + }) + + it("gets the platform url from the environment without tenancy", async () => { + await config.doInTenant(async () => { + const url = await configs.getPlatformUrl({ tenantAware: false }) + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the environment with tenancy", async () => { + await config.doInTenant(async () => { + const url = await configs.getPlatformUrl() + expect(url).toBe(getTenantAwareUrl()) + }) + }) + + it("never gets the platform url from the database", async () => { + await config.doInTenant(async () => { + await setDbPlatformUrl(generator.url()) + const url = await configs.getPlatformUrl() + expect(url).toBe(getTenantAwareUrl()) + }) + }) + }) + }) + + describe("getSettingsConfig", () => { + beforeAll(async () => { + testEnv.selfHosted() + env._set("PLATFORM_URL", "") + }) + + it("returns the platform url with an existing config", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const config = await configs.getSettingsConfig() + expect(config.platformUrl).toBe(dbUrl) + }) + }) + + it("returns the platform url without an existing config", async () => { + await config.doInTenant(async () => { + const config = await configs.getSettingsConfig() + expect(config.platformUrl).toBe(DEFAULT_URL) + }) + }) + }) +}) diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index f7d15b3880..d41098c405 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -68,6 +68,7 @@ export enum DocumentType { MEM_VIEW = "view", USER_FLAG = "flag", AUTOMATION_METADATA = "meta_au", + AUDIT_LOG = "al", } export const StaticDatabases = { @@ -88,6 +89,9 @@ export const StaticDatabases = { install: "install", }, }, + AUDIT_LOGS: { + name: "audit-logs", + }, } export const APP_PREFIX = DocumentType.APP + SEPARATOR diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index 0bf3df4094..e25c90575f 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -41,5 +41,6 @@ export enum Config { OIDC_LOGOS = "logos_oidc", } +export const MIN_VALID_DATE = new Date(-2147483647000) export const MAX_VALID_DATE = new Date(2147483647000) export const DEFAULT_TENANT_ID = "default" diff --git a/packages/backend-core/src/context/Context.ts b/packages/backend-core/src/context/Context.ts index 02b7713764..d29b6935a8 100644 --- a/packages/backend-core/src/context/Context.ts +++ b/packages/backend-core/src/context/Context.ts @@ -1,5 +1,5 @@ import { AsyncLocalStorage } from "async_hooks" -import { ContextMap } from "./mainContext" +import { ContextMap } from "./types" export default class Context { static storage = new AsyncLocalStorage() diff --git a/packages/backend-core/src/context/identity.ts b/packages/backend-core/src/context/identity.ts index 648dd1b5fd..84de3b68c9 100644 --- a/packages/backend-core/src/context/identity.ts +++ b/packages/backend-core/src/context/identity.ts @@ -5,6 +5,8 @@ import { isCloudAccount, Account, AccountUserContext, + UserContext, + Ctx, } from "@budibase/types" import * as context from "." @@ -16,15 +18,22 @@ export function doInIdentityContext(identity: IdentityContext, task: any) { return context.doInIdentityContext(identity, task) } -export function doInUserContext(user: User, task: any) { - const userContext: any = { +// used in server/worker +export function doInUserContext(user: User, ctx: Ctx, task: any) { + const userContext: UserContext = { ...user, _id: user._id as string, type: IdentityType.USER, + hostInfo: { + ipAddress: ctx.request.ip, + // filled in by koa-useragent package + userAgent: ctx.userAgent._agent.source, + }, } return doInIdentityContext(userContext, task) } +// used in account portal export function doInAccountContext(account: Account, task: any) { const _id = getAccountUserId(account) const tenantId = account.tenantId diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 9884d25d5a..02ba16aa8c 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -11,13 +11,7 @@ import { DEFAULT_TENANT_ID, } from "../constants" import { Database, IdentityContext } from "@budibase/types" - -export type ContextMap = { - tenantId?: string - appId?: string - identity?: IdentityContext - environmentVariables?: Record -} +import { ContextMap } from "./types" let TEST_APP_ID: string | null = null @@ -30,14 +24,23 @@ export function getGlobalDBName(tenantId?: string) { return baseGlobalDBName(tenantId) } -export function baseGlobalDBName(tenantId: string | undefined | null) { - let dbName - if (!tenantId || tenantId === DEFAULT_TENANT_ID) { - dbName = StaticDatabases.GLOBAL.name - } else { - dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` +export function getAuditLogDBName(tenantId?: string) { + if (!tenantId) { + tenantId = getTenantId() + } + if (tenantId === DEFAULT_TENANT_ID) { + return StaticDatabases.AUDIT_LOGS.name + } else { + return `${tenantId}${SEPARATOR}${StaticDatabases.AUDIT_LOGS.name}` + } +} + +export function baseGlobalDBName(tenantId: string | undefined | null) { + if (!tenantId || tenantId === DEFAULT_TENANT_ID) { + return StaticDatabases.GLOBAL.name + } else { + return `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` } - return dbName } export function isMultiTenant() { @@ -228,6 +231,13 @@ export function getGlobalDB(): Database { return getDB(baseGlobalDBName(context?.tenantId)) } +export function getAuditLogsDB(): Database { + if (!getTenantId()) { + throw new Error("No tenant ID found - cannot open audit log DB") + } + return getDB(getAuditLogDBName()) +} + /** * Gets the app database based on whatever the request * contained, dev or prod. diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts new file mode 100644 index 0000000000..78197ed528 --- /dev/null +++ b/packages/backend-core/src/context/types.ts @@ -0,0 +1,9 @@ +import { IdentityContext } from "@budibase/types" + +// keep this out of Budibase types, don't want to expose context info +export type ContextMap = { + tenantId?: string + appId?: string + identity?: IdentityContext + environmentVariables?: Record +} diff --git a/packages/backend-core/src/db/index.ts b/packages/backend-core/src/db/index.ts index 0d9f75fa18..a569b17b36 100644 --- a/packages/backend-core/src/db/index.ts +++ b/packages/backend-core/src/db/index.ts @@ -7,3 +7,4 @@ export { default as Replication } from "./Replication" // exports to support old export structure export * from "../constants/db" export { getGlobalDBName, baseGlobalDBName } from "../context" +export * from "./lucene" diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts new file mode 100644 index 0000000000..cba2f0138a --- /dev/null +++ b/packages/backend-core/src/db/lucene.ts @@ -0,0 +1,624 @@ +import fetch from "node-fetch" +import { getCouchInfo } from "./couch" +import { SearchFilters, Row } from "@budibase/types" + +const QUERY_START_REGEX = /\d[0-9]*:/g + +interface SearchResponse { + rows: T[] | any[] + bookmark: string +} + +interface PaginatedSearchResponse extends SearchResponse { + hasNextPage: boolean +} + +export type SearchParams = { + tableId?: string + sort?: string + sortOrder?: string + sortType?: string + limit?: number + bookmark?: string + version?: string + indexer?: () => Promise + disableEscaping?: boolean + rows?: T | Row[] +} + +export function removeKeyNumbering(key: any): string { + if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) { + const parts = key.split(":") + // remove the number + parts.shift() + return parts.join(":") + } else { + return key + } +} + +/** + * Class to build lucene query URLs. + * Optionally takes a base lucene query object. + */ +export class QueryBuilder { + dbName: string + index: string + query: SearchFilters + limit: number + sort?: string + bookmark?: string + sortOrder: string + sortType: string + includeDocs: boolean + version?: string + indexBuilder?: () => Promise + noEscaping = false + + constructor(dbName: string, index: string, base?: SearchFilters) { + this.dbName = dbName + this.index = index + this.query = { + allOr: false, + string: {}, + fuzzy: {}, + range: {}, + equal: {}, + notEqual: {}, + empty: {}, + notEmpty: {}, + oneOf: {}, + contains: {}, + notContains: {}, + containsAny: {}, + ...base, + } + this.limit = 50 + this.sortOrder = "ascending" + this.sortType = "string" + this.includeDocs = true + } + + disableEscaping() { + this.noEscaping = true + return this + } + + setIndexBuilder(builderFn: () => Promise) { + this.indexBuilder = builderFn + return this + } + + setVersion(version?: string) { + if (version != null) { + this.version = version + } + return this + } + + setTable(tableId: string) { + this.query.equal!.tableId = tableId + return this + } + + setLimit(limit?: number) { + if (limit != null) { + this.limit = limit + } + return this + } + + setSort(sort?: string) { + if (sort != null) { + this.sort = sort + } + return this + } + + setSortOrder(sortOrder?: string) { + if (sortOrder != null) { + this.sortOrder = sortOrder + } + return this + } + + setSortType(sortType?: string) { + if (sortType != null) { + this.sortType = sortType + } + return this + } + + setBookmark(bookmark?: string) { + if (bookmark != null) { + this.bookmark = bookmark + } + return this + } + + excludeDocs() { + this.includeDocs = false + return this + } + + addString(key: string, partial: string) { + this.query.string![key] = partial + return this + } + + addFuzzy(key: string, fuzzy: string) { + this.query.fuzzy![key] = fuzzy + return this + } + + addRange(key: string, low: string | number, high: string | number) { + this.query.range![key] = { + low, + high, + } + return this + } + + addEqual(key: string, value: any) { + this.query.equal![key] = value + return this + } + + addNotEqual(key: string, value: any) { + this.query.notEqual![key] = value + return this + } + + addEmpty(key: string, value: any) { + this.query.empty![key] = value + return this + } + + addNotEmpty(key: string, value: any) { + this.query.notEmpty![key] = value + return this + } + + addOneOf(key: string, value: any) { + this.query.oneOf![key] = value + return this + } + + addContains(key: string, value: any) { + this.query.contains![key] = value + return this + } + + addNotContains(key: string, value: any) { + this.query.notContains![key] = value + return this + } + + addContainsAny(key: string, value: any) { + this.query.containsAny![key] = value + return this + } + + handleSpaces(input: string) { + if (this.noEscaping) { + return input + } else { + return input.replace(/ /g, "_") + } + } + + /** + * Preprocesses a value before going into a lucene search. + * Transforms strings to lowercase and wraps strings and bools in quotes. + * @param value The value to process + * @param options The preprocess options + * @returns {string|*} + */ + preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) { + const hasVersion = !!this.version + // Determine if type needs wrapped + const originalType = typeof value + // Convert to lowercase + if (value && lowercase) { + value = value.toLowerCase ? value.toLowerCase() : value + } + // Escape characters + if (!this.noEscaping && escape && originalType === "string") { + value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") + } + + // Wrap in quotes + if (originalType === "string" && !isNaN(value) && !type) { + value = `"${value}"` + } else if (hasVersion && wrap) { + value = originalType === "number" ? value : `"${value}"` + } + return value + } + + buildSearchQuery() { + const builder = this + let allOr = this.query && this.query.allOr + let query = allOr ? "" : "*:*" + const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true } + let tableId + if (this.query.equal!.tableId) { + tableId = this.query.equal!.tableId + delete this.query.equal!.tableId + } + + const equal = (key: string, value: any) => { + // 0 evaluates to false, which means we would return all rows if we don't check it + if (!value && value !== 0) { + return null + } + return `${key}:${builder.preprocess(value, allPreProcessingOpts)}` + } + + const contains = (key: string, value: any, mode = "AND") => { + if (Array.isArray(value) && value.length === 0) { + return null + } + if (!Array.isArray(value)) { + return `${key}:${value}` + } + let statement = `${builder.preprocess(value[0], { escape: true })}` + for (let i = 1; i < value.length; i++) { + statement += ` ${mode} ${builder.preprocess(value[i], { + escape: true, + })}` + } + return `${key}:(${statement})` + } + + const notContains = (key: string, value: any) => { + // @ts-ignore + const allPrefix = allOr === "" ? "*:* AND" : "" + return allPrefix + "NOT " + contains(key, value) + } + + const containsAny = (key: string, value: any) => { + return contains(key, value, "OR") + } + + const oneOf = (key: string, value: any) => { + if (!Array.isArray(value)) { + if (typeof value === "string") { + value = value.split(",") + } else { + return "" + } + } + let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}` + for (let i = 1; i < value.length; i++) { + orStatement += ` OR ${builder.preprocess( + value[i], + allPreProcessingOpts + )}` + } + return `${key}:(${orStatement})` + } + + function build(structure: any, queryFn: any) { + for (let [key, value] of Object.entries(structure)) { + // check for new format - remove numbering if needed + key = removeKeyNumbering(key) + key = builder.preprocess(builder.handleSpaces(key), { + escape: true, + }) + const expression = queryFn(key, value) + if (expression == null) { + continue + } + if (query.length > 0) { + query += ` ${allOr ? "OR" : "AND"} ` + } + query += expression + } + } + + // Construct the actual lucene search query string from JSON structure + if (this.query.string) { + build(this.query.string, (key: string, value: any) => { + if (!value) { + return null + } + value = builder.preprocess(value, { + escape: true, + lowercase: true, + type: "string", + }) + return `${key}:${value}*` + }) + } + if (this.query.range) { + build(this.query.range, (key: string, value: any) => { + if (!value) { + return null + } + if (value.low == null || value.low === "") { + return null + } + if (value.high == null || value.high === "") { + return null + } + const low = builder.preprocess(value.low, allPreProcessingOpts) + const high = builder.preprocess(value.high, allPreProcessingOpts) + return `${key}:[${low} TO ${high}]` + }) + } + if (this.query.fuzzy) { + build(this.query.fuzzy, (key: string, value: any) => { + if (!value) { + return null + } + value = builder.preprocess(value, { + escape: true, + lowercase: true, + type: "fuzzy", + }) + return `${key}:${value}~` + }) + } + if (this.query.equal) { + build(this.query.equal, equal) + } + if (this.query.notEqual) { + build(this.query.notEqual, (key: string, value: any) => { + if (!value) { + return null + } + return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}` + }) + } + if (this.query.empty) { + build(this.query.empty, (key: string) => `!${key}:["" TO *]`) + } + if (this.query.notEmpty) { + build(this.query.notEmpty, (key: string) => `${key}:["" TO *]`) + } + if (this.query.oneOf) { + build(this.query.oneOf, oneOf) + } + if (this.query.contains) { + build(this.query.contains, contains) + } + if (this.query.notContains) { + build(this.query.notContains, notContains) + } + if (this.query.containsAny) { + build(this.query.containsAny, containsAny) + } + // make sure table ID is always added as an AND + if (tableId) { + query = `(${query})` + allOr = false + build({ tableId }, equal) + } + return query + } + + buildSearchBody() { + let body: any = { + q: this.buildSearchQuery(), + limit: Math.min(this.limit, 200), + include_docs: this.includeDocs, + } + if (this.bookmark) { + body.bookmark = this.bookmark + } + if (this.sort) { + const order = this.sortOrder === "descending" ? "-" : "" + const type = `<${this.sortType}>` + body.sort = `${order}${this.handleSpaces(this.sort)}${type}` + } + return body + } + + async run() { + const { url, cookie } = getCouchInfo() + const fullPath = `${url}/${this.dbName}/_design/database/_search/${this.index}` + const body = this.buildSearchBody() + try { + return await runQuery(fullPath, body, cookie) + } catch (err: any) { + if (err.status === 404 && this.indexBuilder) { + await this.indexBuilder() + return await runQuery(fullPath, body, cookie) + } else { + throw err + } + } + } +} + +/** + * Executes a lucene search query. + * @param url The query URL + * @param body The request body defining search criteria + * @param cookie The auth cookie for CouchDB + * @returns {Promise<{rows: []}>} + */ +async function runQuery( + url: string, + body: any, + cookie: string +): Promise> { + const response = await fetch(url, { + body: JSON.stringify(body), + method: "POST", + headers: { + Authorization: cookie, + }, + }) + + if (response.status === 404) { + throw response + } + const json = await response.json() + + let output: any = { + rows: [], + } + if (json.rows != null && json.rows.length > 0) { + output.rows = json.rows.map((row: any) => row.doc) + } + if (json.bookmark) { + output.bookmark = json.bookmark + } + return output +} + +/** + * Gets round the fixed limit of 200 results from a query by fetching as many + * pages as required and concatenating the results. This recursively operates + * until enough results have been found. + * @param dbName {string} Which database to run a lucene query on + * @param index {string} Which search index to utilise + * @param query {object} The JSON query structure + * @param params {object} The search params including: + * tableId {string} The table ID to search + * sort {string} The sort column + * sortOrder {string} The sort order ("ascending" or "descending") + * sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * limit {number} The number of results to fetch + * bookmark {string|null} Current bookmark in the recursive search + * rows {array|null} Current results in the recursive search + * @returns {Promise<*[]|*>} + */ +async function recursiveSearch( + dbName: string, + index: string, + query: any, + params: any +): Promise { + const bookmark = params.bookmark + const rows = params.rows || [] + if (rows.length >= params.limit) { + return rows + } + let pageSize = 200 + if (rows.length > params.limit - 200) { + pageSize = params.limit - rows.length + } + const page = await new QueryBuilder(dbName, index, query) + .setVersion(params.version) + .setTable(params.tableId) + .setBookmark(bookmark) + .setLimit(pageSize) + .setSort(params.sort) + .setSortOrder(params.sortOrder) + .setSortType(params.sortType) + .run() + if (!page.rows.length) { + return rows + } + if (page.rows.length < 200) { + return [...rows, ...page.rows] + } + const newParams = { + ...params, + bookmark: page.bookmark, + rows: [...rows, ...page.rows], + } + return await recursiveSearch(dbName, index, query, newParams) +} + +/** + * Performs a paginated search. A bookmark will be returned to allow the next + * page to be fetched. There is a max limit off 200 results per page in a + * paginated search. + * @param dbName {string} Which database to run a lucene query on + * @param index {string} Which search index to utilise + * @param query {object} The JSON query structure + * @param params {object} The search params including: + * tableId {string} The table ID to search + * sort {string} The sort column + * sortOrder {string} The sort order ("ascending" or "descending") + * sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * limit {number} The desired page size + * bookmark {string} The bookmark to resume from + * @returns {Promise<{hasNextPage: boolean, rows: *[]}>} + */ +export async function paginatedSearch( + dbName: string, + index: string, + query: SearchFilters, + params: SearchParams +) { + let limit = params.limit + if (limit == null || isNaN(limit) || limit < 0) { + limit = 50 + } + limit = Math.min(limit, 200) + const search = new QueryBuilder(dbName, index, query) + if (params.version) { + search.setVersion(params.version) + } + if (params.tableId) { + search.setTable(params.tableId) + } + if (params.sort) { + search + .setSort(params.sort) + .setSortOrder(params.sortOrder) + .setSortType(params.sortType) + } + if (params.indexer) { + search.setIndexBuilder(params.indexer) + } + if (params.disableEscaping) { + search.disableEscaping() + } + const searchResults = await search + .setBookmark(params.bookmark) + .setLimit(limit) + .run() + + // Try fetching 1 row in the next page to see if another page of results + // exists or not + search.setBookmark(searchResults.bookmark).setLimit(1) + if (params.tableId) { + search.setTable(params.tableId) + } + const nextResults = await search.run() + + return { + ...searchResults, + hasNextPage: nextResults.rows && nextResults.rows.length > 0, + } +} + +/** + * Performs a full search, fetching multiple pages if required to return the + * desired amount of results. There is a limit of 1000 results to avoid + * heavy performance hits, and to avoid client components breaking from + * handling too much data. + * @param dbName {string} Which database to run a lucene query on + * @param index {string} Which search index to utilise + * @param query {object} The JSON query structure + * @param params {object} The search params including: + * tableId {string} The table ID to search + * sort {string} The sort column + * sortOrder {string} The sort order ("ascending" or "descending") + * sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * limit {number} The desired number of results + * @returns {Promise<{rows: *}>} + */ +export async function fullSearch( + dbName: string, + index: string, + query: SearchFilters, + params: SearchParams +) { + let limit = params.limit + if (limit == null || isNaN(limit) || limit < 0) { + limit = 1000 + } + params.limit = Math.min(limit, 1000) + const rows = await recursiveSearch(dbName, index, query, params) + return { rows } +} diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts new file mode 100644 index 0000000000..23b01e18df --- /dev/null +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -0,0 +1,161 @@ +import { newid } from "../../newid" +import { getDB } from "../db" +import { Database } from "@budibase/types" +import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene" + +const INDEX_NAME = "main" + +const index = `function(doc) { + let props = ["property", "number"] + for (let key of props) { + if (doc[key]) { + index(key, doc[key]) + } + } +}` + +describe("lucene", () => { + let db: Database, dbName: string + + beforeAll(async () => { + dbName = `db-${newid()}` + // create the DB for testing + db = getDB(dbName) + await db.put({ _id: newid(), property: "word" }) + await db.put({ _id: newid(), property: "word2" }) + await db.put({ _id: newid(), property: "word3", number: 1 }) + }) + + it("should be able to create a lucene index", async () => { + const response = await db.put({ + _id: "_design/database", + indexes: { + [INDEX_NAME]: { + index: index, + analyzer: "standard", + }, + }, + }) + expect(response.ok).toBe(true) + }) + + describe("query builder", () => { + it("should be able to perform a basic query", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.setSort("property") + builder.setSortOrder("desc") + builder.setSortType("string") + const resp = await builder.run() + expect(resp.rows.length).toBe(3) + }) + + it("should handle limits", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.setLimit(1) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a string search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addString("property", "wo") + const resp = await builder.run() + expect(resp.rows.length).toBe(3) + }) + + it("should be able to perform a range search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addRange("number", 0, 1) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform an equal search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addEqual("property", "word2") + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a not equal search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addNotEqual("property", "word2") + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + + it("should be able to perform an empty search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addEmpty("number", true) + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + + it("should be able to perform a not empty search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addNotEmpty("number", true) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a one of search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addOneOf("property", ["word", "word2"]) + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + + it("should be able to perform a contains search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addContains("property", ["word"]) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a not contains search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addNotContains("property", ["word2"]) + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + }) + + describe("paginated search", () => { + it("should be able to perform a paginated search", async () => { + const page = await paginatedSearch( + dbName, + INDEX_NAME, + { + string: { + property: "wo", + }, + }, + { + limit: 1, + sort: "property", + sortType: "string", + sortOrder: "desc", + } + ) + expect(page.rows.length).toBe(1) + expect(page.hasNextPage).toBe(true) + expect(page.bookmark).toBeDefined() + }) + }) + + describe("full search", () => { + it("should be able to perform a full search", async () => { + const page = await fullSearch( + dbName, + INDEX_NAME, + { + string: { + property: "wo", + }, + }, + {} + ) + expect(page.rows.length).toBe(3) + }) + }) +}) diff --git a/packages/backend-core/src/db/tests/utils.spec.ts b/packages/backend-core/src/db/tests/utils.spec.ts index 7bdca5ae8b..138457c65e 100644 --- a/packages/backend-core/src/db/tests/utils.spec.ts +++ b/packages/backend-core/src/db/tests/utils.spec.ts @@ -1,19 +1,13 @@ -import { generator, DBTestConfiguration, testEnv } from "../../../tests" import { getDevelopmentAppID, getProdAppID, isDevAppID, isProdAppID, } from "../conversions" -import { generateAppID, getPlatformUrl, getScopedConfig } from "../utils" -import * as context from "../../context" -import { Config } from "../../constants" -import env from "../../environment" +import { generateAppID } from "../utils" describe("utils", () => { - const config = new DBTestConfiguration() - - describe("app ID manipulation", () => { + describe("generateAppID", () => { function getID() { const appId = generateAppID() const split = appId.split("_") @@ -66,127 +60,4 @@ describe("utils", () => { expect(isProdAppID(devAppId)).toEqual(false) }) }) - - const DEFAULT_URL = "http://localhost:10000" - const ENV_URL = "http://env.com" - - const setDbPlatformUrl = async (dbUrl: string) => { - const db = context.getGlobalDB() - await db.put({ - _id: "config_settings", - type: Config.SETTINGS, - config: { - platformUrl: dbUrl, - }, - }) - } - - const clearSettingsConfig = async () => { - await config.doInTenant(async () => { - const db = context.getGlobalDB() - try { - const config = await db.get("config_settings") - await db.remove("config_settings", config._rev) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - }) - } - - describe("getPlatformUrl", () => { - describe("self host", () => { - beforeEach(async () => { - testEnv.selfHosted() - await clearSettingsConfig() - }) - - it("gets the default url", async () => { - await config.doInTenant(async () => { - const url = await getPlatformUrl() - expect(url).toBe(DEFAULT_URL) - }) - }) - - it("gets the platform url from the environment", async () => { - await config.doInTenant(async () => { - env._set("PLATFORM_URL", ENV_URL) - const url = await getPlatformUrl() - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the database", async () => { - await config.doInTenant(async () => { - const dbUrl = generator.url() - await setDbPlatformUrl(dbUrl) - const url = await getPlatformUrl() - expect(url).toBe(dbUrl) - }) - }) - }) - - describe("cloud", () => { - const TENANT_AWARE_URL = `http://${config.tenantId}.env.com` - - beforeEach(async () => { - testEnv.cloudHosted() - testEnv.multiTenant() - - env._set("PLATFORM_URL", ENV_URL) - await clearSettingsConfig() - }) - - it("gets the platform url from the environment without tenancy", async () => { - await config.doInTenant(async () => { - const url = await getPlatformUrl({ tenantAware: false }) - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the environment with tenancy", async () => { - await config.doInTenant(async () => { - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - - it("never gets the platform url from the database", async () => { - await config.doInTenant(async () => { - await setDbPlatformUrl(generator.url()) - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - }) - }) - - describe("getScopedConfig", () => { - describe("settings config", () => { - beforeEach(async () => { - env._set("SELF_HOSTED", 1) - env._set("PLATFORM_URL", "") - await clearSettingsConfig() - }) - - it("returns the platform url with an existing config", async () => { - await config.doInTenant(async () => { - const dbUrl = generator.url() - await setDbPlatformUrl(dbUrl) - const db = context.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(dbUrl) - }) - }) - - it("returns the platform url without an existing config", async () => { - await config.doInTenant(async () => { - const db = context.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(DEFAULT_URL) - }) - }) - }) - }) }) diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 233d044eaa..76c52d08ad 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -9,12 +9,11 @@ import { InternalTable, APP_PREFIX, } from "../constants" -import { getTenantId, getGlobalDB, getGlobalDBName } from "../context" +import { getTenantId, getGlobalDBName } from "../context" import { doWithDB, directCouchAllDbs } from "./db" import { getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "./conversions" -import * as events from "../events" -import { App, Database, ConfigType, isSettingsConfig } from "@budibase/types" +import { App, Database } from "@budibase/types" /** * Generates a new app ID. @@ -366,6 +365,16 @@ export async function getAllApps({ } } +export async function getAppsByIDs(appIds: string[]) { + const settled = await Promise.allSettled( + appIds.map(appId => getAppMetadata(appId)) + ) + // have to list the apps which exist, some may have been deleted + return settled + .filter(promise => promise.status === "fulfilled") + .map(promise => (promise as PromiseFulfilledResult).value) +} + /** * Utility function for getAllApps but filters to production apps only. */ @@ -382,6 +391,16 @@ export async function getDevAppIDs() { return apps.filter((id: any) => isDevAppID(id)) } +export function isSameAppID( + appId1: string | undefined, + appId2: string | undefined +) { + if (appId1 == undefined || appId2 == undefined) { + return false + } + return getProdAppID(appId1) === getProdAppID(appId2) +} + export async function dbExists(dbName: any) { return doWithDB( dbName, @@ -392,32 +411,6 @@ export async function dbExists(dbName: any) { ) } -/** - * Generates a new configuration ID. - * @returns {string} The new configuration ID which the config doc can be stored under. - */ -export const generateConfigID = ({ type, workspace, user }: any) => { - const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) - - return `${DocumentType.CONFIG}${SEPARATOR}${scope}` -} - -/** - * Gets parameters for retrieving configurations. - */ -export const getConfigParams = ( - { type, workspace, user }: any, - otherProps = {} -) => { - const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) - - return { - ...otherProps, - startkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}`, - endkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`, - } -} - /** * Generates a new dev info document ID - this is scoped to a user. * @returns {string} The new dev info ID which info for dev (like api key) can be stored under. @@ -441,109 +434,6 @@ export const getPluginParams = (pluginId?: string | null, otherProps = {}) => { return getDocParams(DocumentType.PLUGIN, pluginId, otherProps) } -/** - * Returns the most granular configuration document from the DB based on the type, workspace and userID passed. - * @param {Object} db - db instance to query - * @param {Object} scopes - the type, workspace and userID scopes of the configuration. - * @returns The most granular configuration document based on the scope. - */ -export const getScopedFullConfig = async function ( - db: any, - { type, user, workspace }: any -) { - const response = await db.allDocs( - getConfigParams( - { type, user, workspace }, - { - include_docs: true, - } - ) - ) - - function determineScore(row: any) { - const config = row.doc - - // Config is specific to a user and a workspace - if (config._id.includes(generateConfigID({ type, user, workspace }))) { - return 4 - } else if (config._id.includes(generateConfigID({ type, user }))) { - // Config is specific to a user only - return 3 - } else if (config._id.includes(generateConfigID({ type, workspace }))) { - // Config is specific to a workspace only - return 2 - } else if (config._id.includes(generateConfigID({ type }))) { - // Config is specific to a type only - return 1 - } - return 0 - } - - // Find the config with the most granular scope based on context - let scopedConfig = response.rows.sort( - (a: any, b: any) => determineScore(a) - determineScore(b) - )[0] - - // custom logic for settings doc - if (type === ConfigType.SETTINGS) { - if (!scopedConfig || !scopedConfig.doc) { - // defaults - scopedConfig = { - doc: { - _id: generateConfigID({ type, user, workspace }), - type: ConfigType.SETTINGS, - config: { - platformUrl: await getPlatformUrl({ tenantAware: true }), - analyticsEnabled: await events.analytics.enabled(), - }, - }, - } - } - - // will always be true - use assertion function to get type access - if (isSettingsConfig(scopedConfig.doc)) { - // overrides affected by environment - scopedConfig.doc.config.platformUrl = await getPlatformUrl({ - tenantAware: true, - }) - scopedConfig.doc.config.analyticsEnabled = - await events.analytics.enabled() - } - } - - return scopedConfig && scopedConfig.doc -} - -export const getPlatformUrl = async (opts = { tenantAware: true }) => { - let platformUrl = env.PLATFORM_URL || "http://localhost:10000" - - if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { - // cloud and multi tenant - add the tenant to the default platform url - const tenantId = getTenantId() - if (!platformUrl.includes("localhost:")) { - platformUrl = platformUrl.replace("://", `://${tenantId}.`) - } - } else if (env.SELF_HOSTED) { - const db = getGlobalDB() - // get the doc directly instead of with getScopedConfig to prevent loop - let settings - try { - settings = await db.get(generateConfigID({ type: ConfigType.SETTINGS })) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - - // self hosted - check for platform url override - if (settings && settings.config && settings.config.platformUrl) { - platformUrl = settings.config.platformUrl - } - } - - return platformUrl -} - export function pagination( data: any[], pageSize: number, @@ -577,8 +467,3 @@ export function pagination( nextPage, } } - -export async function getScopedConfig(db: any, params: any) { - const configDoc = await getScopedFullConfig(db, params) - return configDoc && configDoc.config ? configDoc.config : configDoc -} diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index ed7a161160..cd7bcca11d 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -28,6 +28,8 @@ const DefaultBucketName = { PLUGINS: "plugins", } +const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") + const environment = { isTest, isJest, @@ -58,7 +60,7 @@ const environment = { process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "", DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, - SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), + SELF_HOSTED: selfHosted, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, PLATFORM_URL: process.env.PLATFORM_URL || "", POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, @@ -84,6 +86,22 @@ const environment = { DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true, + ENABLE_AUDIT_LOG_IP_ADDR: process.env.ENABLE_AUDIT_LOG_IP_ADDR, + // smtp + SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: parseInt(process.env.SMTP_PORT || ""), + SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS, + /** + * Enable to allow an admin user to login using a password. + * This can be useful to prevent lockout when configuring SSO. + * However, this should be turned OFF by default for security purposes. + */ + ENABLE_SSO_MAINTENANCE_MODE: selfHosted + ? process.env.ENABLE_SSO_MAINTENANCE_MODE + : false, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/events/analytics.ts b/packages/backend-core/src/events/analytics.ts index 7fbc6d9c2b..dcfd6d5104 100644 --- a/packages/backend-core/src/events/analytics.ts +++ b/packages/backend-core/src/events/analytics.ts @@ -1,55 +1,6 @@ -import env from "../environment" -import * as context from "../context" -import * as dbUtils from "../db/utils" -import { Config } from "../constants" -import { withCache, TTL, CacheKey } from "../cache" +import * as configs from "../configs" +// wrapper utility function export const enabled = async () => { - // cloud - always use the environment variable - if (!env.SELF_HOSTED) { - return !!env.ENABLE_ANALYTICS - } - - // self host - prefer the settings doc - // use cache as events have high throughput - const enabledInDB = await withCache( - CacheKey.ANALYTICS_ENABLED, - TTL.ONE_DAY, - async () => { - const settings = await getSettingsDoc() - - // need to do explicit checks in case the field is not set - if (settings?.config?.analyticsEnabled === false) { - return false - } else if (settings?.config?.analyticsEnabled === true) { - return true - } - } - ) - - if (enabledInDB !== undefined) { - return enabledInDB - } - - // fallback to the environment variable - // explicitly check for 0 or false here, undefined or otherwise is treated as true - const envEnabled: any = env.ENABLE_ANALYTICS - if (envEnabled === 0 || envEnabled === false) { - return false - } else { - return true - } -} - -const getSettingsDoc = async () => { - const db = context.getGlobalDB() - let settings - try { - settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS })) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - return settings + return configs.analyticsEnabled() } diff --git a/packages/backend-core/src/events/events.ts b/packages/backend-core/src/events/events.ts index 01928221a0..c2f7cf66ec 100644 --- a/packages/backend-core/src/events/events.ts +++ b/packages/backend-core/src/events/events.ts @@ -1,4 +1,4 @@ -import { Event } from "@budibase/types" +import { Event, AuditedEventFriendlyName } from "@budibase/types" import { processors } from "./processors" import identification from "./identification" import * as backfill from "./backfill" diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 7cade9e14b..9534fb293d 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -10,7 +10,6 @@ import { isCloudAccount, isSSOAccount, TenantGroup, - SettingsConfig, CloudAccount, UserIdentity, InstallationGroup, @@ -19,10 +18,9 @@ import { isSSOUser, } from "@budibase/types" import { processors } from "./processors" -import * as dbUtils from "../db/utils" -import { Config } from "../constants" import { newid } from "../utils" import * as installation from "../installation" +import * as configs from "../configs" import { withCache, TTL, CacheKey } from "../cache/generic" const pkg = require("../../package.json") @@ -89,6 +87,7 @@ const getCurrentIdentity = async (): Promise => { installationId, tenantId, environment, + hostInfo: userContext.hostInfo, } } else { throw new Error("Unknown identity type") @@ -270,9 +269,7 @@ const getUniqueTenantId = async (tenantId: string): Promise => { return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { const db = context.getGlobalDB() - const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { - type: Config.SETTINGS, - }) + const config = await configs.getSettingsConfigDoc() let uniqueTenantId: string if (config.config.uniqueTenantId) { diff --git a/packages/backend-core/src/events/processors/AuditLogsProcessor.ts b/packages/backend-core/src/events/processors/AuditLogsProcessor.ts new file mode 100644 index 0000000000..fd68b66871 --- /dev/null +++ b/packages/backend-core/src/events/processors/AuditLogsProcessor.ts @@ -0,0 +1,90 @@ +import { + Event, + Identity, + Group, + IdentityType, + AuditLogQueueEvent, + AuditLogFn, + HostInfo, +} from "@budibase/types" +import { EventProcessor } from "./types" +import { getAppId } from "../../context" +import BullQueue from "bull" +import { createQueue, JobQueue } from "../../queue" +import { isAudited } from "../../utils" +import env from "../../environment" + +export default class AuditLogsProcessor implements EventProcessor { + static auditLogsEnabled = false + static auditLogQueue: BullQueue.Queue + + // can't use constructor as need to return promise + static init(fn: AuditLogFn) { + AuditLogsProcessor.auditLogsEnabled = true + const writeAuditLogs = fn + AuditLogsProcessor.auditLogQueue = createQueue( + JobQueue.AUDIT_LOG + ) + return AuditLogsProcessor.auditLogQueue.process(async job => { + let properties = job.data.properties + if (properties.audited) { + properties = { + ...properties, + ...properties.audited, + } + delete properties.audited + } + + // this feature is disabled by default due to privacy requirements + // in some countries - available as env var in-case it is desired + // in self host deployments + let hostInfo: HostInfo | undefined = {} + if (env.ENABLE_AUDIT_LOG_IP_ADDR) { + hostInfo = job.data.opts.hostInfo + } + + await writeAuditLogs(job.data.event, properties, { + userId: job.data.opts.userId, + timestamp: job.data.opts.timestamp, + appId: job.data.opts.appId, + hostInfo, + }) + }) + } + + async processEvent( + event: Event, + identity: Identity, + properties: any, + timestamp?: string + ): Promise { + if (AuditLogsProcessor.auditLogsEnabled && isAudited(event)) { + // only audit log actual events, don't include backfills + const userId = + identity.type === IdentityType.USER ? identity.id : undefined + // add to the event queue, rather than just writing immediately + await AuditLogsProcessor.auditLogQueue.add({ + event, + properties, + opts: { + userId, + timestamp, + appId: getAppId(), + hostInfo: identity.hostInfo, + }, + }) + } + } + + async identify(identity: Identity, timestamp?: string | number) { + // no-op + } + + async identifyGroup(group: Group, timestamp?: string | number) { + // no-op + } + + shutdown(): void { + AuditLogsProcessor.auditLogQueue?.close() + } +} diff --git a/packages/backend-core/src/events/processors/index.ts b/packages/backend-core/src/events/processors/index.ts index 0e75f050db..6646764e47 100644 --- a/packages/backend-core/src/events/processors/index.ts +++ b/packages/backend-core/src/events/processors/index.ts @@ -1,8 +1,19 @@ import AnalyticsProcessor from "./AnalyticsProcessor" import LoggingProcessor from "./LoggingProcessor" +import AuditLogsProcessor from "./AuditLogsProcessor" import Processors from "./Processors" +import { AuditLogFn } from "@budibase/types" export const analyticsProcessor = new AnalyticsProcessor() const loggingProcessor = new LoggingProcessor() +const auditLogsProcessor = new AuditLogsProcessor() -export const processors = new Processors([analyticsProcessor, loggingProcessor]) +export function init(auditingFn: AuditLogFn) { + return AuditLogsProcessor.init(auditingFn) +} + +export const processors = new Processors([ + analyticsProcessor, + loggingProcessor, + auditLogsProcessor, +]) diff --git a/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts index 593e5ff082..0dbe70d543 100644 --- a/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts +++ b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts @@ -47,6 +47,8 @@ export default class PosthogProcessor implements EventProcessor { return } + properties = this.clearPIIProperties(properties) + properties.version = pkg.version properties.service = env.SERVICE properties.environment = identity.environment @@ -79,6 +81,16 @@ export default class PosthogProcessor implements EventProcessor { this.posthog.capture(payload) } + clearPIIProperties(properties: any) { + if (properties.email) { + delete properties.email + } + if (properties.audited) { + delete properties.audited + } + return properties + } + async identify(identity: Identity, timestamp?: string | number) { const payload: any = { distinctId: identity.id, properties: identity } if (timestamp) { diff --git a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts index 2c1340d36e..8df4e40bcf 100644 --- a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +++ b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts @@ -49,6 +49,25 @@ describe("PosthogProcessor", () => { expect(processor.posthog.capture).toHaveBeenCalledTimes(0) }) + it("removes audited information", async () => { + const processor = new PosthogProcessor("test") + + const identity = newIdentity() + const properties = { + email: "test", + audited: { + name: "test", + }, + } + + await processor.processEvent(Event.USER_CREATED, identity, properties) + expect(processor.posthog.capture).toHaveBeenCalled() + // @ts-ignore + const call = processor.posthog.capture.mock.calls[0][0] + expect(call.properties.audited).toBeUndefined() + expect(call.properties.email).toBeUndefined() + }) + describe("rate limiting", () => { it("sends daily event once in same day", async () => { const processor = new PosthogProcessor("test") diff --git a/packages/backend-core/src/events/publishers/app.ts b/packages/backend-core/src/events/publishers/app.ts index 90da21f3f5..d08d59b5f1 100644 --- a/packages/backend-core/src/events/publishers/app.ts +++ b/packages/backend-core/src/events/publishers/app.ts @@ -19,6 +19,9 @@ const created = async (app: App, timestamp?: string | number) => { const properties: AppCreatedEvent = { appId: app.appId, version: app.version, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_CREATED, properties, timestamp) } @@ -27,6 +30,9 @@ async function updated(app: App) { const properties: AppUpdatedEvent = { appId: app.appId, version: app.version, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_UPDATED, properties) } @@ -34,6 +40,9 @@ async function updated(app: App) { async function deleted(app: App) { const properties: AppDeletedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_DELETED, properties) } @@ -41,6 +50,9 @@ async function deleted(app: App) { async function published(app: App, timestamp?: string | number) { const properties: AppPublishedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_PUBLISHED, properties, timestamp) } @@ -48,6 +60,9 @@ async function published(app: App, timestamp?: string | number) { async function unpublished(app: App) { const properties: AppUnpublishedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_UNPUBLISHED, properties) } @@ -55,6 +70,9 @@ async function unpublished(app: App) { async function fileImported(app: App) { const properties: AppFileImportedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_FILE_IMPORTED, properties) } @@ -63,6 +81,9 @@ async function templateImported(app: App, templateKey: string) { const properties: AppTemplateImportedEvent = { appId: app.appId, templateKey, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_TEMPLATE_IMPORTED, properties) } @@ -76,6 +97,9 @@ async function versionUpdated( appId: app.appId, currentVersion, updatedToVersion, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_VERSION_UPDATED, properties) } @@ -89,6 +113,9 @@ async function versionReverted( appId: app.appId, currentVersion, revertedToVersion, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_VERSION_REVERTED, properties) } @@ -96,6 +123,9 @@ async function versionReverted( async function reverted(app: App) { const properties: AppRevertedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_REVERTED, properties) } @@ -103,6 +133,9 @@ async function reverted(app: App) { async function exported(app: App) { const properties: AppExportedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_EXPORTED, properties) } diff --git a/packages/backend-core/src/events/publishers/auditLog.ts b/packages/backend-core/src/events/publishers/auditLog.ts new file mode 100644 index 0000000000..7cfb76147a --- /dev/null +++ b/packages/backend-core/src/events/publishers/auditLog.ts @@ -0,0 +1,26 @@ +import { + Event, + AuditLogSearchParams, + AuditLogFilteredEvent, + AuditLogDownloadedEvent, +} from "@budibase/types" +import { publishEvent } from "../events" + +async function filtered(search: AuditLogSearchParams) { + const properties: AuditLogFilteredEvent = { + filters: search, + } + await publishEvent(Event.AUDIT_LOGS_FILTERED, properties) +} + +async function downloaded(search: AuditLogSearchParams) { + const properties: AuditLogDownloadedEvent = { + filters: search, + } + await publishEvent(Event.AUDIT_LOGS_DOWNLOADED, properties) +} + +export default { + filtered, + downloaded, +} diff --git a/packages/backend-core/src/events/publishers/auth.ts b/packages/backend-core/src/events/publishers/auth.ts index 4436045599..e275d2dbb0 100644 --- a/packages/backend-core/src/events/publishers/auth.ts +++ b/packages/backend-core/src/events/publishers/auth.ts @@ -12,19 +12,25 @@ import { } from "@budibase/types" import { identification } from ".." -async function login(source: LoginSource) { +async function login(source: LoginSource, email: string) { const identity = await identification.getCurrentIdentity() const properties: LoginEvent = { userId: identity.id, source, + audited: { + email, + }, } await publishEvent(Event.AUTH_LOGIN, properties) } -async function logout() { +async function logout(email?: string) { const identity = await identification.getCurrentIdentity() const properties: LogoutEvent = { userId: identity.id, + audited: { + email, + }, } await publishEvent(Event.AUTH_LOGOUT, properties) } diff --git a/packages/backend-core/src/events/publishers/automation.ts b/packages/backend-core/src/events/publishers/automation.ts index 6eb36ab067..419d4136bd 100644 --- a/packages/backend-core/src/events/publishers/automation.ts +++ b/packages/backend-core/src/events/publishers/automation.ts @@ -18,6 +18,9 @@ async function created(automation: Automation, timestamp?: string | number) { automationId: automation._id as string, triggerId: automation.definition?.trigger?.id, triggerType: automation.definition?.trigger?.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_CREATED, properties, timestamp) } @@ -38,6 +41,9 @@ async function deleted(automation: Automation) { automationId: automation._id as string, triggerId: automation.definition?.trigger?.id, triggerType: automation.definition?.trigger?.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_DELETED, properties) } @@ -71,6 +77,9 @@ async function stepCreated( triggerType: automation.definition?.trigger?.stepId, stepId: step.id!, stepType: step.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_STEP_CREATED, properties, timestamp) } @@ -83,6 +92,9 @@ async function stepDeleted(automation: Automation, step: AutomationStep) { triggerType: automation.definition?.trigger?.stepId, stepId: step.id!, stepType: step.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_STEP_DELETED, properties) } diff --git a/packages/backend-core/src/events/publishers/backup.ts b/packages/backend-core/src/events/publishers/backup.ts index 12263fe1ff..d7d87f09f1 100644 --- a/packages/backend-core/src/events/publishers/backup.ts +++ b/packages/backend-core/src/events/publishers/backup.ts @@ -13,6 +13,7 @@ async function appBackupRestored(backup: AppBackup) { appId: backup.appId, restoreId: backup._id!, backupCreatedAt: backup.timestamp, + name: backup.name as string, } await publishEvent(Event.APP_BACKUP_RESTORED, properties) @@ -22,13 +23,15 @@ async function appBackupTriggered( appId: string, backupId: string, type: AppBackupType, - trigger: AppBackupTrigger + trigger: AppBackupTrigger, + name: string ) { const properties: AppBackupTriggeredEvent = { appId: appId, backupId, type, trigger, + name, } await publishEvent(Event.APP_BACKUP_TRIGGERED, properties) } diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts index d79920562b..a000b880a2 100644 --- a/packages/backend-core/src/events/publishers/group.ts +++ b/packages/backend-core/src/events/publishers/group.ts @@ -8,12 +8,16 @@ import { GroupUsersAddedEvent, GroupUsersDeletedEvent, GroupAddedOnboardingEvent, + GroupPermissionsEditedEvent, UserGroupRoles, } from "@budibase/types" async function created(group: UserGroup, timestamp?: number) { const properties: GroupCreatedEvent = { groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp) } @@ -21,6 +25,9 @@ async function created(group: UserGroup, timestamp?: number) { async function updated(group: UserGroup) { const properties: GroupUpdatedEvent = { groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_UPDATED, properties) } @@ -28,6 +35,9 @@ async function updated(group: UserGroup) { async function deleted(group: UserGroup) { const properties: GroupDeletedEvent = { groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_DELETED, properties) } @@ -36,6 +46,9 @@ async function usersAdded(count: number, group: UserGroup) { const properties: GroupUsersAddedEvent = { count, groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_USERS_ADDED, properties) } @@ -44,6 +57,9 @@ async function usersDeleted(count: number, group: UserGroup) { const properties: GroupUsersDeletedEvent = { count, groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties) } @@ -56,9 +72,13 @@ async function createdOnboarding(groupId: string) { await publishEvent(Event.USER_GROUP_ONBOARDING, properties) } -async function permissionsEdited(roles: UserGroupRoles) { - const properties: UserGroupRoles = { - ...roles, +async function permissionsEdited(group: UserGroup) { + const properties: GroupPermissionsEditedEvent = { + permissions: group.roles!, + groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties) } diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 34e47b2990..87a34bf3f1 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -21,3 +21,4 @@ export { default as group } from "./group" export { default as plugin } from "./plugin" export { default as backup } from "./backup" export { default as environmentVariable } from "./environmentVariable" +export { default as auditLog } from "./auditLog" diff --git a/packages/backend-core/src/events/publishers/screen.ts b/packages/backend-core/src/events/publishers/screen.ts index 27264b5847..df486029e8 100644 --- a/packages/backend-core/src/events/publishers/screen.ts +++ b/packages/backend-core/src/events/publishers/screen.ts @@ -11,6 +11,9 @@ async function created(screen: Screen, timestamp?: string | number) { layoutId: screen.layoutId, screenId: screen._id as string, roleId: screen.routing.roleId, + audited: { + name: screen.routing?.route, + }, } await publishEvent(Event.SCREEN_CREATED, properties, timestamp) } @@ -20,6 +23,9 @@ async function deleted(screen: Screen) { layoutId: screen.layoutId, screenId: screen._id as string, roleId: screen.routing.roleId, + audited: { + name: screen.routing?.route, + }, } await publishEvent(Event.SCREEN_DELETED, properties) } diff --git a/packages/backend-core/src/events/publishers/table.ts b/packages/backend-core/src/events/publishers/table.ts index d50f4df0e1..dc3200291a 100644 --- a/packages/backend-core/src/events/publishers/table.ts +++ b/packages/backend-core/src/events/publishers/table.ts @@ -13,6 +13,9 @@ import { async function created(table: Table, timestamp?: string | number) { const properties: TableCreatedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_CREATED, properties, timestamp) } @@ -20,6 +23,9 @@ async function created(table: Table, timestamp?: string | number) { async function updated(table: Table) { const properties: TableUpdatedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_UPDATED, properties) } @@ -27,6 +33,9 @@ async function updated(table: Table) { async function deleted(table: Table) { const properties: TableDeletedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_DELETED, properties) } @@ -35,6 +44,9 @@ async function exported(table: Table, format: TableExportFormat) { const properties: TableExportedEvent = { tableId: table._id as string, format, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_EXPORTED, properties) } @@ -42,6 +54,9 @@ async function exported(table: Table, format: TableExportFormat) { async function imported(table: Table) { const properties: TableImportedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_IMPORTED, properties) } diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index 1fe50149b5..8dbc494d1e 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -19,6 +19,9 @@ import { async function created(user: User, timestamp?: number) { const properties: UserCreatedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_CREATED, properties, timestamp) } @@ -26,6 +29,9 @@ async function created(user: User, timestamp?: number) { async function updated(user: User) { const properties: UserUpdatedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_UPDATED, properties) } @@ -33,6 +39,9 @@ async function updated(user: User) { async function deleted(user: User) { const properties: UserDeletedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_DELETED, properties) } @@ -40,6 +49,9 @@ async function deleted(user: User) { export async function onboardingComplete(user: User) { const properties: UserOnboardingEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties) } @@ -49,6 +61,9 @@ export async function onboardingComplete(user: User) { async function permissionAdminAssigned(user: User, timestamp?: number) { const properties: UserPermissionAssignedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent( Event.USER_PERMISSION_ADMIN_ASSIGNED, @@ -60,6 +75,9 @@ async function permissionAdminAssigned(user: User, timestamp?: number) { async function permissionAdminRemoved(user: User) { const properties: UserPermissionRemovedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PERMISSION_ADMIN_REMOVED, properties) } @@ -67,6 +85,9 @@ async function permissionAdminRemoved(user: User) { async function permissionBuilderAssigned(user: User, timestamp?: number) { const properties: UserPermissionAssignedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent( Event.USER_PERMISSION_BUILDER_ASSIGNED, @@ -78,20 +99,30 @@ async function permissionBuilderAssigned(user: User, timestamp?: number) { async function permissionBuilderRemoved(user: User) { const properties: UserPermissionRemovedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PERMISSION_BUILDER_REMOVED, properties) } // INVITE -async function invited() { - const properties: UserInvitedEvent = {} +async function invited(email: string) { + const properties: UserInvitedEvent = { + audited: { + email, + }, + } await publishEvent(Event.USER_INVITED, properties) } async function inviteAccepted(user: User) { const properties: UserInviteAcceptedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_INVITED_ACCEPTED, properties) } @@ -101,6 +132,9 @@ async function inviteAccepted(user: User) { async function passwordForceReset(user: User) { const properties: UserPasswordForceResetEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_FORCE_RESET, properties) } @@ -108,6 +142,9 @@ async function passwordForceReset(user: User) { async function passwordUpdated(user: User) { const properties: UserPasswordUpdatedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_UPDATED, properties) } @@ -115,6 +152,9 @@ async function passwordUpdated(user: User) { async function passwordResetRequested(user: User) { const properties: UserPasswordResetRequestedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_RESET_REQUESTED, properties) } @@ -122,6 +162,9 @@ async function passwordResetRequested(user: User) { async function passwordReset(user: User) { const properties: UserPasswordResetEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_RESET, properties) } diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index d507d8175f..48569548e3 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -1,3 +1,4 @@ +export * as configs from "./configs" export * as events from "./events" export * as migrations from "./migrations" export * as users from "./users" @@ -20,11 +21,11 @@ export * as context from "./context" export * as cache from "./cache" export * as objectStore from "./objectStore" export * as redis from "./redis" -export * as locks from "./redis/redlock" +export * as locks from "./redis/redlockImpl" export * as utils from "./utils" export * as errors from "./errors" export { default as env } from "./environment" - +export { SearchParams } from "./db" // Add context to tenancy for backwards compatibility // only do this for external usages to prevent internal // circular dependencies diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 4bb2aaba76..d7e6346b3f 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -8,7 +8,7 @@ import { getGlobalDB, doInTenant } from "../context" import { decrypt } from "../security/encryption" import * as identity from "../context/identity" import env from "../environment" -import { BBContext, EndpointMatcher } from "@budibase/types" +import { Ctx, EndpointMatcher } from "@budibase/types" const ONE_MINUTE = env.SESSION_UPDATE_PERIOD ? parseInt(env.SESSION_UPDATE_PERIOD) @@ -73,7 +73,7 @@ export default function ( } ) { const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] - return async (ctx: BBContext | any, next: any) => { + return async (ctx: Ctx | any, next: any) => { let publicEndpoint = false const version = ctx.request.headers[Header.API_VER] // the path is not authenticated @@ -115,7 +115,8 @@ export default function ( authenticated = true } catch (err: any) { authenticated = false - console.error("Auth Error", err?.message || err) + console.error(`Auth Error: ${err.message}`) + console.error(err) // remove the cookie as the user does not exist anymore clearCookie(ctx, Cookie.Auth) } @@ -148,7 +149,7 @@ export default function ( finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) if (user && user.email) { - return identity.doInUserContext(user, next) + return identity.doInUserContext(user, ctx, next) } else { return next() } diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts index 5ac70c33e5..36aff2cdbc 100644 --- a/packages/backend-core/src/middleware/errorHandling.ts +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -11,6 +11,7 @@ export async function errorHandling(ctx: any, next: any) { if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) { ctx.log.error(err) + console.trace(err) } const error = errors.getPublicError(err) diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index de609f9a3e..addeac6a1a 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -17,4 +17,5 @@ export { default as builderOrAdmin } from "./builderOrAdmin" export { default as builderOnly } from "./builderOnly" export { default as logging } from "./logging" export { default as errorHandling } from "./errorHandling" +export { default as querystringToBody } from "./querystringToBody" export * as joiValidator from "./joi-validator" diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 112f8d2096..32451cb8d2 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,9 +1,8 @@ import * as google from "../sso/google" -import { Cookie, Config } from "../../../constants" +import { Cookie } from "../../../constants" import { clearCookie, getCookie } from "../../../utils" -import { getScopedConfig, getPlatformUrl, doWithDB } from "../../../db" -import environment from "../../../environment" -import { getGlobalDB } from "../../../context" +import { doWithDB } from "../../../db" +import * as configs from "../../../configs" import { BBContext, Database, SSOProfile } from "@budibase/types" import { ssoSaveUserNoOp } from "../sso/sso" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy @@ -13,18 +12,12 @@ type Passport = { } async function fetchGoogleCreds() { - // try and get the config from the tenant - const db = getGlobalDB() - const googleConfig = await getScopedConfig(db, { - type: Config.GOOGLE, - }) - // or fall back to env variables - return ( - googleConfig || { - clientID: environment.GOOGLE_CLIENT_ID, - clientSecret: environment.GOOGLE_CLIENT_SECRET, - } - ) + let config = await configs.getGoogleDatasourceConfig() + + if (!config) { + throw new Error("No google configuration found") + } + return config } export async function preAuth( @@ -34,7 +27,7 @@ export async function preAuth( ) { // get the relevant config const googleConfig = await fetchGoogleCreds() - const platformUrl = await getPlatformUrl({ tenantAware: false }) + const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` const strategy = await google.strategyFactory( @@ -61,7 +54,7 @@ export async function postAuth( ) { // get the relevant config const config = await fetchGoogleCreds() - const platformUrl = await getPlatformUrl({ tenantAware: false }) + const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth) diff --git a/packages/backend-core/src/middleware/passport/sso/google.ts b/packages/backend-core/src/middleware/passport/sso/google.ts index d26d7d6a8d..ad7593e63d 100644 --- a/packages/backend-core/src/middleware/passport/sso/google.ts +++ b/packages/backend-core/src/middleware/passport/sso/google.ts @@ -2,12 +2,11 @@ import { ssoCallbackUrl } from "../utils" import * as sso from "./sso" import { ConfigType, - GoogleConfig, - Database, SSOProfile, SSOAuthDetails, SSOProviderType, SaveSSOUserFunction, + GoogleInnerConfig, } from "@budibase/types" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy @@ -45,7 +44,7 @@ export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { * @returns Dynamically configured Passport Google Strategy */ export async function strategyFactory( - config: GoogleConfig["config"], + config: GoogleInnerConfig, callbackUrl: string, saveUserFn: SaveSSOUserFunction ) { @@ -73,9 +72,6 @@ export async function strategyFactory( } } -export async function getCallbackUrl( - db: Database, - config: { callbackURL?: string } -) { - return ssoCallbackUrl(db, config, ConfigType.GOOGLE) +export async function getCallbackUrl(config: GoogleInnerConfig) { + return ssoCallbackUrl(ConfigType.GOOGLE, config) } diff --git a/packages/backend-core/src/middleware/passport/sso/oidc.ts b/packages/backend-core/src/middleware/passport/sso/oidc.ts index 1fb44b84a3..b6d5eb52e9 100644 --- a/packages/backend-core/src/middleware/passport/sso/oidc.ts +++ b/packages/backend-core/src/middleware/passport/sso/oidc.ts @@ -4,7 +4,6 @@ import { ssoCallbackUrl } from "../utils" import { ConfigType, OIDCInnerConfig, - Database, SSOProfile, OIDCStrategyConfiguration, SSOAuthDetails, @@ -157,9 +156,6 @@ export async function fetchStrategyConfig( } } -export async function getCallbackUrl( - db: Database, - config: { callbackURL?: string } -) { - return ssoCallbackUrl(db, config, ConfigType.OIDC) +export async function getCallbackUrl() { + return ssoCallbackUrl(ConfigType.OIDC) } diff --git a/packages/backend-core/src/middleware/passport/utils.ts b/packages/backend-core/src/middleware/passport/utils.ts index 6eb3bc29d1..7e0d3863a0 100644 --- a/packages/backend-core/src/middleware/passport/utils.ts +++ b/packages/backend-core/src/middleware/passport/utils.ts @@ -1,6 +1,6 @@ -import { isMultiTenant, getTenantId } from "../../context" -import { getScopedConfig } from "../../db" -import { ConfigType, Database } from "@budibase/types" +import { getTenantId, isMultiTenant } from "../../context" +import * as configs from "../../configs" +import { ConfigType, GoogleInnerConfig } from "@budibase/types" /** * Utility to handle authentication errors. @@ -19,17 +19,14 @@ export function authError(done: Function, message: string, err?: any) { } export async function ssoCallbackUrl( - db: Database, - config?: { callbackURL?: string }, - type?: ConfigType + type: ConfigType, + config?: GoogleInnerConfig ) { // incase there is a callback URL from before - if (config && config.callbackURL) { - return config.callbackURL + if (config && (config as GoogleInnerConfig).callbackURL) { + return (config as GoogleInnerConfig).callbackURL as string } - const publicConfig = await getScopedConfig(db, { - type: ConfigType.SETTINGS, - }) + const settingsConfig = await configs.getSettingsConfig() let callbackUrl = `/api/global/auth` if (isMultiTenant()) { @@ -37,5 +34,5 @@ export async function ssoCallbackUrl( } callbackUrl += `/${type}/callback` - return `${publicConfig.platformUrl}${callbackUrl}` + return `${settingsConfig.platformUrl}${callbackUrl}` } diff --git a/packages/backend-core/src/middleware/querystringToBody.ts b/packages/backend-core/src/middleware/querystringToBody.ts new file mode 100644 index 0000000000..b6f109231a --- /dev/null +++ b/packages/backend-core/src/middleware/querystringToBody.ts @@ -0,0 +1,28 @@ +import { Ctx } from "@budibase/types" + +/** + * Expects a standard "query" query string property which is the JSON body + * of the request, which has to be sent via query string due to the requirement + * of making an endpoint a GET request e.g. downloading a file stream. + */ +export default function (ctx: Ctx, next: any) { + const queryString = ctx.request.query?.query as string | undefined + if (ctx.request.method.toLowerCase() !== "get") { + ctx.throw( + 500, + "Query to download middleware can only be used for get requests." + ) + } + if (!queryString) { + return next() + } + const decoded = decodeURIComponent(queryString) + let json + try { + json = JSON.parse(decoded) + } catch (err) { + return next() + } + ctx.request.body = json + return next() +} diff --git a/packages/backend-core/src/platform/tenants.ts b/packages/backend-core/src/platform/tenants.ts index b9f946a735..b6bc3410d8 100644 --- a/packages/backend-core/src/platform/tenants.ts +++ b/packages/backend-core/src/platform/tenants.ts @@ -1,7 +1,7 @@ import { StaticDatabases } from "../constants" import { getPlatformDB } from "./platformDb" import { LockName, LockOptions, LockType, Tenants } from "@budibase/types" -import * as locks from "../redis/redlock" +import * as locks from "../redis/redlockImpl" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants diff --git a/packages/backend-core/src/queue/constants.ts b/packages/backend-core/src/queue/constants.ts index e8323dacb8..9261ed1176 100644 --- a/packages/backend-core/src/queue/constants.ts +++ b/packages/backend-core/src/queue/constants.ts @@ -1,4 +1,5 @@ export enum JobQueue { AUTOMATION = "automationQueue", APP_BACKUP = "appBackupQueue", + AUDIT_LOG = "auditLogQueue", } diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index 8e1fc1fbf3..c57ebafb1f 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -40,8 +40,10 @@ export function createQueue( } export async function shutdown() { - if (QUEUES.length) { + if (cleanupInterval) { clearInterval(cleanupInterval) + } + if (QUEUES.length) { for (let queue of QUEUES) { await queue.close() } diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 5bf2c65c39..6585d6e4fa 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -3,4 +3,4 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" -export * as locks from "./redlock" +export * as locks from "./redlockImpl" diff --git a/packages/backend-core/src/redis/redlock.ts b/packages/backend-core/src/redis/redlockImpl.ts similarity index 100% rename from packages/backend-core/src/redis/redlock.ts rename to packages/backend-core/src/redis/redlockImpl.ts diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index ef76af390d..9ef2c5c31f 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -10,14 +10,38 @@ import { BulkDocsResponse, User } from "@budibase/types" import { getGlobalDB } from "./context" import * as context from "./context" -export const bulkGetGlobalUsersById = async (userIds: string[]) => { +type GetOpts = { cleanup?: boolean } + +function removeUserPassword(users: User | User[]) { + if (Array.isArray(users)) { + return users.map(user => { + if (user) { + delete user.password + return user + } + }) + } else if (users) { + delete users.password + return users + } + return users +} + +export const bulkGetGlobalUsersById = async ( + userIds: string[], + opts?: GetOpts +) => { const db = getGlobalDB() - return ( + let users = ( await db.allDocs({ keys: userIds, include_docs: true, }) ).rows.map(row => row.doc) as User[] + if (opts?.cleanup) { + users = removeUserPassword(users) as User[] + } + return users } export const bulkUpdateGlobalUsers = async (users: User[]) => { @@ -25,18 +49,22 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => { return (await db.bulkDocs(users)) as BulkDocsResponse } -export async function getById(id: string): Promise { +export async function getById(id: string, opts?: GetOpts): Promise { const db = context.getGlobalDB() - return db.get(id) + let user = await db.get(id) + if (opts?.cleanup) { + user = removeUserPassword(user) + } + return user } /** * Given an email address this will use a view to search through * all the users to find one with this email address. - * @param {string} email the email to lookup the user by. */ export const getGlobalUserByEmail = async ( - email: String + email: String, + opts?: GetOpts ): Promise => { if (email == null) { throw "Must supply an email address to view" @@ -52,10 +80,19 @@ export const getGlobalUserByEmail = async ( throw new Error(`Multiple users found with email address: ${email}`) } - return response + let user = response as User + if (opts?.cleanup) { + user = removeUserPassword(user) as User + } + + return user } -export const searchGlobalUsersByApp = async (appId: any, opts: any) => { +export const searchGlobalUsersByApp = async ( + appId: any, + opts: any, + getOpts?: GetOpts +) => { if (typeof appId !== "string") { throw new Error("Must provide a string based app ID") } @@ -67,7 +104,11 @@ export const searchGlobalUsersByApp = async (appId: any, opts: any) => { if (!response) { response = [] } - return Array.isArray(response) ? response : [response] + let users: User[] = Array.isArray(response) ? response : [response] + if (getOpts?.cleanup) { + users = removeUserPassword(users) as User[] + } + return users } export const getGlobalUserByAppPage = (appId: string, user: User) => { @@ -80,7 +121,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { /** * Performs a starts with search on the global email view. */ -export const searchGlobalUsersByEmail = async (email: string, opts: any) => { +export const searchGlobalUsersByEmail = async ( + email: string, + opts: any, + getOpts?: GetOpts +) => { if (typeof email !== "string") { throw new Error("Must provide a string to search by") } @@ -95,5 +140,9 @@ export const searchGlobalUsersByEmail = async (email: string, opts: any) => { if (!response) { response = [] } - return Array.isArray(response) ? response : [response] + let users: User[] = Array.isArray(response) ? response : [response] + if (getOpts?.cleanup) { + users = removeUserPassword(users) as User[] + } + return users } diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index 3731e134ad..3efd40ca80 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -10,7 +10,13 @@ import { import env from "../environment" import * as tenancy from "../tenancy" import * as context from "../context" -import { App, Ctx, TenantResolutionStrategy } from "@budibase/types" +import { + App, + AuditedEventFriendlyName, + Ctx, + Event, + TenantResolutionStrategy, +} from "@budibase/types" import { SetOption } from "cookies" const jwt = require("jsonwebtoken") @@ -217,3 +223,7 @@ export async function getBuildersCount() { export function timeout(timeMs: number) { return new Promise(resolve => setTimeout(resolve, timeMs)) } + +export function isAudited(event: Event) { + return !!AuditedEventFriendlyName[event] +} diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts index e374612f5f..2ca41616e4 100644 --- a/packages/backend-core/tests/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -70,6 +70,10 @@ export const useBackups = () => { return useFeature(Feature.APP_BACKUPS) } +export const useEnforceableSSO = () => { + return useFeature(Feature.ENFORCEABLE_SSO) +} + export const useGroups = () => { return useFeature(Feature.USER_GROUPS) } @@ -78,6 +82,10 @@ export const useEnvironmentVariables = () => { return useFeature(Feature.ENVIRONMENT_VARIABLES) } +export const useAuditLogs = () => { + return useFeature(Feature.AUDIT_LOGS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { diff --git a/packages/backend-core/tests/utilities/structures/generator.ts b/packages/backend-core/tests/utilities/structures/generator.ts new file mode 100644 index 0000000000..51567b152e --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/generator.ts @@ -0,0 +1,2 @@ +import Chance from "chance" +export const generator = new Chance() diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index d0073ba851..ca77f476d0 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -1,8 +1,4 @@ export * from "./common" - -import Chance from "chance" -export const generator = new Chance() - export * as accounts from "./accounts" export * as apps from "./apps" export * as db from "./db" @@ -12,3 +8,4 @@ export * as plugins from "./plugins" export * as sso from "./sso" export * as tenant from "./tenants" export * as users from "./users" +export { generator } from "./generator" diff --git a/packages/backend-core/tests/utilities/structures/shared.ts b/packages/backend-core/tests/utilities/structures/shared.ts new file mode 100644 index 0000000000..de0e19486c --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/shared.ts @@ -0,0 +1,19 @@ +import { User } from "@budibase/types" +import { generator } from "./generator" +import { uuid } from "./common" + +export const newEmail = () => { + return `${uuid()}@test.com` +} + +export const user = (userProps?: any): User => { + return { + email: newEmail(), + password: "test", + roles: { app_test: "admin" }, + firstName: generator.first(), + lastName: generator.last(), + pictureUrl: "http://test.com", + ...userProps, + } +} diff --git a/packages/backend-core/tests/utilities/structures/sso.ts b/packages/backend-core/tests/utilities/structures/sso.ts index ad5e8e87ef..a5957c9233 100644 --- a/packages/backend-core/tests/utilities/structures/sso.ts +++ b/packages/backend-core/tests/utilities/structures/sso.ts @@ -8,8 +8,36 @@ import { SSOProviderType, User, } from "@budibase/types" -import { uuid, generator, users, email } from "./index" +import { generator } from "./generator" +import { uuid, email } from "./common" +import * as shared from "./shared" import _ from "lodash" +import { user } from "./shared" + +export function authDetails(userDoc?: User): SSOAuthDetails { + if (!userDoc) { + userDoc = user() + } + + const userId = userDoc._id || uuid() + const provider = generator.string() + + const profile = ssoProfile(userDoc) + profile.provider = provider + profile.id = userId + + return { + email: userDoc.email, + oauth2: { + refreshToken: generator.string(), + accessToken: generator.string(), + }, + profile, + provider, + providerType: providerType(), + userId, + } +} export function providerType(): SSOProviderType { return _.sample(Object.values(SSOProviderType)) as SSOProviderType @@ -17,7 +45,7 @@ export function providerType(): SSOProviderType { export function ssoProfile(user?: User): SSOProfile { if (!user) { - user = users.user() + user = shared.user() } return { id: user._id!, @@ -33,31 +61,6 @@ export function ssoProfile(user?: User): SSOProfile { } } -export function authDetails(user?: User): SSOAuthDetails { - if (!user) { - user = users.user() - } - - const userId = user._id || uuid() - const provider = generator.string() - - const profile = ssoProfile(user) - profile.provider = provider - profile.id = userId - - return { - email: user.email, - oauth2: { - refreshToken: generator.string(), - accessToken: generator.string(), - }, - profile, - provider, - providerType: providerType(), - userId, - } -} - // OIDC export function oidcConfig(): OIDCInnerConfig { @@ -69,6 +72,7 @@ export function oidcConfig(): OIDCInnerConfig { configUrl: "http://someconfigurl", clientID: generator.string(), clientSecret: generator.string(), + scopes: [], } } diff --git a/packages/backend-core/tests/utilities/structures/users.ts b/packages/backend-core/tests/utilities/structures/users.ts index 332c27ca12..7a6b4f0d80 100644 --- a/packages/backend-core/tests/utilities/structures/users.ts +++ b/packages/backend-core/tests/utilities/structures/users.ts @@ -1,29 +1,13 @@ -import { generator } from "../" import { AdminUser, BuilderUser, SSOAuthDetails, SSOUser, - User, } from "@budibase/types" -import { v4 as uuid } from "uuid" -import * as sso from "./sso" +import { user } from "./shared" +import { authDetails } from "./sso" -export const newEmail = () => { - return `${uuid()}@test.com` -} - -export const user = (userProps?: any): User => { - return { - email: newEmail(), - password: "test", - roles: { app_test: "admin" }, - firstName: generator.first(), - lastName: generator.last(), - pictureUrl: "http://test.com", - ...userProps, - } -} +export { user, newEmail } from "./shared" export const adminUser = (userProps?: any): AdminUser => { return { @@ -53,7 +37,7 @@ export function ssoUser( delete base.password if (!opts.details) { - opts.details = sso.authDetails(base) + opts.details = authDetails(base) } return { diff --git a/packages/backend-core/tests/utilities/testContainerUtils.ts b/packages/backend-core/tests/utilities/testContainerUtils.ts index 11c5fca806..f6c702f7ef 100644 --- a/packages/backend-core/tests/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/utilities/testContainerUtils.ts @@ -1,3 +1,31 @@ +import { execSync } from "child_process" + +let dockerPsResult: string | undefined + +function formatDockerPsResult(serverName: string, port: number) { + const lines = dockerPsResult?.split("\n") + let first = true + if (!lines) { + return null + } + for (let line of lines) { + if (first) { + first = false + continue + } + let toLookFor = serverName.split("-service")[0] + if (!line.includes(toLookFor)) { + continue + } + const regex = new RegExp(`0.0.0.0:([0-9]*)->${port}`, "g") + const found = line.match(regex) + if (found) { + return found[0].split(":")[1].split("->")[0] + } + } + return null +} + function getTestContainerSettings( serverName: string, key: string @@ -14,10 +42,22 @@ function getTestContainerSettings( } function getContainerInfo(containerName: string, port: number) { - const assignedPort = getTestContainerSettings( + let assignedPort = getTestContainerSettings( containerName.toUpperCase(), `PORT_${port}` ) + if (!dockerPsResult) { + try { + const outputBuffer = execSync("docker ps") + dockerPsResult = outputBuffer.toString("utf8") + } catch (err) { + //no-op + } + } + const possiblePort = formatDockerPsResult(containerName, port) + if (possiblePort) { + assignedPort = possiblePort + } const host = getTestContainerSettings(containerName.toUpperCase(), "IP") return { port: assignedPort, @@ -39,12 +79,15 @@ function getRedisConfig() { } export function setupEnv(...envs: any[]) { + const couch = getCouchConfig(), + minio = getCouchConfig(), + redis = getRedisConfig() const configs = [ - { key: "COUCH_DB_PORT", value: getCouchConfig().port }, - { key: "COUCH_DB_URL", value: getCouchConfig().url }, - { key: "MINIO_PORT", value: getMinioConfig().port }, - { key: "MINIO_URL", value: getMinioConfig().url }, - { key: "REDIS_URL", value: getRedisConfig().url }, + { key: "COUCH_DB_PORT", value: couch.port }, + { key: "COUCH_DB_URL", value: couch.url }, + { key: "MINIO_PORT", value: minio.port }, + { key: "MINIO_URL", value: minio.url }, + { key: "REDIS_URL", value: redis.url }, ] for (const config of configs.filter(x => !!x.value)) { diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 05f405d63d..90c79ef807 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": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "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": "2.3.18-alpha.8", + "@budibase/string-templates": "2.3.18-alpha.15", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index d76dba96d3..ab2c941a16 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -14,6 +14,8 @@ export let autocomplete = false export let sort = false export let autoWidth = false + export let fetchTerm = null + export let customPopoverHeight const dispatch = createEventDispatcher() @@ -84,10 +86,12 @@ {options} isPlaceholder={!arrayValue.length} {autocomplete} + bind:fetchTerm {isOptionSelected} {getOptionLabel} {getOptionValue} onSelectOption={toggleOption} {sort} {autoWidth} + {customPopoverHeight} /> diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 32cfcf3310..5cef0f9213 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -31,7 +31,8 @@ export let autoWidth = false export let autocomplete = false export let sort = false - + export let fetchTerm = null + export let customPopoverHeight const dispatch = createEventDispatcher() let searchTerm = null @@ -71,7 +72,7 @@ } const getFilteredOptions = (options, term, getLabel) => { - if (autocomplete && term) { + if (autocomplete && term && !fetchTerm) { const lowerCaseTerm = term.toLowerCase() return options.filter(option => { return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm) @@ -136,6 +137,7 @@ on:close={() => (open = false)} useAnchorWidth={!autoWidth} maxWidth={autoWidth ? 400 : null} + customHeight={customPopoverHeight} >
{#if autocomplete} (searchTerm = event.detail)} + value={fetchTerm ? fetchTerm : searchTerm} + on:change={event => + fetchTerm ? (fetchTerm = event.detail) : (searchTerm = event.detail)} {disabled} placeholder="Search" /> @@ -247,7 +250,7 @@ } .popover-content.auto-width .spectrum-Menu-itemLabel { white-space: nowrap; - overflow: hidden; + overflow: none; text-overflow: ellipsis; } .popover-content:not(.auto-width) .spectrum-Menu-itemLabel { diff --git a/packages/bbui/src/Form/Multiselect.svelte b/packages/bbui/src/Form/Multiselect.svelte index 7bcf22aa06..2237cd1dce 100644 --- a/packages/bbui/src/Form/Multiselect.svelte +++ b/packages/bbui/src/Form/Multiselect.svelte @@ -15,6 +15,10 @@ export let getOptionValue = option => option export let sort = false export let autoWidth = false + export let autocomplete = false + export let fetchTerm = null + export let customPopoverHeight + const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -34,6 +38,9 @@ {getOptionLabel} {getOptionValue} {autoWidth} + {autocomplete} + {customPopoverHeight} + bind:fetchTerm on:change={onChange} on:click /> diff --git a/packages/bbui/src/Form/Select.svelte b/packages/bbui/src/Form/Select.svelte index 69126e648d..76fe613c92 100644 --- a/packages/bbui/src/Form/Select.svelte +++ b/packages/bbui/src/Form/Select.svelte @@ -20,6 +20,8 @@ export let autoWidth = false export let sort = false export let tooltip = "" + export let autocomplete = false + export let customPopoverHeight const dispatch = createEventDispatcher() const onChange = e => { @@ -51,6 +53,8 @@ {getOptionIcon} {getOptionColour} {isOptionEnabled} + {autocomplete} + {customPopoverHeight} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 8f6ef06591..081e3a34df 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -18,6 +18,7 @@ export let useAnchorWidth = false export let dismissible = true export let offset = 5 + export let customHeight $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" @@ -74,6 +75,7 @@ on:keydown={handleEscape} class="spectrum-Popover is-open" role="presentation" + style="height: {customHeight}" transition:fly|local={{ y: -20, duration: 200 }} > diff --git a/packages/builder/package.json b/packages/builder/package.json index fd7c6b5571..27cc1f31e4 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.8", - "@budibase/client": "2.3.18-alpha.8", - "@budibase/frontend-core": "2.3.18-alpha.8", - "@budibase/string-templates": "2.3.18-alpha.8", + "@budibase/bbui": "2.3.18-alpha.15", + "@budibase/client": "2.3.18-alpha.15", + "@budibase/frontend-core": "2.3.18-alpha.15", + "@budibase/string-templates": "2.3.18-alpha.15", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index d7225a6645..352f094507 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -192,13 +192,13 @@ editableColumn.name = originalName } - function deleteColumn() { + async function deleteColumn() { try { editableColumn.name = deleteColName if (editableColumn.name === $tables.selected.primaryDisplay) { notifications.error("You cannot delete the display column") } else { - tables.deleteField(editableColumn) + await tables.deleteField(editableColumn) notifications.success(`Column ${editableColumn.name} deleted.`) confirmDeleteDialog.hide() hide() diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index e3ce048a89..62194df3db 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -28,10 +28,10 @@ const validation = createValidationStore() $: { - const { name, url } = $values + const { url } = $values validation.check({ - name, + ...$values, url: url?.[0] === "/" ? url.substring(1, url.length) : url, }) } @@ -95,9 +95,9 @@ appValidation.url(validation, { apps: applications }) appValidation.file(validation, { template }) // init validation - const { name, url } = $values + const { url } = $values validation.check({ - name, + ...$values, url: url?.[0] === "/" ? url.substring(1, url.length) : url, }) } diff --git a/packages/builder/src/components/start/UpdateAppModal.svelte b/packages/builder/src/components/start/UpdateAppModal.svelte index 4385175816..59d2957bf5 100644 --- a/packages/builder/src/components/start/UpdateAppModal.svelte +++ b/packages/builder/src/components/start/UpdateAppModal.svelte @@ -24,10 +24,10 @@ const validation = createValidationStore() $: { - const { name, url } = $values + const { url } = $values validation.check({ - name, + ...$values, url: url?.[0] === "/" ? url.substring(1, url.length) : url, }) } @@ -37,9 +37,9 @@ appValidation.name(validation, { apps: applications, currentApp: app }) appValidation.url(validation, { apps: applications, currentApp: app }) // init validation - const { name, url } = $values + const { url } = $values validation.check({ - name, + ...$values, url: url?.[0] === "/" ? url.substring(1, url.length) : url, }) } diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 80122c23a5..06e09e4fee 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -79,67 +79,71 @@ - {/if} - - { - formData = { - ...formData, - username: e.detail, - } - }} - validate={() => { - let fieldError = { - username: !formData.username - ? "Please enter a valid email" - : undefined, - } - errors = handleError({ ...errors, ...fieldError }) - }} - error={errors.username} - /> - { - formData = { - ...formData, - password: e.detail, - } - }} - validate={() => { - let fieldError = { - password: !formData.password - ? "Please enter your password" - : undefined, - } - errors = handleError({ ...errors, ...fieldError }) - }} - error={errors.password} - /> - - - - - - - + {#if !$organisation.isSSOEnforced} + + + { + formData = { + ...formData, + username: e.detail, + } + }} + validate={() => { + let fieldError = { + username: !formData.username + ? "Please enter a valid email" + : undefined, + } + errors = handleError({ ...errors, ...fieldError }) + }} + error={errors.username} + /> + { + formData = { + ...formData, + password: e.detail, + } + }} + validate={() => { + let fieldError = { + password: !formData.password + ? "Please enter your password" + : undefined, + } + errors = handleError({ ...errors, ...fieldError }) + }} + error={errors.password} + /> + + {/if} + {#if !$organisation.isSSOEnforced} + + + + + + + {/if} {#if cloud} diff --git a/packages/builder/src/pages/builder/portal/_components/LockedFeature.svelte b/packages/builder/src/pages/builder/portal/_components/LockedFeature.svelte new file mode 100644 index 0000000000..e6f4075e2e --- /dev/null +++ b/packages/builder/src/pages/builder/portal/_components/LockedFeature.svelte @@ -0,0 +1,76 @@ + + + + +
+ {title} + {#if !enabled} + + {planType} + + {/if} +
+ {description} +
+ + + {#if enabled} + + {:else} +
+ + + +
+ {/if} +
+ + diff --git a/packages/builder/src/pages/builder/portal/account/_layout.svelte b/packages/builder/src/pages/builder/portal/account/_layout.svelte index 3083b574a8..892e853aad 100644 --- a/packages/builder/src/pages/builder/portal/account/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/account/_layout.svelte @@ -4,12 +4,13 @@ import { Content, SideNav, SideNavItem } from "components/portal/page" import { menu } from "stores/portal" + $: wide = $isActive("./auditLogs") $: pages = $menu.find(x => x.title === "Account")?.subPages || [] $: !pages.length && $goto("../") - +
{#each pages as { title, href }} diff --git a/packages/builder/src/pages/builder/portal/account/auditLogs/_components/AppColumnRenderer.svelte b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/AppColumnRenderer.svelte new file mode 100644 index 0000000000..08260ecc5c --- /dev/null +++ b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/AppColumnRenderer.svelte @@ -0,0 +1,5 @@ + + +
{value?.name || ""}
diff --git a/packages/builder/src/pages/builder/portal/account/auditLogs/_components/TimeRenderer.svelte b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/TimeRenderer.svelte new file mode 100644 index 0000000000..b6c0262b47 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/TimeRenderer.svelte @@ -0,0 +1,11 @@ + + +
+ {dayjs(row.timestamp).fromNow()} +
diff --git a/packages/builder/src/pages/builder/portal/account/auditLogs/_components/UserRenderer.svelte b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/UserRenderer.svelte new file mode 100644 index 0000000000..16e2a7be04 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/UserRenderer.svelte @@ -0,0 +1,45 @@ + + +
(showTooltip = true)} + on:focus={() => (showTooltip = true)} + on:mouseleave={() => (showTooltip = false)} +> + +
+{#if showTooltip} +
+ +
+{/if} + + diff --git a/packages/builder/src/pages/builder/portal/account/auditLogs/_components/ViewDetailsRenderer.svelte b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/ViewDetailsRenderer.svelte new file mode 100644 index 0000000000..b62ee48cb8 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/account/auditLogs/_components/ViewDetailsRenderer.svelte @@ -0,0 +1,13 @@ + + +Details diff --git a/packages/builder/src/pages/builder/portal/account/auditLogs/index.svelte b/packages/builder/src/pages/builder/portal/account/auditLogs/index.svelte new file mode 100644 index 0000000000..739877db26 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/account/auditLogs/index.svelte @@ -0,0 +1,501 @@ + + + + { + $licensing.goToUpgradePage() + }} +> +
+
+ user._id} + getOptionLabel={user => user.email} + options={sortedUsers} + /> +
+
+ app.instance._id} + getOptionLabel={app => app.name} + options={sortedApps} + bind:value={selectedApps} + /> +
+
+ event.id} + getOptionLabel={event => event.label} + options={sortedEvents} + placeholder="All events" + label="Events" + bind:value={selectedEvents} + /> +
+ +
+ { + if (e.detail[0]?.length === 1) { + startDate = e.detail[0][0].toISOString() + endDate = "" + } else if (e.detail[0]?.length > 1) { + startDate = e.detail[0][0].toISOString() + endDate = e.detail[0][1].toISOString() + } else { + startDate = "" + endDate = "" + } + }} + /> +
+
+ debounce(e.detail)} /> +
+ +
+ downloadLogs()} /> +
+
+ + viewDetails(detail)} + {customRenderers} + data={$auditLogs.logs.data} + allowEditColumns={false} + allowEditRows={false} + allowSelectRows={false} + {schema} + /> + + + + +{#if selectedLog} +
{ + sidePanelVisible = false + }} + > +
+ Audit Log +
+ { + wideSidePanel = !wideSidePanel + }} + /> + { + sidePanelVisible = false + }} + /> +
+
+ + +
+
copyToClipboard(JSON.stringify(selectedLog.metadata))} + class="copy-icon" + > + +
+ +
+
+{/if} + + diff --git a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/index.svelte b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/index.svelte index f2592ec805..4ff9ea386a 100644 --- a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/index.svelte +++ b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/index.svelte @@ -185,7 +185,7 @@ {#if !$licensing.backupsEnabled} - {#if !$auth.accountPortalAccess && !$licensing.groupsEnabled && $admin.cloud} + {#if !$auth.accountPortalAccess && $admin.cloud} Contact your account holder to upgrade your plan. {/if}
diff --git a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte index 608ca271c0..e5196a1cbf 100644 --- a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte @@ -22,10 +22,11 @@ Tags, Icon, Helpers, + Link, } from "@budibase/bbui" import { onMount } from "svelte" import { API } from "api" - import { organisation, admin } from "stores/portal" + import { organisation, admin, licensing } from "stores/portal" const ConfigTypes = { Google: "google", @@ -34,6 +35,8 @@ const HasSpacesRegex = /[\\"\s]/ + $: enforcedSSO = $organisation.isSSOEnforced + // Some older google configs contain a manually specified value - retain the functionality to edit the field // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change $: googleCallbackUrl = undefined @@ -154,6 +157,11 @@ iconDropdownOptions.unshift({ label: fileName, value: fileName }) } + async function toggleIsSSOEnforced() { + const value = $organisation.isSSOEnforced + await organisation.save({ isSSOEnforced: !value }) + } + async function save(docs) { let calls = [] // Only if the user has provided an image, upload it @@ -316,6 +324,49 @@ Authentication Add additional authentication methods from the options below + + + Single Sign-On URL + + Use the following link to access your configured identity provider. + + + + + + + +
+
+
+ Enforce Single Sign-On +
+ {#if !$licensing.enforceableSSO} + + Business plan + + {/if} +
+ {#if $licensing.enforceableSSO} + + {/if} +
+ + Require SSO authentication for all users. It is recommended to read the + help documentation before enabling this feature. + +
{#if providers.google} @@ -546,7 +597,24 @@ input[type="file"] { display: none; } - + .sso-link-icon { + padding-top: 4px; + margin-left: 3px; + } + .sso-link { + margin-top: 12px; + display: flex; + flex-direction: row; + align-items: center; + } + .enforce-sso-title { + margin-right: 10px; + } + .enforce-sso-heading-container { + display: flex; + flex-direction: row; + align-items: start; + } .provider-title { display: flex; flex-direction: row; diff --git a/packages/builder/src/pages/builder/portal/settings/environment/index.svelte b/packages/builder/src/pages/builder/portal/settings/environment/index.svelte index 3c170235e9..cff578febd 100644 --- a/packages/builder/src/pages/builder/portal/settings/environment/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/environment/index.svelte @@ -1,21 +1,17 @@ - - -
- Environment Variables - {#if !$licensing.environmentVariablesEnabled} - - Business plan - - {/if} -
- Add and manage environment variables for development and production -
- - - {#if $licensing.environmentVariablesEnabled} - {#if noEncryptionKey} - - {/if} -
- -
- - -
- - {:else} -
- - - -
+ { + await environment.upgradePanelOpened() + $licensing.goToUpgradePage() + }} +> + {#if noEncryptionKey} + {/if} - +
+ +
+ +
+ + diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index 8e4d6e738c..bba046ce10 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -24,6 +24,7 @@ } const values = writable({ + isSSOEnforced: $organisation.isSSOEnforced, company: $organisation.company, platformUrl: $organisation.platformUrl, analyticsEnabled: $organisation.analyticsEnabled, @@ -54,6 +55,7 @@ } const config = { + isSSOEnforced: $values.isSSOEnforced, company: $values.company ?? "", platformUrl: $values.platformUrl ?? "", analyticsEnabled: $values.analyticsEnabled, diff --git a/packages/builder/src/stores/portal/auditLogs.js b/packages/builder/src/stores/portal/auditLogs.js new file mode 100644 index 0000000000..9abf8ec11b --- /dev/null +++ b/packages/builder/src/stores/portal/auditLogs.js @@ -0,0 +1,43 @@ +import { writable, get } from "svelte/store" +import { API } from "api" +import { licensing } from "stores/portal" + +export function createAuditLogsStore() { + const { subscribe, update } = writable({ + events: {}, + logs: {}, + }) + + async function search(opts = {}) { + if (get(licensing).auditLogsEnabled) { + const paged = await API.searchAuditLogs(opts) + + update(state => { + return { ...state, logs: { ...paged, opts } } + }) + + return paged + } + } + + async function getEventDefinitions() { + const events = await API.getEventDefinitions() + + update(state => { + return { ...state, ...events } + }) + } + + function getDownloadUrl(opts = {}) { + return API.getDownloadUrl(opts) + } + + return { + subscribe, + search, + getEventDefinitions, + getDownloadUrl, + } +} + +export const auditLogs = createAuditLogsStore() diff --git a/packages/builder/src/stores/portal/index.js b/packages/builder/src/stores/portal/index.js index 8d704a5e15..e8f7853165 100644 --- a/packages/builder/src/stores/portal/index.js +++ b/packages/builder/src/stores/portal/index.js @@ -13,3 +13,4 @@ export { backups } from "./backups" export { overview } from "./overview" export { environment } from "./environment" export { menu } from "./menu" +export { auditLogs } from "./auditLogs" diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index 6f5c80e03c..df63e9455b 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -63,7 +63,13 @@ export const createLicensingStore = () => { const environmentVariablesEnabled = license.features.includes( Constants.Features.ENVIRONMENT_VARIABLES ) + const enforceableSSO = license.features.includes( + Constants.Features.ENFORCEABLE_SSO + ) + const auditLogsEnabled = license.features.includes( + Constants.Features.AUDIT_LOGS + ) store.update(state => { return { ...state, @@ -72,6 +78,8 @@ export const createLicensingStore = () => { groupsEnabled, backupsEnabled, environmentVariablesEnabled, + auditLogsEnabled, + enforceableSSO, } }) }, diff --git a/packages/builder/src/stores/portal/menu.js b/packages/builder/src/stores/portal/menu.js index 8eea36c08c..56956fc330 100644 --- a/packages/builder/src/stores/portal/menu.js +++ b/packages/builder/src/stores/portal/menu.js @@ -75,6 +75,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => { title: "Usage", href: "/builder/portal/account/usage", }, + { + title: "Audit Logs", + href: "/builder/portal/account/auditLogs", + }, ] if ($admin.cloud && $auth?.user?.accountPortalAccess) { accountSubPages.push({ @@ -87,6 +91,7 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => { href: "/builder/portal/account/upgrade", }) } + // add license check here if ( $auth?.user?.accountPortalAccess && $auth.user.account.stripeCustomerId diff --git a/packages/builder/src/stores/portal/organisation.js b/packages/builder/src/stores/portal/organisation.js index 9709578fa2..c8b62bb2bc 100644 --- a/packages/builder/src/stores/portal/organisation.js +++ b/packages/builder/src/stores/portal/organisation.js @@ -11,6 +11,7 @@ const DEFAULT_CONFIG = { google: undefined, oidcCallbackUrl: "", googleCallbackUrl: "", + isSSOEnforced: false, } export function createOrganisationStore() { @@ -19,8 +20,8 @@ export function createOrganisationStore() { async function init() { const tenantId = get(auth).tenantId - const tenant = await API.getTenantConfig(tenantId) - set({ ...DEFAULT_CONFIG, ...tenant.config, _rev: tenant._rev }) + const settingsConfigDoc = await API.getTenantConfig(tenantId) + set({ ...DEFAULT_CONFIG, ...settingsConfigDoc.config }) } async function save(config) { @@ -33,7 +34,6 @@ export function createOrganisationStore() { await API.saveConfig({ type: "settings", config: { ...get(store), ...config }, - _rev: get(store)._rev, }) await init() } diff --git a/packages/cli/package.json b/packages/cli/package.json index 48ad2655b9..219f5ed1f3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.8", - "@budibase/string-templates": "2.3.18-alpha.8", - "@budibase/types": "2.3.18-alpha.8", + "@budibase/backend-core": "2.3.18-alpha.15", + "@budibase/string-templates": "2.3.18-alpha.15", + "@budibase/types": "2.3.18-alpha.15", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index d856a6618d..c9711433a7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.8", - "@budibase/frontend-core": "2.3.18-alpha.8", - "@budibase/string-templates": "2.3.18-alpha.8", + "@budibase/bbui": "2.3.18-alpha.15", + "@budibase/frontend-core": "2.3.18-alpha.15", + "@budibase/string-templates": "2.3.18-alpha.15", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index a743d73553..927f40c568 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.18-alpha.8", + "@budibase/bbui": "2.3.18-alpha.15", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/api/auditLogs.js b/packages/frontend-core/src/api/auditLogs.js new file mode 100644 index 0000000000..c4230df6d9 --- /dev/null +++ b/packages/frontend-core/src/api/auditLogs.js @@ -0,0 +1,63 @@ +const buildOpts = ({ + bookmark, + userIds, + appIds, + startDate, + endDate, + fullSearch, + events, +}) => { + const opts = {} + + if (bookmark) { + opts.bookmark = bookmark + } + + if (startDate && endDate) { + opts.startDate = startDate + opts.endDate = endDate + } else if (startDate && !endDate) { + opts.startDate = startDate + } + + if (fullSearch) { + opts.fullSearch = fullSearch + } + + if (events.length) { + opts.events = events + } + + if (userIds.length) { + opts.userIds = userIds + } + + if (appIds.length) { + opts.appIds = appIds + } + + return opts +} + +export const buildAuditLogsEndpoints = API => ({ + /** + * Gets a list of users in the current tenant. + */ + searchAuditLogs: async opts => { + return await API.post({ + url: `/api/global/auditlogs/search`, + body: buildOpts(opts), + }) + }, + + getEventDefinitions: async () => { + return await API.get({ + url: `/api/global/auditlogs/definitions`, + }) + }, + + getDownloadUrl: opts => { + const query = encodeURIComponent(JSON.stringify(opts)) + return `/api/global/auditlogs/download?query=${query}` + }, +}) diff --git a/packages/frontend-core/src/api/index.js b/packages/frontend-core/src/api/index.js index e2935b416b..f8eee45cb8 100644 --- a/packages/frontend-core/src/api/index.js +++ b/packages/frontend-core/src/api/index.js @@ -28,6 +28,8 @@ import { buildPluginEndpoints } from "./plugins" import { buildBackupsEndpoints } from "./backups" import { buildEnvironmentVariableEndpoints } from "./environmentVariables" import { buildEventEndpoints } from "./events" +import { buildAuditLogsEndpoints } from "./auditLogs" + const defaultAPIClientConfig = { /** * Certain definitions can't change at runtime for client apps, such as the @@ -250,5 +252,6 @@ export const createAPIClient = config => { ...buildBackupsEndpoints(API), ...buildEnvironmentVariableEndpoints(API), ...buildEventEndpoints(API), + ...buildAuditLogsEndpoints(API), } } diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 3a16013df2..09b7a00aec 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -115,6 +115,8 @@ export const Features = { USER_GROUPS: "userGroups", BACKUPS: "appBackups", ENVIRONMENT_VARIABLES: "environmentVariables", + AUDIT_LOGS: "auditLogs", + ENFORCEABLE_SSO: "enforceableSSO", } // Role IDs diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d7d5db1e1a..21573b4d74 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/__mocks__/node-fetch.ts b/packages/server/__mocks__/node-fetch.ts index 78071dc8af..fdf44d173d 100644 --- a/packages/server/__mocks__/node-fetch.ts +++ b/packages/server/__mocks__/node-fetch.ts @@ -172,6 +172,9 @@ module FetchMock { ), ok: true, }) + } else if (url === "https://www.googleapis.com/oauth2/v4/token") { + // any valid response + return json({}) } else if (url.includes("failonce.com")) { failCount++ if (failCount === 1) { diff --git a/packages/server/jest.config.ts b/packages/server/jest.config.ts index 331912aa19..f247e90bd1 100644 --- a/packages/server/jest.config.ts +++ b/packages/server/jest.config.ts @@ -39,6 +39,7 @@ const config: Config.InitialOptions = { ], collectCoverageFrom: [ "src/**/*.{js,ts}", + "../backend-core/src/**/*.{js,ts}", // The use of coverage with couchdb view functions breaks tests "!src/db/views/staticViews.*", ], diff --git a/packages/server/package.json b/packages/server/package.json index 91587f06d3..39210f7d4a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.18-alpha.8", - "@budibase/client": "2.3.18-alpha.8", - "@budibase/pro": "2.3.18-alpha.8", - "@budibase/string-templates": "2.3.18-alpha.8", - "@budibase/types": "2.3.18-alpha.8", + "@budibase/backend-core": "2.3.18-alpha.15", + "@budibase/client": "2.3.18-alpha.15", + "@budibase/pro": "2.3.18-alpha.15", + "@budibase/string-templates": "2.3.18-alpha.15", + "@budibase/types": "2.3.18-alpha.15", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", @@ -87,6 +87,7 @@ "koa-send": "5.0.0", "koa-session": "5.12.0", "koa-static": "5.0.0", + "koa-useragent": "^4.1.0", "koa2-ratelimit": "1.1.1", "lodash": "4.17.21", "memorystream": "0.3.1", diff --git a/packages/server/src/api/controllers/cloud.ts b/packages/server/src/api/controllers/cloud.ts index 7f43f21fc9..7be00e3a1d 100644 --- a/packages/server/src/api/controllers/cloud.ts +++ b/packages/server/src/api/controllers/cloud.ts @@ -58,7 +58,7 @@ export async function exportApps(ctx: Ctx) { } async function checkHasBeenImported() { - if (!env.SELF_HOSTED || env.MULTI_TENANCY) { + if (!env.SELF_HOSTED) { return true } const apps = await dbCore.getAllApps({ all: true }) @@ -72,7 +72,7 @@ export async function hasBeenImported(ctx: Ctx) { } export async function importApps(ctx: Ctx) { - if (!env.SELF_HOSTED || env.MULTI_TENANCY) { + if (!env.SELF_HOSTED) { ctx.throw(400, "Importing only allowed in self hosted environments.") } const beenImported = await checkHasBeenImported() diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 2faff95595..bdf8d485f2 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -24,8 +24,7 @@ import { breakExternalTableId, isSQL } from "../../../integrations/utils" import { processObjectSync } from "@budibase/string-templates" import { cloneDeep } from "lodash/fp" import { processFormulas, processDates } from "../../../utilities/rowProcessor" -import { context } from "@budibase/backend-core" -import { removeKeyNumbering } from "./utils" +import { db as dbCore } from "@budibase/backend-core" import sdk from "../../../sdk" export interface ManyRelationship { @@ -61,7 +60,7 @@ function buildFilters( let prefix = 1 for (let operator of Object.values(filters)) { for (let field of Object.keys(operator || {})) { - if (removeKeyNumbering(field) === "_id") { + if (dbCore.removeKeyNumbering(field) === "_id") { if (primary) { const parts = breakRowIdField(operator[field]) for (let field of primary) { @@ -142,7 +141,11 @@ function cleanupConfig(config: RunConfig, table: Table): RunConfig { return config } -function generateIdForRow(row: Row | undefined, table: Table): string { +function generateIdForRow( + row: Row | undefined, + table: Table, + isLinked: boolean = false +): string { const primary = table.primary if (!row || !primary) { return "" @@ -150,8 +153,12 @@ function generateIdForRow(row: Row | undefined, table: Table): string { // build id array let idParts = [] for (let field of primary) { - // need to handle table name + field or just field, depending on if relationships used - const fieldValue = row[`${table.name}.${field}`] || row[field] + let fieldValue = extractFieldValue({ + row, + tableName: table.name, + fieldName: field, + isLinked, + }) if (fieldValue) { idParts.push(fieldValue) } @@ -174,18 +181,52 @@ function getEndpoint(tableId: string | undefined, operation: string) { } } -function basicProcessing(row: Row, table: Table): Row { +// need to handle table name + field or just field, depending on if relationships used +function extractFieldValue({ + row, + tableName, + fieldName, + isLinked, +}: { + row: Row + tableName: string + fieldName: string + isLinked: boolean +}) { + let value = row[`${tableName}.${fieldName}`] + if (value == null && !isLinked) { + value = row[fieldName] + } + return value +} + +function basicProcessing({ + row, + table, + isLinked, +}: { + row: Row + table: Table + isLinked: boolean +}): Row { const thisRow: Row = {} // filter the row down to what is actually the row (not joined) - for (let fieldName of Object.keys(table.schema)) { - const pathValue = row[`${table.name}.${fieldName}`] - const value = pathValue != null ? pathValue : row[fieldName] + for (let field of Object.values(table.schema)) { + const fieldName = field.name + + const value = extractFieldValue({ + row, + tableName: table.name, + fieldName, + isLinked, + }) + // all responses include "select col as table.col" so that overlaps are handled if (value != null) { thisRow[fieldName] = value } } - thisRow._id = generateIdForRow(row, table) + thisRow._id = generateIdForRow(row, table, isLinked) thisRow.tableId = table._id thisRow._rev = "rev" return processFormulas(table, thisRow) @@ -293,7 +334,7 @@ export class ExternalRequest { // we're not inserting a doc, will be a bunch of update calls const otherKey: string = field.throughFrom || linkTablePrimary const thisKey: string = field.throughTo || tablePrimary - row[key].map((relationship: any) => { + row[key].forEach((relationship: any) => { manyRelationships.push({ tableId: field.through || field.tableId, isUpdate: false, @@ -309,7 +350,7 @@ export class ExternalRequest { const thisKey: string = "id" // @ts-ignore const otherKey: string = field.fieldName - row[key].map((relationship: any) => { + row[key].forEach((relationship: any) => { manyRelationships.push({ tableId: field.tableId, isUpdate: true, @@ -379,7 +420,8 @@ export class ExternalRequest { ) { continue } - let linked = basicProcessing(row, linkedTable) + + let linked = basicProcessing({ row, table: linkedTable, isLinked: true }) if (!linked._id) { continue } @@ -427,7 +469,10 @@ export class ExternalRequest { ) continue } - const thisRow = fixArrayTypes(basicProcessing(row, table), table) + const thisRow = fixArrayTypes( + basicProcessing({ row, table, isLinked: false }), + table + ) if (thisRow._id == null) { throw "Unable to generate row ID for SQL rows" } @@ -567,19 +612,41 @@ export class ExternalRequest { const { key, tableId, isUpdate, id, ...rest } = relationship const body: { [key: string]: any } = processObjectSync(rest, row, {}) const linkTable = this.getTable(tableId) - // @ts-ignore - const linkPrimary = linkTable?.primary[0] + const relationshipPrimary = linkTable?.primary || [] + const linkPrimary = relationshipPrimary[0] if (!linkTable || !linkPrimary) { return } + + const linkSecondary = relationshipPrimary[1] + const rows = related[key]?.rows || [] - const found = rows.find( - (row: { [key: string]: any }) => + + function relationshipMatchPredicate({ + row, + linkPrimary, + linkSecondary, + }: { + row: { [key: string]: any } + linkPrimary: string + linkSecondary?: string + }) { + const matchesPrimaryLink = row[linkPrimary] === relationship.id || row[linkPrimary] === body?.[linkPrimary] + if (!matchesPrimaryLink || !linkSecondary) { + return matchesPrimaryLink + } + + const matchesSecondayLink = row[linkSecondary] === body?.[linkSecondary] + return matchesPrimaryLink && matchesSecondayLink + } + + const existingRelationship = rows.find((row: { [key: string]: any }) => + relationshipMatchPredicate({ row, linkPrimary, linkSecondary }) ) const operation = isUpdate ? Operation.UPDATE : Operation.CREATE - if (!found) { + if (!existingRelationship) { promises.push( getDatasourceAndQuery({ endpoint: getEndpoint(tableId, operation), @@ -590,7 +657,7 @@ export class ExternalRequest { ) } else { // remove the relationship from cache so it isn't adjusted again - rows.splice(rows.indexOf(found), 1) + rows.splice(rows.indexOf(existingRelationship), 1) } } // finally cleanup anything that needs to be removed @@ -629,10 +696,7 @@ export class ExternalRequest { * Creating the specific list of fields that we desire, and excluding the ones that are no use to us * is more performant and has the added benefit of protecting against this scenario. */ - buildFields( - table: Table, - includeRelations: IncludeRelationship = IncludeRelationship.INCLUDE - ) { + buildFields(table: Table, includeRelations: boolean) { function extractRealFields(table: Table, existing: string[] = []) { return Object.entries(table.schema) .filter( @@ -691,6 +755,10 @@ export class ExternalRequest { } filters = buildFilters(id, filters || {}, table) const relationships = this.buildRelationships(table) + + const includeSqlRelationships = + config.includeSqlRelationships === IncludeRelationship.INCLUDE + // clean up row on ingress using schema const processed = this.inputProcessing(row, table) row = processed.row @@ -708,9 +776,7 @@ export class ExternalRequest { }, resource: { // have to specify the fields to avoid column overlap (for SQL) - fields: isSql - ? this.buildFields(table, config.includeSqlRelationships) - : [], + fields: isSql ? this.buildFields(table, includeSqlRelationships) : [], }, filters, sort, @@ -725,6 +791,7 @@ export class ExternalRequest { table, }, } + // can't really use response right now const response = await getDatasourceAndQuery(json) // handle many to many relationships now if we know the ID (could be auto increment) diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index 6120c3cdcb..1b2301f139 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -58,7 +58,7 @@ export async function patch(ctx: BBContext) { return handleRequest(Operation.UPDATE, tableId, { id: breakRowIdField(id), row: inputs, - includeSqlRelationships: IncludeRelationship.EXCLUDE, + includeSqlRelationships: IncludeRelationship.INCLUDE, }) } diff --git a/packages/server/src/api/controllers/row/internalSearch.ts b/packages/server/src/api/controllers/row/internalSearch.ts index 7068aabc5a..9dc12342d6 100644 --- a/packages/server/src/api/controllers/row/internalSearch.ts +++ b/packages/server/src/api/controllers/row/internalSearch.ts @@ -1,531 +1,18 @@ -import { SearchIndexes } from "../../../db/utils" -import { removeKeyNumbering } from "./utils" -import fetch from "node-fetch" -import { db as dbCore, context } from "@budibase/backend-core" -import { SearchFilters, Row } from "@budibase/types" +import { db as dbCore, context, SearchParams } from "@budibase/backend-core" +import { SearchFilters, Row, SearchIndex } from "@budibase/types" -type SearchParams = { - tableId: string - sort?: string - sortOrder?: string - sortType?: string - limit?: number - bookmark?: string - version?: string - rows?: Row[] -} - -/** - * Class to build lucene query URLs. - * Optionally takes a base lucene query object. - */ -export class QueryBuilder { - query: SearchFilters - limit: number - sort?: string - bookmark?: string - sortOrder: string - sortType: string - includeDocs: boolean - version?: string - - constructor(base?: SearchFilters) { - this.query = { - allOr: false, - string: {}, - fuzzy: {}, - range: {}, - equal: {}, - notEqual: {}, - empty: {}, - notEmpty: {}, - oneOf: {}, - contains: {}, - notContains: {}, - containsAny: {}, - ...base, - } - this.limit = 50 - this.sortOrder = "ascending" - this.sortType = "string" - this.includeDocs = true - } - - setVersion(version?: string) { - if (version != null) { - this.version = version - } - return this - } - - setTable(tableId: string) { - this.query.equal!.tableId = tableId - return this - } - - setLimit(limit?: number) { - if (limit != null) { - this.limit = limit - } - return this - } - - setSort(sort?: string) { - if (sort != null) { - this.sort = sort - } - return this - } - - setSortOrder(sortOrder?: string) { - if (sortOrder != null) { - this.sortOrder = sortOrder - } - return this - } - - setSortType(sortType?: string) { - if (sortType != null) { - this.sortType = sortType - } - return this - } - - setBookmark(bookmark?: string) { - if (bookmark != null) { - this.bookmark = bookmark - } - return this - } - - excludeDocs() { - this.includeDocs = false - return this - } - - addString(key: string, partial: string) { - this.query.string![key] = partial - return this - } - - addFuzzy(key: string, fuzzy: string) { - this.query.fuzzy![key] = fuzzy - return this - } - - addRange(key: string, low: string | number, high: string | number) { - this.query.range![key] = { - low, - high, - } - return this - } - - addEqual(key: string, value: any) { - this.query.equal![key] = value - return this - } - - addNotEqual(key: string, value: any) { - this.query.notEqual![key] = value - return this - } - - addEmpty(key: string, value: any) { - this.query.empty![key] = value - return this - } - - addNotEmpty(key: string, value: any) { - this.query.notEmpty![key] = value - return this - } - - addOneOf(key: string, value: any) { - this.query.oneOf![key] = value - return this - } - - addContains(key: string, value: any) { - this.query.contains![key] = value - return this - } - - addNotContains(key: string, value: any) { - this.query.notContains![key] = value - return this - } - - addContainsAny(key: string, value: any) { - this.query.containsAny![key] = value - return this - } - - /** - * Preprocesses a value before going into a lucene search. - * Transforms strings to lowercase and wraps strings and bools in quotes. - * @param value The value to process - * @param options The preprocess options - * @returns {string|*} - */ - preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) { - const hasVersion = !!this.version - // Determine if type needs wrapped - const originalType = typeof value - // Convert to lowercase - if (value && lowercase) { - value = value.toLowerCase ? value.toLowerCase() : value - } - // Escape characters - if (escape && originalType === "string") { - value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") - } - - // Wrap in quotes - if (originalType === "string" && !isNaN(value) && !type) { - value = `"${value}"` - } else if (hasVersion && wrap) { - value = originalType === "number" ? value : `"${value}"` - } - return value - } - - buildSearchQuery() { - const builder = this - let allOr = this.query && this.query.allOr - let query = allOr ? "" : "*:*" - const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true } - let tableId - if (this.query.equal!.tableId) { - tableId = this.query.equal!.tableId - delete this.query.equal!.tableId - } - - const equal = (key: string, value: any) => { - // 0 evaluates to false, which means we would return all rows if we don't check it - if (!value && value !== 0) { - return null - } - return `${key}:${builder.preprocess(value, allPreProcessingOpts)}` - } - - const contains = (key: string, value: any, mode = "AND") => { - if (Array.isArray(value) && value.length === 0) { - return null - } - if (!Array.isArray(value)) { - return `${key}:${value}` - } - let statement = `${builder.preprocess(value[0], { escape: true })}` - for (let i = 1; i < value.length; i++) { - statement += ` ${mode} ${builder.preprocess(value[i], { - escape: true, - })}` - } - return `${key}:(${statement})` - } - - const notContains = (key: string, value: any) => { - // @ts-ignore - const allPrefix = allOr === "" ? "*:* AND" : "" - return allPrefix + "NOT " + contains(key, value) - } - - const containsAny = (key: string, value: any) => { - return contains(key, value, "OR") - } - - const oneOf = (key: string, value: any) => { - if (!Array.isArray(value)) { - if (typeof value === "string") { - value = value.split(",") - } else { - return "" - } - } - let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}` - for (let i = 1; i < value.length; i++) { - orStatement += ` OR ${builder.preprocess( - value[i], - allPreProcessingOpts - )}` - } - return `${key}:(${orStatement})` - } - - function build(structure: any, queryFn: any) { - for (let [key, value] of Object.entries(structure)) { - // check for new format - remove numbering if needed - key = removeKeyNumbering(key) - key = builder.preprocess(key.replace(/ /g, "_"), { - escape: true, - }) - const expression = queryFn(key, value) - if (expression == null) { - continue - } - if (query.length > 0) { - query += ` ${allOr ? "OR" : "AND"} ` - } - query += expression - } - } - - // Construct the actual lucene search query string from JSON structure - if (this.query.string) { - build(this.query.string, (key: string, value: any) => { - if (!value) { - return null - } - value = builder.preprocess(value, { - escape: true, - lowercase: true, - type: "string", - }) - return `${key}:${value}*` - }) - } - if (this.query.range) { - build(this.query.range, (key: string, value: any) => { - if (!value) { - return null - } - if (value.low == null || value.low === "") { - return null - } - if (value.high == null || value.high === "") { - return null - } - const low = builder.preprocess(value.low, allPreProcessingOpts) - const high = builder.preprocess(value.high, allPreProcessingOpts) - return `${key}:[${low} TO ${high}]` - }) - } - if (this.query.fuzzy) { - build(this.query.fuzzy, (key: string, value: any) => { - if (!value) { - return null - } - value = builder.preprocess(value, { - escape: true, - lowercase: true, - type: "fuzzy", - }) - return `${key}:${value}~` - }) - } - if (this.query.equal) { - build(this.query.equal, equal) - } - if (this.query.notEqual) { - build(this.query.notEqual, (key: string, value: any) => { - if (!value) { - return null - } - return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}` - }) - } - if (this.query.empty) { - build(this.query.empty, (key: string) => `!${key}:["" TO *]`) - } - if (this.query.notEmpty) { - build(this.query.notEmpty, (key: string) => `${key}:["" TO *]`) - } - if (this.query.oneOf) { - build(this.query.oneOf, oneOf) - } - if (this.query.contains) { - build(this.query.contains, contains) - } - if (this.query.notContains) { - build(this.query.notContains, notContains) - } - if (this.query.containsAny) { - build(this.query.containsAny, containsAny) - } - // make sure table ID is always added as an AND - if (tableId) { - query = `(${query})` - allOr = false - build({ tableId }, equal) - } - return query - } - - buildSearchBody() { - let body: any = { - q: this.buildSearchQuery(), - limit: Math.min(this.limit, 200), - include_docs: this.includeDocs, - } - if (this.bookmark) { - body.bookmark = this.bookmark - } - if (this.sort) { - const order = this.sortOrder === "descending" ? "-" : "" - const type = `<${this.sortType}>` - body.sort = `${order}${this.sort.replace(/ /g, "_")}${type}` - } - return body - } - - async run() { - const appId = context.getAppId() - const { url, cookie } = dbCore.getCouchInfo() - const fullPath = `${url}/${appId}/_design/database/_search/${SearchIndexes.ROWS}` - const body = this.buildSearchBody() - return await runQuery(fullPath, body, cookie) - } -} - -/** - * Executes a lucene search query. - * @param url The query URL - * @param body The request body defining search criteria - * @param cookie The auth cookie for CouchDB - * @returns {Promise<{rows: []}>} - */ -const runQuery = async (url: string, body: any, cookie: string) => { - const response = await fetch(url, { - body: JSON.stringify(body), - method: "POST", - headers: { - Authorization: cookie, - }, - }) - const json = await response.json() - - let output: any = { - rows: [], - } - if (json.rows != null && json.rows.length > 0) { - output.rows = json.rows.map((row: any) => row.doc) - } - if (json.bookmark) { - output.bookmark = json.bookmark - } - return output -} - -/** - * Gets round the fixed limit of 200 results from a query by fetching as many - * pages as required and concatenating the results. This recursively operates - * until enough results have been found. - * @param query {object} The JSON query structure - * @param params {object} The search params including: - * tableId {string} The table ID to search - * sort {string} The sort column - * sortOrder {string} The sort order ("ascending" or "descending") - * sortType {string} Whether to treat sortable values as strings or - * numbers. ("string" or "number") - * limit {number} The number of results to fetch - * bookmark {string|null} Current bookmark in the recursive search - * rows {array|null} Current results in the recursive search - * @returns {Promise<*[]|*>} - */ -async function recursiveSearch(query: any, params: any): Promise { - const bookmark = params.bookmark - const rows = params.rows || [] - if (rows.length >= params.limit) { - return rows - } - let pageSize = 200 - if (rows.length > params.limit - 200) { - pageSize = params.limit - rows.length - } - const page = await new QueryBuilder(query) - .setVersion(params.version) - .setTable(params.tableId) - .setBookmark(bookmark) - .setLimit(pageSize) - .setSort(params.sort) - .setSortOrder(params.sortOrder) - .setSortType(params.sortType) - .run() - if (!page.rows.length) { - return rows - } - if (page.rows.length < 200) { - return [...rows, ...page.rows] - } - const newParams = { - ...params, - bookmark: page.bookmark, - rows: [...rows, ...page.rows], - } - return await recursiveSearch(query, newParams) -} - -/** - * Performs a paginated search. A bookmark will be returned to allow the next - * page to be fetched. There is a max limit off 200 results per page in a - * paginated search. - * @param query {object} The JSON query structure - * @param params {object} The search params including: - * tableId {string} The table ID to search - * sort {string} The sort column - * sortOrder {string} The sort order ("ascending" or "descending") - * sortType {string} Whether to treat sortable values as strings or - * numbers. ("string" or "number") - * limit {number} The desired page size - * bookmark {string} The bookmark to resume from - * @returns {Promise<{hasNextPage: boolean, rows: *[]}>} - */ export async function paginatedSearch( query: SearchFilters, - params: SearchParams + params: SearchParams ) { - let limit = params.limit - if (limit == null || isNaN(limit) || limit < 0) { - limit = 50 - } - limit = Math.min(limit, 200) - const search = new QueryBuilder(query) - .setVersion(params.version) - .setTable(params.tableId) - .setSort(params.sort) - .setSortOrder(params.sortOrder) - .setSortType(params.sortType) - const searchResults = await search - .setBookmark(params.bookmark) - .setLimit(limit) - .run() - - // Try fetching 1 row in the next page to see if another page of results - // exists or not - const nextResults = await search - .setTable(params.tableId) - .setBookmark(searchResults.bookmark) - .setLimit(1) - .run() - - return { - ...searchResults, - hasNextPage: nextResults.rows && nextResults.rows.length > 0, - } + const appId = context.getAppId() + return dbCore.paginatedSearch(appId!, SearchIndex.ROWS, query, params) } -/** - * Performs a full search, fetching multiple pages if required to return the - * desired amount of results. There is a limit of 1000 results to avoid - * heavy performance hits, and to avoid client components breaking from - * handling too much data. - * @param query {object} The JSON query structure - * @param params {object} The search params including: - * tableId {string} The table ID to search - * sort {string} The sort column - * sortOrder {string} The sort order ("ascending" or "descending") - * sortType {string} Whether to treat sortable values as strings or - * numbers. ("string" or "number") - * limit {number} The desired number of results - * @returns {Promise<{rows: *}>} - */ -export async function fullSearch(query: SearchFilters, params: SearchParams) { - let limit = params.limit - if (limit == null || isNaN(limit) || limit < 0) { - limit = 1000 - } - params.limit = Math.min(limit, 1000) - const rows = await recursiveSearch(query, params) - return { rows } +export async function fullSearch( + query: SearchFilters, + params: SearchParams +) { + const appId = context.getAppId() + return dbCore.fullSearch(appId!, SearchIndex.ROWS, query, params) } diff --git a/packages/server/src/api/controllers/row/utils.ts b/packages/server/src/api/controllers/row/utils.ts index f0f1075205..82232b7f98 100644 --- a/packages/server/src/api/controllers/row/utils.ts +++ b/packages/server/src/api/controllers/row/utils.ts @@ -3,8 +3,7 @@ import * as userController from "../user" import { FieldTypes } from "../../../constants" import { context } from "@budibase/backend-core" import { makeExternalQuery } from "../../../integrations/base/query" -import { BBContext, Row, Table } from "@budibase/types" -export { removeKeyNumbering } from "../../../integrations/base/utils" +import { Row, Table } from "@budibase/types" const validateJs = require("validate.js") const { cloneDeep } = require("lodash/fp") import { Format } from "../view/exporters" diff --git a/packages/server/src/api/routes/tests/cloud.seq.spec.ts b/packages/server/src/api/routes/tests/cloud.spec.ts similarity index 70% rename from packages/server/src/api/routes/tests/cloud.seq.spec.ts rename to packages/server/src/api/routes/tests/cloud.spec.ts index d9bd6221ad..aad1214a31 100644 --- a/packages/server/src/api/routes/tests/cloud.seq.spec.ts +++ b/packages/server/src/api/routes/tests/cloud.spec.ts @@ -1,3 +1,5 @@ +import { App } from "@budibase/types" + jest.setTimeout(30000) import { AppStatus } from "../../../db/utils" @@ -5,6 +7,7 @@ import { AppStatus } from "../../../db/utils" import * as setup from "./utilities" import { wipeDb } from "./utilities/TestFunctions" +import { tenancy } from "@budibase/backend-core" describe("/cloud", () => { let request = setup.getRequest()! @@ -12,18 +15,10 @@ describe("/cloud", () => { afterAll(setup.afterAll) - beforeAll(() => { + beforeAll(async () => { // Importing is only allowed in self hosted environments - config.modeSelf() - }) - - beforeEach(async () => { await config.init() - }) - - afterEach(async () => { - // clear all mocks - jest.clearAllMocks() + config.modeSelf() }) describe("import", () => { @@ -32,30 +27,28 @@ describe("/cloud", () => { // import will not run await wipeDb() - // get a count of apps before the import - const preImportApps = await request - .get(`/api/applications?status=${AppStatus.ALL}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - // Perform the import const res = await request .post(`/api/cloud/import`) + .set(config.publicHeaders()) .attach("importFile", "src/api/routes/tests/data/export-test.tar.gz") - .set(config.defaultHeaders()) .expect(200) expect(res.body.message).toEqual("Apps successfully imported.") // get a count of apps after the import const postImportApps = await request .get(`/api/applications?status=${AppStatus.ALL}`) - .set(config.defaultHeaders()) + .set(config.publicHeaders()) .expect("Content-Type", /json/) .expect(200) + const apps = postImportApps.body as App[] // There are two apps in the file that was imported so check for this - expect(postImportApps.body.length).toEqual(2) + expect(apps.length).toEqual(2) + // The new tenant id was assigned to the imported apps + expect(tenancy.getTenantIDFromAppID(apps[0].appId)).toBe( + config.getTenantId() + ) }) }) }) diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts index 3f57c1e8ef..5a1ed5210e 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts @@ -2,7 +2,6 @@ import * as rowController from "../../../controllers/row" import * as appController from "../../../controllers/application" import { AppStatus } from "../../../../db/utils" import { roles, tenancy, context } from "@budibase/backend-core" -import { TENANT_ID } from "../../../../tests/utilities/structures" import env from "../../../../environment" import { db } from "@budibase/backend-core" import Nano from "@budibase/nano" @@ -33,7 +32,7 @@ export const getAllTableRows = async (config: any) => { } export const clearAllApps = async ( - tenantId = TENANT_ID, + tenantId: string, exceptions: Array = [] ) => { await tenancy.doInTenant(tenantId, async () => { diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 03dce4f875..00f2aca7fc 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -32,6 +32,7 @@ import { initialise as initialiseWebsockets } from "./websocket" import { startup } from "./startup" const Sentry = require("@sentry/node") const destroyable = require("server-destroy") +const { userAgent } = require("koa-useragent") const app = new Koa() @@ -53,6 +54,7 @@ app.use( ) app.use(middleware.logging) +app.use(userAgent) if (env.isProd()) { env._set("NODE_ENV", "production") diff --git a/packages/server/src/db/index.ts b/packages/server/src/db/index.ts index fa8027dcc1..157c2f4fb3 100644 --- a/packages/server/src/db/index.ts +++ b/packages/server/src/db/index.ts @@ -1,4 +1,4 @@ -import { init as coreInit } from "@budibase/backend-core" +import * as core from "@budibase/backend-core" import env from "../environment" export function init() { @@ -12,5 +12,5 @@ export function init() { dbConfig.allDbs = true } - coreInit({ db: dbConfig }) + core.init({ db: dbConfig }) } diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index ac5a6162b9..50341e4abc 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -9,10 +9,6 @@ export const AppStatus = { DEPLOYED: "published", } -export const SearchIndexes = { - ROWS: "rows", -} - export const BudibaseInternalDB = { _id: "bb_internal", type: dbCore.BUDIBASE_DATASOURCE_TYPE, diff --git a/packages/server/src/db/views/staticViews.ts b/packages/server/src/db/views/staticViews.ts index 4bccfebeee..730e5b1e66 100644 --- a/packages/server/src/db/views/staticViews.ts +++ b/packages/server/src/db/views/staticViews.ts @@ -1,6 +1,6 @@ import { context } from "@budibase/backend-core" -import { DocumentType, SEPARATOR, ViewName, SearchIndexes } from "../utils" -import { LinkDocument, Row } from "@budibase/types" +import { DocumentType, SEPARATOR, ViewName } from "../utils" +import { LinkDocument, Row, SearchIndex } from "@budibase/types" const SCREEN_PREFIX = DocumentType.SCREEN + SEPARATOR /************************************************** @@ -91,7 +91,7 @@ async function searchIndex(indexName: string, fnString: string) { export async function createAllSearchIndex() { await searchIndex( - SearchIndexes.ROWS, + SearchIndex.ROWS, function (doc: Row) { function idx(input: Row, prev?: string) { for (let key of Object.keys(input)) { diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index c688600e8d..2afff8b786 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -27,7 +27,9 @@ describe("row api - postgres", () => { let makeRequest: MakeRequestResponse, postgresDatasource: Datasource, primaryPostgresTable: Table, - auxPostgresTable: Table + oneToManyRelationshipInfo: ForeignTableInfo, + manyToOneRelationshipInfo: ForeignTableInfo, + manyToManyRelationshipInfo: ForeignTableInfo let host: string let port: number @@ -67,37 +69,58 @@ describe("row api - postgres", () => { }, }) - auxPostgresTable = await config.createTable({ - name: generator.word({ length: 10 }), - type: "external", - primary: ["id"], - schema: { - id: { - name: "id", - type: FieldType.AUTO, - constraints: { - presence: true, + async function createAuxTable(prefix: string) { + return await config.createTable({ + name: `${prefix}_${generator.word({ length: 6 })}`, + type: "external", + primary: ["id"], + primaryDisplay: "title", + schema: { + id: { + name: "id", + type: FieldType.AUTO, + autocolumn: true, + constraints: { + presence: true, + }, + }, + title: { + name: "title", + type: FieldType.STRING, + constraints: { + presence: true, + }, }, }, - title: { - name: "title", - type: FieldType.STRING, - constraints: { - presence: true, - }, - }, - }, - sourceId: postgresDatasource._id, - }) + sourceId: postgresDatasource._id, + }) + } + + oneToManyRelationshipInfo = { + table: await createAuxTable("o2m"), + fieldName: "oneToManyRelation", + relationshipType: RelationshipTypes.ONE_TO_MANY, + } + manyToOneRelationshipInfo = { + table: await createAuxTable("m2o"), + fieldName: "manyToOneRelation", + relationshipType: RelationshipTypes.MANY_TO_ONE, + } + manyToManyRelationshipInfo = { + table: await createAuxTable("m2m"), + fieldName: "manyToManyRelation", + relationshipType: RelationshipTypes.MANY_TO_MANY, + } primaryPostgresTable = await config.createTable({ - name: generator.word({ length: 10 }), + name: `p_${generator.word({ length: 6 })}`, type: "external", primary: ["id"], schema: { id: { name: "id", type: FieldType.AUTO, + autocolumn: true, constraints: { presence: true, }, @@ -117,25 +140,48 @@ describe("row api - postgres", () => { name: "value", type: FieldType.NUMBER, }, - linkedField: { + oneToManyRelation: { type: FieldType.LINK, constraints: { type: "array", presence: false, }, - fieldName: "foreignField", - name: "linkedField", + fieldName: oneToManyRelationshipInfo.fieldName, + name: "oneToManyRelation", relationshipType: RelationshipTypes.ONE_TO_MANY, - tableId: auxPostgresTable._id, + tableId: oneToManyRelationshipInfo.table._id, + main: true, + }, + manyToOneRelation: { + type: FieldType.LINK, + constraints: { + type: "array", + presence: false, + }, + fieldName: manyToOneRelationshipInfo.fieldName, + name: "manyToOneRelation", + relationshipType: RelationshipTypes.MANY_TO_ONE, + tableId: manyToOneRelationshipInfo.table._id, + main: true, + }, + manyToManyRelation: { + type: FieldType.LINK, + constraints: { + type: "array", + presence: false, + }, + fieldName: manyToManyRelationshipInfo.fieldName, + name: "manyToManyRelation", + relationshipType: RelationshipTypes.MANY_TO_MANY, + tableId: manyToManyRelationshipInfo.table._id, + main: true, }, }, sourceId: postgresDatasource._id, }) }) - afterAll(async () => { - await config.end() - }) + afterAll(config.end) function generateRandomPrimaryRowData() { return { @@ -151,22 +197,99 @@ describe("row api - postgres", () => { value: number } + type ForeignTableInfo = { + table: Table + fieldName: string + relationshipType: RelationshipTypes + } + + type ForeignRowsInfo = { + row: Row + relationshipType: RelationshipTypes + } + async function createPrimaryRow(opts: { rowData: PrimaryRowData - createForeignRow?: boolean + createForeignRows?: { + createOneToMany?: boolean + createManyToOne?: number + createManyToMany?: number + } }) { - let { rowData } = opts - let foreignRow: Row | undefined - if (opts?.createForeignRow) { - foreignRow = await config.createRow({ - tableId: auxPostgresTable._id, + let { rowData } = opts as any + let foreignRows: ForeignRowsInfo[] = [] + + async function createForeignRow(tableInfo: ForeignTableInfo) { + const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}` + + const foreignRow = await config.createRow({ + tableId: tableInfo.table._id, title: generator.name(), }) rowData = { ...rowData, - [`fk_${auxPostgresTable.name}_foreignField`]: foreignRow.id, + [foreignKey]: foreignRow.id, } + foreignRows.push({ + row: foreignRow, + + relationshipType: tableInfo.relationshipType, + }) + } + + if (opts?.createForeignRows?.createOneToMany) { + const foreignKey = `fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}` + + const foreignRow = await config.createRow({ + tableId: oneToManyRelationshipInfo.table._id, + title: generator.name(), + }) + + rowData = { + ...rowData, + [foreignKey]: foreignRow.id, + } + foreignRows.push({ + row: foreignRow, + relationshipType: oneToManyRelationshipInfo.relationshipType, + }) + } + + for (let i = 0; i < (opts?.createForeignRows?.createManyToOne || 0); i++) { + const foreignRow = await config.createRow({ + tableId: manyToOneRelationshipInfo.table._id, + title: generator.name(), + }) + + rowData = { + ...rowData, + [manyToOneRelationshipInfo.fieldName]: + rowData[manyToOneRelationshipInfo.fieldName] || [], + } + rowData[manyToOneRelationshipInfo.fieldName].push(foreignRow._id) + foreignRows.push({ + row: foreignRow, + relationshipType: RelationshipTypes.MANY_TO_ONE, + }) + } + + for (let i = 0; i < (opts?.createForeignRows?.createManyToMany || 0); i++) { + const foreignRow = await config.createRow({ + tableId: manyToManyRelationshipInfo.table._id, + title: generator.name(), + }) + + rowData = { + ...rowData, + [manyToManyRelationshipInfo.fieldName]: + rowData[manyToManyRelationshipInfo.fieldName] || [], + } + rowData[manyToManyRelationshipInfo.fieldName].push(foreignRow._id) + foreignRows.push({ + row: foreignRow, + relationshipType: RelationshipTypes.MANY_TO_MANY, + }) } const row = await config.createRow({ @@ -174,7 +297,7 @@ describe("row api - postgres", () => { ...rowData, }) - return { row, foreignRow } + return { row, foreignRows } } async function createDefaultPgTable() { @@ -198,7 +321,9 @@ describe("row api - postgres", () => { async function populatePrimaryRows( count: number, opts?: { - createForeignRow?: boolean + createOneToMany?: boolean + createManyToOne?: number + createManyToMany?: number } ) { return await Promise.all( @@ -210,7 +335,7 @@ describe("row api - postgres", () => { rowData, ...(await createPrimaryRow({ rowData, - createForeignRow: opts?.createForeignRow, + createForeignRows: opts, })), } }) @@ -295,7 +420,7 @@ describe("row api - postgres", () => { describe("given than a row exists", () => { let row: Row beforeEach(async () => { - let rowResponse = _.sample(await populatePrimaryRows(10))! + let rowResponse = _.sample(await populatePrimaryRows(1))! row = rowResponse.row }) @@ -403,7 +528,7 @@ describe("row api - postgres", () => { let rows: { row: Row; rowData: PrimaryRowData }[] beforeEach(async () => { - rows = await populatePrimaryRows(10) + rows = await populatePrimaryRows(5) }) it("a single row can be retrieved successfully", async () => { @@ -419,34 +544,136 @@ describe("row api - postgres", () => { describe("given a row with relation data", () => { let row: Row - let foreignRow: Row - beforeEach(async () => { - let [createdRow] = await populatePrimaryRows(1, { - createForeignRow: true, + let rowData: { + name: string + description: string + value: number + } + let foreignRows: ForeignRowsInfo[] + + describe("with all relationship types", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createOneToMany: true, + createManyToOne: 3, + createManyToMany: 2, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows + }) + + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + const one2ManyForeignRows = foreignRows.filter( + x => x.relationshipType === RelationshipTypes.ONE_TO_MANY + ) + expect(one2ManyForeignRows).toHaveLength(1) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + [`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]: + one2ManyForeignRows[0].row.id, + }) + + expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() }) - row = createdRow.row - foreignRow = createdRow.foreignRow! }) - it("only foreign keys are retrieved", async () => { - const res = await getRow(primaryPostgresTable._id, row.id) - - expect(res.status).toBe(200) - - expect(res.body).toEqual({ - ...row, - _id: expect.any(String), - _rev: expect.any(String), + describe("with only one to many", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createOneToMany: true, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows }) - expect(res.body.foreignField).toBeUndefined() + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) - expect( - res.body[`fk_${auxPostgresTable.name}_foreignField`] - ).toBeDefined() - expect(res.body[`fk_${auxPostgresTable.name}_foreignField`]).toBe( - foreignRow.id - ) + expect(res.status).toBe(200) + + expect(foreignRows).toHaveLength(1) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + [`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]: + foreignRows[0].row.id, + }) + + expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() + }) + }) + + describe("with only many to one", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createManyToOne: 3, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows + }) + + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + expect(foreignRows).toHaveLength(3) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + }) + + expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() + }) + }) + + describe("with only many to many", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createManyToMany: 2, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows + }) + + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + expect(foreignRows).toHaveLength(2) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + }) + + expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() + }) }) }) }) @@ -667,31 +894,74 @@ describe("row api - postgres", () => { const getAll = (tableId: string | undefined, rowId: string | undefined) => makeRequest("get", `/api/${tableId}/${rowId}/enrich`) describe("given a row with relation data", () => { - let row: Row, foreignRow: Row | undefined + let row: Row, rowData: PrimaryRowData, foreignRows: ForeignRowsInfo[] - beforeEach(async () => { - const rowsInfo = await createPrimaryRow({ - rowData: generateRandomPrimaryRowData(), - createForeignRow: true, + describe("with all relationship types", () => { + beforeEach(async () => { + rowData = generateRandomPrimaryRowData() + const rowsInfo = await createPrimaryRow({ + rowData, + createForeignRows: { + createOneToMany: true, + createManyToOne: 3, + createManyToMany: 2, + }, + }) + + row = rowsInfo.row + foreignRows = rowsInfo.foreignRows }) - row = rowsInfo.row - foreignRow = rowsInfo.foreignRow - }) + it("enrich populates the foreign fields", async () => { + const res = await getAll(primaryPostgresTable._id, row.id) - it("enrich populates the foreign field", async () => { - const res = await getAll(primaryPostgresTable._id, row.id) + expect(res.status).toBe(200) - expect(res.status).toBe(200) - - expect(foreignRow).toBeDefined() - expect(res.body).toEqual({ - ...row, - linkedField: [ - { - ...foreignRow, - }, - ], + const foreignRowsByType = _.groupBy( + foreignRows, + x => x.relationshipType + ) + expect(res.body).toEqual({ + ...rowData, + [`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]: + foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row.id, + [oneToManyRelationshipInfo.fieldName]: [ + { + ...foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row, + _id: expect.any(String), + _rev: expect.any(String), + }, + ], + [manyToOneRelationshipInfo.fieldName]: [ + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][0].row, + [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: + row.id, + }, + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][1].row, + [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: + row.id, + }, + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][2].row, + [`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]: + row.id, + }, + ], + [manyToManyRelationshipInfo.fieldName]: [ + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][0].row, + }, + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][1].row, + }, + ], + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + }) }) }) }) @@ -714,7 +984,7 @@ describe("row api - postgres", () => { const rowsCount = 6 let rows: { row: Row - foreignRow: Row | undefined + foreignRows: ForeignRowsInfo[] rowData: PrimaryRowData }[] beforeEach(async () => { diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index e66795a6db..00e62d3ce4 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -6,11 +6,11 @@ import { SearchFilters, SortDirection, } from "@budibase/types" +import { db as dbCore } from "@budibase/backend-core" import { QueryOptions } from "../../definitions/datasource" import { isIsoDateString, SqlClient } from "../utils" import SqlTableQueryBuilder from "./sqlTable" import environment from "../../environment" -import { removeKeyNumbering } from "./utils" const envLimit = environment.SQL_MAX_ROWS ? parseInt(environment.SQL_MAX_ROWS) @@ -136,7 +136,7 @@ class InternalBuilder { fn: (key: string, value: any) => void ) { for (let [key, value] of Object.entries(structure)) { - const updatedKey = removeKeyNumbering(key) + const updatedKey = dbCore.removeKeyNumbering(key) const isRelationshipField = updatedKey.includes(".") if (!opts.relationship && !isRelationshipField) { fn(`${opts.tableName}.${updatedKey}`, value) @@ -415,9 +415,7 @@ class InternalBuilder { if (opts.disableReturning) { return query.insert(parsedBody) } else { - return query - .insert(parsedBody) - .returning(generateSelectStatement(json, knex)) + return query.insert(parsedBody).returning("*") } } @@ -502,9 +500,7 @@ class InternalBuilder { if (opts.disableReturning) { return query.update(parsedBody) } else { - return query - .update(parsedBody) - .returning(generateSelectStatement(json, knex)) + return query.update(parsedBody).returning("*") } } diff --git a/packages/server/src/integrations/base/utils.ts b/packages/server/src/integrations/base/utils.ts deleted file mode 100644 index 54efdb91a0..0000000000 --- a/packages/server/src/integrations/base/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -const QUERY_START_REGEX = /\d[0-9]*:/g - -export function removeKeyNumbering(key: any): string { - if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) { - const parts = key.split(":") - // remove the number - parts.shift() - return parts.join(":") - } else { - return key - } -} diff --git a/packages/server/src/integrations/googlesheets.ts b/packages/server/src/integrations/googlesheets.ts index d3caf0b944..6f202f9c3a 100644 --- a/packages/server/src/integrations/googlesheets.ts +++ b/packages/server/src/integrations/googlesheets.ts @@ -11,9 +11,8 @@ import { OAuth2Client } from "google-auth-library" import { buildExternalTableId } from "./utils" import { DataSourceOperation, FieldTypes } from "../constants" import { GoogleSpreadsheet } from "google-spreadsheet" -import env from "../environment" -import { tenancy, db as dbCore, constants } from "@budibase/backend-core" -const fetch = require("node-fetch") +import fetch from "node-fetch" +import { configs, HTTPError } from "@budibase/backend-core" interface GoogleSheetsConfig { spreadsheetId: string @@ -112,7 +111,7 @@ const SCHEMA: Integration = { class GoogleSheetsIntegration implements DatasourcePlus { private readonly config: GoogleSheetsConfig - private client: any + private client: GoogleSpreadsheet public tables: Record = {} public schemaErrors: Record = {} @@ -173,16 +172,9 @@ class GoogleSheetsIntegration implements DatasourcePlus { async connect() { try { // Initialise oAuth client - const db = tenancy.getGlobalDB() - let googleConfig = await dbCore.getScopedConfig(db, { - type: constants.Config.GOOGLE, - }) - + let googleConfig = await configs.getGoogleDatasourceConfig() if (!googleConfig) { - googleConfig = { - clientID: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, - } + throw new HTTPError("Google config not found", 400) } const oauthClient = new OAuth2Client({ @@ -211,7 +203,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async buildSchema(datasourceId: string) { await this.connect() - const sheets = await this.client.sheetsByIndex + const sheets = this.client.sheetsByIndex const tables: Record = {} for (let sheet of sheets) { // must fetch rows to determine schema @@ -294,7 +286,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async updateTable(table?: any) { try { await this.connect() - const sheet = await this.client.sheetsByTitle[table.name] + const sheet = this.client.sheetsByTitle[table.name] await sheet.loadHeaderRow() if (table._rename) { @@ -308,10 +300,17 @@ class GoogleSheetsIntegration implements DatasourcePlus { } await sheet.setHeaderRow(headers) } else { - let newField = Object.keys(table.schema).find( + const updatedHeaderValues = [...sheet.headerValues] + + const newField = Object.keys(table.schema).find( key => !sheet.headerValues.includes(key) ) - await sheet.setHeaderRow([...sheet.headerValues, newField]) + + if (newField) { + updatedHeaderValues.push(newField) + } + + await sheet.setHeaderRow(updatedHeaderValues) } } catch (err) { console.error("Error updating table in google sheets", err) @@ -322,7 +321,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async deleteTable(sheet: any) { try { await this.connect() - const sheetToDelete = await this.client.sheetsByTitle[sheet] + const sheetToDelete = this.client.sheetsByTitle[sheet] return await sheetToDelete.delete() } catch (err) { console.error("Error deleting table in google sheets", err) @@ -333,7 +332,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async create(query: { sheet: string; row: any }) { try { await this.connect() - const sheet = await this.client.sheetsByTitle[query.sheet] + const sheet = this.client.sheetsByTitle[query.sheet] const rowToInsert = typeof query.row === "string" ? JSON.parse(query.row) : query.row const row = await sheet.addRow(rowToInsert) @@ -349,7 +348,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async read(query: { sheet: string }) { try { await this.connect() - const sheet = await this.client.sheetsByTitle[query.sheet] + const sheet = this.client.sheetsByTitle[query.sheet] const rows = await sheet.getRows() const headerValues = sheet.headerValues const response = [] @@ -368,7 +367,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async update(query: { sheet: string; rowIndex: number; row: any }) { try { await this.connect() - const sheet = await this.client.sheetsByTitle[query.sheet] + const sheet = this.client.sheetsByTitle[query.sheet] const rows = await sheet.getRows() const row = rows[query.rowIndex] if (row) { @@ -392,7 +391,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async delete(query: { sheet: string; rowIndex: number }) { await this.connect() - const sheet = await this.client.sheetsByTitle[query.sheet] + const sheet = this.client.sheetsByTitle[query.sheet] const rows = await sheet.getRows() const row = rows[query.rowIndex] if (row) { diff --git a/packages/server/src/integrations/tests/googlesheets.spec.ts b/packages/server/src/integrations/tests/googlesheets.spec.ts new file mode 100644 index 0000000000..1e28be33c6 --- /dev/null +++ b/packages/server/src/integrations/tests/googlesheets.spec.ts @@ -0,0 +1,122 @@ +import type { GoogleSpreadsheetWorksheet } from "google-spreadsheet" + +jest.mock("google-auth-library") +const { OAuth2Client } = require("google-auth-library") + +const setCredentialsMock = jest.fn() +const getAccessTokenMock = jest.fn() + +OAuth2Client.mockImplementation(() => { + return { + setCredentials: setCredentialsMock, + getAccessToken: getAccessTokenMock, + } +}) + +jest.mock("google-spreadsheet") +const { GoogleSpreadsheet } = require("google-spreadsheet") + +const sheetsByTitle: { [title: string]: GoogleSpreadsheetWorksheet } = {} + +GoogleSpreadsheet.mockImplementation(() => { + return { + useOAuth2Client: jest.fn(), + loadInfo: jest.fn(), + sheetsByTitle, + } +}) + +import { structures } from "@budibase/backend-core/tests" +import TestConfiguration from "../../tests/utilities/TestConfiguration" +import GoogleSheetsIntegration from "../googlesheets" +import { FieldType, Table, TableSchema } from "../../../../types/src/documents" + +describe("Google Sheets Integration", () => { + let integration: any, + config = new TestConfiguration() + + beforeEach(async () => { + integration = new GoogleSheetsIntegration.integration({ + spreadsheetId: "randomId", + auth: { + appId: "appId", + accessToken: "accessToken", + refreshToken: "refreshToken", + }, + }) + await config.init() + }) + + function createBasicTable(name: string, columns: string[]): Table { + return { + name, + schema: { + ...columns.reduce((p, c) => { + p[c] = { + name: c, + type: FieldType.STRING, + constraints: { + type: "string", + }, + } + return p + }, {} as TableSchema), + }, + } + } + + function createSheet({ + headerValues, + }: { + headerValues: string[] + }): GoogleSpreadsheetWorksheet { + return { + // to ignore the unmapped fields + ...({} as any), + loadHeaderRow: jest.fn(), + headerValues, + setHeaderRow: jest.fn(), + } + } + + describe("update table", () => { + test("adding a new field will be adding a new header row", async () => { + await config.doInContext(structures.uuid(), async () => { + const tableColumns = ["name", "description", "new field"] + const table = createBasicTable(structures.uuid(), tableColumns) + + const sheet = createSheet({ headerValues: ["name", "description"] }) + sheetsByTitle[table.name] = sheet + await integration.updateTable(table) + + expect(sheet.loadHeaderRow).toBeCalledTimes(1) + expect(sheet.setHeaderRow).toBeCalledTimes(1) + expect(sheet.setHeaderRow).toBeCalledWith(tableColumns) + }) + }) + + test("removing an existing field will not remove the data from the spreadsheet", async () => { + await config.doInContext(structures.uuid(), async () => { + const tableColumns = ["name"] + const table = createBasicTable(structures.uuid(), tableColumns) + + const sheet = createSheet({ + headerValues: ["name", "description", "location"], + }) + sheetsByTitle[table.name] = sheet + await integration.updateTable(table) + + expect(sheet.loadHeaderRow).toBeCalledTimes(1) + expect(sheet.setHeaderRow).toBeCalledTimes(1) + expect(sheet.setHeaderRow).toBeCalledWith([ + "name", + "description", + "location", + ]) + + // No undefineds are sent + expect((sheet.setHeaderRow as any).mock.calls[0][0]).toHaveLength(3) + }) + }) + }) +}) diff --git a/packages/server/src/migrations/functions/backfill/global/configs.ts b/packages/server/src/migrations/functions/backfill/global/configs.ts index 7eaa987bc7..1b76727bbe 100644 --- a/packages/server/src/migrations/functions/backfill/global/configs.ts +++ b/packages/server/src/migrations/functions/backfill/global/configs.ts @@ -1,4 +1,9 @@ -import { events, db as dbUtils } from "@budibase/backend-core" +import { + events, + DocumentType, + SEPARATOR, + UNICODE_MAX, +} from "@budibase/backend-core" import { Config, isSMTPConfig, @@ -9,15 +14,16 @@ import { } from "@budibase/types" import env from "./../../../../environment" +export const getConfigParams = () => { + return { + include_docs: true, + startkey: `${DocumentType.CONFIG}${SEPARATOR}`, + endkey: `${DocumentType.CONFIG}${SEPARATOR}${UNICODE_MAX}`, + } +} + const getConfigs = async (globalDb: any): Promise => { - const response = await globalDb.allDocs( - dbUtils.getConfigParams( - {}, - { - include_docs: true, - } - ) - ) + const response = await globalDb.allDocs(getConfigParams()) return response.rows.map((row: any) => row.doc) } diff --git a/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js b/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js index c0066b1c71..15ddcfd1ef 100644 --- a/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js +++ b/packages/server/src/migrations/functions/tests/userEmailViewCasing.spec.js @@ -8,9 +8,8 @@ jest.mock("@budibase/backend-core", () => { } } }) -const { tenancy, db: dbCore } = require("@budibase/backend-core") +const { context, db: dbCore } = require("@budibase/backend-core") const TestConfig = require("../../../tests/utilities/TestConfiguration") -const { TENANT_ID } = require("../../../tests/utilities/structures") // mock email view creation @@ -26,8 +25,8 @@ describe("run", () => { afterAll(config.end) it("runs successfully", async () => { - await tenancy.doInTenant(TENANT_ID, async () => { - const globalDb = tenancy.getGlobalDB() + await config.doInTenant(async () => { + const globalDb = context.getGlobalDB() await migration.run(globalDb) expect(dbCore.createNewUserEmailView).toHaveBeenCalledTimes(1) }) diff --git a/packages/server/src/migrations/tests/helpers.ts b/packages/server/src/migrations/tests/helpers.ts index 17658d5290..35831a2fd0 100644 --- a/packages/server/src/migrations/tests/helpers.ts +++ b/packages/server/src/migrations/tests/helpers.ts @@ -1,7 +1,7 @@ // Mimic configs test configuration from worker, creation configs directly in database import * as structures from "./structures" -import { db } from "@budibase/backend-core" +import { configs } from "@budibase/backend-core" import { Config } from "@budibase/types" export const saveSettingsConfig = async (globalDb: any) => { @@ -25,7 +25,7 @@ export const saveSmtpConfig = async (globalDb: any) => { } const saveConfig = async (config: Config, globalDb: any) => { - config._id = db.generateConfigID({ type: config.type }) + config._id = configs.generateConfigID(config.type) let response try { diff --git a/packages/server/src/migrations/tests/structures.ts b/packages/server/src/migrations/tests/structures.ts index bd48bf63cd..b075c04f5c 100644 --- a/packages/server/src/migrations/tests/structures.ts +++ b/packages/server/src/migrations/tests/structures.ts @@ -20,6 +20,7 @@ export const oidc = (conf?: OIDCConfig): OIDCConfig => { name: "Active Directory", uuid: utils.newid(), activated: true, + scopes: [], ...conf, }, ], diff --git a/packages/server/src/startup.ts b/packages/server/src/startup.ts index f70694226d..6cdbf87c2c 100644 --- a/packages/server/src/startup.ts +++ b/packages/server/src/startup.ts @@ -5,7 +5,7 @@ import { generateApiKey, getChecklist, } from "./utilities/workerRequests" -import { installation, tenancy, logging } from "@budibase/backend-core" +import { installation, tenancy, logging, events } from "@budibase/backend-core" import fs from "fs" import { watch } from "./watch" import * as automations from "./automations" @@ -124,6 +124,9 @@ export async function startup(app?: any, server?: any) { // get the references to the queue promises, don't await as // they will never end, unless the processing stops let queuePromises = [] + // configure events to use the pro audit log write + // can't integrate directly into backend-core due to cyclic issues + queuePromises.push(events.processors.init(pro.sdk.auditLogs.write)) queuePromises.push(automations.init()) queuePromises.push(initPro()) if (app) { diff --git a/packages/server/src/tests/jestEnv.ts b/packages/server/src/tests/jestEnv.ts index c567b260b3..7727bb6007 100644 --- a/packages/server/src/tests/jestEnv.ts +++ b/packages/server/src/tests/jestEnv.ts @@ -8,3 +8,4 @@ process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" process.env.ENABLE_4XX_HTTP_LOGGING = "0" process.env.MOCK_REDIS = "1" +process.env.PLATFORM_URL = "http://localhost:10000" diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 88545dbcbb..e9b770229f 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -21,7 +21,6 @@ import { basicScreen, basicLayout, basicWebhook, - TENANT_ID, } from "./structures" import { constants, @@ -41,8 +40,8 @@ import { generateUserMetadataID } from "../../db/utils" import { startup } from "../../startup" import supertest from "supertest" import { + App, AuthToken, - Database, Datasource, Row, SourceName, @@ -63,7 +62,7 @@ class TestConfiguration { started: boolean appId: string | null allApps: any[] - app: any + app?: App prodApp: any prodAppId: any user: any @@ -73,7 +72,7 @@ class TestConfiguration { linkedTable: any automation: any datasource: any - tenantId: string | null + tenantId?: string defaultUserValues: DefaultUserValues constructor(openServer = true) { @@ -89,7 +88,6 @@ class TestConfiguration { } this.appId = null this.allApps = [] - this.tenantId = null this.defaultUserValues = this.populateDefaultUserValues() } @@ -154,19 +152,10 @@ class TestConfiguration { // use a new id as the name to avoid name collisions async init(appName = newid()) { - this.defaultUserValues = this.populateDefaultUserValues() - if (context.isMultiTenant()) { - this.tenantId = structures.tenant.id() - } - if (!this.started) { await startup() } - this.user = await this.globalUser() - this.globalUserId = this.user._id - this.userMetadataId = generateUserMetadataID(this.globalUserId) - - return this.createApp(appName) + return this.newTenant(appName) } end() { @@ -182,24 +171,22 @@ class TestConfiguration { } // MODES - #setMultiTenancy = (value: boolean) => { + setMultiTenancy = (value: boolean) => { env._set("MULTI_TENANCY", value) coreEnv._set("MULTI_TENANCY", value) } - #setSelfHosted = (value: boolean) => { + setSelfHosted = (value: boolean) => { env._set("SELF_HOSTED", value) coreEnv._set("SELF_HOSTED", value) } modeCloud = () => { - this.#setSelfHosted(false) - this.#setMultiTenancy(true) + this.setSelfHosted(false) } modeSelf = () => { - this.#setSelfHosted(true) - this.#setMultiTenancy(false) + this.setSelfHosted(true) } // UTILS @@ -354,6 +341,8 @@ class TestConfiguration { }) } + // HEADERS + defaultHeaders(extras = {}) { const tenantId = this.getTenantId() const authObj: AuthToken = { @@ -374,6 +363,7 @@ class TestConfiguration { `${constants.Cookie.CurrentApp}=${appToken}`, ], [constants.Header.CSRF_TOKEN]: this.defaultUserValues.csrfToken, + Host: this.tenantHost(), ...extras, } @@ -383,10 +373,6 @@ class TestConfiguration { return headers } - getTenantId() { - return this.tenantId || TENANT_ID - } - publicHeaders({ prodApp = true } = {}) { const appId = prodApp ? this.prodAppId : this.appId @@ -397,9 +383,7 @@ class TestConfiguration { headers[constants.Header.APP_ID] = appId } - if (this.tenantId) { - headers[constants.Header.TENANT_ID] = this.tenantId - } + headers[constants.Header.TENANT_ID] = this.getTenantId() return headers } @@ -413,6 +397,34 @@ class TestConfiguration { return this.login({ email, roleId, builder, prodApp }) } + // TENANCY + + tenantHost() { + const tenantId = this.getTenantId() + const platformHost = new URL(coreEnv.PLATFORM_URL).host.split(":")[0] + return `${tenantId}.${platformHost}` + } + + getTenantId() { + if (!this.tenantId) { + throw new Error("no test tenant id - init has not been called") + } + return this.tenantId + } + + async newTenant(appName = newid()): Promise { + this.defaultUserValues = this.populateDefaultUserValues() + this.tenantId = structures.tenant.id() + this.user = await this.globalUser() + this.globalUserId = this.user._id + this.userMetadataId = generateUserMetadataID(this.globalUserId) + return this.createApp(appName) + } + + doInTenant(task: any) { + return context.doInTenant(this.getTenantId(), task) + } + // API async generateApiKey(userId = this.defaultUserValues.globalUserId) { @@ -432,7 +444,7 @@ class TestConfiguration { } // APP - async createApp(appName: string) { + async createApp(appName: string): Promise { // create dev app // clear any old app this.appId = null @@ -442,7 +454,7 @@ class TestConfiguration { null, controllers.app.create ) - this.appId = this.app.appId + this.appId = this.app?.appId! }) return await context.doInAppContext(this.appId, async () => { // create production app diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index c1959dc791..e38c0a5275 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -13,8 +13,6 @@ import { const { v4: uuidv4 } = require("uuid") -export const TENANT_ID = "default" - export function basicTable() { return { name: "TestTable", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index f2dd97a6c5..465b849997 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.8": - version "2.3.18-alpha.8" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.8.tgz#a48406adf2f781471822f62d42cba3e27c660778" - integrity sha512-uP6ff/h47d/YwBJdws6sMZVNnMyoGJsLO/Wqz5JlLR1g3F5k5fViMtwx1kK4FcQ8J3JGjjfXtxWJ4wLyWucFAA== +"@budibase/backend-core@2.3.18-alpha.15": + version "2.3.18-alpha.15" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.15.tgz#62e17491c4f546ff0e772005f0a6b4c55f9d2b63" + integrity sha512-TX7LU0E3EoCxcMSZFSO/CmQdhkHYBjT5y1O/UhNMzp9xOVsFUcuFNDXFhylruHnWo7VnbiIBwYDxcXgg0Bz9ig== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.8" + "@budibase/types" "2.3.18-alpha.15" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1367,6 +1367,31 @@ svelte-flatpickr "^3.2.3" svelte-portal "^1.0.0" +"@budibase/handlebars-helpers@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841" + integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ== + dependencies: + array-sort "^1.0.0" + define-property "^2.0.2" + extend-shallow "^3.0.2" + for-in "^1.0.2" + get-object "^0.2.0" + get-value "^3.0.1" + handlebars "^4.7.7" + handlebars-utils "^1.0.6" + has-value "^2.0.2" + helper-md "^0.2.2" + html-tag "^2.0.0" + is-even "^1.0.0" + is-glob "^4.0.1" + kind-of "^6.0.3" + micromatch "^3.1.5" + relative "^3.0.2" + striptags "^3.1.1" + to-gfm-code-block "^0.1.1" + year "^0.2.1" + "@budibase/nano@10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.1.tgz#36ccda4d9bb64b5ee14dd2b27a295b40739b1038" @@ -1392,18 +1417,20 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.8": - version "2.3.18-alpha.8" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.8.tgz#24aa885d35b51123e5152094f67961edb10785b8" - integrity sha512-pozClQBcntm7SIc66CIblPBHnoWekxqWSxoulj9zbXvO/gmIc0oU7z0xT7nwKvsU9RCthXaFXDHeH+rWbyOPNg== +"@budibase/pro@2.3.18-alpha.15": + version "2.3.18-alpha.15" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.15.tgz#5237b6f9b8f8627cd46bc014b9be6c083412a370" + integrity sha512-owEtr2peoF4zpPQ+UZepfi4nYxWRKA/qZa/748qJc03T3hOdU7dXj6Nkjl7nc0PHuYdZ3NewJKK5gnr2wMb2+A== dependencies: - "@budibase/backend-core" "2.3.18-alpha.8" - "@budibase/types" "2.3.18-alpha.8" + "@budibase/backend-core" "2.3.18-alpha.15" + "@budibase/string-templates" "2.3.18-alpha.14" + "@budibase/types" "2.3.18-alpha.15" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" jsonwebtoken "8.5.1" lru-cache "^7.14.1" + memorystream "^0.3.1" node-fetch "^2.6.1" "@budibase/standard-components@^0.9.139": @@ -1424,10 +1451,22 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.18-alpha.8": - version "2.3.18-alpha.8" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.8.tgz#7822eb07b7abf2af48d6940a0ab208b8be4ab51a" - integrity sha512-M3GM35OkFgkw6ByQkSlZn/SG3/T6e/kyY/3f2mqft4dYL1Y89bPn7zKG6nB4BqTwU7ItLkUCNVtJNOBhXZCKqg== +"@budibase/string-templates@2.3.18-alpha.14": + version "2.3.18-alpha.14" + resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-2.3.18-alpha.14.tgz#c3b8d45ced321088c76bcda4efd7e9c7635a2788" + integrity sha512-xamfugDHgvzupe3EkvTY7ymXn9cRxb61nKaap52NsQQl8Zby2W2qJNVBNnuSnhnkQQeF5EatIFgGni+yBDchtQ== + dependencies: + "@budibase/handlebars-helpers" "^0.11.8" + dayjs "^1.10.4" + handlebars "^4.7.6" + handlebars-utils "^1.0.6" + lodash "^4.17.20" + vm2 "^3.9.4" + +"@budibase/types@2.3.18-alpha.15": + version "2.3.18-alpha.15" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.15.tgz#a7b4189b5dd823100f3785b5a991e428529c7e21" + integrity sha512-94+OIOpY4d+kdsXMIXU0RYB8WKQN8y8ueqJZt1OUHvSgCnb8CWIJoVv0LlHFgiW7871JPO2KS6nbpndDXeJhgA== "@bull-board/api@3.7.0": version "3.7.0" @@ -4230,7 +4269,7 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -argparse@^1.0.7: +argparse@^1.0.10, argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== @@ -4277,6 +4316,15 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha512-H3LU5RLiSsGXPhN+Nipar0iR0IofH+8r89G2y1tBKxQ/agagKyAjhkAFDRBfodP2caPrNKHpAWNIM/c9yeL7uA== +array-sort@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" + integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== + dependencies: + default-compare "^1.0.0" + get-value "^2.0.6" + kind-of "^5.0.2" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" @@ -4420,6 +4468,13 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +autolinker@~0.28.0: + version "0.28.1" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47" + integrity sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ== + dependencies: + gulp-header "^1.7.1" + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -5500,6 +5555,13 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-with-sourcemaps@*: + version "1.1.0" + resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" + integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== + dependencies: + source-map "^0.6.1" + condense-newlines@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f" @@ -5941,6 +6003,13 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" + integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== + dependencies: + kind-of "^5.0.2" + default-shell@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-1.0.1.tgz#752304bddc6174f49eb29cb988feea0b8813c8bc" @@ -6122,20 +6191,13 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -docker-compose@0.23.17: +docker-compose@0.23.17, docker-compose@^0.23.5: version "0.23.17" resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.17.tgz#8816bef82562d9417dc8c790aa4871350f93a2ba" integrity sha512-YJV18YoYIcxOdJKeFcCFihE6F4M2NExWM/d4S1ITcS9samHKnNUihz9kjggr0dNtsrbpFNc7/Yzd19DWs+m1xg== dependencies: yaml "^1.10.2" -docker-compose@^0.23.5: - version "0.23.19" - resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.19.tgz#9947726e2fe67bdfa9e8efe1ff15aa0de2e10eb8" - integrity sha512-v5vNLIdUqwj4my80wxFDkNH+4S85zsRuH29SO7dCWVWPCMt/ohZBsGN6g6KXWifT0pzQ7uOxqEKCYCDPJ8Vz4g== - dependencies: - yaml "^1.10.2" - docker-modem@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.6.tgz#8c76338641679e28ec2323abb65b3276fb1ce597" @@ -6424,6 +6486,11 @@ enhanced-resolve@^5.9.3: graceful-fs "^4.2.4" tapable "^2.2.0" +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + entities@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" @@ -6983,6 +7050,11 @@ expose-loader@^3.1.0: resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-3.1.0.tgz#7a0bdecb345b921ca238a8c4715a4ea7e227213f" integrity sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA== +express-useragent@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/express-useragent/-/express-useragent-1.0.15.tgz#cefda5fa4904345d51d3368b117a8dd4124985d9" + integrity sha512-eq5xMiYCYwFPoekffMjvEIk+NWdlQY9Y38OsTyl13IvA728vKT+q/CSERYWzcw93HGBJcIqMIsZC5CZGARPVdg== + ext-list@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" @@ -7619,6 +7691,14 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.3" +get-object@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/get-object/-/get-object-0.2.0.tgz#d92ff7d5190c64530cda0543dac63a3d47fe8c0c" + integrity sha512-7P6y6k6EzEFmO/XyUyFlXm1YLJy9xeA1x/grNV8276abX5GuwUtYgKFkRFkLixw4hf4Pz9q2vgv/8Ar42R0HuQ== + dependencies: + is-number "^2.0.2" + isobject "^0.2.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -7681,6 +7761,13 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== +get-value@^3.0.0, get-value@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" + integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA== + dependencies: + isobject "^3.0.1" + getopts@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" @@ -7968,7 +8055,24 @@ gtoken@^5.0.4: google-p12-pem "^3.1.3" jws "^4.0.0" -handlebars@^4.7.7: +gulp-header@^1.7.1: + version "1.8.12" + resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" + integrity sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ== + dependencies: + concat-with-sourcemaps "*" + lodash.template "^4.4.0" + through2 "^2.0.0" + +handlebars-utils@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/handlebars-utils/-/handlebars-utils-1.0.6.tgz#cb9db43362479054782d86ffe10f47abc76357f9" + integrity sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw== + dependencies: + kind-of "^6.0.0" + typeof-article "^0.1.1" + +handlebars@^4.7.6, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -8062,6 +8166,14 @@ has-value@^1.0.0: has-values "^1.0.0" isobject "^3.0.0" +has-value@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-2.0.2.tgz#d0f12e8780ba8e90e66ad1a21c707fdb67c25658" + integrity sha512-ybKOlcRsK2MqrM3Hmz/lQxXHZ6ejzSPzpNabKB45jb5qDgJvKPa3SdapTsTLwEb9WltgWpOmNax7i+DzNOk4TA== + dependencies: + get-value "^3.0.0" + has-values "^2.0.1" + has-values@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" @@ -8075,6 +8187,13 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +has-values@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-2.0.1.tgz#3876200ff86d8a8546a9264a952c17d5fc17579d" + integrity sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w== + dependencies: + kind-of "^6.0.2" + has-yarn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" @@ -8087,6 +8206,16 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +helper-md@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/helper-md/-/helper-md-0.2.2.tgz#c1f59d7e55bbae23362fd8a0e971607aec69d41f" + integrity sha512-49TaQzK+Ic7ZVTq4i1UZxRUJEmAilTk8hz7q4I0WNUaTclLR8ArJV5B3A1fe1xF2HtsDTr2gYKLaVTof/Lt84Q== + dependencies: + ent "^2.2.0" + extend-shallow "^2.0.1" + fs-exists-sync "^0.1.0" + remarkable "^1.6.2" + hexoid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" @@ -8121,6 +8250,14 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-tag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tag/-/html-tag-2.0.0.tgz#36c3bc8d816fd30b570d5764a497a641640c2fed" + integrity sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g== + dependencies: + is-self-closing "^1.0.1" + kind-of "^6.0.0" + http-assert@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" @@ -8597,6 +8734,13 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-even@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-even/-/is-even-1.0.0.tgz#76b5055fbad8d294a86b6a949015e1c97b717c06" + integrity sha512-LEhnkAdJqic4Dbqn58A0y52IXoHWlsueqQkKfMfdEnIYG8A1sm/GHidKkS6yvXlMoRrkM34csHnXQtOqcb+Jzg== + dependencies: + is-odd "^0.1.2" + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -8703,6 +8847,13 @@ is-number-object@^1.0.4: dependencies: has-tostringtag "^1.0.0" +is-number@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg== + dependencies: + kind-of "^3.0.2" + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -8725,6 +8876,13 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== +is-odd@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-0.1.2.tgz#bc573b5ce371ef2aad6e6f49799b72bef13978a7" + integrity sha512-Ri7C2K7o5IrUU9UEI8losXJCCD/UtsaIrkR5sxIcFg4xQ9cRJXlWA5DQvTE0yDc0krvSNLsRGXN11UPS6KyfBw== + dependencies: + is-number "^3.0.0" + is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -8765,6 +8923,13 @@ is-retry-allowed@^2.2.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== +is-self-closing@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-self-closing/-/is-self-closing-1.0.1.tgz#5f406b527c7b12610176320338af0fa3896416e4" + integrity sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg== + dependencies: + self-closing-tags "^1.0.1" + 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" @@ -8880,6 +9045,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isobject@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-0.2.0.tgz#a3432192f39b910b5f02cc989487836ec70aa85e" + integrity sha512-VaWq6XYAsbvM0wf4dyBO7WH9D7GosB7ZZlqrawI9BBiTMINBeCyqSKBa35m870MY3O4aM31pYyZi9DfGrYMJrQ== + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -10074,7 +10244,7 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.1.0, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== @@ -10088,12 +10258,12 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" -kind-of@^5.0.0: +kind-of@^5.0.0, kind-of@^5.0.2: version "5.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -10243,6 +10413,13 @@ koa-static@5.0.0, koa-static@^5.0.0: debug "^3.1.0" koa-send "^5.0.0" +koa-useragent@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-useragent/-/koa-useragent-4.1.0.tgz#d3f128b552c6da3e5e9e9e9c887b2922b16e4468" + integrity sha512-x/HUDZ1zAmNNh5hA9hHbPm9p3UVg2prlpHzxCXQCzbibrNS0kmj7MkCResCbAbG7ZT6FVxNSMjR94ZGamdMwxA== + dependencies: + express-useragent "^1.0.15" + koa-views@^7.0.1: version "7.0.2" resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.2.tgz#c96fd9e2143ef00c29dc5160c5ed639891aa723d" @@ -10513,6 +10690,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -10623,6 +10805,21 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== +lodash.template@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -10638,7 +10835,7 @@ lodash.xor@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.xor/-/lodash.xor-4.5.0.tgz#4d48ed7e98095b0632582ba714d3ff8ae8fb1db6" integrity sha512-sVN2zimthq7aZ5sPGXnSz32rZPuqcparVW50chJQe+mzTYV+IsxSsl/2gnkWWE2Of7K3myBQBqtLKOUEHJKRsQ== -lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.3: +lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -10706,7 +10903,12 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.14.0, lru-cache@^7.14.1: +lru-cache@^7.14.0: + version "7.16.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.16.1.tgz#7acea16fecd9ed11430e78443c2bb81a06d3dea9" + integrity sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w== + +lru-cache@^7.14.1: version "7.14.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== @@ -10865,7 +11067,7 @@ memory-pager@^1.0.2: resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== -memorystream@0.3.1: +memorystream@0.3.1, memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== @@ -10890,7 +11092,7 @@ methods@^1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^3.1.10, micromatch@^3.1.4: +micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.5: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -13086,6 +13288,21 @@ relative-microtime@^2.0.0: resolved "https://registry.yarnpkg.com/relative-microtime/-/relative-microtime-2.0.0.tgz#cceed2af095ecd72ea32011279c79e5fcc7de29b" integrity sha512-l18ha6HEZc+No/uK4GyAnNxgKW7nvEe35IaeN54sShMojtqik2a6GbTyuiezkjpPaqP874Z3lW5ysBo5irz4NA== +relative@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" + integrity sha512-Q5W2qeYtY9GbiR8z1yHNZ1DGhyjb4AnLEjt8iE6XfcC1QIu+FAtj3HQaO0wH28H1mX6cqNLvAqWhP402dxJGyA== + dependencies: + isobject "^2.0.0" + +remarkable@^1.6.2: + version "1.7.4" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00" + integrity sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg== + dependencies: + argparse "^1.0.10" + autolinker "~0.28.0" + 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" @@ -13434,6 +13651,11 @@ seek-bzip@^1.0.5: dependencies: commander "^2.8.1" +self-closing-tags@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/self-closing-tags/-/self-closing-tags-1.0.1.tgz#6c5fa497994bb826b484216916371accee490a5d" + integrity sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -14222,6 +14444,11 @@ strip-outer@^1.0.0: dependencies: escape-string-regexp "^1.0.2" +striptags@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052" + integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw== + style-loader@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" @@ -14706,6 +14933,11 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +to-gfm-code-block@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz#25d045a5fae553189e9637b590900da732d8aa82" + integrity sha512-LQRZWyn8d5amUKnfR9A9Uu7x9ss7Re8peuWR2gkh1E+ildOfv2aF26JpuDg8JtvCduu5+hOrMIH+XstZtnagqg== + to-json-schema@0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/to-json-schema/-/to-json-schema-0.2.5.tgz#ef3c3f11ad64460dcfbdbafd0fd525d69d62a98f" @@ -14974,6 +15206,13 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typeof-article@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/typeof-article/-/typeof-article-0.1.1.tgz#9f07e733c3fbb646ffa9e61c08debacd460e06af" + integrity sha512-Vn42zdX3FhmUrzEmitX3iYyLb+Umwpmv8fkZRIknYh84lmdrwqZA5xYaoKiIj2Rc5i/5wcDrpUmZcbk1U51vTw== + dependencies: + kind-of "^3.1.0" + typeof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typeof/-/typeof-1.0.0.tgz#9c84403f2323ae5399167275497638ea1d2f2440" @@ -15334,6 +15573,14 @@ vm2@3.9.11: acorn "^8.7.0" acorn-walk "^8.2.0" +vm2@^3.9.4: + version "3.9.14" + resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.14.tgz#964042b474cf1e6e4f475a39144773cdb9deb734" + integrity sha512-HgvPHYHeQy8+QhzlFryvSteA4uQLBCOub02mgqdR+0bN/akRZ48TGB1v0aCv7ksyc0HXx16AZtMHKS38alc6TA== + dependencies: + acorn "^8.7.0" + acorn-walk "^8.2.0" + vuvuzela@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/vuvuzela/-/vuvuzela-1.0.3.tgz#3be145e58271c73ca55279dd851f12a682114b0b" @@ -15895,6 +16142,11 @@ yauzl@^2.4.2: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" +year@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/year/-/year-0.2.1.tgz#4083ae520a318b23ec86037f3000cb892bdf9bb0" + integrity sha512-9GnJUZ0QM4OgXuOzsKNzTJ5EOkums1Xc+3YQXp+Q+UxFjf7zLucp9dQ8QMIft0Szs1E1hUiXFim1OYfEKFq97w== + ylru@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785" diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index bdf8258cc0..fe03c45bdd 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "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 c340fc9851..19bb756723 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/types/src/api/web/global/auditLogs.ts b/packages/types/src/api/web/global/auditLogs.ts new file mode 100644 index 0000000000..8ce0e742d2 --- /dev/null +++ b/packages/types/src/api/web/global/auditLogs.ts @@ -0,0 +1,44 @@ +import { Event, AuditedEventFriendlyName } from "../../../sdk" +import { + PaginationResponse, + PaginationRequest, + BasicPaginationRequest, +} from "../" +import { User, App } from "../../../" + +export interface AuditLogSearchParams { + userIds?: string[] + appIds?: string[] + events?: Event[] + startDate?: string + endDate?: string + fullSearch?: string + bookmark?: string +} + +export interface DownloadAuditLogsRequest extends AuditLogSearchParams {} + +export interface SearchAuditLogsRequest + extends BasicPaginationRequest, + AuditLogSearchParams {} + +export enum AuditLogResourceStatus { + DELETED = "deleted", +} + +export interface AuditLogEnriched { + app?: App | { _id: string; status: AuditLogResourceStatus } + user: User | { _id: string; status: AuditLogResourceStatus } + event: Event + timestamp: string + name: string + metadata: any +} + +export interface SearchAuditLogsResponse extends PaginationResponse { + data: AuditLogEnriched[] +} + +export interface DefinitionsAuditLogsResponse { + events: Record +} diff --git a/packages/types/src/api/web/global/configs.ts b/packages/types/src/api/web/global/configs.ts new file mode 100644 index 0000000000..1476d13cee --- /dev/null +++ b/packages/types/src/api/web/global/configs.ts @@ -0,0 +1,23 @@ +import { SettingsConfig, SettingsInnerConfig } from "../../../documents" + +/** + * Settings that aren't stored in the database - enriched at runtime. + */ +export interface PublicSettingsInnerConfig extends SettingsInnerConfig { + google: boolean + oidc: boolean + oidcCallbackUrl: string + googleCallbackUrl: string +} + +export interface GetPublicSettingsResponse extends SettingsConfig { + config: PublicSettingsInnerConfig +} + +export interface PublicOIDCConfig { + logo?: string + name?: string + uuid?: string +} + +export type GetPublicOIDCConfigResponse = PublicOIDCConfig[] diff --git a/packages/types/src/api/web/global/index.ts b/packages/types/src/api/web/global/index.ts index 415ed55ab1..21a5de3727 100644 --- a/packages/types/src/api/web/global/index.ts +++ b/packages/types/src/api/web/global/index.ts @@ -1,2 +1,4 @@ export * from "./environmentVariables" +export * from "./auditLogs" export * from "./events" +export * from "./configs" diff --git a/packages/types/src/api/web/index.ts b/packages/types/src/api/web/index.ts index 8ed5b0aad4..141119c837 100644 --- a/packages/types/src/api/web/index.ts +++ b/packages/types/src/api/web/index.ts @@ -5,3 +5,4 @@ export * from "./errors" export * from "./schedule" export * from "./app" export * from "./global" +export * from "./pagination" diff --git a/packages/types/src/api/web/pagination.ts b/packages/types/src/api/web/pagination.ts new file mode 100644 index 0000000000..ae4c56971a --- /dev/null +++ b/packages/types/src/api/web/pagination.ts @@ -0,0 +1,27 @@ +export enum SortOrder { + ASCENDING = "ascending", + DESCENDING = "descending", +} + +export enum SortType { + STRING = "string", + number = "number", +} + +export interface BasicPaginationRequest { + bookmark?: string +} + +export interface PaginationRequest extends BasicPaginationRequest { + limit?: number + sort?: { + order: SortOrder + column: string + type: SortType + } +} + +export interface PaginationResponse { + bookmark: string + hasNextPage: boolean +} diff --git a/packages/types/src/documents/global/auditLogs.ts b/packages/types/src/documents/global/auditLogs.ts new file mode 100644 index 0000000000..7902997b39 --- /dev/null +++ b/packages/types/src/documents/global/auditLogs.ts @@ -0,0 +1,13 @@ +import { Document } from "../document" +import { Event } from "../../sdk" + +export const AuditLogSystemUser = "SYSTEM" + +export interface AuditLogDoc extends Document { + appId?: string + event: Event + userId: string + timestamp: string + metadata: any + name: string +} diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts index 99dec534b6..f4bba21e0f 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -5,32 +5,45 @@ export interface Config extends Document { config: any } -export interface SMTPConfig extends Config { - config: { - port: number - host: string - from: string - subject: string - secure: boolean +export interface SMTPInnerConfig { + port: number + host: string + from: string + subject?: string + secure: boolean + auth?: { + user: string + pass: string } + connectionTimeout?: any +} + +export interface SMTPConfig extends Config { + config: SMTPInnerConfig +} + +export interface SettingsInnerConfig { + platformUrl?: string + company?: string + logoUrl?: string // Populated on read + logoUrlEtag?: string + uniqueTenantId?: string + analyticsEnabled?: boolean + isSSOEnforced?: boolean } export interface SettingsConfig extends Config { - config: { - company: string - // Populated on read - logoUrl?: string - logoUrlEtag?: boolean - platformUrl: string - uniqueTenantId?: string - analyticsEnabled?: boolean - } + config: SettingsInnerConfig } export interface GoogleInnerConfig { clientID: string clientSecret: string activated: boolean + /** + * @deprecated read only + */ + callbackURL?: string } export interface GoogleConfig extends Config { @@ -55,6 +68,7 @@ export interface OIDCInnerConfig { name: string uuid: string activated: boolean + scopes: string[] } export interface OIDCConfig extends Config { diff --git a/packages/types/src/documents/global/index.ts b/packages/types/src/documents/global/index.ts index 11ce7513f2..b728439dd6 100644 --- a/packages/types/src/documents/global/index.ts +++ b/packages/types/src/documents/global/index.ts @@ -6,3 +6,4 @@ export * from "./quotas" export * from "./schedule" export * from "./templates" export * from "./environmentVariables" +export * from "./auditLogs" diff --git a/packages/types/src/sdk/auditLogs.ts b/packages/types/src/sdk/auditLogs.ts new file mode 100644 index 0000000000..0322d2e862 --- /dev/null +++ b/packages/types/src/sdk/auditLogs.ts @@ -0,0 +1,21 @@ +import { Event, HostInfo } from "./events" +import { AuditLogDoc } from "../documents" + +export type AuditWriteOpts = { + appId?: string + timestamp?: string | number + userId?: string + hostInfo?: HostInfo +} + +export type AuditLogFn = ( + event: Event, + metadata: any, + opts: AuditWriteOpts +) => Promise + +export type AuditLogQueueEvent = { + event: Event + properties: any + opts: AuditWriteOpts +} diff --git a/packages/types/src/sdk/context.ts b/packages/types/src/sdk/context.ts index b3403df8af..c8345de196 100644 --- a/packages/types/src/sdk/context.ts +++ b/packages/types/src/sdk/context.ts @@ -1,5 +1,5 @@ import { User, Account } from "../documents" -import { IdentityType } from "./events" +import { IdentityType, HostInfo } from "./events" export interface BaseContext { _id: string @@ -16,6 +16,7 @@ export interface UserContext extends BaseContext, User { _id: string tenantId: string account?: Account + hostInfo: HostInfo } export type IdentityContext = BaseContext | AccountUserContext | UserContext diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index 35d198ccb2..6e213b5831 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -1,5 +1,11 @@ import Nano from "@budibase/nano" import { AllDocsResponse, AnyDocument, Document } from "../" +import { Writable } from "stream" + +export enum SearchIndex { + ROWS = "rows", + AUDIT = "audit", +} export type PouchOptions = { inMemory?: boolean @@ -63,6 +69,18 @@ export const isDocument = (doc: any): doc is Document => { return typeof doc === "object" && doc._id && doc._rev } +export interface DatabaseDumpOpts { + filter?: (doc: AnyDocument) => boolean + batch_size?: number + batch_limit?: number + style?: "main_only" | "all_docs" + timeout?: number + doc_ids?: string[] + query_params?: any + view?: string + selector?: any +} + export interface Database { name: string @@ -87,7 +105,7 @@ export interface Database { compact(): Promise // these are all PouchDB related functions that are rarely used - in future // should be replaced by better typed/non-pouch implemented methods - dump(...args: any[]): Promise + dump(stream: Writable, opts?: DatabaseDumpOpts): Promise load(...args: any[]): Promise createIndex(...args: any[]): Promise deleteIndex(...args: any[]): Promise diff --git a/packages/types/src/sdk/events/app.ts b/packages/types/src/sdk/events/app.ts index 73d491070f..602dd8b6a5 100644 --- a/packages/types/src/sdk/events/app.ts +++ b/packages/types/src/sdk/events/app.ts @@ -3,50 +3,83 @@ import { BaseEvent } from "./event" export interface AppCreatedEvent extends BaseEvent { appId: string version: string + audited: { + name: string + } } export interface AppUpdatedEvent extends BaseEvent { appId: string version: string + audited: { + name: string + } } export interface AppDeletedEvent extends BaseEvent { appId: string + audited: { + name: string + } } export interface AppPublishedEvent extends BaseEvent { appId: string + audited: { + name: string + } } export interface AppUnpublishedEvent extends BaseEvent { appId: string + audited: { + name: string + } } export interface AppFileImportedEvent extends BaseEvent { appId: string + audited: { + name: string + } } export interface AppTemplateImportedEvent extends BaseEvent { appId: string templateKey: string + audited: { + name: string + } } export interface AppVersionUpdatedEvent extends BaseEvent { appId: string currentVersion: string updatedToVersion: string + audited: { + name: string + } } export interface AppVersionRevertedEvent extends BaseEvent { appId: string currentVersion: string revertedToVersion: string + audited: { + name: string + } } export interface AppRevertedEvent extends BaseEvent { appId: string + audited: { + name: string + } } export interface AppExportedEvent extends BaseEvent { appId: string + audited: { + name: string + } } diff --git a/packages/types/src/sdk/events/auditLog.ts b/packages/types/src/sdk/events/auditLog.ts new file mode 100644 index 0000000000..5f3edfb826 --- /dev/null +++ b/packages/types/src/sdk/events/auditLog.ts @@ -0,0 +1,10 @@ +import { BaseEvent } from "./event" +import { AuditLogSearchParams } from "../../api" + +export interface AuditLogFilteredEvent extends BaseEvent { + filters: AuditLogSearchParams +} + +export interface AuditLogDownloadedEvent extends BaseEvent { + filters: AuditLogSearchParams +} diff --git a/packages/types/src/sdk/events/auth.ts b/packages/types/src/sdk/events/auth.ts index eb9f3148a3..c7171d923d 100644 --- a/packages/types/src/sdk/events/auth.ts +++ b/packages/types/src/sdk/events/auth.ts @@ -7,10 +7,16 @@ export type SSOType = ConfigType.OIDC | ConfigType.GOOGLE export interface LoginEvent extends BaseEvent { userId: string source: LoginSource + audited: { + email: string + } } export interface LogoutEvent extends BaseEvent { userId: string + audited: { + email?: string + } } export interface SSOCreatedEvent extends BaseEvent { diff --git a/packages/types/src/sdk/events/automation.ts b/packages/types/src/sdk/events/automation.ts index beabdbb9eb..a3f4d15186 100644 --- a/packages/types/src/sdk/events/automation.ts +++ b/packages/types/src/sdk/events/automation.ts @@ -5,6 +5,9 @@ export interface AutomationCreatedEvent extends BaseEvent { automationId: string triggerId: string triggerType: string + audited: { + name: string + } } export interface AutomationTriggerUpdatedEvent extends BaseEvent { @@ -19,6 +22,9 @@ export interface AutomationDeletedEvent extends BaseEvent { automationId: string triggerId: string triggerType: string + audited: { + name: string + } } export interface AutomationTestedEvent extends BaseEvent { @@ -35,6 +41,9 @@ export interface AutomationStepCreatedEvent extends BaseEvent { triggerType: string stepId: string stepType: string + audited: { + name: string + } } export interface AutomationStepDeletedEvent extends BaseEvent { @@ -44,6 +53,9 @@ export interface AutomationStepDeletedEvent extends BaseEvent { triggerType: string stepId: string stepType: string + audited: { + name: string + } } export interface AutomationsRunEvent extends BaseEvent { diff --git a/packages/types/src/sdk/events/backup.ts b/packages/types/src/sdk/events/backup.ts index 1dddc109cc..23863cf8ac 100644 --- a/packages/types/src/sdk/events/backup.ts +++ b/packages/types/src/sdk/events/backup.ts @@ -5,6 +5,7 @@ export interface AppBackupRestoreEvent extends BaseEvent { appId: string restoreId: string backupCreatedAt: string + name: string } export interface AppBackupTriggeredEvent extends BaseEvent { @@ -12,4 +13,5 @@ export interface AppBackupTriggeredEvent extends BaseEvent { appId: string trigger: AppBackupTrigger type: AppBackupType + name: string } diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index f509682add..3d0d3122b5 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -180,6 +180,190 @@ export enum Event { ENVIRONMENT_VARIABLE_CREATED = "environment_variable:created", ENVIRONMENT_VARIABLE_DELETED = "environment_variable:deleted", ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED = "environment_variable:upgrade_panel_opened", + + // AUDIT LOG + AUDIT_LOGS_FILTERED = "audit_log:filtered", + AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded", +} + +// all events that are not audited have been added to this record as undefined, this means +// that Typescript can protect us against new events being added and auditing of those +// events not being considered. This might be a little ugly, but provides a level of +// Typescript build protection for the audit log feature, any new event also needs to be +// added to this map, during which the developer will need to consider if it should be +// a user facing event or not. +export const AuditedEventFriendlyName: Record = { + // USER + [Event.USER_CREATED]: `User "{{ email }}" created`, + [Event.USER_UPDATED]: `User "{{ email }}" updated`, + [Event.USER_DELETED]: `User "{{ email }}" deleted`, + [Event.USER_PERMISSION_ADMIN_ASSIGNED]: `User "{{ email }}" admin role assigned`, + [Event.USER_PERMISSION_ADMIN_REMOVED]: `User "{{ email }}" admin role removed`, + [Event.USER_PERMISSION_BUILDER_ASSIGNED]: `User "{{ email }}" builder role assigned`, + [Event.USER_PERMISSION_BUILDER_REMOVED]: `User "{{ email }}" builder role removed`, + [Event.USER_INVITED]: `User "{{ email }}" invited`, + [Event.USER_INVITED_ACCEPTED]: `User "{{ email }}" accepted invite`, + [Event.USER_PASSWORD_UPDATED]: `User "{{ email }}" password updated`, + [Event.USER_PASSWORD_RESET_REQUESTED]: `User "{{ email }}" password reset requested`, + [Event.USER_PASSWORD_RESET]: `User "{{ email }}" password reset`, + [Event.USER_GROUP_CREATED]: `User group "{{ name }}" created`, + [Event.USER_GROUP_UPDATED]: `User group "{{ name }}" updated`, + [Event.USER_GROUP_DELETED]: `User group "{{ name }}" deleted`, + [Event.USER_GROUP_USERS_ADDED]: `User group "{{ name }}" {{ count }} users added`, + [Event.USER_GROUP_USERS_REMOVED]: `User group "{{ name }}" {{ count }} users removed`, + [Event.USER_GROUP_PERMISSIONS_EDITED]: `User group "{{ name }}" permissions edited`, + [Event.USER_PASSWORD_FORCE_RESET]: undefined, + [Event.USER_GROUP_ONBOARDING]: undefined, + [Event.USER_ONBOARDING_COMPLETE]: undefined, + + // EMAIL + [Event.EMAIL_SMTP_CREATED]: `Email configuration created`, + [Event.EMAIL_SMTP_UPDATED]: `Email configuration updated`, + + // AUTH + [Event.AUTH_SSO_CREATED]: `SSO configuration created`, + [Event.AUTH_SSO_UPDATED]: `SSO configuration updated`, + [Event.AUTH_SSO_ACTIVATED]: `SSO configuration activated`, + [Event.AUTH_SSO_DEACTIVATED]: `SSO configuration deactivated`, + [Event.AUTH_LOGIN]: `User "{{ email }}" logged in`, + [Event.AUTH_LOGOUT]: `User "{{ email }}" logged out`, + + // ORG + [Event.ORG_NAME_UPDATED]: `Organisation name updated`, + [Event.ORG_LOGO_UPDATED]: `Organisation logo updated`, + [Event.ORG_PLATFORM_URL_UPDATED]: `Organisation platform URL updated`, + + // APP + [Event.APP_CREATED]: `App "{{ name }}" created`, + [Event.APP_UPDATED]: `App "{{ name }}" updated`, + [Event.APP_DELETED]: `App "{{ name }}" deleted`, + [Event.APP_PUBLISHED]: `App "{{ name }}" published`, + [Event.APP_UNPUBLISHED]: `App "{{ name }}" unpublished`, + [Event.APP_TEMPLATE_IMPORTED]: `App "{{ name }}" template imported`, + [Event.APP_FILE_IMPORTED]: `App "{{ name }}" file imported`, + [Event.APP_VERSION_UPDATED]: `App "{{ name }}" version updated`, + [Event.APP_VERSION_REVERTED]: `App "{{ name }}" version reverted`, + [Event.APP_REVERTED]: `App "{{ name }}" reverted`, + [Event.APP_EXPORTED]: `App "{{ name }}" exported`, + [Event.APP_BACKUP_RESTORED]: `App backup "{{ name }}" restored`, + [Event.APP_BACKUP_TRIGGERED]: `App backup "{{ name }}" triggered`, + + // DATASOURCE + [Event.DATASOURCE_CREATED]: `Datasource created`, + [Event.DATASOURCE_UPDATED]: `Datasource updated`, + [Event.DATASOURCE_DELETED]: `Datasource deleted`, + + // QUERY + [Event.QUERY_CREATED]: `Query created`, + [Event.QUERY_UPDATED]: `Query updated`, + [Event.QUERY_DELETED]: `Query deleted`, + [Event.QUERY_IMPORT]: `Query import`, + [Event.QUERIES_RUN]: undefined, + [Event.QUERY_PREVIEWED]: undefined, + + // TABLE + [Event.TABLE_CREATED]: `Table "{{ name }}" created`, + [Event.TABLE_UPDATED]: `Table "{{ name }}" updated`, + [Event.TABLE_DELETED]: `Table "{{ name }}" deleted`, + [Event.TABLE_EXPORTED]: `Table "{{ name }}" exported`, + [Event.TABLE_IMPORTED]: `Table "{{ name }}" imported`, + [Event.TABLE_DATA_IMPORTED]: `Data imported to table`, + + // ROWS + [Event.ROWS_CREATED]: `Rows created`, + [Event.ROWS_IMPORTED]: `Rows imported`, + + // AUTOMATION + [Event.AUTOMATION_CREATED]: `Automation "{{ name }}" created`, + [Event.AUTOMATION_DELETED]: `Automation "{{ name }}" deleted`, + [Event.AUTOMATION_STEP_CREATED]: `Automation "{{ name }}" step added`, + [Event.AUTOMATION_STEP_DELETED]: `Automation "{{ name }}" step removed`, + [Event.AUTOMATION_TESTED]: undefined, + [Event.AUTOMATIONS_RUN]: undefined, + [Event.AUTOMATION_TRIGGER_UPDATED]: undefined, + + // SCREEN + [Event.SCREEN_CREATED]: `Screen "{{ name }}" created`, + [Event.SCREEN_DELETED]: `Screen "{{ name }}" deleted`, + + // COMPONENT + [Event.COMPONENT_CREATED]: `Component created`, + [Event.COMPONENT_DELETED]: `Component deleted`, + + // ENVIRONMENT VARIABLE + [Event.ENVIRONMENT_VARIABLE_CREATED]: `Environment variable created`, + [Event.ENVIRONMENT_VARIABLE_DELETED]: `Environment variable deleted`, + [Event.ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED]: undefined, + + // PLUGIN + [Event.PLUGIN_IMPORTED]: `Plugin imported`, + [Event.PLUGIN_DELETED]: `Plugin deleted`, + [Event.PLUGIN_INIT]: undefined, + + // ROLE - NOT AUDITED + [Event.ROLE_CREATED]: undefined, + [Event.ROLE_UPDATED]: undefined, + [Event.ROLE_DELETED]: undefined, + [Event.ROLE_ASSIGNED]: undefined, + [Event.ROLE_UNASSIGNED]: undefined, + + // LICENSE - NOT AUDITED + [Event.LICENSE_PLAN_CHANGED]: undefined, + [Event.LICENSE_TIER_CHANGED]: undefined, + [Event.LICENSE_ACTIVATED]: undefined, + [Event.LICENSE_PAYMENT_FAILED]: undefined, + [Event.LICENSE_PAYMENT_RECOVERED]: undefined, + [Event.LICENSE_CHECKOUT_OPENED]: undefined, + [Event.LICENSE_CHECKOUT_SUCCESS]: undefined, + [Event.LICENSE_PORTAL_OPENED]: undefined, + + // ACCOUNT - NOT AUDITED + [Event.ACCOUNT_CREATED]: undefined, + [Event.ACCOUNT_DELETED]: undefined, + [Event.ACCOUNT_VERIFIED]: undefined, + + // BACKFILL - NOT AUDITED + [Event.APP_BACKFILL_SUCCEEDED]: undefined, + [Event.APP_BACKFILL_FAILED]: undefined, + [Event.TENANT_BACKFILL_SUCCEEDED]: undefined, + [Event.TENANT_BACKFILL_FAILED]: undefined, + [Event.INSTALLATION_BACKFILL_SUCCEEDED]: undefined, + [Event.INSTALLATION_BACKFILL_FAILED]: undefined, + + // LAYOUT - NOT AUDITED + [Event.LAYOUT_CREATED]: undefined, + [Event.LAYOUT_DELETED]: undefined, + + // VIEW - NOT AUDITED + [Event.VIEW_CREATED]: undefined, + [Event.VIEW_UPDATED]: undefined, + [Event.VIEW_DELETED]: undefined, + [Event.VIEW_EXPORTED]: undefined, + [Event.VIEW_FILTER_CREATED]: undefined, + [Event.VIEW_FILTER_UPDATED]: undefined, + [Event.VIEW_FILTER_DELETED]: undefined, + [Event.VIEW_CALCULATION_CREATED]: undefined, + [Event.VIEW_CALCULATION_UPDATED]: undefined, + [Event.VIEW_CALCULATION_DELETED]: undefined, + + // SERVED - NOT AUDITED + [Event.SERVED_BUILDER]: undefined, + [Event.SERVED_APP]: undefined, + [Event.SERVED_APP_PREVIEW]: undefined, + + // ANALYTICS - NOT AUDITED + [Event.ANALYTICS_OPT_OUT]: undefined, + [Event.ANALYTICS_OPT_IN]: undefined, + + // INSTALLATION - NOT AUDITED + [Event.INSTALLATION_VERSION_CHECKED]: undefined, + [Event.INSTALLATION_VERSION_UPGRADED]: undefined, + [Event.INSTALLATION_VERSION_DOWNGRADED]: undefined, + [Event.INSTALLATION_FIRST_STARTUP]: undefined, + + // AUDIT LOG - NOT AUDITED + [Event.AUDIT_LOGS_FILTERED]: undefined, + [Event.AUDIT_LOGS_DOWNLOADED]: undefined, } // properties added at the final stage of the event pipeline @@ -191,6 +375,11 @@ export interface BaseEvent { installationId?: string tenantId?: string hosting?: Hosting + // any props in the audited section will be removed before passing events + // up out of system (purely for use with auditing) + audited?: { + [key: string]: any + } } export type TableExportFormat = "json" | "csv" diff --git a/packages/types/src/sdk/events/identification.ts b/packages/types/src/sdk/events/identification.ts index 3f4e7ec9d4..627254882e 100644 --- a/packages/types/src/sdk/events/identification.ts +++ b/packages/types/src/sdk/events/identification.ts @@ -34,6 +34,11 @@ export enum IdentityType { INSTALLATION = "installation", } +export interface HostInfo { + ipAddress?: string + userAgent?: string +} + export interface Identity { id: string type: IdentityType @@ -41,6 +46,7 @@ export interface Identity { environment: string installationId?: string tenantId?: string + hostInfo?: HostInfo } export interface UserIdentity extends Identity { diff --git a/packages/types/src/sdk/events/index.ts b/packages/types/src/sdk/events/index.ts index 009d9beac4..745f84d2a3 100644 --- a/packages/types/src/sdk/events/index.ts +++ b/packages/types/src/sdk/events/index.ts @@ -22,3 +22,4 @@ export * from "./userGroup" export * from "./plugin" export * from "./backup" export * from "./environmentVariable" +export * from "./auditLog" diff --git a/packages/types/src/sdk/events/screen.ts b/packages/types/src/sdk/events/screen.ts index 23c468b7ad..e6b1d4d360 100644 --- a/packages/types/src/sdk/events/screen.ts +++ b/packages/types/src/sdk/events/screen.ts @@ -4,10 +4,16 @@ export interface ScreenCreatedEvent extends BaseEvent { screenId: string layoutId?: string roleId: string + audited: { + name: string + } } export interface ScreenDeletedEvent extends BaseEvent { screenId: string layoutId?: string roleId: string + audited: { + name: string + } } diff --git a/packages/types/src/sdk/events/table.ts b/packages/types/src/sdk/events/table.ts index da3c7edf47..8df2a95796 100644 --- a/packages/types/src/sdk/events/table.ts +++ b/packages/types/src/sdk/events/table.ts @@ -2,21 +2,36 @@ import { BaseEvent, TableExportFormat } from "./event" export interface TableCreatedEvent extends BaseEvent { tableId: string + audited: { + name: string + } } export interface TableUpdatedEvent extends BaseEvent { tableId: string + audited: { + name: string + } } export interface TableDeletedEvent extends BaseEvent { tableId: string + audited: { + name: string + } } export interface TableExportedEvent extends BaseEvent { tableId: string format: TableExportFormat + audited: { + name: string + } } export interface TableImportedEvent extends BaseEvent { tableId: string + audited: { + name: string + } } diff --git a/packages/types/src/sdk/events/user.ts b/packages/types/src/sdk/events/user.ts index 3f8f72801c..ab4b4d9724 100644 --- a/packages/types/src/sdk/events/user.ts +++ b/packages/types/src/sdk/events/user.ts @@ -2,47 +2,84 @@ import { BaseEvent } from "./event" export interface UserCreatedEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserUpdatedEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserDeletedEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserOnboardingEvent extends BaseEvent { userId: string step?: string + audited: { + email: string + } } export interface UserPermissionAssignedEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserPermissionRemovedEvent extends BaseEvent { userId: string + audited: { + email: string + } } -export interface UserInvitedEvent extends BaseEvent {} +export interface UserInvitedEvent extends BaseEvent { + audited: { + email: string + } +} export interface UserInviteAcceptedEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserPasswordForceResetEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserPasswordUpdatedEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserPasswordResetRequestedEvent extends BaseEvent { userId: string + audited: { + email: string + } } export interface UserPasswordResetEvent extends BaseEvent { userId: string + audited: { + email: string + } } diff --git a/packages/types/src/sdk/events/userGroup.ts b/packages/types/src/sdk/events/userGroup.ts index 2ce642e274..d82ab70b4c 100644 --- a/packages/types/src/sdk/events/userGroup.ts +++ b/packages/types/src/sdk/events/userGroup.ts @@ -2,27 +2,50 @@ import { BaseEvent } from "./event" export interface GroupCreatedEvent extends BaseEvent { groupId: string + audited: { + name: string + } } export interface GroupUpdatedEvent extends BaseEvent { groupId: string + audited: { + name: string + } } export interface GroupDeletedEvent extends BaseEvent { groupId: string + audited: { + name: string + } } export interface GroupUsersAddedEvent extends BaseEvent { count: number groupId: string + audited: { + name: string + } } export interface GroupUsersDeletedEvent extends BaseEvent { count: number groupId: string + audited: { + name: string + } } export interface GroupAddedOnboardingEvent extends BaseEvent { groupId: string onboarding: boolean } + +export interface GroupPermissionsEditedEvent extends BaseEvent { + permissions: Record + groupId: string + audited: { + name: string + } +} diff --git a/packages/types/src/sdk/index.ts b/packages/types/src/sdk/index.ts index be12d45527..ccd59bf720 100644 --- a/packages/types/src/sdk/index.ts +++ b/packages/types/src/sdk/index.ts @@ -12,5 +12,6 @@ export * from "./db" export * from "./middleware" export * from "./featureFlag" export * from "./environmentVariables" +export * from "./auditLogs" export * from "./sso" export * from "./user" diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index a39bcab18b..9f6090623b 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -2,4 +2,6 @@ export enum Feature { USER_GROUPS = "userGroups", APP_BACKUPS = "appBackups", ENVIRONMENT_VARIABLES = "environmentVariables", + AUDIT_LOGS = "auditLogs", + ENFORCEABLE_SSO = "enforceableSSO", } diff --git a/packages/worker/jest.config.ts b/packages/worker/jest.config.ts index cdacfa411a..3655479d82 100644 --- a/packages/worker/jest.config.ts +++ b/packages/worker/jest.config.ts @@ -7,7 +7,7 @@ const config: Config.InitialOptions = { preset: "@trendyol/jest-testcontainers", setupFiles: ["./src/tests/jestEnv.ts"], setupFilesAfterEnv: ["./src/tests/jestSetup.ts"], - collectCoverageFrom: ["src/**/*.{js,ts}"], + collectCoverageFrom: ["src/**/*.{js,ts}", "../backend-core/src/**/*.{js,ts}"], coverageReporters: ["lcov", "json", "clover"], transform: { "^.+\\.ts?$": "@swc/jest", diff --git a/packages/worker/package.json b/packages/worker/package.json index 8800695a31..28c1566697 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.18-alpha.8", + "version": "2.3.18-alpha.15", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.8", - "@budibase/pro": "2.3.18-alpha.8", - "@budibase/string-templates": "2.3.18-alpha.8", - "@budibase/types": "2.3.18-alpha.8", + "@budibase/backend-core": "2.3.18-alpha.15", + "@budibase/pro": "2.3.18-alpha.15", + "@budibase/string-templates": "2.3.18-alpha.15", + "@budibase/types": "2.3.18-alpha.15", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", @@ -60,6 +60,7 @@ "koa-send": "5.0.1", "koa-session": "5.13.1", "koa-static": "5.0.0", + "koa-useragent": "^4.1.0", "node-fetch": "2.6.7", "nodemailer": "6.7.2", "passport-google-oauth": "2.0.0", diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 948a98cf3a..92cf014a48 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -2,10 +2,9 @@ import { auth as authCore, constants, context, - db as dbCore, events, - tenancy, utils as utilsCore, + configs, } from "@budibase/backend-core" import { ConfigType, @@ -15,6 +14,7 @@ import { SSOUser, PasswordResetRequest, PasswordResetUpdateRequest, + GoogleInnerConfig, } from "@budibase/types" import env from "../../../environment" @@ -61,16 +61,16 @@ export const login = async (ctx: Ctx, next: any) => { const email = ctx.request.body.username const user = await userSdk.getUserByEmail(email) - if (user && (await userSdk.isPreventSSOPasswords(user))) { - ctx.throw(400, "SSO user cannot login using password") + if (user && (await userSdk.isPreventPasswordActions(user))) { + ctx.throw(400, "Password login is disabled for this user") } return passport.authenticate( "local", async (err: any, user: User, info: any) => { await passportCallback(ctx, user, err, info) - await context.identity.doInUserContext(user, async () => { - await events.auth.login("local") + await context.identity.doInUserContext(user, ctx, async () => { + await events.auth.login("local", user.email) }) ctx.status = 200 } @@ -163,8 +163,8 @@ export const datasourceAuth = async (ctx: any, next: any) => { // GOOGLE SSO -export async function googleCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE) +export async function googleCallbackUrl(config?: GoogleInnerConfig) { + return ssoCallbackUrl(ConfigType.GOOGLE, config) } /** @@ -172,12 +172,10 @@ export async function googleCallbackUrl(config?: { callbackURL?: string }) { * On a successful login, you will be redirected to the googleAuth callback route. */ export const googlePreAuth = async (ctx: any, next: any) => { - const db = tenancy.getGlobalDB() - - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.GOOGLE, - workspace: ctx.query.workspace, - }) + const config = await configs.getGoogleConfig() + if (!config) { + return ctx.throw(400, "Google config not found") + } let callbackUrl = await googleCallbackUrl(config) const strategy = await google.strategyFactory( config, @@ -193,12 +191,10 @@ export const googlePreAuth = async (ctx: any, next: any) => { } export const googleCallback = async (ctx: any, next: any) => { - const db = tenancy.getGlobalDB() - - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.GOOGLE, - workspace: ctx.query.workspace, - }) + const config = await configs.getGoogleConfig() + if (!config) { + return ctx.throw(400, "Google config not found") + } const callbackUrl = await googleCallbackUrl(config) const strategy = await google.strategyFactory( config, @@ -211,8 +207,8 @@ export const googleCallback = async (ctx: any, next: any) => { { successRedirect: "/", failureRedirect: "/error" }, async (err: any, user: SSOUser, info: any) => { await passportCallback(ctx, user, err, info) - await context.identity.doInUserContext(user, async () => { - await events.auth.login("google-internal") + await context.identity.doInUserContext(user, ctx, async () => { + await events.auth.login("google-internal", user.email) }) ctx.redirect("/") } @@ -221,25 +217,20 @@ export const googleCallback = async (ctx: any, next: any) => { // OIDC SSO -export async function oidcCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC) +export async function oidcCallbackUrl() { + return ssoCallbackUrl(ConfigType.OIDC) } export const oidcStrategyFactory = async (ctx: any, configId: any) => { - const db = tenancy.getGlobalDB() - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.OIDC, - group: ctx.query.group, - }) + const config = await configs.getOIDCConfig() + if (!config) { + return ctx.throw(400, "OIDC config not found") + } - const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] - let callbackUrl = await oidcCallbackUrl(chosenConfig) + let callbackUrl = await oidcCallbackUrl() //Remote Config - const enrichedConfig = await oidc.fetchStrategyConfig( - chosenConfig, - callbackUrl - ) + const enrichedConfig = await oidc.fetchStrategyConfig(config, callbackUrl) return oidc.strategyFactory(enrichedConfig, userSdk.save) } @@ -247,23 +238,23 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => { * The initial call that OIDC authentication makes to take you to the configured OIDC login screen. * On a successful login, you will be redirected to the oidcAuth callback route. */ -export const oidcPreAuth = async (ctx: any, next: any) => { +export const oidcPreAuth = async (ctx: Ctx, next: any) => { const { configId } = ctx.params + if (!configId) { + ctx.throw(400, "OIDC config id is required") + } const strategy = await oidcStrategyFactory(ctx, configId) setCookie(ctx, configId, Cookie.OIDC_CONFIG) - const db = tenancy.getGlobalDB() - const config = await dbCore.getScopedConfig(db, { - type: ConfigType.OIDC, - group: ctx.query.group, - }) - - const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] + const config = await configs.getOIDCConfigById(configId) + if (!config) { + return ctx.throw(400, "OIDC config not found") + } let authScopes = - chosenConfig.scopes?.length > 0 - ? chosenConfig.scopes + config.scopes?.length > 0 + ? config.scopes : ["profile", "email", "offline_access"] return passport.authenticate(strategy, { @@ -281,8 +272,8 @@ export const oidcCallback = async (ctx: any, next: any) => { { successRedirect: "/", failureRedirect: "/error" }, async (err: any, user: SSOUser, info: any) => { await passportCallback(ctx, user, err, info) - await context.identity.doInUserContext(user, async () => { - await events.auth.login("oidc") + await context.identity.doInUserContext(user, ctx, async () => { + await events.auth.login("oidc", user.email) }) ctx.redirect("/") } diff --git a/packages/worker/src/api/controllers/global/configs.ts b/packages/worker/src/api/controllers/global/configs.ts index 855d766a87..1ea0f52c96 100644 --- a/packages/worker/src/api/controllers/global/configs.ts +++ b/packages/worker/src/api/controllers/global/configs.ts @@ -2,38 +2,32 @@ import * as email from "../../../utilities/email" import env from "../../../environment" import { googleCallbackUrl, oidcCallbackUrl } from "./auth" import { - events, cache, - objectStore, - tenancy, + configs, db as dbCore, env as coreEnv, + events, + objectStore, + tenancy, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" import { - Database, - Config as ConfigDoc, + Config, ConfigType, - SSOType, - GoogleConfig, - OIDCConfig, - SettingsConfig, + Ctx, + GetPublicOIDCConfigResponse, + GetPublicSettingsResponse, isGoogleConfig, isOIDCConfig, isSettingsConfig, isSMTPConfig, - Ctx, UserCtx, } from "@budibase/types" +import * as pro from "@budibase/pro" -const getEventFns = async (db: Database, config: ConfigDoc) => { +const getEventFns = async (config: Config, existing?: Config) => { const fns = [] - let existing - if (config._id) { - existing = await db.get(config._id) - } - if (!existing) { if (isSMTPConfig(config)) { fns.push(events.email.SMTPCreated) @@ -125,21 +119,21 @@ const getEventFns = async (db: Database, config: ConfigDoc) => { return fns } -export async function save(ctx: UserCtx) { - const db = tenancy.getGlobalDB() - const { type, workspace, user, config } = ctx.request.body - let eventFns = await getEventFns(db, ctx.request.body) - // Config does not exist yet - if (!ctx.request.body._id) { - ctx.request.body._id = dbCore.generateConfigID({ - type, - workspace, - user, - }) +export async function save(ctx: UserCtx) { + const body = ctx.request.body + const type = body.type + const config = body.config + + const existingConfig = await configs.getConfig(type) + let eventFns = await getEventFns(ctx.request.body, existingConfig) + + if (existingConfig) { + body._rev = existingConfig._rev } + try { // verify the configuration - switch (type) { + switch (config.type) { case ConfigType.SMTP: await email.verifyConfig(config) break @@ -149,7 +143,8 @@ export async function save(ctx: UserCtx) { } try { - const response = await db.put(ctx.request.body) + body._id = configs.generateConfigID(type) + const response = await configs.save(body) await cache.bustCache(cache.CacheKey.CHECKLIST) await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED) @@ -167,44 +162,11 @@ export async function save(ctx: UserCtx) { } } -export async function fetch(ctx: UserCtx) { - const db = tenancy.getGlobalDB() - const response = await db.allDocs( - dbCore.getConfigParams( - { type: ctx.params.type }, - { - include_docs: true, - } - ) - ) - ctx.body = response.rows.map(row => row.doc) -} - -/** - * Gets the most granular config for a particular configuration type. - * The hierarchy is type -> workspace -> user. - */ export async function find(ctx: UserCtx) { - const db = tenancy.getGlobalDB() - - const { userId, workspaceId } = ctx.query - if (workspaceId && userId) { - const workspace = await db.get(workspaceId as string) - const userInWorkspace = workspace.users.some( - (workspaceUser: any) => workspaceUser === userId - ) - if (!ctx.user!.admin && !userInWorkspace) { - ctx.throw(400, `User is not in specified workspace: ${workspace}.`) - } - } - try { // Find the config with the most granular scope based on context - const scopedConfig = await dbCore.getScopedFullConfig(db, { - type: ctx.params.type, - user: userId, - workspace: workspaceId, - }) + const type = ctx.params.type + const scopedConfig = await configs.getConfig(type) if (scopedConfig) { ctx.body = scopedConfig @@ -217,85 +179,70 @@ export async function find(ctx: UserCtx) { } } -export async function publicOidc(ctx: Ctx) { - const db = tenancy.getGlobalDB() +export async function publicOidc(ctx: Ctx) { try { // Find the config with the most granular scope based on context - const oidcConfig: OIDCConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.OIDC, - }) + const config = await configs.getOIDCConfig() - if (!oidcConfig) { - ctx.body = {} + if (!config) { + ctx.body = [] } else { - ctx.body = oidcConfig.config.configs.map(config => ({ - logo: config.logo, - name: config.name, - uuid: config.uuid, - })) + ctx.body = [ + { + logo: config.logo, + name: config.name, + uuid: config.uuid, + }, + ] } } catch (err: any) { ctx.throw(err.status, err) } } -export async function publicSettings(ctx: Ctx) { - const db = tenancy.getGlobalDB() - +export async function publicSettings( + ctx: Ctx +) { try { - // Find the config with the most granular scope based on context - const publicConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.SETTINGS, - }) - - const googleConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.GOOGLE, - }) - - const oidcConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.OIDC, - }) - - let config - if (!publicConfig) { - config = { - config: {}, - } - } else { - config = publicConfig - } - - // enrich the logo url - // empty url means deleted - if (config.config.logoUrl && config.config.logoUrl !== "") { - config.config.logoUrl = objectStore.getGlobalFileUrl( + // settings + const configDoc = await configs.getSettingsConfigDoc() + const config = configDoc.config + // enrich the logo url - empty url means deleted + if (config.logoUrl && config.logoUrl !== "") { + config.logoUrl = objectStore.getGlobalFileUrl( "settings", "logoUrl", - config.config.logoUrlEtag + config.logoUrlEtag ) } - // google button flag - if (googleConfig && googleConfig.config) { - // activated by default for configs pre-activated flag - config.config.google = - googleConfig.config.activated == null || googleConfig.config.activated - } else { - config.config.google = false + // google + const googleConfig = await configs.getGoogleConfig() + const preActivated = googleConfig?.activated == null + const google = preActivated || !!googleConfig?.activated + const _googleCallbackUrl = await googleCallbackUrl(googleConfig) + + // oidc + const oidcConfig = await configs.getOIDCConfig() + const oidc = oidcConfig?.activated || false + const _oidcCallbackUrl = await oidcCallbackUrl() + + // sso enforced + const isSSOEnforced = await pro.features.isSSOEnforced({ config }) + + ctx.body = { + type: ConfigType.SETTINGS, + _id: configDoc._id, + _rev: configDoc._rev, + config: { + ...config, + google, + oidc, + isSSOEnforced, + oidcCallbackUrl: _oidcCallbackUrl, + googleCallbackUrl: _googleCallbackUrl, + }, } - - // callback urls - config.config.oidcCallbackUrl = await oidcCallbackUrl() - config.config.googleCallbackUrl = await googleCallbackUrl() - - // oidc button flag - if (oidcConfig && oidcConfig.config) { - config.config.oidc = oidcConfig.config.configs[0].activated - } else { - config.config.oidc = false - } - - ctx.body = config } catch (err: any) { ctx.throw(err.status, err) } @@ -319,12 +266,11 @@ export async function upload(ctx: UserCtx) { }) // add to configuration structure - // TODO: right now this only does a global level - const db = tenancy.getGlobalDB() - let cfgStructure = await dbCore.getScopedFullConfig(db, { type }) - if (!cfgStructure) { - cfgStructure = { - _id: dbCore.generateConfigID({ type }), + let config = await configs.getConfig(type) + if (!config) { + config = { + _id: configs.generateConfigID(type), + type, config: {}, } } @@ -332,14 +278,14 @@ export async function upload(ctx: UserCtx) { // save the Etag for cache bursting const etag = result.ETag if (etag) { - cfgStructure.config[`${name}Etag`] = etag.replace(/"/g, "") + config.config[`${name}Etag`] = etag.replace(/"/g, "") } // save the file key - cfgStructure.config[`${name}`] = key + config.config[`${name}`] = key // write back to db - await db.put(cfgStructure) + await configs.save(config) ctx.body = { message: "File has been uploaded and url stored to config.", @@ -360,7 +306,6 @@ export async function destroy(ctx: UserCtx) { } export async function configChecklist(ctx: Ctx) { - const db = tenancy.getGlobalDB() const tenantId = tenancy.getTenantId() try { @@ -375,19 +320,13 @@ export async function configChecklist(ctx: Ctx) { } // They have set up SMTP - const smtpConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.SMTP, - }) + const smtpConfig = await configs.getSMTPConfig() // They have set up Google Auth - const googleConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.GOOGLE, - }) + const googleConfig = await configs.getGoogleConfig() // They have set up OIDC - const oidcConfig = await dbCore.getScopedFullConfig(db, { - type: ConfigType.OIDC, - }) + const oidcConfig = await configs.getOIDCConfig() // They have set up a global user const userExists = await checkAnyUserExists() diff --git a/packages/worker/src/api/routes/global/configs.ts b/packages/worker/src/api/routes/global/configs.ts index 38a31a28e6..922bcea212 100644 --- a/packages/worker/src/api/routes/global/configs.ts +++ b/packages/worker/src/api/routes/global/configs.ts @@ -104,13 +104,7 @@ router controller.save ) .delete("/api/global/configs/:id/:rev", auth.adminOnly, controller.destroy) - .get("/api/global/configs", controller.fetch) .get("/api/global/configs/checklist", controller.configChecklist) - .get( - "/api/global/configs/all/:type", - buildConfigGetValidation(), - controller.fetch - ) .get("/api/global/configs/public", controller.publicSettings) .get("/api/global/configs/public/oidc", controller.publicOidc) .get("/api/global/configs/:type", buildConfigGetValidation(), controller.find) diff --git a/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts b/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts new file mode 100644 index 0000000000..19e3cd64b4 --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts @@ -0,0 +1,111 @@ +import { mocks, structures } from "@budibase/backend-core/tests" +import { context, events } from "@budibase/backend-core" +import { Event, IdentityType } from "@budibase/types" +import { TestConfiguration } from "../../../../tests" + +mocks.licenses.useAuditLogs() + +const BASE_IDENTITY = { + account: undefined, + type: IdentityType.USER, +} +const USER_AUDIT_LOG_COUNT = 3 +const APP_ID = "app_1" + +describe("/api/global/auditlogs", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + describe("POST /api/global/auditlogs/search", () => { + it("should be able to fire some events (create audit logs)", async () => { + await context.doInTenant(config.tenantId, async () => { + const userId = config.user!._id! + const identity = { + ...BASE_IDENTITY, + _id: userId, + tenantId: config.tenantId, + } + await context.doInIdentityContext(identity, async () => { + for (let i = 0; i < USER_AUDIT_LOG_COUNT; i++) { + await events.user.created(structures.users.user()) + } + await context.doInAppContext(APP_ID, async () => { + await events.app.created(structures.apps.app(APP_ID)) + }) + // fetch the user created events + const response = await config.api.auditLogs.search({ + events: [Event.USER_CREATED], + }) + expect(response.data).toBeDefined() + // there will be an initial event which comes from the default user creation + expect(response.data.length).toBe(USER_AUDIT_LOG_COUNT + 1) + }) + }) + }) + + it("should be able to search by event", async () => { + const response = await config.api.auditLogs.search({ + events: [Event.USER_CREATED], + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.event).toBe(Event.USER_CREATED) + } + }) + + it("should be able to search by time range (frozen)", async () => { + // this is frozen, only need to add 1 and minus 1 + const now = new Date() + const start = new Date() + start.setSeconds(now.getSeconds() - 1) + const end = new Date() + end.setSeconds(now.getSeconds() + 1) + const response = await config.api.auditLogs.search({ + startDate: start.toISOString(), + endDate: end.toISOString(), + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.timestamp).toBe(now.toISOString()) + } + }) + + it("should be able to search by user ID", async () => { + const userId = config.user!._id! + const response = await config.api.auditLogs.search({ + userIds: [userId], + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.user._id).toBe(userId) + } + }) + + it("should be able to search by app ID", async () => { + const response = await config.api.auditLogs.search({ + appIds: [APP_ID], + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.app?._id).toBe(APP_ID) + } + }) + + it("should be able to search by full string", async () => { + const response = await config.api.auditLogs.search({ + fullSearch: "User", + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.name.includes("User")).toBe(true) + } + }) + }) +}) diff --git a/packages/worker/src/api/routes/global/tests/auth.spec.ts b/packages/worker/src/api/routes/global/tests/auth.spec.ts index 84f8ce1b0a..9b5392fc73 100644 --- a/packages/worker/src/api/routes/global/tests/auth.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auth.spec.ts @@ -110,7 +110,7 @@ describe("/api/global/auth", () => { ) expect(response.body).toEqual({ - message: "SSO user cannot login using password", + message: "Password login is disabled for this user", status: 400, }) } @@ -175,7 +175,7 @@ describe("/api/global/auth", () => { ) expect(res.body).toEqual({ - message: "SSO user cannot reset password", + message: "Password reset is disabled for this user", status: 400, error: { code: "http", @@ -367,7 +367,7 @@ describe("/api/global/auth", () => { const res = await config.api.configs.OIDCCallback(configId, preAuthRes) - expect(events.auth.login).toBeCalledWith("oidc") + expect(events.auth.login).toBeCalledWith("oidc", "oauth@example.com") expect(events.auth.login).toBeCalledTimes(1) expect(res.status).toBe(302) const location: string = res.get("location") diff --git a/packages/worker/src/api/routes/global/tests/configs.spec.ts b/packages/worker/src/api/routes/global/tests/configs.spec.ts index 39ad74d295..892fe8a67b 100644 --- a/packages/worker/src/api/routes/global/tests/configs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/configs.spec.ts @@ -2,7 +2,8 @@ jest.mock("nodemailer") import { TestConfiguration, structures, mocks } from "../../../../tests" mocks.email.mock() -import { Config, events } from "@budibase/backend-core" +import { events } from "@budibase/backend-core" +import { GetPublicSettingsResponse, Config, ConfigType } from "@budibase/types" describe("configs", () => { const config = new TestConfiguration() @@ -19,22 +20,29 @@ describe("configs", () => { await config.afterAll() }) - describe("post /api/global/configs", () => { - const saveConfig = async (conf: any, _id?: string, _rev?: string) => { - const data = { - ...conf, - _id, - _rev, - } - - const res = await config.api.configs.saveConfig(data) - - return { - ...data, - ...res.body, - } + const saveConfig = async (conf: Config, _id?: string, _rev?: string) => { + const data = { + ...conf, + _id, + _rev, } + const res = await config.api.configs.saveConfig(data) + return { + ...data, + ...res.body, + } + } + const saveSettingsConfig = async ( + conf?: any, + _id?: string, + _rev?: string + ) => { + const settingsConfig = structures.configs.settings(conf) + return saveConfig(settingsConfig, _id, _rev) + } + + describe("POST /api/global/configs", () => { describe("google", () => { const saveGoogleConfig = async ( conf?: any, @@ -49,20 +57,20 @@ describe("configs", () => { it("should create activated google config", async () => { await saveGoogleConfig() expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSODeactivated).not.toBeCalled() expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE) - await config.deleteConfig(Config.GOOGLE) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) it("should create deactivated google config", async () => { await saveGoogleConfig({ activated: false }) expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSOActivated).not.toBeCalled() expect(events.auth.SSODeactivated).not.toBeCalled() - await config.deleteConfig(Config.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) }) @@ -76,11 +84,11 @@ describe("configs", () => { googleConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSOActivated).not.toBeCalled() expect(events.auth.SSODeactivated).toBeCalledTimes(1) - expect(events.auth.SSODeactivated).toBeCalledWith(Config.GOOGLE) - await config.deleteConfig(Config.GOOGLE) + expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) it("should update google config to activated", async () => { @@ -92,11 +100,11 @@ describe("configs", () => { googleConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE) expect(events.auth.SSODeactivated).not.toBeCalled() expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE) - await config.deleteConfig(Config.GOOGLE) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE) + await config.deleteConfig(ConfigType.GOOGLE) }) }) }) @@ -115,20 +123,20 @@ describe("configs", () => { it("should create activated OIDC config", async () => { await saveOIDCConfig() expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC) expect(events.auth.SSODeactivated).not.toBeCalled() expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) it("should create deactivated OIDC config", async () => { await saveOIDCConfig({ activated: false }) expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC) expect(events.auth.SSOActivated).not.toBeCalled() expect(events.auth.SSODeactivated).not.toBeCalled() - await config.deleteConfig(Config.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) }) @@ -142,11 +150,11 @@ describe("configs", () => { oidcConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC) expect(events.auth.SSOActivated).not.toBeCalled() expect(events.auth.SSODeactivated).toBeCalledTimes(1) - expect(events.auth.SSODeactivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) + expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) it("should update OIDC config to activated", async () => { @@ -158,11 +166,11 @@ describe("configs", () => { oidcConf._rev ) expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC) expect(events.auth.SSODeactivated).not.toBeCalled() expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) + expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC) + await config.deleteConfig(ConfigType.OIDC) }) }) }) @@ -179,11 +187,11 @@ describe("configs", () => { describe("create", () => { it("should create SMTP config", async () => { - await config.deleteConfig(Config.SMTP) + await config.deleteConfig(ConfigType.SMTP) await saveSMTPConfig() expect(events.email.SMTPUpdated).not.toBeCalled() expect(events.email.SMTPCreated).toBeCalledTimes(1) - await config.deleteConfig(Config.SMTP) + await config.deleteConfig(ConfigType.SMTP) }) }) @@ -194,24 +202,15 @@ describe("configs", () => { await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev) expect(events.email.SMTPCreated).not.toBeCalled() expect(events.email.SMTPUpdated).toBeCalledTimes(1) - await config.deleteConfig(Config.SMTP) + await config.deleteConfig(ConfigType.SMTP) }) }) }) describe("settings", () => { - 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 () => { - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(ConfigType.SETTINGS) await saveSettingsConfig() @@ -222,7 +221,7 @@ describe("configs", () => { it("should create settings config with non-default settings", async () => { config.selfHosted() - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(ConfigType.SETTINGS) const conf = { company: "acme", logoUrl: "http://example.com", @@ -241,7 +240,7 @@ describe("configs", () => { describe("update", () => { it("should update settings config", async () => { config.selfHosted() - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(ConfigType.SETTINGS) const settingsConfig = await saveSettingsConfig() settingsConfig.config.company = "acme" settingsConfig.config.logoUrl = "http://example.com" @@ -262,14 +261,43 @@ describe("configs", () => { }) }) - it("should return the correct checklist status based on the state of the budibase installation", async () => { - await config.saveSmtpConfig() + describe("GET /api/global/configs/checklist", () => { + it("should return the correct checklist", async () => { + await config.saveSmtpConfig() - const res = await config.api.configs.getConfigChecklist() - const checklist = res.body + const res = await config.api.configs.getConfigChecklist() + const checklist = res.body - expect(checklist.apps.checked).toBeFalsy() - expect(checklist.smtp.checked).toBeTruthy() - expect(checklist.adminUser.checked).toBeTruthy() + expect(checklist.apps.checked).toBeFalsy() + expect(checklist.smtp.checked).toBeTruthy() + expect(checklist.adminUser.checked).toBeTruthy() + }) + }) + + describe("GET /api/global/configs/public", () => { + it("should return the expected public settings", async () => { + await saveSettingsConfig() + + const res = await config.api.configs.getPublicSettings() + const body = res.body as GetPublicSettingsResponse + + const expected = { + _id: "config_settings", + type: "settings", + config: { + company: "Budibase", + logoUrl: "", + analyticsEnabled: false, + google: true, + googleCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/google/callback`, + isSSOEnforced: false, + oidc: false, + oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`, + platformUrl: "http://localhost:10000", + }, + } + delete body._rev + expect(body).toEqual(expected) + }) }) }) diff --git a/packages/worker/src/api/routes/global/tests/realEmail.spec.ts b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts index 1c180be75d..8ad7363c85 100644 --- a/packages/worker/src/api/routes/global/tests/realEmail.spec.ts +++ b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts @@ -1,3 +1,4 @@ +jest.unmock("node-fetch") import { TestConfiguration } from "../../../../tests" import { EmailTemplatePurpose } from "../../../../constants" const nodemailer = require("nodemailer") diff --git a/packages/worker/src/api/routes/index.ts b/packages/worker/src/api/routes/index.ts index 3aa9422238..c64ad44423 100644 --- a/packages/worker/src/api/routes/index.ts +++ b/packages/worker/src/api/routes/index.ts @@ -18,6 +18,8 @@ import accountRoutes from "./system/accounts" import restoreRoutes from "./system/restore" let userGroupRoutes = api.groups +let auditLogRoutes = api.auditLogs + export const routes: Router[] = [ configRoutes, userRoutes, @@ -32,6 +34,7 @@ export const routes: Router[] = [ selfRoutes, licenseRoutes, userGroupRoutes, + auditLogRoutes, migrationRoutes, accountRoutes, restoreRoutes, diff --git a/packages/worker/src/db/index.ts b/packages/worker/src/db/index.ts index d74d00d910..157c2f4fb3 100644 --- a/packages/worker/src/db/index.ts +++ b/packages/worker/src/db/index.ts @@ -1,10 +1,16 @@ import * as core from "@budibase/backend-core" import env from "../environment" -export const init = () => { - const dbConfig: any = {} +export function init() { + const dbConfig: any = { + replication: true, + find: true, + } + if (env.isTest() && !env.COUCH_DB_URL) { dbConfig.inMemory = true + dbConfig.allDbs = true } + core.init({ db: dbConfig }) } diff --git a/packages/worker/src/environment.ts b/packages/worker/src/environment.ts index 71fd89f276..cd5360f7f7 100644 --- a/packages/worker/src/environment.ts +++ b/packages/worker/src/environment.ts @@ -26,8 +26,6 @@ function parseIntSafe(number: any) { } } -const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") - const environment = { // auth MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, @@ -51,7 +49,7 @@ const environment = { CLUSTER_PORT: process.env.CLUSTER_PORT, // flags NODE_ENV: process.env.NODE_ENV, - SELF_HOSTED: selfHosted, + 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, @@ -71,14 +69,6 @@ const environment = { * Mock the email service in use - links to ethereal hosted emails are logged instead. */ ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE, - /** - * Enable to allow an admin user to login using a password. - * This can be useful to prevent lockout when configuring SSO. - * However, this should be turned OFF by default for security purposes. - */ - ENABLE_SSO_MAINTENANCE_MODE: selfHosted - ? process.env.ENABLE_SSO_MAINTENANCE_MODE - : false, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 1e3ff3cbdf..04413e8429 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -13,7 +13,15 @@ import { Event } from "@sentry/types/dist/event" import Application from "koa" import { bootstrap } from "global-agent" import * as db from "./db" -import { auth, logging, events, middleware } from "@budibase/backend-core" +import { sdk as proSdk } from "@budibase/pro" +import { + auth, + logging, + events, + middleware, + queue, + env as coreEnv, +} from "@budibase/backend-core" db.init() import Koa from "koa" import koaBody from "koa-body" @@ -23,9 +31,15 @@ import * as redis from "./utilities/redis" const Sentry = require("@sentry/node") const koaSession = require("koa-session") const logger = require("koa-pino-logger") +const { userAgent } = require("koa-useragent") + import destroyable from "server-destroy" -if (env.ENABLE_SSO_MAINTENANCE_MODE) { +// configure events to use the pro audit log write +// can't integrate directly into backend-core due to cyclic issues +events.processors.init(proSdk.auditLogs.write) + +if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) { console.warn( "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress" ) @@ -43,6 +57,7 @@ app.use(koaBody({ multipart: true })) app.use(koaSession(app)) app.use(middleware.logging) app.use(logger(logging.pinoSettings())) +app.use(userAgent) // authentication app.use(auth.passport.initialize()) @@ -78,6 +93,7 @@ server.on("close", async () => { console.log("Server Closed") await redis.shutdown() await events.shutdown() + await queue.shutdown() if (!env.isTest()) { process.exit(errCode) } diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 15a4f3c7e7..8e9cff18dd 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -58,8 +58,8 @@ export const reset = async (email: string) => { } // exit if user has sso - if (await userSdk.isPreventSSOPasswords(user)) { - throw new HTTPError("SSO user cannot reset password", 400) + if (await userSdk.isPreventPasswordActions(user)) { + throw new HTTPError("Password reset is disabled for this user", 400) } // send password reset diff --git a/packages/worker/src/sdk/users/tests/users.spec.ts b/packages/worker/src/sdk/users/tests/users.spec.ts index 41d9298997..77f02eec7a 100644 --- a/packages/worker/src/sdk/users/tests/users.spec.ts +++ b/packages/worker/src/sdk/users/tests/users.spec.ts @@ -1,26 +1,50 @@ import { structures } from "../../../tests" -import * as users from "../users" -import env from "../../../environment" import { mocks } from "@budibase/backend-core/tests" +import { env } from "@budibase/backend-core" +import * as users from "../users" import { CloudAccount } from "@budibase/types" +import { isPreventPasswordActions } from "../users" + +jest.mock("@budibase/pro") +import * as _pro from "@budibase/pro" +const pro = jest.mocked(_pro, true) describe("users", () => { - describe("isPreventSSOPasswords", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("isPreventPasswordActions", () => { + it("returns false for non sso user", async () => { + const user = structures.users.user() + const result = await users.isPreventPasswordActions(user) + expect(result).toBe(false) + }) + it("returns true for sso account user", async () => { const user = structures.users.user() mocks.accounts.getAccount.mockReturnValue( Promise.resolve(structures.accounts.ssoAccount() as CloudAccount) ) - const result = await users.isPreventSSOPasswords(user) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(true) }) it("returns true for sso user", async () => { const user = structures.users.ssoUser() - const result = await users.isPreventSSOPasswords(user) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(true) }) + describe("enforced sso", () => { + it("returns true for all users when sso is enforced", async () => { + const user = structures.users.user() + pro.features.isSSOEnforced.mockReturnValue(Promise.resolve(true)) + const result = await users.isPreventPasswordActions(user) + expect(result).toBe(true) + }) + }) + describe("sso maintenance mode", () => { beforeEach(() => { env._set("ENABLE_SSO_MAINTENANCE_MODE", true) @@ -33,7 +57,7 @@ describe("users", () => { describe("non-admin user", () => { it("returns true", async () => { const user = structures.users.ssoUser() - const result = await users.isPreventSSOPasswords(user) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(true) }) }) @@ -43,7 +67,7 @@ describe("users", () => { const user = structures.users.ssoUser({ user: structures.users.adminUser(), }) - const result = await users.isPreventSSOPasswords(user) + const result = await users.isPreventPasswordActions(user) expect(result).toBe(false) }) }) diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 7d4a2f04f0..18d5a04cda 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -14,6 +14,7 @@ import { users as usersCore, utils, ViewName, + env as coreEnv, } from "@budibase/backend-core" import { AccountMetadata, @@ -34,7 +35,7 @@ import { } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" -import { groups as groupsSdk } from "@budibase/pro" +import * as pro from "@budibase/pro" import * as accountSdk from "../accounts" const PAGE_LIMIT = 8 @@ -122,8 +123,8 @@ const buildUser = async ( let hashedPassword if (password) { - if (await isPreventSSOPasswords(user)) { - throw new HTTPError("SSO user cannot set password", 400) + if (await isPreventPasswordActions(user)) { + throw new HTTPError("Password change is disabled for this user", 400) } hashedPassword = opts.hashPassword ? await utils.hash(password) : password } else if (dbUser) { @@ -188,13 +189,18 @@ const validateUniqueUser = async (email: string, tenantId: string) => { } } -export async function isPreventSSOPasswords(user: User) { +export async function isPreventPasswordActions(user: User) { // when in maintenance mode we allow sso users with the admin role // to perform any password action - this prevents lockout - if (env.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) { + if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) { return false } + // SSO is enforced for all users + if (await pro.features.isSSOEnforced()) { + return true + } + // Check local sso if (isSSOUser(user)) { return true @@ -278,7 +284,7 @@ export const save = async ( if (userGroups.length > 0) { for (let groupId of userGroups) { - groupPromises.push(groupsSdk.addUsers(groupId, [_id])) + groupPromises.push(pro.groups.addUsers(groupId, [_id])) } } } @@ -456,7 +462,7 @@ export const bulkCreate = async ( const groupPromises = [] const createdUserIds = saved.map(user => user._id) for (let groupId of groups) { - groupPromises.push(groupsSdk.addUsers(groupId, createdUserIds)) + groupPromises.push(pro.groups.addUsers(groupId, createdUserIds)) } await Promise.all(groupPromises) } @@ -631,7 +637,7 @@ export const invite = async ( } await sendEmail(user.email, EmailTemplatePurpose.INVITATION, opts) response.successful.push({ email: user.email }) - await events.user.invited() + await events.user.invited(user.email) } catch (e) { console.error(`Failed to send email invitation email=${user.email}`, e) response.unsuccessful.push({ diff --git a/packages/worker/src/tests/api/auditLogs.ts b/packages/worker/src/tests/api/auditLogs.ts new file mode 100644 index 0000000000..d7bc4d99fb --- /dev/null +++ b/packages/worker/src/tests/api/auditLogs.ts @@ -0,0 +1,26 @@ +import { AuditLogSearchParams, SearchAuditLogsResponse } from "@budibase/types" +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class AuditLogAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + search = async (search: AuditLogSearchParams) => { + const res = await this.request + .post("/api/global/auditlogs/search") + .send(search) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + return res.body as SearchAuditLogsResponse + } + + download = (search: AuditLogSearchParams) => { + const query = encodeURIComponent(JSON.stringify(search)) + return this.request + .get(`/api/global/auditlogs/download?query=${query}`) + .set(this.config.defaultHeaders()) + } +} diff --git a/packages/worker/src/tests/api/configs.ts b/packages/worker/src/tests/api/configs.ts index 76a6f31415..74cef2bf8b 100644 --- a/packages/worker/src/tests/api/configs.ts +++ b/packages/worker/src/tests/api/configs.ts @@ -14,6 +14,14 @@ export class ConfigAPI extends TestAPI { .expect("Content-Type", /json/) } + getPublicSettings = () => { + return this.request + .get(`/api/global/configs/public`) + .set(this.config.defaultHeaders()) + .expect(200) + .expect("Content-Type", /json/) + } + saveConfig = (data: any) => { return this.request .post(`/api/global/configs`) diff --git a/packages/worker/src/tests/api/email.ts b/packages/worker/src/tests/api/email.ts index ba7c7dbec0..fd3c622cfa 100644 --- a/packages/worker/src/tests/api/email.ts +++ b/packages/worker/src/tests/api/email.ts @@ -13,6 +13,7 @@ export class EmailAPI extends TestAPI { email: "test@test.com", purpose, tenantId: this.config.getTenantId(), + userId: this.config.user?._id!, }) .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts index 0bd0308e2f..166996e792 100644 --- a/packages/worker/src/tests/api/index.ts +++ b/packages/worker/src/tests/api/index.ts @@ -14,6 +14,7 @@ import { GroupsAPI } from "./groups" import { RolesAPI } from "./roles" import { TemplatesAPI } from "./templates" import { LicenseAPI } from "./license" +import { AuditLogAPI } from "./auditLogs" export default class API { accounts: AccountAPI auth: AuthAPI @@ -30,6 +31,7 @@ export default class API { roles: RolesAPI templates: TemplatesAPI license: LicenseAPI + auditLogs: AuditLogAPI constructor(config: TestConfiguration) { this.accounts = new AccountAPI(config) @@ -47,5 +49,6 @@ export default class API { this.roles = new RolesAPI(config) this.templates = new TemplatesAPI(config) this.license = new LicenseAPI(config) + this.auditLogs = new AuditLogAPI(config) } } diff --git a/packages/worker/src/tests/structures/configs.ts b/packages/worker/src/tests/structures/configs.ts index 2c76f271c4..d50f5ebc72 100644 --- a/packages/worker/src/tests/structures/configs.ts +++ b/packages/worker/src/tests/structures/configs.ts @@ -1,9 +1,15 @@ -import { Config } from "../../constants" import { utils } from "@budibase/backend-core" +import { + SettingsConfig, + ConfigType, + SMTPConfig, + GoogleConfig, + OIDCConfig, +} from "@budibase/types" -export function oidc(conf?: any) { +export function oidc(conf?: any): OIDCConfig { return { - type: Config.OIDC, + type: ConfigType.OIDC, config: { configs: [ { @@ -21,9 +27,9 @@ export function oidc(conf?: any) { } } -export function google(conf?: any) { +export function google(conf?: any): GoogleConfig { return { - type: Config.GOOGLE, + type: ConfigType.GOOGLE, config: { clientID: "clientId", clientSecret: "clientSecret", @@ -33,9 +39,9 @@ export function google(conf?: any) { } } -export function smtp(conf?: any) { +export function smtp(conf?: any): SMTPConfig { return { - type: Config.SMTP, + type: ConfigType.SMTP, config: { port: 12345, host: "smtptesthost.com", @@ -47,25 +53,26 @@ export function smtp(conf?: any) { } } -export function smtpEthereal() { +export function smtpEthereal(): SMTPConfig { return { - type: Config.SMTP, + type: ConfigType.SMTP, config: { port: 587, host: "smtp.ethereal.email", + from: "testfrom@test.com", secure: false, auth: { - user: "don.bahringer@ethereal.email", - pass: "yCKSH8rWyUPbnhGYk9", + user: "wyatt.zulauf29@ethereal.email", + pass: "tEwDtHBWWxusVWAPfa", }, connectionTimeout: 1000, // must be less than the jest default of 5000 }, } } -export function settings(conf?: any) { +export function settings(conf?: any): SettingsConfig { return { - type: Config.SETTINGS, + type: ConfigType.SETTINGS, config: { platformUrl: "http://localhost:10000", logoUrl: "", diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index 66e860edcb..69861684eb 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -1,11 +1,11 @@ import env from "../environment" -import { EmailTemplatePurpose, TemplateType, Config } from "../constants" +import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" import { getResetPasswordCode, getInviteCode } from "./redis" -import { User, Database } from "@budibase/types" -import { tenancy, db as dbCore } from "@budibase/backend-core" +import { User, SMTPInnerConfig } from "@budibase/types" +import { configs } from "@budibase/backend-core" const nodemailer = require("nodemailer") type SendEmailOpts = { @@ -36,24 +36,24 @@ const FULL_EMAIL_PURPOSES = [ EmailTemplatePurpose.CUSTOM, ] -function createSMTPTransport(config: any) { +function createSMTPTransport(config?: SMTPInnerConfig) { let options: any - let secure = config.secure + let secure = config?.secure // default it if not specified if (secure == null) { - secure = config.port === 465 + secure = config?.port === 465 } if (!TEST_MODE) { options = { - port: config.port, - host: config.host, + port: config?.port, + host: config?.host, secure: secure, - auth: config.auth, + auth: config?.auth, } options.tls = { rejectUnauthorized: false, } - if (config.connectionTimeout) { + if (config?.connectionTimeout) { options.connectionTimeout = config.connectionTimeout } } else { @@ -134,57 +134,16 @@ async function buildEmail( }) } -/** - * Utility function for finding most valid SMTP configuration. - * @param {object} db The CouchDB database which is to be looked up within. - * @param {string|null} workspaceId If using finer grain control of configs a workspace can be used. - * @param {boolean|null} automation Whether or not the configuration is being fetched for an email automation. - * @return {Promise} returns the SMTP configuration if it exists - */ -async function getSmtpConfiguration( - db: Database, - workspaceId?: string, - automation?: boolean -) { - const params: any = { - type: Config.SMTP, - } - if (workspaceId) { - params.workspace = workspaceId - } - - const customConfig = await dbCore.getScopedConfig(db, params) - - if (customConfig) { - return customConfig - } - - // Use an SMTP fallback configuration from env variables - if (!automation && env.SMTP_FALLBACK_ENABLED) { - return { - port: env.SMTP_PORT, - host: env.SMTP_HOST, - secure: false, - from: env.SMTP_FROM_ADDRESS, - auth: { - user: env.SMTP_USER, - pass: env.SMTP_PASSWORD, - }, - } - } -} - /** * Checks if a SMTP config exists based on passed in parameters. * @return {Promise} returns true if there is a configuration that can be used. */ -export async function isEmailConfigured(workspaceId?: string) { +export async function isEmailConfigured() { // when "testing" or smtp fallback is enabled simply return true if (TEST_MODE || env.SMTP_FALLBACK_ENABLED) { return true } - const db = tenancy.getGlobalDB() - const config = await getSmtpConfiguration(db, workspaceId) + const config = await configs.getSMTPConfig() return config != null } @@ -202,22 +161,17 @@ export async function sendEmail( purpose: EmailTemplatePurpose, opts: SendEmailOpts ) { - const db = tenancy.getGlobalDB() - let config = - (await getSmtpConfiguration(db, opts?.workspaceId, opts?.automation)) || {} - if (Object.keys(config).length === 0 && !TEST_MODE) { + const config = await configs.getSMTPConfig(opts?.automation) + if (!config && !TEST_MODE) { throw "Unable to find SMTP configuration." } const transport = createSMTPTransport(config) // if there is a link code needed this will retrieve it const code = await getLinkCode(purpose, email, opts.user, opts?.info) - let context - if (code) { - context = await getSettingsTemplateContext(purpose, code) - } + let context = await getSettingsTemplateContext(purpose, code) let message: any = { - from: opts?.from || config.from, + from: opts?.from || config?.from, html: await buildEmail(purpose, email, context, { user: opts?.user, contents: opts?.contents, @@ -231,9 +185,9 @@ export async function sendEmail( bcc: opts?.bcc, } - if (opts?.subject || config.subject) { + if (opts?.subject || config?.subject) { message.subject = await processString( - opts?.subject || config.subject, + (opts?.subject || config?.subject) as string, context ) } diff --git a/packages/worker/src/utilities/templates.ts b/packages/worker/src/utilities/templates.ts index ede95dbe4a..1597325c7c 100644 --- a/packages/worker/src/utilities/templates.ts +++ b/packages/worker/src/utilities/templates.ts @@ -1,6 +1,5 @@ -import { db as dbCore, tenancy } from "@budibase/backend-core" +import { tenancy, configs } from "@budibase/backend-core" import { - Config, InternalTemplateBinding, LOGO_URL, EmailTemplatePurpose, @@ -10,20 +9,16 @@ const BASE_COMPANY = "Budibase" export async function getSettingsTemplateContext( purpose: EmailTemplatePurpose, - code?: string + code?: string | null ) { - const db = tenancy.getGlobalDB() - // TODO: use more granular settings in the future if required - let settings = - (await dbCore.getScopedConfig(db, { type: Config.SETTINGS })) || {} + let settings = await configs.getSettingsConfig() const URL = settings.platformUrl const context: any = { [InternalTemplateBinding.LOGO_URL]: checkSlashesInUrl(`${URL}/${settings.logoUrl}`) || LOGO_URL, [InternalTemplateBinding.PLATFORM_URL]: URL, [InternalTemplateBinding.COMPANY]: settings.company || BASE_COMPANY, - [InternalTemplateBinding.DOCS_URL]: - settings.docsUrl || "https://docs.budibase.com/", + [InternalTemplateBinding.DOCS_URL]: "https://docs.budibase.com/", [InternalTemplateBinding.LOGIN_URL]: checkSlashesInUrl( tenancy.addTenantToUrl(`${URL}/login`) ), diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index d91fc340ec..d382f67ed0 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.8": - version "2.3.18-alpha.8" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.8.tgz#a48406adf2f781471822f62d42cba3e27c660778" - integrity sha512-uP6ff/h47d/YwBJdws6sMZVNnMyoGJsLO/Wqz5JlLR1g3F5k5fViMtwx1kK4FcQ8J3JGjjfXtxWJ4wLyWucFAA== +"@budibase/backend-core@2.3.18-alpha.15": + version "2.3.18-alpha.15" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.15.tgz#62e17491c4f546ff0e772005f0a6b4c55f9d2b63" + integrity sha512-TX7LU0E3EoCxcMSZFSO/CmQdhkHYBjT5y1O/UhNMzp9xOVsFUcuFNDXFhylruHnWo7VnbiIBwYDxcXgg0Bz9ig== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.8" + "@budibase/types" "2.3.18-alpha.15" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -514,6 +514,31 @@ uuid "8.3.2" zlib "1.0.5" +"@budibase/handlebars-helpers@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841" + integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ== + dependencies: + array-sort "^1.0.0" + define-property "^2.0.2" + extend-shallow "^3.0.2" + for-in "^1.0.2" + get-object "^0.2.0" + get-value "^3.0.1" + handlebars "^4.7.7" + handlebars-utils "^1.0.6" + has-value "^2.0.2" + helper-md "^0.2.2" + html-tag "^2.0.0" + is-even "^1.0.0" + is-glob "^4.0.1" + kind-of "^6.0.3" + micromatch "^3.1.5" + relative "^3.0.2" + striptags "^3.1.1" + to-gfm-code-block "^0.1.1" + year "^0.2.1" + "@budibase/nano@10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.1.tgz#36ccda4d9bb64b5ee14dd2b27a295b40739b1038" @@ -539,24 +564,38 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.8": - version "2.3.18-alpha.8" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.8.tgz#24aa885d35b51123e5152094f67961edb10785b8" - integrity sha512-pozClQBcntm7SIc66CIblPBHnoWekxqWSxoulj9zbXvO/gmIc0oU7z0xT7nwKvsU9RCthXaFXDHeH+rWbyOPNg== +"@budibase/pro@2.3.18-alpha.15": + version "2.3.18-alpha.15" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.15.tgz#5237b6f9b8f8627cd46bc014b9be6c083412a370" + integrity sha512-owEtr2peoF4zpPQ+UZepfi4nYxWRKA/qZa/748qJc03T3hOdU7dXj6Nkjl7nc0PHuYdZ3NewJKK5gnr2wMb2+A== dependencies: - "@budibase/backend-core" "2.3.18-alpha.8" - "@budibase/types" "2.3.18-alpha.8" + "@budibase/backend-core" "2.3.18-alpha.15" + "@budibase/string-templates" "2.3.18-alpha.14" + "@budibase/types" "2.3.18-alpha.15" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" jsonwebtoken "8.5.1" lru-cache "^7.14.1" + memorystream "^0.3.1" node-fetch "^2.6.1" -"@budibase/types@2.3.18-alpha.8": - version "2.3.18-alpha.8" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.8.tgz#7822eb07b7abf2af48d6940a0ab208b8be4ab51a" - integrity sha512-M3GM35OkFgkw6ByQkSlZn/SG3/T6e/kyY/3f2mqft4dYL1Y89bPn7zKG6nB4BqTwU7ItLkUCNVtJNOBhXZCKqg== +"@budibase/string-templates@2.3.18-alpha.14": + version "2.3.18-alpha.14" + resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-2.3.18-alpha.14.tgz#c3b8d45ced321088c76bcda4efd7e9c7635a2788" + integrity sha512-xamfugDHgvzupe3EkvTY7ymXn9cRxb61nKaap52NsQQl8Zby2W2qJNVBNnuSnhnkQQeF5EatIFgGni+yBDchtQ== + dependencies: + "@budibase/handlebars-helpers" "^0.11.8" + dayjs "^1.10.4" + handlebars "^4.7.6" + handlebars-utils "^1.0.6" + lodash "^4.17.20" + vm2 "^3.9.4" + +"@budibase/types@2.3.18-alpha.15": + version "2.3.18-alpha.15" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.15.tgz#a7b4189b5dd823100f3785b5a991e428529c7e21" + integrity sha512-94+OIOpY4d+kdsXMIXU0RYB8WKQN8y8ueqJZt1OUHvSgCnb8CWIJoVv0LlHFgiW7871JPO2KS6nbpndDXeJhgA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -1930,7 +1969,7 @@ acorn-jsx@^5.2.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.1.1: +acorn-walk@^8.1.1, acorn-walk@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -1950,6 +1989,11 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.7.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + 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" @@ -2060,7 +2104,7 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -argparse@^1.0.7: +argparse@^1.0.10, argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== @@ -2082,11 +2126,40 @@ argsarray@0.0.1: resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg== +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== + +array-sort@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" + integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== + dependencies: + default-compare "^1.0.0" + get-value "^2.0.6" + kind-of "^5.0.2" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== + asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -2104,6 +2177,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== + ast-types@0.9.6: version "0.9.6" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" @@ -2138,11 +2216,23 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +autolinker@~0.28.0: + version "0.28.1" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47" + integrity sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ== + dependencies: + gulp-header "^1.7.1" + aws-cloudfront-sign@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/aws-cloudfront-sign/-/aws-cloudfront-sign-2.2.0.tgz#3910f5a6d0d90fec07f2b4ef8ab07f3eefb5625d" @@ -2279,6 +2369,19 @@ 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== +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.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" @@ -2357,6 +2460,22 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -2468,6 +2587,21 @@ bytes@3.1.2, bytes@^3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + cache-content-type@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" @@ -2622,6 +2756,16 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + cli-boxes@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" @@ -2694,6 +2838,14 @@ collect-v8-coverage@^1.0.0: resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw== + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2750,7 +2902,7 @@ commoner@^0.10.1: q "^1.1.2" recast "^0.11.17" -component-emitter@^1.3.0: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -2780,6 +2932,13 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-with-sourcemaps@*: + version "1.1.0" + resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" + integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== + dependencies: + source-map "^0.6.1" + configstore@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" @@ -2849,6 +3008,11 @@ cookies@~0.8.0: depd "~2.0.0" keygrip "~1.1.0" +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== + copyfiles@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5" @@ -2961,6 +3125,11 @@ dateformat@^4.5.1: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== +dayjs@^1.10.4: + version "1.11.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" + integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + dd-trace@3.13.2: version "3.13.2" resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.13.2.tgz#95b1ec480ab9ac406e1da7591a8c6f678d3799fd" @@ -3001,6 +3170,13 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, d dependencies: ms "2.1.2" +debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3013,6 +3189,11 @@ debuglog@^1.0.0: resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw== +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -3052,6 +3233,13 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" + integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== + dependencies: + kind-of "^5.0.2" + defer-to-connect@^1.0.1: version "1.1.3" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" @@ -3078,6 +3266,28 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA== + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA== + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" @@ -3371,6 +3581,11 @@ end-stream@~0.1.0: dependencies: write-stream "~0.4.3" +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + errno@~0.1.1: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -3635,6 +3850,19 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA== + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + expand-tilde@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" @@ -3653,6 +3881,26 @@ expect@^28.1.3: jest-message-util "^28.1.3" jest-util "^28.1.3" +express-useragent@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/express-useragent/-/express-useragent-1.0.15.tgz#cefda5fa4904345d51d3368b117a8dd4124985d9" + integrity sha512-eq5xMiYCYwFPoekffMjvEIk+NWdlQY9Y38OsTyl13IvA728vKT+q/CSERYWzcw93HGBJcIqMIsZC5CZGARPVdg== + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -3667,6 +3915,20 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -3779,6 +4041,16 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -3843,6 +4115,11 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -3895,6 +4172,13 @@ forwarded-parse@^2.1.0: 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" + integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA== + dependencies: + map-cache "^0.2.2" + fresh@^0.5.2, fresh@~0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -3986,6 +4270,14 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.3" +get-object@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/get-object/-/get-object-0.2.0.tgz#d92ff7d5190c64530cda0543dac63a3d47fe8c0c" + integrity sha512-7P6y6k6EzEFmO/XyUyFlXm1YLJy9xeA1x/grNV8276abX5GuwUtYgKFkRFkLixw4hf4Pz9q2vgv/8Ar42R0HuQ== + dependencies: + is-number "^2.0.2" + isobject "^0.2.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -4023,6 +4315,18 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== + +get-value@^3.0.0, get-value@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" + integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA== + dependencies: + isobject "^3.0.1" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -4167,6 +4471,35 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +gulp-header@^1.7.1: + version "1.8.12" + resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" + integrity sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ== + dependencies: + concat-with-sourcemaps "*" + lodash.template "^4.4.0" + through2 "^2.0.0" + +handlebars-utils@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/handlebars-utils/-/handlebars-utils-1.0.6.tgz#cb9db43362479054782d86ffe10f47abc76357f9" + integrity sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw== + dependencies: + kind-of "^6.0.0" + typeof-article "^0.1.1" + +handlebars@^4.7.6, handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -4219,6 +4552,52 @@ has-unicode@^2.0.1: resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q== + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw== + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-value@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-2.0.2.tgz#d0f12e8780ba8e90e66ad1a21c707fdb67c25658" + integrity sha512-ybKOlcRsK2MqrM3Hmz/lQxXHZ6ejzSPzpNabKB45jb5qDgJvKPa3SdapTsTLwEb9WltgWpOmNax7i+DzNOk4TA== + dependencies: + get-value "^3.0.0" + has-values "^2.0.1" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ== + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ== + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has-values@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-2.0.1.tgz#3876200ff86d8a8546a9264a952c17d5fc17579d" + integrity sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w== + dependencies: + kind-of "^6.0.2" + has-yarn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" @@ -4231,6 +4610,16 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +helper-md@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/helper-md/-/helper-md-0.2.2.tgz#c1f59d7e55bbae23362fd8a0e971607aec69d41f" + integrity sha512-49TaQzK+Ic7ZVTq4i1UZxRUJEmAilTk8hz7q4I0WNUaTclLR8ArJV5B3A1fe1xF2HtsDTr2gYKLaVTof/Lt84Q== + dependencies: + ent "^2.2.0" + extend-shallow "^2.0.1" + fs-exists-sync "^0.1.0" + remarkable "^1.6.2" + hexoid@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" @@ -4248,6 +4637,14 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-tag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tag/-/html-tag-2.0.0.tgz#36c3bc8d816fd30b570d5764a497a641640c2fed" + integrity sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g== + dependencies: + is-self-closing "^1.0.1" + kind-of "^6.0.0" + http-assert@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" @@ -4519,6 +4916,20 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A== + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4546,7 +4957,7 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-buffer@~1.1.6: +is-buffer@^1.1.5, 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== @@ -4582,6 +4993,20 @@ is-core-module@^2.9.0: 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" + integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg== + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + 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" @@ -4589,6 +5014,43 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-even@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-even/-/is-even-1.0.0.tgz#76b5055fbad8d294a86b6a949015e1c97b717c06" + integrity sha512-LEhnkAdJqic4Dbqn58A0y52IXoHWlsueqQkKfMfdEnIYG8A1sm/GHidKkS6yvXlMoRrkM34csHnXQtOqcb+Jzg== + dependencies: + is-odd "^0.1.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -4673,6 +5135,20 @@ is-number-object@^1.0.4: dependencies: has-tostringtag "^1.0.0" +is-number@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg== + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg== + dependencies: + kind-of "^3.0.2" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -4683,11 +5159,25 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-odd@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-0.1.2.tgz#bc573b5ce371ef2aad6e6f49799b72bef13978a7" + integrity sha512-Ri7C2K7o5IrUU9UEI8losXJCCD/UtsaIrkR5sxIcFg4xQ9cRJXlWA5DQvTE0yDc0krvSNLsRGXN11UPS6KyfBw== + dependencies: + is-number "^3.0.0" + is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -4701,6 +5191,13 @@ is-retry-allowed@^2.2.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== +is-self-closing@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-self-closing/-/is-self-closing-1.0.1.tgz#5f406b527c7b12610176320338af0fa3896416e4" + integrity sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg== + dependencies: + self-closing-tags "^1.0.1" + 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" @@ -4753,6 +5250,11 @@ is-windows@^0.2.0: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" integrity sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q== +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + 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" @@ -4763,7 +5265,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@^1.0.0, isarray@~1.0.0: +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== @@ -4773,6 +5275,23 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isobject@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-0.2.0.tgz#a3432192f39b910b5f02cc989487836ec70aa85e" + integrity sha512-VaWq6XYAsbvM0wf4dyBO7WH9D7GosB7ZZlqrawI9BBiTMINBeCyqSKBa35m870MY3O4aM31pYyZi9DfGrYMJrQ== + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA== + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -5383,6 +5902,30 @@ keyv@^4.0.0: compress-brotli "^1.3.8" json-buffer "3.0.1" +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.1.0, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0, kind-of@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -5467,6 +6010,13 @@ koa-static@5.0.0: debug "^3.1.0" koa-send "^5.0.0" +koa-useragent@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-useragent/-/koa-useragent-4.1.0.tgz#d3f128b552c6da3e5e9e9e9c887b2922b16e4468" + integrity sha512-x/HUDZ1zAmNNh5hA9hHbPm9p3UVg2prlpHzxCXQCzbibrNS0kmj7MkCResCbAbG7ZT6FVxNSMjR94ZGamdMwxA== + dependencies: + express-useragent "^1.0.15" + koa@2.13.4, koa@^2.13.4: version "2.13.4" resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e" @@ -5639,6 +6189,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -5709,12 +6264,27 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== +lodash.template@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5793,6 +6363,18 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w== + 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" @@ -5849,6 +6431,11 @@ memdown@1.4.1: ltgt "~2.2.0" safe-buffer "~5.1.1" +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + merge-descriptors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -5869,6 +6456,25 @@ methods@^1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromatch@^3.1.5: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -5926,6 +6532,11 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass@^3.0.0: version "3.1.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" @@ -5941,6 +6552,14 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" @@ -5973,6 +6592,11 @@ mri@1.1.4: resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -6014,6 +6638,23 @@ nan@^2.15.0, nan@^2.16.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + napi-macros@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" @@ -6039,6 +6680,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +neo-async@^2.6.0: + version "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" @@ -6222,6 +6868,15 @@ 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-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ== + dependencies: + copy-descriptor "^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" @@ -6244,6 +6899,13 @@ 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-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA== + dependencies: + isobject "^3.0.0" + object.assign@^4.1.2: version "4.1.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" @@ -6263,6 +6925,13 @@ object.entries@^1.0.4, object.entries@^1.1.0: define-properties "^1.1.3" es-abstract "^1.19.1" +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== + dependencies: + isobject "^3.0.1" + on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -6418,6 +7087,11 @@ parseurl@^1.3.2, parseurl@^1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== + passport-google-oauth1@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz#af74a803df51ec646f66a44d82282be6f108e0cc" @@ -6622,6 +7296,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== + posthog-node@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-1.3.0.tgz#804ed2f213a2f05253f798bf9569d55a9cad94f7" @@ -7158,6 +7837,14 @@ 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== +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + 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" @@ -7191,11 +7878,36 @@ relative-microtime@^2.0.0: resolved "https://registry.yarnpkg.com/relative-microtime/-/relative-microtime-2.0.0.tgz#cceed2af095ecd72ea32011279c79e5fcc7de29b" integrity sha512-l18ha6HEZc+No/uK4GyAnNxgKW7nvEe35IaeN54sShMojtqik2a6GbTyuiezkjpPaqP874Z3lW5ysBo5irz4NA== +relative@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" + integrity sha512-Q5W2qeYtY9GbiR8z1yHNZ1DGhyjb4AnLEjt8iE6XfcC1QIu+FAtj3HQaO0wH28H1mX6cqNLvAqWhP402dxJGyA== + dependencies: + isobject "^2.0.0" + +remarkable@^1.6.2: + version "1.7.4" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00" + integrity sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg== + dependencies: + argparse "^1.0.10" + autolinker "~0.28.0" + 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" integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -7279,6 +7991,11 @@ resolve-path@^1.4.0: http-errors "~1.6.2" path-is-absolute "1.0.1" +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== + resolve.exports@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" @@ -7324,6 +8041,11 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + retry@^0.10.1: version "0.10.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" @@ -7394,6 +8116,13 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg== + dependencies: + ret "~0.1.10" + "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" @@ -7414,6 +8143,11 @@ sax@>=0.1.1, sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +self-closing-tags@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/self-closing-tags/-/self-closing-tags-1.0.1.tgz#6c5fa497994bb826b484216916371accee490a5d" + integrity sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -7472,6 +8206,16 @@ set-cookie-serde@^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" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -7549,6 +8293,36 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + sonic-boom@^1.0.2: version "1.4.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" @@ -7557,6 +8331,17 @@ sonic-boom@^1.0.2: atomic-sleep "^1.0.0" flatstr "^1.0.12" +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -7565,6 +8350,11 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + source-map@^0.4.2: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" @@ -7572,6 +8362,11 @@ source-map@^0.4.2: dependencies: amdefine ">=0.0.4" +source-map@^0.5.6, source-map@~0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -7589,11 +8384,6 @@ source-map@^0.8.0-beta.0: 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" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - spark-md5@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.1.tgz#83a0e255734f2ab4e5c466e5a2cfc9ba2aa2124d" @@ -7609,6 +8399,13 @@ split-ca@^1.0.1: resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + split2@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" @@ -7688,6 +8485,14 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g== + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + statuses@2.0.1, statuses@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -7824,6 +8629,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +striptags@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052" + integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw== + sublevel-pouchdb@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/sublevel-pouchdb/-/sublevel-pouchdb-7.2.2.tgz#49e46cd37883bf7ff5006d7c5b9bcc7bcc1f422f" @@ -8033,11 +8843,31 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +to-gfm-code-block@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz#25d045a5fae553189e9637b590900da732d8aa82" + integrity sha512-LQRZWyn8d5amUKnfR9A9Uu7x9ss7Re8peuWR2gkh1E+ildOfv2aF26JpuDg8JtvCduu5+hOrMIH+XstZtnagqg== + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg== + dependencies: + kind-of "^3.0.2" + to-readable-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -8045,6 +8875,16 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^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" @@ -8226,11 +9066,23 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typeof-article@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/typeof-article/-/typeof-article-0.1.1.tgz#9f07e733c3fbb646ffa9e61c08debacd460e06af" + integrity sha512-Vn42zdX3FhmUrzEmitX3iYyLb+Umwpmv8fkZRIknYh84lmdrwqZA5xYaoKiIj2Rc5i/5wcDrpUmZcbk1U51vTw== + dependencies: + kind-of "^3.1.0" + typescript@4.7.3: version "4.7.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== +uglify-js@^3.1.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + uid2@0.0.x: version "0.0.4" resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" @@ -8264,6 +9116,16 @@ unicode-substring@^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" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" @@ -8286,6 +9148,14 @@ unpipe@1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ== + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" @@ -8336,6 +9206,11 @@ urijs@^1.19.2: resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc" integrity sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ== +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== + url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" @@ -8359,6 +9234,11 @@ url@0.10.3: punycode "1.3.2" querystring "0.2.0" +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -8422,6 +9302,14 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vm2@^3.9.4: + version "3.9.14" + resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.14.tgz#964042b474cf1e6e4f475a39144773cdb9deb734" + integrity sha512-HgvPHYHeQy8+QhzlFryvSteA4uQLBCOub02mgqdR+0bN/akRZ48TGB1v0aCv7ksyc0HXx16AZtMHKS38alc6TA== + dependencies: + acorn "^8.7.0" + acorn-walk "^8.2.0" + vuvuzela@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/vuvuzela/-/vuvuzela-1.0.3.tgz#3be145e58271c73ca55279dd851f12a682114b0b" @@ -8513,6 +9401,11 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -8645,6 +9538,11 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +year@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/year/-/year-0.2.1.tgz#4083ae520a318b23ec86037f3000cb892bdf9bb0" + integrity sha512-9GnJUZ0QM4OgXuOzsKNzTJ5EOkums1Xc+3YQXp+Q+UxFjf7zLucp9dQ8QMIft0Szs1E1hUiXFim1OYfEKFq97w== + ylru@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785" diff --git a/scripts/link-dependencies.sh b/scripts/link-dependencies.sh index d2a501162b..9926f3dd2b 100755 --- a/scripts/link-dependencies.sh +++ b/scripts/link-dependencies.sh @@ -44,6 +44,9 @@ if [ -d "../budibase-pro" ]; then echo "Linking types to pro" yarn link '@budibase/types' + echo "Linking string-templates to pro" + yarn link '@budibase/string-templates' + cd ../../../budibase echo "Linking pro to worker" diff --git a/yarn.lock b/yarn.lock index f80cde1b6f..bcd33ad2b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1064,6 +1064,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== +"@typescript-eslint/types@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" + integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== + "@typescript-eslint/typescript-estree@5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" @@ -1090,6 +1095,19 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@^5.13.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz#bc651dc28cf18ab248ecd18a4c886c744aebd690" + integrity sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w== + dependencies: + "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/visitor-keys" "5.53.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/visitor-keys@4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" @@ -1106,6 +1124,14 @@ "@typescript-eslint/types" "5.45.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.53.0": + version "5.53.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" + integrity sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w== + dependencies: + "@typescript-eslint/types" "5.53.0" + eslint-visitor-keys "^3.3.0" + JSONStream@^1.0.4, JSONStream@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -1214,6 +1240,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +any-promise@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + app-module-path@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5" @@ -1793,6 +1824,11 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commander@^9.1.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2139,16 +2175,16 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -dependency-tree@^8.1.1: - version "8.1.2" - resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-8.1.2.tgz#c9e652984f53bd0239bc8a3e50cbd52f05b2e770" - integrity sha512-c4CL1IKxkKng0oT5xrg4uNiiMVFqTGOXqHSFx7XEFdgSsp6nw3AGGruICppzJUrfad/r7GLqt26rmWU4h4j39A== +dependency-tree@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-9.0.0.tgz#9288dd6daf35f6510c1ea30d9894b75369aa50a2" + integrity sha512-osYHZJ1fBSon3lNLw70amAXsQ+RGzXsPvk9HbBgTLbp/bQBmpH5mOmsUvqXU+YEWVU0ZLewsmzOET/8jWswjDQ== dependencies: commander "^2.20.3" debug "^4.3.1" filing-cabinet "^3.0.1" - precinct "^8.0.0" - typescript "^3.9.7" + precinct "^9.0.0" + typescript "^4.0.0" deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" @@ -2170,6 +2206,16 @@ detective-amd@^3.1.0: get-amd-module-type "^3.0.0" node-source-walk "^4.2.0" +detective-amd@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/detective-amd/-/detective-amd-4.0.1.tgz#1a827d9e4fa2f832506bd87aa392f124155bca3a" + integrity sha512-bDo22IYbJ8yzALB0Ow5CQLtyhU1BpDksLB9dsWHI9Eh0N3OQR6aQqhjPsNDd69ncYwRfL1sTo7OA9T3VRVSe2Q== + dependencies: + ast-module-types "^3.0.0" + escodegen "^2.0.0" + get-amd-module-type "^4.0.0" + node-source-walk "^5.0.0" + detective-cjs@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/detective-cjs/-/detective-cjs-3.1.3.tgz#50e107d67b37f459b0ec02966ceb7e20a73f268b" @@ -2178,13 +2224,28 @@ detective-cjs@^3.1.1: ast-module-types "^3.0.0" node-source-walk "^4.0.0" -detective-es6@^2.2.0, detective-es6@^2.2.1: +detective-cjs@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detective-cjs/-/detective-cjs-4.0.0.tgz#316e2c7ae14276a5dcfe0c43dc05d8cf9a0e5cf9" + integrity sha512-VsD6Yo1+1xgxJWoeDRyut7eqZ8EWaJI70C5eanSAPcBHzenHZx0uhjxaaEfIm0cHII7dBiwU98Orh44bwXN2jg== + dependencies: + ast-module-types "^3.0.0" + node-source-walk "^5.0.0" + +detective-es6@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-2.2.2.tgz#ee5f880981d9fecae9a694007029a2f6f26d8d28" integrity sha512-eZUKCUsbHm8xoeoCM0z6JFwvDfJ5Ww5HANo+jPR7AzkFpW9Mun3t/TqIF2jjeWa2TFbAiGaWESykf2OQp3oeMw== dependencies: node-source-walk "^4.0.0" +detective-es6@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-3.0.0.tgz#78c9ef5492d5b59748b411aecaaf52b5ca0f0bc2" + integrity sha512-Uv2b5Uih7vorYlqGzCX+nTPUb4CMzUAn3VPHTV5p5lBkAN4cAApLGgUz4mZE2sXlBfv4/LMmeP7qzxHV/ZcfWA== + dependencies: + node-source-walk "^5.0.0" + detective-less@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/detective-less/-/detective-less-1.0.2.tgz#a68af9ca5f69d74b7d0aa190218b211d83b4f7e3" @@ -2204,14 +2265,14 @@ detective-postcss@^4.0.0: postcss "^8.1.7" postcss-values-parser "^2.0.1" -detective-postcss@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/detective-postcss/-/detective-postcss-5.1.1.tgz#ec23ac3818f8be95ac3a38a8b9f3b6d43103ef87" - integrity sha512-YJMsvA0Y6/ST9abMNcQytl9iFQ2bfu4I7B74IUiAvyThfaI9Y666yipL+SrqfReoIekeIEwmGH72oeqX63mwUw== +detective-postcss@^6.0.1, detective-postcss@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detective-postcss/-/detective-postcss-6.1.0.tgz#01ca6f83dbc3158540fb0e6a6c631f3998946e54" + integrity sha512-ZFZnEmUrL2XHAC0j/4D1fdwZbo/anAcK84soJh7qc7xfx2Kc8gFO5Bk5I9jU7NLC/OAF1Yho1GLxEDnmQnRH2A== dependencies: is-url "^1.2.4" - postcss "^8.4.6" - postcss-values-parser "^5.0.0" + postcss "^8.4.12" + postcss-values-parser "^6.0.2" detective-sass@^3.0.1: version "3.0.2" @@ -2221,6 +2282,14 @@ detective-sass@^3.0.1: gonzales-pe "^4.3.0" node-source-walk "^4.0.0" +detective-sass@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/detective-sass/-/detective-sass-4.0.1.tgz#2a9e303a0bd472d00aaa79512334845e3acbaffc" + integrity sha512-80zfpxux1krOrkxCHbtwvIs2gNHUBScnSqlGl0FvUuHVz8HD6vD2ov66OroMctyvzhM67fxhuEeVjIk18s6yTQ== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^5.0.0" + detective-scss@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/detective-scss/-/detective-scss-2.0.2.tgz#7d2a642616d44bf677963484fa8754d9558b8235" @@ -2229,11 +2298,24 @@ detective-scss@^2.0.1: gonzales-pe "^4.3.0" node-source-walk "^4.0.0" +detective-scss@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/detective-scss/-/detective-scss-3.0.0.tgz#c3c7bc4799f51515a4f0ed1e8ca491151364230f" + integrity sha512-37MB/mhJyS45ngqfzd6eTbuLMoDgdZnH03ZOMW2m9WqJ/Rlbuc8kZAr0Ypovaf1DJiTRzy5mmxzOTja85jbzlA== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^5.0.0" + detective-stylus@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/detective-stylus/-/detective-stylus-1.0.3.tgz#20a702936c9fd7d4203fd7a903314b5dd43ac713" integrity sha512-4/bfIU5kqjwugymoxLXXLltzQNeQfxGoLm2eIaqtnkWxqbhap9puDVpJPVDx96hnptdERzS5Cy6p9N8/08A69Q== +detective-stylus@^2.0.0, detective-stylus@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detective-stylus/-/detective-stylus-2.0.1.tgz#d528dfa7ef3c4eb2fbc9a7249d54906ec4e05d09" + integrity sha512-/Tvs1pWLg8eYwwV6kZQY5IslGaYqc/GACxjcaGudiNtN5nKCH6o2WnJK3j0gA3huCnoQcbv8X7oz/c1lnvE3zQ== + detective-typescript@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/detective-typescript/-/detective-typescript-7.0.2.tgz#c6e00b4c28764741ef719662250e6b014a5f3c8e" @@ -2244,6 +2326,16 @@ detective-typescript@^7.0.0: node-source-walk "^4.2.0" typescript "^3.9.10" +detective-typescript@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/detective-typescript/-/detective-typescript-9.0.0.tgz#57d674cec49ec775460ab975b5bcbb5c2d32ff8e" + integrity sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA== + dependencies: + "@typescript-eslint/typescript-estree" "^5.13.0" + ast-module-types "^3.0.0" + node-source-walk "^5.0.0" + typescript "^4.5.5" + dezalgo@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -2937,6 +3029,14 @@ get-amd-module-type@^3.0.0: ast-module-types "^3.0.0" node-source-walk "^4.2.2" +get-amd-module-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-4.0.0.tgz#3d4e5b44eec81f8337157d7c52c4fa9389aff78b" + integrity sha512-GbBawUCuA2tY8ztiMiVo3e3P95gc2TVrfYFfpUHdHQA8WyxMCckK29bQsVKhYX8SUf+w6JLhL2LG8tSC0ANt9Q== + dependencies: + ast-module-types "^3.0.0" + node-source-walk "^5.0.0" + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -3148,13 +3248,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graphviz@0.0.9: - version "0.0.9" - resolved "https://registry.yarnpkg.com/graphviz/-/graphviz-0.0.9.tgz#0bbf1df588c6a92259282da35323622528c4bbc4" - integrity sha512-SmoY2pOtcikmMCqCSy2NO1YsRfu9OO0wpTlOYW++giGjfX1a6gax/m1Fo8IdUd0/3H15cTOfR1SMKwohj4LKsg== - dependencies: - temp "~0.4.0" - handlebars@^4.7.6: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -4167,31 +4260,32 @@ macos-release@^2.2.0: resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2" integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g== -madge@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/madge/-/madge-5.0.1.tgz#2096d9006558ea0669b3ade89c2cda708a24e22b" - integrity sha512-krmSWL9Hkgub74bOjnjWRoFPAJvPwSG6Dbta06qhWOq6X/n/FPzO3ESZvbFYVIvG2g4UHXvCJN1b+RZLaSs9nA== +madge@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/madge/-/madge-6.0.0.tgz#d17d68d1023376318cae89abd16629d329f4ed0a" + integrity sha512-dddxP62sj5pL+l9MJnq9C34VFqmRj+2+uSOdn/7lOTSliLRH0WyQ8uCEF3VxkPRNOBvMKK2xumnIE15HRSAL9A== dependencies: chalk "^4.1.1" commander "^7.2.0" commondir "^1.0.1" debug "^4.3.1" - dependency-tree "^8.1.1" - detective-amd "^3.1.0" - detective-cjs "^3.1.1" - detective-es6 "^2.2.0" + dependency-tree "^9.0.0" + detective-amd "^4.0.1" + detective-cjs "^4.0.0" + detective-es6 "^3.0.0" detective-less "^1.0.2" - detective-postcss "^5.0.0" - detective-sass "^3.0.1" - detective-scss "^2.0.1" - detective-stylus "^1.0.0" - detective-typescript "^7.0.0" - graphviz "0.0.9" + detective-postcss "^6.1.0" + detective-sass "^4.0.1" + detective-scss "^3.0.0" + detective-stylus "^2.0.1" + detective-typescript "^9.0.0" ora "^5.4.1" pluralize "^8.0.0" precinct "^8.1.0" pretty-ms "^7.0.1" rc "^1.2.7" + stream-to-array "^2.3.0" + ts-graphviz "^1.5.0" typescript "^3.9.5" walkdir "^0.4.1" @@ -4481,6 +4575,14 @@ module-definition@^3.3.1: ast-module-types "^3.0.0" node-source-walk "^4.0.0" +module-definition@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-4.0.0.tgz#5b39cca9be28c5b0fec768eb2d9fd8de08a2550b" + integrity sha512-wntiAHV4lDn24BQn2kX6LKq0y85phHLHiv3aOPDF+lIs06kVjEMTe/ZTdrbVLnQV5FQsjik21taknvMhKY1Cug== + dependencies: + ast-module-types "^3.0.0" + node-source-walk "^5.0.0" + module-lookup-amd@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/module-lookup-amd/-/module-lookup-amd-7.0.1.tgz#d67c1a93f2ff8e38b8774b99a638e9a4395774b2" @@ -4616,6 +4718,13 @@ node-source-walk@^4.0.0, node-source-walk@^4.2.0, node-source-walk@^4.2.2: dependencies: "@babel/parser" "^7.0.0" +node-source-walk@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-5.0.0.tgz#7cf93a0d12408081531fc440a00d7019eb3d5665" + integrity sha512-58APXoMXpmmU+oVBJFajhTCoD8d/OGtngnVAWzIo2A8yn0IXwBzvIVIsTzoie/SrA37u+1hnpNz2HMWx/VIqlw== + dependencies: + "@babel/parser" "^7.0.0" + "nopt@2 || 3": version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -5246,16 +5355,16 @@ postcss-values-parser@^2.0.1: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-values-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-5.0.0.tgz#10c61ac3f488e4de25746b829ea8d8894e9ac3d2" - integrity sha512-2viDDjMMrt21W2izbeiJxl3kFuD/+asgB0CBwPEgSyhCmBnDIa/y+pLaoyX+q3I3DHH0oPPL3cgjVTQvlS1Maw== +postcss-values-parser@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz#636edc5b86c953896f1bb0d7a7a6615df00fb76f" + integrity sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw== dependencies: color-name "^1.1.4" is-url-superb "^4.0.0" quote-unquote "^1.0.0" -postcss@^8.1.7, postcss@^8.4.6: +postcss@^8.1.7: version "8.4.16" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== @@ -5264,7 +5373,16 @@ postcss@^8.1.7, postcss@^8.4.6: picocolors "^1.0.0" source-map-js "^1.0.2" -precinct@^8.0.0, precinct@^8.1.0: +postcss@^8.4.12: + version "8.4.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +precinct@^8.1.0: version "8.3.1" resolved "https://registry.yarnpkg.com/precinct/-/precinct-8.3.1.tgz#94b99b623df144eed1ce40e0801c86078466f0dc" integrity sha512-pVppfMWLp2wF68rwHqBIpPBYY8Kd12lDhk8LVQzOwqllifVR15qNFyod43YLyFpurKRZQKnE7E4pofAagDOm2Q== @@ -5283,6 +5401,24 @@ precinct@^8.0.0, precinct@^8.1.0: module-definition "^3.3.1" node-source-walk "^4.2.0" +precinct@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/precinct/-/precinct-9.0.1.tgz#64e7ea0de4bea1b73572e50531dfe297c7b8a7c7" + integrity sha512-hVNS6JvfvlZ64B3ezKeGAcVhIuOvuAiSVzagHX/+KjVPkYWoCNkfyMgCl1bjDtAFQSlzi95NcS9ykUWrl1L1vA== + dependencies: + commander "^9.1.0" + detective-amd "^4.0.1" + detective-cjs "^4.0.0" + detective-es6 "^3.0.0" + detective-less "^1.0.2" + detective-postcss "^6.0.1" + detective-sass "^4.0.1" + detective-scss "^3.0.0" + detective-stylus "^2.0.0" + detective-typescript "^9.0.0" + module-definition "^4.0.0" + node-source-walk "^5.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -6168,6 +6304,13 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +stream-to-array@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" + integrity sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA== + dependencies: + any-promise "^1.1.0" + strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" @@ -6401,11 +6544,6 @@ temp-write@^3.4.0: temp-dir "^1.0.0" uuid "^3.0.1" -temp@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/temp/-/temp-0.4.0.tgz#671ad63d57be0fe9d7294664b3fc400636678a60" - integrity sha512-IsFisGgDKk7qzK9erMIkQe/XwiSUdac7z3wYOsjcLkhPBy3k1SlvLoIh2dAHIlEpgA971CgguMrx9z8fFg7tSA== - text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" @@ -6523,6 +6661,11 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +ts-graphviz@^1.5.0: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.5.4.tgz#61a3059afeac4f6d4be3c6729a4d88546ca9e095" + integrity sha512-oxhI6wfPQBJC8WSiP0AjDI8z+9hcc9yl1etaynQJmQ2Yivn0KlT0oImLzJct7Es0TR/6xphzvJBAhGzgOjTGmQ== + tsconfig-paths@^3.10.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -6606,6 +6749,11 @@ typescript@^3.9.10, typescript@^3.9.5, typescript@^3.9.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== +typescript@^4.0.0, typescript@^4.5.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + uglify-js@^3.1.4: version "3.16.1" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.1.tgz#0e7ec928b3d0b1e1d952bce634c384fd56377317"