diff --git a/.eslintignore b/.eslintignore index 579bd55947..1dac74b117 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,6 +3,8 @@ public dist packages/server/builder packages/server/coverage +packages/worker/coverage +packages/backend-core/coverage packages/server/client packages/builder/.routify packages/builder/cypress/support/queryLevelTransformerFunction.js diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 475bd4f66a..c07f9b2c28 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -58,7 +58,7 @@ jobs: - uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: ./packages/server/coverage/clover.xml + files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml name: codecov-umbrella verbose: true diff --git a/.gitignore b/.gitignore index e1d3e6db0e..654b483288 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ stats.html *.tsbuildinfo budibase-component budibase-datasource + +*.iml \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 3a381d255e..7eb567d517 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,8 @@ dist packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte packages/server/builder packages/server/coverage +packages/worker/coverage +packages/backend-core/coverage packages/server/client packages/server/src/definitions/openapi.ts packages/builder/.routify diff --git a/.vscode/settings.json b/.vscode/settings.json index 4838a4fd89..71f0092a59 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "editor.defaultFormatter": "vscode.json-language-features" }, "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "debug.javascript.terminalOptions": { "skipFiles": [ @@ -16,4 +16,7 @@ "/**" ] }, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, } diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml new file mode 100644 index 0000000000..304b2bc79b --- /dev/null +++ b/artifacthub-repo.yml @@ -0,0 +1,10 @@ +# Artifact Hub repository metadata file +# This file is used to verify ownership of the budibase Helm chart repo +# so that we appear as a verified owner on artifacthub.io + +repositoryID: a7536764-e72e-4961-87d8-efe7c8dedfa3 +owners: # (optional, used to claim repository ownership) + - name: Martin + email: budimaster@budibase.com + - name: DevOps + email: devops@budibase.com \ No newline at end of file diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index d71ee6e178..a3e4790430 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -4,6 +4,9 @@ metadata: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) +{{ if .Values.globals.logAnnotations }} +{{ toYaml .Values.globals.logAnnotations | indent 4 }} +{{ end }} creationTimestamp: null labels: io.kompose.service: app-service @@ -60,8 +63,6 @@ spec: secretKeyRef: name: {{ template "budibase.fullname" . }} key: jwtSecret - - name: LOG_LEVEL - value: {{ .Values.services.apps.logLevel | default "info" | quote }} {{ if .Values.services.objectStore.region }} - name: AWS_REGION value: {{ .Values.services.objectStore.region }} @@ -84,6 +85,8 @@ spec: value: {{ .Values.services.objectStore.appsBucketName | quote }} - name: GLOBAL_CLOUD_BUCKET_NAME value: {{ .Values.services.objectStore.globalBucketName | quote }} + - name: BACKUPS_BUCKET_NAME + value: {{ .Values.services.objectStore.backupsBucketName | quote }} - name: PORT value: {{ .Values.services.apps.port | quote }} {{ if .Values.services.worker.publicApiRateLimitPerSecond }} @@ -156,6 +159,24 @@ spec: - name: ELASTIC_APM_SERVER_URL value: {{ .Values.globals.elasticApmServerUrl | quote }} {{ end }} + {{ if .Values.globals.globalAgentHttpProxy }} + - name: GLOBAL_AGENT_HTTP_PROXY + value: {{ .Values.globals.globalAgentHttpProxy | quote }} + {{ end }} + {{ if .Values.globals.globalAgentHttpsProxy }} + - name: GLOBAL_AGENT_HTTPS_PROXY + value: {{ .Values.globals.globalAgentHttpsProxy | quote }} + {{ end }} + {{ if .Values.globals.globalAgentNoProxy }} + - name: GLOBAL_AGENT_NO_PROXY + value: {{ .Values.globals.globalAgentNoProxy | quote }} + {{ end }} + - name: CDN_URL + value: {{ .Values.globals.cdnUrl }} + {{ if .Values.services.tlsRejectUnauthorized }} + - name: NODE_TLS_REJECT_UNAUTHORIZED + value: {{ .Values.services.tlsRejectUnauthorized }} + {{ end }} image: budibase/apps:{{ .Values.globals.appVersion }} imagePullPolicy: Always diff --git a/charts/budibase/templates/minio-service-deployment.yaml b/charts/budibase/templates/minio-service-deployment.yaml index 144dbe539a..d0a367653d 100644 --- a/charts/budibase/templates/minio-service-deployment.yaml +++ b/charts/budibase/templates/minio-service-deployment.yaml @@ -42,6 +42,7 @@ spec: secretKeyRef: name: {{ template "budibase.fullname" . }} key: objectStoreSecret + image: minio/minio imagePullPolicy: "" livenessProbe: diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index 5588022032..3cde7a2388 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -4,6 +4,9 @@ metadata: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) +{{ if .Values.globals.logAnnotations }} +{{ toYaml .Values.globals.logAnnotations | indent 4 }} +{{ end }} creationTimestamp: null labels: app.kubernetes.io/name: budibase-proxy diff --git a/charts/budibase/templates/redis-service-deployment.yaml b/charts/budibase/templates/redis-service-deployment.yaml index d94e4d70f8..5916c6d3f9 100644 --- a/charts/budibase/templates/redis-service-deployment.yaml +++ b/charts/budibase/templates/redis-service-deployment.yaml @@ -60,5 +60,6 @@ spec: - name: redis-data persistentVolumeClaim: claimName: redis-data + status: {} {{- end }} diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index ffcda1ab72..44bbb8aa20 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -4,6 +4,9 @@ metadata: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) +{{ if .Values.globals.logAnnotations }} +{{ toYaml .Values.globals.logAnnotations | indent 4 }} +{{ end }} creationTimestamp: null labels: io.kompose.service: worker-service @@ -83,6 +86,8 @@ spec: value: {{ .Values.services.objectStore.appsBucketName | quote }} - name: GLOBAL_CLOUD_BUCKET_NAME value: {{ .Values.services.objectStore.globalBucketName | quote }} + - name: BACKUPS_BUCKET_NAME + value: {{ .Values.services.objectStore.backupsBucketName | quote }} - name: PORT value: {{ .Values.services.worker.port | quote }} - name: MULTI_TENANCY @@ -145,6 +150,24 @@ spec: - name: ELASTIC_APM_SERVER_URL value: {{ .Values.globals.elasticApmServerUrl | quote }} {{ end }} + {{ if .Values.globals.globalAgentHttpProxy }} + - name: GLOBAL_AGENT_HTTP_PROXY + value: {{ .Values.globals.globalAgentHttpProxy | quote }} + {{ end }} + {{ if .Values.globals.globalAgentHttpsProxy }} + - name: GLOBAL_AGENT_HTTPS_PROXY + value: {{ .Values.globals.globalAgentHttpsProxy | quote }} + {{ end }} + {{ if .Values.globals.globalAgentNoProxy }} + - name: GLOBAL_AGENT_NO_PROXY + value: {{ .Values.globals.globalAgentNoProxy | quote }} + {{ end }} + - name: CDN_URL + value: {{ .Values.globals.cdnUrl }} + {{ if .Values.services.tlsRejectUnauthorized }} + - name: NODE_TLS_REJECT_UNAUTHORIZED + value: {{ .Values.services.tlsRejectUnauthorized }} + {{ end }} image: budibase/worker:{{ .Values.globals.appVersion }} imagePullPolicy: Always diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 5c4004cb57..726df7585b 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -22,6 +22,12 @@ serviceAccount: podAnnotations: {} +# logAnnotations: +# co.elastic.logs/multiline.type: pattern +# co.elastic.logs/multiline.pattern: '^[[:space:]]' +# co.elastic.logs/multiline.negate: false +# co.elastic.logs/multiline.match: after + podSecurityContext: {} # fsGroup: 2000 @@ -98,6 +104,7 @@ globals: # if createSecrets is set to false, you can hard-code your secrets here internalApiKey: "" jwtSecret: "" + cdnUrl: "" smtp: enabled: false @@ -105,10 +112,14 @@ globals: # elasticApmEnabled: # elasticApmSecretToken: # elasticApmServerUrl: +# globalAgentHttpProxy: +# globalAgentHttpsProxy: +# globalAgentNoProxy: services: budibaseVersion: latest dns: cluster.local + # tlsRejectUnauthorized: 0 proxy: port: 10000 diff --git a/hosting/nginx.dev.conf.hbs b/hosting/nginx.dev.conf.hbs index 39a8dc52af..93a07435e5 100644 --- a/hosting/nginx.dev.conf.hbs +++ b/hosting/nginx.dev.conf.hbs @@ -58,71 +58,101 @@ http { } location ~ ^/api/(system|admin|global)/ { - proxy_pass http://worker-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header Connection ""; + + proxy_pass http://worker-service; + } + + location /api/backups/ { + proxy_read_timeout 1800s; + proxy_connect_timeout 1800s; + proxy_send_timeout 1800s; + proxy_pass http://app-service; + proxy_http_version 1.1; proxy_set_header Connection ""; } location /api/ { proxy_read_timeout 120s; proxy_connect_timeout 120s; - proxy_send_timeout 120s; - proxy_pass http://app-service; + proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location = / { - proxy_pass http://app-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location /app_ { - proxy_pass http://app-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location /app { - proxy_pass http://app-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location /builder { - proxy_pass http://builder; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://builder; rewrite ^/builder(.*)$ /builder/$1 break; } location /builder/ { - proxy_pass http://builder; - proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; + + proxy_pass http://builder; } location /vite/ { diff --git a/hosting/nginx.prod.conf.hbs b/hosting/nginx.prod.conf.hbs index f3202ad4a4..6f0f1b420d 100644 --- a/hosting/nginx.prod.conf.hbs +++ b/hosting/nginx.prod.conf.hbs @@ -51,11 +51,11 @@ http { proxy_buffering off; set $csp_default "default-src 'self'"; - set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io"; + set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io"; set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_object "object-src 'none'"; set $csp_base_uri "base-uri 'self'"; - set $csp_connect "connect-src 'self' https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com"; + set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_frame "frame-src 'self' https:"; set $csp_img "img-src http: https: data: blob:"; @@ -100,22 +100,47 @@ http { location ~ ^/(builder|app_) { proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://$apps:4002; } location ~ ^/api/(system|admin|global)/ { + proxy_set_header Host $host; + proxy_pass http://$worker:4003; } location /worker/ { + proxy_set_header Host $host; + proxy_pass http://$worker:4003; rewrite ^/worker/(.*)$ /$1 break; } + location /api/backups/ { + # calls to export apps are limited + limit_req zone=ratelimit burst=20 nodelay; + + # 1800s timeout for app export requests + proxy_read_timeout 1800s; + proxy_connect_timeout 1800s; + proxy_send_timeout 1800s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://$apps:4002; + } + location /api/ { # calls to the API are rate limited with bursting limit_req zone=ratelimit burst=20 nodelay; @@ -130,6 +155,7 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; proxy_pass http://$apps:4002; } @@ -149,6 +175,7 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; proxy_pass http://$apps:4002; } @@ -171,11 +198,13 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; proxy_connect_timeout 300; proxy_http_version 1.1; proxy_set_header Connection ""; chunked_transfer_encoding off; + proxy_pass http://$minio:9000; } diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index 58796f0362..5127db9897 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -61,7 +61,8 @@ ADD hosting/single/nginx/nginx.conf /etc/nginx ADD hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default RUN mkdir -p /var/log/nginx && \ touch /var/log/nginx/error.log && \ - touch /var/run/nginx.pid + touch /var/run/nginx.pid && \ + usermod -a -G tty www-data WORKDIR / RUN mkdir -p scripts/integrations/oracle diff --git a/hosting/single/nginx/nginx-default-site.conf b/hosting/single/nginx/nginx-default-site.conf index bd89e21251..acadb06250 100644 --- a/hosting/single/nginx/nginx-default-site.conf +++ b/hosting/single/nginx/nginx-default-site.conf @@ -2,7 +2,8 @@ server { listen 80 default_server; listen [::]:80 default_server; server_name _; - + error_log /dev/stderr warn; + access_log /dev/stdout main; client_max_body_size 1000m; ignore_invalid_headers off; proxy_buffering off; @@ -43,6 +44,24 @@ server { rewrite ^/worker/(.*)$ /$1 break; } + location /api/backups/ { + # calls to export apps are limited + limit_req zone=ratelimit burst=20 nodelay; + + # 1800s timeout for app export requests + proxy_read_timeout 1800s; + proxy_connect_timeout 1800s; + proxy_send_timeout 1800s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://127.0.0.1:4001; + } + location /api/ { # calls to the API are rate limited with bursting limit_req zone=ratelimit burst=20 nodelay; diff --git a/hosting/single/nginx/nginx.conf b/hosting/single/nginx/nginx.conf index 1e5d1c20d2..c86e3877f3 100644 --- a/hosting/single/nginx/nginx.conf +++ b/hosting/single/nginx/nginx.conf @@ -1,5 +1,5 @@ user www-data www-data; -error_log /var/log/nginx/error.log; +error_log /dev/stderr warn; pid /var/run/nginx.pid; worker_processes auto; worker_rlimit_nofile 8192; diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index a95c21a98f..ea825131db 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -27,12 +27,14 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then else DATA_DIR=${DATA_DIR:-/data} fi - +mkdir -p ${DATA_DIR} # Mount NFS or GCP Filestore if env vars exist for it -if [[ -z ${FILESHARE_IP} && -z ${FILESHARE_NAME} ]]; then +if [[ ! -z ${FILESHARE_IP} && ! -z ${FILESHARE_NAME} ]]; then + echo "Mounting NFS share" + apt update && apt install -y nfs-common nfs-kernel-server echo "Mount file share ${FILESHARE_IP}:/${FILESHARE_NAME} to ${DATA_DIR}" mount -o nolock ${FILESHARE_IP}:/${FILESHARE_NAME} ${DATA_DIR} - echo "Mounting completed." + echo "Mounting result: $?" fi if [ -f "${DATA_DIR}/.env" ]; then @@ -74,9 +76,9 @@ mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/minio mkdir -p ${DATA_DIR}/search chown -R couchdb:couchdb ${DATA_DIR}/couch -redis-server --requirepass $REDIS_PASSWORD & -/opt/clouseau/bin/clouseau & -/minio/minio server ${DATA_DIR}/minio & +redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 & +/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & +/minio/minio server ${DATA_DIR}/minio > /dev/stdout 2>&1 & /docker-entrypoint.sh /opt/couchdb/bin/couchdb & /etc/init.d/nginx restart if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then @@ -85,16 +87,18 @@ if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then chmod +x /etc/cron.d/certificate-renew # Request the certbot certificate /app/letsencrypt/certificate-request.sh ${CUSTOM_DOMAIN} + /etc/init.d/nginx restart fi -/etc/init.d/nginx restart pushd app -pm2 start --name app "yarn run:docker" +pm2 start -l /dev/stdout --name app "yarn run:docker" popd pushd worker -pm2 start --name worker "yarn run:docker" +pm2 start -l /dev/stdout --name worker "yarn run:docker" popd sleep 10 +echo "curl to couchdb endpoints" curl -X PUT ${COUCH_DB_URL}/_users curl -X PUT ${COUCH_DB_URL}/_replicator +echo "end of runner.sh, sleeping ..." sleep infinity diff --git a/lerna.json b/lerna.json index 4bd0929014..0d02624756 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.0.34-alpha.5", + "version": "2.1.46-alpha.6", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 7733a6df95..6c147698ad 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "devDependencies": { "@rollup/plugin-json": "^4.0.2", - "@typescript-eslint/parser": "4.28.0", + "@typescript-eslint/parser": "5.45.0", "babel-eslint": "^10.0.3", "eslint": "^7.28.0", "eslint-plugin-cypress": "^2.11.3", @@ -18,7 +18,7 @@ "rimraf": "^3.0.2", "rollup-plugin-replace": "^2.2.0", "svelte": "^3.38.2", - "typescript": "4.5.5" + "typescript": "4.7.3" }, "scripts": { "setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev", @@ -75,8 +75,8 @@ "env:multi:disable": "lerna run env:multi:disable", "env:selfhost:enable": "lerna run env:selfhost:enable", "env:selfhost:disable": "lerna run env:selfhost:disable", - "env:localdomain:enable": "lerna run env:localdomain:enable", - "env:localdomain:disable": "lerna run env:localdomain:disable", + "env:localdomain:enable": "./scripts/localdomain.sh enable", + "env:localdomain:disable": "./scripts/localdomain.sh disable", "env:account:enable": "lerna run env:account:enable", "env:account:disable": "lerna run env:account:disable", "mode:self": "yarn env:selfhost:enable && yarn env:multi:disable && yarn env:account:disable", @@ -87,4 +87,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/__mocks__/aws-sdk.ts b/packages/backend-core/__mocks__/aws-sdk.ts new file mode 100644 index 0000000000..7fac80faa9 --- /dev/null +++ b/packages/backend-core/__mocks__/aws-sdk.ts @@ -0,0 +1,15 @@ +const mockS3 = { + headBucket: jest.fn().mockReturnThis(), + deleteObject: jest.fn().mockReturnThis(), + deleteObjects: jest.fn().mockReturnThis(), + createBucket: jest.fn().mockReturnThis(), + listObjects: jest.fn().mockReturnThis(), + promise: jest.fn().mockReturnThis(), + catch: jest.fn(), +} + +const AWS = { + S3: jest.fn(() => mockS3), +} + +export default AWS diff --git a/packages/backend-core/accounts.js b/packages/backend-core/accounts.js deleted file mode 100644 index 47ad03456a..0000000000 --- a/packages/backend-core/accounts.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/cloud/accounts") diff --git a/packages/backend-core/auth.js b/packages/backend-core/auth.js deleted file mode 100644 index bbfe3d41dd..0000000000 --- a/packages/backend-core/auth.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/auth") diff --git a/packages/backend-core/cache.js b/packages/backend-core/cache.js deleted file mode 100644 index c8bd3c9b6f..0000000000 --- a/packages/backend-core/cache.js +++ /dev/null @@ -1,9 +0,0 @@ -const generic = require("./src/cache/generic") - -module.exports = { - user: require("./src/cache/user"), - app: require("./src/cache/appMetadata"), - writethrough: require("./src/cache/writethrough"), - ...generic, - cache: generic, -} diff --git a/packages/backend-core/constants.js b/packages/backend-core/constants.js deleted file mode 100644 index 4abb7703db..0000000000 --- a/packages/backend-core/constants.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/constants") diff --git a/packages/backend-core/context.js b/packages/backend-core/context.js deleted file mode 100644 index c6fa87a337..0000000000 --- a/packages/backend-core/context.js +++ /dev/null @@ -1,24 +0,0 @@ -const { - getAppDB, - getDevAppDB, - getProdAppDB, - getAppId, - updateAppId, - doInAppContext, - doInTenant, - doInContext, -} = require("./src/context") - -const identity = require("./src/context/identity") - -module.exports = { - getAppDB, - getDevAppDB, - getProdAppDB, - getAppId, - updateAppId, - doInAppContext, - doInTenant, - identity, - doInContext, -} diff --git a/packages/backend-core/db.js b/packages/backend-core/db.js deleted file mode 100644 index 0d2869d9f1..0000000000 --- a/packages/backend-core/db.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - ...require("./src/db/utils"), - ...require("./src/db/constants"), - ...require("./src/db"), - ...require("./src/db/views"), - ...require("./src/db/pouch"), -} diff --git a/packages/backend-core/deprovision.js b/packages/backend-core/deprovision.js deleted file mode 100644 index 672da214ff..0000000000 --- a/packages/backend-core/deprovision.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/context/deprovision") diff --git a/packages/backend-core/encryption.js b/packages/backend-core/encryption.js deleted file mode 100644 index 4ccb6e3a99..0000000000 --- a/packages/backend-core/encryption.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/security/encryption") diff --git a/packages/backend-core/jest.config.ts b/packages/backend-core/jest.config.ts new file mode 100644 index 0000000000..d0e5d3d4e7 --- /dev/null +++ b/packages/backend-core/jest.config.ts @@ -0,0 +1,21 @@ +import { Config } from "@jest/types" + +const config: Config.InitialOptions = { + preset: "ts-jest", + testEnvironment: "node", + setupFiles: ["./tests/jestSetup.ts"], + collectCoverageFrom: ["src/**/*.{js,ts}"], + coverageReporters: ["lcov", "json", "clover"], +} + +if (!process.env.CI) { + // use sources when not in CI + config.moduleNameMapper = { + "@budibase/types": "/../types/src", + "^axios.*$": "/node_modules/axios/lib/axios.js", + } +} else { + console.log("Running tests with compiled dependency sources") +} + +export default config diff --git a/packages/backend-core/logging.js b/packages/backend-core/logging.js deleted file mode 100644 index da40fe3100..0000000000 --- a/packages/backend-core/logging.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/logging") diff --git a/packages/backend-core/middleware.js b/packages/backend-core/middleware.js deleted file mode 100644 index 30fec96239..0000000000 --- a/packages/backend-core/middleware.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/middleware") diff --git a/packages/backend-core/migrations.js b/packages/backend-core/migrations.js deleted file mode 100644 index 2de19ebf65..0000000000 --- a/packages/backend-core/migrations.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/migrations") diff --git a/packages/backend-core/objectStore.js b/packages/backend-core/objectStore.js deleted file mode 100644 index 3ee433f224..0000000000 --- a/packages/backend-core/objectStore.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - ...require("./src/objectStore"), - ...require("./src/objectStore/utils"), -} diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 5b032b88ba..e3be11a056 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.0.34-alpha.5", + "version": "2.1.46-alpha.6", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -16,11 +16,11 @@ "prepack": "cp package.json dist", "build": "tsc -p tsconfig.build.json", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", - "test": "jest", + "test": "jest --coverage --maxWorkers=2", "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "2.0.34-alpha.5", + "@budibase/types": "2.1.46-alpha.6", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", @@ -35,8 +35,8 @@ "koa-passport": "4.1.4", "lodash": "4.17.21", "lodash.isarguments": "3.1.0", + "nano": "^10.1.0", "node-fetch": "2.6.7", - "passport-google-auth": "1.0.2", "passport-google-oauth": "2.0.0", "passport-jwt": "4.0.0", "passport-local": "1.0.0", @@ -52,21 +52,11 @@ "uuid": "8.3.2", "zlib": "1.0.5" }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "moduleNameMapper": { - "@budibase/types": "/../types/src" - }, - "setupFiles": [ - "./scripts/jestSetup.ts" - ] - }, "devDependencies": { "@types/chance": "1.1.3", "@types/ioredis": "4.28.0", "@types/jest": "27.5.1", - "@types/koa": "2.0.52", + "@types/koa": "2.13.4", "@types/lodash": "4.14.180", "@types/node": "14.18.20", "@types/node-fetch": "2.6.1", @@ -77,12 +67,14 @@ "@types/uuid": "8.3.4", "chance": "1.1.3", "ioredis-mock": "5.8.0", - "jest": "27.5.1", - "koa": "2.7.0", + "jest": "28.1.1", + "koa": "2.13.4", "nodemon": "2.0.16", "pouchdb-adapter-memory": "7.2.2", "timekeeper": "2.2.0", - "ts-jest": "27.1.5", + "ts-jest": "28.0.4", + "ts-node": "10.8.1", + "tsconfig-paths": "4.0.0", "typescript": "4.7.3" }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" diff --git a/packages/backend-core/permissions.js b/packages/backend-core/permissions.js deleted file mode 100644 index 42f37c9c7e..0000000000 --- a/packages/backend-core/permissions.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/security/permissions") diff --git a/packages/backend-core/plugins.js b/packages/backend-core/plugins.js deleted file mode 100644 index 018e214dcb..0000000000 --- a/packages/backend-core/plugins.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - ...require("./src/plugin"), -} diff --git a/packages/backend-core/plugins.ts b/packages/backend-core/plugins.ts new file mode 100644 index 0000000000..33354eaf64 --- /dev/null +++ b/packages/backend-core/plugins.ts @@ -0,0 +1 @@ +export * from "./src/plugin" diff --git a/packages/backend-core/redis.js b/packages/backend-core/redis.js deleted file mode 100644 index 1f7a48540a..0000000000 --- a/packages/backend-core/redis.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - Client: require("./src/redis"), - utils: require("./src/redis/utils"), - clients: require("./src/redis/init"), -} diff --git a/packages/backend-core/roles.js b/packages/backend-core/roles.js deleted file mode 100644 index 158bcdb6b8..0000000000 --- a/packages/backend-core/roles.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/security/roles") diff --git a/packages/backend-core/scripts/jestSetup.ts b/packages/backend-core/scripts/jestSetup.ts deleted file mode 100644 index 1e86649a24..0000000000 --- a/packages/backend-core/scripts/jestSetup.ts +++ /dev/null @@ -1,12 +0,0 @@ -import env from "../src/environment" -import { mocks } from "../tests/utilities" - -// mock all dates to 2020-01-01T00:00:00.000Z -// use tk.reset() to use real dates in individual tests -import tk from "timekeeper" -tk.freeze(mocks.date.MOCK_DATE) - -env._set("SELF_HOSTED", "1") -env._set("NODE_ENV", "jest") -env._set("JWT_SECRET", "test-jwtsecret") -env._set("LOG_LEVEL", "silent") diff --git a/packages/backend-core/sessions.js b/packages/backend-core/sessions.js deleted file mode 100644 index c07efa2380..0000000000 --- a/packages/backend-core/sessions.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/security/sessions") diff --git a/packages/backend-core/src/auth.ts b/packages/backend-core/src/auth/auth.ts similarity index 76% rename from packages/backend-core/src/auth.ts rename to packages/backend-core/src/auth/auth.ts index 23873b84e7..75e425bd0f 100644 --- a/packages/backend-core/src/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -1,16 +1,14 @@ -const passport = require("koa-passport") +const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -import { getGlobalDB } from "./tenancy" +import { getGlobalDB } from "../tenancy" const refresh = require("passport-oauth2-refresh") -import { Configs } from "./constants" -import { getScopedConfig } from "./db/utils" +import { Config } from "../constants" +import { getScopedConfig } from "../db" import { - jwt, + jwt as jwtPassport, local, authenticated, - google, - oidc, auditLog, tenancy, authError, @@ -21,17 +19,41 @@ import { builderOnly, builderOrAdmin, joiValidator, -} from "./middleware" -import { invalidateUser } from "./cache/user" + oidc, + google, +} from "../middleware" +import { invalidateUser } from "../cache/user" import { User } from "@budibase/types" +import { logAlert } from "../logging" +export { + auditLog, + authError, + internalApi, + ssoCallbackUrl, + adminOnly, + builderOnly, + builderOrAdmin, + joiValidator, + google, + oidc, +} from "../middleware" +export const buildAuthMiddleware = authenticated +export const buildTenancyMiddleware = tenancy +export const buildCsrfMiddleware = csrf +export const passport = _passport +export const jwt = require("jsonwebtoken") // Strategies -passport.use(new LocalStrategy(local.options, local.authenticate)) -passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) +_passport.use(new LocalStrategy(local.options, local.authenticate)) +if (jwtPassport.options.secretOrKey) { + _passport.use(new JwtStrategy(jwtPassport.options, jwtPassport.authenticate)) +} else { + logAlert("No JWT Secret supplied, cannot configure JWT strategy") +} -passport.serializeUser((user: User, done: any) => done(null, user)) +_passport.serializeUser((user: User, done: any) => done(null, user)) -passport.deserializeUser(async (user: User, done: any) => { +_passport.deserializeUser(async (user: User, done: any) => { const db = getGlobalDB() try { @@ -71,7 +93,7 @@ async function refreshOIDCAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Configs.OIDC, + Config.OIDC, refreshToken, (err: any, accessToken: string, refreshToken: any, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -101,7 +123,7 @@ async function refreshGoogleAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Configs.GOOGLE, + Config.GOOGLE, refreshToken, (err: any, accessToken: string, refreshToken: string, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -110,7 +132,7 @@ async function refreshGoogleAccessToken( }) } -async function refreshOAuthToken( +export async function refreshOAuthToken( refreshToken: string, configType: string, configId: string @@ -124,7 +146,7 @@ async function refreshOAuthToken( let chosenConfig = {} let refreshResponse - if (configType === Configs.OIDC) { + if (configType === Config.OIDC) { // configId - retrieved from cookie. chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] if (!chosenConfig) { @@ -147,7 +169,7 @@ async function refreshOAuthToken( return refreshResponse } -async function updateUserOAuth(userId: string, oAuthConfig: any) { +export async function updateUserOAuth(userId: string, oAuthConfig: any) { const details = { accessToken: oAuthConfig.accessToken, refreshToken: oAuthConfig.refreshToken, @@ -174,23 +196,3 @@ async function updateUserOAuth(userId: string, oAuthConfig: any) { console.error("Could not update OAuth details for current user", e) } } - -export = { - buildAuthMiddleware: authenticated, - passport, - google, - oidc, - jwt: require("jsonwebtoken"), - buildTenancyMiddleware: tenancy, - auditLog, - authError, - buildCsrfMiddleware: csrf, - internalApi, - refreshOAuthToken, - updateUserOAuth, - ssoCallbackUrl, - adminOnly, - builderOnly, - builderOrAdmin, - joiValidator, -} diff --git a/packages/backend-core/src/auth/index.ts b/packages/backend-core/src/auth/index.ts new file mode 100644 index 0000000000..306751af96 --- /dev/null +++ b/packages/backend-core/src/auth/index.ts @@ -0,0 +1 @@ +export * from "./auth" diff --git a/packages/backend-core/src/cache/appMetadata.js b/packages/backend-core/src/cache/appMetadata.ts similarity index 79% rename from packages/backend-core/src/cache/appMetadata.js rename to packages/backend-core/src/cache/appMetadata.ts index a7ff0d2fc1..d24c4a3140 100644 --- a/packages/backend-core/src/cache/appMetadata.js +++ b/packages/backend-core/src/cache/appMetadata.ts @@ -1,6 +1,6 @@ -const redis = require("../redis/init") -const { doWithDB } = require("../db") -const { DocumentType } = require("../db/constants") +import { getAppClient } from "../redis/init" +import { doWithDB, DocumentType } from "../db" +import { Database } from "@budibase/types" const AppState = { INVALID: "invalid", @@ -10,17 +10,17 @@ const EXPIRY_SECONDS = 3600 /** * The default populate app metadata function */ -const populateFromDB = async appId => { +async function populateFromDB(appId: string) { return doWithDB( appId, - db => { + (db: Database) => { return db.get(DocumentType.APP_METADATA) }, { skip_setup: true } ) } -const isInvalid = metadata => { +function isInvalid(metadata?: { state: string }) { return !metadata || metadata.state === AppState.INVALID } @@ -31,15 +31,15 @@ const isInvalid = metadata => { * @param {string} appId the id of the app to get metadata from. * @returns {object} the app metadata. */ -exports.getAppMetadata = async appId => { - const client = await redis.getAppClient() +export async function getAppMetadata(appId: string) { + const client = await getAppClient() // try cache let metadata = await client.get(appId) if (!metadata) { - let expiry = EXPIRY_SECONDS + let expiry: number | undefined = EXPIRY_SECONDS try { metadata = await populateFromDB(appId) - } catch (err) { + } catch (err: any) { // app DB left around, but no metadata, it is invalid if (err && err.status === 404) { metadata = { state: AppState.INVALID } @@ -74,11 +74,11 @@ exports.getAppMetadata = async appId => { * @param newMetadata {object|undefined} optional - can simply provide the new metadata to update with. * @return {Promise} will respond with success when cache is updated. */ -exports.invalidateAppMetadata = async (appId, newMetadata = null) => { +export async function invalidateAppMetadata(appId: string, newMetadata?: any) { if (!appId) { throw "Cannot invalidate if no app ID provided." } - const client = await redis.getAppClient() + const client = await getAppClient() await client.delete(appId) if (newMetadata) { await client.store(appId, newMetadata, EXPIRY_SECONDS) diff --git a/packages/backend-core/src/cache/base/index.ts b/packages/backend-core/src/cache/base/index.ts index f3216531f4..ab620a900e 100644 --- a/packages/backend-core/src/cache/base/index.ts +++ b/packages/backend-core/src/cache/base/index.ts @@ -1,6 +1,6 @@ import { getTenantId } from "../../context" -import redis from "../../redis/init" -import RedisWrapper from "../../redis" +import * as redis from "../../redis/init" +import { Client } from "../../redis" function generateTenantKey(key: string) { const tenantId = getTenantId() @@ -8,9 +8,9 @@ function generateTenantKey(key: string) { } export = class BaseCache { - client: RedisWrapper | undefined + client: Client | undefined - constructor(client: RedisWrapper | undefined = undefined) { + constructor(client: Client | undefined = undefined) { this.client = client } diff --git a/packages/backend-core/src/cache/generic.js b/packages/backend-core/src/cache/generic.js deleted file mode 100644 index 26ef0c6bb0..0000000000 --- a/packages/backend-core/src/cache/generic.js +++ /dev/null @@ -1,30 +0,0 @@ -const BaseCache = require("./base") - -const GENERIC = new BaseCache() - -exports.CacheKeys = { - CHECKLIST: "checklist", - INSTALLATION: "installation", - ANALYTICS_ENABLED: "analyticsEnabled", - UNIQUE_TENANT_ID: "uniqueTenantId", - EVENTS: "events", - BACKFILL_METADATA: "backfillMetadata", - EVENTS_RATE_LIMIT: "eventsRateLimit", -} - -exports.TTL = { - ONE_MINUTE: 600, - ONE_HOUR: 3600, - ONE_DAY: 86400, -} - -function performExport(funcName) { - return (...args) => GENERIC[funcName](...args) -} - -exports.keys = performExport("keys") -exports.get = performExport("get") -exports.store = performExport("store") -exports.delete = performExport("delete") -exports.withCache = performExport("withCache") -exports.bustCache = performExport("bustCache") diff --git a/packages/backend-core/src/cache/generic.ts b/packages/backend-core/src/cache/generic.ts new file mode 100644 index 0000000000..d8a54e4a3f --- /dev/null +++ b/packages/backend-core/src/cache/generic.ts @@ -0,0 +1,30 @@ +const BaseCache = require("./base") + +const GENERIC = new BaseCache() + +export enum CacheKey { + CHECKLIST = "checklist", + INSTALLATION = "installation", + ANALYTICS_ENABLED = "analyticsEnabled", + UNIQUE_TENANT_ID = "uniqueTenantId", + EVENTS = "events", + BACKFILL_METADATA = "backfillMetadata", + EVENTS_RATE_LIMIT = "eventsRateLimit", +} + +export enum TTL { + ONE_MINUTE = 600, + ONE_HOUR = 3600, + ONE_DAY = 86400, +} + +function performExport(funcName: string) { + return (...args: any) => GENERIC[funcName](...args) +} + +export const keys = performExport("keys") +export const get = performExport("get") +export const store = performExport("store") +export const destroy = performExport("delete") +export const withCache = performExport("withCache") +export const bustCache = performExport("bustCache") diff --git a/packages/backend-core/src/cache/index.ts b/packages/backend-core/src/cache/index.ts new file mode 100644 index 0000000000..58928c271a --- /dev/null +++ b/packages/backend-core/src/cache/index.ts @@ -0,0 +1,5 @@ +export * as generic from "./generic" +export * as user from "./user" +export * as app from "./appMetadata" +export * as writethrough from "./writethrough" +export * from "./generic" diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.js b/packages/backend-core/src/cache/tests/writethrough.spec.js index 68db24b325..716d3f9c23 100644 --- a/packages/backend-core/src/cache/tests/writethrough.spec.js +++ b/packages/backend-core/src/cache/tests/writethrough.spec.js @@ -1,6 +1,6 @@ -require("../../../tests/utilities/TestConfiguration") +require("../../../tests") const { Writethrough } = require("../writethrough") -const { dangerousGetDB } = require("../../db") +const { getDB } = require("../../db") const tk = require("timekeeper") const START_DATE = Date.now() @@ -8,8 +8,8 @@ tk.freeze(START_DATE) const DELAY = 5000 -const db = dangerousGetDB("test") -const db2 = dangerousGetDB("test2") +const db = getDB("test") +const db2 = getDB("test2") const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY) describe("writethrough", () => { diff --git a/packages/backend-core/src/cache/user.js b/packages/backend-core/src/cache/user.ts similarity index 68% rename from packages/backend-core/src/cache/user.js rename to packages/backend-core/src/cache/user.ts index 130da1915e..a128465cd6 100644 --- a/packages/backend-core/src/cache/user.js +++ b/packages/backend-core/src/cache/user.ts @@ -1,15 +1,16 @@ -const redis = require("../redis/init") -const { getTenantId, lookupTenantId, doWithGlobalDB } = require("../tenancy") -const env = require("../environment") -const accounts = require("../cloud/accounts") +import * as redis from "../redis/init" +import { getTenantId, lookupTenantId, doWithGlobalDB } from "../tenancy" +import env from "../environment" +import * as accounts from "../cloud/accounts" +import { Database } from "@budibase/types" const EXPIRY_SECONDS = 3600 /** * The default populate user function */ -const populateFromDB = async (userId, tenantId) => { - const user = await doWithGlobalDB(tenantId, db => db.get(userId)) +async function populateFromDB(userId: string, tenantId: string) { + const user = await doWithGlobalDB(tenantId, (db: Database) => db.get(userId)) user.budibaseAccess = true if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const account = await accounts.getAccount(user.email) @@ -31,7 +32,11 @@ const populateFromDB = async (userId, tenantId) => { * @param {*} populateUser function to provide the user for re-caching. default to couch db * @returns */ -exports.getUser = async (userId, tenantId = null, populateUser = null) => { +export async function getUser( + userId: string, + tenantId?: string, + populateUser?: any +) { if (!populateUser) { populateUser = populateFromDB } @@ -47,7 +52,7 @@ exports.getUser = async (userId, tenantId = null, populateUser = null) => { let user = await client.get(userId) if (!user) { user = await populateUser(userId, tenantId) - client.store(userId, user, EXPIRY_SECONDS) + await client.store(userId, user, EXPIRY_SECONDS) } if (user && !user.tenantId && tenantId) { // make sure the tenant ID is always correct/set @@ -56,7 +61,7 @@ exports.getUser = async (userId, tenantId = null, populateUser = null) => { return user } -exports.invalidateUser = async userId => { +export async function invalidateUser(userId: string) { const client = await redis.getUserClient() await client.delete(userId) } diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts index 495ba58590..dc889d5b18 100644 --- a/packages/backend-core/src/cache/writethrough.ts +++ b/packages/backend-core/src/cache/writethrough.ts @@ -1,7 +1,7 @@ import BaseCache from "./base" import { getWritethroughClient } from "../redis/init" import { logWarn } from "../logging" -import PouchDB from "pouchdb" +import { Database } from "@budibase/types" const DEFAULT_WRITE_RATE_MS = 10000 let CACHE: BaseCache | null = null @@ -19,7 +19,7 @@ async function getCache() { return CACHE } -function makeCacheKey(db: PouchDB.Database, key: string) { +function makeCacheKey(db: Database, key: string) { return db.name + key } @@ -28,7 +28,7 @@ function makeCacheItem(doc: any, lastWrite: number | null = null): CacheItem { } export async function put( - db: PouchDB.Database, + db: Database, doc: any, writeRateMs: number = DEFAULT_WRITE_RATE_MS ) { @@ -64,7 +64,7 @@ export async function put( return { ok: true, id: output._id, rev: output._rev } } -export async function get(db: PouchDB.Database, id: string): Promise { +export async function get(db: Database, id: string): Promise { const cache = await getCache() const cacheKey = makeCacheKey(db, id) let cacheItem: CacheItem = await cache.get(cacheKey) @@ -77,7 +77,7 @@ export async function get(db: PouchDB.Database, id: string): Promise { } export async function remove( - db: PouchDB.Database, + db: Database, docOrId: any, rev?: any ): Promise { @@ -95,13 +95,10 @@ export async function remove( } export class Writethrough { - db: PouchDB.Database + db: Database writeRateMs: number - constructor( - db: PouchDB.Database, - writeRateMs: number = DEFAULT_WRITE_RATE_MS - ) { + constructor(db: Database, writeRateMs: number = DEFAULT_WRITE_RATE_MS) { this.db = db this.writeRateMs = writeRateMs } diff --git a/packages/backend-core/src/cloud/accounts.ts b/packages/backend-core/src/cloud/accounts.ts index cca7469060..90fa7ab824 100644 --- a/packages/backend-core/src/cloud/accounts.ts +++ b/packages/backend-core/src/cloud/accounts.ts @@ -1,6 +1,6 @@ import API from "./api" import env from "../environment" -import { Headers } from "../constants" +import { Header } from "../constants" import { CloudAccount } from "@budibase/types" const api = new API(env.ACCOUNT_PORTAL_URL) @@ -14,7 +14,7 @@ export const getAccount = async ( const response = await api.post(`/api/accounts/search`, { body: payload, headers: { - [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, + [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, }, }) @@ -35,7 +35,7 @@ export const getAccountByTenantId = async ( const response = await api.post(`/api/accounts/search`, { body: payload, headers: { - [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, + [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, }, }) @@ -50,7 +50,7 @@ export const getAccountByTenantId = async ( export const getStatus = async () => { const response = await api.get(`/api/status`, { headers: { - [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, + [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, }, }) const json = await response.json() diff --git a/packages/backend-core/src/cloud/api.js b/packages/backend-core/src/cloud/api.js deleted file mode 100644 index d4d4b6c8bb..0000000000 --- a/packages/backend-core/src/cloud/api.js +++ /dev/null @@ -1,42 +0,0 @@ -const fetch = require("node-fetch") -class API { - constructor(host) { - this.host = host - } - - apiCall = - method => - async (url = "", options = {}) => { - if (!options.headers) { - options.headers = {} - } - - if (!options.headers["Content-Type"]) { - options.headers = { - "Content-Type": "application/json", - Accept: "application/json", - ...options.headers, - } - } - - let json = options.headers["Content-Type"] === "application/json" - - const requestOptions = { - method: method, - body: json ? JSON.stringify(options.body) : options.body, - headers: options.headers, - // TODO: See if this is necessary - credentials: "include", - } - - return await fetch(`${this.host}${url}`, requestOptions) - } - - post = this.apiCall("POST") - get = this.apiCall("GET") - patch = this.apiCall("PATCH") - del = this.apiCall("DELETE") - put = this.apiCall("PUT") -} - -module.exports = API diff --git a/packages/backend-core/src/cloud/api.ts b/packages/backend-core/src/cloud/api.ts new file mode 100644 index 0000000000..287c447271 --- /dev/null +++ b/packages/backend-core/src/cloud/api.ts @@ -0,0 +1,55 @@ +import fetch from "node-fetch" + +export = class API { + host: string + + constructor(host: string) { + this.host = host + } + + async apiCall(method: string, url: string, options?: any) { + if (!options.headers) { + options.headers = {} + } + + if (!options.headers["Content-Type"]) { + options.headers = { + "Content-Type": "application/json", + Accept: "application/json", + ...options.headers, + } + } + + let json = options.headers["Content-Type"] === "application/json" + + const requestOptions = { + method: method, + body: json ? JSON.stringify(options.body) : options.body, + headers: options.headers, + // TODO: See if this is necessary + credentials: "include", + } + + return await fetch(`${this.host}${url}`, requestOptions) + } + + async post(url: string, options?: any) { + return this.apiCall("POST", url, options) + } + + async get(url: string, options?: any) { + return this.apiCall("GET", url, options) + } + + async patch(url: string, options?: any) { + return this.apiCall("PATCH", url, options) + } + + async del(url: string, options?: any) { + return this.apiCall("DELETE", url, options) + } + + async put(url: string, options?: any) { + return this.apiCall("PUT", url, options) + } +} diff --git a/packages/backend-core/src/clshooked/index.js b/packages/backend-core/src/clshooked/index.js deleted file mode 100644 index d69ffdd914..0000000000 --- a/packages/backend-core/src/clshooked/index.js +++ /dev/null @@ -1,650 +0,0 @@ -const util = require("util") -const assert = require("assert") -const wrapEmitter = require("emitter-listener") -const async_hooks = require("async_hooks") - -const CONTEXTS_SYMBOL = "cls@contexts" -const ERROR_SYMBOL = "error@context" - -const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED - -let currentUid = -1 - -module.exports = { - getNamespace: getNamespace, - createNamespace: createNamespace, - destroyNamespace: destroyNamespace, - reset: reset, - ERROR_SYMBOL: ERROR_SYMBOL, -} - -function Namespace(name) { - this.name = name - // changed in 2.7: no default context - this.active = null - this._set = [] - this.id = null - this._contexts = new Map() - this._indent = 0 - this._hook = null -} - -Namespace.prototype.set = function set(key, value) { - if (!this.active) { - throw new Error( - "No context available. ns.run() or ns.bind() must be called first." - ) - } - - this.active[key] = value - - if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - indentStr + - "CONTEXT-SET KEY:" + - key + - "=" + - value + - " in ns:" + - this.name + - " currentUid:" + - currentUid + - " active:" + - util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) - ) - } - - return value -} - -Namespace.prototype.get = function get(key) { - if (!this.active) { - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.currentId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}` - ) - } - return undefined - } - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - indentStr + - "CONTEXT-GETTING KEY:" + - key + - "=" + - this.active[key] + - " (" + - this.name + - ") currentUid:" + - currentUid + - " active:" + - util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) - ) - debug2( - `${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${ - this.active[key] - } currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ - this._set.length - } active:${util.inspect(this.active)}` - ) - } - return this.active[key] -} - -Namespace.prototype.createContext = function createContext() { - // Prototype inherit existing context if created a new child context within existing context. - let context = Object.create(this.active ? this.active : Object.prototype) - context._ns_name = this.name - context.id = currentUid - - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-CREATED Context: (${ - this.name - }) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ - this._set.length - } context:${util.inspect(context, { - showHidden: true, - depth: 2, - colors: true, - })}` - ) - } - - return context -} - -Namespace.prototype.run = function run(fn) { - let context = this.createContext() - this.enter(context) - - try { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-RUN BEGIN: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } context:${util.inspect(context)}` - ) - } - fn(context) - return context - } catch (exception) { - if (exception) { - exception[ERROR_SYMBOL] = context - } - throw exception - } finally { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-RUN END: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } ${util.inspect(context)}` - ) - } - this.exit(context) - } -} - -Namespace.prototype.runAndReturn = function runAndReturn(fn) { - let value - this.run(function (context) { - value = fn(context) - }) - return value -} - -/** - * Uses global Promise and assumes Promise is cls friendly or wrapped already. - * @param {function} fn - * @returns {*} - */ -Namespace.prototype.runPromise = function runPromise(fn) { - let context = this.createContext() - this.enter(context) - - let promise = fn(context) - if (!promise || !promise.then || !promise.catch) { - throw new Error("fn must return a promise.") - } - - if (DEBUG_CLS_HOOKED) { - debug2( - "CONTEXT-runPromise BEFORE: (" + - this.name + - ") currentUid:" + - currentUid + - " len:" + - this._set.length + - " " + - util.inspect(context) - ) - } - - return promise - .then(result => { - if (DEBUG_CLS_HOOKED) { - debug2( - "CONTEXT-runPromise AFTER then: (" + - this.name + - ") currentUid:" + - currentUid + - " len:" + - this._set.length + - " " + - util.inspect(context) - ) - } - this.exit(context) - return result - }) - .catch(err => { - err[ERROR_SYMBOL] = context - if (DEBUG_CLS_HOOKED) { - debug2( - "CONTEXT-runPromise AFTER catch: (" + - this.name + - ") currentUid:" + - currentUid + - " len:" + - this._set.length + - " " + - util.inspect(context) - ) - } - this.exit(context) - throw err - }) -} - -Namespace.prototype.bind = function bindFactory(fn, context) { - if (!context) { - if (!this.active) { - context = this.createContext() - } else { - context = this.active - } - } - - let self = this - return function clsBind() { - self.enter(context) - try { - return fn.apply(this, arguments) - } catch (exception) { - if (exception) { - exception[ERROR_SYMBOL] = context - } - throw exception - } finally { - self.exit(context) - } - } -} - -Namespace.prototype.enter = function enter(context) { - assert.ok(context, "context must be provided for entering") - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-ENTER: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } ${util.inspect(context)}` - ) - } - - this._set.push(this.active) - this.active = context -} - -Namespace.prototype.exit = function exit(context) { - assert.ok(context, "context must be provided for exiting") - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-EXIT: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } ${util.inspect(context)}` - ) - } - - // Fast path for most exits that are at the top of the stack - if (this.active === context) { - assert.ok(this._set.length, "can't remove top context") - this.active = this._set.pop() - return - } - - // Fast search in the stack using lastIndexOf - let index = this._set.lastIndexOf(context) - - if (index < 0) { - if (DEBUG_CLS_HOOKED) { - debug2( - "??ERROR?? context exiting but not entered - ignoring: " + - util.inspect(context) - ) - } - assert.ok( - index >= 0, - "context not currently entered; can't exit. \n" + - util.inspect(this) + - "\n" + - util.inspect(context) - ) - } else { - assert.ok(index, "can't remove top context") - this._set.splice(index, 1) - } -} - -Namespace.prototype.bindEmitter = function bindEmitter(emitter) { - assert.ok( - emitter.on && emitter.addListener && emitter.emit, - "can only bind real EEs" - ) - - let namespace = this - let thisSymbol = "context@" + this.name - - // Capture the context active at the time the emitter is bound. - function attach(listener) { - if (!listener) { - return - } - if (!listener[CONTEXTS_SYMBOL]) { - listener[CONTEXTS_SYMBOL] = Object.create(null) - } - - listener[CONTEXTS_SYMBOL][thisSymbol] = { - namespace: namespace, - context: namespace.active, - } - } - - // At emit time, bind the listener within the correct context. - function bind(unwrapped) { - if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) { - return unwrapped - } - - let wrapped = unwrapped - let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL] - Object.keys(unwrappedContexts).forEach(function (name) { - let thunk = unwrappedContexts[name] - wrapped = thunk.namespace.bind(wrapped, thunk.context) - }) - return wrapped - } - - wrapEmitter(emitter, attach, bind) -} - -/** - * If an error comes out of a namespace, it will have a context attached to it. - * This function knows how to find it. - * - * @param {Error} exception Possibly annotated error. - */ -Namespace.prototype.fromException = function fromException(exception) { - return exception[ERROR_SYMBOL] -} - -function getNamespace(name) { - return process.namespaces[name] -} - -function createNamespace(name) { - assert.ok(name, "namespace must be given a name.") - - if (DEBUG_CLS_HOOKED) { - debug2(`NS-CREATING NAMESPACE (${name})`) - } - let namespace = new Namespace(name) - namespace.id = currentUid - - const hook = async_hooks.createHook({ - init(asyncId, type, triggerId, resource) { - currentUid = async_hooks.executionAsyncId() - - //CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec - // let initContext = namespace.active; - // if(!initContext && triggerId) { - // let parentContext = namespace._contexts.get(triggerId); - // if (parentContext) { - // namespace.active = parentContext; - // namespace._contexts.set(currentUid, parentContext); - // if (DEBUG_CLS_HOOKED) { - // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); - // debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); - // } - // } else if (DEBUG_CLS_HOOKED) { - // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); - // debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); - // } - // }else { - // namespace._contexts.set(currentUid, namespace.active); - // if (DEBUG_CLS_HOOKED) { - // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); - // debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); - // } - // } - if (namespace.active) { - namespace._contexts.set(asyncId, namespace.active) - - if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - } else if (currentUid === 0) { - // CurrentId will be 0 when triggered from C++. Promise events - // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid - const triggerId = async_hooks.triggerAsyncId() - const triggerIdContext = namespace._contexts.get(triggerId) - if (triggerIdContext) { - namespace._contexts.set(asyncId, triggerIdContext) - if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - } else if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - } - - if (DEBUG_CLS_HOOKED && type === "PROMISE") { - debug2(util.inspect(resource, { showHidden: true })) - const parentId = resource.parentId - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - }, - before(asyncId) { - currentUid = async_hooks.executionAsyncId() - let context - - /* - if(currentUid === 0){ - // CurrentId will be 0 when triggered from C++. Promise events - // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid - //const triggerId = async_hooks.triggerAsyncId(); - context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); - }else{ - context = namespace._contexts.get(currentUid); - } - */ - - //HACK to work with promises until they are fixed in node > 8.1.1 - context = - namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) - - if (context) { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(context)}` - ) - namespace._indent += 2 - } - - namespace.enter(context) - } else if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} namespace._contexts:${util.inspect(namespace._contexts, { - showHidden: true, - depth: 2, - colors: true, - })}` - ) - namespace._indent += 2 - } - }, - after(asyncId) { - currentUid = async_hooks.executionAsyncId() - let context // = namespace._contexts.get(currentUid); - /* - if(currentUid === 0){ - // CurrentId will be 0 when triggered from C++. Promise events - // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid - //const triggerId = async_hooks.triggerAsyncId(); - context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); - }else{ - context = namespace._contexts.get(currentUid); - } - */ - //HACK to work with promises until they are fixed in node > 8.1.1 - context = - namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) - - if (context) { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - namespace._indent -= 2 - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(context)}` - ) - } - - namespace.exit(context) - } else if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - namespace._indent -= 2 - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(context)}` - ) - } - }, - destroy(asyncId) { - currentUid = async_hooks.executionAsyncId() - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(namespace._contexts.get(currentUid))}` - ) - } - - namespace._contexts.delete(asyncId) - }, - }) - - hook.enable() - namespace._hook = hook - - process.namespaces[name] = namespace - return namespace -} - -function destroyNamespace(name) { - let namespace = getNamespace(name) - - assert.ok(namespace, "can't delete nonexistent namespace! \"" + name + '"') - assert.ok( - namespace.id, - "don't assign to process.namespaces directly! " + util.inspect(namespace) - ) - - namespace._hook.disable() - namespace._contexts = null - process.namespaces[name] = null -} - -function reset() { - // must unregister async listeners - if (process.namespaces) { - Object.keys(process.namespaces).forEach(function (name) { - destroyNamespace(name) - }) - } - process.namespaces = Object.create(null) -} - -process.namespaces = process.namespaces || {} - -//const fs = require('fs'); -function debug2(...args) { - if (DEBUG_CLS_HOOKED) { - //fs.writeSync(1, `${util.format(...args)}\n`); - process._rawDebug(`${util.format(...args)}`) - } -} - -/*function getFunctionName(fn) { - if (!fn) { - return fn; - } - if (typeof fn === 'function') { - if (fn.name) { - return fn.name; - } - return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; - } else if (fn.constructor && fn.constructor.name) { - return fn.constructor.name; - } -}*/ diff --git a/packages/backend-core/src/constants.js b/packages/backend-core/src/constants.js deleted file mode 100644 index 44c271a4f8..0000000000 --- a/packages/backend-core/src/constants.js +++ /dev/null @@ -1,44 +0,0 @@ -exports.UserStatus = { - ACTIVE: "active", - INACTIVE: "inactive", -} - -exports.Cookies = { - CurrentApp: "budibase:currentapp", - Auth: "budibase:auth", - Init: "budibase:init", - ACCOUNT_RETURN_URL: "budibase:account:returnurl", - DatasourceAuth: "budibase:datasourceauth", - OIDC_CONFIG: "budibase:oidc:config", -} - -exports.Headers = { - API_KEY: "x-budibase-api-key", - LICENSE_KEY: "x-budibase-license-key", - API_VER: "x-budibase-api-version", - APP_ID: "x-budibase-app-id", - TYPE: "x-budibase-type", - PREVIEW_ROLE: "x-budibase-role", - TENANT_ID: "x-budibase-tenant-id", - TOKEN: "x-budibase-token", - CSRF_TOKEN: "x-csrf-token", -} - -exports.GlobalRoles = { - OWNER: "owner", - ADMIN: "admin", - BUILDER: "builder", - WORKSPACE_MANAGER: "workspace_manager", -} - -exports.Configs = { - SETTINGS: "settings", - ACCOUNT: "account", - SMTP: "smtp", - GOOGLE: "google", - OIDC: "oidc", - OIDC_LOGOS: "logos_oidc", -} - -exports.MAX_VALID_DATE = new Date(2147483647000) -exports.DEFAULT_TENANT_ID = "default" diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/constants/db.ts similarity index 97% rename from packages/backend-core/src/db/constants.ts rename to packages/backend-core/src/constants/db.ts index 446f1f7d01..92392457d6 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/constants/db.ts @@ -92,3 +92,4 @@ export const StaticDatabases = { export const APP_PREFIX = DocumentType.APP + SEPARATOR export const APP_DEV = DocumentType.APP_DEV + SEPARATOR export const APP_DEV_PREFIX = APP_DEV +export const BUDIBASE_DATASOURCE_TYPE = "budibase" diff --git a/packages/backend-core/src/constants/index.ts b/packages/backend-core/src/constants/index.ts new file mode 100644 index 0000000000..62d5e08e63 --- /dev/null +++ b/packages/backend-core/src/constants/index.ts @@ -0,0 +1,2 @@ +export * from "./db" +export * from "./misc" diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts new file mode 100644 index 0000000000..61b3cea1f6 --- /dev/null +++ b/packages/backend-core/src/constants/misc.ts @@ -0,0 +1,44 @@ +export enum UserStatus { + ACTIVE = "active", + INACTIVE = "inactive", +} + +export enum Cookie { + CurrentApp = "budibase:currentapp", + Auth = "budibase:auth", + Init = "budibase:init", + ACCOUNT_RETURN_URL = "budibase:account:returnurl", + DatasourceAuth = "budibase:datasourceauth", + OIDC_CONFIG = "budibase:oidc:config", +} + +export enum Header { + API_KEY = "x-budibase-api-key", + LICENSE_KEY = "x-budibase-license-key", + API_VER = "x-budibase-api-version", + APP_ID = "x-budibase-app-id", + TYPE = "x-budibase-type", + PREVIEW_ROLE = "x-budibase-role", + TENANT_ID = "x-budibase-tenant-id", + TOKEN = "x-budibase-token", + CSRF_TOKEN = "x-csrf-token", +} + +export enum GlobalRole { + OWNER = "owner", + ADMIN = "admin", + BUILDER = "builder", + WORKSPACE_MANAGER = "workspace_manager", +} + +export enum Config { + SETTINGS = "settings", + ACCOUNT = "account", + SMTP = "smtp", + GOOGLE = "google", + OIDC = "oidc", + OIDC_LOGOS = "logos_oidc", +} + +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 new file mode 100644 index 0000000000..f0ccdb97a8 --- /dev/null +++ b/packages/backend-core/src/context/Context.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from "async_hooks" + +export default class Context { + static storage = new AsyncLocalStorage>() + + static run(context: Record, func: any) { + return Context.storage.run(context, () => func()) + } + + static get(): Record { + return Context.storage.getStore() as Record + } + + static set(context: Record) { + Context.storage.enterWith(context) + } +} diff --git a/packages/backend-core/src/context/FunctionContext.js b/packages/backend-core/src/context/FunctionContext.js deleted file mode 100644 index c0ed34fe78..0000000000 --- a/packages/backend-core/src/context/FunctionContext.js +++ /dev/null @@ -1,47 +0,0 @@ -const cls = require("../clshooked") -const { newid } = require("../hashing") - -const REQUEST_ID_KEY = "requestId" -const MAIN_CTX = cls.createNamespace("main") - -function getContextStorage(namespace) { - if (namespace && namespace.active) { - let contextData = namespace.active - delete contextData.id - delete contextData._ns_name - return contextData - } - return {} -} - -class FunctionContext { - static run(callback) { - return MAIN_CTX.runAndReturn(async () => { - const namespaceId = newid() - MAIN_CTX.set(REQUEST_ID_KEY, namespaceId) - const namespace = cls.createNamespace(namespaceId) - let response = await namespace.runAndReturn(callback) - cls.destroyNamespace(namespaceId) - return response - }) - } - - static setOnContext(key, value) { - const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) - const namespace = cls.getNamespace(namespaceId) - namespace.set(key, value) - } - - static getFromContext(key) { - const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) - const namespace = cls.getNamespace(namespaceId) - const context = getContextStorage(namespace) - if (context) { - return context[key] - } else { - return null - } - } -} - -module.exports = FunctionContext diff --git a/packages/backend-core/src/context/constants.ts b/packages/backend-core/src/context/constants.ts deleted file mode 100644 index 937ad8f248..0000000000 --- a/packages/backend-core/src/context/constants.ts +++ /dev/null @@ -1,17 +0,0 @@ -export enum ContextKey { - TENANT_ID = "tenantId", - GLOBAL_DB = "globalDb", - APP_ID = "appId", - IDENTITY = "identity", - // whatever the request app DB was - CURRENT_DB = "currentDb", - // get the prod app DB from the request - PROD_DB = "prodDb", - // get the dev app DB from the request - DEV_DB = "devDb", - DB_OPTS = "dbOpts", - // check if something else is using the context, don't close DB - TENANCY_IN_USE = "tenancyInUse", - APP_IN_USE = "appInUse", - IDENTITY_IN_USE = "identityInUse", -} diff --git a/packages/backend-core/src/context/deprovision.js b/packages/backend-core/src/context/deprovision.ts similarity index 58% rename from packages/backend-core/src/context/deprovision.js rename to packages/backend-core/src/context/deprovision.ts index ba3c2d8449..81f03096dc 100644 --- a/packages/backend-core/src/context/deprovision.js +++ b/packages/backend-core/src/context/deprovision.ts @@ -1,15 +1,19 @@ -const { getGlobalUserParams, getAllApps } = require("../db/utils") -const { doWithDB } = require("../db") -const { doWithGlobalDB } = require("../tenancy") -const { StaticDatabases } = require("../db/constants") +import { + getGlobalUserParams, + getAllApps, + doWithDB, + StaticDatabases, +} from "../db" +import { doWithGlobalDB } from "../tenancy" +import { App, Tenants, User, Database } from "@budibase/types" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name -const removeTenantFromInfoDB = async tenantId => { +async function removeTenantFromInfoDB(tenantId: string) { try { - await doWithDB(PLATFORM_INFO_DB, async infoDb => { - let tenants = await infoDb.get(TENANT_DOC) + await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { + const tenants = (await infoDb.get(TENANT_DOC)) as Tenants tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) await infoDb.put(tenants) @@ -20,14 +24,14 @@ const removeTenantFromInfoDB = async tenantId => { } } -exports.removeUserFromInfoDB = async dbUser => { - await doWithDB(PLATFORM_INFO_DB, async infoDb => { - const keys = [dbUser._id, dbUser.email] +export async function removeUserFromInfoDB(dbUser: User) { + await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { + const keys = [dbUser._id!, dbUser.email] const userDocs = await infoDb.allDocs({ keys, include_docs: true, }) - const toDelete = userDocs.rows.map(row => { + const toDelete = userDocs.rows.map((row: any) => { return { ...row.doc, _deleted: true, @@ -37,18 +41,18 @@ exports.removeUserFromInfoDB = async dbUser => { }) } -const removeUsersFromInfoDB = async tenantId => { - return doWithGlobalDB(tenantId, async db => { +async function removeUsersFromInfoDB(tenantId: string) { + return doWithGlobalDB(tenantId, async (db: any) => { try { const allUsers = await db.allDocs( getGlobalUserParams(null, { include_docs: true, }) ) - await doWithDB(PLATFORM_INFO_DB, async infoDb => { - const allEmails = allUsers.rows.map(row => row.doc.email) + await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { + const allEmails = allUsers.rows.map((row: any) => row.doc.email) // get the id docs - let keys = allUsers.rows.map(row => row.id) + let keys = allUsers.rows.map((row: any) => row.id) // and the email docs keys = keys.concat(allEmails) // retrieve the docs and delete them @@ -56,7 +60,7 @@ const removeUsersFromInfoDB = async tenantId => { keys, include_docs: true, }) - const toDelete = userDocs.rows.map(row => { + const toDelete = userDocs.rows.map((row: any) => { return { ...row.doc, _deleted: true, @@ -71,8 +75,8 @@ const removeUsersFromInfoDB = async tenantId => { }) } -const removeGlobalDB = async tenantId => { - return doWithGlobalDB(tenantId, async db => { +async function removeGlobalDB(tenantId: string) { + return doWithGlobalDB(tenantId, async (db: Database) => { try { await db.destroy() } catch (err) { @@ -82,11 +86,11 @@ const removeGlobalDB = async tenantId => { }) } -const removeTenantApps = async tenantId => { +async function removeTenantApps(tenantId: string) { try { - const apps = await getAllApps({ all: true }) + const apps = (await getAllApps({ all: true })) as App[] const destroyPromises = apps.map(app => - doWithDB(app.appId, db => db.destroy()) + doWithDB(app.appId, (db: Database) => db.destroy()) ) await Promise.allSettled(destroyPromises) } catch (err) { @@ -96,7 +100,7 @@ const removeTenantApps = async tenantId => { } // can't live in tenancy package due to circular dependency on db/utils -exports.deleteTenant = async tenantId => { +export async function deleteTenant(tenantId: string) { await removeTenantFromInfoDB(tenantId) await removeUsersFromInfoDB(tenantId) await removeGlobalDB(tenantId) diff --git a/packages/backend-core/src/context/identity.ts b/packages/backend-core/src/context/identity.ts index 37e1ecf40a..648dd1b5fd 100644 --- a/packages/backend-core/src/context/identity.ts +++ b/packages/backend-core/src/context/identity.ts @@ -2,23 +2,22 @@ import { IdentityContext, IdentityType, User, - UserContext, isCloudAccount, Account, AccountUserContext, } from "@budibase/types" import * as context from "." -export const getIdentity = (): IdentityContext | undefined => { +export function getIdentity(): IdentityContext | undefined { return context.getIdentity() } -export const doInIdentityContext = (identity: IdentityContext, task: any) => { +export function doInIdentityContext(identity: IdentityContext, task: any) { return context.doInIdentityContext(identity, task) } -export const doInUserContext = (user: User, task: any) => { - const userContext: UserContext = { +export function doInUserContext(user: User, task: any) { + const userContext: any = { ...user, _id: user._id as string, type: IdentityType.USER, @@ -26,7 +25,7 @@ export const doInUserContext = (user: User, task: any) => { return doInIdentityContext(userContext, task) } -export const doInAccountContext = (account: Account, task: any) => { +export function doInAccountContext(account: Account, task: any) { const _id = getAccountUserId(account) const tenantId = account.tenantId const accountContext: AccountUserContext = { @@ -38,12 +37,12 @@ export const doInAccountContext = (account: Account, task: any) => { return doInIdentityContext(accountContext, task) } -export const getAccountUserId = (account: Account) => { +export function getAccountUserId(account: Account) { let userId: string if (isCloudAccount(account)) { userId = account.budibaseUserId } else { - // use account id as user id for self hosting + // use account id as user id for self-hosting userId = account.accountId } return userId diff --git a/packages/backend-core/src/context/index.ts b/packages/backend-core/src/context/index.ts index 7efe0e23f7..9c70363170 100644 --- a/packages/backend-core/src/context/index.ts +++ b/packages/backend-core/src/context/index.ts @@ -1,267 +1,3 @@ -import env from "../environment" -import { SEPARATOR, DocumentType } from "../db/constants" -import cls from "./FunctionContext" -import { dangerousGetDB, closeDB } from "../db" -import { baseGlobalDBName } from "../db/tenancy" -import { IdentityContext } from "@budibase/types" -import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants" -import { ContextKey } from "./constants" -import { - updateUsing, - closeWithUsing, - setAppTenantId, - setIdentity, - closeAppDBs, - getContextDB, -} from "./utils" - -export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID - -// some test cases call functions directly, need to -// store an app ID to pretend there is a context -let TEST_APP_ID: string | null = null - -export const closeTenancy = async () => { - let db - try { - if (env.USE_COUCH) { - db = getGlobalDB() - } - } catch (err) { - // no DB found - skip closing - return - } - await closeDB(db) - // clear from context now that database is closed/task is finished - cls.setOnContext(ContextKey.TENANT_ID, null) - cls.setOnContext(ContextKey.GLOBAL_DB, null) -} - -// export const isDefaultTenant = () => { -// return getTenantId() === DEFAULT_TENANT_ID -// } - -export const isMultiTenant = () => { - return env.MULTI_TENANCY -} - -/** - * Given an app ID this will attempt to retrieve the tenant ID from it. - * @return {null|string} The tenant ID found within the app ID. - */ -export const getTenantIDFromAppID = (appId: string) => { - if (!appId) { - return null - } - if (!isMultiTenant()) { - return DEFAULT_TENANT_ID - } - const split = appId.split(SEPARATOR) - const hasDev = split[1] === DocumentType.DEV - if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { - return null - } - if (hasDev) { - return split[2] - } else { - return split[1] - } -} - -export const doInContext = async (appId: string, task: any) => { - // gets the tenant ID from the app ID - const tenantId = getTenantIDFromAppID(appId) - return doInTenant(tenantId, async () => { - return doInAppContext(appId, async () => { - return task() - }) - }) -} - -export const doInTenant = (tenantId: string | null, task: any) => { - // make sure default always selected in single tenancy - if (!env.MULTI_TENANCY) { - tenantId = tenantId || DEFAULT_TENANT_ID - } - // the internal function is so that we can re-use an existing - // context - don't want to close DB on a parent context - async function internal(opts = { existing: false }) { - // set the tenant id + global db if this is a new context - if (!opts.existing) { - updateTenantId(tenantId) - } - - try { - // invoke the task - return await task() - } finally { - await closeWithUsing(ContextKey.TENANCY_IN_USE, () => { - return closeTenancy() - }) - } - } - - const existing = cls.getFromContext(ContextKey.TENANT_ID) === tenantId - return updateUsing(ContextKey.TENANCY_IN_USE, existing, internal) -} - -export const doInAppContext = (appId: string, task: any) => { - if (!appId) { - throw new Error("appId is required") - } - - const identity = getIdentity() - - // the internal function is so that we can re-use an existing - // context - don't want to close DB on a parent context - async function internal(opts = { existing: false }) { - // set the app tenant id - if (!opts.existing) { - setAppTenantId(appId) - } - // set the app ID - cls.setOnContext(ContextKey.APP_ID, appId) - - // preserve the identity - if (identity) { - setIdentity(identity) - } - try { - // invoke the task - return await task() - } finally { - await closeWithUsing(ContextKey.APP_IN_USE, async () => { - await closeAppDBs() - await closeTenancy() - }) - } - } - const existing = cls.getFromContext(ContextKey.APP_ID) === appId - return updateUsing(ContextKey.APP_IN_USE, existing, internal) -} - -export const doInIdentityContext = (identity: IdentityContext, task: any) => { - if (!identity) { - throw new Error("identity is required") - } - - async function internal(opts = { existing: false }) { - if (!opts.existing) { - cls.setOnContext(ContextKey.IDENTITY, identity) - // set the tenant so that doInTenant will preserve identity - if (identity.tenantId) { - updateTenantId(identity.tenantId) - } - } - - try { - // invoke the task - return await task() - } finally { - await closeWithUsing(ContextKey.IDENTITY_IN_USE, async () => { - setIdentity(null) - await closeTenancy() - }) - } - } - - const existing = cls.getFromContext(ContextKey.IDENTITY) - return updateUsing(ContextKey.IDENTITY_IN_USE, existing, internal) -} - -export const getIdentity = (): IdentityContext | undefined => { - try { - return cls.getFromContext(ContextKey.IDENTITY) - } catch (e) { - // do nothing - identity is not in context - } -} - -export const updateTenantId = (tenantId: string | null) => { - cls.setOnContext(ContextKey.TENANT_ID, tenantId) - if (env.USE_COUCH) { - setGlobalDB(tenantId) - } -} - -export const updateAppId = async (appId: string) => { - try { - // have to close first, before removing the databases from context - await closeAppDBs() - cls.setOnContext(ContextKey.APP_ID, appId) - } catch (err) { - if (env.isTest()) { - TEST_APP_ID = appId - } else { - throw err - } - } -} - -export const setGlobalDB = (tenantId: string | null) => { - const dbName = baseGlobalDBName(tenantId) - const db = dangerousGetDB(dbName) - cls.setOnContext(ContextKey.GLOBAL_DB, db) - return db -} - -export const getGlobalDB = () => { - const db = cls.getFromContext(ContextKey.GLOBAL_DB) - if (!db) { - throw new Error("Global DB not found") - } - return db -} - -export const isTenantIdSet = () => { - const tenantId = cls.getFromContext(ContextKey.TENANT_ID) - return !!tenantId -} - -export const getTenantId = () => { - if (!isMultiTenant()) { - return DEFAULT_TENANT_ID - } - const tenantId = cls.getFromContext(ContextKey.TENANT_ID) - if (!tenantId) { - throw new Error("Tenant id not found") - } - return tenantId -} - -export const getAppId = () => { - const foundId = cls.getFromContext(ContextKey.APP_ID) - if (!foundId && env.isTest() && TEST_APP_ID) { - return TEST_APP_ID - } else { - return foundId - } -} - -export const isTenancyEnabled = () => { - return env.MULTI_TENANCY -} - -/** - * Opens the app database based on whatever the request - * contained, dev or prod. - */ -export const getAppDB = (opts?: any) => { - return getContextDB(ContextKey.CURRENT_DB, opts) -} - -/** - * This specifically gets the prod app ID, if the request - * contained a development app ID, this will open the prod one. - */ -export const getProdAppDB = (opts?: any) => { - return getContextDB(ContextKey.PROD_DB, opts) -} - -/** - * This specifically gets the dev app ID, if the request - * contained a prod app ID, this will open the dev one. - */ -export const getDevAppDB = (opts?: any) => { - return getContextDB(ContextKey.DEV_DB, opts) -} +export { DEFAULT_TENANT_ID } from "../constants" +export * as identity from "./identity" +export * from "./mainContext" diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts new file mode 100644 index 0000000000..d743d2f49b --- /dev/null +++ b/packages/backend-core/src/context/mainContext.ts @@ -0,0 +1,245 @@ +// some test cases call functions directly, need to +// store an app ID to pretend there is a context +import env from "../environment" +import Context from "./Context" +import { getDevelopmentAppID, getProdAppID } from "../db/conversions" +import { getDB } from "../db/db" +import { + DocumentType, + SEPARATOR, + StaticDatabases, + DEFAULT_TENANT_ID, +} from "../constants" +import { Database, IdentityContext } from "@budibase/types" + +export type ContextMap = { + tenantId?: string + appId?: string + identity?: IdentityContext +} + +let TEST_APP_ID: string | null = null + +export function getGlobalDBName(tenantId?: string) { + // tenant ID can be set externally, for example user API where + // new tenants are being created, this may be the case + if (!tenantId) { + tenantId = getTenantId() + } + 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}` + } + return dbName +} + +export function isMultiTenant() { + return env.MULTI_TENANCY +} + +export function isTenantIdSet() { + const context = Context.get() + return !!context?.tenantId +} + +export function isTenancyEnabled() { + return env.MULTI_TENANCY +} + +/** + * Given an app ID this will attempt to retrieve the tenant ID from it. + * @return {null|string} The tenant ID found within the app ID. + */ +export function getTenantIDFromAppID(appId: string) { + if (!appId) { + return undefined + } + if (!isMultiTenant()) { + return DEFAULT_TENANT_ID + } + const split = appId.split(SEPARATOR) + const hasDev = split[1] === DocumentType.DEV + if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { + return undefined + } + if (hasDev) { + return split[2] + } else { + return split[1] + } +} + +function updateContext(updates: ContextMap) { + let context: ContextMap + try { + context = Context.get() + } catch (err) { + // no context, start empty + context = {} + } + context = { + ...context, + ...updates, + } + return context +} + +async function newContext(updates: ContextMap, task: any) { + // see if there already is a context setup + let context: ContextMap = updateContext(updates) + return Context.run(context, task) +} + +export async function doInContext(appId: string, task: any): Promise { + const tenantId = getTenantIDFromAppID(appId) + return newContext( + { + tenantId, + appId, + }, + task + ) +} + +export async function doInTenant( + tenantId: string | null, + task: any +): Promise { + // make sure default always selected in single tenancy + if (!env.MULTI_TENANCY) { + tenantId = tenantId || DEFAULT_TENANT_ID + } + + const updates = tenantId ? { tenantId } : {} + return newContext(updates, task) +} + +export async function doInAppContext(appId: string, task: any): Promise { + if (!appId) { + throw new Error("appId is required") + } + + const tenantId = getTenantIDFromAppID(appId) + const updates: ContextMap = { appId } + if (tenantId) { + updates.tenantId = tenantId + } + return newContext(updates, task) +} + +export async function doInIdentityContext( + identity: IdentityContext, + task: any +): Promise { + if (!identity) { + throw new Error("identity is required") + } + + const context: ContextMap = { + identity, + } + if (identity.tenantId) { + context.tenantId = identity.tenantId + } + return newContext(context, task) +} + +export function getIdentity(): IdentityContext | undefined { + try { + const context = Context.get() + return context?.identity + } catch (e) { + // do nothing - identity is not in context + } +} + +export function getTenantId(): string { + if (!isMultiTenant()) { + return DEFAULT_TENANT_ID + } + const context = Context.get() + const tenantId = context?.tenantId + if (!tenantId) { + throw new Error("Tenant id not found") + } + return tenantId +} + +export function getAppId(): string | undefined { + const context = Context.get() + const foundId = context?.appId + if (!foundId && env.isTest() && TEST_APP_ID) { + return TEST_APP_ID + } else { + return foundId + } +} + +export function updateTenantId(tenantId?: string) { + let context: ContextMap = updateContext({ + tenantId, + }) + Context.set(context) +} + +export function updateAppId(appId: string) { + let context: ContextMap = updateContext({ + appId, + }) + try { + Context.set(context) + } catch (err) { + if (env.isTest()) { + TEST_APP_ID = appId + } else { + throw err + } + } +} + +export function getGlobalDB(): Database { + const context = Context.get() + if (!context || (env.MULTI_TENANCY && !context.tenantId)) { + throw new Error("Global DB not found") + } + return getDB(baseGlobalDBName(context?.tenantId)) +} + +/** + * Gets the app database based on whatever the request + * contained, dev or prod. + */ +export function getAppDB(opts?: any): Database { + const appId = getAppId() + return getDB(appId, opts) +} + +/** + * This specifically gets the prod app ID, if the request + * contained a development app ID, this will get the prod one. + */ +export function getProdAppDB(opts?: any): Database { + const appId = getAppId() + if (!appId) { + throw new Error("Unable to retrieve prod DB - no app ID.") + } + return getDB(getProdAppID(appId), opts) +} + +/** + * This specifically gets the dev app ID, if the request + * contained a prod app ID, this will get the dev one. + */ +export function getDevAppDB(opts?: any): Database { + const appId = getAppId() + if (!appId) { + throw new Error("Unable to retrieve dev DB - no app ID.") + } + return getDB(getDevelopmentAppID(appId), opts) +} diff --git a/packages/backend-core/src/context/tests/index.spec.ts b/packages/backend-core/src/context/tests/index.spec.js similarity index 80% rename from packages/backend-core/src/context/tests/index.spec.ts rename to packages/backend-core/src/context/tests/index.spec.js index 55ecd333a3..ea60806d21 100644 --- a/packages/backend-core/src/context/tests/index.spec.ts +++ b/packages/backend-core/src/context/tests/index.spec.js @@ -1,18 +1,9 @@ -import "../../../tests/utilities/TestConfiguration" -import * as context from ".." -import { DEFAULT_TENANT_ID } from "../../constants" -import env from "../../environment" - -// must use require to spy index file exports due to known issue in jest -const dbUtils = require("../../db") -jest.spyOn(dbUtils, "closeDB") -jest.spyOn(dbUtils, "dangerousGetDB") +require("../../../tests") +const context = require("../") +const { DEFAULT_TENANT_ID } = require("../../constants") +const env = require("../../environment") describe("context", () => { - beforeEach(() => { - jest.clearAllMocks() - }) - describe("doInTenant", () => { describe("single-tenancy", () => { it("defaults to the default tenant", () => { @@ -25,8 +16,6 @@ describe("context", () => { const db = context.getGlobalDB() expect(db.name).toBe("global-db") }) - expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) - expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) }) }) @@ -40,7 +29,7 @@ describe("context", () => { let error try { context.getTenantId() - } catch (e: any) { + } catch (e) { error = e } expect(error.message).toBe("Tenant id not found") @@ -59,7 +48,7 @@ describe("context", () => { let error try { context.getGlobalDB() - } catch (e: any) { + } catch (e) { error = e } expect(error.message).toBe("Global DB not found") @@ -85,8 +74,6 @@ describe("context", () => { const db = context.getGlobalDB() expect(db.name).toBe("test_global-db") }) - expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) - expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) }) it("sets the tenant id when nested with same tenant id", async () => { @@ -121,10 +108,6 @@ describe("context", () => { }) }) }) - - // only 1 db is opened and closed - expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) - expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) }) it("sets different tenant id inside another context", () => { diff --git a/packages/backend-core/src/context/utils.ts b/packages/backend-core/src/context/utils.ts deleted file mode 100644 index 6e7100b594..0000000000 --- a/packages/backend-core/src/context/utils.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - DEFAULT_TENANT_ID, - getAppId, - getTenantIDFromAppID, - updateTenantId, -} from "./index" -import cls from "./FunctionContext" -import { IdentityContext } from "@budibase/types" -import { ContextKey } from "./constants" -import { dangerousGetDB, closeDB } from "../db" -import { isEqual } from "lodash" -import { getDevelopmentAppID, getProdAppID } from "../db/conversions" -import env from "../environment" - -export async function updateUsing( - usingKey: string, - existing: boolean, - internal: (opts: { existing: boolean }) => Promise -) { - const using = cls.getFromContext(usingKey) - if (using && existing) { - cls.setOnContext(usingKey, using + 1) - return internal({ existing: true }) - } else { - return cls.run(async () => { - cls.setOnContext(usingKey, 1) - return internal({ existing: false }) - }) - } -} - -export async function closeWithUsing( - usingKey: string, - closeFn: () => Promise -) { - const using = cls.getFromContext(usingKey) - if (!using || using <= 1) { - await closeFn() - } else { - cls.setOnContext(usingKey, using - 1) - } -} - -export const setAppTenantId = (appId: string) => { - const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID - updateTenantId(appTenantId) -} - -export const setIdentity = (identity: IdentityContext | null) => { - cls.setOnContext(ContextKey.IDENTITY, identity) -} - -// this function makes sure the PouchDB objects are closed and -// fully deleted when finished - this protects against memory leaks -export async function closeAppDBs() { - const dbKeys = [ContextKey.CURRENT_DB, ContextKey.PROD_DB, ContextKey.DEV_DB] - for (let dbKey of dbKeys) { - const db = cls.getFromContext(dbKey) - if (!db) { - continue - } - await closeDB(db) - // clear the DB from context, incase someone tries to use it again - cls.setOnContext(dbKey, null) - } - // clear the app ID now that the databases are closed - if (cls.getFromContext(ContextKey.APP_ID)) { - cls.setOnContext(ContextKey.APP_ID, null) - } - if (cls.getFromContext(ContextKey.DB_OPTS)) { - cls.setOnContext(ContextKey.DB_OPTS, null) - } -} - -export function getContextDB(key: string, opts: any) { - const dbOptsKey = `${key}${ContextKey.DB_OPTS}` - let storedOpts = cls.getFromContext(dbOptsKey) - let db = cls.getFromContext(key) - if (db && isEqual(opts, storedOpts)) { - return db - } - - const appId = getAppId() - let toUseAppId - - switch (key) { - case ContextKey.CURRENT_DB: - toUseAppId = appId - break - case ContextKey.PROD_DB: - toUseAppId = getProdAppID(appId) - break - case ContextKey.DEV_DB: - toUseAppId = getDevelopmentAppID(appId) - break - } - db = dangerousGetDB(toUseAppId, opts) - try { - cls.setOnContext(key, db) - if (opts) { - cls.setOnContext(dbOptsKey, opts) - } - } catch (err) { - if (!env.isTest()) { - throw err - } - } - return db -} diff --git a/packages/backend-core/src/db/Replication.ts b/packages/backend-core/src/db/Replication.ts index e0bd3c7a43..eb9d613a58 100644 --- a/packages/backend-core/src/db/Replication.ts +++ b/packages/backend-core/src/db/Replication.ts @@ -1,5 +1,5 @@ -import { dangerousGetDB, closeDB } from "." -import { DocumentType } from "./constants" +import { getPouchDB, closePouchDB } from "./couch" +import { DocumentType } from "../constants" class Replication { source: any @@ -12,12 +12,12 @@ class Replication { * @param {String} target - the DB you want to replicate to, or rollback from */ constructor({ source, target }: any) { - this.source = dangerousGetDB(source) - this.target = dangerousGetDB(target) + this.source = getPouchDB(source) + this.target = getPouchDB(target) } close() { - return Promise.all([closeDB(this.source), closeDB(this.target)]) + return Promise.all([closePouchDB(this.source), closePouchDB(this.target)]) } promisify(operation: any, opts = {}) { @@ -68,7 +68,7 @@ class Replication { async rollback() { await this.target.destroy() // Recreate the DB again - this.target = dangerousGetDB(this.target.name) + this.target = getPouchDB(this.target.name) // take the opportunity to remove deleted tombstones await this.replicate() } diff --git a/packages/backend-core/src/db/conversions.js b/packages/backend-core/src/db/conversions.ts similarity index 68% rename from packages/backend-core/src/db/conversions.js rename to packages/backend-core/src/db/conversions.ts index 5b1a785ecc..381c5cb90f 100644 --- a/packages/backend-core/src/db/conversions.js +++ b/packages/backend-core/src/db/conversions.ts @@ -1,32 +1,33 @@ +import { APP_DEV_PREFIX, APP_PREFIX } from "../constants" +import { App } from "@budibase/types" const NO_APP_ERROR = "No app provided" -const { APP_DEV_PREFIX, APP_PREFIX } = require("./constants") -exports.isDevAppID = appId => { +export function isDevAppID(appId?: string) { if (!appId) { throw NO_APP_ERROR } return appId.startsWith(APP_DEV_PREFIX) } -exports.isProdAppID = appId => { +export function isProdAppID(appId?: string) { if (!appId) { throw NO_APP_ERROR } - return appId.startsWith(APP_PREFIX) && !exports.isDevAppID(appId) + return appId.startsWith(APP_PREFIX) && !isDevAppID(appId) } -exports.isDevApp = app => { +export function isDevApp(app: App) { if (!app) { throw NO_APP_ERROR } - return exports.isDevAppID(app.appId) + return isDevAppID(app.appId) } /** * Generates a development app ID from a real app ID. * @returns {string} the dev app ID which can be used for dev database. */ -exports.getDevelopmentAppID = appId => { +export function getDevelopmentAppID(appId: string) { if (!appId || appId.startsWith(APP_DEV_PREFIX)) { return appId } @@ -36,12 +37,12 @@ exports.getDevelopmentAppID = appId => { const rest = split.join(APP_PREFIX) return `${APP_DEV_PREFIX}${rest}` } -exports.getDevAppID = exports.getDevelopmentAppID +export const getDevAppID = getDevelopmentAppID /** * Convert a development app ID to a deployed app ID. */ -exports.getProdAppID = appId => { +export function getProdAppID(appId: string) { if (!appId || !appId.startsWith(APP_DEV_PREFIX)) { return appId } @@ -52,7 +53,7 @@ exports.getProdAppID = appId => { return `${APP_PREFIX}${rest}` } -exports.extractAppUUID = id => { +export function extractAppUUID(id: string) { const split = id?.split("_") || [] return split.length ? split[split.length - 1] : null } diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts new file mode 100644 index 0000000000..de06b4e8ee --- /dev/null +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -0,0 +1,193 @@ +import Nano from "nano" +import { + AllDocsResponse, + AnyDocument, + Database, + DatabaseOpts, + DatabaseQueryOpts, + DatabasePutOpts, + DatabaseCreateIndexOpts, + DatabaseDeleteIndexOpts, + Document, + isDocument, +} from "@budibase/types" +import { getCouchInfo } from "./connections" +import { directCouchCall } from "./utils" +import { getPouchDB } from "./pouchDB" +import { WriteStream, ReadStream } from "fs" + +export class DatabaseImpl implements Database { + public readonly name: string + private static nano: Nano.ServerScope + private readonly pouchOpts: DatabaseOpts + + constructor(dbName?: string, opts?: DatabaseOpts) { + if (dbName == null) { + throw new Error("Database name cannot be undefined.") + } + this.name = dbName + this.pouchOpts = opts || {} + if (!DatabaseImpl.nano) { + DatabaseImpl.init() + } + } + + static init() { + const couchInfo = getCouchInfo() + DatabaseImpl.nano = Nano({ + url: couchInfo.url, + requestDefaults: { + headers: { + Authorization: couchInfo.cookie, + }, + }, + parseUrl: false, + }) + } + + async exists() { + let response = await directCouchCall(`/${this.name}`, "HEAD") + return response.status === 200 + } + + async checkSetup() { + let shouldCreate = !this.pouchOpts?.skip_setup + // check exists in a lightweight fashion + let exists = await this.exists() + if (!shouldCreate && !exists) { + throw new Error("DB does not exist") + } + if (!exists) { + await DatabaseImpl.nano.db.create(this.name) + } + return DatabaseImpl.nano.db.use(this.name) + } + + private async updateOutput(fnc: any) { + try { + return await fnc() + } catch (err: any) { + if (err.statusCode) { + err.status = err.statusCode + } + throw err + } + } + + async get(id?: string): Promise { + const db = await this.checkSetup() + if (!id) { + throw new Error("Unable to get doc without a valid _id.") + } + return this.updateOutput(() => db.get(id)) + } + + async remove(idOrDoc: string | Document, rev?: string) { + const db = await this.checkSetup() + let _id: string + let _rev: string + + if (isDocument(idOrDoc)) { + _id = idOrDoc._id! + _rev = idOrDoc._rev! + } else { + _id = idOrDoc + _rev = rev! + } + + if (!_id || !_rev) { + throw new Error("Unable to remove doc without a valid _id and _rev.") + } + return this.updateOutput(() => db.destroy(_id, _rev)) + } + + async put(document: AnyDocument, opts?: DatabasePutOpts) { + if (!document._id) { + throw new Error("Cannot store document without _id field.") + } + const db = await this.checkSetup() + if (!document.createdAt) { + document.createdAt = new Date().toISOString() + } + document.updatedAt = new Date().toISOString() + if (opts?.force && document._id) { + try { + const existing = await this.get(document._id) + if (existing) { + document._rev = existing._rev + } + } catch (err: any) { + if (err.status !== 404) { + throw err + } + } + } + return this.updateOutput(() => db.insert(document)) + } + + async bulkDocs(documents: AnyDocument[]) { + const db = await this.checkSetup() + return this.updateOutput(() => db.bulk({ docs: documents })) + } + + async allDocs(params: DatabaseQueryOpts): Promise> { + const db = await this.checkSetup() + return this.updateOutput(() => db.list(params)) + } + + async query( + viewName: string, + params: DatabaseQueryOpts + ): Promise> { + const db = await this.checkSetup() + const [database, view] = viewName.split("/") + return this.updateOutput(() => db.view(database, view, params)) + } + + async destroy() { + try { + await DatabaseImpl.nano.db.destroy(this.name) + } catch (err: any) { + // didn't exist, don't worry + if (err.statusCode === 404) { + return + } else { + throw { ...err, status: err.statusCode } + } + } + } + + async compact() { + const db = await this.checkSetup() + return this.updateOutput(() => db.compact()) + } + + // All below functions are in-frequently called, just utilise PouchDB + // for them as it implements them better than we can + async dump(stream: WriteStream, opts?: { filter?: any }) { + const pouch = getPouchDB(this.name) + // @ts-ignore + return pouch.dump(stream, opts) + } + + async load(stream: ReadStream) { + const pouch = getPouchDB(this.name) + // @ts-ignore + return pouch.load(stream) + } + + async createIndex(opts: DatabaseCreateIndexOpts) { + const pouch = getPouchDB(this.name) + return pouch.createIndex(opts) + } + + async deleteIndex(opts: DatabaseDeleteIndexOpts) { + const pouch = getPouchDB(this.name) + return pouch.deleteIndex(opts) + } + + async getIndexes() { + const pouch = getPouchDB(this.name) + return pouch.getIndexes() + } +} diff --git a/packages/backend-core/src/db/pouch.ts b/packages/backend-core/src/db/couch/connections.ts similarity index 58% rename from packages/backend-core/src/db/pouch.ts rename to packages/backend-core/src/db/couch/connections.ts index 1e37da9240..a2206de634 100644 --- a/packages/backend-core/src/db/pouch.ts +++ b/packages/backend-core/src/db/couch/connections.ts @@ -1,5 +1,37 @@ -import PouchDB from "pouchdb" -import env from "../environment" +import env from "../../environment" + +export const getCouchInfo = () => { + const urlInfo = getUrlInfo() + let username + let password + if (env.COUCH_DB_USERNAME) { + // set from env + username = env.COUCH_DB_USERNAME + } else if (urlInfo.auth.username) { + // set from url + username = urlInfo.auth.username + } else if (!env.isTest()) { + throw new Error("CouchDB username not set") + } + if (env.COUCH_DB_PASSWORD) { + // set from env + password = env.COUCH_DB_PASSWORD + } else if (urlInfo.auth.password) { + // set from url + password = urlInfo.auth.password + } else if (!env.isTest()) { + throw new Error("CouchDB password not set") + } + const authCookie = Buffer.from(`${username}:${password}`).toString("base64") + return { + url: urlInfo.url!, + auth: { + username: username, + password: password, + }, + cookie: `Basic ${authCookie}`, + } +} export const getUrlInfo = (url = env.COUCH_DB_URL) => { let cleanUrl, username, password, host @@ -43,85 +75,3 @@ export const getUrlInfo = (url = env.COUCH_DB_URL) => { }, } } - -export const getCouchInfo = () => { - const urlInfo = getUrlInfo() - let username - let password - if (env.COUCH_DB_USERNAME) { - // set from env - username = env.COUCH_DB_USERNAME - } else if (urlInfo.auth.username) { - // set from url - username = urlInfo.auth.username - } else if (!env.isTest()) { - throw new Error("CouchDB username not set") - } - if (env.COUCH_DB_PASSWORD) { - // set from env - password = env.COUCH_DB_PASSWORD - } else if (urlInfo.auth.password) { - // set from url - password = urlInfo.auth.password - } else if (!env.isTest()) { - throw new Error("CouchDB password not set") - } - const authCookie = Buffer.from(`${username}:${password}`).toString("base64") - return { - url: urlInfo.url, - auth: { - username: username, - password: password, - }, - cookie: `Basic ${authCookie}`, - } -} - -/** - * Return a constructor for PouchDB. - * This should be rarely used outside of the main application config. - * Exposed for exceptional cases such as in-memory views. - */ -export const getPouch = (opts: any = {}) => { - let { url, cookie } = getCouchInfo() - let POUCH_DB_DEFAULTS = { - prefix: url, - fetch: (url: string, opts: any) => { - // use a specific authorization cookie - be very explicit about how we authenticate - opts.headers.set("Authorization", cookie) - return PouchDB.fetch(url, opts) - }, - } - - if (opts.inMemory) { - const inMemory = require("pouchdb-adapter-memory") - PouchDB.plugin(inMemory) - POUCH_DB_DEFAULTS = { - prefix: undefined, - // @ts-ignore - adapter: "memory", - } - } - - if (opts.onDisk) { - POUCH_DB_DEFAULTS = { - prefix: undefined, - // @ts-ignore - adapter: "leveldb", - } - } - - if (opts.replication) { - const replicationStream = require("pouchdb-replication-stream") - PouchDB.plugin(replicationStream.plugin) - // @ts-ignore - PouchDB.adapter("writableStream", replicationStream.adapters.writableStream) - } - - if (opts.find) { - const find = require("pouchdb-find") - PouchDB.plugin(find) - } - - return PouchDB.defaults(POUCH_DB_DEFAULTS) -} diff --git a/packages/backend-core/src/db/couch/index.ts b/packages/backend-core/src/db/couch/index.ts new file mode 100644 index 0000000000..c731d20d6c --- /dev/null +++ b/packages/backend-core/src/db/couch/index.ts @@ -0,0 +1,4 @@ +export * from "./connections" +export * from "./DatabaseImpl" +export * from "./utils" +export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB" diff --git a/packages/backend-core/src/db/couch/pouchDB.ts b/packages/backend-core/src/db/couch/pouchDB.ts new file mode 100644 index 0000000000..a6f4323d88 --- /dev/null +++ b/packages/backend-core/src/db/couch/pouchDB.ts @@ -0,0 +1,97 @@ +import PouchDB from "pouchdb" +import env from "../../environment" +import { PouchOptions } from "@budibase/types" +import { getCouchInfo } from "./connections" + +let Pouch: any +let initialised = false + +/** + * Return a constructor for PouchDB. + * This should be rarely used outside of the main application config. + * Exposed for exceptional cases such as in-memory views. + */ +export const getPouch = (opts: PouchOptions = {}) => { + let { url, cookie } = getCouchInfo() + let POUCH_DB_DEFAULTS = { + prefix: url, + fetch: (url: string, opts: any) => { + // use a specific authorization cookie - be very explicit about how we authenticate + opts.headers.set("Authorization", cookie) + return PouchDB.fetch(url, opts) + }, + } + + if (opts.inMemory) { + const inMemory = require("pouchdb-adapter-memory") + PouchDB.plugin(inMemory) + POUCH_DB_DEFAULTS = { + // @ts-ignore + adapter: "memory", + } + } + + if (opts.onDisk) { + POUCH_DB_DEFAULTS = { + // @ts-ignore + adapter: "leveldb", + } + } + + if (opts.replication) { + const replicationStream = require("pouchdb-replication-stream") + PouchDB.plugin(replicationStream.plugin) + // @ts-ignore + PouchDB.adapter("writableStream", replicationStream.adapters.writableStream) + } + + if (opts.find) { + const find = require("pouchdb-find") + PouchDB.plugin(find) + } + + return PouchDB.defaults(POUCH_DB_DEFAULTS) +} + +export function init(opts?: PouchOptions) { + Pouch = getPouch(opts) + initialised = true +} + +const checkInitialised = () => { + if (!initialised) { + throw new Error("init has not been called") + } +} + +export function getPouchDB(dbName: string, opts?: any): PouchDB.Database { + checkInitialised() + const db = new Pouch(dbName, opts) + const dbPut = db.put + db.put = async (doc: any, options = {}) => { + if (!doc.createdAt) { + doc.createdAt = new Date().toISOString() + } + doc.updatedAt = new Date().toISOString() + return dbPut(doc, options) + } + db.exists = async () => { + const info = await db.info() + return !info.error + } + return db +} + +// use this function if you have called getPouchDB - close +// the databases you've opened once finished +export async function closePouchDB(db: PouchDB.Database) { + if (!db || env.isTest()) { + return + } + try { + // specifically await so that if there is an error, it can be ignored + return await db.close() + } catch (err) { + // ignore error, already closed + } +} diff --git a/packages/backend-core/src/db/couch/utils.ts b/packages/backend-core/src/db/couch/utils.ts new file mode 100644 index 0000000000..426bf92158 --- /dev/null +++ b/packages/backend-core/src/db/couch/utils.ts @@ -0,0 +1,36 @@ +import { getCouchInfo } from "./connections" +import fetch from "node-fetch" +import { checkSlashesInUrl } from "../../helpers" + +export async function directCouchCall( + path: string, + method: string = "GET", + body?: any +) { + let { url, cookie } = getCouchInfo() + const couchUrl = `${url}/${path}` + const params: any = { + method: method, + headers: { + Authorization: cookie, + }, + } + if (body && method !== "GET") { + params.body = JSON.stringify(body) + params.headers["Content-Type"] = "application/json" + } + return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params) +} + +export async function directCouchQuery( + path: string, + method: string = "GET", + body?: any +) { + const response = await directCouchCall(path, method, body) + if (response.status < 300) { + return await response.json() + } else { + throw "Cannot connect to CouchDB instance" + } +} diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts new file mode 100644 index 0000000000..3887e8b09f --- /dev/null +++ b/packages/backend-core/src/db/db.ts @@ -0,0 +1,46 @@ +import env from "../environment" +import { directCouchQuery, getPouchDB } from "./couch" +import { CouchFindOptions, Database } from "@budibase/types" +import { DatabaseImpl } from "../db" + +const dbList = new Set() + +export function getDB(dbName?: string, opts?: any): Database { + // TODO: once using the test image, need to remove this + if (env.isTest()) { + dbList.add(dbName) + // @ts-ignore + return getPouchDB(dbName, opts) + } + return new DatabaseImpl(dbName, opts) +} + +// we have to use a callback for this so that we can close +// the DB when we're done, without this manual requests would +// need to close the database when done with it to avoid memory leaks +export async function doWithDB(dbName: string, cb: any, opts = {}) { + const db = getDB(dbName, opts) + // need this to be async so that we can correctly close DB after all + // async operations have been completed + return await cb(db) +} + +export function allDbs() { + if (!env.isTest()) { + throw new Error("Cannot be used outside test environment.") + } + return [...dbList] +} + +export async function directCouchAllDbs(queryString?: string) { + let couchPath = "/_all_dbs" + if (queryString) { + couchPath += `?${queryString}` + } + return await directCouchQuery(couchPath) +} + +export async function directCouchFind(dbName: string, opts: CouchFindOptions) { + const json = await directCouchQuery(`${dbName}/_find`, "POST", opts) + return { rows: json.docs, bookmark: json.bookmark } +} diff --git a/packages/backend-core/src/db/index.ts b/packages/backend-core/src/db/index.ts index 429cd61fc1..0d9f75fa18 100644 --- a/packages/backend-core/src/db/index.ts +++ b/packages/backend-core/src/db/index.ts @@ -1,133 +1,9 @@ -import * as pouch from "./pouch" -import env from "../environment" -import { checkSlashesInUrl } from "../helpers" -import fetch from "node-fetch" -import { PouchOptions, CouchFindOptions } from "@budibase/types" -import PouchDB from "pouchdb" - -const openDbs: string[] = [] -let Pouch: any -let initialised = false -const dbList = new Set() - -if (env.MEMORY_LEAK_CHECK) { - setInterval(() => { - console.log("--- OPEN DBS ---") - console.log(openDbs) - }, 5000) -} - -const put = - (dbPut: any) => - async (doc: any, options = {}) => { - if (!doc.createdAt) { - doc.createdAt = new Date().toISOString() - } - doc.updatedAt = new Date().toISOString() - return dbPut(doc, options) - } - -const checkInitialised = () => { - if (!initialised) { - throw new Error("init has not been called") - } -} - -export async function init(opts?: PouchOptions) { - Pouch = pouch.getPouch(opts) - initialised = true -} - -// NOTE: THIS IS A DANGEROUS FUNCTION - USE WITH CAUTION -// this function is prone to leaks, should only be used -// in situations that using the function doWithDB does not work -export function dangerousGetDB(dbName: string, opts?: any): PouchDB.Database { - checkInitialised() - if (env.isTest()) { - dbList.add(dbName) - } - const db = new Pouch(dbName, opts) - if (env.MEMORY_LEAK_CHECK) { - openDbs.push(db.name) - } - const dbPut = db.put - db.put = put(dbPut) - return db -} - -// use this function if you have called dangerousGetDB - close -// the databases you've opened once finished -export async function closeDB(db: PouchDB.Database) { - if (!db || env.isTest()) { - return - } - if (env.MEMORY_LEAK_CHECK) { - openDbs.splice(openDbs.indexOf(db.name), 1) - } - try { - // specifically await so that if there is an error, it can be ignored - return await db.close() - } catch (err) { - // ignore error, already closed - } -} - -// we have to use a callback for this so that we can close -// the DB when we're done, without this manual requests would -// need to close the database when done with it to avoid memory leaks -export async function doWithDB(dbName: string, cb: any, opts = {}) { - const db = dangerousGetDB(dbName, opts) - // need this to be async so that we can correctly close DB after all - // async operations have been completed - try { - return await cb(db) - } finally { - await closeDB(db) - } -} - -export function allDbs() { - if (!env.isTest()) { - throw new Error("Cannot be used outside test environment.") - } - checkInitialised() - return [...dbList] -} - -export async function directCouchQuery( - path: string, - method: string = "GET", - body?: any -) { - let { url, cookie } = pouch.getCouchInfo() - const couchUrl = `${url}/${path}` - const params: any = { - method: method, - headers: { - Authorization: cookie, - }, - } - if (body && method !== "GET") { - params.body = JSON.stringify(body) - params.headers["Content-Type"] = "application/json" - } - const response = await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params) - if (response.status < 300) { - return await response.json() - } else { - throw "Cannot connect to CouchDB instance" - } -} - -export async function directCouchAllDbs(queryString?: string) { - let couchPath = "/_all_dbs" - if (queryString) { - couchPath += `?${queryString}` - } - return await directCouchQuery(couchPath) -} - -export async function directCouchFind(dbName: string, opts: CouchFindOptions) { - const json = await directCouchQuery(`${dbName}/_find`, "POST", opts) - return { rows: json.docs, bookmark: json.bookmark } -} +export * from "./couch" +export * from "./db" +export * from "./utils" +export * from "./views" +export * from "./conversions" +export { default as Replication } from "./Replication" +// exports to support old export structure +export * from "../constants/db" +export { getGlobalDBName, baseGlobalDBName } from "../context" diff --git a/packages/backend-core/src/db/tenancy.ts b/packages/backend-core/src/db/tenancy.ts deleted file mode 100644 index d920f7cd41..0000000000 --- a/packages/backend-core/src/db/tenancy.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DEFAULT_TENANT_ID } from "../constants" -import { StaticDatabases, SEPARATOR } from "./constants" -import { getTenantId } from "../context" - -export const getGlobalDBName = (tenantId?: string) => { - // tenant ID can be set externally, for example user API where - // new tenants are being created, this may be the case - if (!tenantId) { - tenantId = getTenantId() - } - return baseGlobalDBName(tenantId) -} - -export const baseGlobalDBName = (tenantId: string | undefined | null) => { - let dbName - if (!tenantId || tenantId === DEFAULT_TENANT_ID) { - dbName = StaticDatabases.GLOBAL.name - } else { - dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` - } - return dbName -} diff --git a/packages/backend-core/src/db/tests/index.spec.js b/packages/backend-core/src/db/tests/index.spec.js index bc0c638126..fc0094d354 100644 --- a/packages/backend-core/src/db/tests/index.spec.js +++ b/packages/backend-core/src/db/tests/index.spec.js @@ -1,11 +1,11 @@ -require("../../../tests/utilities/TestConfiguration") -const { dangerousGetDB } = require("../") +require("../../../tests") +const { getDB } = require("../") describe("db", () => { describe("getDB", () => { it("returns a db", async () => { - const db = dangerousGetDB("test") + const db = getDB("test") expect(db).toBeDefined() expect(db._adapter).toBe("memory") expect(db.prefix).toBe("_pouch_") @@ -13,7 +13,7 @@ describe("db", () => { }) it("uses the custom put function", async () => { - const db = dangerousGetDB("test") + const db = getDB("test") let doc = { _id: "test" } await db.put(doc) doc = await db.get(doc._id) diff --git a/packages/backend-core/src/db/tests/pouch.spec.js b/packages/backend-core/src/db/tests/pouch.spec.js index 30cdd0f5ec..f0abc82240 100644 --- a/packages/backend-core/src/db/tests/pouch.spec.js +++ b/packages/backend-core/src/db/tests/pouch.spec.js @@ -1,5 +1,5 @@ -require("../../../tests/utilities/TestConfiguration") -const getUrlInfo = require("../pouch").getUrlInfo +require("../../../tests") +const getUrlInfo = require("../couch").getUrlInfo describe("pouch", () => { describe("Couch DB URL parsing", () => { diff --git a/packages/backend-core/src/db/tests/utils.spec.js b/packages/backend-core/src/db/tests/utils.spec.js index 5f9a224e7a..f95889c1cc 100644 --- a/packages/backend-core/src/db/tests/utils.spec.js +++ b/packages/backend-core/src/db/tests/utils.spec.js @@ -1,15 +1,17 @@ -require("../../../tests/utilities/TestConfiguration"); +require("../../../tests") const { - generateAppID, getDevelopmentAppID, getProdAppID, isDevAppID, isProdAppID, +} = require("../conversions") +const { + generateAppID, getPlatformUrl, getScopedConfig } = require("../utils") -const tenancy = require("../../tenancy"); -const { Configs, DEFAULT_TENANT_ID } = require("../../constants"); +const tenancy = require("../../tenancy") +const { Config, DEFAULT_TENANT_ID } = require("../../constants") const env = require("../../environment") describe("utils", () => { @@ -77,7 +79,7 @@ const setDbPlatformUrl = async () => { const db = tenancy.getGlobalDB() db.put({ _id: "config_settings", - type: Configs.SETTINGS, + type: Config.SETTINGS, config: { platformUrl: DB_URL } @@ -178,7 +180,7 @@ describe("getScopedConfig", () => { await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { await setDbPlatformUrl() const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Configs.SETTINGS }) + const config = await getScopedConfig(db, { type: Config.SETTINGS }) expect(config.platformUrl).toBe(DB_URL) }) }) @@ -186,7 +188,7 @@ describe("getScopedConfig", () => { it("returns the platform url without an existing config", async () => { await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Configs.SETTINGS }) + 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 c04da5da4f..590c3eeef8 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -1,31 +1,26 @@ -import { newid } from "../hashing" -import { DEFAULT_TENANT_ID, Configs } from "../constants" +import { newid } from "../newid" import env from "../environment" import { + DEFAULT_TENANT_ID, SEPARATOR, DocumentType, UNICODE_MAX, ViewName, InternalTable, -} from "./constants" -import { getTenantId, getGlobalDB } from "../context" -import { getGlobalDBName } from "./tenancy" -import { doWithDB, allDbs, directCouchAllDbs } from "./index" + APP_PREFIX, +} from "../constants" +import { getTenantId, getGlobalDB, getGlobalDBName } from "../context" +import { doWithDB, allDbs, directCouchAllDbs } from "./db" import { getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "./conversions" -import { APP_PREFIX } from "./constants" import * as events from "../events" - -export * from "./constants" -export * from "./conversions" -export { default as Replication } from "./Replication" -export * from "./tenancy" +import { App, Database, ConfigType } from "@budibase/types" /** * Generates a new app ID. * @returns {string} The new app ID which the app doc can be stored under. */ -export const generateAppID = (tenantId = null) => { +export const generateAppID = (tenantId?: string | null) => { let id = APP_PREFIX if (tenantId) { id += `${tenantId}${SEPARATOR}` @@ -170,7 +165,7 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) { /** * Gets parameters for retrieving users, this is a utility function for the getDocParams function. */ -export function getUserMetadataParams(userId?: string, otherProps = {}) { +export function getUserMetadataParams(userId?: string | null, otherProps = {}) { return getRowParams(InternalTable.USER_METADATA, userId, otherProps) } @@ -243,18 +238,18 @@ export function getTemplateParams( * Generates a new role ID. * @returns {string} The new role ID which the role doc can be stored under. */ -export function generateRoleID(id: any) { +export function generateRoleID(id?: any) { return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}` } /** * Gets parameters for retrieving a role, this is a utility function for the getDocParams function. */ -export function getRoleParams(roleId = null, otherProps = {}) { +export function getRoleParams(roleId?: string | null, otherProps = {}) { return getDocParams(DocumentType.ROLE, roleId, otherProps) } -export function getStartEndKeyURL(baseKey: any, tenantId = null) { +export function getStartEndKeyURL(baseKey: any, tenantId?: string) { const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : "" return `startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"` } @@ -301,7 +296,12 @@ export async function getAllDbs(opts = { efficient: false }) { * * @return {Promise} returns the app information document stored in each app database. */ -export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) { +export async function getAllApps({ + dev, + all, + idsOnly, + efficient, +}: any = {}): Promise { let tenantId = getTenantId() if (!env.MULTI_TENANCY && !tenantId) { tenantId = DEFAULT_TENANT_ID @@ -373,35 +373,23 @@ export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) { * Utility function for getAllApps but filters to production apps only. */ export async function getProdAppIDs() { - return (await getAllApps({ idsOnly: true })).filter( - (id: any) => !isDevAppID(id) - ) + const apps = (await getAllApps({ idsOnly: true })) as string[] + return apps.filter((id: any) => !isDevAppID(id)) } /** * Utility function for the inverse of above. */ export async function getDevAppIDs() { - return (await getAllApps({ idsOnly: true })).filter((id: any) => - isDevAppID(id) - ) + const apps = (await getAllApps({ idsOnly: true })) as string[] + return apps.filter((id: any) => isDevAppID(id)) } export async function dbExists(dbName: any) { - let exists = false return doWithDB( dbName, - async (db: any) => { - try { - // check if database exists - const info = await db.info() - if (info && !info.error) { - exists = true - } - } catch (err) { - exists = false - } - return exists + async (db: Database) => { + return await db.exists() }, { skip_setup: true } ) @@ -500,7 +488,7 @@ export const getScopedFullConfig = async function ( )[0] // custom logic for settings doc - if (type === Configs.SETTINGS) { + if (type === ConfigType.SETTINGS) { if (scopedConfig && scopedConfig.doc) { // overrides affected by environment variables scopedConfig.doc.config.platformUrl = await getPlatformUrl({ @@ -539,7 +527,7 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => { // get the doc directly instead of with getScopedConfig to prevent loop let settings try { - settings = await db.get(generateConfigID({ type: Configs.SETTINGS })) + settings = await db.get(generateConfigID({ type: ConfigType.SETTINGS })) } catch (e: any) { if (e.status !== 404) { throw e diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index f0fff918fc..4a87be0a68 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -1,8 +1,13 @@ -import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils" +import { + DocumentType, + ViewName, + DeprecatedViews, + SEPARATOR, + StaticDatabases, +} from "../constants" import { getGlobalDB } from "../context" -import PouchDB from "pouchdb" -import { StaticDatabases } from "./constants" import { doWithDB } from "./" +import { Database, DatabaseQueryOpts } from "@budibase/types" const DESIGN_DB = "_design/database" @@ -19,7 +24,7 @@ interface DesignDocument { views: any } -async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) { +async function removeDeprecated(db: Database, viewName: ViewName) { // @ts-ignore if (!DeprecatedViews[viewName]) { return @@ -70,16 +75,13 @@ export const createAccountEmailView = async () => { emit(doc.email.toLowerCase(), doc._id) } }` - await doWithDB( - StaticDatabases.PLATFORM_INFO.name, - async (db: PouchDB.Database) => { - await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) - } - ) + await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { + await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) + }) } export const createUserAppView = async () => { - const db = getGlobalDB() as PouchDB.Database + const db = getGlobalDB() const viewJs = `function(doc) { if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) { for (let prodAppId of Object.keys(doc.roles)) { @@ -117,12 +119,9 @@ export const createPlatformUserView = async () => { emit(doc._id.toLowerCase(), doc._id) } }` - await doWithDB( - StaticDatabases.PLATFORM_INFO.name, - async (db: PouchDB.Database) => { - await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) - } - ) + await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { + await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) + }) } export interface QueryViewOptions { @@ -131,25 +130,29 @@ export interface QueryViewOptions { export const queryView = async ( viewName: ViewName, - params: PouchDB.Query.Options, - db: PouchDB.Database, + params: DatabaseQueryOpts, + db: Database, createFunc: any, opts?: QueryViewOptions ): Promise => { try { - let response = await db.query(`database/${viewName}`, params) + let response = await db.query(`database/${viewName}`, params) const rows = response.rows - const docs = rows.map(row => (params.include_docs ? row.doc : row.value)) + const docs = rows.map((row: any) => + params.include_docs ? row.doc : row.value + ) // if arrayResponse has been requested, always return array regardless of length if (opts?.arrayResponse) { - return docs + return docs as T[] } else { // return the single document if there is only one - return docs.length <= 1 ? docs[0] : docs + return docs.length <= 1 ? (docs[0] as T) : (docs as T[]) } } catch (err: any) { - if (err != null && err.name === "not_found") { + const pouchNotFound = err && err.name === "not_found" + const couchNotFound = err && err.status === 404 + if (pouchNotFound || couchNotFound) { await removeDeprecated(db, viewName) await createFunc() return queryView(viewName, params, db, createFunc, opts) @@ -161,7 +164,7 @@ export const queryView = async ( export const queryPlatformView = async ( viewName: ViewName, - params: PouchDB.Query.Options, + params: DatabaseQueryOpts, opts?: QueryViewOptions ): Promise => { const CreateFuncByName: any = { @@ -169,19 +172,16 @@ export const queryPlatformView = async ( [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, } - return doWithDB( - StaticDatabases.PLATFORM_INFO.name, - async (db: PouchDB.Database) => { - const createFn = CreateFuncByName[viewName] - return queryView(viewName, params, db, createFn, opts) - } - ) + return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { + const createFn = CreateFuncByName[viewName] + return queryView(viewName, params, db, createFn, opts) + }) } export const queryGlobalView = async ( viewName: ViewName, - params: PouchDB.Query.Options, - db?: PouchDB.Database, + params: DatabaseQueryOpts, + db?: Database, opts?: QueryViewOptions ): Promise => { const CreateFuncByName: any = { @@ -192,8 +192,8 @@ export const queryGlobalView = async ( } // can pass DB in if working with something specific if (!db) { - db = getGlobalDB() as PouchDB.Database + db = getGlobalDB() } const createFn = CreateFuncByName[viewName] - return queryView(viewName, params, db, createFn, opts) + return queryView(viewName, params, db!, createFn, opts) } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 6e2ac94be9..2377c8ceba 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -1,9 +1,13 @@ function isTest() { - return ( - process.env.NODE_ENV === "jest" || - process.env.NODE_ENV === "cypress" || - process.env.JEST_WORKER_ID != null - ) + return isCypress() || isJest() +} + +function isJest() { + return !!(process.env.NODE_ENV === "jest" || process.env.JEST_WORKER_ID) +} + +function isCypress() { + return process.env.NODE_ENV === "cypress" } function isDev() { @@ -25,8 +29,9 @@ const DefaultBucketName = { PLUGINS: "plugins", } -const env = { +const environment = { isTest, + isJest, isDev, JS_BCRYPT: process.env.JS_BCRYPT, JWT_SECRET: process.env.JWT_SECRET, @@ -69,24 +74,24 @@ const env = { DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, SERVICE: process.env.SERVICE || "budibase", - MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false, LOG_LEVEL: process.env.LOG_LEVEL, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", _set(key: any, value: any) { process.env[key] = value - module.exports[key] = value + // @ts-ignore + environment[key] = value }, } // clean up any environment variable edge cases -for (let [key, value] of Object.entries(env)) { +for (let [key, value] of Object.entries(environment)) { // handle the edge case of "0" to disable an environment variable if (value === "0") { // @ts-ignore - env[key] = 0 + environment[key] = 0 } } -export = env +export = environment diff --git a/packages/backend-core/src/events/analytics.ts b/packages/backend-core/src/events/analytics.ts index 802b6d6314..f621a9c98b 100644 --- a/packages/backend-core/src/events/analytics.ts +++ b/packages/backend-core/src/events/analytics.ts @@ -1,8 +1,8 @@ import env from "../environment" -import tenancy from "../tenancy" +import * as tenancy from "../tenancy" import * as dbUtils from "../db/utils" -import { Configs } from "../constants" -import { withCache, TTL, CacheKeys } from "../cache/generic" +import { Config } from "../constants" +import { withCache, TTL, CacheKey } from "../cache" export const enabled = async () => { // cloud - always use the environment variable @@ -13,7 +13,7 @@ export const enabled = async () => { // self host - prefer the settings doc // use cache as events have high throughput const enabledInDB = await withCache( - CacheKeys.ANALYTICS_ENABLED, + CacheKey.ANALYTICS_ENABLED, TTL.ONE_DAY, async () => { const settings = await getSettingsDoc() @@ -45,9 +45,7 @@ const getSettingsDoc = async () => { const db = tenancy.getGlobalDB() let settings try { - settings = await db.get( - dbUtils.generateConfigID({ type: Configs.SETTINGS }) - ) + settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS })) } catch (e: any) { if (e.status !== 404) { throw e diff --git a/packages/backend-core/src/events/backfill.ts b/packages/backend-core/src/events/backfill.ts index e4577c5ab4..c8025a8e4e 100644 --- a/packages/backend-core/src/events/backfill.ts +++ b/packages/backend-core/src/events/backfill.ts @@ -21,7 +21,7 @@ import { AppCreatedEvent, } from "@budibase/types" import * as context from "../context" -import { CacheKeys } from "../cache/generic" +import { CacheKey } from "../cache/generic" import * as cache from "../cache/generic" // LIFECYCLE @@ -48,18 +48,18 @@ export const end = async () => { // CRUD const getBackfillMetadata = async (): Promise => { - return cache.get(CacheKeys.BACKFILL_METADATA) + return cache.get(CacheKey.BACKFILL_METADATA) } const saveBackfillMetadata = async ( backfill: BackfillMetadata ): Promise => { // no TTL - deleted by backfill - return cache.store(CacheKeys.BACKFILL_METADATA, backfill) + return cache.store(CacheKey.BACKFILL_METADATA, backfill) } const deleteBackfillMetadata = async (): Promise => { - await cache.delete(CacheKeys.BACKFILL_METADATA) + await cache.destroy(CacheKey.BACKFILL_METADATA) } const clearEvents = async () => { @@ -70,7 +70,7 @@ const clearEvents = async () => { for (const key of keys) { // delete each key // don't use tenancy, already in the key - await cache.delete(key, { useTenancy: false }) + await cache.destroy(key, { useTenancy: false }) } } @@ -167,7 +167,7 @@ const getEventKey = (event?: Event, properties?: any) => { const tenantId = context.getTenantId() if (event) { - eventKey = `${CacheKeys.EVENTS}:${tenantId}:${event}` + eventKey = `${CacheKey.EVENTS}:${tenantId}:${event}` // use some properties to make the key more unique const custom = CUSTOM_PROPERTY_SUFFIX[event] @@ -176,7 +176,7 @@ const getEventKey = (event?: Event, properties?: any) => { eventKey = `${eventKey}:${suffix}` } } else { - eventKey = `${CacheKeys.EVENTS}:${tenantId}:*` + eventKey = `${CacheKey.EVENTS}:${tenantId}:*` } return eventKey diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index a29a6821cd..b93bd44968 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -19,10 +19,10 @@ import { } from "@budibase/types" import { processors } from "./processors" import * as dbUtils from "../db/utils" -import { Configs } from "../constants" -import * as hashing from "../hashing" +import { Config } from "../constants" +import { newid } from "../utils" import * as installation from "../installation" -import { withCache, TTL, CacheKeys } from "../cache/generic" +import { withCache, TTL, CacheKey } from "../cache/generic" const pkg = require("../../package.json") @@ -270,17 +270,17 @@ const getEventTenantId = async (tenantId: string): Promise => { const getUniqueTenantId = async (tenantId: string): Promise => { // make sure this tenantId always matches the tenantId in context return context.doInTenant(tenantId, () => { - return withCache(CacheKeys.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { + return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { const db = context.getGlobalDB() const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { - type: Configs.SETTINGS, + type: Config.SETTINGS, }) let uniqueTenantId: string if (config.config.uniqueTenantId) { return config.config.uniqueTenantId } else { - uniqueTenantId = `${hashing.newid()}_${tenantId}` + uniqueTenantId = `${newid()}_${tenantId}` config.config.uniqueTenantId = uniqueTenantId await db.put(config) return uniqueTenantId diff --git a/packages/backend-core/src/events/processors/posthog/rateLimiting.ts b/packages/backend-core/src/events/processors/posthog/rateLimiting.ts index 9c7b7876d6..89da10defa 100644 --- a/packages/backend-core/src/events/processors/posthog/rateLimiting.ts +++ b/packages/backend-core/src/events/processors/posthog/rateLimiting.ts @@ -1,5 +1,5 @@ import { Event } from "@budibase/types" -import { CacheKeys, TTL } from "../../../cache/generic" +import { CacheKey, TTL } from "../../../cache/generic" import * as cache from "../../../cache/generic" import * as context from "../../../context" @@ -74,7 +74,7 @@ export const limited = async (event: Event): Promise => { } const eventKey = (event: RateLimitedEvent) => { - let key = `${CacheKeys.EVENTS_RATE_LIMIT}:${event}` + let key = `${CacheKey.EVENTS_RATE_LIMIT}:${event}` if (isPerApp(event)) { key = key + ":" + context.getAppId() } 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 d14b697966..349a0427ac 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 @@ -1,9 +1,9 @@ -import "../../../../../tests/utilities/TestConfiguration" +import "../../../../../tests" import PosthogProcessor from "../PosthogProcessor" import { Event, IdentityType, Hosting } from "@budibase/types" const tk = require("timekeeper") import * as cache from "../../../../cache/generic" -import { CacheKeys } from "../../../../cache/generic" +import { CacheKey } from "../../../../cache/generic" import * as context from "../../../../context" const newIdentity = () => { @@ -19,7 +19,7 @@ describe("PosthogProcessor", () => { beforeEach(async () => { jest.clearAllMocks() await cache.bustCache( - `${CacheKeys.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}` + `${CacheKey.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}` ) }) @@ -89,7 +89,7 @@ describe("PosthogProcessor", () => { await processor.processEvent(Event.SERVED_BUILDER, identity, properties) await cache.bustCache( - `${CacheKeys.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}` + `${CacheKey.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}` ) tk.freeze(new Date(2022, 0, 1, 14, 0)) diff --git a/packages/backend-core/src/events/publishers/automation.ts b/packages/backend-core/src/events/publishers/automation.ts index 95f9cb8db6..8b2574b739 100644 --- a/packages/backend-core/src/events/publishers/automation.ts +++ b/packages/backend-core/src/events/publishers/automation.ts @@ -72,7 +72,7 @@ export async function stepCreated( automationId: automation._id as string, triggerId: automation.definition?.trigger?.id, triggerType: automation.definition?.trigger?.stepId, - stepId: step.id, + stepId: step.id!, stepType: step.stepId, } await publishEvent(Event.AUTOMATION_STEP_CREATED, properties, timestamp) @@ -87,7 +87,7 @@ export async function stepDeleted( automationId: automation._id as string, triggerId: automation.definition?.trigger?.id, triggerType: automation.definition?.trigger?.stepId, - stepId: step.id, + stepId: step.id!, stepType: step.stepId, } 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 0fc81da259..4a68364016 100644 --- a/packages/backend-core/src/events/publishers/backup.ts +++ b/packages/backend-core/src/events/publishers/backup.ts @@ -1,12 +1,34 @@ -import { AppBackup, AppBackupRestoreEvent, Event } from "@budibase/types" +import { + AppBackup, + AppBackupRestoreEvent, + AppBackupTriggeredEvent, + AppBackupTrigger, + AppBackupType, + Event, +} from "@budibase/types" import { publishEvent } from "../events" export async function appBackupRestored(backup: AppBackup) { const properties: AppBackupRestoreEvent = { appId: backup.appId, - backupName: backup.name!, + restoreId: backup._id!, backupCreatedAt: backup.timestamp, } await publishEvent(Event.APP_BACKUP_RESTORED, properties) } + +export async function appBackupTriggered( + appId: string, + backupId: string, + type: AppBackupType, + trigger: AppBackupTrigger +) { + const properties: AppBackupTriggeredEvent = { + appId: appId, + backupId, + type, + trigger, + } + await publishEvent(Event.APP_BACKUP_TRIGGERED, properties) +} diff --git a/packages/backend-core/src/featureFlags/index.js b/packages/backend-core/src/featureFlags/index.ts similarity index 70% rename from packages/backend-core/src/featureFlags/index.js rename to packages/backend-core/src/featureFlags/index.ts index 8a8162d0ba..71e226c976 100644 --- a/packages/backend-core/src/featureFlags/index.js +++ b/packages/backend-core/src/featureFlags/index.ts @@ -1,17 +1,17 @@ -const env = require("../environment") -const tenancy = require("../tenancy") +import env from "../environment" +import * as tenancy from "../tenancy" /** * Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. * The env var is formatted as: * tenant1:feature1:feature2,tenant2:feature1 */ -const getFeatureFlags = () => { +function getFeatureFlags() { if (!env.TENANT_FEATURE_FLAGS) { return } - const tenantFeatureFlags = {} + const tenantFeatureFlags: Record = {} env.TENANT_FEATURE_FLAGS.split(",").forEach(tenantToFeatures => { const [tenantId, ...features] = tenantToFeatures.split(":") @@ -29,13 +29,13 @@ const getFeatureFlags = () => { const TENANT_FEATURE_FLAGS = getFeatureFlags() -exports.isEnabled = featureFlag => { +export function isEnabled(featureFlag: string) { const tenantId = tenancy.getTenantId() - const flags = exports.getTenantFeatureFlags(tenantId) + const flags = getTenantFeatureFlags(tenantId) return flags.includes(featureFlag) } -exports.getTenantFeatureFlags = tenantId => { +export function getTenantFeatureFlags(tenantId: string) { const flags = [] if (TENANT_FEATURE_FLAGS) { @@ -53,8 +53,8 @@ exports.getTenantFeatureFlags = tenantId => { return flags } -exports.TenantFeatureFlag = { - LICENSING: "LICENSING", - GOOGLE_SHEETS: "GOOGLE_SHEETS", - USER_GROUPS: "USER_GROUPS", +export enum TenantFeatureFlag { + LICENSING = "LICENSING", + GOOGLE_SHEETS = "GOOGLE_SHEETS", + USER_GROUPS = "USER_GROUPS", } diff --git a/packages/worker/src/utilities/index.js b/packages/backend-core/src/helpers.ts similarity index 85% rename from packages/worker/src/utilities/index.js rename to packages/backend-core/src/helpers.ts index b402a82cf3..e1e065bd4e 100644 --- a/packages/worker/src/utilities/index.js +++ b/packages/backend-core/src/helpers.ts @@ -4,6 +4,6 @@ * @param {string} url The URL to test and remove any extra double slashes. * @return {string} The updated url. */ -exports.checkSlashesInUrl = url => { +export function checkSlashesInUrl(url: string) { return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2") } diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 659a56c051..a4d4ad0a80 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -4,30 +4,28 @@ import * as events from "./events" import * as migrations from "./migrations" import * as users from "./users" import * as roles from "./security/roles" +import * as permissions from "./security/permissions" import * as accounts from "./cloud/accounts" import * as installation from "./installation" import env from "./environment" -import tenancy from "./tenancy" -import featureFlags from "./featureFlags" +import * as tenancy from "./tenancy" +import * as featureFlags from "./featureFlags" import * as sessions from "./security/sessions" -import deprovisioning from "./context/deprovision" -import auth from "./auth" -import constants from "./constants" -import * as dbConstants from "./db/constants" +import * as deprovisioning from "./context/deprovision" +import * as auth from "./auth" +import * as constants from "./constants" import * as logging from "./logging" -import pino from "./pino" +import * as pino from "./pino" import * as middleware from "./middleware" -import plugins from "./plugin" -import encryption from "./security/encryption" +import * as plugins from "./plugin" +import * as encryption from "./security/encryption" import * as queue from "./queue" - -// mimic the outer package exports -import * as db from "./pkg/db" -import * as objectStore from "./pkg/objectStore" -import * as utils from "./pkg/utils" -import redis from "./pkg/redis" -import cache from "./pkg/cache" -import context from "./pkg/context" +import * as db from "./db" +import * as context from "./context" +import * as cache from "./cache" +import * as objectStore from "./objectStore" +import * as redis from "./redis" +import * as utils from "./utils" const init = (opts: any = {}) => { db.init(opts.db) @@ -36,7 +34,7 @@ const init = (opts: any = {}) => { const core = { init, db, - ...dbConstants, + ...constants, redis, locks: redis.redlock, objectStore, @@ -45,7 +43,6 @@ const core = { cache, auth, constants, - ...constants, migrations, env, accounts, @@ -65,6 +62,7 @@ const core = { middleware, encryption, queue, + permissions, } export = core diff --git a/packages/backend-core/src/installation.ts b/packages/backend-core/src/installation.ts index da9b6c5b76..4e78a508a5 100644 --- a/packages/backend-core/src/installation.ts +++ b/packages/backend-core/src/installation.ts @@ -1,16 +1,16 @@ -import * as hashing from "./hashing" +import { newid } from "./utils" import * as events from "./events" -import { StaticDatabases } from "./db/constants" +import { StaticDatabases } from "./db" import { doWithDB } from "./db" import { Installation, IdentityType } from "@budibase/types" import * as context from "./context" import semver from "semver" -import { bustCache, withCache, TTL, CacheKeys } from "./cache/generic" +import { bustCache, withCache, TTL, CacheKey } from "./cache/generic" const pkg = require("../package.json") export const getInstall = async (): Promise => { - return withCache(CacheKeys.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, { + return withCache(CacheKey.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, { useTenancy: false, }) } @@ -28,7 +28,7 @@ const getInstallFromDB = async (): Promise => { if (e.status === 404) { install = { _id: StaticDatabases.PLATFORM_INFO.docs.install, - installId: hashing.newid(), + installId: newid(), version: pkg.version, } const resp = await platformDb.put(install) @@ -50,7 +50,7 @@ const updateVersion = async (version: string): Promise => { const install = await getInstall() install.version = version await platformDb.put(install) - await bustCache(CacheKeys.INSTALLATION) + await bustCache(CacheKey.INSTALLATION) } ) } catch (e: any) { diff --git a/packages/backend-core/src/middleware/adminOnly.js b/packages/backend-core/src/middleware/adminOnly.ts similarity index 63% rename from packages/backend-core/src/middleware/adminOnly.js rename to packages/backend-core/src/middleware/adminOnly.ts index 4bfdf83848..30fdf2907b 100644 --- a/packages/backend-core/src/middleware/adminOnly.js +++ b/packages/backend-core/src/middleware/adminOnly.ts @@ -1,4 +1,6 @@ -module.exports = async (ctx, next) => { +import { BBContext } from "@budibase/types" + +export = async (ctx: BBContext, next: any) => { if ( !ctx.internal && (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) diff --git a/packages/backend-core/src/middleware/auditLog.js b/packages/backend-core/src/middleware/auditLog.js deleted file mode 100644 index c9063ae2e0..0000000000 --- a/packages/backend-core/src/middleware/auditLog.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = async (ctx, next) => { - // Placeholder for audit log middleware - return next() -} diff --git a/packages/backend-core/src/middleware/auditLog.ts b/packages/backend-core/src/middleware/auditLog.ts new file mode 100644 index 0000000000..a2c30ade8a --- /dev/null +++ b/packages/backend-core/src/middleware/auditLog.ts @@ -0,0 +1,6 @@ +import { BBContext } from "@budibase/types" + +export = async (ctx: BBContext | any, next: any) => { + // Placeholder for audit log middleware + return next() +} diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index a3c6b67cde..fcf07c50a5 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -1,17 +1,18 @@ -import { Cookies, Headers } from "../constants" +import { Cookie, Header } from "../constants" import { getCookie, clearCookie, openJwt } from "../utils" import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" import { buildMatcherRegex, matches } from "./matchers" -import { SEPARATOR } from "../db/constants" -import { ViewName } from "../db/utils" -import { queryGlobalView } from "../db/views" +import { SEPARATOR, queryGlobalView, ViewName } from "../db" import { getGlobalDB, doInTenant } from "../tenancy" import { decrypt } from "../security/encryption" -const identity = require("../context/identity") -const env = require("../environment") +import * as identity from "../context/identity" +import env from "../environment" +import { BBContext, EndpointMatcher } from "@budibase/types" -const ONE_MINUTE = env.SESSION_UPDATE_PERIOD || 60 * 1000 +const ONE_MINUTE = env.SESSION_UPDATE_PERIOD + ? parseInt(env.SESSION_UPDATE_PERIOD) + : 60 * 1000 interface FinaliseOpts { authenticated?: boolean @@ -42,13 +43,13 @@ async function checkApiKey(apiKey: string, populateUser?: Function) { return doInTenant(tenantId, async () => { const db = getGlobalDB() // api key is encrypted in the database - const userId = await queryGlobalView( + const userId = (await queryGlobalView( ViewName.BY_API_KEY, { key: apiKey, }, db - ) + )) as string if (userId) { return { valid: true, @@ -65,16 +66,16 @@ async function checkApiKey(apiKey: string, populateUser?: Function) { * The tenancy modules should not be used here and it should be assumed that the tenancy context * has not yet been populated. */ -export = ( - noAuthPatterns = [], - opts: { publicAllowed: boolean; populateUser?: Function } = { +export = function ( + noAuthPatterns: EndpointMatcher[] = [], + opts: { publicAllowed?: boolean; populateUser?: Function } = { publicAllowed: false, } -) => { +) { const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] - return async (ctx: any, next: any) => { + return async (ctx: BBContext | any, next: any) => { let publicEndpoint = false - const version = ctx.request.headers[Headers.API_VER] + const version = ctx.request.headers[Header.API_VER] // the path is not authenticated const found = matches(ctx, noAuthOptions) if (found) { @@ -82,10 +83,10 @@ export = ( } try { // check the actual user is authenticated first, try header or cookie - const headerToken = ctx.request.headers[Headers.TOKEN] - const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken) - const apiKey = ctx.request.headers[Headers.API_KEY] - const tenantId = ctx.request.headers[Headers.TENANT_ID] + const headerToken = ctx.request.headers[Header.TOKEN] + const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken) + const apiKey = ctx.request.headers[Header.API_KEY] + const tenantId = ctx.request.headers[Header.TENANT_ID] let authenticated = false, user = null, internal = false @@ -116,7 +117,7 @@ export = ( authenticated = false console.error("Auth Error", err?.message || err) // remove the cookie as the user does not exist anymore - clearCookie(ctx, Cookies.Auth) + clearCookie(ctx, Cookie.Auth) } } // this is an internal request, no user made it @@ -140,7 +141,7 @@ export = ( delete user.password } // be explicit - if (authenticated !== true) { + if (!authenticated) { authenticated = false } // isAuthenticated is a function, so use a variable to be able to check authed state @@ -152,9 +153,10 @@ export = ( return next() } } catch (err: any) { + console.error("Auth Error", err?.message || err) // invalid token, clear the cookie if (err && err.name === "JsonWebTokenError") { - clearCookie(ctx, Cookies.Auth) + clearCookie(ctx, Cookie.Auth) } // allow configuring for public access if ((opts && opts.publicAllowed) || publicEndpoint) { diff --git a/packages/backend-core/src/middleware/builderOnly.js b/packages/backend-core/src/middleware/builderOnly.ts similarity index 64% rename from packages/backend-core/src/middleware/builderOnly.js rename to packages/backend-core/src/middleware/builderOnly.ts index 2128626db4..e13882d7f6 100644 --- a/packages/backend-core/src/middleware/builderOnly.js +++ b/packages/backend-core/src/middleware/builderOnly.ts @@ -1,4 +1,6 @@ -module.exports = async (ctx, next) => { +import { BBContext } from "@budibase/types" + +export = async (ctx: BBContext, next: any) => { if ( !ctx.internal && (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) diff --git a/packages/backend-core/src/middleware/builderOrAdmin.js b/packages/backend-core/src/middleware/builderOrAdmin.ts similarity index 71% rename from packages/backend-core/src/middleware/builderOrAdmin.js rename to packages/backend-core/src/middleware/builderOrAdmin.ts index 6440766298..26664695f8 100644 --- a/packages/backend-core/src/middleware/builderOrAdmin.js +++ b/packages/backend-core/src/middleware/builderOrAdmin.ts @@ -1,4 +1,6 @@ -module.exports = async (ctx, next) => { +import { BBContext } from "@budibase/types" + +export = async (ctx: BBContext, next: any) => { if ( !ctx.internal && (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) && diff --git a/packages/backend-core/src/middleware/csrf.js b/packages/backend-core/src/middleware/csrf.ts similarity index 84% rename from packages/backend-core/src/middleware/csrf.js rename to packages/backend-core/src/middleware/csrf.ts index 12bd9473e6..654ba47e07 100644 --- a/packages/backend-core/src/middleware/csrf.js +++ b/packages/backend-core/src/middleware/csrf.ts @@ -1,5 +1,6 @@ -const { Headers } = require("../constants") -const { buildMatcherRegex, matches } = require("./matchers") +import { Header } from "../constants" +import { buildMatcherRegex, matches } from "./matchers" +import { BBContext, EndpointMatcher } from "@budibase/types" /** * GET, HEAD and OPTIONS methods are considered safe operations @@ -31,9 +32,11 @@ const INCLUDED_CONTENT_TYPES = [ * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern * */ -module.exports = (opts = { noCsrfPatterns: [] }) => { +export = function ( + opts: { noCsrfPatterns: EndpointMatcher[] } = { noCsrfPatterns: [] } +) { const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns) - return async (ctx, next) => { + return async (ctx: BBContext | any, next: any) => { // don't apply for excluded paths const found = matches(ctx, noCsrfOptions) if (found) { @@ -62,13 +65,13 @@ module.exports = (opts = { noCsrfPatterns: [] }) => { // apply csrf when there is a token in the session (new logins) // in future there should be a hard requirement that the token is present - const userToken = ctx.user.csrfToken + const userToken = ctx.user?.csrfToken if (!userToken) { return next() } // reject if no token in request or mismatch - const requestToken = ctx.get(Headers.CSRF_TOKEN) + const requestToken = ctx.get(Header.CSRF_TOKEN) if (!requestToken || requestToken !== userToken) { ctx.throw(403, "Invalid CSRF token") } diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index 998c231b3d..2b332f5c49 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -1,18 +1,18 @@ -const jwt = require("./passport/jwt") -const local = require("./passport/local") -const google = require("./passport/google") -const oidc = require("./passport/oidc") -const { authError, ssoCallbackUrl } = require("./passport/utils") -const authenticated = require("./authenticated") -const auditLog = require("./auditLog") -const tenancy = require("./tenancy") -const internalApi = require("./internalApi") -const datasourceGoogle = require("./passport/datasource/google") -const csrf = require("./csrf") -const adminOnly = require("./adminOnly") -const builderOrAdmin = require("./builderOrAdmin") -const builderOnly = require("./builderOnly") -const joiValidator = require("./joi-validator") +import * as jwt from "./passport/jwt" +import * as local from "./passport/local" +import * as google from "./passport/google" +import * as oidc from "./passport/oidc" +import { authError, ssoCallbackUrl } from "./passport/utils" +import authenticated from "./authenticated" +import auditLog from "./auditLog" +import tenancy from "./tenancy" +import internalApi from "./internalApi" +import * as datasourceGoogle from "./passport/datasource/google" +import csrf from "./csrf" +import adminOnly from "./adminOnly" +import builderOrAdmin from "./builderOrAdmin" +import builderOnly from "./builderOnly" +import * as joiValidator from "./joi-validator" const pkg = { google, diff --git a/packages/backend-core/src/middleware/internalApi.js b/packages/backend-core/src/middleware/internalApi.js deleted file mode 100644 index 275d559a9e..0000000000 --- a/packages/backend-core/src/middleware/internalApi.js +++ /dev/null @@ -1,14 +0,0 @@ -const env = require("../environment") -const { Headers } = require("../constants") - -/** - * API Key only endpoint. - */ -module.exports = async (ctx, next) => { - const apiKey = ctx.request.headers[Headers.API_KEY] - if (apiKey !== env.INTERNAL_API_KEY) { - ctx.throw(403, "Unauthorized") - } - - return next() -} diff --git a/packages/backend-core/src/middleware/internalApi.ts b/packages/backend-core/src/middleware/internalApi.ts new file mode 100644 index 0000000000..f4f08ec2dd --- /dev/null +++ b/packages/backend-core/src/middleware/internalApi.ts @@ -0,0 +1,15 @@ +import env from "../environment" +import { Header } from "../constants" +import { BBContext } from "@budibase/types" + +/** + * API Key only endpoint. + */ +export = async (ctx: BBContext, next: any) => { + const apiKey = ctx.request.headers[Header.API_KEY] + if (apiKey !== env.INTERNAL_API_KEY) { + ctx.throw(403, "Unauthorized") + } + + return next() +} diff --git a/packages/backend-core/src/middleware/joi-validator.js b/packages/backend-core/src/middleware/joi-validator.ts similarity index 50% rename from packages/backend-core/src/middleware/joi-validator.js rename to packages/backend-core/src/middleware/joi-validator.ts index 6812dbdd54..fcc8316886 100644 --- a/packages/backend-core/src/middleware/joi-validator.js +++ b/packages/backend-core/src/middleware/joi-validator.ts @@ -1,21 +1,27 @@ -const Joi = require("joi") +import Joi, { ObjectSchema } from "joi" +import { BBContext } from "@budibase/types" -function validate(schema, property) { +function validate( + schema: Joi.ObjectSchema | Joi.ArraySchema, + property: string +) { // Return a Koa middleware function - return (ctx, next) => { + return (ctx: BBContext, next: any) => { if (!schema) { return next() } let params = null + // @ts-ignore + let reqProp = ctx.request?.[property] if (ctx[property] != null) { params = ctx[property] - } else if (ctx.request[property] != null) { - params = ctx.request[property] + } else if (reqProp != null) { + params = reqProp } // not all schemas have the append property e.g. array schemas - if (schema.append) { - schema = schema.append({ + if ((schema as Joi.ObjectSchema).append) { + schema = (schema as Joi.ObjectSchema).append({ createdAt: Joi.any().optional(), updatedAt: Joi.any().optional(), }) @@ -30,10 +36,10 @@ function validate(schema, property) { } } -module.exports.body = schema => { +export function body(schema: Joi.ObjectSchema | Joi.ArraySchema) { return validate(schema, "body") } -module.exports.params = schema => { +export function params(schema: Joi.ObjectSchema | Joi.ArraySchema) { return validate(schema, "params") } diff --git a/packages/backend-core/src/middleware/matchers.js b/packages/backend-core/src/middleware/matchers.ts similarity index 62% rename from packages/backend-core/src/middleware/matchers.js rename to packages/backend-core/src/middleware/matchers.ts index 3d5065c069..efbdec2dbe 100644 --- a/packages/backend-core/src/middleware/matchers.js +++ b/packages/backend-core/src/middleware/matchers.ts @@ -1,27 +1,34 @@ +import { BBContext, EndpointMatcher, RegexMatcher } from "@budibase/types" + const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g -exports.buildMatcherRegex = patterns => { +export const buildMatcherRegex = ( + patterns: EndpointMatcher[] +): RegexMatcher[] => { if (!patterns) { return [] } return patterns.map(pattern => { - const isObj = typeof pattern === "object" && pattern.route - const method = isObj ? pattern.method : "GET" + let route = pattern.route + const method = pattern.method const strict = pattern.strict ? pattern.strict : false - let route = isObj ? pattern.route : pattern + // if there is a param in the route + // use a wildcard pattern const matches = route.match(PARAM_REGEX) if (matches) { for (let match of matches) { - const pattern = "/.*" + (match.endsWith("/") ? "/" : "") + const suffix = match.endsWith("/") ? "/" : "" + const pattern = "/.*" + suffix route = route.replace(match, pattern) } } + return { regex: new RegExp(route), method, strict, route } }) } -exports.matches = (ctx, options) => { +export const matches = (ctx: BBContext, options: RegexMatcher[]) => { return options.find(({ regex, method, strict, route }) => { let urlMatch if (strict) { diff --git a/packages/backend-core/src/middleware/passport/datasource/google.js b/packages/backend-core/src/middleware/passport/datasource/google.ts similarity index 67% rename from packages/backend-core/src/middleware/passport/datasource/google.js rename to packages/backend-core/src/middleware/passport/datasource/google.ts index 8f2022c2d7..65620d7aa3 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.js +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,17 +1,21 @@ -const google = require("../google") +import * as google from "../google" +import { Cookie, Config } from "../../../constants" +import { clearCookie, getCookie } from "../../../utils" +import { getScopedConfig, getPlatformUrl, doWithDB } from "../../../db" +import environment from "../../../environment" +import { getGlobalDB } from "../../../tenancy" +import { BBContext, Database, SSOProfile } from "@budibase/types" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy -const { Cookies, Configs } = require("../../../constants") -const { clearCookie, getCookie } = require("../../../utils") -const { getScopedConfig, getPlatformUrl } = require("../../../db/utils") -const { doWithDB } = require("../../../db") -const environment = require("../../../environment") -const { getGlobalDB } = require("../../../tenancy") + +type Passport = { + authenticate: any +} async function fetchGoogleCreds() { // try and get the config from the tenant const db = getGlobalDB() const googleConfig = await getScopedConfig(db, { - type: Configs.GOOGLE, + type: Config.GOOGLE, }) // or fall back to env variables return ( @@ -22,7 +26,11 @@ async function fetchGoogleCreds() { ) } -async function preAuth(passport, ctx, next) { +export async function preAuth( + passport: Passport, + ctx: BBContext, + next: Function +) { // get the relevant config const googleConfig = await fetchGoogleCreds() const platformUrl = await getPlatformUrl({ tenantAware: false }) @@ -41,13 +49,17 @@ async function preAuth(passport, ctx, next) { })(ctx, next) } -async function postAuth(passport, ctx, next) { +export async function postAuth( + passport: Passport, + ctx: BBContext, + next: Function +) { // get the relevant config const config = await fetchGoogleCreds() const platformUrl = await getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` - const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth) + const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth) return passport.authenticate( new GoogleStrategy( @@ -56,15 +68,20 @@ async function postAuth(passport, ctx, next) { clientSecret: config.clientSecret, callbackURL: callbackUrl, }, - (accessToken, refreshToken, profile, done) => { - clearCookie(ctx, Cookies.DatasourceAuth) + ( + accessToken: string, + refreshToken: string, + profile: SSOProfile, + done: Function + ) => { + clearCookie(ctx, Cookie.DatasourceAuth) done(null, { accessToken, refreshToken }) } ), { successRedirect: "/", failureRedirect: "/error" }, - async (err, tokens) => { + async (err: any, tokens: string[]) => { // update the DB for the datasource with all the user info - await doWithDB(authStateCookie.appId, async db => { + await doWithDB(authStateCookie.appId, async (db: Database) => { const datasource = await db.get(authStateCookie.datasourceId) if (!datasource.config) { datasource.config = {} @@ -78,6 +95,3 @@ async function postAuth(passport, ctx, next) { } )(ctx, next) } - -exports.preAuth = preAuth -exports.postAuth = postAuth diff --git a/packages/backend-core/src/middleware/passport/google.js b/packages/backend-core/src/middleware/passport/google.ts similarity index 62% rename from packages/backend-core/src/middleware/passport/google.js rename to packages/backend-core/src/middleware/passport/google.ts index 7419974cd7..dd3dc8b86d 100644 --- a/packages/backend-core/src/middleware/passport/google.js +++ b/packages/backend-core/src/middleware/passport/google.ts @@ -1,10 +1,15 @@ +import { ssoCallbackUrl } from "./utils" +import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" +import { ConfigType, GoogleConfig, Database, SSOProfile } from "@budibase/types" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy -const { ssoCallbackUrl } = require("./utils") -const { authenticateThirdParty } = require("./third-party-common") -const { Configs } = require("../../../constants") -const buildVerifyFn = saveUserFn => { - return (accessToken, refreshToken, profile, done) => { +export function buildVerifyFn(saveUserFn?: SaveUserFunction) { + return ( + accessToken: string, + refreshToken: string, + profile: SSOProfile, + done: Function + ) => { const thirdPartyUser = { provider: profile.provider, // should always be 'google' providerType: "google", @@ -31,7 +36,11 @@ const buildVerifyFn = saveUserFn => { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport Google Strategy */ -exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { +export async function strategyFactory( + config: GoogleConfig["config"], + callbackUrl: string, + saveUserFn?: SaveUserFunction +) { try { const { clientID, clientSecret } = config @@ -50,18 +59,15 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { }, verify ) - } catch (err) { + } catch (err: any) { console.error(err) - throw new Error( - `Error constructing google authentication strategy: ${err}`, - err - ) + throw new Error(`Error constructing google authentication strategy: ${err}`) } } -exports.getCallbackUrl = async function (db, config) { - return ssoCallbackUrl(db, config, Configs.GOOGLE) +export async function getCallbackUrl( + db: Database, + config: { callbackURL?: string } +) { + return ssoCallbackUrl(db, config, ConfigType.GOOGLE) } - -// expose for testing -exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/jwt.js b/packages/backend-core/src/middleware/passport/jwt.js deleted file mode 100644 index 690c2ac8a1..0000000000 --- a/packages/backend-core/src/middleware/passport/jwt.js +++ /dev/null @@ -1,18 +0,0 @@ -const { Cookies } = require("../../constants") -const env = require("../../environment") -const { authError } = require("./utils") - -exports.options = { - secretOrKey: env.JWT_SECRET, - jwtFromRequest: function (ctx) { - return ctx.cookies.get(Cookies.Auth) - }, -} - -exports.authenticate = async function (jwt, done) { - try { - return done(null, jwt) - } catch (err) { - return authError(done, "JWT invalid", err) - } -} diff --git a/packages/backend-core/src/middleware/passport/jwt.ts b/packages/backend-core/src/middleware/passport/jwt.ts new file mode 100644 index 0000000000..95dc8f2656 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/jwt.ts @@ -0,0 +1,19 @@ +import { Cookie } from "../../constants" +import env from "../../environment" +import { authError } from "./utils" +import { BBContext } from "@budibase/types" + +export const options = { + secretOrKey: env.JWT_SECRET, + jwtFromRequest: function (ctx: BBContext) { + return ctx.cookies.get(Cookie.Auth) + }, +} + +export async function authenticate(jwt: Function, done: Function) { + try { + return done(null, jwt) + } catch (err) { + return authError(done, "JWT invalid", err) + } +} diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.ts similarity index 73% rename from packages/backend-core/src/middleware/passport/local.js rename to packages/backend-core/src/middleware/passport/local.ts index b955d29102..8b85d3734c 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.ts @@ -1,18 +1,18 @@ +import { UserStatus } from "../../constants" +import { compare, newid } from "../../utils" +import env from "../../environment" +import * as users from "../../users" +import { authError } from "./utils" +import { createASession } from "../../security/sessions" +import { getTenantId } from "../../tenancy" +import { BBContext } from "@budibase/types" const jwt = require("jsonwebtoken") -const { UserStatus } = require("../../constants") -const { compare } = require("../../hashing") -const env = require("../../environment") -const users = require("../../users") -const { authError } = require("./utils") -const { newid } = require("../../hashing") -const { createASession } = require("../../security/sessions") -const { getTenantId } = require("../../tenancy") const INVALID_ERR = "Invalid credentials" const SSO_NO_PASSWORD = "SSO user does not have a password set" const EXPIRED = "This account has expired. Please reset your password" -exports.options = { +export const options = { passReqToCallback: true, } @@ -24,7 +24,12 @@ exports.options = { * @param {*} done callback from passport to return user information and errors * @returns The authenticated user, or errors if they occur */ -exports.authenticate = async function (ctx, email, password, done) { +export async function authenticate( + ctx: BBContext, + email: string, + password: string, + done: Function +) { if (!email) return authError(done, "Email Required") if (!password) return authError(done, "Password Required") @@ -56,9 +61,9 @@ exports.authenticate = async function (ctx, email, password, done) { const sessionId = newid() const tenantId = getTenantId() - await createASession(dbUser._id, { sessionId, tenantId }) + await createASession(dbUser._id!, { sessionId, tenantId }) - dbUser.token = jwt.sign( + const token = jwt.sign( { userId: dbUser._id, sessionId, @@ -69,7 +74,10 @@ exports.authenticate = async function (ctx, email, password, done) { // Remove users password in payload delete dbUser.password - return done(null, dbUser) + return done(null, { + ...dbUser, + token, + }) } else { return authError(done, INVALID_ERR) } diff --git a/packages/backend-core/src/middleware/passport/oidc.js b/packages/backend-core/src/middleware/passport/oidc.ts similarity index 72% rename from packages/backend-core/src/middleware/passport/oidc.js rename to packages/backend-core/src/middleware/passport/oidc.ts index 20dbd4669b..40bc22ec0c 100644 --- a/packages/backend-core/src/middleware/passport/oidc.js +++ b/packages/backend-core/src/middleware/passport/oidc.ts @@ -1,10 +1,23 @@ -const fetch = require("node-fetch") +import fetch from "node-fetch" +import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" +import { ssoCallbackUrl } from "./utils" +import { + Config, + ConfigType, + OIDCInnerCfg, + Database, + SSOProfile, + ThirdPartyUser, + OIDCConfiguration, +} from "@budibase/types" const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy -const { authenticateThirdParty } = require("./third-party-common") -const { ssoCallbackUrl } = require("./utils") -const { Configs } = require("../../../constants") -const buildVerifyFn = saveUserFn => { +type JwtClaims = { + preferred_username: string + email: string +} + +export function buildVerifyFn(saveUserFn?: SaveUserFunction) { /** * @param {*} issuer The identity provider base URL * @param {*} sub The user ID @@ -17,17 +30,17 @@ const buildVerifyFn = saveUserFn => { * @param {*} done The passport callback: err, user, info */ return async ( - issuer, - sub, - profile, - jwtClaims, - accessToken, - refreshToken, - idToken, - params, - done + issuer: string, + sub: string, + profile: SSOProfile, + jwtClaims: JwtClaims, + accessToken: string, + refreshToken: string, + idToken: string, + params: any, + done: Function ) => { - const thirdPartyUser = { + const thirdPartyUser: ThirdPartyUser = { // store the issuer info to enable sync in future provider: issuer, providerType: "oidc", @@ -53,7 +66,7 @@ const buildVerifyFn = saveUserFn => { * @param {*} profile The structured profile created by passport using the user info endpoint * @param {*} jwtClaims The claims returned in the id token */ -function getEmail(profile, jwtClaims) { +function getEmail(profile: SSOProfile, jwtClaims: JwtClaims) { // profile not guaranteed to contain email e.g. github connected azure ad account if (profile._json.email) { return profile._json.email @@ -77,7 +90,7 @@ function getEmail(profile, jwtClaims) { ) } -function validEmail(value) { +function validEmail(value: string) { return ( value && !!value.match( @@ -91,19 +104,25 @@ function validEmail(value) { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport OIDC Strategy */ -exports.strategyFactory = async function (config, saveUserFn) { +export async function strategyFactory( + config: OIDCConfiguration, + saveUserFn?: SaveUserFunction +) { try { const verify = buildVerifyFn(saveUserFn) const strategy = new OIDCStrategy(config, verify) strategy.name = "oidc" return strategy - } catch (err) { + } catch (err: any) { console.error(err) - throw new Error("Error constructing OIDC authentication strategy", err) + throw new Error(`Error constructing OIDC authentication strategy - ${err}`) } } -exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) { +export async function fetchStrategyConfig( + enrichedConfig: OIDCInnerCfg, + callbackUrl?: string +): Promise { try { const { clientID, clientSecret, configUrl } = enrichedConfig @@ -135,13 +154,15 @@ exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) { } } catch (err) { console.error(err) - throw new Error("Error constructing OIDC authentication configuration", err) + throw new Error( + `Error constructing OIDC authentication configuration - ${err}` + ) } } -exports.getCallbackUrl = async function (db, config) { - return ssoCallbackUrl(db, config, Configs.OIDC) +export async function getCallbackUrl( + db: Database, + config: { callbackURL?: string } +) { + return ssoCallbackUrl(db, config, ConfigType.OIDC) } - -// expose for testing -exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js index c00ab2ea7d..4c8aa94ddf 100644 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js @@ -1,7 +1,6 @@ // Mock data - +const mockFetch = require("node-fetch") const { data } = require("./utilities/mock-data") - const issuer = "mockIssuer" const sub = "mockSub" const profile = { @@ -39,8 +38,6 @@ describe("oidc", () => { const mockStrategy = require("@techpass/passport-openidconnect").Strategy // mock the request to retrieve the oidc configuration - jest.mock("node-fetch") - const mockFetch = require("node-fetch") mockFetch.mockReturnValue({ ok: true, json: () => oidcConfigUrlResponse diff --git a/packages/backend-core/src/middleware/passport/tests/third-party-common.spec.js b/packages/backend-core/src/middleware/passport/tests/third-party-common.spec.js index 41a253b328..d377d602f1 100644 --- a/packages/backend-core/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/third-party-common.spec.js @@ -1,10 +1,10 @@ -require("../../../../tests/utilities/TestConfiguration") +require("../../../../tests") const { authenticateThirdParty } = require("../third-party-common") const { data } = require("./utilities/mock-data") const { DEFAULT_TENANT_ID } = require("../../../constants") const { generateGlobalUserID } = require("../../../db/utils") -const { newid } = require("../../../hashing") +const { newid } = require("../../../utils") const { doWithGlobalDB, doInTenant } = require("../../../tenancy") const done = jest.fn() diff --git a/packages/backend-core/src/middleware/passport/third-party-common.js b/packages/backend-core/src/middleware/passport/third-party-common.ts similarity index 72% rename from packages/backend-core/src/middleware/passport/third-party-common.js rename to packages/backend-core/src/middleware/passport/third-party-common.ts index 1c5891fce7..451cdf6cc6 100644 --- a/packages/backend-core/src/middleware/passport/third-party-common.js +++ b/packages/backend-core/src/middleware/passport/third-party-common.ts @@ -1,21 +1,33 @@ -const env = require("../../environment") +import env from "../../environment" +import { generateGlobalUserID } from "../../db" +import { authError } from "./utils" +import { newid } from "../../utils" +import { createASession } from "../../security/sessions" +import * as users from "../../users" +import { getGlobalDB, getTenantId } from "../../tenancy" +import fetch from "node-fetch" +import { ThirdPartyUser } from "@budibase/types" const jwt = require("jsonwebtoken") -const { generateGlobalUserID } = require("../../db/utils") -const { authError } = require("./utils") -const { newid } = require("../../hashing") -const { createASession } = require("../../security/sessions") -const users = require("../../users") -const { getGlobalDB, getTenantId } = require("../../tenancy") -const fetch = require("node-fetch") + +type SaveUserOpts = { + requirePassword?: boolean + hashPassword?: boolean + currentUserId?: string +} + +export type SaveUserFunction = ( + user: ThirdPartyUser, + opts: SaveUserOpts +) => Promise /** * Common authentication logic for third parties. e.g. OAuth, OIDC. */ -exports.authenticateThirdParty = async function ( - thirdPartyUser, - requireLocalAccount = true, - done, - saveUserFn +export async function authenticateThirdParty( + thirdPartyUser: ThirdPartyUser, + requireLocalAccount: boolean = true, + done: Function, + saveUserFn?: SaveUserFunction ) { if (!saveUserFn) { throw new Error("Save user function must be provided") @@ -39,7 +51,7 @@ exports.authenticateThirdParty = async function ( // try to load by id try { dbUser = await db.get(userId) - } catch (err) { + } catch (err: any) { // abort when not 404 error if (!err.status || err.status !== 404) { return authError( @@ -80,8 +92,8 @@ exports.authenticateThirdParty = async function ( // create or sync the user try { - await saveUserFn(dbUser, false, false) - } catch (err) { + await saveUserFn(dbUser, { hashPassword: false, requirePassword: false }) + } catch (err: any) { return authError(done, err) } @@ -104,13 +116,16 @@ exports.authenticateThirdParty = async function ( return done(null, dbUser) } -async function syncProfilePicture(user, thirdPartyUser) { - const pictureUrl = thirdPartyUser.profile._json.picture +async function syncProfilePicture( + user: ThirdPartyUser, + thirdPartyUser: ThirdPartyUser +) { + const pictureUrl = thirdPartyUser.profile?._json.picture if (pictureUrl) { const response = await fetch(pictureUrl) if (response.status === 200) { - const type = response.headers.get("content-type") + const type = response.headers.get("content-type") as string if (type.startsWith("image/")) { user.pictureUrl = pictureUrl } @@ -123,7 +138,7 @@ async function syncProfilePicture(user, thirdPartyUser) { /** * @returns a user that has been sync'd with third party information */ -async function syncUser(user, thirdPartyUser) { +async function syncUser(user: ThirdPartyUser, thirdPartyUser: ThirdPartyUser) { // provider user.provider = thirdPartyUser.provider user.providerType = thirdPartyUser.providerType diff --git a/packages/backend-core/src/middleware/passport/utils.js b/packages/backend-core/src/middleware/passport/utils.ts similarity index 64% rename from packages/backend-core/src/middleware/passport/utils.js rename to packages/backend-core/src/middleware/passport/utils.ts index 217130cd6d..3d79aada28 100644 --- a/packages/backend-core/src/middleware/passport/utils.js +++ b/packages/backend-core/src/middleware/passport/utils.ts @@ -1,6 +1,6 @@ -const { isMultiTenant, getTenantId } = require("../../tenancy") -const { getScopedConfig } = require("../../db/utils") -const { Configs } = require("../../constants") +import { isMultiTenant, getTenantId } from "../../tenancy" +import { getScopedConfig } from "../../db" +import { ConfigType, Database, Config } from "@budibase/types" /** * Utility to handle authentication errors. @@ -10,7 +10,7 @@ const { Configs } = require("../../constants") * @param {*} err (Optional) error that will be logged */ -exports.authError = function (done, message, err = null) { +export function authError(done: Function, message: string, err?: any) { return done( err, null, // never return a user @@ -18,13 +18,17 @@ exports.authError = function (done, message, err = null) { ) } -exports.ssoCallbackUrl = async (db, config, type) => { +export async function ssoCallbackUrl( + db: Database, + config?: { callbackURL?: string }, + type?: ConfigType +) { // incase there is a callback URL from before if (config && config.callbackURL) { return config.callbackURL } const publicConfig = await getScopedConfig(db, { - type: Configs.SETTINGS, + type: ConfigType.SETTINGS, }) let callbackUrl = `/api/global/auth` diff --git a/packages/backend-core/src/middleware/tenancy.js b/packages/backend-core/src/middleware/tenancy.js deleted file mode 100644 index 8083322b29..0000000000 --- a/packages/backend-core/src/middleware/tenancy.js +++ /dev/null @@ -1,52 +0,0 @@ -const { doInTenant, isMultiTenant, DEFAULT_TENANT_ID } = require("../tenancy") -const { buildMatcherRegex, matches } = require("./matchers") -const { Headers } = require("../constants") - -const getTenantID = (ctx, opts = { allowQs: false, allowNoTenant: false }) => { - // exit early if not multi-tenant - if (!isMultiTenant()) { - return DEFAULT_TENANT_ID - } - - let tenantId - const allowQs = opts && opts.allowQs - const allowNoTenant = opts && opts.allowNoTenant - const header = ctx.request.headers[Headers.TENANT_ID] - const user = ctx.user || {} - if (allowQs) { - const query = ctx.request.query || {} - tenantId = query.tenantId - } - // override query string (if allowed) by user, or header - // URL params cannot be used in a middleware, as they are - // processed later in the chain - tenantId = user.tenantId || header || tenantId - - // Set the tenantId from the subdomain - if (!tenantId) { - tenantId = ctx.subdomains && ctx.subdomains[0] - } - - if (!tenantId && !allowNoTenant) { - ctx.throw(403, "Tenant id not set") - } - - return tenantId -} - -module.exports = ( - allowQueryStringPatterns, - noTenancyPatterns, - opts = { noTenancyRequired: false } -) => { - const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) - const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) - - return async function (ctx, next) { - const allowNoTenant = - opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) - const allowQs = !!matches(ctx, allowQsOptions) - const tenantId = getTenantID(ctx, { allowQs, allowNoTenant }) - return doInTenant(tenantId, next) - } -} diff --git a/packages/backend-core/src/middleware/tenancy.ts b/packages/backend-core/src/middleware/tenancy.ts new file mode 100644 index 0000000000..78da2bb3e8 --- /dev/null +++ b/packages/backend-core/src/middleware/tenancy.ts @@ -0,0 +1,35 @@ +import { doInTenant, getTenantIDFromCtx } from "../tenancy" +import { buildMatcherRegex, matches } from "./matchers" +import { Header } from "../constants" +import { + BBContext, + EndpointMatcher, + GetTenantIdOptions, + TenantResolutionStrategy, +} from "@budibase/types" + +export = function ( + allowQueryStringPatterns: EndpointMatcher[], + noTenancyPatterns: EndpointMatcher[], + opts: { noTenancyRequired?: boolean } = { noTenancyRequired: false } +) { + const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) + const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) + + return async function (ctx: BBContext | any, next: any) { + const allowNoTenant = + opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) + const tenantOpts: GetTenantIdOptions = { + allowNoTenant, + } + + const allowQs = !!matches(ctx, allowQsOptions) + if (!allowQs) { + tenantOpts.excludeStrategies = [TenantResolutionStrategy.QUERY] + } + + const tenantId = getTenantIDFromCtx(ctx, tenantOpts) + ctx.set(Header.TENANT_ID, tenantId as string) + return doInTenant(tenantId, next) + } +} diff --git a/packages/backend-core/src/middleware/tests/matchers.spec.ts b/packages/backend-core/src/middleware/tests/matchers.spec.ts new file mode 100644 index 0000000000..c39bbb6dd3 --- /dev/null +++ b/packages/backend-core/src/middleware/tests/matchers.spec.ts @@ -0,0 +1,134 @@ +import * as matchers from "../matchers" +import { structures } from "../../../tests" + +describe("matchers", () => { + it("matches by path and method", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + it("wildcards path", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id/something/else" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + it("doesn't wildcard path with strict", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + strict: true, + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id/something/else" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("matches with param", () => { + const pattern = [ + { + route: "/api/tests/:testId", + method: "GET", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + // TODO: Support the below behaviour + // Strict does not work when a param is present + // it("matches with param with strict", () => { + // const pattern = [{ + // route: "/api/tests/:testId", + // method: "GET", + // strict: true + // }] + // const ctx = structures.koa.newContext() + // ctx.request.url = "/api/tests/id" + // ctx.request.method = "GET" + // + // const built = matchers.buildMatcherRegex(pattern) + // + // expect(!!matchers.matches(ctx, built)).toBe(true) + // }) + + it("doesn't match by path", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/unknown" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("doesn't match by method", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("matches by path and wildcard method", () => { + const pattern = [ + { + route: "/api/tests", + method: "ALL", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) +}) diff --git a/packages/backend-core/src/migrations/definitions.ts b/packages/backend-core/src/migrations/definitions.ts index 6eba56ab43..0dd57fe639 100644 --- a/packages/backend-core/src/migrations/definitions.ts +++ b/packages/backend-core/src/migrations/definitions.ts @@ -21,6 +21,10 @@ export const DEFINITIONS: MigrationDefinition[] = [ type: MigrationType.APP, name: MigrationName.EVENT_APP_BACKFILL, }, + { + type: MigrationType.APP, + name: MigrationName.TABLE_SETTINGS_LINKS_TO_ACTIONS, + }, { type: MigrationType.GLOBAL, name: MigrationName.EVENT_GLOBAL_BACKFILL, diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index 90a12acec2..7bc2dec290 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -1,10 +1,13 @@ import { DEFAULT_TENANT_ID } from "../constants" -import { doWithDB } from "../db" -import { DocumentType, StaticDatabases } from "../db/constants" -import { getAllApps } from "../db/utils" +import { + DocumentType, + StaticDatabases, + getAllApps, + getGlobalDBName, + doWithDB, +} from "../db" import environment from "../environment" import { doInTenant, getTenantIds, getTenantId } from "../tenancy" -import { getGlobalDBName } from "../db/tenancy" import * as context from "../context" import { DEFINITIONS } from "." import { @@ -12,6 +15,7 @@ import { MigrationOptions, MigrationType, MigrationNoOpOptions, + App, } from "@budibase/types" export const getMigrationsDoc = async (db: any) => { @@ -41,7 +45,7 @@ export const runMigration = async ( options: MigrationOptions = {} ) => { const migrationType = migration.type - let tenantId: string + let tenantId: string | undefined if (migrationType !== MigrationType.INSTALLATION) { tenantId = getTenantId() } @@ -55,14 +59,17 @@ export const runMigration = async ( } // get the db to store the migration in - let dbNames + let dbNames: string[] if (migrationType === MigrationType.GLOBAL) { dbNames = [getGlobalDBName()] } else if (migrationType === MigrationType.APP) { if (options.noOp) { + if (!options.noOp.appId) { + throw new Error("appId is required for noOp app migration") + } dbNames = [options.noOp.appId] } else { - const apps = await getAllApps(migration.appOpts) + const apps = (await getAllApps(migration.appOpts)) as App[] dbNames = apps.map(app => app.appId) } } else if (migrationType === MigrationType.INSTALLATION) { diff --git a/packages/backend-core/src/migrations/tests/index.spec.js b/packages/backend-core/src/migrations/tests/index.spec.js index c5ec143143..b7d2e14ea5 100644 --- a/packages/backend-core/src/migrations/tests/index.spec.js +++ b/packages/backend-core/src/migrations/tests/index.spec.js @@ -1,9 +1,9 @@ -require("../../../tests/utilities/TestConfiguration") +require("../../../tests") const { runMigrations, getMigrationsDoc } = require("../index") -const { dangerousGetDB } = require("../../db") +const { getDB } = require("../../db") const { StaticDatabases, -} = require("../../db/utils") +} = require("../../constants") let db @@ -18,7 +18,7 @@ describe("migrations", () => { }] beforeEach(() => { - db = dangerousGetDB(StaticDatabases.GLOBAL.name) + db = getDB(StaticDatabases.GLOBAL.name) }) afterEach(async () => { diff --git a/packages/backend-core/src/newid.ts b/packages/backend-core/src/newid.ts new file mode 100644 index 0000000000..5676c23f48 --- /dev/null +++ b/packages/backend-core/src/newid.ts @@ -0,0 +1,5 @@ +import { v4 } from "uuid" + +export function newid() { + return v4().replace(/-/g, "") +} diff --git a/packages/backend-core/src/objectStore/index.ts b/packages/backend-core/src/objectStore/index.ts index 8453c9aee6..2971834f0e 100644 --- a/packages/backend-core/src/objectStore/index.ts +++ b/packages/backend-core/src/objectStore/index.ts @@ -1,402 +1,2 @@ -const sanitize = require("sanitize-s3-objectkey") -import AWS from "aws-sdk" -import stream from "stream" -import fetch from "node-fetch" -import tar from "tar-fs" -const zlib = require("zlib") -import { promisify } from "util" -import { join } from "path" -import fs from "fs" -import env from "../environment" -import { budibaseTempDir, ObjectStoreBuckets } from "./utils" -import { v4 } from "uuid" -import { APP_PREFIX, APP_DEV_PREFIX } from "../db/utils" - -const streamPipeline = promisify(stream.pipeline) -// use this as a temporary store of buckets that are being created -const STATE = { - bucketCreationPromises: {}, -} - -type ListParams = { - ContinuationToken?: string -} - -const CONTENT_TYPE_MAP: any = { - html: "text/html", - css: "text/css", - js: "application/javascript", - json: "application/json", - gz: "application/gzip", -} -const STRING_CONTENT_TYPES = [ - CONTENT_TYPE_MAP.html, - CONTENT_TYPE_MAP.css, - CONTENT_TYPE_MAP.js, - CONTENT_TYPE_MAP.json, -] - -// does normal sanitization and then swaps dev apps to apps -export function sanitizeKey(input: string) { - return sanitize(sanitizeBucket(input)).replace(/\\/g, "/") -} - -// simply handles the dev app to app conversion -export function sanitizeBucket(input: string) { - return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX) -} - -function publicPolicy(bucketName: string) { - return { - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Principal: { - AWS: ["*"], - }, - Action: "s3:GetObject", - Resource: [`arn:aws:s3:::${bucketName}/*`], - }, - ], - } -} - -const PUBLIC_BUCKETS = [ - ObjectStoreBuckets.APPS, - ObjectStoreBuckets.GLOBAL, - ObjectStoreBuckets.PLUGINS, -] - -/** - * Gets a connection to the object store using the S3 SDK. - * @param {string} bucket the name of the bucket which blobs will be uploaded/retrieved from. - * @return {Object} an S3 object store object, check S3 Nodejs SDK for usage. - * @constructor - */ -export const ObjectStore = (bucket: string) => { - const config: any = { - s3ForcePathStyle: true, - signatureVersion: "v4", - apiVersion: "2006-03-01", - accessKeyId: env.MINIO_ACCESS_KEY, - secretAccessKey: env.MINIO_SECRET_KEY, - region: env.AWS_REGION, - } - if (bucket) { - config.params = { - Bucket: sanitizeBucket(bucket), - } - } - if (env.MINIO_URL) { - config.endpoint = env.MINIO_URL - } - return new AWS.S3(config) -} - -/** - * Given an object store and a bucket name this will make sure the bucket exists, - * if it does not exist then it will create it. - */ -export const makeSureBucketExists = async (client: any, bucketName: string) => { - bucketName = sanitizeBucket(bucketName) - try { - await client - .headBucket({ - Bucket: bucketName, - }) - .promise() - } catch (err: any) { - const promises: any = STATE.bucketCreationPromises - const doesntExist = err.statusCode === 404, - noAccess = err.statusCode === 403 - if (promises[bucketName]) { - await promises[bucketName] - } else if (doesntExist || noAccess) { - if (doesntExist) { - // bucket doesn't exist create it - promises[bucketName] = client - .createBucket({ - Bucket: bucketName, - }) - .promise() - await promises[bucketName] - delete promises[bucketName] - } - // public buckets are quite hidden in the system, make sure - // no bucket is set accidentally - if (PUBLIC_BUCKETS.includes(bucketName)) { - await client - .putBucketPolicy({ - Bucket: bucketName, - Policy: JSON.stringify(publicPolicy(bucketName)), - }) - .promise() - } - } else { - throw new Error("Unable to write to object store bucket.") - } - } -} - -/** - * Uploads the contents of a file given the required parameters, useful when - * temp files in use (for example file uploaded as an attachment). - */ -export const upload = async ({ - bucket: bucketName, - filename, - path, - type, - metadata, -}: any) => { - const extension = filename.split(".").pop() - const fileBytes = fs.readFileSync(path) - - const objectStore = ObjectStore(bucketName) - await makeSureBucketExists(objectStore, bucketName) - - const config: any = { - // windows file paths need to be converted to forward slashes for s3 - Key: sanitizeKey(filename), - Body: fileBytes, - ContentType: type || CONTENT_TYPE_MAP[extension.toLowerCase()], - } - if (metadata) { - config.Metadata = metadata - } - return objectStore.upload(config).promise() -} - -/** - * Similar to the upload function but can be used to send a file stream - * through to the object store. - */ -export const streamUpload = async ( - bucketName: string, - filename: string, - stream: any, - extra = {} -) => { - const objectStore = ObjectStore(bucketName) - await makeSureBucketExists(objectStore, bucketName) - - // Set content type for certain known extensions - if (filename?.endsWith(".js")) { - extra = { - ...extra, - ContentType: "application/javascript", - } - } else if (filename?.endsWith(".svg")) { - extra = { - ...extra, - ContentType: "image", - } - } - - const params = { - Bucket: sanitizeBucket(bucketName), - Key: sanitizeKey(filename), - Body: stream, - ...extra, - } - return objectStore.upload(params).promise() -} - -/** - * retrieves the contents of a file from the object store, if it is a known content type it - * will be converted, otherwise it will be returned as a buffer stream. - */ -export const retrieve = async (bucketName: string, filepath: string) => { - const objectStore = ObjectStore(bucketName) - const params = { - Bucket: sanitizeBucket(bucketName), - Key: sanitizeKey(filepath), - } - const response: any = await objectStore.getObject(params).promise() - // currently these are all strings - if (STRING_CONTENT_TYPES.includes(response.ContentType)) { - return response.Body.toString("utf8") - } else { - return response.Body - } -} - -export const listAllObjects = async (bucketName: string, path: string) => { - const objectStore = ObjectStore(bucketName) - const list = (params: ListParams = {}) => { - return objectStore - .listObjectsV2({ - ...params, - Bucket: sanitizeBucket(bucketName), - Prefix: sanitizeKey(path), - }) - .promise() - } - let isTruncated = false, - token, - objects: AWS.S3.Types.Object[] = [] - do { - let params: ListParams = {} - if (token) { - params.ContinuationToken = token - } - const response = await list(params) - if (response.Contents) { - objects = objects.concat(response.Contents) - } - isTruncated = !!response.IsTruncated - } while (isTruncated) - return objects -} - -/** - * Same as retrieval function but puts to a temporary file. - */ -export const retrieveToTmp = async (bucketName: string, filepath: string) => { - bucketName = sanitizeBucket(bucketName) - filepath = sanitizeKey(filepath) - const data = await retrieve(bucketName, filepath) - const outputPath = join(budibaseTempDir(), v4()) - fs.writeFileSync(outputPath, data) - return outputPath -} - -export const retrieveDirectory = async (bucketName: string, path: string) => { - let writePath = join(budibaseTempDir(), v4()) - fs.mkdirSync(writePath) - const objects = await listAllObjects(bucketName, path) - let fullObjects = await Promise.all( - objects.map(obj => retrieve(bucketName, obj.Key!)) - ) - let count = 0 - for (let obj of objects) { - const filename = obj.Key! - const data = fullObjects[count++] - const possiblePath = filename.split("/") - if (possiblePath.length > 1) { - const dirs = possiblePath.slice(0, possiblePath.length - 1) - fs.mkdirSync(join(writePath, ...dirs), { recursive: true }) - } - fs.writeFileSync(join(writePath, ...possiblePath), data) - } - return writePath -} - -/** - * Delete a single file. - */ -export const deleteFile = async (bucketName: string, filepath: string) => { - const objectStore = ObjectStore(bucketName) - await makeSureBucketExists(objectStore, bucketName) - const params = { - Bucket: bucketName, - Key: filepath, - } - return objectStore.deleteObject(params) -} - -export const deleteFiles = async (bucketName: string, filepaths: string[]) => { - const objectStore = ObjectStore(bucketName) - await makeSureBucketExists(objectStore, bucketName) - const params = { - Bucket: bucketName, - Delete: { - Objects: filepaths.map((path: any) => ({ Key: path })), - }, - } - return objectStore.deleteObjects(params).promise() -} - -/** - * Delete a path, including everything within. - */ -export const deleteFolder = async ( - bucketName: string, - folder: string -): Promise => { - bucketName = sanitizeBucket(bucketName) - folder = sanitizeKey(folder) - const client = ObjectStore(bucketName) - const listParams = { - Bucket: bucketName, - Prefix: folder, - } - - let response: any = await client.listObjects(listParams).promise() - if (response.Contents.length === 0) { - return - } - const deleteParams: any = { - Bucket: bucketName, - Delete: { - Objects: [], - }, - } - - response.Contents.forEach((content: any) => { - deleteParams.Delete.Objects.push({ Key: content.Key }) - }) - - response = await client.deleteObjects(deleteParams).promise() - // can only empty 1000 items at once - if (response.Deleted.length === 1000) { - return deleteFolder(bucketName, folder) - } -} - -export const uploadDirectory = async ( - bucketName: string, - localPath: string, - bucketPath: string -) => { - bucketName = sanitizeBucket(bucketName) - let uploads = [] - const files = fs.readdirSync(localPath, { withFileTypes: true }) - for (let file of files) { - const path = sanitizeKey(join(bucketPath, file.name)) - const local = join(localPath, file.name) - if (file.isDirectory()) { - uploads.push(uploadDirectory(bucketName, local, path)) - } else { - uploads.push(streamUpload(bucketName, path, fs.createReadStream(local))) - } - } - await Promise.all(uploads) - return files -} - -exports.downloadTarballDirect = async ( - url: string, - path: string, - headers = {} -) => { - path = sanitizeKey(path) - const response = await fetch(url, { headers }) - if (!response.ok) { - throw new Error(`unexpected response ${response.statusText}`) - } - - await streamPipeline(response.body, zlib.Unzip(), tar.extract(path)) -} - -export const downloadTarball = async ( - url: string, - bucketName: string, - path: string -) => { - bucketName = sanitizeBucket(bucketName) - path = sanitizeKey(path) - const response = await fetch(url) - if (!response.ok) { - throw new Error(`unexpected response ${response.statusText}`) - } - - const tmpPath = join(budibaseTempDir(), path) - await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) - if (!env.isTest() && env.SELF_HOSTED) { - await uploadDirectory(bucketName, tmpPath, path) - } - // return the temporary path incase there is a use for it - return tmpPath -} +export * from "./objectStore" +export * from "./utils" diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts new file mode 100644 index 0000000000..2ae8848c53 --- /dev/null +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -0,0 +1,426 @@ +const sanitize = require("sanitize-s3-objectkey") +import AWS from "aws-sdk" +import stream from "stream" +import fetch from "node-fetch" +import tar from "tar-fs" +const zlib = require("zlib") +import { promisify } from "util" +import { join } from "path" +import fs from "fs" +import env from "../environment" +import { budibaseTempDir, ObjectStoreBuckets } from "./utils" +import { v4 } from "uuid" +import { APP_PREFIX, APP_DEV_PREFIX } from "../db" + +const streamPipeline = promisify(stream.pipeline) +// use this as a temporary store of buckets that are being created +const STATE = { + bucketCreationPromises: {}, +} + +type ListParams = { + ContinuationToken?: string +} + +type UploadParams = { + bucket: string + filename: string + path: string + type?: string + // can be undefined, we will remove it + metadata?: { + [key: string]: string | undefined + } +} + +const CONTENT_TYPE_MAP: any = { + txt: "text/plain", + html: "text/html", + css: "text/css", + js: "application/javascript", + json: "application/json", + gz: "application/gzip", +} +const STRING_CONTENT_TYPES = [ + CONTENT_TYPE_MAP.html, + CONTENT_TYPE_MAP.css, + CONTENT_TYPE_MAP.js, + CONTENT_TYPE_MAP.json, +] + +// does normal sanitization and then swaps dev apps to apps +export function sanitizeKey(input: string) { + return sanitize(sanitizeBucket(input)).replace(/\\/g, "/") +} + +// simply handles the dev app to app conversion +export function sanitizeBucket(input: string) { + return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX) +} + +function publicPolicy(bucketName: string) { + return { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { + AWS: ["*"], + }, + Action: "s3:GetObject", + Resource: [`arn:aws:s3:::${bucketName}/*`], + }, + ], + } +} + +const PUBLIC_BUCKETS = [ + ObjectStoreBuckets.APPS, + ObjectStoreBuckets.GLOBAL, + ObjectStoreBuckets.PLUGINS, +] + +/** + * Gets a connection to the object store using the S3 SDK. + * @param {string} bucket the name of the bucket which blobs will be uploaded/retrieved from. + * @return {Object} an S3 object store object, check S3 Nodejs SDK for usage. + * @constructor + */ +export const ObjectStore = (bucket: string) => { + const config: any = { + s3ForcePathStyle: true, + signatureVersion: "v4", + apiVersion: "2006-03-01", + accessKeyId: env.MINIO_ACCESS_KEY, + secretAccessKey: env.MINIO_SECRET_KEY, + region: env.AWS_REGION, + } + if (bucket) { + config.params = { + Bucket: sanitizeBucket(bucket), + } + } + if (env.MINIO_URL) { + config.endpoint = env.MINIO_URL + } + return new AWS.S3(config) +} + +/** + * Given an object store and a bucket name this will make sure the bucket exists, + * if it does not exist then it will create it. + */ +export const makeSureBucketExists = async (client: any, bucketName: string) => { + bucketName = sanitizeBucket(bucketName) + try { + await client + .headBucket({ + Bucket: bucketName, + }) + .promise() + } catch (err: any) { + const promises: any = STATE.bucketCreationPromises + const doesntExist = err.statusCode === 404, + noAccess = err.statusCode === 403 + if (promises[bucketName]) { + await promises[bucketName] + } else if (doesntExist || noAccess) { + if (doesntExist) { + // bucket doesn't exist create it + promises[bucketName] = client + .createBucket({ + Bucket: bucketName, + }) + .promise() + await promises[bucketName] + delete promises[bucketName] + } + // public buckets are quite hidden in the system, make sure + // no bucket is set accidentally + if (PUBLIC_BUCKETS.includes(bucketName)) { + await client + .putBucketPolicy({ + Bucket: bucketName, + Policy: JSON.stringify(publicPolicy(bucketName)), + }) + .promise() + } + } else { + throw new Error("Unable to write to object store bucket.") + } + } +} + +/** + * Uploads the contents of a file given the required parameters, useful when + * temp files in use (for example file uploaded as an attachment). + */ +export const upload = async ({ + bucket: bucketName, + filename, + path, + type, + metadata, +}: UploadParams) => { + const extension = filename.split(".").pop() + const fileBytes = fs.readFileSync(path) + + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) + + let contentType = type + if (!contentType) { + contentType = extension + ? CONTENT_TYPE_MAP[extension.toLowerCase()] + : CONTENT_TYPE_MAP.txt + } + const config: any = { + // windows file paths need to be converted to forward slashes for s3 + Key: sanitizeKey(filename), + Body: fileBytes, + ContentType: contentType, + } + if (metadata && typeof metadata === "object") { + // remove any nullish keys from the metadata object, as these may be considered invalid + for (let key of Object.keys(metadata)) { + if (!metadata[key] || typeof metadata[key] !== "string") { + delete metadata[key] + } + } + config.Metadata = metadata + } + return objectStore.upload(config).promise() +} + +/** + * Similar to the upload function but can be used to send a file stream + * through to the object store. + */ +export const streamUpload = async ( + bucketName: string, + filename: string, + stream: any, + extra = {} +) => { + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) + + // Set content type for certain known extensions + if (filename?.endsWith(".js")) { + extra = { + ...extra, + ContentType: "application/javascript", + } + } else if (filename?.endsWith(".svg")) { + extra = { + ...extra, + ContentType: "image", + } + } + + const params = { + Bucket: sanitizeBucket(bucketName), + Key: sanitizeKey(filename), + Body: stream, + ...extra, + } + return objectStore.upload(params).promise() +} + +/** + * retrieves the contents of a file from the object store, if it is a known content type it + * will be converted, otherwise it will be returned as a buffer stream. + */ +export const retrieve = async (bucketName: string, filepath: string) => { + const objectStore = ObjectStore(bucketName) + const params = { + Bucket: sanitizeBucket(bucketName), + Key: sanitizeKey(filepath), + } + const response: any = await objectStore.getObject(params).promise() + // currently these are all strings + if (STRING_CONTENT_TYPES.includes(response.ContentType)) { + return response.Body.toString("utf8") + } else { + return response.Body + } +} + +export const listAllObjects = async (bucketName: string, path: string) => { + const objectStore = ObjectStore(bucketName) + const list = (params: ListParams = {}) => { + return objectStore + .listObjectsV2({ + ...params, + Bucket: sanitizeBucket(bucketName), + Prefix: sanitizeKey(path), + }) + .promise() + } + let isTruncated = false, + token, + objects: AWS.S3.Types.Object[] = [] + do { + let params: ListParams = {} + if (token) { + params.ContinuationToken = token + } + const response = await list(params) + if (response.Contents) { + objects = objects.concat(response.Contents) + } + isTruncated = !!response.IsTruncated + } while (isTruncated) + return objects +} + +/** + * Same as retrieval function but puts to a temporary file. + */ +export const retrieveToTmp = async (bucketName: string, filepath: string) => { + bucketName = sanitizeBucket(bucketName) + filepath = sanitizeKey(filepath) + const data = await retrieve(bucketName, filepath) + const outputPath = join(budibaseTempDir(), v4()) + fs.writeFileSync(outputPath, data) + return outputPath +} + +export const retrieveDirectory = async (bucketName: string, path: string) => { + let writePath = join(budibaseTempDir(), v4()) + fs.mkdirSync(writePath) + const objects = await listAllObjects(bucketName, path) + let fullObjects = await Promise.all( + objects.map(obj => retrieve(bucketName, obj.Key!)) + ) + let count = 0 + for (let obj of objects) { + const filename = obj.Key! + const data = fullObjects[count++] + const possiblePath = filename.split("/") + if (possiblePath.length > 1) { + const dirs = possiblePath.slice(0, possiblePath.length - 1) + fs.mkdirSync(join(writePath, ...dirs), { recursive: true }) + } + fs.writeFileSync(join(writePath, ...possiblePath), data) + } + return writePath +} + +/** + * Delete a single file. + */ +export const deleteFile = async (bucketName: string, filepath: string) => { + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) + const params = { + Bucket: bucketName, + Key: filepath, + } + return objectStore.deleteObject(params) +} + +export const deleteFiles = async (bucketName: string, filepaths: string[]) => { + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) + const params = { + Bucket: bucketName, + Delete: { + Objects: filepaths.map((path: any) => ({ Key: path })), + }, + } + return objectStore.deleteObjects(params).promise() +} + +/** + * Delete a path, including everything within. + */ +export const deleteFolder = async ( + bucketName: string, + folder: string +): Promise => { + bucketName = sanitizeBucket(bucketName) + folder = sanitizeKey(folder) + const client = ObjectStore(bucketName) + const listParams = { + Bucket: bucketName, + Prefix: folder, + } + + let response: any = await client.listObjects(listParams).promise() + if (response.Contents.length === 0) { + return + } + const deleteParams: any = { + Bucket: bucketName, + Delete: { + Objects: [], + }, + } + + response.Contents.forEach((content: any) => { + deleteParams.Delete.Objects.push({ Key: content.Key }) + }) + + response = await client.deleteObjects(deleteParams).promise() + // can only empty 1000 items at once + if (response.Deleted.length === 1000) { + return deleteFolder(bucketName, folder) + } +} + +export const uploadDirectory = async ( + bucketName: string, + localPath: string, + bucketPath: string +) => { + bucketName = sanitizeBucket(bucketName) + let uploads = [] + const files = fs.readdirSync(localPath, { withFileTypes: true }) + for (let file of files) { + const path = sanitizeKey(join(bucketPath, file.name)) + const local = join(localPath, file.name) + if (file.isDirectory()) { + uploads.push(uploadDirectory(bucketName, local, path)) + } else { + uploads.push(streamUpload(bucketName, path, fs.createReadStream(local))) + } + } + await Promise.all(uploads) + return files +} + +export const downloadTarballDirect = async ( + url: string, + path: string, + headers = {} +) => { + path = sanitizeKey(path) + const response = await fetch(url, { headers }) + if (!response.ok) { + throw new Error(`unexpected response ${response.statusText}`) + } + + await streamPipeline(response.body, zlib.Unzip(), tar.extract(path)) +} + +export const downloadTarball = async ( + url: string, + bucketName: string, + path: string +) => { + bucketName = sanitizeBucket(bucketName) + path = sanitizeKey(path) + const response = await fetch(url) + if (!response.ok) { + throw new Error(`unexpected response ${response.statusText}`) + } + + const tmpPath = join(budibaseTempDir(), path) + await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) + if (!env.isTest() && env.SELF_HOSTED) { + await uploadDirectory(bucketName, tmpPath, path) + } + // return the temporary path incase there is a use for it + return tmpPath +} diff --git a/packages/backend-core/src/objectStore/utils.js b/packages/backend-core/src/objectStore/utils.ts similarity index 71% rename from packages/backend-core/src/objectStore/utils.js rename to packages/backend-core/src/objectStore/utils.ts index 2d4faf55d1..f3c9e93943 100644 --- a/packages/backend-core/src/objectStore/utils.js +++ b/packages/backend-core/src/objectStore/utils.ts @@ -1,14 +1,15 @@ -const { join } = require("path") -const { tmpdir } = require("os") -const fs = require("fs") -const env = require("../environment") +import { join } from "path" +import { tmpdir } from "os" +import fs from "fs" +import env from "../environment" /**************************************************** * NOTE: When adding a new bucket - name * * sure that S3 usages (like budibase-infra) * * have been updated to have a unique bucket name. * ****************************************************/ -exports.ObjectStoreBuckets = { +// can't be an enum - only numbers can be used for computed types +export const ObjectStoreBuckets = { BACKUPS: env.BACKUPS_BUCKET_NAME, APPS: env.APPS_BUCKET_NAME, TEMPLATES: env.TEMPLATES_BUCKET_NAME, @@ -22,6 +23,6 @@ if (!fs.existsSync(bbTmp)) { fs.mkdirSync(bbTmp) } -exports.budibaseTempDir = function () { +export function budibaseTempDir() { return bbTmp } diff --git a/packages/backend-core/src/pino.js b/packages/backend-core/src/pino.js deleted file mode 100644 index 69962b3841..0000000000 --- a/packages/backend-core/src/pino.js +++ /dev/null @@ -1,11 +0,0 @@ -const env = require("./environment") - -exports.pinoSettings = () => ({ - prettyPrint: { - levelFirst: true, - }, - level: env.LOG_LEVEL || "error", - autoLogging: { - ignore: req => req.url.includes("/health"), - }, -}) diff --git a/packages/backend-core/src/pino.ts b/packages/backend-core/src/pino.ts new file mode 100644 index 0000000000..4140f428e1 --- /dev/null +++ b/packages/backend-core/src/pino.ts @@ -0,0 +1,13 @@ +import env from "./environment" + +export function pinoSettings() { + return { + prettyPrint: { + levelFirst: true, + }, + level: env.LOG_LEVEL || "error", + autoLogging: { + ignore: (req: { url: string }) => req.url.includes("/health"), + }, + } +} diff --git a/packages/backend-core/src/pkg/cache.ts b/packages/backend-core/src/pkg/cache.ts deleted file mode 100644 index 1aaa40370d..0000000000 --- a/packages/backend-core/src/pkg/cache.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Mimic the outer package export for usage in index.ts -// The outer exports can't be used as they now reference dist directly -import * as generic from "../cache/generic" -import * as user from "../cache/user" -import * as app from "../cache/appMetadata" - -export = { - app, - user, - ...generic, -} diff --git a/packages/backend-core/src/pkg/context.ts b/packages/backend-core/src/pkg/context.ts deleted file mode 100644 index 4915cc6e41..0000000000 --- a/packages/backend-core/src/pkg/context.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Mimic the outer package export for usage in index.ts -// The outer exports can't be used as they now reference dist directly -import { - getAppDB, - getDevAppDB, - getProdAppDB, - getAppId, - updateAppId, - doInAppContext, - doInTenant, - doInContext, -} from "../context" - -import * as identity from "../context/identity" - -export = { - getAppDB, - getDevAppDB, - getProdAppDB, - getAppId, - updateAppId, - doInAppContext, - doInTenant, - doInContext, - identity, -} diff --git a/packages/backend-core/src/pkg/db.ts b/packages/backend-core/src/pkg/db.ts deleted file mode 100644 index 0254adddd5..0000000000 --- a/packages/backend-core/src/pkg/db.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Mimic the outer package export for usage in index.ts -// The outer exports can't be used as they now reference dist directly -export * from "../db" -export * from "../db/utils" -export * from "../db/views" -export * from "../db/pouch" -export * from "../db/constants" diff --git a/packages/backend-core/src/pkg/objectStore.ts b/packages/backend-core/src/pkg/objectStore.ts deleted file mode 100644 index 0447c6b3c2..0000000000 --- a/packages/backend-core/src/pkg/objectStore.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Mimic the outer package export for usage in index.ts -// The outer exports can't be used as they now reference dist directly -export * from "../objectStore" -export * from "../objectStore/utils" diff --git a/packages/backend-core/src/pkg/redis.ts b/packages/backend-core/src/pkg/redis.ts deleted file mode 100644 index 297c2b54f4..0000000000 --- a/packages/backend-core/src/pkg/redis.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Mimic the outer package export for usage in index.ts -// The outer exports can't be used as they now reference dist directly -import Client from "../redis" -import utils from "../redis/utils" -import clients from "../redis/init" -import * as redlock from "../redis/redlock" - -export = { - Client, - utils, - clients, - redlock, -} diff --git a/packages/backend-core/src/pkg/utils.ts b/packages/backend-core/src/pkg/utils.ts deleted file mode 100644 index 5272046524..0000000000 --- a/packages/backend-core/src/pkg/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Mimic the outer package export for usage in index.ts -// The outer exports can't be used as they now reference dist directly -export * from "../utils" -export * from "../hashing" diff --git a/packages/backend-core/src/plugin/index.ts b/packages/backend-core/src/plugin/index.ts index a6d1853007..3eeaeaa90c 100644 --- a/packages/backend-core/src/plugin/index.ts +++ b/packages/backend-core/src/plugin/index.ts @@ -1,7 +1 @@ -import * as utils from "./utils" - -const pkg = { - ...utils, -} - -export = pkg +export * from "./utils" diff --git a/packages/backend-core/src/plugin/utils.js b/packages/backend-core/src/plugin/utils.ts similarity index 87% rename from packages/backend-core/src/plugin/utils.js rename to packages/backend-core/src/plugin/utils.ts index 60a40f3a76..7b62248bb5 100644 --- a/packages/backend-core/src/plugin/utils.js +++ b/packages/backend-core/src/plugin/utils.ts @@ -1,9 +1,5 @@ -const { - DatasourceFieldType, - QueryType, - PluginType, -} = require("@budibase/types") -const joi = require("joi") +import { DatasourceFieldType, QueryType, PluginType } from "@budibase/types" +import joi from "joi" const DATASOURCE_TYPES = [ "Relational", @@ -14,14 +10,14 @@ const DATASOURCE_TYPES = [ "API", ] -function runJoi(validator, schema) { +function runJoi(validator: joi.Schema, schema: any) { const { error } = validator.validate(schema) if (error) { throw error } } -function validateComponent(schema) { +function validateComponent(schema: any) { const validator = joi.object({ type: joi.string().allow("component").required(), metadata: joi.object().unknown(true).required(), @@ -37,7 +33,7 @@ function validateComponent(schema) { runJoi(validator, schema) } -function validateDatasource(schema) { +function validateDatasource(schema: any) { const fieldValidator = joi.object({ type: joi .string() @@ -51,6 +47,7 @@ function validateDatasource(schema) { const queryValidator = joi .object({ type: joi.string().allow(...Object.values(QueryType)), + readable: joi.boolean(), fields: joi.object().pattern(joi.string(), fieldValidator), }) .required() @@ -85,7 +82,7 @@ function validateDatasource(schema) { runJoi(validator, schema) } -exports.validate = schema => { +export function validate(schema: any) { switch (schema?.type) { case PluginType.COMPONENT: validateComponent(schema) diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index 80ee7362e4..acfff1c7b8 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -1,4 +1,5 @@ import events from "events" +import { timeout } from "../utils" /** * Bull works with a Job wrapper around all messages that contains a lot more information about @@ -27,6 +28,8 @@ class InMemoryQueue { _opts?: any _messages: any[] _emitter: EventEmitter + _runCount: number + _addCount: number /** * The constructor the queue, exactly the same as that of Bulls. * @param {string} name The name of the queue which is being configured. @@ -38,6 +41,8 @@ class InMemoryQueue { this._opts = opts this._messages = [] this._emitter = new events.EventEmitter() + this._runCount = 0 + this._addCount = 0 } /** @@ -59,6 +64,7 @@ class InMemoryQueue { if (resp.then != null) { await resp } + this._runCount++ }) } @@ -77,6 +83,7 @@ class InMemoryQueue { throw "Queue only supports carrying JSON." } this._messages.push(newJob(this._name, msg)) + this._addCount++ this._emitter.emit("message") } @@ -122,6 +129,12 @@ class InMemoryQueue { on() { // do nothing } + + async waitForCompletion() { + do { + await timeout(50) + } while (this._addCount < this._runCount) + } } export = InMemoryQueue diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index b4eeeb31aa..b34d46e463 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -39,7 +39,7 @@ export function createQueue( return queue } -exports.shutdown = async () => { +export async function shutdown() { if (QUEUES.length) { clearInterval(cleanupInterval) for (let queue of QUEUES) { diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 8a15320ff3..ea4379f048 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -1,278 +1,6 @@ -import RedisWrapper from "../redis" -const env = require("../environment") -// ioredis mock is all in memory -const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") -const { - addDbPrefix, - removeDbPrefix, - getRedisOptions, - SEPARATOR, - SelectableDatabases, -} = require("./utils") - -const RETRY_PERIOD_MS = 2000 -const STARTUP_TIMEOUT_MS = 5000 -const CLUSTERED = false -const DEFAULT_SELECT_DB = SelectableDatabases.DEFAULT - -// for testing just generate the client once -let CLOSED = false -let CLIENTS: { [key: number]: any } = {} -// if in test always connected -let CONNECTED = env.isTest() - -function pickClient(selectDb: number): any { - return CLIENTS[selectDb] -} - -function connectionError( - selectDb: number, - timeout: NodeJS.Timeout, - err: Error | string -) { - // manually shut down, ignore errors - if (CLOSED) { - return - } - pickClient(selectDb).disconnect() - CLOSED = true - // always clear this on error - clearTimeout(timeout) - CONNECTED = false - console.error("Redis connection failed - " + err) - setTimeout(() => { - init() - }, RETRY_PERIOD_MS) -} - -/** - * Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise - * will return the ioredis client which will be ready to use. - */ -function init(selectDb = DEFAULT_SELECT_DB) { - let timeout: NodeJS.Timeout - CLOSED = false - let client = pickClient(selectDb) - // already connected, ignore - if (client && CONNECTED) { - return - } - // testing uses a single in memory client - if (env.isTest()) { - CLIENTS[selectDb] = new Redis(getRedisOptions()) - } - // start the timer - only allowed 5 seconds to connect - timeout = setTimeout(() => { - if (!CONNECTED) { - connectionError( - selectDb, - timeout, - "Did not successfully connect in timeout" - ) - } - }, STARTUP_TIMEOUT_MS) - - // disconnect any lingering client - if (client) { - client.disconnect() - } - const { redisProtocolUrl, opts, host, port } = getRedisOptions(CLUSTERED) - - if (CLUSTERED) { - client = new Redis.Cluster([{ host, port }], opts) - } else if (redisProtocolUrl) { - client = new Redis(redisProtocolUrl) - } else { - client = new Redis(opts) - } - // attach handlers - client.on("end", (err: Error) => { - connectionError(selectDb, timeout, err) - }) - client.on("error", (err: Error) => { - connectionError(selectDb, timeout, err) - }) - client.on("connect", () => { - clearTimeout(timeout) - CONNECTED = true - }) - CLIENTS[selectDb] = client -} - -function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) { - return new Promise(resolve => { - if (pickClient(selectDb) == null) { - init() - } else if (CONNECTED) { - resolve("") - return - } - // check if the connection is ready - const interval = setInterval(() => { - if (CONNECTED) { - clearInterval(interval) - resolve("") - } - }, 500) - }) -} - -/** - * Utility function, takes a redis stream and converts it to a promisified response - - * this can only be done with redis streams because they will have an end. - * @param stream A redis stream, specifically as this type of stream will have an end. - * @param client The client to use for further lookups. - * @return {Promise} The final output of the stream - */ -function promisifyStream(stream: any, client: RedisWrapper) { - return new Promise((resolve, reject) => { - const outputKeys = new Set() - stream.on("data", (keys: string[]) => { - keys.forEach(key => { - outputKeys.add(key) - }) - }) - stream.on("error", (err: Error) => { - reject(err) - }) - stream.on("end", async () => { - const keysArray: string[] = Array.from(outputKeys) as string[] - try { - let getPromises = [] - for (let key of keysArray) { - getPromises.push(client.get(key)) - } - const jsonArray = await Promise.all(getPromises) - resolve( - keysArray.map(key => ({ - key: removeDbPrefix(key), - value: JSON.parse(jsonArray.shift()), - })) - ) - } catch (err) { - reject(err) - } - }) - }) -} - -export = class RedisWrapper { - _db: string - _select: number - - constructor(db: string, selectDb: number | null = null) { - this._db = db - this._select = selectDb || DEFAULT_SELECT_DB - } - - getClient() { - return pickClient(this._select) - } - - async init() { - CLOSED = false - init(this._select) - await waitForConnection(this._select) - return this - } - - async finish() { - CLOSED = true - this.getClient().disconnect() - } - - async scan(key = ""): Promise { - const db = this._db - key = `${db}${SEPARATOR}${key}` - let stream - if (CLUSTERED) { - let node = this.getClient().nodes("master") - stream = node[0].scanStream({ match: key + "*", count: 100 }) - } else { - stream = this.getClient().scanStream({ match: key + "*", count: 100 }) - } - return promisifyStream(stream, this.getClient()) - } - - async keys(pattern: string) { - const db = this._db - return this.getClient().keys(addDbPrefix(db, pattern)) - } - - async get(key: string) { - const db = this._db - let response = await this.getClient().get(addDbPrefix(db, key)) - // overwrite the prefixed key - if (response != null && response.key) { - response.key = key - } - // if its not an object just return the response - try { - return JSON.parse(response) - } catch (err) { - return response - } - } - - async bulkGet(keys: string[]) { - const db = this._db - if (keys.length === 0) { - return {} - } - const prefixedKeys = keys.map(key => addDbPrefix(db, key)) - let response = await this.getClient().mget(prefixedKeys) - if (Array.isArray(response)) { - let final: any = {} - let count = 0 - for (let result of response) { - if (result) { - let parsed - try { - parsed = JSON.parse(result) - } catch (err) { - parsed = result - } - final[keys[count]] = parsed - } - count++ - } - return final - } else { - throw new Error(`Invalid response: ${response}`) - } - } - - async store(key: string, value: any, expirySeconds: number | null = null) { - const db = this._db - if (typeof value === "object") { - value = JSON.stringify(value) - } - const prefixedKey = addDbPrefix(db, key) - await this.getClient().set(prefixedKey, value) - if (expirySeconds) { - await this.getClient().expire(prefixedKey, expirySeconds) - } - } - - async getTTL(key: string) { - const db = this._db - const prefixedKey = addDbPrefix(db, key) - return this.getClient().ttl(prefixedKey) - } - - async setExpiry(key: string, expirySeconds: number | null) { - const db = this._db - const prefixedKey = addDbPrefix(db, key) - await this.getClient().expire(prefixedKey, expirySeconds) - } - - async delete(key: string) { - const db = this._db - await this.getClient().del(addDbPrefix(db, key)) - } - - async clear() { - let items = await this.scan() - await Promise.all(items.map((obj: any) => this.delete(obj.key))) - } -} +// Mimic the outer package export for usage in index.ts +// The outer exports can't be used as they now reference dist directly +export { default as Client } from "./redis" +export * as utils from "./utils" +export * as clients from "./init" +export * as redlock from "./redlock" diff --git a/packages/backend-core/src/redis/init.js b/packages/backend-core/src/redis/init.js deleted file mode 100644 index 3150ef2c1c..0000000000 --- a/packages/backend-core/src/redis/init.js +++ /dev/null @@ -1,69 +0,0 @@ -const Client = require("./index") -const utils = require("./utils") - -let userClient, - sessionClient, - appClient, - cacheClient, - writethroughClient, - lockClient - -async function init() { - userClient = await new Client(utils.Databases.USER_CACHE).init() - sessionClient = await new Client(utils.Databases.SESSIONS).init() - appClient = await new Client(utils.Databases.APP_METADATA).init() - cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init() - lockClient = await new Client(utils.Databases.LOCKS).init() - writethroughClient = await new Client( - utils.Databases.WRITE_THROUGH, - utils.SelectableDatabases.WRITE_THROUGH - ).init() -} - -process.on("exit", async () => { - if (userClient) await userClient.finish() - if (sessionClient) await sessionClient.finish() - if (appClient) await appClient.finish() - if (cacheClient) await cacheClient.finish() - if (writethroughClient) await writethroughClient.finish() - if (lockClient) await lockClient.finish() -}) - -module.exports = { - getUserClient: async () => { - if (!userClient) { - await init() - } - return userClient - }, - getSessionClient: async () => { - if (!sessionClient) { - await init() - } - return sessionClient - }, - getAppClient: async () => { - if (!appClient) { - await init() - } - return appClient - }, - getCacheClient: async () => { - if (!cacheClient) { - await init() - } - return cacheClient - }, - getWritethroughClient: async () => { - if (!writethroughClient) { - await init() - } - return writethroughClient - }, - getLockClient: async () => { - if (!lockClient) { - await init() - } - return lockClient - }, -} diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts new file mode 100644 index 0000000000..00329ffb84 --- /dev/null +++ b/packages/backend-core/src/redis/init.ts @@ -0,0 +1,72 @@ +import Client from "./redis" +import * as utils from "./utils" + +let userClient: Client, + sessionClient: Client, + appClient: Client, + cacheClient: Client, + writethroughClient: Client, + lockClient: Client + +async function init() { + userClient = await new Client(utils.Databases.USER_CACHE).init() + sessionClient = await new Client(utils.Databases.SESSIONS).init() + appClient = await new Client(utils.Databases.APP_METADATA).init() + cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init() + lockClient = await new Client(utils.Databases.LOCKS).init() + writethroughClient = await new Client( + utils.Databases.WRITE_THROUGH, + utils.SelectableDatabase.WRITE_THROUGH + ).init() +} + +process.on("exit", async () => { + if (userClient) await userClient.finish() + if (sessionClient) await sessionClient.finish() + if (appClient) await appClient.finish() + if (cacheClient) await cacheClient.finish() + if (writethroughClient) await writethroughClient.finish() + if (lockClient) await lockClient.finish() +}) + +export async function getUserClient() { + if (!userClient) { + await init() + } + return userClient +} + +export async function getSessionClient() { + if (!sessionClient) { + await init() + } + return sessionClient +} + +export async function getAppClient() { + if (!appClient) { + await init() + } + return appClient +} + +export async function getCacheClient() { + if (!cacheClient) { + await init() + } + return cacheClient +} + +export async function getWritethroughClient() { + if (!writethroughClient) { + await init() + } + return writethroughClient +} + +export async function getLockClient() { + if (!lockClient) { + await init() + } + return lockClient +} diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts new file mode 100644 index 0000000000..58734fc4f1 --- /dev/null +++ b/packages/backend-core/src/redis/redis.ts @@ -0,0 +1,279 @@ +import env from "../environment" +// ioredis mock is all in memory +const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") +import { + addDbPrefix, + removeDbPrefix, + getRedisOptions, + SEPARATOR, + SelectableDatabase, +} from "./utils" + +const RETRY_PERIOD_MS = 2000 +const STARTUP_TIMEOUT_MS = 5000 +const CLUSTERED = false +const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT + +// for testing just generate the client once +let CLOSED = false +let CLIENTS: { [key: number]: any } = {} +// if in test always connected +let CONNECTED = env.isTest() + +function pickClient(selectDb: number): any { + return CLIENTS[selectDb] +} + +function connectionError( + selectDb: number, + timeout: NodeJS.Timeout, + err: Error | string +) { + // manually shut down, ignore errors + if (CLOSED) { + return + } + pickClient(selectDb).disconnect() + CLOSED = true + // always clear this on error + clearTimeout(timeout) + CONNECTED = false + console.error("Redis connection failed - " + err) + setTimeout(() => { + init() + }, RETRY_PERIOD_MS) +} + +/** + * Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise + * will return the ioredis client which will be ready to use. + */ +function init(selectDb = DEFAULT_SELECT_DB) { + let timeout: NodeJS.Timeout + CLOSED = false + let client = pickClient(selectDb) + // already connected, ignore + if (client && CONNECTED) { + return + } + // testing uses a single in memory client + if (env.isTest()) { + CLIENTS[selectDb] = new Redis(getRedisOptions()) + } + // start the timer - only allowed 5 seconds to connect + timeout = setTimeout(() => { + if (!CONNECTED) { + connectionError( + selectDb, + timeout, + "Did not successfully connect in timeout" + ) + } + }, STARTUP_TIMEOUT_MS) + + // disconnect any lingering client + if (client) { + client.disconnect() + } + const { redisProtocolUrl, opts, host, port } = getRedisOptions(CLUSTERED) + + if (CLUSTERED) { + client = new Redis.Cluster([{ host, port }], opts) + } else if (redisProtocolUrl) { + client = new Redis(redisProtocolUrl) + } else { + client = new Redis(opts) + } + // attach handlers + client.on("end", (err: Error) => { + connectionError(selectDb, timeout, err) + }) + client.on("error", (err: Error) => { + connectionError(selectDb, timeout, err) + }) + client.on("connect", () => { + clearTimeout(timeout) + CONNECTED = true + }) + CLIENTS[selectDb] = client +} + +function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) { + return new Promise(resolve => { + if (pickClient(selectDb) == null) { + init() + } else if (CONNECTED) { + resolve("") + return + } + // check if the connection is ready + const interval = setInterval(() => { + if (CONNECTED) { + clearInterval(interval) + resolve("") + } + }, 500) + }) +} + +/** + * Utility function, takes a redis stream and converts it to a promisified response - + * this can only be done with redis streams because they will have an end. + * @param stream A redis stream, specifically as this type of stream will have an end. + * @param client The client to use for further lookups. + * @return {Promise} The final output of the stream + */ +function promisifyStream(stream: any, client: RedisWrapper) { + return new Promise((resolve, reject) => { + const outputKeys = new Set() + stream.on("data", (keys: string[]) => { + keys.forEach(key => { + outputKeys.add(key) + }) + }) + stream.on("error", (err: Error) => { + reject(err) + }) + stream.on("end", async () => { + const keysArray: string[] = Array.from(outputKeys) as string[] + try { + let getPromises = [] + for (let key of keysArray) { + getPromises.push(client.get(key)) + } + const jsonArray = await Promise.all(getPromises) + resolve( + keysArray.map(key => ({ + key: removeDbPrefix(key), + value: JSON.parse(jsonArray.shift()), + })) + ) + } catch (err) { + reject(err) + } + }) + }) +} + +class RedisWrapper { + _db: string + _select: number + + constructor(db: string, selectDb: number | null = null) { + this._db = db + this._select = selectDb || DEFAULT_SELECT_DB + } + + getClient() { + return pickClient(this._select) + } + + async init() { + CLOSED = false + init(this._select) + await waitForConnection(this._select) + return this + } + + async finish() { + CLOSED = true + this.getClient().disconnect() + } + + async scan(key = ""): Promise { + const db = this._db + key = `${db}${SEPARATOR}${key}` + let stream + if (CLUSTERED) { + let node = this.getClient().nodes("master") + stream = node[0].scanStream({ match: key + "*", count: 100 }) + } else { + stream = this.getClient().scanStream({ match: key + "*", count: 100 }) + } + return promisifyStream(stream, this.getClient()) + } + + async keys(pattern: string) { + const db = this._db + return this.getClient().keys(addDbPrefix(db, pattern)) + } + + async get(key: string) { + const db = this._db + let response = await this.getClient().get(addDbPrefix(db, key)) + // overwrite the prefixed key + if (response != null && response.key) { + response.key = key + } + // if its not an object just return the response + try { + return JSON.parse(response) + } catch (err) { + return response + } + } + + async bulkGet(keys: string[]) { + const db = this._db + if (keys.length === 0) { + return {} + } + const prefixedKeys = keys.map(key => addDbPrefix(db, key)) + let response = await this.getClient().mget(prefixedKeys) + if (Array.isArray(response)) { + let final: any = {} + let count = 0 + for (let result of response) { + if (result) { + let parsed + try { + parsed = JSON.parse(result) + } catch (err) { + parsed = result + } + final[keys[count]] = parsed + } + count++ + } + return final + } else { + throw new Error(`Invalid response: ${response}`) + } + } + + async store(key: string, value: any, expirySeconds: number | null = null) { + const db = this._db + if (typeof value === "object") { + value = JSON.stringify(value) + } + const prefixedKey = addDbPrefix(db, key) + await this.getClient().set(prefixedKey, value) + if (expirySeconds) { + await this.getClient().expire(prefixedKey, expirySeconds) + } + } + + async getTTL(key: string) { + const db = this._db + const prefixedKey = addDbPrefix(db, key) + return this.getClient().ttl(prefixedKey) + } + + async setExpiry(key: string, expirySeconds: number | null) { + const db = this._db + const prefixedKey = addDbPrefix(db, key) + await this.getClient().expire(prefixedKey, expirySeconds) + } + + async delete(key: string) { + const db = this._db + await this.getClient().del(addDbPrefix(db, key)) + } + + async clear() { + let items = await this.scan() + await Promise.all(items.map((obj: any) => this.delete(obj.key))) + } +} + +export = RedisWrapper diff --git a/packages/backend-core/src/redis/utils.js b/packages/backend-core/src/redis/utils.ts similarity index 68% rename from packages/backend-core/src/redis/utils.js rename to packages/backend-core/src/redis/utils.ts index af719197b5..4c556ebd54 100644 --- a/packages/backend-core/src/redis/utils.js +++ b/packages/backend-core/src/redis/utils.ts @@ -1,10 +1,10 @@ -const env = require("../environment") +import env from "../environment" const SLOT_REFRESH_MS = 2000 const CONNECT_TIMEOUT_MS = 10000 -const SEPARATOR = "-" const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD +export const SEPARATOR = "-" /** * These Redis databases help us to segment up a Redis keyspace by prepending the @@ -12,23 +12,23 @@ const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD * can be split up a bit; allowing us to use scans on small databases to find some particular * keys within. * If writing a very large volume of keys is expected (say 10K+) then it is better to keep these out - * of the default keyspace and use a separate one - the SelectableDatabases can be used for this. + * of the default keyspace and use a separate one - the SelectableDatabase can be used for this. */ -exports.Databases = { - PW_RESETS: "pwReset", - VERIFICATIONS: "verification", - INVITATIONS: "invitation", - DEV_LOCKS: "devLocks", - DEBOUNCE: "debounce", - SESSIONS: "session", - USER_CACHE: "users", - FLAGS: "flags", - APP_METADATA: "appMetadata", - QUERY_VARS: "queryVars", - LICENSES: "license", - GENERIC_CACHE: "data_cache", - WRITE_THROUGH: "writeThrough", - LOCKS: "locks", +export enum Databases { + PW_RESETS = "pwReset", + VERIFICATIONS = "verification", + INVITATIONS = "invitation", + DEV_LOCKS = "devLocks", + DEBOUNCE = "debounce", + SESSIONS = "session", + USER_CACHE = "users", + FLAGS = "flags", + APP_METADATA = "appMetadata", + QUERY_VARS = "queryVars", + LICENSES = "license", + GENERIC_CACHE = "data_cache", + WRITE_THROUGH = "writeThrough", + LOCKS = "locks", } /** @@ -40,30 +40,28 @@ exports.Databases = { * but if you need to walk through all values in a database periodically then a separate selectable * keyspace should be used. */ -exports.SelectableDatabases = { - DEFAULT: 0, - WRITE_THROUGH: 1, - UNUSED_1: 2, - UNUSED_2: 3, - UNUSED_3: 4, - UNUSED_4: 5, - UNUSED_5: 6, - UNUSED_6: 7, - UNUSED_7: 8, - UNUSED_8: 9, - UNUSED_9: 10, - UNUSED_10: 11, - UNUSED_11: 12, - UNUSED_12: 13, - UNUSED_13: 14, - UNUSED_14: 15, +export enum SelectableDatabase { + DEFAULT = 0, + WRITE_THROUGH = 1, + UNUSED_1 = 2, + UNUSED_2 = 3, + UNUSED_3 = 4, + UNUSED_4 = 5, + UNUSED_5 = 6, + UNUSED_6 = 7, + UNUSED_7 = 8, + UNUSED_8 = 9, + UNUSED_9 = 10, + UNUSED_10 = 11, + UNUSED_11 = 12, + UNUSED_12 = 13, + UNUSED_13 = 14, + UNUSED_14 = 15, } -exports.SEPARATOR = SEPARATOR - -exports.getRedisOptions = (clustered = false) => { +export function getRedisOptions(clustered = false) { let password = REDIS_PASSWORD - let url = REDIS_URL.split("//") + let url: string[] | string = REDIS_URL.split("//") // get rid of the protocol url = url.length > 1 ? url[1] : url[0] // check for a password etc @@ -84,7 +82,7 @@ exports.getRedisOptions = (clustered = false) => { redisProtocolUrl = REDIS_URL } - const opts = { + const opts: any = { connectTimeout: CONNECT_TIMEOUT_MS, } if (clustered) { @@ -92,7 +90,7 @@ exports.getRedisOptions = (clustered = false) => { opts.redisOptions.tls = {} opts.redisOptions.password = password opts.slotsRefreshTimeout = SLOT_REFRESH_MS - opts.dnsLookup = (address, callback) => callback(null, address) + opts.dnsLookup = (address: string, callback: any) => callback(null, address) } else { opts.host = host opts.port = port @@ -101,14 +99,14 @@ exports.getRedisOptions = (clustered = false) => { return { opts, host, port, redisProtocolUrl } } -exports.addDbPrefix = (db, key) => { +export function addDbPrefix(db: string, key: string) { if (key.includes(db)) { return key } return `${db}${SEPARATOR}${key}` } -exports.removeDbPrefix = key => { +export function removeDbPrefix(key: string) { let parts = key.split(SEPARATOR) if (parts.length >= 2) { parts.shift() diff --git a/packages/backend-core/src/security/apiKeys.js b/packages/backend-core/src/security/apiKeys.js deleted file mode 100644 index e90418abb8..0000000000 --- a/packages/backend-core/src/security/apiKeys.js +++ /dev/null @@ -1 +0,0 @@ -exports.lookupApiKey = async () => {} diff --git a/packages/backend-core/src/security/encryption.js b/packages/backend-core/src/security/encryption.ts similarity index 73% rename from packages/backend-core/src/security/encryption.js rename to packages/backend-core/src/security/encryption.ts index c31f597652..a9006f302d 100644 --- a/packages/backend-core/src/security/encryption.js +++ b/packages/backend-core/src/security/encryption.ts @@ -1,5 +1,5 @@ -const crypto = require("crypto") -const env = require("../environment") +import crypto from "crypto" +import env from "../environment" const ALGO = "aes-256-ctr" const SECRET = env.JWT_SECRET @@ -8,13 +8,13 @@ const ITERATIONS = 10000 const RANDOM_BYTES = 16 const STRETCH_LENGTH = 32 -function stretchString(string, salt) { +function stretchString(string: string, salt: Buffer) { return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512") } -exports.encrypt = input => { +export function encrypt(input: string) { const salt = crypto.randomBytes(RANDOM_BYTES) - const stretched = stretchString(SECRET, salt) + const stretched = stretchString(SECRET!, salt) const cipher = crypto.createCipheriv(ALGO, stretched, salt) const base = cipher.update(input) const final = cipher.final() @@ -22,10 +22,10 @@ exports.encrypt = input => { return `${salt.toString("hex")}${SEPARATOR}${encrypted}` } -exports.decrypt = input => { +export function decrypt(input: string) { const [salt, encrypted] = input.split(SEPARATOR) const saltBuffer = Buffer.from(salt, "hex") - const stretched = stretchString(SECRET, saltBuffer) + const stretched = stretchString(SECRET!, saltBuffer) const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer) const base = decipher.update(Buffer.from(encrypted, "hex")) const final = decipher.final() diff --git a/packages/backend-core/src/security/permissions.js b/packages/backend-core/src/security/permissions.js deleted file mode 100644 index 2ecb8a9f1e..0000000000 --- a/packages/backend-core/src/security/permissions.js +++ /dev/null @@ -1,164 +0,0 @@ -const { flatten } = require("lodash") -const { cloneDeep } = require("lodash/fp") - -const PermissionLevels = { - READ: "read", - WRITE: "write", - EXECUTE: "execute", - ADMIN: "admin", -} - -// these are the global types, that govern the underlying default behaviour -const PermissionTypes = { - APP: "app", - TABLE: "table", - USER: "user", - AUTOMATION: "automation", - WEBHOOK: "webhook", - BUILDER: "builder", - VIEW: "view", - QUERY: "query", -} - -function Permission(type, level) { - this.level = level - this.type = type -} - -function levelToNumber(perm) { - switch (perm) { - // not everything has execute privileges - case PermissionLevels.EXECUTE: - return 0 - case PermissionLevels.READ: - return 1 - case PermissionLevels.WRITE: - return 2 - case PermissionLevels.ADMIN: - return 3 - default: - return -1 - } -} - -/** - * Given the specified permission level for the user return the levels they are allowed to carry out. - * @param {string} userPermLevel The permission level of the user. - * @return {string[]} All the permission levels this user is allowed to carry out. - */ -function getAllowedLevels(userPermLevel) { - switch (userPermLevel) { - case PermissionLevels.EXECUTE: - return [PermissionLevels.EXECUTE] - case PermissionLevels.READ: - return [PermissionLevels.EXECUTE, PermissionLevels.READ] - case PermissionLevels.WRITE: - case PermissionLevels.ADMIN: - return [ - PermissionLevels.READ, - PermissionLevels.WRITE, - PermissionLevels.EXECUTE, - ] - default: - return [] - } -} - -exports.BUILTIN_PERMISSION_IDS = { - PUBLIC: "public", - READ_ONLY: "read_only", - WRITE: "write", - ADMIN: "admin", - POWER: "power", -} - -const BUILTIN_PERMISSIONS = { - PUBLIC: { - _id: exports.BUILTIN_PERMISSION_IDS.PUBLIC, - name: "Public", - permissions: [ - new Permission(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE), - ], - }, - READ_ONLY: { - _id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY, - name: "Read only", - permissions: [ - new Permission(PermissionTypes.QUERY, PermissionLevels.READ), - new Permission(PermissionTypes.TABLE, PermissionLevels.READ), - new Permission(PermissionTypes.VIEW, PermissionLevels.READ), - ], - }, - WRITE: { - _id: exports.BUILTIN_PERMISSION_IDS.WRITE, - name: "Read/Write", - permissions: [ - new Permission(PermissionTypes.QUERY, PermissionLevels.WRITE), - new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE), - new Permission(PermissionTypes.VIEW, PermissionLevels.READ), - new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), - ], - }, - POWER: { - _id: exports.BUILTIN_PERMISSION_IDS.POWER, - name: "Power", - permissions: [ - new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE), - new Permission(PermissionTypes.USER, PermissionLevels.READ), - new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), - new Permission(PermissionTypes.VIEW, PermissionLevels.READ), - new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ), - ], - }, - ADMIN: { - _id: exports.BUILTIN_PERMISSION_IDS.ADMIN, - name: "Admin", - permissions: [ - new Permission(PermissionTypes.TABLE, PermissionLevels.ADMIN), - new Permission(PermissionTypes.USER, PermissionLevels.ADMIN), - new Permission(PermissionTypes.AUTOMATION, PermissionLevels.ADMIN), - new Permission(PermissionTypes.VIEW, PermissionLevels.ADMIN), - new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ), - new Permission(PermissionTypes.QUERY, PermissionLevels.ADMIN), - ], - }, -} - -exports.getBuiltinPermissions = () => { - return cloneDeep(BUILTIN_PERMISSIONS) -} - -exports.getBuiltinPermissionByID = id => { - const perms = Object.values(BUILTIN_PERMISSIONS) - return perms.find(perm => perm._id === id) -} - -exports.doesHaveBasePermission = (permType, permLevel, rolesHierarchy) => { - const basePermissions = [ - ...new Set(rolesHierarchy.map(role => role.permissionId)), - ] - const builtins = Object.values(BUILTIN_PERMISSIONS) - let permissions = flatten( - builtins - .filter(builtin => basePermissions.indexOf(builtin._id) !== -1) - .map(builtin => builtin.permissions) - ) - for (let permission of permissions) { - if ( - permission.type === permType && - getAllowedLevels(permission.level).indexOf(permLevel) !== -1 - ) { - return true - } - } - return false -} - -exports.isPermissionLevelHigherThanRead = level => { - return levelToNumber(level) > 1 -} - -// utility as a lot of things need simply the builder permission -exports.BUILDER = PermissionTypes.BUILDER -exports.PermissionTypes = PermissionTypes -exports.PermissionLevels = PermissionLevels diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts new file mode 100644 index 0000000000..42189bba0c --- /dev/null +++ b/packages/backend-core/src/security/permissions.ts @@ -0,0 +1,175 @@ +const { flatten } = require("lodash") +const { cloneDeep } = require("lodash/fp") + +export type RoleHierarchy = { + permissionId: string +}[] + +export enum PermissionLevel { + READ = "read", + WRITE = "write", + EXECUTE = "execute", + ADMIN = "admin", +} + +// these are the global types, that govern the underlying default behaviour +export enum PermissionType { + APP = "app", + TABLE = "table", + USER = "user", + AUTOMATION = "automation", + WEBHOOK = "webhook", + BUILDER = "builder", + VIEW = "view", + QUERY = "query", +} + +class Permission { + type: PermissionType + level: PermissionLevel + + constructor(type: PermissionType, level: PermissionLevel) { + this.type = type + this.level = level + } +} + +function levelToNumber(perm: PermissionLevel) { + switch (perm) { + // not everything has execute privileges + case PermissionLevel.EXECUTE: + return 0 + case PermissionLevel.READ: + return 1 + case PermissionLevel.WRITE: + return 2 + case PermissionLevel.ADMIN: + return 3 + default: + return -1 + } +} + +/** + * Given the specified permission level for the user return the levels they are allowed to carry out. + * @param {string} userPermLevel The permission level of the user. + * @return {string[]} All the permission levels this user is allowed to carry out. + */ +function getAllowedLevels(userPermLevel: PermissionLevel) { + switch (userPermLevel) { + case PermissionLevel.EXECUTE: + return [PermissionLevel.EXECUTE] + case PermissionLevel.READ: + return [PermissionLevel.EXECUTE, PermissionLevel.READ] + case PermissionLevel.WRITE: + case PermissionLevel.ADMIN: + return [ + PermissionLevel.READ, + PermissionLevel.WRITE, + PermissionLevel.EXECUTE, + ] + default: + return [] + } +} + +export enum BuiltinPermissionID { + PUBLIC = "public", + READ_ONLY = "read_only", + WRITE = "write", + ADMIN = "admin", + POWER = "power", +} + +const BUILTIN_PERMISSIONS = { + PUBLIC: { + _id: BuiltinPermissionID.PUBLIC, + name: "Public", + permissions: [ + new Permission(PermissionType.WEBHOOK, PermissionLevel.EXECUTE), + ], + }, + READ_ONLY: { + _id: BuiltinPermissionID.READ_ONLY, + name: "Read only", + permissions: [ + new Permission(PermissionType.QUERY, PermissionLevel.READ), + new Permission(PermissionType.TABLE, PermissionLevel.READ), + new Permission(PermissionType.VIEW, PermissionLevel.READ), + ], + }, + WRITE: { + _id: BuiltinPermissionID.WRITE, + name: "Read/Write", + permissions: [ + new Permission(PermissionType.QUERY, PermissionLevel.WRITE), + new Permission(PermissionType.TABLE, PermissionLevel.WRITE), + new Permission(PermissionType.VIEW, PermissionLevel.READ), + new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), + ], + }, + POWER: { + _id: BuiltinPermissionID.POWER, + name: "Power", + permissions: [ + new Permission(PermissionType.TABLE, PermissionLevel.WRITE), + new Permission(PermissionType.USER, PermissionLevel.READ), + new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), + new Permission(PermissionType.VIEW, PermissionLevel.READ), + new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), + ], + }, + ADMIN: { + _id: BuiltinPermissionID.ADMIN, + name: "Admin", + permissions: [ + new Permission(PermissionType.TABLE, PermissionLevel.ADMIN), + new Permission(PermissionType.USER, PermissionLevel.ADMIN), + new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN), + new Permission(PermissionType.VIEW, PermissionLevel.ADMIN), + new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), + new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), + ], + }, +} + +export function getBuiltinPermissions() { + return cloneDeep(BUILTIN_PERMISSIONS) +} + +export function getBuiltinPermissionByID(id: string) { + const perms = Object.values(BUILTIN_PERMISSIONS) + return perms.find(perm => perm._id === id) +} + +export function doesHaveBasePermission( + permType: PermissionType, + permLevel: PermissionLevel, + rolesHierarchy: RoleHierarchy +) { + const basePermissions = [ + ...new Set(rolesHierarchy.map(role => role.permissionId)), + ] + const builtins = Object.values(BUILTIN_PERMISSIONS) + let permissions = flatten( + builtins + .filter(builtin => basePermissions.indexOf(builtin._id) !== -1) + .map(builtin => builtin.permissions) + ) + for (let permission of permissions) { + if ( + permission.type === permType && + getAllowedLevels(permission.level).indexOf(permLevel) !== -1 + ) { + return true + } + } + return false +} + +export function isPermissionLevelHigherThanRead(level: PermissionLevel) { + return levelToNumber(level) > 1 +} + +// utility as a lot of things need simply the builder permission +export const BUILDER = PermissionType.BUILDER diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.ts similarity index 60% rename from packages/backend-core/src/security/roles.js rename to packages/backend-core/src/security/roles.ts index 33c9123b63..bdf7a38726 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.ts @@ -1,21 +1,22 @@ +import { BuiltinPermissionID, PermissionLevel } from "./permissions" +import { generateRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db" +import { getAppDB } from "../context" +import { doWithDB } from "../db" +import { Screen, Role as RoleDoc } from "@budibase/types" const { cloneDeep } = require("lodash/fp") -const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions") -const { - generateRoleID, - getRoleParams, - DocumentType, - SEPARATOR, -} = require("../db/utils") -const { getAppDB } = require("../context") -const { doWithDB } = require("../db") -const BUILTIN_IDS = { +export const BUILTIN_ROLE_IDS = { ADMIN: "ADMIN", POWER: "POWER", BASIC: "BASIC", PUBLIC: "PUBLIC", } +const BUILTIN_IDS = { + ...BUILTIN_ROLE_IDS, + BUILDER: "BUILDER", +} + // exclude internal roles like builder const EXTERNAL_BUILTIN_ROLE_IDS = [ BUILTIN_IDS.ADMIN, @@ -24,60 +25,70 @@ const EXTERNAL_BUILTIN_ROLE_IDS = [ BUILTIN_IDS.PUBLIC, ] -function Role(id, name) { - this._id = id - this.name = name -} +export class Role implements RoleDoc { + _id: string + _rev?: string + name: string + permissionId: string + inherits?: string + permissions = {} -Role.prototype.addPermission = function (permissionId) { - this.permissionId = permissionId - return this -} + constructor(id: string, name: string, permissionId: string) { + this._id = id + this.name = name + this.permissionId = permissionId + } -Role.prototype.addInheritance = function (inherits) { - this.inherits = inherits - return this + addInheritance(inherits: string) { + this.inherits = inherits + return this + } } const BUILTIN_ROLES = { - ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin") - .addPermission(BUILTIN_PERMISSION_IDS.ADMIN) - .addInheritance(BUILTIN_IDS.POWER), - POWER: new Role(BUILTIN_IDS.POWER, "Power") - .addPermission(BUILTIN_PERMISSION_IDS.POWER) - .addInheritance(BUILTIN_IDS.BASIC), - BASIC: new Role(BUILTIN_IDS.BASIC, "Basic") - .addPermission(BUILTIN_PERMISSION_IDS.WRITE) - .addInheritance(BUILTIN_IDS.PUBLIC), - PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission( - BUILTIN_PERMISSION_IDS.PUBLIC - ), - BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission( - BUILTIN_PERMISSION_IDS.ADMIN - ), + ADMIN: new Role( + BUILTIN_IDS.ADMIN, + "Admin", + BuiltinPermissionID.ADMIN + ).addInheritance(BUILTIN_IDS.POWER), + POWER: new Role( + BUILTIN_IDS.POWER, + "Power", + BuiltinPermissionID.POWER + ).addInheritance(BUILTIN_IDS.BASIC), + BASIC: new Role( + BUILTIN_IDS.BASIC, + "Basic", + BuiltinPermissionID.WRITE + ).addInheritance(BUILTIN_IDS.PUBLIC), + PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public", BuiltinPermissionID.PUBLIC), + BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder", BuiltinPermissionID.ADMIN), } -exports.getBuiltinRoles = () => { +export function getBuiltinRoles(): { [key: string]: RoleDoc } { return cloneDeep(BUILTIN_ROLES) } -exports.BUILTIN_ROLE_ID_ARRAY = Object.values(BUILTIN_ROLES).map( +export const BUILTIN_ROLE_ID_ARRAY = Object.values(BUILTIN_ROLES).map( role => role._id ) -exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(BUILTIN_ROLES).map( +export const BUILTIN_ROLE_NAME_ARRAY = Object.values(BUILTIN_ROLES).map( role => role.name ) -function isBuiltin(role) { - return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin)) +export function isBuiltin(role?: string) { + return BUILTIN_ROLE_ID_ARRAY.some(builtin => role?.includes(builtin)) } /** * Works through the inheritance ranks to see how far up the builtin stack this ID is. */ -exports.builtinRoleToNumber = id => { - const builtins = exports.getBuiltinRoles() +export function builtinRoleToNumber(id?: string) { + if (!id) { + return 0 + } + const builtins = getBuiltinRoles() const MAX = Object.values(builtins).length + 1 if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { return MAX @@ -88,7 +99,7 @@ exports.builtinRoleToNumber = id => { if (!role) { break } - role = builtins[role.inherits] + role = builtins[role.inherits!] count++ } while (role !== null) return count @@ -97,14 +108,14 @@ exports.builtinRoleToNumber = id => { /** * Converts any role to a number, but has to be async to get the roles from db. */ -exports.roleToNumber = async id => { - if (exports.isBuiltin(id)) { - return exports.builtinRoleToNumber(id) +export async function roleToNumber(id?: string) { + if (isBuiltin(id)) { + return builtinRoleToNumber(id) } - const hierarchy = await exports.getUserRoleHierarchy(id) + const hierarchy = (await getUserRoleHierarchy(id)) as RoleDoc[] for (let role of hierarchy) { - if (isBuiltin(role.inherits)) { - return exports.builtinRoleToNumber(role.inherits) + 1 + if (isBuiltin(role?.inherits)) { + return builtinRoleToNumber(role.inherits) + 1 } } return 0 @@ -113,15 +124,14 @@ exports.roleToNumber = async id => { /** * Returns whichever builtin roleID is lower. */ -exports.lowerBuiltinRoleID = (roleId1, roleId2) => { +export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string { if (!roleId1) { - return roleId2 + return roleId2 as string } if (!roleId2) { - return roleId1 + return roleId1 as string } - return exports.builtinRoleToNumber(roleId1) > - exports.builtinRoleToNumber(roleId2) + return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2) ? roleId2 : roleId1 } @@ -132,11 +142,11 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => { * @param {string|null} roleId The level ID to lookup. * @returns {Promise} The role object, which may contain an "inherits" property. */ -exports.getRole = async roleId => { +export async function getRole(roleId?: string): Promise { if (!roleId) { - return null + return undefined } - let role = {} + let role: any = {} // built in roles mostly come from the in-code implementation, // but can be extended by a doc stored about them (e.g. permissions) if (isBuiltin(roleId)) { @@ -146,10 +156,10 @@ exports.getRole = async roleId => { } try { const db = getAppDB() - const dbRole = await db.get(exports.getDBRoleID(roleId)) + const dbRole = await db.get(getDBRoleID(roleId)) role = Object.assign(role, dbRole) // finalise the ID - role._id = exports.getExternalRoleID(role._id) + role._id = getExternalRoleID(role._id) } catch (err) { // only throw an error if there is no role at all if (Object.keys(role).length === 0) { @@ -162,12 +172,12 @@ exports.getRole = async roleId => { /** * Simple function to get all the roles based on the top level user role ID. */ -async function getAllUserRoles(userRoleId) { +async function getAllUserRoles(userRoleId?: string): Promise { // admins have access to all roles if (userRoleId === BUILTIN_IDS.ADMIN) { - return exports.getAllRoles() + return getAllRoles() } - let currentRole = await exports.getRole(userRoleId) + let currentRole = await getRole(userRoleId) let roles = currentRole ? [currentRole] : [] let roleIds = [userRoleId] // get all the inherited roles @@ -177,8 +187,10 @@ async function getAllUserRoles(userRoleId) { roleIds.indexOf(currentRole.inherits) === -1 ) { roleIds.push(currentRole.inherits) - currentRole = await exports.getRole(currentRole.inherits) - roles.push(currentRole) + currentRole = await getRole(currentRole.inherits) + if (currentRole) { + roles.push(currentRole) + } } return roles } @@ -191,7 +203,10 @@ async function getAllUserRoles(userRoleId) { * @returns {Promise} returns an ordered array of the roles, with the first being their * highest level of access and the last being the lowest level. */ -exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => { +export async function getUserRoleHierarchy( + userRoleId?: string, + opts = { idOnly: true } +) { // special case, if they don't have a role then they are a public user const roles = await getAllUserRoles(userRoleId) return opts.idOnly ? roles.map(role => role._id) : roles @@ -200,12 +215,15 @@ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => { // this function checks that the provided permissions are in an array format // some templates/older apps will use a simple string instead of array for roles // convert the string to an array using the theory that write is higher than read -exports.checkForRoleResourceArray = (rolePerms, resourceId) => { +export function checkForRoleResourceArray( + rolePerms: { [key: string]: string[] }, + resourceId: string +) { if (rolePerms && !Array.isArray(rolePerms[resourceId])) { - const permLevel = rolePerms[resourceId] + const permLevel = rolePerms[resourceId] as any rolePerms[resourceId] = [permLevel] - if (permLevel === PermissionLevels.WRITE) { - rolePerms[resourceId].push(PermissionLevels.READ) + if (permLevel === PermissionLevel.WRITE) { + rolePerms[resourceId].push(PermissionLevel.READ) } } return rolePerms @@ -215,7 +233,7 @@ exports.checkForRoleResourceArray = (rolePerms, resourceId) => { * Given an app ID this will retrieve all of the roles that are currently within that app. * @return {Promise} An array of the role objects that were found. */ -exports.getAllRoles = async appId => { +export async function getAllRoles(appId?: string) { if (appId) { return doWithDB(appId, internal) } else { @@ -227,30 +245,30 @@ exports.getAllRoles = async appId => { } return internal(appDB) } - async function internal(db) { - let roles = [] + async function internal(db: any) { + let roles: RoleDoc[] = [] if (db) { const body = await db.allDocs( getRoleParams(null, { include_docs: true, }) ) - roles = body.rows.map(row => row.doc) + roles = body.rows.map((row: any) => row.doc) } - const builtinRoles = exports.getBuiltinRoles() + const builtinRoles = getBuiltinRoles() // need to combine builtin with any DB record of them (for sake of permissions) for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) { const builtinRole = builtinRoles[builtinRoleId] const dbBuiltin = roles.filter( - dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId + dbRole => getExternalRoleID(dbRole._id) === builtinRoleId )[0] if (dbBuiltin == null) { roles.push(builtinRole || builtinRoles.BASIC) } else { // remove role and all back after combining with the builtin roles = roles.filter(role => role._id !== dbBuiltin._id) - dbBuiltin._id = exports.getExternalRoleID(dbBuiltin._id) + dbBuiltin._id = getExternalRoleID(dbBuiltin._id) roles.push(Object.assign(builtinRole, dbBuiltin)) } } @@ -260,7 +278,7 @@ exports.getAllRoles = async appId => { continue } for (let resourceId of Object.keys(role.permissions)) { - role.permissions = exports.checkForRoleResourceArray( + role.permissions = checkForRoleResourceArray( role.permissions, resourceId ) @@ -277,11 +295,11 @@ exports.getAllRoles = async appId => { * @param subResourceId The sub resource being requested * @return {Promise<{permissions}|Object>} returns the permissions required to access. */ -exports.getRequiredResourceRole = async ( - permLevel, - { resourceId, subResourceId } -) => { - const roles = await exports.getAllRoles() +export async function getRequiredResourceRole( + permLevel: string, + { resourceId, subResourceId }: { resourceId?: string; subResourceId?: string } +) { + const roles = await getAllRoles() let main = [], sub = [] for (let role of roles) { @@ -289,8 +307,8 @@ exports.getRequiredResourceRole = async ( if (!role.permissions) { continue } - const mainRes = role.permissions[resourceId] - const subRes = role.permissions[subResourceId] + const mainRes = resourceId ? role.permissions[resourceId] : undefined + const subRes = subResourceId ? role.permissions[subResourceId] : undefined if (mainRes && mainRes.indexOf(permLevel) !== -1) { main.push(role._id) } else if (subRes && subRes.indexOf(permLevel) !== -1) { @@ -301,12 +319,13 @@ exports.getRequiredResourceRole = async ( return main.concat(sub) } -class AccessController { +export class AccessController { + userHierarchies: { [key: string]: string[] } constructor() { this.userHierarchies = {} } - async hasAccess(tryingRoleId, userRoleId) { + async hasAccess(tryingRoleId?: string, userRoleId?: string) { // special cases, the screen has no role, the roles are the same or the user // is currently in the builder if ( @@ -318,16 +337,18 @@ class AccessController { ) { return true } - let roleIds = this.userHierarchies[userRoleId] - if (!roleIds) { - roleIds = await exports.getUserRoleHierarchy(userRoleId) + let roleIds = userRoleId ? this.userHierarchies[userRoleId] : null + if (!roleIds && userRoleId) { + roleIds = (await getUserRoleHierarchy(userRoleId, { + idOnly: true, + })) as string[] this.userHierarchies[userRoleId] = roleIds } - return roleIds.indexOf(tryingRoleId) !== -1 + return roleIds?.indexOf(tryingRoleId) !== -1 } - async checkScreensAccess(screens, userRoleId) { + async checkScreensAccess(screens: Screen[], userRoleId: string) { let accessibleScreens = [] // don't want to handle this with Promise.all as this would mean all custom roles would be // retrieved at same time, it is likely a custom role will be re-used and therefore want @@ -341,8 +362,8 @@ class AccessController { return accessibleScreens } - async checkScreenAccess(screen, userRoleId) { - const roleId = screen && screen.routing ? screen.routing.roleId : null + async checkScreenAccess(screen: Screen, userRoleId: string) { + const roleId = screen && screen.routing ? screen.routing.roleId : undefined if (await this.hasAccess(roleId, userRoleId)) { return screen } @@ -353,8 +374,8 @@ class AccessController { /** * Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions). */ -exports.getDBRoleID = roleId => { - if (roleId.startsWith(DocumentType.ROLE)) { +export function getDBRoleID(roleId?: string) { + if (roleId?.startsWith(DocumentType.ROLE)) { return roleId } return generateRoleID(roleId) @@ -363,15 +384,10 @@ exports.getDBRoleID = roleId => { /** * Remove the "role_" from builtin role IDs that have been written to the DB (for permissions). */ -exports.getExternalRoleID = roleId => { - // for built in roles we want to remove the DB role ID element (role_) - if (roleId.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) { +export function getExternalRoleID(roleId?: string) { + // for built-in roles we want to remove the DB role ID element (role_) + if (roleId?.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) { return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1] } return roleId } - -exports.AccessController = AccessController -exports.BUILTIN_ROLE_IDS = BUILTIN_IDS -exports.isBuiltin = isBuiltin -exports.Role = Role diff --git a/packages/backend-core/src/security/sessions.ts b/packages/backend-core/src/security/sessions.ts index 33230afc60..48e75b0d60 100644 --- a/packages/backend-core/src/security/sessions.ts +++ b/packages/backend-core/src/security/sessions.ts @@ -89,6 +89,7 @@ export async function createASession( userId, } await client.store(key, session, EXPIRY_SECONDS) + return session } export async function updateSessionTTL(session: Session) { diff --git a/packages/backend-core/src/tenancy/index.ts b/packages/backend-core/src/tenancy/index.ts index e0006abab2..1618a136dd 100644 --- a/packages/backend-core/src/tenancy/index.ts +++ b/packages/backend-core/src/tenancy/index.ts @@ -1,9 +1,2 @@ -import * as context from "../context" -import * as tenancy from "./tenancy" - -const pkg = { - ...context, - ...tenancy, -} - -export = pkg +export * from "../context" +export * from "./tenancy" diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index ad5c6b5287..e0e0703433 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -1,20 +1,23 @@ -import { doWithDB } from "../db" -import { queryPlatformView } from "../db/views" -import { StaticDatabases, ViewName } from "../db/constants" -import { getGlobalDBName } from "../db/tenancy" +import { doWithDB, queryPlatformView, getGlobalDBName } from "../db" import { - getTenantId, DEFAULT_TENANT_ID, - isMultiTenant, + getTenantId, getTenantIDFromAppID, + isMultiTenant, } from "../context" import env from "../environment" -import { PlatformUser } from "@budibase/types" +import { + BBContext, + PlatformUser, + TenantResolutionStrategy, + GetTenantIdOptions, +} from "@budibase/types" +import { Header, StaticDatabases, ViewName } from "../constants" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name -export const addTenantToUrl = (url: string) => { +export function addTenantToUrl(url: string) { const tenantId = getTenantId() if (isMultiTenant()) { @@ -25,7 +28,7 @@ export const addTenantToUrl = (url: string) => { return url } -export const doesTenantExist = async (tenantId: string) => { +export async function doesTenantExist(tenantId: string) { return doWithDB(PLATFORM_INFO_DB, async (db: any) => { let tenants try { @@ -42,12 +45,12 @@ export const doesTenantExist = async (tenantId: string) => { }) } -export const tryAddTenant = async ( +export async function tryAddTenant( tenantId: string, userId: string, email: string, afterCreateTenant: () => Promise -) => { +) { return doWithDB(PLATFORM_INFO_DB, async (db: any) => { const getDoc = async (id: string) => { if (!id) { @@ -89,11 +92,11 @@ export const tryAddTenant = async ( }) } -export const doWithGlobalDB = (tenantId: string, cb: any) => { +export function doWithGlobalDB(tenantId: string, cb: any) { return doWithDB(getGlobalDBName(tenantId), cb) } -export const lookupTenantId = async (userId: string) => { +export async function lookupTenantId(userId: string) { return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null try { @@ -109,19 +112,26 @@ export const lookupTenantId = async (userId: string) => { } // lookup, could be email or userId, either will return a doc -export const getTenantUser = async ( +export async function getTenantUser( identifier: string -): Promise => { +): Promise { // use the view here and allow to find anyone regardless of casing - // Use lowercase to ensure email login is case insensitive - const response = queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { - keys: [identifier.toLowerCase()], - include_docs: true, - }) as Promise - return response + // Use lowercase to ensure email login is case-insensitive + const users = await queryPlatformView( + ViewName.PLATFORM_USERS_LOWERCASE, + { + keys: [identifier.toLowerCase()], + include_docs: true, + } + ) + if (Array.isArray(users)) { + return users[0] + } else { + return users + } } -export const isUserInAppTenant = (appId: string, user?: any) => { +export function isUserInAppTenant(appId: string, user?: any) { let userTenantId if (user) { userTenantId = user.tenantId || DEFAULT_TENANT_ID @@ -132,7 +142,7 @@ export const isUserInAppTenant = (appId: string, user?: any) => { return tenantId === userTenantId } -export const getTenantIds = async () => { +export async function getTenantIds() { return doWithDB(PLATFORM_INFO_DB, async (db: any) => { let tenants try { @@ -144,3 +154,108 @@ export const getTenantIds = async () => { return (tenants && tenants.tenantIds) || [] }) } + +const ALL_STRATEGIES = Object.values(TenantResolutionStrategy) + +export const getTenantIDFromCtx = ( + ctx: BBContext, + opts: GetTenantIdOptions +): string | null => { + // exit early if not multi-tenant + if (!isMultiTenant()) { + return DEFAULT_TENANT_ID + } + + // opt defaults + if (opts.allowNoTenant === undefined) { + opts.allowNoTenant = false + } + if (!opts.includeStrategies) { + opts.includeStrategies = ALL_STRATEGIES + } + if (!opts.excludeStrategies) { + opts.excludeStrategies = [] + } + + const isAllowed = (strategy: TenantResolutionStrategy) => { + // excluded takes precedence + if (opts.excludeStrategies?.includes(strategy)) { + return false + } + if (opts.includeStrategies?.includes(strategy)) { + return true + } + } + + // always use user first + if (isAllowed(TenantResolutionStrategy.USER)) { + const userTenantId = ctx.user?.tenantId + if (userTenantId) { + return userTenantId + } + } + + // header + if (isAllowed(TenantResolutionStrategy.HEADER)) { + const headerTenantId = ctx.request.headers[Header.TENANT_ID] + if (headerTenantId) { + return headerTenantId as string + } + } + + // query param + if (isAllowed(TenantResolutionStrategy.QUERY)) { + const queryTenantId = ctx.request.query.tenantId + if (queryTenantId) { + return queryTenantId as string + } + } + + // subdomain + if (isAllowed(TenantResolutionStrategy.SUBDOMAIN)) { + // e.g. budibase.app or local.com:10000 + const platformHost = new URL(env.PLATFORM_URL).host.split(":")[0] + // e.g. tenant.budibase.app or tenant.local.com + const requestHost = ctx.host + // parse the tenant id from the difference + if (requestHost.includes(platformHost)) { + const tenantId = requestHost.substring( + 0, + requestHost.indexOf(`.${platformHost}`) + ) + if (tenantId) { + return tenantId + } + } + } + + // path + if (isAllowed(TenantResolutionStrategy.PATH)) { + // params - have to parse manually due to koa-router not run yet + const match = ctx.matched.find( + (m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId") + ) + + // get the raw path url - without any query params + const ctxUrl = ctx.originalUrl + let url + if (ctxUrl.includes("?")) { + url = ctxUrl.split("?")[0] + } else { + url = ctxUrl + } + + if (match) { + const params = match.params(url, match.captures(url), {}) + if (params.tenantId) { + return params.tenantId + } + } + } + + if (!opts.allowNoTenant) { + ctx.throw(403, "Tenant id not set") + } + + return null +} diff --git a/packages/backend-core/src/tests/utils.spec.js b/packages/backend-core/src/tests/utils.spec.js index b487f23c6d..fb3828921d 100644 --- a/packages/backend-core/src/tests/utils.spec.js +++ b/packages/backend-core/src/tests/utils.spec.js @@ -1,8 +1,8 @@ -require("../../tests/utilities/TestConfiguration") -const { structures } = require("../../tests/utilities") +const { structures } = require("../../tests") const utils = require("../utils") const events = require("../events") -const { doInTenant, DEFAULT_TENANT_ID }= require("../context") +const { DEFAULT_TENANT_ID } = require("../constants") +const { doInTenant } = require("../context") describe("utils", () => { describe("platformLogout", () => { diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 44f04749c9..1720a79a83 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -3,15 +3,14 @@ import { getUsersByAppParams, getProdAppID, generateAppUserID, -} from "./db/utils" -import { queryGlobalView } from "./db/views" -import { UNICODE_MAX } from "./db/constants" + queryGlobalView, + UNICODE_MAX, +} from "./db" import { BulkDocsResponse, User } from "@budibase/types" import { getGlobalDB } from "./context" -import PouchDB from "pouchdb" export const bulkGetGlobalUsersById = async (userIds: string[]) => { - const db = getGlobalDB() as PouchDB.Database + const db = getGlobalDB() return ( await db.allDocs({ keys: userIds, @@ -21,7 +20,7 @@ export const bulkGetGlobalUsersById = async (userIds: string[]) => { } export const bulkUpdateGlobalUsers = async (users: User[]) => { - const db = getGlobalDB() as PouchDB.Database + const db = getGlobalDB() return (await db.bulkDocs(users)) as BulkDocsResponse } @@ -69,7 +68,7 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { if (!user) { return } - return generateAppUserID(getProdAppID(appId), user._id!) + return generateAppUserID(getProdAppID(appId)!, user._id!) } /** diff --git a/packages/backend-core/src/hashing.js b/packages/backend-core/src/utils/hashing.ts similarity index 53% rename from packages/backend-core/src/hashing.js rename to packages/backend-core/src/utils/hashing.ts index 7524e66043..220ffea47f 100644 --- a/packages/backend-core/src/hashing.js +++ b/packages/backend-core/src/utils/hashing.ts @@ -1,18 +1,14 @@ -const env = require("./environment") +import env from "../environment" +export * from "../newid" const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt") -const { v4 } = require("uuid") const SALT_ROUNDS = env.SALT_ROUNDS || 10 -exports.hash = async data => { +export async function hash(data: string) { const salt = await bcrypt.genSalt(SALT_ROUNDS) return bcrypt.hash(data, salt) } -exports.compare = async (data, encrypted) => { +export async function compare(data: string, encrypted: string) { return bcrypt.compare(data, encrypted) } - -exports.newid = function () { - return v4().replace(/-/g, "") -} diff --git a/packages/backend-core/src/utils/index.ts b/packages/backend-core/src/utils/index.ts new file mode 100644 index 0000000000..8e663bce52 --- /dev/null +++ b/packages/backend-core/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./hashing" +export * from "./utils" diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils/utils.ts similarity index 64% rename from packages/backend-core/src/utils.js rename to packages/backend-core/src/utils/utils.ts index 6b59c7cb72..3e9fbb177a 100644 --- a/packages/backend-core/src/utils.js +++ b/packages/backend-core/src/utils/utils.ts @@ -1,38 +1,46 @@ -const { DocumentType, SEPARATOR, ViewName, getAllApps } = require("./db/utils") +import { getAllApps, queryGlobalView } from "../db" +import { options } from "../middleware/passport/jwt" +import { Header, Cookie, MAX_VALID_DATE } from "../constants" +import env from "../environment" +import * as userCache from "../cache/user" +import { getSessionsForUser, invalidateSessions } from "../security/sessions" +import * as events from "../events" +import * as tenancy from "../tenancy" +import { + App, + BBContext, + PlatformLogoutOpts, + TenantResolutionStrategy, +} from "@budibase/types" +import { SetOption } from "cookies" +import { DocumentType, SEPARATOR, ViewName } from "../constants" const jwt = require("jsonwebtoken") -const { options } = require("./middleware/passport/jwt") -const { queryGlobalView } = require("./db/views") -const { Headers, Cookies, MAX_VALID_DATE } = require("./constants") -const env = require("./environment") -const userCache = require("./cache/user") -const { - getSessionsForUser, - invalidateSessions, -} = require("./security/sessions") -const events = require("./events") -const tenancy = require("./tenancy") const APP_PREFIX = DocumentType.APP + SEPARATOR const PROD_APP_PREFIX = "/app/" -function confirmAppId(possibleAppId) { +function confirmAppId(possibleAppId: string | undefined) { return possibleAppId && possibleAppId.startsWith(APP_PREFIX) ? possibleAppId : undefined } -async function resolveAppUrl(ctx) { +async function resolveAppUrl(ctx: BBContext) { const appUrl = ctx.path.split("/")[2] let possibleAppUrl = `/${appUrl.toLowerCase()}` - let tenantId = tenancy.getTenantId() - if (!env.SELF_HOSTED && ctx.subdomains.length) { - // always use the tenant id from the url in cloud - tenantId = ctx.subdomains[0] + let tenantId: string | null = tenancy.getTenantId() + if (env.MULTI_TENANCY) { + // always use the tenant id from the subdomain in multi tenancy + // this ensures the logged-in user tenant id doesn't overwrite + // e.g. in the case of viewing a public app while already logged-in to another tenant + tenantId = tenancy.getTenantIDFromCtx(ctx, { + includeStrategies: [TenantResolutionStrategy.SUBDOMAIN], + }) } // search prod apps for a url that matches - const apps = await tenancy.doInTenant(tenantId, () => + const apps: App[] = await tenancy.doInTenant(tenantId, () => getAllApps({ dev: false }) ) const app = apps.filter( @@ -42,7 +50,7 @@ async function resolveAppUrl(ctx) { return app && app.appId ? app.appId : undefined } -exports.isServingApp = ctx => { +export function isServingApp(ctx: BBContext) { // dev app if (ctx.path.startsWith(`/${APP_PREFIX}`)) { return true @@ -59,12 +67,12 @@ exports.isServingApp = ctx => { * @param {object} ctx The main request body to look through. * @returns {string|undefined} If an appId was found it will be returned. */ -exports.getAppIdFromCtx = async ctx => { +export async function getAppIdFromCtx(ctx: BBContext) { // look in headers - const options = [ctx.headers[Headers.APP_ID]] + const options = [ctx.headers[Header.APP_ID]] let appId for (let option of options) { - appId = confirmAppId(option) + appId = confirmAppId(option as string) if (appId) { break } @@ -95,7 +103,7 @@ exports.getAppIdFromCtx = async ctx => { * opens the contents of the specified encrypted JWT. * @return {object} the contents of the token. */ -exports.openJwt = token => { +export function openJwt(token: string) { if (!token) { return token } @@ -107,14 +115,14 @@ exports.openJwt = token => { * @param {object} ctx The request which is to be manipulated. * @param {string} name The name of the cookie to get. */ -exports.getCookie = (ctx, name) => { +export function getCookie(ctx: BBContext, name: string) { const cookie = ctx.cookies.get(name) if (!cookie) { return cookie } - return exports.openJwt(cookie) + return openJwt(cookie) } /** @@ -124,12 +132,17 @@ exports.getCookie = (ctx, name) => { * @param {string|object} value The value of cookie which will be set. * @param {object} opts options like whether to sign. */ -exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => { +export function setCookie( + ctx: BBContext, + value: any, + name = "builder", + opts = { sign: true } +) { if (value && opts && opts.sign) { value = jwt.sign(value, options.secretOrKey) } - const config = { + const config: SetOption = { expires: MAX_VALID_DATE, path: "/", httpOnly: false, @@ -146,8 +159,8 @@ exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => { /** * Utility function, simply calls setCookie with an empty string for value */ -exports.clearCookie = (ctx, name) => { - exports.setCookie(ctx, null, name) +export function clearCookie(ctx: BBContext, name: string) { + setCookie(ctx, null, name) } /** @@ -156,11 +169,11 @@ exports.clearCookie = (ctx, name) => { * @param {object} ctx The koa context object to be tested. * @return {boolean} returns true if the call is from the client lib (a built app rather than the builder). */ -exports.isClient = ctx => { - return ctx.headers[Headers.TYPE] === "client" +export function isClient(ctx: BBContext) { + return ctx.headers[Header.TYPE] === "client" } -const getBuilders = async () => { +async function getBuilders() { const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, { include_docs: false, }) @@ -176,7 +189,7 @@ const getBuilders = async () => { } } -exports.getBuildersCount = async () => { +export async function getBuildersCount() { const builders = await getBuilders() return builders.length } @@ -184,10 +197,14 @@ exports.getBuildersCount = async () => { /** * Logs a user out from budibase. Re-used across account portal and builder. */ -exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { +export async function platformLogout(opts: PlatformLogoutOpts) { + const ctx = opts.ctx + const userId = opts.userId + const keepActiveSession = opts.keepActiveSession + if (!ctx) throw new Error("Koa context must be supplied to logout.") - const currentSession = exports.getCookie(ctx, Cookies.Auth) + const currentSession = getCookie(ctx, Cookie.Auth) let sessions = await getSessionsForUser(userId) if (keepActiveSession) { @@ -196,8 +213,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { ) } else { // clear cookies - exports.clearCookie(ctx, Cookies.Auth) - exports.clearCookie(ctx, Cookies.CurrentApp) + clearCookie(ctx, Cookie.Auth) + clearCookie(ctx, Cookie.CurrentApp) } const sessionIds = sessions.map(({ sessionId }) => sessionId) @@ -206,6 +223,6 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { await userCache.invalidateUser(userId) } -exports.timeout = timeMs => { +export function timeout(timeMs: number) { return new Promise(resolve => setTimeout(resolve, timeMs)) } diff --git a/packages/backend-core/tenancy.js b/packages/backend-core/tenancy.js deleted file mode 100644 index 9ca808b74e..0000000000 --- a/packages/backend-core/tenancy.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/tenancy") diff --git a/packages/worker/scripts/jestSetup.js b/packages/backend-core/tests/jestSetup.ts similarity index 62% rename from packages/worker/scripts/jestSetup.js rename to packages/backend-core/tests/jestSetup.ts index 8ee2d33d70..7870a721aa 100644 --- a/packages/worker/scripts/jestSetup.js +++ b/packages/backend-core/tests/jestSetup.ts @@ -1,17 +1,21 @@ -const env = require("../src/environment") +import env from "../src/environment" +import { mocks } from "./utilities" + +// must explicitly enable fetch mock +mocks.fetch.enable() + +// mock all dates to 2020-01-01T00:00:00.000Z +// use tk.reset() to use real dates in individual tests +import tk from "timekeeper" +tk.freeze(mocks.date.MOCK_DATE) env._set("SELF_HOSTED", "1") env._set("NODE_ENV", "jest") env._set("JWT_SECRET", "test-jwtsecret") env._set("LOG_LEVEL", "silent") -env._set("MULTI_TENANCY", true) - -const { mocks } = require("@budibase/backend-core/tests") - -// mock all dates to 2020-01-01T00:00:00.000Z -// use tk.reset() to use real dates in individual tests -const tk = require("timekeeper") -tk.freeze(mocks.date.MOCK_DATE) +env._set("MINIO_URL", "http://localhost") +env._set("MINIO_ACCESS_KEY", "test") +env._set("MINIO_SECRET_KEY", "test") global.console.log = jest.fn() // console.log are ignored in tests diff --git a/packages/backend-core/tests/utilities/TestConfiguration.js b/packages/backend-core/tests/utilities/TestConfiguration.js deleted file mode 100644 index 207b1d937f..0000000000 --- a/packages/backend-core/tests/utilities/TestConfiguration.js +++ /dev/null @@ -1 +0,0 @@ -require("./db") diff --git a/packages/backend-core/tests/utilities/db.js b/packages/backend-core/tests/utilities/db.js deleted file mode 100644 index 9d94bc2c7c..0000000000 --- a/packages/backend-core/tests/utilities/db.js +++ /dev/null @@ -1,6 +0,0 @@ -const core = require("../../src/index") -const dbConfig = { - inMemory: true, - allDbs: true, -} -core.init({ db: dbConfig }) diff --git a/packages/backend-core/tests/utilities/db.ts b/packages/backend-core/tests/utilities/db.ts new file mode 100644 index 0000000000..84b77bb201 --- /dev/null +++ b/packages/backend-core/tests/utilities/db.ts @@ -0,0 +1,9 @@ +import * as db from "../../src/db" + +const dbConfig = { + inMemory: true, +} + +export const init = () => { + db.init(dbConfig) +} diff --git a/packages/backend-core/tests/utilities/index.ts b/packages/backend-core/tests/utilities/index.ts index 1e73be4c17..65578ff013 100644 --- a/packages/backend-core/tests/utilities/index.ts +++ b/packages/backend-core/tests/utilities/index.ts @@ -1,2 +1,6 @@ export * as mocks from "./mocks" export * as structures from "./structures" +export { generator } from "./structures" + +import * as dbConfig from "./db" +dbConfig.init() diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts index 79436443db..cb4c68b65e 100644 --- a/packages/backend-core/tests/utilities/mocks/accounts.ts +++ b/packages/backend-core/tests/utilities/mocks/accounts.ts @@ -1,7 +1,9 @@ export const getAccount = jest.fn() export const getAccountByTenantId = jest.fn() +export const getStatus = jest.fn() jest.mock("../../../src/cloud/accounts", () => ({ getAccount, getAccountByTenantId, + getStatus, })) diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts index 415d59019d..40c3706a55 100644 --- a/packages/backend-core/tests/utilities/mocks/events.ts +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -117,3 +117,7 @@ jest.spyOn(events.view, "filterDeleted") jest.spyOn(events.view, "calculationCreated") jest.spyOn(events.view, "calculationUpdated") jest.spyOn(events.view, "calculationDeleted") + +jest.spyOn(events.plugin, "init") +jest.spyOn(events.plugin, "imported") +jest.spyOn(events.plugin, "deleted") diff --git a/packages/backend-core/tests/utilities/mocks/fetch.ts b/packages/backend-core/tests/utilities/mocks/fetch.ts new file mode 100644 index 0000000000..eeb0ccda45 --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/fetch.ts @@ -0,0 +1,10 @@ +const mockFetch = jest.fn() + +const enable = () => { + jest.mock("node-fetch", () => mockFetch) +} + +export default { + ...mockFetch, + enable, +} diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts index 7031b225ec..931816be45 100644 --- a/packages/backend-core/tests/utilities/mocks/index.ts +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -2,3 +2,5 @@ import "./posthog" import "./events" export * as accounts from "./accounts" export * as date from "./date" +export * as licenses from "./licenses" +export { default as fetch } from "./fetch" diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts new file mode 100644 index 0000000000..0ef5eedb73 --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -0,0 +1,83 @@ +import { Feature, License, Quotas } from "@budibase/types" +import _ from "lodash" + +let CLOUD_FREE_LICENSE: License +let TEST_LICENSE: License +let getCachedLicense: any + +// init for the packages other than pro +export function init(proPkg: any) { + initInternal({ + CLOUD_FREE_LICENSE: proPkg.constants.licenses.CLOUD_FREE_LICENSE, + TEST_LICENSE: proPkg.constants.licenses.DEVELOPER_FREE_LICENSE, + getCachedLicense: proPkg.licensing.cache.getCachedLicense, + }) +} + +// init for the pro package +export function initInternal(opts: { + CLOUD_FREE_LICENSE: License + TEST_LICENSE: License + getCachedLicense: any +}) { + CLOUD_FREE_LICENSE = opts.CLOUD_FREE_LICENSE + TEST_LICENSE = opts.TEST_LICENSE + getCachedLicense = opts.getCachedLicense +} + +export interface UseLicenseOpts { + features?: Feature[] + quotas?: Quotas +} + +// LICENSES + +export const useLicense = (license: License, opts?: UseLicenseOpts) => { + if (opts) { + if (opts.features) { + license.features.push(...opts.features) + } + if (opts.quotas) { + license.quotas = opts.quotas + } + } + + getCachedLicense.mockReturnValue(license) + + return license +} + +export const useUnlimited = (opts?: UseLicenseOpts) => { + return useLicense(TEST_LICENSE, opts) +} + +export const useCloudFree = () => { + return useLicense(CLOUD_FREE_LICENSE) +} + +// FEATURES + +const useFeature = (feature: Feature) => { + const license = _.cloneDeep(TEST_LICENSE) + const opts: UseLicenseOpts = { + features: [feature], + } + + return useLicense(license, opts) +} + +export const useBackups = () => { + return useFeature(Feature.APP_BACKUPS) +} + +export const useGroups = () => { + return useFeature(Feature.USER_GROUPS) +} + +// QUOTAS + +export const setAutomationLogsQuota = (value: number) => { + const license = _.cloneDeep(TEST_LICENSE) + license.quotas.constant.automationLogRetentionDays.value = value + return useLicense(license) +} diff --git a/packages/backend-core/tests/utilities/structures/accounts.ts b/packages/backend-core/tests/utilities/structures/accounts.ts index 5d23962575..f1718aecc0 100644 --- a/packages/backend-core/tests/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/utilities/structures/accounts.ts @@ -1,23 +1,29 @@ import { generator, uuid } from "." -import { AuthType, CloudAccount, Hosting } from "@budibase/types" import * as db from "../../../src/db/utils" +import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types" -export const cloudAccount = (): CloudAccount => { +export const account = (): Account => { return { accountId: uuid(), + tenantId: generator.word(), + email: generator.email(), + tenantName: generator.word(), + hosting: Hosting.SELF, createdAt: Date.now(), verified: true, verificationSent: true, - tier: "", - email: generator.email(), - tenantId: generator.word(), - hosting: Hosting.CLOUD, + tier: "FREE", // DEPRECATED authType: AuthType.PASSWORD, - password: generator.word(), - tenantName: generator.word(), name: generator.name(), size: "10+", profession: "Software Engineer", + } +} + +export const cloudAccount = (): CloudAccount => { + return { + ...account(), + hosting: Hosting.CLOUD, budibaseUserId: db.generateGlobalUserID(), } } diff --git a/packages/backend-core/tests/utilities/structures/common.ts b/packages/backend-core/tests/utilities/structures/common.ts index 51ae220254..05b879f36b 100644 --- a/packages/backend-core/tests/utilities/structures/common.ts +++ b/packages/backend-core/tests/utilities/structures/common.ts @@ -1 +1,7 @@ +import { v4 as uuid } from "uuid" + export { v4 as uuid } from "uuid" + +export const email = () => { + return `${uuid()}@test.com` +} diff --git a/packages/backend-core/tests/utilities/structures/koa.ts b/packages/backend-core/tests/utilities/structures/koa.ts index 6f0f7866e6..a33dca1546 100644 --- a/packages/backend-core/tests/utilities/structures/koa.ts +++ b/packages/backend-core/tests/utilities/structures/koa.ts @@ -1,5 +1,14 @@ -import { createMockContext } from "@shopify/jest-koa-mocks" +import { createMockContext, createMockCookies } from "@shopify/jest-koa-mocks" +import { BBContext } from "@budibase/types" -export const newContext = () => { - return createMockContext() +export const newContext = (): BBContext => { + const ctx = createMockContext() + return { + ...ctx, + cookies: createMockCookies(), + request: { + ...ctx.request, + body: {}, + }, + } } diff --git a/packages/backend-core/tsconfig.build.json b/packages/backend-core/tsconfig.build.json index 40ffe6b827..9682f3e32f 100644 --- a/packages/backend-core/tsconfig.build.json +++ b/packages/backend-core/tsconfig.build.json @@ -3,7 +3,6 @@ "target": "es6", "module": "commonjs", "lib": ["es2020"], - "allowJs": true, "strict": true, "noImplicitAny": true, "esModuleInterop": true, @@ -12,7 +11,7 @@ "sourceMap": true, "declaration": true, "types": [ "node", "jest" ], - "outDir": "dist" + "outDir": "dist", }, "include": [ "**/*.js", diff --git a/packages/backend-core/utils.js b/packages/backend-core/utils.js deleted file mode 100644 index 2ef920e103..0000000000 --- a/packages/backend-core/utils.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - ...require("./src/utils"), - ...require("./src/hashing"), -} diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index d301526ba1..0a25d5fb43 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -17,12 +17,45 @@ dependencies: "@babel/highlight" "^7.16.7" +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + "@babel/compat-data@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== -"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": +"@babel/compat-data@^7.20.0": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" + integrity sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ== + +"@babel/core@^7.11.6": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92" + integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.2" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-module-transforms" "^7.20.2" + "@babel/helpers" "^7.20.1" + "@babel/parser" "^7.20.2" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/core@^7.12.3": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.2.tgz#87b2fcd7cce9becaa7f5acebdc4f09f3dd19d876" integrity sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ== @@ -52,6 +85,15 @@ "@jridgewell/gen-mapping" "^0.3.0" jsesc "^2.5.1" +"@babel/generator@^7.20.1", "@babel/generator@^7.20.2": + version "7.20.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" + integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== + dependencies: + "@babel/types" "^7.20.2" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + "@babel/helper-compilation-targets@^7.18.2": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" @@ -62,11 +104,26 @@ browserslist "^4.20.2" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" + integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== + dependencies: + "@babel/compat-data" "^7.20.0" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + semver "^6.3.0" + "@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.2": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + "@babel/helper-function-name@^7.17.9": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" @@ -75,6 +132,14 @@ "@babel/template" "^7.16.7" "@babel/types" "^7.17.0" +"@babel/helper-function-name@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" + integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== + dependencies: + "@babel/template" "^7.18.10" + "@babel/types" "^7.19.0" + "@babel/helper-hoist-variables@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" @@ -82,6 +147,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + "@babel/helper-module-imports@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" @@ -89,6 +161,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + "@babel/helper-module-transforms@^7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" @@ -103,6 +182,20 @@ "@babel/traverse" "^7.18.0" "@babel/types" "^7.18.0" +"@babel/helper-module-transforms@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" + integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.17.12", "@babel/helper-plugin-utils@^7.8.0": version "7.17.12" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" @@ -115,6 +208,13 @@ dependencies: "@babel/types" "^7.18.2" +"@babel/helper-simple-access@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" + integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== + dependencies: + "@babel/types" "^7.20.2" + "@babel/helper-split-export-declaration@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" @@ -122,16 +222,38 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + "@babel/helper-validator-option@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + "@babel/helpers@^7.18.2": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.2.tgz#970d74f0deadc3f5a938bfa250738eb4ac889384" @@ -141,6 +263,15 @@ "@babel/traverse" "^7.18.2" "@babel/types" "^7.18.2" +"@babel/helpers@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9" + integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg== + dependencies: + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.0" + "@babel/highlight@^7.16.7": version "7.17.12" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" @@ -150,11 +281,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.18.0": version "7.18.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== +"@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" + integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -262,6 +407,15 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/template@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + "@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.7.2": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.2.tgz#b77a52604b5cc836a9e1e08dca01cba67a12d2e8" @@ -278,6 +432,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" + integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.1" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.20.1" + "@babel/types" "^7.20.0" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.18.0", "@babel/types@^7.18.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.18.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" @@ -286,11 +456,27 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" + integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -319,173 +505,197 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" - integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== +"@jest/console@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-28.1.3.tgz#2030606ec03a18c31803b8a36382762e447655df" + integrity sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^28.1.3" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^27.5.1" - jest-util "^27.5.1" + jest-message-util "^28.1.3" + jest-util "^28.1.3" slash "^3.0.0" -"@jest/core@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" - integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== +"@jest/core@^28.1.1", "@jest/core@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-28.1.3.tgz#0ebf2bd39840f1233cd5f2d1e6fc8b71bd5a1ac7" + integrity sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA== dependencies: - "@jest/console" "^27.5.1" - "@jest/reporters" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/console" "^28.1.3" + "@jest/reporters" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - emittery "^0.8.1" + ci-info "^3.2.0" exit "^0.1.2" graceful-fs "^4.2.9" - jest-changed-files "^27.5.1" - jest-config "^27.5.1" - jest-haste-map "^27.5.1" - jest-message-util "^27.5.1" - jest-regex-util "^27.5.1" - jest-resolve "^27.5.1" - jest-resolve-dependencies "^27.5.1" - jest-runner "^27.5.1" - jest-runtime "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" - jest-validate "^27.5.1" - jest-watcher "^27.5.1" + jest-changed-files "^28.1.3" + jest-config "^28.1.3" + jest-haste-map "^28.1.3" + jest-message-util "^28.1.3" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.3" + jest-resolve-dependencies "^28.1.3" + jest-runner "^28.1.3" + jest-runtime "^28.1.3" + jest-snapshot "^28.1.3" + jest-util "^28.1.3" + jest-validate "^28.1.3" + jest-watcher "^28.1.3" micromatch "^4.0.4" + pretty-format "^28.1.3" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" - integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== +"@jest/environment@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-28.1.3.tgz#abed43a6b040a4c24fdcb69eab1f97589b2d663e" + integrity sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA== dependencies: - "@jest/fake-timers" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/fake-timers" "^28.1.3" + "@jest/types" "^28.1.3" "@types/node" "*" - jest-mock "^27.5.1" + jest-mock "^28.1.3" -"@jest/fake-timers@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" - integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== +"@jest/expect-utils@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.3.tgz#58561ce5db7cd253a7edddbc051fb39dda50f525" + integrity sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA== dependencies: - "@jest/types" "^27.5.1" - "@sinonjs/fake-timers" "^8.0.1" + jest-get-type "^28.0.2" + +"@jest/expect@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-28.1.3.tgz#9ac57e1d4491baca550f6bdbd232487177ad6a72" + integrity sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw== + dependencies: + expect "^28.1.3" + jest-snapshot "^28.1.3" + +"@jest/fake-timers@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-28.1.3.tgz#230255b3ad0a3d4978f1d06f70685baea91c640e" + integrity sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw== + dependencies: + "@jest/types" "^28.1.3" + "@sinonjs/fake-timers" "^9.1.2" "@types/node" "*" - jest-message-util "^27.5.1" - jest-mock "^27.5.1" - jest-util "^27.5.1" + jest-message-util "^28.1.3" + jest-mock "^28.1.3" + jest-util "^28.1.3" -"@jest/globals@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" - integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== +"@jest/globals@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-28.1.3.tgz#a601d78ddc5fdef542728309894895b4a42dc333" + integrity sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA== dependencies: - "@jest/environment" "^27.5.1" - "@jest/types" "^27.5.1" - expect "^27.5.1" + "@jest/environment" "^28.1.3" + "@jest/expect" "^28.1.3" + "@jest/types" "^28.1.3" -"@jest/reporters@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" - integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== +"@jest/reporters@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-28.1.3.tgz#9adf6d265edafc5fc4a434cfb31e2df5a67a369a" + integrity sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/console" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" + "@jridgewell/trace-mapping" "^0.3.13" "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" - glob "^7.1.2" + glob "^7.1.3" graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" istanbul-lib-instrument "^5.1.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-haste-map "^27.5.1" - jest-resolve "^27.5.1" - jest-util "^27.5.1" - jest-worker "^27.5.1" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + jest-worker "^28.1.3" slash "^3.0.0" - source-map "^0.6.0" string-length "^4.0.1" + strip-ansi "^6.0.0" terminal-link "^2.0.0" - v8-to-istanbul "^8.1.0" + v8-to-istanbul "^9.0.1" -"@jest/source-map@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" - integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/source-map@^28.1.2": + version "28.1.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-28.1.2.tgz#7fe832b172b497d6663cdff6c13b0a920e139e24" + integrity sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww== + dependencies: + "@jridgewell/trace-mapping" "^0.3.13" callsites "^3.0.0" graceful-fs "^4.2.9" - source-map "^0.6.0" -"@jest/test-result@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" - integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== +"@jest/test-result@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-28.1.3.tgz#5eae945fd9f4b8fcfce74d239e6f725b6bf076c5" + integrity sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg== dependencies: - "@jest/console" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/console" "^28.1.3" + "@jest/types" "^28.1.3" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" - integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== +"@jest/test-sequencer@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz#9d0c283d906ac599c74bde464bc0d7e6a82886c3" + integrity sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw== dependencies: - "@jest/test-result" "^27.5.1" + "@jest/test-result" "^28.1.3" graceful-fs "^4.2.9" - jest-haste-map "^27.5.1" - jest-runtime "^27.5.1" + jest-haste-map "^28.1.3" + slash "^3.0.0" -"@jest/transform@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" - integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== +"@jest/transform@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-28.1.3.tgz#59d8098e50ab07950e0f2fc0fc7ec462371281b0" + integrity sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA== dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^27.5.1" + "@babel/core" "^7.11.6" + "@jest/types" "^28.1.3" + "@jridgewell/trace-mapping" "^0.3.13" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.9" - jest-haste-map "^27.5.1" - jest-regex-util "^27.5.1" - jest-util "^27.5.1" + jest-haste-map "^28.1.3" + jest-regex-util "^28.0.2" + jest-util "^28.1.3" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" + write-file-atomic "^4.0.1" -"@jest/types@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" - integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== +"@jest/types@^28.1.1", "@jest/types@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" + integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== dependencies: + "@jest/schemas" "^28.1.3" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" - "@types/yargs" "^16.0.0" + "@types/yargs" "^17.0.8" chalk "^4.0.0" "@jridgewell/gen-mapping@^0.1.0": @@ -505,6 +715,20 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + "@jridgewell/resolve-uri@^3.0.3": version "3.0.7" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" @@ -515,11 +739,37 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.13" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" @@ -598,6 +848,11 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@sinclair/typebox@^0.24.1": + version "0.24.51" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" + integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -610,10 +865,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^8.0.1": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" - integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== +"@sinonjs/fake-timers@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== dependencies: "@sinonjs/commons" "^1.7.0" @@ -635,10 +890,25 @@ request "^2.88.0" webfinger "^0.4.2" -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== "@types/accepts@*": version "1.3.5" @@ -647,7 +917,7 @@ dependencies: "@types/node" "*" -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": +"@types/babel__core@^7.1.14": version "7.1.19" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== @@ -673,7 +943,7 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": version "7.17.1" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.1.tgz#1a0e73e8c28c7e832656db372b779bfd2ef37314" integrity sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA== @@ -746,7 +1016,7 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/graceful-fs@^4.1.2": +"@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== @@ -809,7 +1079,7 @@ dependencies: "@types/koa" "*" -"@types/koa@*": +"@types/koa@*", "@types/koa@2.13.4": version "2.13.4" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b" integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw== @@ -823,18 +1093,6 @@ "@types/koa-compose" "*" "@types/node" "*" -"@types/koa@2.0.52": - version "2.0.52" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.52.tgz#7dd11de4189ab339ad66c4ccad153716b14e525f" - integrity sha512-cp/GTOhOYwomlSKqEoG0kaVEVJEzP4ojYmfa7EKaGkmkkRwJ4B/1VBLbQZ49Z+WJNvzXejQB/9GIKqMo9XLgFQ== - dependencies: - "@types/accepts" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" - "@types/lodash@4.14.180": version "4.14.180" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" @@ -1077,6 +1335,11 @@ dependencies: "@types/node" "*" +"@types/tough-cookie@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/uuid@8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -1087,18 +1350,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== -"@types/yargs@^16.0.0": - version "16.0.4" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" - integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== +"@types/yargs@^17.0.8": + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" + integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== dependencies: "@types/yargs-parser" "*" -abab@^2.0.3, abab@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" - integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== - abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1148,30 +1406,17 @@ accepts@^1.3.5, accepts@^1.3.7: mime-types "~2.1.34" negotiator "0.6.3" -acorn-globals@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" - integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== - dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" +acorn-walk@^8.1.1: + 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== -acorn-walk@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn@^8.4.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== -acorn@^7.1.1: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - -acorn@^8.2.4: - version "8.7.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" - integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== - -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -1226,11 +1471,6 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -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== - anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -1252,6 +1492,11 @@ are-we-there-yet@^2.0.0: delegates "^1.0.0" readable-stream "^3.6.0" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1276,13 +1521,6 @@ 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== -async@~2.1.4: - version "2.1.5" - resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" - integrity sha512-+g/Ncjbx0JSq2Mk03WQkyKvNh5q9Qvyo/RIqIqnmC5feJY70PNl2ESwZU2BhAB+AZPkHNzzyC2Dq2AS5VnTKhQ== - dependencies: - lodash "^4.14.0" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1328,16 +1566,24 @@ axios@0.24.0: dependencies: follow-redirects "^1.14.4" -babel-jest@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" - integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== +axios@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35" + integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA== dependencies: - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +babel-jest@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5" + integrity sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q== + dependencies: + "@jest/transform" "^28.1.3" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^27.5.1" + babel-preset-jest "^28.1.3" chalk "^4.0.0" graceful-fs "^4.2.9" slash "^3.0.0" @@ -1353,14 +1599,14 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" - integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== +babel-plugin-jest-hoist@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz#1952c4d0ea50f2d6d794353762278d1d8cca3fbe" + integrity sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" - "@types/babel__core" "^7.0.0" + "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" babel-preset-current-node-syntax@^1.0.0: @@ -1381,12 +1627,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" - integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== +babel-preset-jest@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz#5dfc20b99abed5db994406c2b9ab94c73aaa419d" + integrity sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A== dependencies: - babel-plugin-jest-hoist "^27.5.1" + babel-plugin-jest-hoist "^28.1.3" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -1472,11 +1718,6 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - browserslist@^4.20.2: version "4.20.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477" @@ -1488,6 +1729,16 @@ browserslist@^4.20.2: node-releases "^2.0.5" picocolors "^1.0.0" +browserslist@^4.21.3: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -1570,6 +1821,14 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1590,6 +1849,11 @@ caniuse-lite@^1.0.30001349: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz#cc6f5da3f983979ad1e2cdbae0505dccaa7c6a12" integrity sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA== +caniuse-lite@^1.0.30001400: + version "1.0.30001431" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" + integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1672,13 +1936,13 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: string-width "^4.2.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" wrap-ansi "^7.0.0" clone-buffer@1.0.0: @@ -1790,14 +2054,6 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" -cookies@~0.7.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa" - integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A== - dependencies: - depd "~1.1.2" - keygrip "~1.0.3" - cookies@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" @@ -1816,6 +2072,11 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cron-parser@^4.2.1: version "4.6.0" resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.6.0.tgz#404c3fdbff10ae80eef6b709555d577ef2fd2e0d" @@ -1842,23 +2103,6 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - -cssstyle@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" - integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== - dependencies: - cssom "~0.3.6" - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -1866,15 +2110,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-urls@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" - integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== - dependencies: - abab "^2.0.3" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" - debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -1889,23 +2124,11 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw== -decimal.js@^10.2.1: - version "10.3.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" - integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== - decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -1928,11 +2151,6 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@~0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -1966,7 +2184,7 @@ denque@^1.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== -depd@^1.1.0, depd@^1.1.2, depd@~1.1.2: +depd@^1.1.0, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== @@ -1996,12 +2214,15 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== -domexception@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" - integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== - dependencies: - webidl-conversions "^5.0.0" +diff-sequences@^28.1.1: + version "28.1.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" + integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== dot-prop@^5.2.0: version "5.3.0" @@ -2050,6 +2271,11 @@ electron-to-chromium@^1.4.147: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.150.tgz#89f0e12505462d5df7e56c5b91aff7e1dfdd33ec" integrity sha512-MP3oBer0X7ZeS9GJ0H6lmkn561UxiwOIY9TTkdxVY7lI9G6GVCKfgJaHaDcakwdKxBXA4T3ybeswH/WBIN/KTA== +electron-to-chromium@^1.4.251: + version "1.4.284" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + emitter-listener@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" @@ -2057,10 +2283,10 @@ emitter-listener@1.1.2: dependencies: shimmer "^1.2.0" -emittery@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" - integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +emittery@^0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" + integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== emoji-regex@^8.0.0: version "8.0.0" @@ -2110,11 +2336,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error-inject@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" - integrity sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg== - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -2140,33 +2361,11 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" - integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== - dependencies: - esprima "^4.0.1" - estraverse "^5.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -esprima@^4.0.0, esprima@^4.0.1: +esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -2197,15 +2396,16 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" - integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== +expect@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.3.tgz#90a7c1a124f1824133dd4533cce2d2bdcb6603ec" + integrity sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g== dependencies: - "@jest/types" "^27.5.1" - jest-get-type "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" + "@jest/expect-utils" "^28.1.3" + jest-get-type "^28.0.2" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" extend@~3.0.2: version "3.0.2" @@ -2232,11 +2432,6 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -2292,6 +2487,11 @@ follow-redirects@^1.14.4: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -2306,6 +2506,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -2377,6 +2586,15 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + 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" @@ -2420,7 +2638,7 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: +glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2444,32 +2662,6 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -google-auth-library@~0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" - integrity sha512-KM54Y9GhdAzfXUHmWEoYmaOykSLuMG7W4HvVLYqyogxOyE6px8oSS8W13ngqW0oDGZ915GFW3V6OM6+qcdvPOA== - dependencies: - gtoken "^1.2.1" - jws "^3.1.4" - lodash.noop "^3.0.1" - request "^2.74.0" - -google-p12-pem@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" - integrity sha512-puhMlJ2+E/rgvxWaqgN/nC7x623OAE8MR9vBUqxF0inCE7HoVfCHvTeQ9+BR+rj9KM0fIg6XV6tmbt7XHHssoQ== - dependencies: - node-forge "^0.7.1" - -googleapis@^16.0.0: - version "16.1.0" - resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576" - integrity sha512-5czmF7xkIlJKc1+/+5tltrI1skoR3HKtkDOld9rk+DOucTpZRjOhCoJzoSjxB3M8rP2tEb1VIr1TPyzR3V2PUQ== - dependencies: - async "~2.1.4" - google-auth-library "~0.10.0" - string-template "~1.0.0" - got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -2492,16 +2684,6 @@ 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== -gtoken@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8" - integrity sha512-wQAJflfoqSgMWrSBk9Fg86q+sd6s7y6uJhIvvIPz++RElGlMtEqsdAR2oWwZ/WTEtp7P9xFbJRrT976oRgzJ/w== - dependencies: - google-p12-pem "^0.1.0" - jws "^3.0.0" - mime "^1.4.1" - request "^2.72.0" - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -2525,7 +2707,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.2: +has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -2554,13 +2736,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -html-encoding-sniffer@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" - integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== - dependencies: - whatwg-encoding "^1.0.5" - html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -2579,6 +2754,13 @@ http-cache-semantics@^4.0.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== +http-cookie-agent@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/http-cookie-agent/-/http-cookie-agent-4.0.2.tgz#dcdaae18ed1f7452d81ae4d5cd80b227d6831b69" + integrity sha512-noTmxdH5CuytTnLj/Qv3Z84e/YFq8yLXAw3pqIYZ25Edhb9pQErIAC+ednw40Cic6Le/h9ryph5/TqsvkOaUCw== + dependencies: + agent-base "^6.0.2" + http-errors@^1.6.3, http-errors@~1.8.0: version "1.8.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" @@ -2590,15 +2772,6 @@ http-errors@^1.6.3, http-errors@~1.8.0: statuses ">= 1.5.0 < 2" toidentifier "1.0.1" -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2621,13 +2794,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - ieee754@1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -2826,11 +2992,6 @@ is-path-inside@^3.0.2: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-potential-custom-element-name@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" - integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== - is-retry-allowed@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" @@ -2913,85 +3074,82 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" - integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== +jest-changed-files@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-28.1.3.tgz#d9aeee6792be3686c47cb988a8eaf82ff4238831" + integrity sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA== dependencies: - "@jest/types" "^27.5.1" execa "^5.0.0" - throat "^6.0.1" + p-limit "^3.1.0" -jest-circus@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" - integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== +jest-circus@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-28.1.3.tgz#d14bd11cf8ee1a03d69902dc47b6bd4634ee00e4" + integrity sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow== dependencies: - "@jest/environment" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/environment" "^28.1.3" + "@jest/expect" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" - expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" - jest-runtime "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" - pretty-format "^27.5.1" + jest-each "^28.1.3" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-runtime "^28.1.3" + jest-snapshot "^28.1.3" + jest-util "^28.1.3" + p-limit "^3.1.0" + pretty-format "^28.1.3" slash "^3.0.0" stack-utils "^2.0.3" - throat "^6.0.1" -jest-cli@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" - integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== +jest-cli@^28.1.1: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-28.1.3.tgz#558b33c577d06de55087b8448d373b9f654e46b2" + integrity sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ== dependencies: - "@jest/core" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/core" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^27.5.1" - jest-util "^27.5.1" - jest-validate "^27.5.1" + jest-config "^28.1.3" + jest-util "^28.1.3" + jest-validate "^28.1.3" prompts "^2.0.1" - yargs "^16.2.0" + yargs "^17.3.1" -jest-config@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" - integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== +jest-config@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-28.1.3.tgz#e315e1f73df3cac31447eed8b8740a477392ec60" + integrity sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ== dependencies: - "@babel/core" "^7.8.0" - "@jest/test-sequencer" "^27.5.1" - "@jest/types" "^27.5.1" - babel-jest "^27.5.1" + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^28.1.3" + "@jest/types" "^28.1.3" + babel-jest "^28.1.3" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" - glob "^7.1.1" + glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^27.5.1" - jest-environment-jsdom "^27.5.1" - jest-environment-node "^27.5.1" - jest-get-type "^27.5.1" - jest-jasmine2 "^27.5.1" - jest-regex-util "^27.5.1" - jest-resolve "^27.5.1" - jest-runner "^27.5.1" - jest-util "^27.5.1" - jest-validate "^27.5.1" + jest-circus "^28.1.3" + jest-environment-node "^28.1.3" + jest-get-type "^28.0.2" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.3" + jest-runner "^28.1.3" + jest-util "^28.1.3" + jest-validate "^28.1.3" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^27.5.1" + pretty-format "^28.1.3" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -3005,106 +3163,84 @@ jest-diff@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-docblock@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" - integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== +jest-diff@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.3.tgz#948a192d86f4e7a64c5264ad4da4877133d8792f" + integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== + dependencies: + chalk "^4.0.0" + diff-sequences "^28.1.1" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-docblock@^28.1.1: + version "28.1.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-28.1.1.tgz#6f515c3bf841516d82ecd57a62eed9204c2f42a8" + integrity sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA== dependencies: detect-newline "^3.0.0" -jest-each@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" - integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== +jest-each@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-28.1.3.tgz#bdd1516edbe2b1f3569cfdad9acd543040028f81" + integrity sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^28.1.3" chalk "^4.0.0" - jest-get-type "^27.5.1" - jest-util "^27.5.1" - pretty-format "^27.5.1" + jest-get-type "^28.0.2" + jest-util "^28.1.3" + pretty-format "^28.1.3" -jest-environment-jsdom@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" - integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== +jest-environment-node@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-28.1.3.tgz#7e74fe40eb645b9d56c0c4b70ca4357faa349be5" + integrity sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A== dependencies: - "@jest/environment" "^27.5.1" - "@jest/fake-timers" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/environment" "^28.1.3" + "@jest/fake-timers" "^28.1.3" + "@jest/types" "^28.1.3" "@types/node" "*" - jest-mock "^27.5.1" - jest-util "^27.5.1" - jsdom "^16.6.0" - -jest-environment-node@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" - integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== - dependencies: - "@jest/environment" "^27.5.1" - "@jest/fake-timers" "^27.5.1" - "@jest/types" "^27.5.1" - "@types/node" "*" - jest-mock "^27.5.1" - jest-util "^27.5.1" + jest-mock "^28.1.3" + jest-util "^28.1.3" jest-get-type@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== -jest-haste-map@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" - integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== +jest-get-type@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" + integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== + +jest-haste-map@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-28.1.3.tgz#abd5451129a38d9841049644f34b034308944e2b" + integrity sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA== dependencies: - "@jest/types" "^27.5.1" - "@types/graceful-fs" "^4.1.2" + "@jest/types" "^28.1.3" + "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.9" - jest-regex-util "^27.5.1" - jest-serializer "^27.5.1" - jest-util "^27.5.1" - jest-worker "^27.5.1" + jest-regex-util "^28.0.2" + jest-util "^28.1.3" + jest-worker "^28.1.3" micromatch "^4.0.4" - walker "^1.0.7" + walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" - integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== +jest-leak-detector@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz#a6685d9b074be99e3adee816ce84fd30795e654d" + integrity sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA== dependencies: - "@jest/environment" "^27.5.1" - "@jest/source-map" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^27.5.1" - is-generator-fn "^2.0.0" - jest-each "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" - jest-runtime "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" - pretty-format "^27.5.1" - throat "^6.0.1" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" -jest-leak-detector@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" - integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== - dependencies: - jest-get-type "^27.5.1" - pretty-format "^27.5.1" - -jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: +jest-matcher-utils@^27.0.0: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== @@ -3114,27 +3250,37 @@ jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-message-util@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" - integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== +jest-matcher-utils@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz#5a77f1c129dd5ba3b4d7fc20728806c78893146e" + integrity sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw== + dependencies: + chalk "^4.0.0" + jest-diff "^28.1.3" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-message-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.3.tgz#232def7f2e333f1eecc90649b5b94b0055e7c43d" + integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.5.1" + "@jest/types" "^28.1.3" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^27.5.1" + pretty-format "^28.1.3" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" - integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== +jest-mock@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-28.1.3.tgz#d4e9b1fc838bea595c77ab73672ebf513ab249da" + integrity sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^28.1.3" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -3142,181 +3288,174 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-regex-util@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" - integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== +jest-regex-util@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead" + integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== -jest-resolve-dependencies@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" - integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== +jest-resolve-dependencies@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz#8c65d7583460df7275c6ea2791901fa975c1fe66" + integrity sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA== dependencies: - "@jest/types" "^27.5.1" - jest-regex-util "^27.5.1" - jest-snapshot "^27.5.1" + jest-regex-util "^28.0.2" + jest-snapshot "^28.1.3" -jest-resolve@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" - integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== +jest-resolve@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-28.1.3.tgz#cfb36100341ddbb061ec781426b3c31eb51aa0a8" + integrity sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ== dependencies: - "@jest/types" "^27.5.1" chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^27.5.1" + jest-haste-map "^28.1.3" jest-pnp-resolver "^1.2.2" - jest-util "^27.5.1" - jest-validate "^27.5.1" + jest-util "^28.1.3" + jest-validate "^28.1.3" resolve "^1.20.0" resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" - integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== +jest-runner@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-28.1.3.tgz#5eee25febd730b4713a2cdfd76bdd5557840f9a1" + integrity sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA== dependencies: - "@jest/console" "^27.5.1" - "@jest/environment" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/console" "^28.1.3" + "@jest/environment" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" "@types/node" "*" chalk "^4.0.0" - emittery "^0.8.1" + emittery "^0.10.2" graceful-fs "^4.2.9" - jest-docblock "^27.5.1" - jest-environment-jsdom "^27.5.1" - jest-environment-node "^27.5.1" - jest-haste-map "^27.5.1" - jest-leak-detector "^27.5.1" - jest-message-util "^27.5.1" - jest-resolve "^27.5.1" - jest-runtime "^27.5.1" - jest-util "^27.5.1" - jest-worker "^27.5.1" - source-map-support "^0.5.6" - throat "^6.0.1" + jest-docblock "^28.1.1" + jest-environment-node "^28.1.3" + jest-haste-map "^28.1.3" + jest-leak-detector "^28.1.3" + jest-message-util "^28.1.3" + jest-resolve "^28.1.3" + jest-runtime "^28.1.3" + jest-util "^28.1.3" + jest-watcher "^28.1.3" + jest-worker "^28.1.3" + p-limit "^3.1.0" + source-map-support "0.5.13" -jest-runtime@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" - integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== +jest-runtime@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-28.1.3.tgz#a57643458235aa53e8ec7821949e728960d0605f" + integrity sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw== dependencies: - "@jest/environment" "^27.5.1" - "@jest/fake-timers" "^27.5.1" - "@jest/globals" "^27.5.1" - "@jest/source-map" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/environment" "^28.1.3" + "@jest/fake-timers" "^28.1.3" + "@jest/globals" "^28.1.3" + "@jest/source-map" "^28.1.2" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" execa "^5.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^27.5.1" - jest-message-util "^27.5.1" - jest-mock "^27.5.1" - jest-regex-util "^27.5.1" - jest-resolve "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" + jest-haste-map "^28.1.3" + jest-message-util "^28.1.3" + jest-mock "^28.1.3" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.3" + jest-snapshot "^28.1.3" + jest-util "^28.1.3" slash "^3.0.0" strip-bom "^4.0.0" -jest-serializer@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" - integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== +jest-snapshot@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-28.1.3.tgz#17467b3ab8ddb81e2f605db05583d69388fc0668" + integrity sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg== dependencies: - "@types/node" "*" - graceful-fs "^4.2.9" - -jest-snapshot@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" - integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== - dependencies: - "@babel/core" "^7.7.2" + "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" - "@babel/types" "^7.0.0" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" - "@types/babel__traverse" "^7.0.4" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/babel__traverse" "^7.0.6" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^27.5.1" + expect "^28.1.3" graceful-fs "^4.2.9" - jest-diff "^27.5.1" - jest-get-type "^27.5.1" - jest-haste-map "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" - jest-util "^27.5.1" + jest-diff "^28.1.3" + jest-get-type "^28.0.2" + jest-haste-map "^28.1.3" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" natural-compare "^1.4.0" - pretty-format "^27.5.1" - semver "^7.3.2" + pretty-format "^28.1.3" + semver "^7.3.5" -jest-util@^27.0.0, jest-util@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" - integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== +jest-util@^28.0.0, jest-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.3.tgz#f4f932aa0074f0679943220ff9cbba7e497028b0" + integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^28.1.3" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" - integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== +jest-validate@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-28.1.3.tgz#e322267fd5e7c64cea4629612c357bbda96229df" + integrity sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^28.1.3" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^27.5.1" + jest-get-type "^28.0.2" leven "^3.1.0" - pretty-format "^27.5.1" + pretty-format "^28.1.3" -jest-watcher@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" - integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== +jest-watcher@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-28.1.3.tgz#c6023a59ba2255e3b4c57179fc94164b3e73abd4" + integrity sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g== dependencies: - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^27.5.1" + emittery "^0.10.2" + jest-util "^28.1.3" string-length "^4.0.1" -jest-worker@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== +jest-worker@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.3.tgz#7e3c4ce3fa23d1bb6accb169e7f396f98ed4bb98" + integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" - integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== +jest@28.1.1: + version "28.1.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-28.1.1.tgz#3c39a3a09791e16e9ef283597d24ab19a0df701e" + integrity sha512-qw9YHBnjt6TCbIDMPMpJZqf9E12rh6869iZaN08/vpOGgHJSAaLLUn6H8W3IAEuy34Ls3rct064mZLETkxJ2XA== dependencies: - "@jest/core" "^27.5.1" + "@jest/core" "^28.1.1" + "@jest/types" "^28.1.1" import-local "^3.0.2" - jest-cli "^27.5.1" + jest-cli "^28.1.1" jmespath@0.15.0: version "0.15.0" @@ -3357,39 +3496,6 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== -jsdom@^16.6.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" - integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== - dependencies: - abab "^2.0.5" - acorn "^8.2.4" - acorn-globals "^6.0.0" - cssom "^0.4.4" - cssstyle "^2.3.0" - data-urls "^2.0.0" - decimal.js "^10.2.1" - domexception "^2.0.1" - escodegen "^2.0.0" - form-data "^3.0.0" - html-encoding-sniffer "^2.0.1" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "6.0.1" - saxes "^5.0.1" - symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.1.0" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.5.0" - ws "^7.4.6" - xml-name-validator "^3.0.0" - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -3420,7 +3526,7 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@2.x, json5@^2.2.1: +json5@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== @@ -3460,7 +3566,7 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.0.0, jws@^3.1.4, jws@^3.2.2: +jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== @@ -3468,11 +3574,6 @@ jws@^3.0.0, jws@^3.1.4, jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -keygrip@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" - integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g== - keygrip@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" @@ -3492,26 +3593,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -koa-compose@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" - integrity sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw== - dependencies: - any-promise "^1.1.0" - koa-compose@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== -koa-convert@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" - integrity sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA== - dependencies: - co "^4.6.0" - koa-compose "^3.0.0" - koa-convert@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5" @@ -3520,11 +3606,6 @@ koa-convert@^2.0.0: co "^4.6.0" koa-compose "^4.1.0" -koa-is-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" - integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw== - koa-passport@4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa" @@ -3532,37 +3613,7 @@ koa-passport@4.1.4: dependencies: passport "^0.4.0" -koa@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf" - integrity sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q== - dependencies: - accepts "^1.3.5" - cache-content-type "^1.0.0" - content-disposition "~0.5.2" - content-type "^1.0.4" - cookies "~0.7.1" - debug "~3.1.0" - delegates "^1.0.0" - depd "^1.1.2" - destroy "^1.0.4" - error-inject "^1.0.0" - escape-html "^1.0.3" - fresh "~0.5.2" - http-assert "^1.3.0" - http-errors "^1.6.3" - is-generator-function "^1.0.7" - koa-compose "^4.1.0" - koa-convert "^1.2.0" - koa-is-json "^1.0.0" - on-finished "^2.3.0" - only "~0.0.2" - parseurl "^1.3.2" - statuses "^1.5.0" - type-is "^1.6.16" - vary "^1.1.2" - -koa@^2.13.4: +koa@2.13.4, koa@^2.13.4: version "2.13.4" resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e" integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g== @@ -3692,14 +3743,6 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - lie@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" @@ -3769,11 +3812,6 @@ lodash.memoize@4.x: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== -lodash.noop@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" - integrity sha512-TmYdmu/pebrdTIBDK/FDx9Bmfzs9x0sZG6QIJuMDTqEPfeciLcN13ij+cOd0i9vwJfBtbG9UQ+C7MkXgYxrIJg== - lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -3784,7 +3822,7 @@ lodash.pick@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== -lodash@4.17.21, lodash@^4.14.0, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3823,7 +3861,7 @@ make-dir@^3.0.0, make-dir@^3.1.0: dependencies: semver "^6.0.0" -make-error@1.x: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -3896,7 +3934,7 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24, dependencies: mime-db "1.52.0" -mime@^1.3.4, mime@^1.4.1: +mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -3923,6 +3961,11 @@ minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + minipass@^3.0.0: version "3.1.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" @@ -3948,11 +3991,6 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -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" @@ -3984,6 +4022,18 @@ msgpackr@^1.5.2: optionalDependencies: msgpackr-extract "^2.1.2" +nano@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/nano/-/nano-10.1.0.tgz#afdd5a7440e62f09a8e23f41fcea328d27383922" + integrity sha512-COeN2TpLcHuSN44QLnPmfZCoCsKAg8/aelPOVqqm/2/MvRHDEA11/Kld5C4sLzDlWlhFZ3SO2WGJGevCsvcEzQ== + dependencies: + "@types/tough-cookie" "^4.0.2" + axios "^1.1.3" + http-cookie-agent "^4.0.2" + node-abort-controller "^3.0.1" + qs "^6.11.0" + tough-cookie "^4.1.2" + napi-macros@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" @@ -4009,6 +4059,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +node-abort-controller@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== + node-addon-api@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -4026,11 +4081,6 @@ node-fetch@2.6.7, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-forge@^0.7.1: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - node-gyp-build-optional-packages@5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" @@ -4067,6 +4117,11 @@ node-releases@^2.0.5: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + nodemon@2.0.16: version "2.0.16" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.16.tgz#d71b31bfdb226c25de34afea53486c8ef225fdef" @@ -4124,11 +4179,6 @@ npmlog@^5.0.1: gauge "^3.0.0" set-blocking "^2.0.0" -nwsapi@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" - integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== - oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -4144,6 +4194,11 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -4170,18 +4225,6 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -4204,6 +4247,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -4248,24 +4298,11 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - parseurl@^1.3.2, parseurl@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -passport-google-auth@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938" - integrity sha512-cfAqna6jZLyMEwUdd4PIwAh2mQKQVEDAaRIaom1pG6h4x4Gwjllf/Jflt3TkR1Sen5Rkvr3l7kSXCWE1EKkh8g== - dependencies: - googleapis "^16.0.0" - passport-strategy "1.x" - 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" @@ -4328,7 +4365,7 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x, passport-strategy@1.x.x, passport-strategy@^1.0.0: +passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== @@ -4614,11 +4651,6 @@ pouchdb@7.3.0: uuid "8.3.2" vuvuzela "1.0.3" -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== - prepend-http@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" @@ -4633,6 +4665,16 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -4646,6 +4688,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -4686,6 +4733,13 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" +qs@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -4696,6 +4750,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + range-parser@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -4716,6 +4775,11 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + readable-stream@1.1.14, readable-stream@^1.0.27-1: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -4813,7 +4877,7 @@ remove-trailing-slash@^0.1.1: resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== -request@^2.72.0, request@^2.74.0, request@^2.88.0: +request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -4844,6 +4908,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -4894,7 +4963,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +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" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -4914,13 +4983,6 @@ 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== -saxes@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" - integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== - dependencies: - xmlchars "^2.2.0" - semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -4972,7 +5034,16 @@ shimmer@^1.2.0: resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -4987,24 +5058,19 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -source-map-support@^0.5.6: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== +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" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +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" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - spark-md5@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.1.tgz#83a0e255734f2ab4e5c466e5a2cfc9ba2aa2124d" @@ -5077,11 +5143,6 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-template@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" - integrity sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg== - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -5117,6 +5178,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" @@ -5181,11 +5247,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -symbol-tree@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" - integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== - tar-fs@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -5236,11 +5297,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -throat@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" - integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== - through2@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" @@ -5303,7 +5359,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.0.0: +"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== @@ -5312,6 +5368,16 @@ touch@^3.1.0: punycode "^2.1.1" universalify "^0.1.2" +tough-cookie@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -5320,31 +5386,52 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tr46@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" - integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== - dependencies: - punycode "^2.1.1" - tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -ts-jest@27.1.5: - version "27.1.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.5.tgz#0ddf1b163fbaae3d5b7504a1e65c914a95cff297" - integrity sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA== +ts-jest@28.0.4: + version "28.0.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-28.0.4.tgz#0ab705a60fc4b9f3506f35e26edfa9e9c915c31b" + integrity sha512-S6uRDDdCJBvnZqyGjB4VCnwbQrbgdL8WPeP4jevVSpYsBaeGRQAIS08o3Svav2Ex+oXwLgJ/m7F24TNq62kA1A== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" - jest-util "^27.0.0" - json5 "2.x" + jest-util "^28.0.0" + json5 "^2.2.1" lodash.memoize "4.x" make-error "1.x" semver "7.x" - yargs-parser "20.x" + yargs-parser "^20.x" + +ts-node@10.8.1: + version "10.8.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.1.tgz#ea2bd3459011b52699d7e88daa55a45a1af4f066" + integrity sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tsconfig-paths@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.0.0.tgz#1082f5d99fd127b72397eef4809e4dd06d229b64" + integrity sha512-SLBg2GBKlR6bVtMgJJlud/o3waplKtL7skmLkExomIiaAtLGtVsoXIqP3SYdjbcH9lq/KVv7pMZeCBpLYOit6Q== + dependencies: + json5 "^2.2.1" + minimist "^1.2.6" + strip-bom "^3.0.0" tsscmp@1.0.6: version "1.0.6" @@ -5363,13 +5450,6 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -5427,6 +5507,19 @@ universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + update-notifier@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" @@ -5461,6 +5554,14 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" @@ -5499,14 +5600,19 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v8-to-istanbul@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" - integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" + integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== dependencies: + "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" - source-map "^0.7.3" vary@^1.1.2: version "1.1.2" @@ -5527,21 +5633,7 @@ vuvuzela@1.0.3: resolved "https://registry.yarnpkg.com/vuvuzela/-/vuvuzela-1.0.3.tgz#3be145e58271c73ca55279dd851f12a682114b0b" integrity sha1-O+FF5YJxxzylUnndhR8SpoIRSws= -w3c-hr-time@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== - dependencies: - browser-process-hrtime "^1.0.0" - -w3c-xmlserializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" - integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== - dependencies: - xml-name-validator "^3.0.0" - -walker@^1.0.7: +walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== @@ -5561,28 +5653,6 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= -webidl-conversions@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" - integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== - -webidl-conversions@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" - integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== - -whatwg-encoding@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" - -whatwg-mimetype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -5591,15 +5661,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -whatwg-url@^8.0.0, whatwg-url@^8.5.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" - integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== - dependencies: - lodash "^4.7.0" - tr46 "^2.1.0" - webidl-conversions "^6.1.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -5621,11 +5682,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -word-wrap@~1.2.3: - version "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== - wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -5650,6 +5706,14 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +write-file-atomic@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + write-stream@~0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/write-stream/-/write-stream-0.4.3.tgz#83cc8c0347d0af6057a93862b4e3ae01de5c81c1" @@ -5657,21 +5721,11 @@ write-stream@~0.4.3: dependencies: readable-stream "~0.0.2" -ws@^7.4.6: - version "7.5.8" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a" - integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== - xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== - xml2js@0.1.x: version "0.1.14" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c" @@ -5692,11 +5746,6 @@ xmlbuilder@~9.0.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= -xmlchars@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" - integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== - xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -5712,29 +5761,44 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@20.x, yargs-parser@^20.2.2: +yargs-parser@^20.x: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== dependencies: - cliui "^7.0.2" + cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" - string-width "^4.2.0" + string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^20.2.2" + yargs-parser "^21.1.1" ylru@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785" integrity sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA== +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zlib@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index ab33a95048..18fc6dffcb 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.0.34-alpha.5", + "version": "2.1.46-alpha.6", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -37,53 +37,56 @@ "dist" ], "dependencies": { - "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "2.0.34-alpha.5", - "@spectrum-css/actionbutton": "^1.0.1", - "@spectrum-css/actiongroup": "^1.0.1", - "@spectrum-css/avatar": "^3.0.2", - "@spectrum-css/button": "^3.0.1", - "@spectrum-css/buttongroup": "^3.0.2", - "@spectrum-css/checkbox": "^3.0.2", - "@spectrum-css/dialog": "^3.0.1", - "@spectrum-css/divider": "^1.0.3", - "@spectrum-css/dropzone": "^3.0.2", - "@spectrum-css/fieldgroup": "^3.0.2", - "@spectrum-css/fieldlabel": "^3.0.1", - "@spectrum-css/icon": "^3.0.1", - "@spectrum-css/illustratedmessage": "^3.0.2", - "@spectrum-css/inlinealert": "^2.0.1", - "@spectrum-css/inputgroup": "^3.0.2", - "@spectrum-css/label": "^2.0.10", - "@spectrum-css/link": "^3.1.1", - "@spectrum-css/menu": "^3.0.1", - "@spectrum-css/modal": "^3.0.1", - "@spectrum-css/pagination": "^3.0.3", - "@spectrum-css/picker": "^1.0.1", - "@spectrum-css/popover": "^3.0.1", - "@spectrum-css/progressbar": "^1.0.2", - "@spectrum-css/progresscircle": "^1.0.2", - "@spectrum-css/radio": "^3.0.2", - "@spectrum-css/search": "^3.0.2", - "@spectrum-css/sidenav": "^3.0.2", + "@adobe/spectrum-css-workflow-icons": "1.2.1", + "@budibase/string-templates": "2.1.46-alpha.6", + "@spectrum-css/actionbutton": "1.0.1", + "@spectrum-css/actiongroup": "1.0.1", + "@spectrum-css/avatar": "3.0.2", + "@spectrum-css/button": "3.0.1", + "@spectrum-css/buttongroup": "3.0.2", + "@spectrum-css/checkbox": "3.0.2", + "@spectrum-css/dialog": "3.0.1", + "@spectrum-css/divider": "1.0.3", + "@spectrum-css/dropzone": "3.0.2", + "@spectrum-css/fieldgroup": "3.0.2", + "@spectrum-css/fieldlabel": "3.0.1", + "@spectrum-css/icon": "3.0.1", + "@spectrum-css/illustratedmessage": "3.0.2", + "@spectrum-css/inlinealert": "2.0.1", + "@spectrum-css/inputgroup": "3.0.2", + "@spectrum-css/label": "2.0.10", + "@spectrum-css/link": "3.1.1", + "@spectrum-css/menu": "3.0.1", + "@spectrum-css/modal": "3.0.1", + "@spectrum-css/pagination": "3.0.3", + "@spectrum-css/picker": "1.0.1", + "@spectrum-css/popover": "3.0.1", + "@spectrum-css/progressbar": "1.0.2", + "@spectrum-css/progresscircle": "1.0.2", + "@spectrum-css/radio": "3.0.2", + "@spectrum-css/search": "3.0.2", + "@spectrum-css/sidenav": "3.0.2", "@spectrum-css/slider": "3.0.1", - "@spectrum-css/statuslight": "^3.0.2", - "@spectrum-css/stepper": "^3.0.3", - "@spectrum-css/switch": "^1.0.2", - "@spectrum-css/table": "^3.0.1", - "@spectrum-css/tabs": "^3.2.12", - "@spectrum-css/tags": "^3.0.2", - "@spectrum-css/textfield": "^3.0.1", - "@spectrum-css/toast": "^3.0.1", - "@spectrum-css/tooltip": "^3.0.3", - "@spectrum-css/treeview": "^3.0.2", - "@spectrum-css/typography": "^3.0.1", - "@spectrum-css/underlay": "^2.0.9", - "@spectrum-css/vars": "^3.0.1", + "@spectrum-css/statuslight": "3.0.2", + "@spectrum-css/stepper": "3.0.3", + "@spectrum-css/switch": "1.0.2", + "@spectrum-css/table": "3.0.1", + "@spectrum-css/tabs": "3.2.12", + "@spectrum-css/tags": "3.0.2", + "@spectrum-css/textfield": "3.0.1", + "@spectrum-css/toast": "3.0.1", + "@spectrum-css/tooltip": "3.0.3", + "@spectrum-css/treeview": "3.0.2", + "@spectrum-css/typography": "3.0.1", + "@spectrum-css/underlay": "2.0.9", + "@spectrum-css/vars": "3.0.1", "dayjs": "^1.10.4", "easymde": "^2.16.1", "svelte-flatpickr": "^3.2.3", "svelte-portal": "^1.0.0" }, + "resolutions": { + "loader-utils": "1.4.1" + }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" } diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index 7fd2879071..3a08484635 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -1,18 +1,53 @@ -export default function clickOutside(element, callbackFunction) { - function onClick(event) { - if (!element.contains(event.target)) { - callbackFunction(event) +const ignoredClasses = [".flatpickr-calendar", ".modal-container"] +let clickHandlers = [] + +/** + * Handle a body click event + */ +const handleClick = event => { + // Ignore click if needed + for (let className of ignoredClasses) { + if (event.target.closest(className)) { + return } } - document.body.addEventListener("click", onClick, true) + // Process handlers + clickHandlers.forEach(handler => { + if (!handler.element.contains(event.target)) { + handler.callback?.(event) + } + }) +} +document.documentElement.addEventListener("click", handleClick, true) - return { - update(newCallbackFunction) { - callbackFunction = newCallbackFunction - }, - destroy() { - document.body.removeEventListener("click", onClick, true) - }, +/** + * Adds or updates a click handler + */ +const updateHandler = (id, element, callback) => { + let existingHandler = clickHandlers.find(x => x.id === id) + if (!existingHandler) { + clickHandlers.push({ id, element, callback }) + } else { + existingHandler.callback = callback + } +} + +/** + * Removes a click handler + */ +const removeHandler = id => { + clickHandlers = clickHandlers.filter(x => x.id !== id) +} + +/** + * Svelte action to apply a click outside handler for a certain element + */ +export default (element, callback) => { + const id = Math.random() + updateHandler(id, element, callback) + return { + update: newCallback => updateHandler(id, element, newCallback), + destroy: () => removeHandler(id), } } diff --git a/packages/bbui/src/ColorPicker/ColorPicker.svelte b/packages/bbui/src/ColorPicker/ColorPicker.svelte index 331de38371..9a70134fb6 100644 --- a/packages/bbui/src/ColorPicker/ColorPicker.svelte +++ b/packages/bbui/src/ColorPicker/ColorPicker.svelte @@ -139,7 +139,7 @@ {#if open}
diff --git a/packages/bbui/src/DetailSummary/DetailSummary.svelte b/packages/bbui/src/DetailSummary/DetailSummary.svelte index 518c615504..f7e2611792 100644 --- a/packages/bbui/src/DetailSummary/DetailSummary.svelte +++ b/packages/bbui/src/DetailSummary/DetailSummary.svelte @@ -19,13 +19,19 @@
-
-
{name}
- {#if collapsible} - - {/if} -
-
+ {#if name} +
+
{name}
+ {#if collapsible} + + {/if} +
+ {/if} +
@@ -72,6 +78,9 @@ padding: var(--spacing-s) var(--spacing-xl) var(--spacing-xl) var(--spacing-xl); } + .property-panel.no-title { + padding: var(--spacing-xl); + } .show { display: flex; diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 9e7d44dbc3..6996525a76 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -23,6 +23,15 @@ let open = false let flatpickr, flatpickrOptions + // Another classic flatpickr issue. Errors were randomly being thrown due to + // flatpickr internal code. Making sure that "destroy" is a valid function + // fixes it. The sooner we remove flatpickr the better. + $: { + if (flatpickr && !flatpickr.destroy) { + flatpickr.destroy = () => {} + } + } + const resolveTimeStamp = timestamp => { let maskedDate = new Date(`0-${timestamp}`) @@ -41,7 +50,7 @@ time_24hr: time24hr || false, altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y", wrap: true, - mode: range ? "range" : null, + mode: range ? "range" : "single", appendTo, disableMobile: "true", onReady: () => { @@ -197,6 +206,7 @@ {/if} idx === selectedImageIdx).map(item => item.key) ) + fileInput.value = "" } selectedImageIdx = 0 } @@ -234,6 +236,7 @@ type="file" multiple accept={extensions} + bind:this={fileInput} on:change={handleFile} /> selectedLookupMap[optionValue] === true $: toggleOption = makeToggleOption(selectedLookupMap, value) diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 16d13ef2cc..97bd1394b4 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -205,7 +205,10 @@ width: 100%; } .spectrum-Popover.auto-width :global(.spectrum-Menu-itemLabel) { + max-width: 400px; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .spectrum-Picker { width: 100%; diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte index 604e446099..b0a884738f 100644 --- a/packages/bbui/src/Form/Core/PickerDropdown.svelte +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -61,6 +61,7 @@ const onPickPrimary = newValue => { dispatch("pickprimary", newValue) primaryOpen = false + dispatch("closed") } const onClearPrimary = () => { @@ -92,6 +93,7 @@ if (primaryOpen) { event.stopPropagation() primaryOpen = false + dispatch("closed") } } diff --git a/packages/bbui/src/Form/PickerDropdown.svelte b/packages/bbui/src/Form/PickerDropdown.svelte index b64a4bd117..58f68a6386 100644 --- a/packages/bbui/src/Form/PickerDropdown.svelte +++ b/packages/bbui/src/Form/PickerDropdown.svelte @@ -128,5 +128,6 @@ on:blur on:focus on:keyup + on:closed /> diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index f2cae14f0b..8290acd7cc 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -64,7 +64,7 @@ transition: color var(--spectrum-global-animation-duration-100, 130ms); } svg.hoverable:hover { - color: var(--spectrum-alias-icon-color-selected-hover); + color: var(--spectrum-alias-icon-color-selected-hover) !important; cursor: pointer; } diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index 7d3a8d7c40..eb73e255d5 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -28,9 +28,9 @@ let loading = false $: confirmDisabled = disabled || loading - async function secondary() { + async function secondary(e) { loading = true - if (!secondaryAction || (await secondaryAction()) !== false) { + if (!secondaryAction || (await secondaryAction(e)) !== false) { hide() } loading = false diff --git a/packages/bbui/src/Notification/Notification.svelte b/packages/bbui/src/Notification/Notification.svelte index 53ab062701..eb2922a5de 100644 --- a/packages/bbui/src/Notification/Notification.svelte +++ b/packages/bbui/src/Notification/Notification.svelte @@ -25,7 +25,7 @@ {/if}
-
{message || ""}
+
{message || ""}
{#if action}
{actionMessage}
@@ -53,6 +53,10 @@
diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 7745c3c407..cec270486a 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -71,7 +71,8 @@ visibleRowCount, rowCount, totalRowCount, - rowHeight + rowHeight, + loading ) $: sortedRows = sortRows(rows, sortColumn, sortOrder) $: gridStyle = getGridStyle(fields, schema, showEditColumn) @@ -120,8 +121,12 @@ visibleRowCount, rowCount, totalRowCount, - rowHeight + rowHeight, + loading ) => { + if (loading) { + return `height: ${headerHeight + visibleRowCount * rowHeight}px;` + } if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) { return "" } @@ -270,155 +275,159 @@ } -
- {#if !loaded} -
- -
- {:else} -
- {#if fields.length} -
- {#if showEditColumn} -
- {#if allowSelectRows} - - {:else} - Edit - {/if} -
- {/if} - {#each fields as field} -
sortBy(schema[field])} - > -
{getDisplayName(schema[field])}
- {#if schema[field]?.autocolumn} - - - - {/if} - {#if sortColumn === field} - - {/if} - {#if allowEditColumns && schema[field]?.editable !== false} - editColumn(e, field)} - > - - - {/if} -
- {/each} -
- {/if} - {#if sortedRows?.length} - {#each sortedRows as row, idx} -
+{#key fields?.length} +
+ {#if loading} +
+ + + +
+ {:else} +
+ {#if fields.length} +
{#if showEditColumn}
{ - toggleSelectRow(row) - e.stopPropagation() - }} + class:noBorderHeader={!showHeaderBorder} + class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit" > - selectedRow._id === row._id - ) !== -1} - onEdit={e => editRow(e, row)} - {allowSelectRows} - {allowEditRows} - /> + {#if allowSelectRows} + + {:else} + Edit + {/if}
{/if} {#each fields as field}
{ - if (!schema[field]?.preventSelectRow) { - dispatch("click", row) - toggleSelectRow(row) - } - }} + class="spectrum-Table-headCell" + class:noBorderHeader={!showHeaderBorder} + class:spectrum-Table-headCell--alignCenter={schema[field] + .align === "Center"} + class:spectrum-Table-headCell--alignRight={schema[field] + .align === "Right"} + class:is-sortable={schema[field].sortable !== false} + class:is-sorted-desc={sortColumn === field && + sortOrder === "Descending"} + class:is-sorted-asc={sortColumn === field && + sortOrder === "Ascending"} + on:click={() => sortBy(schema[field])} > - - - +
{getDisplayName(schema[field])}
+ {#if schema[field]?.autocolumn} + + + + {/if} + {#if sortColumn === field} + + {/if} + {#if allowEditColumns && schema[field]?.editable !== false} + editColumn(e, field)} + > + + + {/if}
{/each}
- {/each} - {:else} -
- {#if customPlaceholder} - - {:else} -
- - - -
{placeholderText}
+ {/if} + {#if sortedRows?.length} + {#each sortedRows as row, idx} +
+ {#if showEditColumn} +
{ + toggleSelectRow(row) + e.stopPropagation() + }} + > + selectedRow._id === row._id + ) !== -1} + onEdit={e => editRow(e, row)} + {allowSelectRows} + {allowEditRows} + /> +
+ {/if} + {#each fields as field} +
{ + if (!schema[field]?.preventSelectRow) { + dispatch("click", row) + toggleSelectRow(row) + } + }} + > + + + +
+ {/each}
- {/if} -
- {/if} -
- {/if} -
+ {/each} + {:else} +
+ {#if customPlaceholder} + + {:else} +
+ + + +
{placeholderText}
+
+ {/if} +
+ {/if} +
+ {/if} +
+{/key} diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte index eb148534f3..3549bc6de8 100644 --- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte @@ -76,7 +76,7 @@ (nameTouched = true)} + on:input={() => (nameTouched = true)} bind:error={nameError} label="Name" /> @@ -124,11 +124,14 @@ padding: var(--spectrum-alias-item-padding-s); background: var(--spectrum-alias-background-color-secondary); transition: 0.3s all; - border: solid var(--spectrum-alias-border-color); border-radius: 5px; box-sizing: border-box; border-width: 2px; } + + .item:hover { + background: var(--spectrum-alias-background-color-tertiary); + } .selected { background: var(--spectrum-alias-background-color-tertiary); } diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index bb70938dfe..d09faa34c9 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -38,13 +38,15 @@ export let testData export let schemaProperties export let isTestModal = false + let webhookModal let drawer - let tempFilters = lookForFilters(schemaProperties) || [] let fillWidth = true let codeBindingOpen = false let inputData + $: filters = lookForFilters(schemaProperties) || [] + $: tempFilters = filters $: stepId = block.stepId $: bindings = getAvailableBindings( block || $automationStore.selectedBlock, @@ -120,7 +122,7 @@ allSteps[idx]?.stepId === ActionStepID.LOOP && allSteps.find(x => x.blockToLoop === block.id) - // If the previous block was a loop block, decerement the index so the following + // If the previous block was a loop block, decrement the index so the following // steps are in the correct order if (wasLoopBlock) { loopBlockCount++ @@ -222,16 +224,17 @@ {:else if value.customType === "filters"} Define filters - + (tempFilters = e.detail)} /> {:else if value.customType === "password"} diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte index c72acc252a..e5a5b2af8c 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte @@ -14,6 +14,12 @@ export let block export let isTestModal + $: parsedBindings = bindings.map(binding => { + let clone = Object.assign({}, binding) + clone.icon = "ShareAndroid" + return clone + }) + let table let schemaFields @@ -79,6 +85,10 @@ return [value] } + if (type === "json") { + return value.value + } + return value } @@ -109,7 +119,7 @@ {isTestModal} {field} {schema} - {bindings} + bindings={parsedBindings} {value} {onChange} /> @@ -124,7 +134,7 @@ on:change={e => onChange(e, field, schema.type)} label={field} type="string" - {bindings} + bindings={parsedBindings} fillWidth={true} allowJS={true} updateOnChange={false} diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte index f3abad6e25..0e5e13ce1e 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte @@ -5,11 +5,13 @@ DatePicker, Multiselect, TextArea, + Label, } from "@budibase/bbui" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" + import Editor from "components/integration/QueryEditor.svelte" export let onChange export let field @@ -18,6 +20,12 @@ export let bindings export let isTestModal + $: parsedBindings = bindings.map(binding => { + let clone = Object.assign({}, binding) + clone.icon = "ShareAndroid" + return clone + }) + function schemaHasOptions(schema) { return !!schema.constraints?.inclusion?.length } @@ -50,6 +58,20 @@ /> {:else if schema.type === "longform"}