Merge branch 'master' of github.com:Budibase/budibase into fix/internal-db-enrich-perf
This commit is contained in:
commit
13dadbcc7d
|
@ -1,9 +1,14 @@
|
||||||
packages/server/node_modules
|
*
|
||||||
packages/builder
|
!/packages/
|
||||||
packages/frontend-core
|
!/scripts/
|
||||||
packages/backend-core
|
/packages/*/node_modules
|
||||||
packages/worker/node_modules
|
packages/server/scripts/
|
||||||
packages/cli
|
!packages/server/scripts/integrations/oracle
|
||||||
packages/client
|
!nx.json
|
||||||
packages/bbui
|
!/hosting/single/
|
||||||
packages/string-templates
|
!/hosting/letsencrypt /
|
||||||
|
|
||||||
|
!package.json
|
||||||
|
!yarn.lock
|
||||||
|
!lerna.json
|
||||||
|
!.yarnrc
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
||||||
PAYLOAD_PR_NUMBER: ${{ github.ref }}
|
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
with:
|
with:
|
||||||
repository: budibase/budibase-deploys
|
repository: budibase/budibase-deploys
|
||||||
event: featurebranch-qa-close
|
event: featurebranch-qa-close
|
||||||
|
|
|
@ -14,7 +14,6 @@ env:
|
||||||
# Posthog token used by ui at build time
|
# Posthog token used by ui at build time
|
||||||
POSTHOG_TOKEN: phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
POSTHOG_TOKEN: phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -110,7 +109,6 @@ jobs:
|
||||||
git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
|
git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
|
||||||
git push
|
git push
|
||||||
|
|
||||||
|
|
||||||
trigger-deploy-to-qa-env:
|
trigger-deploy-to-qa-env:
|
||||||
needs: [release-helm-chart]
|
needs: [release-helm-chart]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
REGISTRY_URL: registry.hub.docker.com
|
||||||
|
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: "build"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18.x]
|
||||||
|
steps:
|
||||||
|
- name: "Checkout"
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: "yarn"
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Run Yarn
|
||||||
|
run: yarn
|
||||||
|
- name: Run Yarn Build
|
||||||
|
run: yarn build --scope @budibase/server --scope @budibase/worker
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
- name: Get the latest release version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
|
echo $release_version
|
||||||
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
- name: Tag and release Budibase service docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: budibase/budibase-test:test
|
||||||
|
file: ./hosting/single/Dockerfile.v2
|
||||||
|
cache-from: type=registry,ref=budibase/budibase-test:test
|
||||||
|
cache-to: type=inline
|
||||||
|
- name: Tag and release Budibase Azure App Service docker image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
build-args: TARGETBUILD=aas
|
||||||
|
tags: budibase/budibase-test:aas
|
||||||
|
file: ./hosting/single/Dockerfile.v2
|
|
@ -20,8 +20,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
root-reserve-mb: 30000
|
root-reserve-mb: 30000
|
||||||
swap-size-mb: 1024
|
swap-size-mb: 1024
|
||||||
remove-android: 'true'
|
remove-android: "true"
|
||||||
remove-dotnet: 'true'
|
remove-dotnet: "true"
|
||||||
- name: Fail if not a tag
|
- name: Fail if not a tag
|
||||||
run: |
|
run: |
|
||||||
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
|
@ -48,7 +48,7 @@ jobs:
|
||||||
- name: Update versions
|
- name: Update versions
|
||||||
run: ./scripts/updateVersions.sh
|
run: ./scripts/updateVersions.sh
|
||||||
- name: Run Yarn Build
|
- name: Run Yarn Build
|
||||||
run: yarn build:docker:pre
|
run: yarn build
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -134,8 +134,6 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
- name: SELF_HOSTED
|
- name: SELF_HOSTED
|
||||||
value: {{ .Values.globals.selfHosted | quote }}
|
value: {{ .Values.globals.selfHosted | quote }}
|
||||||
- name: SENTRY_DSN
|
|
||||||
value: {{ .Values.globals.sentryDSN | quote }}
|
|
||||||
- name: POSTHOG_TOKEN
|
- name: POSTHOG_TOKEN
|
||||||
value: {{ .Values.globals.posthogToken | quote }}
|
value: {{ .Values.globals.posthogToken | quote }}
|
||||||
- name: WORKER_URL
|
- name: WORKER_URL
|
||||||
|
|
|
@ -130,8 +130,6 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
- name: SELF_HOSTED
|
- name: SELF_HOSTED
|
||||||
value: {{ .Values.globals.selfHosted | quote }}
|
value: {{ .Values.globals.selfHosted | quote }}
|
||||||
- name: SENTRY_DSN
|
|
||||||
value: {{ .Values.globals.sentryDSN }}
|
|
||||||
- name: ENABLE_ANALYTICS
|
- name: ENABLE_ANALYTICS
|
||||||
value: {{ .Values.globals.enableAnalytics | quote }}
|
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||||
- name: POSTHOG_TOKEN
|
- name: POSTHOG_TOKEN
|
||||||
|
|
|
@ -78,7 +78,6 @@ globals:
|
||||||
budibaseEnv: PRODUCTION
|
budibaseEnv: PRODUCTION
|
||||||
tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
||||||
enableAnalytics: "1"
|
enableAnalytics: "1"
|
||||||
sentryDSN: ""
|
|
||||||
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
|
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
|
||||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||||
|
|
|
@ -138,6 +138,8 @@ To develop the Budibase platform you'll need [Docker](https://www.docker.com/) a
|
||||||
|
|
||||||
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
|
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
|
||||||
|
|
||||||
|
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above command.
|
||||||
|
|
||||||
##### Manual method
|
##### Manual method
|
||||||
|
|
||||||
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
|
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
|
||||||
|
@ -146,6 +148,8 @@ The following commands can be executed to manually get Budibase up and running (
|
||||||
|
|
||||||
`yarn build` will build all budibase packages.
|
`yarn build` will build all budibase packages.
|
||||||
|
|
||||||
|
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above commands.
|
||||||
|
|
||||||
#### 4. Running
|
#### 4. Running
|
||||||
|
|
||||||
To run the budibase server and builder in dev mode (i.e. with live reloading):
|
To run the budibase server and builder in dev mode (i.e. with live reloading):
|
||||||
|
|
|
@ -3,3 +3,6 @@
|
||||||
[couchdb]
|
[couchdb]
|
||||||
database_dir = DATA_DIR/couch/dbs
|
database_dir = DATA_DIR/couch/dbs
|
||||||
view_index_dir = DATA_DIR/couch/views
|
view_index_dir = DATA_DIR/couch/views
|
||||||
|
|
||||||
|
[chttpd_auth]
|
||||||
|
timeout = 7200 ; 2 hours in seconds
|
||||||
|
|
|
@ -19,7 +19,6 @@ services:
|
||||||
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
|
||||||
ENABLE_ANALYTICS: "true"
|
ENABLE_ANALYTICS: "true"
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
@ -48,7 +47,6 @@ services:
|
||||||
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
|
||||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
|
|
@ -20,7 +20,6 @@ services:
|
||||||
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
|
||||||
ENABLE_ANALYTICS: "true"
|
ENABLE_ANALYTICS: "true"
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
@ -31,8 +30,8 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- worker-service
|
- worker-service
|
||||||
- redis-service
|
- redis-service
|
||||||
# volumes:
|
# volumes:
|
||||||
# - /some/path/to/plugins:/plugins
|
# - /some/path/to/plugins:/plugins
|
||||||
|
|
||||||
worker-service:
|
worker-service:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -51,7 +50,6 @@ services:
|
||||||
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
|
||||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
@ -113,7 +111,12 @@ services:
|
||||||
PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984"
|
PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984"
|
||||||
depends_on:
|
depends_on:
|
||||||
- couchdb-service
|
- couchdb-service
|
||||||
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
|
command:
|
||||||
|
[
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;",
|
||||||
|
]
|
||||||
|
|
||||||
redis-service:
|
redis-service:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
@ -12,14 +12,14 @@ RUN chmod +x /cleanup.sh
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD packages/server .
|
ADD packages/server .
|
||||||
COPY yarn.lock .
|
COPY yarn.lock .
|
||||||
RUN yarn install --production=true --network-timeout 100000
|
RUN yarn install --production=true --network-timeout 1000000
|
||||||
RUN /cleanup.sh
|
RUN /cleanup.sh
|
||||||
|
|
||||||
# build worker
|
# build worker
|
||||||
WORKDIR /worker
|
WORKDIR /worker
|
||||||
ADD packages/worker .
|
ADD packages/worker .
|
||||||
COPY yarn.lock .
|
COPY yarn.lock .
|
||||||
RUN yarn install --production=true --network-timeout 100000
|
RUN yarn install --production=true --network-timeout 1000000
|
||||||
RUN /cleanup.sh
|
RUN /cleanup.sh
|
||||||
|
|
||||||
FROM budibase/couchdb
|
FROM budibase/couchdb
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
FROM node:18-slim as build
|
||||||
|
|
||||||
|
# install node-gyp dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq
|
||||||
|
|
||||||
|
|
||||||
|
# copy and install dependencies
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json .
|
||||||
|
COPY yarn.lock .
|
||||||
|
COPY lerna.json .
|
||||||
|
COPY .yarnrc .
|
||||||
|
|
||||||
|
COPY packages/server/package.json packages/server/package.json
|
||||||
|
COPY packages/worker/package.json packages/worker/package.json
|
||||||
|
# string-templates does not get bundled during the esbuild process, so we want to use the local version
|
||||||
|
COPY packages/string-templates/package.json packages/string-templates/package.json
|
||||||
|
|
||||||
|
|
||||||
|
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
||||||
|
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||||
|
RUN ./scripts/removeWorkspaceDependencies.sh
|
||||||
|
|
||||||
|
|
||||||
|
# We will never want to sync pro, but the script is still required
|
||||||
|
RUN echo '' > scripts/syncProPackage.js
|
||||||
|
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
|
||||||
|
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
|
||||||
|
|
||||||
|
# copy the actual code
|
||||||
|
COPY packages/server/dist packages/server/dist
|
||||||
|
COPY packages/server/pm2.config.js packages/server/pm2.config.js
|
||||||
|
COPY packages/server/client packages/server/client
|
||||||
|
COPY packages/server/builder packages/server/builder
|
||||||
|
COPY packages/worker/dist packages/worker/dist
|
||||||
|
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
|
||||||
|
COPY packages/string-templates packages/string-templates
|
||||||
|
|
||||||
|
|
||||||
|
FROM budibase/couchdb as runner
|
||||||
|
ARG TARGETARCH
|
||||||
|
ENV TARGETARCH $TARGETARCH
|
||||||
|
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
||||||
|
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
||||||
|
ARG TARGETBUILD=single
|
||||||
|
ENV TARGETBUILD $TARGETBUILD
|
||||||
|
|
||||||
|
# install base dependencies
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
|
||||||
|
|
||||||
|
# Install postgres client for pg_dump utils
|
||||||
|
RUN apt install software-properties-common apt-transport-https gpg -y \
|
||||||
|
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
|
||||||
|
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
|
||||||
|
&& apt update -y \
|
||||||
|
&& apt install postgresql-client-15 -y \
|
||||||
|
&& apt remove software-properties-common apt-transport-https gpg -y
|
||||||
|
|
||||||
|
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||||
|
WORKDIR /nodejs
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_18.x -o /tmp/nodesource_setup.sh && \
|
||||||
|
bash /tmp/nodesource_setup.sh && \
|
||||||
|
apt-get install -y --no-install-recommends libaio1 nodejs && \
|
||||||
|
npm install --global yarn pm2
|
||||||
|
|
||||||
|
# setup nginx
|
||||||
|
COPY hosting/single/nginx/nginx.conf /etc/nginx
|
||||||
|
COPY 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 && \
|
||||||
|
usermod -a -G tty www-data
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
RUN mkdir -p scripts/integrations/oracle
|
||||||
|
COPY packages/server/scripts/integrations/oracle scripts/integrations/oracle
|
||||||
|
RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh
|
||||||
|
|
||||||
|
# setup minio
|
||||||
|
WORKDIR /minio
|
||||||
|
COPY scripts/install-minio.sh ./install.sh
|
||||||
|
RUN chmod +x install.sh && ./install.sh
|
||||||
|
|
||||||
|
# setup runner file
|
||||||
|
WORKDIR /
|
||||||
|
COPY hosting/single/runner.sh .
|
||||||
|
RUN chmod +x ./runner.sh
|
||||||
|
COPY hosting/single/healthcheck.sh .
|
||||||
|
RUN chmod +x ./healthcheck.sh
|
||||||
|
|
||||||
|
# Script below sets the path for storing data based on $DATA_DIR
|
||||||
|
# For Azure App Service install SSH & point data locations to /home
|
||||||
|
COPY hosting/single/ssh/sshd_config /etc/
|
||||||
|
COPY hosting/single/ssh/ssh_setup.sh /tmp
|
||||||
|
RUN /build-target-paths.sh
|
||||||
|
|
||||||
|
|
||||||
|
# setup letsencrypt certificate
|
||||||
|
RUN apt-get install -y certbot python3-certbot-nginx
|
||||||
|
COPY hosting/letsencrypt /app/letsencrypt
|
||||||
|
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh
|
||||||
|
|
||||||
|
COPY --from=build /app/node_modules /node_modules
|
||||||
|
COPY --from=build /app/package.json /package.json
|
||||||
|
COPY --from=build /app/packages/server /app
|
||||||
|
COPY --from=build /app/packages/worker /worker
|
||||||
|
COPY --from=build /app/packages/string-templates /string-templates
|
||||||
|
|
||||||
|
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
|
||||||
|
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 443
|
||||||
|
# Expose port 2222 for SSH on Azure App Service build
|
||||||
|
EXPOSE 2222
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh"
|
||||||
|
|
||||||
|
# must set this just before running
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
CMD ["./runner.sh"]
|
|
@ -7,16 +7,16 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
|
||||||
[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION
|
[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80
|
[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80
|
||||||
[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker
|
[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker
|
||||||
[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000
|
[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://127.0.0.1:9000
|
||||||
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
|
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
|
||||||
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
||||||
[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app
|
[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||||
[[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379
|
[[ -z "${REDIS_URL}" ]] && export REDIS_URL=127.0.0.1:6379
|
||||||
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
|
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
|
||||||
[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
|
[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
|
||||||
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
|
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://127.0.0.1:4002
|
||||||
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001
|
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://127.0.0.1:4001
|
||||||
[[ -z "${SERVER_TOP_LEVEL_PATH}" ]] && export SERVER_TOP_LEVEL_PATH=/app
|
[[ -z "${SERVER_TOP_LEVEL_PATH}" ]] && export SERVER_TOP_LEVEL_PATH=/app
|
||||||
# export CUSTOM_DOMAIN=budi001.custom.com
|
# export CUSTOM_DOMAIN=budi001.custom.com
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [[ -z "${COUCH_DB_URL}" ]]; then
|
if [[ -z "${COUCH_DB_URL}" ]]; then
|
||||||
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@127.0.0.1:5984
|
||||||
fi
|
fi
|
||||||
if [ ! -f "${DATA_DIR}/.env" ]; then
|
if [ ! -f "${DATA_DIR}/.env" ]; then
|
||||||
touch ${DATA_DIR}/.env
|
touch ${DATA_DIR}/.env
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.11.34",
|
"version": "2.11.38",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
6
nx.json
6
nx.json
|
@ -8,5 +8,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"targetDefaults": {}
|
"targetDefaults": {
|
||||||
|
"build": {
|
||||||
|
"inputs": ["{workspaceRoot}/scripts/build.js"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
11
package.json
11
package.json
|
@ -3,14 +3,11 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||||
"@nx/js": "16.4.3",
|
|
||||||
"@rollup/plugin-json": "^4.0.2",
|
|
||||||
"@typescript-eslint/parser": "6.7.2",
|
"@typescript-eslint/parser": "6.7.2",
|
||||||
"esbuild": "^0.18.17",
|
"esbuild": "^0.18.17",
|
||||||
"esbuild-node-externals": "^1.8.0",
|
"esbuild-node-externals": "^1.8.0",
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.44.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "7.1.1",
|
"lerna": "7.1.1",
|
||||||
"madge": "^6.0.0",
|
"madge": "^6.0.0",
|
||||||
|
@ -19,8 +16,6 @@
|
||||||
"nx-cloud": "16.0.5",
|
"nx-cloud": "16.0.5",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
|
||||||
"svelte": "3.49.0",
|
"svelte": "3.49.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"@babel/core": "^7.22.5",
|
"@babel/core": "^7.22.5",
|
||||||
|
@ -51,7 +46,7 @@
|
||||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||||
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||||
"test": "lerna run --stream test --stream",
|
"test": "lerna run --stream test --stream",
|
||||||
"lint:eslint": "eslint packages qa-core --max-warnings=0",
|
"lint:eslint": "eslint packages qa-core --max-warnings=0",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
|
@ -61,7 +56,6 @@
|
||||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"build:specs": "lerna run --stream specs",
|
"build:specs": "lerna run --stream specs",
|
||||||
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||||
"build:docker:pre": "yarn build && lerna run --stream predocker",
|
|
||||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||||
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
||||||
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
|
@ -69,8 +63,7 @@
|
||||||
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
||||||
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
|
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
|
||||||
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
"build:docker:single": "./scripts/build-single-image.sh",
|
||||||
"build:docker:single": "yarn build && lerna run --concurrency 1 predocker && yarn build:docker:single:image",
|
|
||||||
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
|
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
|
||||||
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
|
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
|
||||||
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
|
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"@budibase/shared-core": "0.0.0",
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "3.0.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
|
|
|
@ -33,8 +33,8 @@ function isInvalid(metadata?: { state: string }) {
|
||||||
* Get the requested app metadata by id.
|
* Get the requested app metadata by id.
|
||||||
* Use redis cache to first read the app metadata.
|
* Use redis cache to first read the app metadata.
|
||||||
* If not present fallback to loading the app metadata directly and re-caching.
|
* If not present fallback to loading the app metadata directly and re-caching.
|
||||||
* @param {string} appId the id of the app to get metadata from.
|
* @param appId the id of the app to get metadata from.
|
||||||
* @returns {object} the app metadata.
|
* @returns the app metadata.
|
||||||
*/
|
*/
|
||||||
export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
||||||
const client = await getAppClient()
|
const client = await getAppClient()
|
||||||
|
@ -72,9 +72,9 @@ export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate/reset the cached metadata when a change occurs in the db.
|
* Invalidate/reset the cached metadata when a change occurs in the db.
|
||||||
* @param appId {string} the cache key to bust/update.
|
* @param appId the cache key to bust/update.
|
||||||
* @param newMetadata {object|undefined} optional - can simply provide the new metadata to update with.
|
* @param newMetadata optional - can simply provide the new metadata to update with.
|
||||||
* @return {Promise<void>} will respond with success when cache is updated.
|
* @return will respond with success when cache is updated.
|
||||||
*/
|
*/
|
||||||
export async function invalidateAppMetadata(appId: string, newMetadata?: any) {
|
export async function invalidateAppMetadata(appId: string, newMetadata?: any) {
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
|
|
|
@ -61,9 +61,9 @@ async function populateUsersFromDB(
|
||||||
* Get the requested user by id.
|
* Get the requested user by id.
|
||||||
* Use redis cache to first read the user.
|
* Use redis cache to first read the user.
|
||||||
* If not present fallback to loading the user directly and re-caching.
|
* If not present fallback to loading the user directly and re-caching.
|
||||||
* @param {*} userId the id of the user to get
|
* @param userId the id of the user to get
|
||||||
* @param {*} tenantId the tenant of the user to get
|
* @param tenantId the tenant of the user to get
|
||||||
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
* @param populateUser function to provide the user for re-caching. default to couch db
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function getUser(
|
export async function getUser(
|
||||||
|
@ -111,8 +111,8 @@ export async function getUser(
|
||||||
* Get the requested users by id.
|
* Get the requested users by id.
|
||||||
* Use redis cache to first read the users.
|
* Use redis cache to first read the users.
|
||||||
* If not present fallback to loading the users directly and re-caching.
|
* If not present fallback to loading the users directly and re-caching.
|
||||||
* @param {*} userIds the ids of the user to get
|
* @param userIds the ids of the user to get
|
||||||
* @param {*} tenantId the tenant of the users to get
|
* @param tenantId the tenant of the users to get
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function getUsers(
|
export async function getUsers(
|
||||||
|
|
|
@ -119,8 +119,8 @@ export class Writethrough {
|
||||||
this.writeRateMs = writeRateMs
|
this.writeRateMs = writeRateMs
|
||||||
}
|
}
|
||||||
|
|
||||||
async put(doc: any) {
|
async put(doc: any, writeRateMs: number = this.writeRateMs) {
|
||||||
return put(this.db, doc, this.writeRateMs)
|
return put(this.db, doc, writeRateMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get(id: string) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import environment from "../environment"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new configuration ID.
|
* Generates a new configuration ID.
|
||||||
* @returns {string} The new configuration ID which the config doc can be stored under.
|
* @returns The new configuration ID which the config doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export function generateConfigID(type: ConfigType) {
|
export function generateConfigID(type: ConfigType) {
|
||||||
return `${DocumentType.CONFIG}${SEPARATOR}${type}`
|
return `${DocumentType.CONFIG}${SEPARATOR}${type}`
|
||||||
|
|
|
@ -62,7 +62,7 @@ export function isTenancyEnabled() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
* 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.
|
* @return The tenant ID found within the app ID.
|
||||||
*/
|
*/
|
||||||
export function getTenantIDFromAppID(appId: string) {
|
export function getTenantIDFromAppID(appId: string) {
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
|
|
|
@ -8,8 +8,8 @@ class Replication {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {String} source - the DB you want to replicate or rollback to
|
* @param source - the DB you want to replicate or rollback to
|
||||||
* @param {String} target - the DB you want to replicate to, or rollback from
|
* @param target - the DB you want to replicate to, or rollback from
|
||||||
*/
|
*/
|
||||||
constructor({ source, target }: any) {
|
constructor({ source, target }: any) {
|
||||||
this.source = getPouchDB(source)
|
this.source = getPouchDB(source)
|
||||||
|
@ -38,7 +38,7 @@ class Replication {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Two way replication operation, intended to be promise based.
|
* Two way replication operation, intended to be promise based.
|
||||||
* @param {Object} opts - PouchDB replication options
|
* @param opts - PouchDB replication options
|
||||||
*/
|
*/
|
||||||
sync(opts = {}) {
|
sync(opts = {}) {
|
||||||
this.replication = this.promisify(this.source.sync, opts)
|
this.replication = this.promisify(this.source.sync, opts)
|
||||||
|
@ -47,7 +47,7 @@ class Replication {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One way replication operation, intended to be promise based.
|
* One way replication operation, intended to be promise based.
|
||||||
* @param {Object} opts - PouchDB replication options
|
* @param opts - PouchDB replication options
|
||||||
*/
|
*/
|
||||||
replicate(opts = {}) {
|
replicate(opts = {}) {
|
||||||
this.replication = this.promisify(this.source.replicate.to, opts)
|
this.replication = this.promisify(this.source.replicate.to, opts)
|
||||||
|
|
|
@ -599,10 +599,10 @@ async function runQuery<T>(
|
||||||
* Gets round the fixed limit of 200 results from a query by fetching as many
|
* Gets round the fixed limit of 200 results from a query by fetching as many
|
||||||
* pages as required and concatenating the results. This recursively operates
|
* pages as required and concatenating the results. This recursively operates
|
||||||
* until enough results have been found.
|
* until enough results have been found.
|
||||||
* @param dbName {string} Which database to run a lucene query on
|
* @param dbName Which database to run a lucene query on
|
||||||
* @param index {string} Which search index to utilise
|
* @param index Which search index to utilise
|
||||||
* @param query {object} The JSON query structure
|
* @param query The JSON query structure
|
||||||
* @param params {object} The search params including:
|
* @param params The search params including:
|
||||||
* tableId {string} The table ID to search
|
* tableId {string} The table ID to search
|
||||||
* sort {string} The sort column
|
* sort {string} The sort column
|
||||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
@ -655,10 +655,10 @@ async function recursiveSearch<T>(
|
||||||
* Performs a paginated search. A bookmark will be returned to allow the next
|
* Performs a paginated search. A bookmark will be returned to allow the next
|
||||||
* page to be fetched. There is a max limit off 200 results per page in a
|
* page to be fetched. There is a max limit off 200 results per page in a
|
||||||
* paginated search.
|
* paginated search.
|
||||||
* @param dbName {string} Which database to run a lucene query on
|
* @param dbName Which database to run a lucene query on
|
||||||
* @param index {string} Which search index to utilise
|
* @param index Which search index to utilise
|
||||||
* @param query {object} The JSON query structure
|
* @param query The JSON query structure
|
||||||
* @param params {object} The search params including:
|
* @param params The search params including:
|
||||||
* tableId {string} The table ID to search
|
* tableId {string} The table ID to search
|
||||||
* sort {string} The sort column
|
* sort {string} The sort column
|
||||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
@ -722,10 +722,10 @@ export async function paginatedSearch<T>(
|
||||||
* desired amount of results. There is a limit of 1000 results to avoid
|
* desired amount of results. There is a limit of 1000 results to avoid
|
||||||
* heavy performance hits, and to avoid client components breaking from
|
* heavy performance hits, and to avoid client components breaking from
|
||||||
* handling too much data.
|
* handling too much data.
|
||||||
* @param dbName {string} Which database to run a lucene query on
|
* @param dbName Which database to run a lucene query on
|
||||||
* @param index {string} Which search index to utilise
|
* @param index Which search index to utilise
|
||||||
* @param query {object} The JSON query structure
|
* @param query The JSON query structure
|
||||||
* @param params {object} The search params including:
|
* @param params The search params including:
|
||||||
* tableId {string} The table ID to search
|
* tableId {string} The table ID to search
|
||||||
* sort {string} The sort column
|
* sort {string} The sort column
|
||||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
|
|
@ -45,7 +45,7 @@ export async function getAllDbs(opts = { efficient: false }) {
|
||||||
* Lots of different points in the system need to find the full list of apps, this will
|
* Lots of different points in the system need to find the full list of apps, this will
|
||||||
* enumerate the entire CouchDB cluster and get the list of databases (every app).
|
* enumerate the entire CouchDB cluster and get the list of databases (every app).
|
||||||
*
|
*
|
||||||
* @return {Promise<object[]>} returns the app information document stored in each app database.
|
* @return returns the app information document stored in each app database.
|
||||||
*/
|
*/
|
||||||
export async function getAllApps({
|
export async function getAllApps({
|
||||||
dev,
|
dev,
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function isDevApp(app: App) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a development app ID from a real app ID.
|
* Generates a development app ID from a real app ID.
|
||||||
* @returns {string} the dev app ID which can be used for dev database.
|
* @returns the dev app ID which can be used for dev database.
|
||||||
*/
|
*/
|
||||||
export function getDevelopmentAppID(appId: string) {
|
export function getDevelopmentAppID(appId: string) {
|
||||||
if (!appId || appId.startsWith(APP_DEV_PREFIX)) {
|
if (!appId || appId.startsWith(APP_DEV_PREFIX)) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { newid } from "./newid"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new app ID.
|
* Generates a new app ID.
|
||||||
* @returns {string} The new app ID which the app doc can be stored under.
|
* @returns The new app ID which the app doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export const generateAppID = (tenantId?: string | null) => {
|
export const generateAppID = (tenantId?: string | null) => {
|
||||||
let id = APP_PREFIX
|
let id = APP_PREFIX
|
||||||
|
@ -20,9 +20,9 @@ export const generateAppID = (tenantId?: string | null) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a new row ID for the specified table.
|
* Gets a new row ID for the specified table.
|
||||||
* @param {string} tableId The table which the row is being created for.
|
* @param tableId The table which the row is being created for.
|
||||||
* @param {string|null} id If an ID is to be used then the UUID can be substituted for this.
|
* @param id If an ID is to be used then the UUID can be substituted for this.
|
||||||
* @returns {string} The new ID which a row doc can be stored under.
|
* @returns The new ID which a row doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export function generateRowID(tableId: string, id?: string) {
|
export function generateRowID(tableId: string, id?: string) {
|
||||||
id = id || newid()
|
id = id || newid()
|
||||||
|
@ -31,7 +31,7 @@ export function generateRowID(tableId: string, id?: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new workspace ID.
|
* Generates a new workspace ID.
|
||||||
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
* @returns The new workspace ID which the workspace doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export function generateWorkspaceID() {
|
export function generateWorkspaceID() {
|
||||||
return `${DocumentType.WORKSPACE}${SEPARATOR}${newid()}`
|
return `${DocumentType.WORKSPACE}${SEPARATOR}${newid()}`
|
||||||
|
@ -39,7 +39,7 @@ export function generateWorkspaceID() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new global user ID.
|
* Generates a new global user ID.
|
||||||
* @returns {string} The new user ID which the user doc can be stored under.
|
* @returns The new user ID which the user doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export function generateGlobalUserID(id?: any) {
|
export function generateGlobalUserID(id?: any) {
|
||||||
return `${DocumentType.USER}${SEPARATOR}${id || newid()}`
|
return `${DocumentType.USER}${SEPARATOR}${id || newid()}`
|
||||||
|
@ -52,8 +52,8 @@ export function isGlobalUserID(id: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new user ID based on the passed in global ID.
|
* Generates a new user ID based on the passed in global ID.
|
||||||
* @param {string} globalId The ID of the global user.
|
* @param globalId The ID of the global user.
|
||||||
* @returns {string} The new user ID which the user doc can be stored under.
|
* @returns The new user ID which the user doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export function generateUserMetadataID(globalId: string) {
|
export function generateUserMetadataID(globalId: string) {
|
||||||
return generateRowID(InternalTable.USER_METADATA, globalId)
|
return generateRowID(InternalTable.USER_METADATA, globalId)
|
||||||
|
@ -84,7 +84,7 @@ export function generateAppUserID(prodAppId: string, userId: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new role ID.
|
* Generates a new role ID.
|
||||||
* @returns {string} The new role ID which the role doc can be stored under.
|
* @returns The new role ID which the role doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export function generateRoleID(name: string) {
|
export function generateRoleID(name: string) {
|
||||||
const prefix = `${DocumentType.ROLE}${SEPARATOR}`
|
const prefix = `${DocumentType.ROLE}${SEPARATOR}`
|
||||||
|
@ -103,7 +103,7 @@ export function prefixRoleID(name: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new dev info document ID - this is scoped to a user.
|
* Generates a new dev info document ID - this is scoped to a user.
|
||||||
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
|
* @returns The new dev info ID which info for dev (like api key) can be stored under.
|
||||||
*/
|
*/
|
||||||
export const generateDevInfoID = (userId: any) => {
|
export const generateDevInfoID = (userId: any) => {
|
||||||
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
|
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
|
||||||
|
@ -111,7 +111,7 @@ export const generateDevInfoID = (userId: any) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new plugin ID - to be used in the global DB.
|
* Generates a new plugin ID - to be used in the global DB.
|
||||||
* @returns {string} The new plugin ID which a plugin metadata document can be stored under.
|
* @returns The new plugin ID which a plugin metadata document can be stored under.
|
||||||
*/
|
*/
|
||||||
export const generatePluginID = (name: string) => {
|
export const generatePluginID = (name: string) => {
|
||||||
return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
|
return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
|
||||||
|
|
|
@ -12,12 +12,12 @@ import { getProdAppID } from "./conversions"
|
||||||
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
||||||
* More complex cases such as link docs and rows which have multiple levels of IDs that their
|
* More complex cases such as link docs and rows which have multiple levels of IDs that their
|
||||||
* ID consists of need their own functions to build the allDocs parameters.
|
* ID consists of need their own functions to build the allDocs parameters.
|
||||||
* @param {string} docType The type of document which input params are being built for, e.g. user,
|
* @param docType The type of document which input params are being built for, e.g. user,
|
||||||
* link, app, table and so on.
|
* link, app, table and so on.
|
||||||
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking
|
* @param docId The ID of the document minus its type - this is only needed if looking
|
||||||
* for a singular document.
|
* for a singular document.
|
||||||
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs.
|
* @param otherProps Add any other properties onto the request, e.g. include_docs.
|
||||||
* @returns {object} Parameters which can then be used with an allDocs request.
|
* @returns Parameters which can then be used with an allDocs request.
|
||||||
*/
|
*/
|
||||||
export function getDocParams(
|
export function getDocParams(
|
||||||
docType: string,
|
docType: string,
|
||||||
|
@ -36,11 +36,11 @@ export function getDocParams(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the DB allDocs/query params for retrieving a row.
|
* Gets the DB allDocs/query params for retrieving a row.
|
||||||
* @param {string|null} tableId The table in which the rows have been stored.
|
* @param tableId The table in which the rows have been stored.
|
||||||
* @param {string|null} rowId The ID of the row which is being specifically queried for. This can be
|
* @param rowId The ID of the row which is being specifically queried for. This can be
|
||||||
* left null to get all the rows in the table.
|
* left null to get all the rows in the table.
|
||||||
* @param {object} otherProps Any other properties to add to the request.
|
* @param otherProps Any other properties to add to the request.
|
||||||
* @returns {object} Parameters which can then be used with an allDocs request.
|
* @returns Parameters which can then be used with an allDocs request.
|
||||||
*/
|
*/
|
||||||
export function getRowParams(
|
export function getRowParams(
|
||||||
tableId?: string | null,
|
tableId?: string | null,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Makes sure that a URL has the correct number of slashes, while maintaining the
|
* Makes sure that a URL has the correct number of slashes, while maintaining the
|
||||||
* http(s):// double slashes.
|
* http(s):// double slashes.
|
||||||
* @param {string} url The URL to test and remove any extra double slashes.
|
* @param url The URL to test and remove any extra double slashes.
|
||||||
* @return {string} The updated url.
|
* @return The updated url.
|
||||||
*/
|
*/
|
||||||
export function checkSlashesInUrl(url: string) {
|
export function checkSlashesInUrl(url: string) {
|
||||||
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
|
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
|
||||||
|
|
|
@ -13,10 +13,10 @@ export const options = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passport Local Authentication Middleware.
|
* Passport Local Authentication Middleware.
|
||||||
* @param {*} ctx the request structure
|
* @param ctx the request structure
|
||||||
* @param {*} email username to login with
|
* @param email username to login with
|
||||||
* @param {*} password plain text password to log in with
|
* @param password plain text password to log in with
|
||||||
* @param {*} done callback from passport to return user information and errors
|
* @param done callback from passport to return user information and errors
|
||||||
* @returns The authenticated user, or errors if they occur
|
* @returns The authenticated user, or errors if they occur
|
||||||
*/
|
*/
|
||||||
export async function authenticate(
|
export async function authenticate(
|
||||||
|
|
|
@ -17,15 +17,15 @@ const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
||||||
|
|
||||||
export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {
|
export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {
|
||||||
/**
|
/**
|
||||||
* @param {*} issuer The identity provider base URL
|
* @param issuer The identity provider base URL
|
||||||
* @param {*} sub The user ID
|
* @param sub The user ID
|
||||||
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
* @param profile The user profile information. Created by passport from the /userinfo response
|
||||||
* @param {*} jwtClaims The parsed id_token claims
|
* @param jwtClaims The parsed id_token claims
|
||||||
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
* @param accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||||
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
* @param refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||||
* @param {*} idToken The id_token - always a JWT
|
* @param idToken The id_token - always a JWT
|
||||||
* @param {*} params The response body from requesting an access_token
|
* @param params The response body from requesting an access_token
|
||||||
* @param {*} done The passport callback: err, user, info
|
* @param done The passport callback: err, user, info
|
||||||
*/
|
*/
|
||||||
return async (
|
return async (
|
||||||
issuer: string,
|
issuer: string,
|
||||||
|
@ -61,8 +61,8 @@ export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {*} profile The structured profile created by passport using the user info endpoint
|
* @param profile The structured profile created by passport using the user info endpoint
|
||||||
* @param {*} jwtClaims The claims returned in the id token
|
* @param jwtClaims The claims returned in the id token
|
||||||
*/
|
*/
|
||||||
function getEmail(profile: SSOProfile, jwtClaims: JwtClaims) {
|
function getEmail(profile: SSOProfile, jwtClaims: JwtClaims) {
|
||||||
// profile not guaranteed to contain email e.g. github connected azure ad account
|
// profile not guaranteed to contain email e.g. github connected azure ad account
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { ConfigType, GoogleInnerConfig } from "@budibase/types"
|
||||||
/**
|
/**
|
||||||
* Utility to handle authentication errors.
|
* Utility to handle authentication errors.
|
||||||
*
|
*
|
||||||
* @param {*} done The passport callback.
|
* @param done The passport callback.
|
||||||
* @param {*} message Message that will be returned in the response body
|
* @param message Message that will be returned in the response body
|
||||||
* @param {*} err (Optional) error that will be logged
|
* @param err (Optional) error that will be logged
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function authError(done: Function, message: string, err?: any) {
|
export function authError(done: Function, message: string, err?: any) {
|
||||||
|
|
|
@ -6,10 +6,10 @@ import * as cloudfront from "../cloudfront"
|
||||||
* In production the client library is stored in the object store, however in development
|
* In production the client library is stored in the object store, however in development
|
||||||
* we use the symlinked version produced by lerna, located in node modules. We link to this
|
* we use the symlinked version produced by lerna, located in node modules. We link to this
|
||||||
* via a specific endpoint (under /api/assets/client).
|
* via a specific endpoint (under /api/assets/client).
|
||||||
* @param {string} appId In production we need the appId to look up the correct bucket, as the
|
* @param appId In production we need the appId to look up the correct bucket, as the
|
||||||
* version of the client lib may differ between apps.
|
* version of the client lib may differ between apps.
|
||||||
* @param {string} version The version to retrieve.
|
* @param version The version to retrieve.
|
||||||
* @return {string} The URL to be inserted into appPackage response or server rendered
|
* @return The URL to be inserted into appPackage response or server rendered
|
||||||
* app index file.
|
* app index file.
|
||||||
*/
|
*/
|
||||||
export const clientLibraryUrl = (appId: string, version: string) => {
|
export const clientLibraryUrl = (appId: string, version: string) => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
const cfsign = require("aws-cloudfront-sign")
|
import * as cfsign from "aws-cloudfront-sign"
|
||||||
|
|
||||||
let PRIVATE_KEY: string | undefined
|
let PRIVATE_KEY: string | undefined
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ function getPrivateKey() {
|
||||||
|
|
||||||
const getCloudfrontSignParams = () => {
|
const getCloudfrontSignParams = () => {
|
||||||
return {
|
return {
|
||||||
keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID,
|
keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!,
|
||||||
privateKeyString: getPrivateKey(),
|
privateKeyString: getPrivateKey(),
|
||||||
expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour
|
expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,9 +61,9 @@ export function sanitizeBucket(input: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a connection to the object store using the S3 SDK.
|
* 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.
|
* @param bucket the name of the bucket which blobs will be uploaded/retrieved from.
|
||||||
* @param {object} opts configuration for the object store.
|
* @param opts configuration for the object store.
|
||||||
* @return {Object} an S3 object store object, check S3 Nodejs SDK for usage.
|
* @return an S3 object store object, check S3 Nodejs SDK for usage.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export const ObjectStore = (
|
export const ObjectStore = (
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
AutomationStepIdArray,
|
AutomationStepIdArray,
|
||||||
AutomationIOType,
|
AutomationIOType,
|
||||||
AutomationCustomIOType,
|
AutomationCustomIOType,
|
||||||
|
DatasourceFeature,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import joi from "joi"
|
import joi from "joi"
|
||||||
|
|
||||||
|
@ -67,9 +68,27 @@ function validateDatasource(schema: any) {
|
||||||
version: joi.string().optional(),
|
version: joi.string().optional(),
|
||||||
schema: joi.object({
|
schema: joi.object({
|
||||||
docs: joi.string(),
|
docs: joi.string(),
|
||||||
|
plus: joi.boolean().optional(),
|
||||||
|
isSQL: joi.boolean().optional(),
|
||||||
|
auth: joi
|
||||||
|
.object({
|
||||||
|
type: joi.string().required(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
features: joi
|
||||||
|
.object(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.values(DatasourceFeature).map(key => [
|
||||||
|
key,
|
||||||
|
joi.boolean().optional(),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
relationships: joi.boolean().optional(),
|
||||||
|
description: joi.string().required(),
|
||||||
friendlyName: joi.string().required(),
|
friendlyName: joi.string().required(),
|
||||||
type: joi.string().allow(...DATASOURCE_TYPES),
|
type: joi.string().allow(...DATASOURCE_TYPES),
|
||||||
description: joi.string().required(),
|
|
||||||
datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
|
datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
|
||||||
query: joi
|
query: joi
|
||||||
.object()
|
.object()
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { timeout } from "../utils"
|
||||||
* Bull works with a Job wrapper around all messages that contains a lot more information about
|
* Bull works with a Job wrapper around all messages that contains a lot more information about
|
||||||
* the state of the message, this object constructor implements the same schema of Bull jobs
|
* the state of the message, this object constructor implements the same schema of Bull jobs
|
||||||
* for the sake of maintaining API consistency.
|
* for the sake of maintaining API consistency.
|
||||||
* @param {string} queue The name of the queue which the message will be carried on.
|
* @param queue The name of the queue which the message will be carried on.
|
||||||
* @param {object} message The JSON message which will be passed back to the consumer.
|
* @param message The JSON message which will be passed back to the consumer.
|
||||||
* @returns {Object} A new job which can now be put onto the queue, this is mostly an
|
* @returns A new job which can now be put onto the queue, this is mostly an
|
||||||
* internal structure so that an in memory queue can be easily swapped for a Bull queue.
|
* internal structure so that an in memory queue can be easily swapped for a Bull queue.
|
||||||
*/
|
*/
|
||||||
function newJob(queue: string, message: any) {
|
function newJob(queue: string, message: any) {
|
||||||
|
@ -32,8 +32,8 @@ class InMemoryQueue {
|
||||||
_addCount: number
|
_addCount: number
|
||||||
/**
|
/**
|
||||||
* The constructor the queue, exactly the same as that of Bulls.
|
* The constructor the queue, exactly the same as that of Bulls.
|
||||||
* @param {string} name The name of the queue which is being configured.
|
* @param name The name of the queue which is being configured.
|
||||||
* @param {object|null} opts This is not used by the in memory queue as there is no real use
|
* @param opts This is not used by the in memory queue as there is no real use
|
||||||
* case when in memory, but is the same API as Bull
|
* case when in memory, but is the same API as Bull
|
||||||
*/
|
*/
|
||||||
constructor(name: string, opts = null) {
|
constructor(name: string, opts = null) {
|
||||||
|
@ -49,7 +49,7 @@ class InMemoryQueue {
|
||||||
* Same callback API as Bull, each callback passed to this will consume messages as they are
|
* Same callback API as Bull, each callback passed to this will consume messages as they are
|
||||||
* available. Please note this is a queue service, not a notification service, so each
|
* available. Please note this is a queue service, not a notification service, so each
|
||||||
* consumer will receive different messages.
|
* consumer will receive different messages.
|
||||||
* @param {function<object>} func The callback function which will return a "Job", the same
|
* @param func The callback function which will return a "Job", the same
|
||||||
* as the Bull API, within this job the property "data" contains the JSON message. Please
|
* as the Bull API, within this job the property "data" contains the JSON message. Please
|
||||||
* note this is incredibly limited compared to Bull as in reality the Job would contain
|
* note this is incredibly limited compared to Bull as in reality the Job would contain
|
||||||
* a lot more information about the queue and current status of Bull cluster.
|
* a lot more information about the queue and current status of Bull cluster.
|
||||||
|
@ -73,9 +73,9 @@ class InMemoryQueue {
|
||||||
* Simple function to replicate the add message functionality of Bull, putting
|
* Simple function to replicate the add message functionality of Bull, putting
|
||||||
* a new message on the queue. This then emits an event which will be used to
|
* a new message on the queue. This then emits an event which will be used to
|
||||||
* return the message to a consumer (if one is attached).
|
* return the message to a consumer (if one is attached).
|
||||||
* @param {object} msg A message to be transported over the queue, this should be
|
* @param msg A message to be transported over the queue, this should be
|
||||||
* a JSON message as this is required by Bull.
|
* a JSON message as this is required by Bull.
|
||||||
* @param {boolean} repeat serves no purpose for the import queue.
|
* @param repeat serves no purpose for the import queue.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
add(msg: any, repeat: boolean) {
|
add(msg: any, repeat: boolean) {
|
||||||
|
@ -96,7 +96,7 @@ class InMemoryQueue {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This removes a cron which has been implemented, this is part of Bull API.
|
* This removes a cron which has been implemented, this is part of Bull API.
|
||||||
* @param {string} cronJobId The cron which is to be removed.
|
* @param cronJobId The cron which is to be removed.
|
||||||
*/
|
*/
|
||||||
removeRepeatableByKey(cronJobId: string) {
|
removeRepeatableByKey(cronJobId: string) {
|
||||||
// TODO: implement for testing
|
// TODO: implement for testing
|
||||||
|
|
|
@ -142,7 +142,7 @@ function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) {
|
||||||
* this can only be done with redis streams because they will have an end.
|
* 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 stream A redis stream, specifically as this type of stream will have an end.
|
||||||
* @param client The client to use for further lookups.
|
* @param client The client to use for further lookups.
|
||||||
* @return {Promise<object>} The final output of the stream
|
* @return The final output of the stream
|
||||||
*/
|
*/
|
||||||
function promisifyStream(stream: any, client: RedisWrapper) {
|
function promisifyStream(stream: any, client: RedisWrapper) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -36,8 +36,8 @@ export function levelToNumber(perm: PermissionLevel) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the specified permission level for the user return the levels they are allowed to carry out.
|
* 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.
|
* @param userPermLevel The permission level of the user.
|
||||||
* @return {string[]} All the permission levels this user is allowed to carry out.
|
* @return All the permission levels this user is allowed to carry out.
|
||||||
*/
|
*/
|
||||||
export function getAllowedLevels(userPermLevel: PermissionLevel): string[] {
|
export function getAllowedLevels(userPermLevel: PermissionLevel): string[] {
|
||||||
switch (userPermLevel) {
|
switch (userPermLevel) {
|
||||||
|
|
|
@ -149,9 +149,9 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
|
||||||
/**
|
/**
|
||||||
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
||||||
* to check if the role inherits any others.
|
* to check if the role inherits any others.
|
||||||
* @param {string|null} roleId The level ID to lookup.
|
* @param roleId The level ID to lookup.
|
||||||
* @param {object|null} opts options for the function, like whether to halt errors, instead return public.
|
* @param opts options for the function, like whether to halt errors, instead return public.
|
||||||
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
|
* @returns The role object, which may contain an "inherits" property.
|
||||||
*/
|
*/
|
||||||
export async function getRole(
|
export async function getRole(
|
||||||
roleId?: string,
|
roleId?: string,
|
||||||
|
@ -225,8 +225,8 @@ export async function getUserRoleIdHierarchy(
|
||||||
/**
|
/**
|
||||||
* Returns an ordered array of the user's inherited role IDs, this can be used
|
* Returns an ordered array of the user's inherited role IDs, this can be used
|
||||||
* to determine if a user can access something that requires a specific role.
|
* to determine if a user can access something that requires a specific role.
|
||||||
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
* @param userRoleId The user's role ID, this can be found in their access token.
|
||||||
* @returns {Promise<object[]>} returns an ordered array of the roles, with the first being their
|
* @returns returns an ordered array of the roles, with the first being their
|
||||||
* highest level of access and the last being the lowest level.
|
* highest level of access and the last being the lowest level.
|
||||||
*/
|
*/
|
||||||
export async function getUserRoleHierarchy(userRoleId?: string) {
|
export async function getUserRoleHierarchy(userRoleId?: string) {
|
||||||
|
@ -258,7 +258,7 @@ export async function getAllRoleIds(appId?: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
||||||
* @return {Promise<object[]>} An array of the role objects that were found.
|
* @return An array of the role objects that were found.
|
||||||
*/
|
*/
|
||||||
export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
||||||
if (appId) {
|
if (appId) {
|
||||||
|
|
|
@ -21,17 +21,21 @@ import {
|
||||||
User,
|
User,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
ContextUser,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getAccountHolderFromUserIds,
|
getAccountHolderFromUserIds,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
isCreator,
|
||||||
validateUniqueUser,
|
validateUniqueUser,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { searchExistingEmails } from "./lookup"
|
import { searchExistingEmails } from "./lookup"
|
||||||
import { hash } from "../utils"
|
import { hash } from "../utils"
|
||||||
|
|
||||||
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
|
type QuotaUpdateFn = (
|
||||||
|
change: number,
|
||||||
|
creatorsChange: number,
|
||||||
|
cb?: () => Promise<any>
|
||||||
|
) => Promise<any>
|
||||||
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
|
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
|
||||||
type FeatureFn = () => Promise<Boolean>
|
type FeatureFn = () => Promise<Boolean>
|
||||||
type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
|
type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
|
||||||
|
@ -135,7 +139,7 @@ export class UserDB {
|
||||||
if (!fullUser.roles) {
|
if (!fullUser.roles) {
|
||||||
fullUser.roles = {}
|
fullUser.roles = {}
|
||||||
}
|
}
|
||||||
// add the active status to a user if its not provided
|
// add the active status to a user if it's not provided
|
||||||
if (fullUser.status == null) {
|
if (fullUser.status == null) {
|
||||||
fullUser.status = UserStatus.ACTIVE
|
fullUser.status = UserStatus.ACTIVE
|
||||||
}
|
}
|
||||||
|
@ -246,7 +250,8 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
|
|
||||||
const change = dbUser ? 0 : 1 // no change if there is existing user
|
const change = dbUser ? 0 : 1 // no change if there is existing user
|
||||||
return UserDB.quotas.addUsers(change, async () => {
|
const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0
|
||||||
|
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
||||||
await validateUniqueUser(email, tenantId)
|
await validateUniqueUser(email, tenantId)
|
||||||
|
|
||||||
let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser)
|
let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser)
|
||||||
|
@ -308,6 +313,7 @@ export class UserDB {
|
||||||
|
|
||||||
let usersToSave: any[] = []
|
let usersToSave: any[] = []
|
||||||
let newUsers: any[] = []
|
let newUsers: any[] = []
|
||||||
|
let newCreators: any[] = []
|
||||||
|
|
||||||
const emails = newUsersRequested.map((user: User) => user.email)
|
const emails = newUsersRequested.map((user: User) => user.email)
|
||||||
const existingEmails = await searchExistingEmails(emails)
|
const existingEmails = await searchExistingEmails(emails)
|
||||||
|
@ -328,59 +334,66 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
newUser.userGroups = groups
|
newUser.userGroups = groups
|
||||||
newUsers.push(newUser)
|
newUsers.push(newUser)
|
||||||
|
if (isCreator(newUser)) {
|
||||||
|
newCreators.push(newUser)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await accountSdk.getAccountByTenantId(tenantId)
|
const account = await accountSdk.getAccountByTenantId(tenantId)
|
||||||
return UserDB.quotas.addUsers(newUsers.length, async () => {
|
return UserDB.quotas.addUsers(
|
||||||
// create the promises array that will be called by bulkDocs
|
newUsers.length,
|
||||||
newUsers.forEach((user: any) => {
|
newCreators.length,
|
||||||
usersToSave.push(
|
async () => {
|
||||||
UserDB.buildUser(
|
// create the promises array that will be called by bulkDocs
|
||||||
user,
|
newUsers.forEach((user: any) => {
|
||||||
{
|
usersToSave.push(
|
||||||
hashPassword: true,
|
UserDB.buildUser(
|
||||||
requirePassword: user.requirePassword,
|
user,
|
||||||
},
|
{
|
||||||
tenantId,
|
hashPassword: true,
|
||||||
undefined, // no dbUser
|
requirePassword: user.requirePassword,
|
||||||
account
|
},
|
||||||
|
tenantId,
|
||||||
|
undefined, // no dbUser
|
||||||
|
account
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const usersToBulkSave = await Promise.all(usersToSave)
|
const usersToBulkSave = await Promise.all(usersToSave)
|
||||||
await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
|
await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
|
||||||
|
|
||||||
// Post-processing of bulk added users, e.g. events and cache operations
|
// Post-processing of bulk added users, e.g. events and cache operations
|
||||||
for (const user of usersToBulkSave) {
|
for (const user of usersToBulkSave) {
|
||||||
// TODO: Refactor to bulk insert users into the info db
|
// TODO: Refactor to bulk insert users into the info db
|
||||||
// instead of relying on looping tenant creation
|
// instead of relying on looping tenant creation
|
||||||
await platform.users.addUser(tenantId, user._id, user.email)
|
await platform.users.addUser(tenantId, user._id, user.email)
|
||||||
await eventHelpers.handleSaveEvents(user, undefined)
|
await eventHelpers.handleSaveEvents(user, undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saved = usersToBulkSave.map(user => {
|
||||||
|
return {
|
||||||
|
_id: user._id,
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// now update the groups
|
||||||
|
if (Array.isArray(saved) && groups) {
|
||||||
|
const groupPromises = []
|
||||||
|
const createdUserIds = saved.map(user => user._id)
|
||||||
|
for (let groupId of groups) {
|
||||||
|
groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
|
||||||
|
}
|
||||||
|
await Promise.all(groupPromises)
|
||||||
|
}
|
||||||
|
|
||||||
const saved = usersToBulkSave.map(user => {
|
|
||||||
return {
|
return {
|
||||||
_id: user._id,
|
successful: saved,
|
||||||
email: user.email,
|
unsuccessful,
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// now update the groups
|
|
||||||
if (Array.isArray(saved) && groups) {
|
|
||||||
const groupPromises = []
|
|
||||||
const createdUserIds = saved.map(user => user._id)
|
|
||||||
for (let groupId of groups) {
|
|
||||||
groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
|
|
||||||
}
|
|
||||||
await Promise.all(groupPromises)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
return {
|
|
||||||
successful: saved,
|
|
||||||
unsuccessful,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async bulkDelete(userIds: string[]): Promise<BulkUserDeleted> {
|
static async bulkDelete(userIds: string[]): Promise<BulkUserDeleted> {
|
||||||
|
@ -420,11 +433,12 @@ export class UserDB {
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
}))
|
}))
|
||||||
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
||||||
|
const creatorsToDelete = usersToDelete.filter(isCreator)
|
||||||
|
|
||||||
await UserDB.quotas.removeUsers(toDelete.length)
|
|
||||||
for (let user of usersToDelete) {
|
for (let user of usersToDelete) {
|
||||||
await bulkDeleteProcessing(user)
|
await bulkDeleteProcessing(user)
|
||||||
}
|
}
|
||||||
|
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
|
||||||
|
|
||||||
// Build Response
|
// Build Response
|
||||||
// index users by id
|
// index users by id
|
||||||
|
@ -473,7 +487,8 @@ export class UserDB {
|
||||||
|
|
||||||
await db.remove(userId, dbUser._rev)
|
await db.remove(userId, dbUser._rev)
|
||||||
|
|
||||||
await UserDB.quotas.removeUsers(1)
|
const creatorsToDelete = isCreator(dbUser) ? 1 : 0
|
||||||
|
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
||||||
await eventHelpers.handleDeleteEvents(dbUser)
|
await eventHelpers.handleDeleteEvents(dbUser)
|
||||||
await cache.user.invalidateUser(userId)
|
await cache.user.invalidateUser(userId)
|
||||||
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
||||||
|
|
|
@ -14,14 +14,15 @@ import {
|
||||||
} from "../db"
|
} from "../db"
|
||||||
import {
|
import {
|
||||||
BulkDocsResponse,
|
BulkDocsResponse,
|
||||||
ContextUser,
|
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQueryOperators,
|
SearchQueryOperators,
|
||||||
SearchUsersRequest,
|
SearchUsersRequest,
|
||||||
User,
|
User,
|
||||||
|
ContextUser,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as context from "../context"
|
|
||||||
import { getGlobalDB } from "../context"
|
import { getGlobalDB } from "../context"
|
||||||
|
import * as context from "../context"
|
||||||
|
import { isCreator } from "./utils"
|
||||||
|
|
||||||
type GetOpts = { cleanup?: boolean }
|
type GetOpts = { cleanup?: boolean }
|
||||||
|
|
||||||
|
@ -283,6 +284,19 @@ export async function getUserCount() {
|
||||||
return response.total_rows
|
return response.total_rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCreatorCount() {
|
||||||
|
let creators = 0
|
||||||
|
async function iterate(startPage?: string) {
|
||||||
|
const page = await paginatedUsers({ bookmark: startPage })
|
||||||
|
creators += page.data.filter(isCreator).length
|
||||||
|
if (page.hasNextPage) {
|
||||||
|
await iterate(page.nextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await iterate()
|
||||||
|
return creators
|
||||||
|
}
|
||||||
|
|
||||||
// used to remove the builder/admin permissions, for processing the
|
// used to remove the builder/admin permissions, for processing the
|
||||||
// user as an app user (they may have some specific role/group
|
// user as an app user (they may have some specific role/group
|
||||||
export function removePortalUserPermissions(user: User | ContextUser) {
|
export function removePortalUserPermissions(user: User | ContextUser) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { getAccountByTenantId } from "../accounts"
|
||||||
// extract from shared-core to make easily accessible from backend-core
|
// extract from shared-core to make easily accessible from backend-core
|
||||||
export const isBuilder = sdk.users.isBuilder
|
export const isBuilder = sdk.users.isBuilder
|
||||||
export const isAdmin = sdk.users.isAdmin
|
export const isAdmin = sdk.users.isAdmin
|
||||||
|
export const isCreator = sdk.users.isCreator
|
||||||
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||||
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||||
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||||
|
|
|
@ -79,8 +79,8 @@ export function isPublicApiRequest(ctx: Ctx): boolean {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a request tries to find the appId, which can be located in various places
|
* Given a request tries to find the appId, which can be located in various places
|
||||||
* @param {object} ctx The main request body to look through.
|
* @param ctx The main request body to look through.
|
||||||
* @returns {string|undefined} If an appId was found it will be returned.
|
* @returns If an appId was found it will be returned.
|
||||||
*/
|
*/
|
||||||
export async function getAppIdFromCtx(ctx: Ctx) {
|
export async function getAppIdFromCtx(ctx: Ctx) {
|
||||||
// look in headers
|
// look in headers
|
||||||
|
@ -135,7 +135,7 @@ function parseAppIdFromUrl(url?: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* opens the contents of the specified encrypted JWT.
|
* opens the contents of the specified encrypted JWT.
|
||||||
* @return {object} the contents of the token.
|
* @return the contents of the token.
|
||||||
*/
|
*/
|
||||||
export function openJwt(token: string) {
|
export function openJwt(token: string) {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -169,8 +169,8 @@ export function isValidInternalAPIKey(apiKey: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a cookie from context, and decrypt if necessary.
|
* Get a cookie from context, and decrypt if necessary.
|
||||||
* @param {object} ctx The request which is to be manipulated.
|
* @param ctx The request which is to be manipulated.
|
||||||
* @param {string} name The name of the cookie to get.
|
* @param name The name of the cookie to get.
|
||||||
*/
|
*/
|
||||||
export function getCookie(ctx: Ctx, name: string) {
|
export function getCookie(ctx: Ctx, name: string) {
|
||||||
const cookie = ctx.cookies.get(name)
|
const cookie = ctx.cookies.get(name)
|
||||||
|
@ -184,10 +184,10 @@ export function getCookie(ctx: Ctx, name: string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a cookie for the request - it will not expire.
|
* Store a cookie for the request - it will not expire.
|
||||||
* @param {object} ctx The request which is to be manipulated.
|
* @param ctx The request which is to be manipulated.
|
||||||
* @param {string} name The name of the cookie to set.
|
* @param name The name of the cookie to set.
|
||||||
* @param {string|object} value The value of cookie which will be set.
|
* @param value The value of cookie which will be set.
|
||||||
* @param {object} opts options like whether to sign.
|
* @param opts options like whether to sign.
|
||||||
*/
|
*/
|
||||||
export function setCookie(
|
export function setCookie(
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
|
@ -223,8 +223,8 @@ export function clearCookie(ctx: Ctx, name: string) {
|
||||||
/**
|
/**
|
||||||
* Checks if the API call being made (based on the provided ctx object) is from the client. If
|
* Checks if the API call being made (based on the provided ctx object) is from the client. If
|
||||||
* the call is not from a client app then it is from the builder.
|
* the call is not from a client app then it is from the builder.
|
||||||
* @param {object} ctx The koa context object to be tested.
|
* @param 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).
|
* @return returns true if the call is from the client lib (a built app rather than the builder).
|
||||||
*/
|
*/
|
||||||
export function isClient(ctx: Ctx) {
|
export function isClient(ctx: Ctx) {
|
||||||
return ctx.headers[Header.TYPE] === "client"
|
return ctx.headers[Header.TYPE] === "client"
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
const {structures} = require("../../../tests")
|
||||||
|
|
||||||
|
jest.mock("../../../src/context")
|
||||||
|
jest.mock("../../../src/db")
|
||||||
|
|
||||||
|
const context = require("../../../src/context")
|
||||||
|
const db = require("../../../src/db")
|
||||||
|
|
||||||
|
const {getCreatorCount} = require('../../../src/users/users')
|
||||||
|
|
||||||
|
describe("Users", () => {
|
||||||
|
|
||||||
|
let getGlobalDBMock
|
||||||
|
let getGlobalUserParamsMock
|
||||||
|
let paginationMock
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
|
||||||
|
getGlobalDBMock = jest.spyOn(context, "getGlobalDB")
|
||||||
|
getGlobalUserParamsMock = jest.spyOn(db, "getGlobalUserParams")
|
||||||
|
paginationMock = jest.spyOn(db, "pagination")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Retrieves the number of creators", async () => {
|
||||||
|
const getUsers = (offset, limit, creators = false) => {
|
||||||
|
const range = _.range(offset, limit)
|
||||||
|
const opts = creators ? {builder: {global: true}} : undefined
|
||||||
|
return range.map(() => structures.users.user(opts))
|
||||||
|
}
|
||||||
|
const page1Data = getUsers(0, 8)
|
||||||
|
const page2Data = getUsers(8, 12, true)
|
||||||
|
getGlobalDBMock.mockImplementation(() => ({
|
||||||
|
name : "fake-db",
|
||||||
|
allDocs: () => ({
|
||||||
|
rows: [...page1Data, ...page2Data]
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
paginationMock.mockImplementationOnce(() => ({
|
||||||
|
data: page1Data,
|
||||||
|
hasNextPage: true,
|
||||||
|
nextPage: "1"
|
||||||
|
}))
|
||||||
|
paginationMock.mockImplementation(() => ({
|
||||||
|
data: page2Data,
|
||||||
|
hasNextPage: false,
|
||||||
|
nextPage: undefined
|
||||||
|
}))
|
||||||
|
const creatorsCount = await getCreatorCount()
|
||||||
|
expect(creatorsCount).toBe(4)
|
||||||
|
expect(paginationMock).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
|
@ -72,6 +72,11 @@ export function quotas(): Quotas {
|
||||||
value: 1,
|
value: 1,
|
||||||
triggers: [],
|
triggers: [],
|
||||||
},
|
},
|
||||||
|
creators: {
|
||||||
|
name: "Creators",
|
||||||
|
value: 1,
|
||||||
|
triggers: [],
|
||||||
|
},
|
||||||
userGroups: {
|
userGroups: {
|
||||||
name: "User Groups",
|
name: "User Groups",
|
||||||
value: 1,
|
value: 1,
|
||||||
|
@ -118,6 +123,10 @@ export function customer(): Customer {
|
||||||
export function subscription(): Subscription {
|
export function subscription(): Subscription {
|
||||||
return {
|
return {
|
||||||
amount: 10000,
|
amount: 10000,
|
||||||
|
amounts: {
|
||||||
|
user: 10000,
|
||||||
|
creator: 0,
|
||||||
|
},
|
||||||
cancelAt: undefined,
|
cancelAt: undefined,
|
||||||
currency: "usd",
|
currency: "usd",
|
||||||
currentPeriodEnd: 0,
|
currentPeriodEnd: 0,
|
||||||
|
@ -126,6 +135,10 @@ export function subscription(): Subscription {
|
||||||
duration: PriceDuration.MONTHLY,
|
duration: PriceDuration.MONTHLY,
|
||||||
pastDueAt: undefined,
|
pastDueAt: undefined,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
|
quantities: {
|
||||||
|
user: 0,
|
||||||
|
creator: 0,
|
||||||
|
},
|
||||||
status: "active",
|
status: "active",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { MonthlyQuotaName, QuotaUsage } from "@budibase/types"
|
import { MonthlyQuotaName, QuotaUsage } from "@budibase/types"
|
||||||
|
|
||||||
export const usage = (): QuotaUsage => {
|
export const usage = (users: number = 0, creators: number = 0): QuotaUsage => {
|
||||||
return {
|
return {
|
||||||
_id: "usage_quota",
|
_id: "usage_quota",
|
||||||
quotaReset: new Date().toISOString(),
|
quotaReset: new Date().toISOString(),
|
||||||
|
@ -58,7 +58,8 @@ export const usage = (): QuotaUsage => {
|
||||||
usageQuota: {
|
usageQuota: {
|
||||||
apps: 0,
|
apps: 0,
|
||||||
plugins: 0,
|
plugins: 0,
|
||||||
users: 0,
|
users,
|
||||||
|
creators,
|
||||||
userGroups: 0,
|
userGroups: 0,
|
||||||
rows: 0,
|
rows: 0,
|
||||||
triggers: {},
|
triggers: {},
|
||||||
|
|
|
@ -82,9 +82,9 @@
|
||||||
"@spectrum-css/vars": "3.0.1",
|
"@spectrum-css/vars": "3.0.1",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
|
"svelte-dnd-action": "^0.9.8",
|
||||||
"svelte-flatpickr": "3.2.3",
|
"svelte-flatpickr": "3.2.3",
|
||||||
"svelte-portal": "^1.0.0",
|
"svelte-portal": "^1.0.0"
|
||||||
"svelte-dnd-action": "^0.9.8"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"loader-utils": "1.4.1"
|
"loader-utils": "1.4.1"
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
const ncp = require("ncp").ncp
|
|
||||||
|
|
||||||
ncp("./dist", "../server/builder", function (err) {
|
|
||||||
if (err) {
|
|
||||||
return console.error(err)
|
|
||||||
}
|
|
||||||
console.log("Copied dist folder to ../server/builder")
|
|
||||||
})
|
|
|
@ -64,7 +64,6 @@
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
"@sentry/browser": "5.19.1",
|
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"codemirror": "^5.59.0",
|
"codemirror": "^5.59.0",
|
||||||
|
@ -85,8 +84,8 @@
|
||||||
"@babel/core": "^7.12.14",
|
"@babel/core": "^7.12.14",
|
||||||
"@babel/plugin-transform-runtime": "^7.13.10",
|
"@babel/plugin-transform-runtime": "^7.13.10",
|
||||||
"@babel/preset-env": "^7.13.12",
|
"@babel/preset-env": "^7.13.12",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^5.0.3",
|
||||||
"@roxi/routify": "2.18.5",
|
"@roxi/routify": "2.18.12",
|
||||||
"@sveltejs/vite-plugin-svelte": "1.0.1",
|
"@sveltejs/vite-plugin-svelte": "1.0.1",
|
||||||
"@testing-library/jest-dom": "5.17.0",
|
"@testing-library/jest-dom": "5.17.0",
|
||||||
"@testing-library/svelte": "^3.2.2",
|
"@testing-library/svelte": "^3.2.2",
|
||||||
|
@ -95,16 +94,18 @@
|
||||||
"jest": "29.6.2",
|
"jest": "29.6.2",
|
||||||
"jsdom": "^21.1.1",
|
"jsdom": "^21.1.1",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"rollup": "^2.44.0",
|
|
||||||
"svelte": "^3.48.0",
|
"svelte": "^3.48.0",
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
"vite": "^3.0.8",
|
"vite": "^4.4.11",
|
||||||
"vite-plugin-static-copy": "^0.16.0",
|
"vite-plugin-static-copy": "^0.17.0",
|
||||||
"vitest": "^0.29.2"
|
"vitest": "^0.29.2"
|
||||||
},
|
},
|
||||||
"nx": {
|
"nx": {
|
||||||
"targets": {
|
"targets": {
|
||||||
"build": {
|
"build": {
|
||||||
|
"outputs": [
|
||||||
|
"{workspaceRoot}/packages/server/builder"
|
||||||
|
],
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
{
|
{
|
||||||
"projects": [
|
"projects": [
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import * as Sentry from "@sentry/browser"
|
|
||||||
|
|
||||||
export default class SentryClient {
|
|
||||||
constructor(dsn) {
|
|
||||||
this.dsn = dsn
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.dsn) {
|
|
||||||
Sentry.init({ dsn: this.dsn })
|
|
||||||
|
|
||||||
this.initalised = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capture an exception and send it to sentry.
|
|
||||||
* @param {Error} err - JS error object
|
|
||||||
*/
|
|
||||||
captureException(err) {
|
|
||||||
if (!this.initalised) return
|
|
||||||
|
|
||||||
Sentry.captureException(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identify user in sentry.
|
|
||||||
* @param {String} id - Unique user id
|
|
||||||
*/
|
|
||||||
identify(id) {
|
|
||||||
if (!this.initalised) return
|
|
||||||
|
|
||||||
Sentry.configureScope(scope => {
|
|
||||||
scope.setUser({ id })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import PosthogClient from "./PosthogClient"
|
import PosthogClient from "./PosthogClient"
|
||||||
import IntercomClient from "./IntercomClient"
|
import IntercomClient from "./IntercomClient"
|
||||||
import SentryClient from "./SentryClient"
|
|
||||||
import { Events, EventSource } from "./constants"
|
import { Events, EventSource } from "./constants"
|
||||||
|
|
||||||
const posthog = new PosthogClient(process.env.POSTHOG_TOKEN)
|
const posthog = new PosthogClient(process.env.POSTHOG_TOKEN)
|
||||||
const sentry = new SentryClient(process.env.SENTRY_DSN)
|
|
||||||
const intercom = new IntercomClient(process.env.INTERCOM_TOKEN)
|
const intercom = new IntercomClient(process.env.INTERCOM_TOKEN)
|
||||||
|
|
||||||
class AnalyticsHub {
|
class AnalyticsHub {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.clients = [posthog, sentry, intercom]
|
this.clients = [posthog, intercom]
|
||||||
}
|
}
|
||||||
|
|
||||||
async activate() {
|
async activate() {
|
||||||
|
@ -23,12 +21,9 @@ class AnalyticsHub {
|
||||||
|
|
||||||
identify(id) {
|
identify(id) {
|
||||||
posthog.identify(id)
|
posthog.identify(id)
|
||||||
sentry.identify(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
captureException(err) {
|
captureException(_err) {}
|
||||||
sentry.captureException(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
captureEvent(eventName, props = {}) {
|
captureEvent(eventName, props = {}) {
|
||||||
posthog.captureEvent(eventName, props)
|
posthog.captureEvent(eventName, props)
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
|
|
||||||
const AUTO_TYPE = "auto"
|
const AUTO_TYPE = FIELDS.AUTO.type
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
const LINK_TYPE = FIELDS.LINK.type
|
const LINK_TYPE = FIELDS.LINK.type
|
||||||
const STRING_TYPE = FIELDS.STRING.type
|
const STRING_TYPE = FIELDS.STRING.type
|
||||||
|
@ -60,8 +60,13 @@
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
function makeFieldId(type, subtype) {
|
function makeFieldId(type, subtype, autocolumn) {
|
||||||
return `${type}${subtype || ""}`.toUpperCase()
|
// don't make field IDs for auto types
|
||||||
|
if (type === AUTO_TYPE || autocolumn) {
|
||||||
|
return type.toUpperCase()
|
||||||
|
} else {
|
||||||
|
return `${type}${subtype || ""}`.toUpperCase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let originalName
|
let originalName
|
||||||
|
@ -183,7 +188,8 @@
|
||||||
if (!savingColumn) {
|
if (!savingColumn) {
|
||||||
editableColumn.fieldId = makeFieldId(
|
editableColumn.fieldId = makeFieldId(
|
||||||
editableColumn.type,
|
editableColumn.type,
|
||||||
editableColumn.subtype
|
editableColumn.subtype,
|
||||||
|
editableColumn.autocolumn
|
||||||
)
|
)
|
||||||
|
|
||||||
allowedTypes = getAllowedTypes().map(t => ({
|
allowedTypes = getAllowedTypes().map(t => ({
|
||||||
|
@ -419,7 +425,7 @@
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
FIELDS.JSON,
|
FIELDS.JSON,
|
||||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
isUsers ? FIELDS.USERS : FIELDS.USER,
|
||||||
{ name: "Auto Column", type: AUTO_TYPE },
|
FIELDS.AUTO,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
let fields = [
|
let fields = [
|
||||||
|
@ -538,7 +544,7 @@
|
||||||
getOptionValue={field => field.fieldId}
|
getOptionValue={field => field.fieldId}
|
||||||
getOptionIcon={field => field.icon}
|
getOptionIcon={field => field.icon}
|
||||||
isOptionEnabled={option => {
|
isOptionEnabled={option => {
|
||||||
if (option.type == AUTO_TYPE) {
|
if (option.type === AUTO_TYPE) {
|
||||||
return availableAutoColumnKeys?.length > 0
|
return availableAutoColumnKeys?.length > 0
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { RelationshipErrorChecker } from "./relationshipErrors"
|
import { RelationshipErrorChecker } from "./relationshipErrors"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
|
import { PrettyRelationshipDefinitions } from "constants/backend"
|
||||||
|
|
||||||
export let save
|
export let save
|
||||||
export let datasource
|
export let datasource
|
||||||
|
@ -22,16 +24,21 @@
|
||||||
export let selectedFromTable
|
export let selectedFromTable
|
||||||
export let close
|
export let close
|
||||||
|
|
||||||
const relationshipTypes = [
|
let relationshipMap = {
|
||||||
{
|
[RelationshipType.MANY_TO_MANY]: {
|
||||||
label: "One to Many",
|
part1: PrettyRelationshipDefinitions.MANY,
|
||||||
value: RelationshipType.MANY_TO_ONE,
|
part2: PrettyRelationshipDefinitions.MANY,
|
||||||
},
|
},
|
||||||
{
|
[RelationshipType.MANY_TO_ONE]: {
|
||||||
label: "Many to Many",
|
part1: PrettyRelationshipDefinitions.ONE,
|
||||||
value: RelationshipType.MANY_TO_MANY,
|
part2: PrettyRelationshipDefinitions.MANY,
|
||||||
},
|
},
|
||||||
]
|
}
|
||||||
|
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
|
||||||
|
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
|
||||||
|
|
||||||
|
let relationshipPart1 = PrettyRelationshipDefinitions.MANY
|
||||||
|
let relationshipPart2 = PrettyRelationshipDefinitions.ONE
|
||||||
|
|
||||||
let originalFromColumnName = toRelationship.name,
|
let originalFromColumnName = toRelationship.name,
|
||||||
originalToColumnName = fromRelationship.name
|
originalToColumnName = fromRelationship.name
|
||||||
|
@ -49,14 +56,32 @@
|
||||||
)
|
)
|
||||||
let errors = {}
|
let errors = {}
|
||||||
let fromPrimary, fromForeign, fromColumn, toColumn
|
let fromPrimary, fromForeign, fromColumn, toColumn
|
||||||
let fromId, toId, throughId, throughToKey, throughFromKey
|
|
||||||
|
let throughId, throughToKey, throughFromKey
|
||||||
let isManyToMany, isManyToOne, relationshipType
|
let isManyToMany, isManyToOne, relationshipType
|
||||||
let hasValidated = false
|
let hasValidated = false
|
||||||
|
|
||||||
|
$: fromId = null
|
||||||
|
$: toId = null
|
||||||
|
|
||||||
$: tableOptions = plusTables.map(table => ({
|
$: tableOptions = plusTables.map(table => ({
|
||||||
label: table.name,
|
label: table.name,
|
||||||
value: table._id,
|
value: table._id,
|
||||||
|
name: table.name,
|
||||||
|
_id: table._id,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Determine the relationship type based on the selected values of both parts
|
||||||
|
relationshipType = Object.entries(relationshipMap).find(
|
||||||
|
([_, parts]) =>
|
||||||
|
parts.part1 === relationshipPart1 && parts.part2 === relationshipPart2
|
||||||
|
)?.[0]
|
||||||
|
|
||||||
|
changed(() => {
|
||||||
|
hasValidated = false
|
||||||
|
})
|
||||||
|
}
|
||||||
$: valid =
|
$: valid =
|
||||||
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
|
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
|
||||||
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||||
|
@ -338,33 +363,34 @@
|
||||||
onConfirm={saveRelationship}
|
onConfirm={saveRelationship}
|
||||||
disabled={!valid}
|
disabled={!valid}
|
||||||
>
|
>
|
||||||
<Select
|
|
||||||
label="Relationship type"
|
|
||||||
options={relationshipTypes}
|
|
||||||
bind:value={relationshipType}
|
|
||||||
bind:error={errors.relationshipType}
|
|
||||||
on:change={() =>
|
|
||||||
changed(() => {
|
|
||||||
hasValidated = false
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<div class="headings">
|
<div class="headings">
|
||||||
<Detail>Tables</Detail>
|
<Detail>Tables</Detail>
|
||||||
</div>
|
</div>
|
||||||
{#if !selectedFromTable}
|
|
||||||
<Select
|
<RelationshipSelector
|
||||||
label="Select from table"
|
bind:relationshipPart1
|
||||||
options={tableOptions}
|
bind:relationshipPart2
|
||||||
bind:value={fromId}
|
bind:relationshipTableIdPrimary={fromId}
|
||||||
bind:error={errors.fromTable}
|
bind:relationshipTableIdSecondary={toId}
|
||||||
on:change={e =>
|
{relationshipOpts1}
|
||||||
changed(() => {
|
{relationshipOpts2}
|
||||||
const table = plusTables.find(tbl => tbl._id === e.detail)
|
{tableOptions}
|
||||||
fromColumn = table?.name || ""
|
{errors}
|
||||||
fromPrimary = table?.primary?.[0]
|
primaryDisabled={selectedFromTable}
|
||||||
})}
|
primaryTableChanged={e =>
|
||||||
/>
|
changed(() => {
|
||||||
{/if}
|
const table = plusTables.find(tbl => tbl._id === e.detail)
|
||||||
|
fromColumn = table?.name || ""
|
||||||
|
fromPrimary = table?.primary?.[0]
|
||||||
|
})}
|
||||||
|
secondaryTableChanged={e =>
|
||||||
|
changed(() => {
|
||||||
|
const table = plusTables.find(tbl => tbl._id === e.detail)
|
||||||
|
toColumn = table.name || ""
|
||||||
|
fromForeign = null
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if isManyToOne && fromId}
|
{#if isManyToOne && fromId}
|
||||||
<Select
|
<Select
|
||||||
label={`Primary Key (${getTable(fromId).name})`}
|
label={`Primary Key (${getTable(fromId).name})`}
|
||||||
|
@ -374,18 +400,6 @@
|
||||||
on:change={changed}
|
on:change={changed}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Select
|
|
||||||
label={"Select to table"}
|
|
||||||
options={tableOptions}
|
|
||||||
bind:value={toId}
|
|
||||||
bind:error={errors.toTable}
|
|
||||||
on:change={e =>
|
|
||||||
changed(() => {
|
|
||||||
const table = plusTables.find(tbl => tbl._id === e.detail)
|
|
||||||
toColumn = table.name || ""
|
|
||||||
fromForeign = null
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{#if isManyToMany}
|
{#if isManyToMany}
|
||||||
<Select
|
<Select
|
||||||
label={"Through"}
|
label={"Through"}
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
{#if $store.error}
|
{#if $store.error}
|
||||||
<InlineAlert
|
<InlineAlert
|
||||||
type="error"
|
type="error"
|
||||||
header={$store.error.title}
|
header="Error fetching {tableType}"
|
||||||
message={$store.error.description}
|
message={$store.error.description}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { derived, writable, get } from "svelte/store"
|
import { derived, writable, get } from "svelte/store"
|
||||||
import { keepOpen, notifications } from "@budibase/bbui"
|
import { keepOpen, notifications } from "@budibase/bbui"
|
||||||
import { datasources, ImportTableError, tables } from "stores/backend"
|
import { datasources, tables } from "stores/backend"
|
||||||
|
|
||||||
export const createTableSelectionStore = (integration, datasource) => {
|
export const createTableSelectionStore = (integration, datasource) => {
|
||||||
const tableNamesStore = writable([])
|
const tableNamesStore = writable([])
|
||||||
|
@ -30,12 +30,7 @@ export const createTableSelectionStore = (integration, datasource) => {
|
||||||
notifications.success(`Tables fetched successfully.`)
|
notifications.success(`Tables fetched successfully.`)
|
||||||
await onComplete()
|
await onComplete()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ImportTableError) {
|
errorStore.set(err)
|
||||||
errorStore.set(err)
|
|
||||||
} else {
|
|
||||||
notifications.error("Error fetching tables.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return keepOpen
|
return keepOpen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,14 @@
|
||||||
export let relationshipTableIdPrimary
|
export let relationshipTableIdPrimary
|
||||||
export let relationshipTableIdSecondary
|
export let relationshipTableIdSecondary
|
||||||
export let editableColumn
|
export let editableColumn
|
||||||
export let linkEditDisabled
|
export let linkEditDisabled = false
|
||||||
export let tableOptions
|
export let tableOptions
|
||||||
export let errors
|
export let errors
|
||||||
export let relationshipOpts1
|
export let relationshipOpts1
|
||||||
export let relationshipOpts2
|
export let relationshipOpts2
|
||||||
|
export let primaryTableChanged
|
||||||
|
export let secondaryTableChanged
|
||||||
|
export let primaryDisabled = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relationship-container">
|
<div class="relationship-container">
|
||||||
|
@ -19,16 +22,19 @@
|
||||||
disabled={linkEditDisabled}
|
disabled={linkEditDisabled}
|
||||||
bind:value={relationshipPart1}
|
bind:value={relationshipPart1}
|
||||||
options={relationshipOpts1}
|
options={relationshipOpts1}
|
||||||
|
bind:error={errors.relationshipType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="relationship-label">in</div>
|
<div class="relationship-label">in</div>
|
||||||
<div class="relationship-part">
|
<div class="relationship-part">
|
||||||
<Select
|
<Select
|
||||||
disabled
|
disabled={primaryDisabled}
|
||||||
options={tableOptions}
|
options={tableOptions}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
bind:value={relationshipTableIdPrimary}
|
bind:value={relationshipTableIdPrimary}
|
||||||
|
on:change={primaryTableChanged}
|
||||||
|
bind:error={errors.fromTable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,20 +52,24 @@
|
||||||
<Select
|
<Select
|
||||||
disabled={linkEditDisabled}
|
disabled={linkEditDisabled}
|
||||||
bind:value={relationshipTableIdSecondary}
|
bind:value={relationshipTableIdSecondary}
|
||||||
|
bind:error={errors.toTable}
|
||||||
options={tableOptions.filter(
|
options={tableOptions.filter(
|
||||||
table => table._id !== relationshipTableIdPrimary
|
table => table._id !== relationshipTableIdPrimary
|
||||||
)}
|
)}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
|
on:change={secondaryTableChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
{#if editableColumn}
|
||||||
disabled={linkEditDisabled}
|
<Input
|
||||||
label={`Column name in other table`}
|
disabled={linkEditDisabled}
|
||||||
bind:value={editableColumn.fieldName}
|
label={`Column name in other table`}
|
||||||
error={errors.relatedName}
|
bind:value={editableColumn.fieldName}
|
||||||
/>
|
error={errors.relatedName}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.relationship-container {
|
.relationship-container {
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import ColumnDrawer from "./ColumnEditor/ColumnDrawer.svelte"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
import {
|
|
||||||
getDatasourceForProvider,
|
|
||||||
getSchemaForDatasource,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { currentAsset } from "builderStore"
|
|
||||||
import { getFields } from "helpers/searchFields"
|
|
||||||
|
|
||||||
export let componentInstance
|
|
||||||
export let value = []
|
|
||||||
export let allowCellEditing = true
|
|
||||||
export let subject = "Table"
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
let drawer
|
|
||||||
let boundValue
|
|
||||||
|
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
|
||||||
$: schema = getSchema($currentAsset, datasource)
|
|
||||||
$: options = allowCellEditing
|
|
||||||
? Object.keys(schema || {})
|
|
||||||
: enrichedSchemaFields?.map(field => field.name)
|
|
||||||
$: sanitisedValue = getValidColumns(value, options)
|
|
||||||
$: updateBoundValue(sanitisedValue)
|
|
||||||
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
|
|
||||||
allowLinks: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getSchema = (asset, datasource) => {
|
|
||||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
|
||||||
|
|
||||||
// Don't show ID and rev in tables
|
|
||||||
if (schema) {
|
|
||||||
delete schema._id
|
|
||||||
delete schema._rev
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateBoundValue = value => {
|
|
||||||
boundValue = cloneDeep(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValidColumns = (columns, options) => {
|
|
||||||
if (!Array.isArray(columns) || !columns.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
// We need to account for legacy configs which would just be an array
|
|
||||||
// of strings
|
|
||||||
if (typeof columns[0] === "string") {
|
|
||||||
columns = columns.map(col => ({
|
|
||||||
name: col,
|
|
||||||
displayName: col,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return columns.filter(column => {
|
|
||||||
return options.includes(column.name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
updateBoundValue(sanitisedValue)
|
|
||||||
drawer.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = () => {
|
|
||||||
dispatch("change", getValidColumns(boundValue, options))
|
|
||||||
drawer.hide()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
|
||||||
<Drawer bind:this={drawer} title="{subject} Columns">
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Configure the columns in your {subject.toLowerCase()}.
|
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
|
||||||
<ColumnDrawer
|
|
||||||
slot="body"
|
|
||||||
bind:columns={boundValue}
|
|
||||||
{options}
|
|
||||||
{schema}
|
|
||||||
{allowCellEditing}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
|
|
@ -54,6 +54,7 @@
|
||||||
label="App export"
|
label="App export"
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
file = e.detail?.[0]
|
file = e.detail?.[0]
|
||||||
|
encrypted = file?.name?.endsWith(".enc.tar.gz")
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Toggle text="Encrypted" bind:value={encrypted} />
|
<Toggle text="Encrypted" bind:value={encrypted} />
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
import { FieldType, FieldSubtype } from "@budibase/types"
|
import { FieldType, FieldSubtype } from "@budibase/types"
|
||||||
|
|
||||||
|
export const AUTO_COLUMN_SUB_TYPES = {
|
||||||
|
AUTO_ID: "autoID",
|
||||||
|
CREATED_BY: "createdBy",
|
||||||
|
CREATED_AT: "createdAt",
|
||||||
|
UPDATED_BY: "updatedBy",
|
||||||
|
UPDATED_AT: "updatedAt",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AUTO_COLUMN_DISPLAY_NAMES = {
|
||||||
|
AUTO_ID: "Auto ID",
|
||||||
|
CREATED_BY: "Created By",
|
||||||
|
CREATED_AT: "Created At",
|
||||||
|
UPDATED_BY: "Updated By",
|
||||||
|
UPDATED_AT: "Updated At",
|
||||||
|
}
|
||||||
|
|
||||||
export const FIELDS = {
|
export const FIELDS = {
|
||||||
STRING: {
|
STRING: {
|
||||||
name: "Text",
|
name: "Text",
|
||||||
|
@ -107,6 +123,12 @@ export const FIELDS = {
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
AUTO: {
|
||||||
|
name: "Auto Column",
|
||||||
|
type: FieldType.AUTO,
|
||||||
|
icon: "MagicWand",
|
||||||
|
constraints: {},
|
||||||
|
},
|
||||||
FORMULA: {
|
FORMULA: {
|
||||||
name: "Formula",
|
name: "Formula",
|
||||||
type: FieldType.FORMULA,
|
type: FieldType.FORMULA,
|
||||||
|
@ -139,22 +161,6 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AUTO_COLUMN_SUB_TYPES = {
|
|
||||||
AUTO_ID: "autoID",
|
|
||||||
CREATED_BY: "createdBy",
|
|
||||||
CREATED_AT: "createdAt",
|
|
||||||
UPDATED_BY: "updatedBy",
|
|
||||||
UPDATED_AT: "updatedAt",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AUTO_COLUMN_DISPLAY_NAMES = {
|
|
||||||
AUTO_ID: "Auto ID",
|
|
||||||
CREATED_BY: "Created By",
|
|
||||||
CREATED_AT: "Created At",
|
|
||||||
UPDATED_BY: "Updated By",
|
|
||||||
UPDATED_AT: "Updated At",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FILE_TYPES = {
|
export const FILE_TYPES = {
|
||||||
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
|
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
|
||||||
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
|
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
|
||||||
|
|
|
@ -62,7 +62,14 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
<Body>{getSubtitle(datasource)}</Body>
|
<Body>
|
||||||
|
{@const subtitle = getSubtitle(datasource)}
|
||||||
|
{#if subtitle}
|
||||||
|
{subtitle}
|
||||||
|
{:else}
|
||||||
|
{Object.values(datasource.config).join(" / ")}
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
|
|
@ -23,5 +23,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key $params.datasourceId}
|
{#key $params.datasourceId}
|
||||||
<slot />
|
{#if $datasources.selected}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -16,8 +16,7 @@
|
||||||
let selectedPanel = null
|
let selectedPanel = null
|
||||||
let panelOptions = []
|
let panelOptions = []
|
||||||
|
|
||||||
// datasources.selected can return null temporarily on datasource deletion
|
$: datasource = $datasources.selected
|
||||||
$: datasource = $datasources.selected || {}
|
|
||||||
|
|
||||||
$: getOptions(datasource)
|
$: getOptions(datasource)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||||
import ImportAppModal from "components/start/ImportAppModal.svelte"
|
import ImportAppModal from "components/start/ImportAppModal.svelte"
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(app => app.devId == $store.appId)
|
$: filteredApps = $apps.filter(app => app.devId === $store.appId)
|
||||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TestimonialPage>
|
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<span class="heading-wrap">
|
<span class="heading-wrap">
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TestimonialPage>
|
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
|
|
|
@ -9,15 +9,19 @@ import { API } from "api"
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
|
|
||||||
export class ImportTableError extends Error {
|
class TableImportError extends Error {
|
||||||
constructor(message) {
|
constructor(errors) {
|
||||||
super(message)
|
super()
|
||||||
const [title, description] = message.split(" - ")
|
this.name = "TableImportError"
|
||||||
|
this.errors = errors
|
||||||
|
}
|
||||||
|
|
||||||
this.name = "TableSelectionError"
|
get description() {
|
||||||
// Capitalize the first character of both the title and description
|
let message = ""
|
||||||
this.title = title[0].toUpperCase() + title.substr(1)
|
for (const key in this.errors) {
|
||||||
this.description = description[0].toUpperCase() + description.substr(1)
|
message += `${key}: ${this.errors[key]}\n`
|
||||||
|
}
|
||||||
|
return message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +29,6 @@ export function createDatasourcesStore() {
|
||||||
const store = writable({
|
const store = writable({
|
||||||
list: [],
|
list: [],
|
||||||
selectedDatasourceId: null,
|
selectedDatasourceId: null,
|
||||||
schemaError: null,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const derivedStore = derived([store, tables], ([$store, $tables]) => {
|
const derivedStore = derived([store, tables], ([$store, $tables]) => {
|
||||||
|
@ -75,18 +78,13 @@ export function createDatasourcesStore() {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
selectedDatasourceId: id,
|
selectedDatasourceId: id,
|
||||||
// Remove any possible schema error
|
|
||||||
schemaError: null,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDatasource = response => {
|
const updateDatasource = response => {
|
||||||
const { datasource, error } = response
|
const { datasource, errors } = response
|
||||||
if (error) {
|
if (errors && Object.keys(errors).length > 0) {
|
||||||
store.update(state => ({
|
throw new TableImportError(errors)
|
||||||
...state,
|
|
||||||
schemaError: error,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
replaceDatasource(datasource._id, datasource)
|
replaceDatasource(datasource._id, datasource)
|
||||||
select(datasource._id)
|
select(datasource._id)
|
||||||
|
@ -94,20 +92,11 @@ export function createDatasourcesStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSchema = async (datasource, tablesFilter) => {
|
const updateSchema = async (datasource, tablesFilter) => {
|
||||||
try {
|
const response = await API.buildDatasourceSchema({
|
||||||
const response = await API.buildDatasourceSchema({
|
datasourceId: datasource?._id,
|
||||||
datasourceId: datasource?._id,
|
tablesFilter,
|
||||||
tablesFilter,
|
})
|
||||||
})
|
updateDatasource(response)
|
||||||
updateDatasource(response)
|
|
||||||
} catch (e) {
|
|
||||||
// buildDatasourceSchema call returns user presentable errors with two parts divided with a " - ".
|
|
||||||
if (e.message.split(" - ").length === 2) {
|
|
||||||
throw new ImportTableError(e.message)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceCount = source => {
|
const sourceCount = source => {
|
||||||
|
@ -136,6 +125,7 @@ export function createDatasourcesStore() {
|
||||||
config,
|
config,
|
||||||
name: `${integration.friendlyName}${nameModifier}`,
|
name: `${integration.friendlyName}${nameModifier}`,
|
||||||
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||||
|
isSQL: integration.isSQL,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await checkDatasourceValidity(integration, datasource)) {
|
if (await checkDatasourceValidity(integration, datasource)) {
|
||||||
|
@ -171,12 +161,6 @@ export function createDatasourcesStore() {
|
||||||
replaceDatasource(datasource._id, null)
|
replaceDatasource(datasource._id, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSchemaError = () => {
|
|
||||||
store.update(state => {
|
|
||||||
return { ...state, schemaError: null }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const replaceDatasource = (datasourceId, datasource) => {
|
const replaceDatasource = (datasourceId, datasource) => {
|
||||||
if (!datasourceId) {
|
if (!datasourceId) {
|
||||||
return
|
return
|
||||||
|
@ -229,7 +213,6 @@ export function createDatasourcesStore() {
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
delete: deleteDatasource,
|
delete: deleteDatasource,
|
||||||
removeSchemaError,
|
|
||||||
replaceDatasource,
|
replaceDatasource,
|
||||||
getTableNames,
|
getTableNames,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ export { views } from "./views"
|
||||||
export { viewsV2 } from "./viewsV2"
|
export { viewsV2 } from "./viewsV2"
|
||||||
export { permissions } from "./permissions"
|
export { permissions } from "./permissions"
|
||||||
export { roles } from "./roles"
|
export { roles } from "./roles"
|
||||||
export { datasources, ImportTableError } from "./datasources"
|
export { datasources } from "./datasources"
|
||||||
export { integrations } from "./integrations"
|
export { integrations } from "./integrations"
|
||||||
export { sortedIntegrations } from "./sortedIntegrations"
|
export { sortedIntegrations } from "./sortedIntegrations"
|
||||||
export { queries } from "./queries"
|
export { queries } from "./queries"
|
||||||
|
|
|
@ -80,7 +80,6 @@ export default defineConfig(({ mode }) => {
|
||||||
"process.env.INTERCOM_TOKEN": JSON.stringify(
|
"process.env.INTERCOM_TOKEN": JSON.stringify(
|
||||||
process.env.INTERCOM_TOKEN
|
process.env.INTERCOM_TOKEN
|
||||||
),
|
),
|
||||||
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
|
|
||||||
}),
|
}),
|
||||||
copyFonts("fonts"),
|
copyFonts("fonts"),
|
||||||
...(isProduction ? [] : devOnlyPlugins),
|
...(isProduction ? [] : devOnlyPlugins),
|
||||||
|
|
|
@ -5609,6 +5609,21 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On row click",
|
||||||
|
"key": "onRowClick",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Clicked row",
|
||||||
|
"key": "row"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "allowEditRows",
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Add rows",
|
"label": "Add rows",
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const API = createAPIClient({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Show an error notification for all API failures.
|
// Show an error notification for all API failures.
|
||||||
// We could also log these to sentry.
|
// We could also log these to Posthog.
|
||||||
// Or we could check error.status and redirect to login on a 403 etc.
|
// Or we could check error.status and redirect to login on a 403 etc.
|
||||||
onError: error => {
|
onError: error => {
|
||||||
const { status, method, url, message, handled, suppressErrors } =
|
const { status, method, url, message, handled, suppressErrors } =
|
||||||
|
|
|
@ -14,12 +14,14 @@
|
||||||
export let initialSortOrder = null
|
export let initialSortOrder = null
|
||||||
export let fixedRowHeight = null
|
export let fixedRowHeight = null
|
||||||
export let columns = null
|
export let columns = null
|
||||||
|
export let onRowClick = null
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, API, builderStore, notificationStore } = getContext("sdk")
|
const { styleable, API, builderStore, notificationStore } = getContext("sdk")
|
||||||
|
|
||||||
$: columnWhitelist = columns?.map(col => col.name)
|
$: columnWhitelist = columns?.map(col => col.name)
|
||||||
$: schemaOverrides = getSchemaOverrides(columns)
|
$: schemaOverrides = getSchemaOverrides(columns)
|
||||||
|
$: handleRowClick = allowEditRows ? undefined : onRowClick
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = columns => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
|
@ -56,6 +58,7 @@
|
||||||
showControls={false}
|
showControls={false}
|
||||||
notifySuccess={notificationStore.actions.success}
|
notifySuccess={notificationStore.actions.success}
|
||||||
notifyError={notificationStore.actions.error}
|
notifyError={notificationStore.actions.error}
|
||||||
|
on:rowclick={e => handleRowClick?.({ row: e.detail })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,24 @@
|
||||||
const { config, dispatch, selectedRows } = getContext("grid")
|
const { config, dispatch, selectedRows } = getContext("grid")
|
||||||
const svelteDispatch = createEventDispatcher()
|
const svelteDispatch = createEventDispatcher()
|
||||||
|
|
||||||
const select = () => {
|
const select = e => {
|
||||||
|
e.stopPropagation()
|
||||||
svelteDispatch("select")
|
svelteDispatch("select")
|
||||||
const id = row?._id
|
const id = row?._id
|
||||||
if (id) {
|
if (id) {
|
||||||
selectedRows.actions.toggleRow(id)
|
selectedRows.actions.toggleRow(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bulkDelete = e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
dispatch("request-bulk-delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
const expand = e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
svelteDispatch("expand")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<GridCell
|
<GridCell
|
||||||
|
@ -56,7 +67,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if rowSelected && $config.canDeleteRows}
|
{#if rowSelected && $config.canDeleteRows}
|
||||||
<div class="delete" on:click={() => dispatch("request-bulk-delete")}>
|
<div class="delete" on:click={bulkDelete}>
|
||||||
<Icon
|
<Icon
|
||||||
name="Delete"
|
name="Delete"
|
||||||
size="S"
|
size="S"
|
||||||
|
@ -65,12 +76,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="expand" class:visible={$config.canExpandRows && expandable}>
|
<div class="expand" class:visible={$config.canExpandRows && expandable}>
|
||||||
<Icon
|
<Icon size="S" name="Maximize" hoverable on:click={expand} />
|
||||||
size="S"
|
|
||||||
name="Maximize"
|
|
||||||
hoverable
|
|
||||||
on:click={() => svelteDispatch("expand")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -260,29 +260,31 @@
|
||||||
class:wrap={editable || contentLines > 1}
|
class:wrap={editable || contentLines > 1}
|
||||||
on:wheel={e => (focused ? e.stopPropagation() : null)}
|
on:wheel={e => (focused ? e.stopPropagation() : null)}
|
||||||
>
|
>
|
||||||
{#each value || [] as relationship}
|
{#if Array.isArray(value) && value.length}
|
||||||
{#if relationship[primaryDisplay] || relationship.primaryDisplay}
|
{#each value as relationship}
|
||||||
<div class="badge">
|
{#if relationship[primaryDisplay] || relationship.primaryDisplay}
|
||||||
<span
|
<div class="badge">
|
||||||
on:click={editable
|
<span
|
||||||
? () => showRelationship(relationship._id)
|
on:click={editable
|
||||||
: null}
|
? () => showRelationship(relationship._id)
|
||||||
>
|
: null}
|
||||||
{readable(
|
>
|
||||||
relationship[primaryDisplay] || relationship.primaryDisplay
|
{readable(
|
||||||
)}
|
relationship[primaryDisplay] || relationship.primaryDisplay
|
||||||
</span>
|
)}
|
||||||
{#if editable}
|
</span>
|
||||||
<Icon
|
{#if editable}
|
||||||
name="Close"
|
<Icon
|
||||||
size="XS"
|
name="Close"
|
||||||
hoverable
|
size="XS"
|
||||||
on:click={() => toggleRow(relationship)}
|
hoverable
|
||||||
/>
|
on:click={() => toggleRow(relationship)}
|
||||||
{/if}
|
/>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
{/each}
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
{#if editable}
|
{#if editable}
|
||||||
<div class="add" on:click={open}>
|
<div class="add" on:click={open}>
|
||||||
<Icon name="Add" size="S" />
|
<Icon name="Add" size="S" />
|
||||||
|
@ -318,7 +320,7 @@
|
||||||
<div class="searching">
|
<div class="searching">
|
||||||
<ProgressCircle size="S" />
|
<ProgressCircle size="S" />
|
||||||
</div>
|
</div>
|
||||||
{:else if searchResults?.length}
|
{:else if Array.isArray(searchResults) && searchResults.length}
|
||||||
<div class="results">
|
<div class="results">
|
||||||
{#each searchResults as row, idx}
|
{#each searchResults as row, idx}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={body} class="grid-body">
|
<div bind:this={body} class="grid-body">
|
||||||
<GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
|
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
<GridRow
|
<GridRow
|
||||||
{row}
|
{row}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
columnHorizontalInversionIndex,
|
columnHorizontalInversionIndex,
|
||||||
contentLines,
|
contentLines,
|
||||||
isDragging,
|
isDragging,
|
||||||
|
dispatch,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: rowSelected = !!$selectedRows[row._id]
|
$: rowSelected = !!$selectedRows[row._id]
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
on:focus
|
on:focus
|
||||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
|
on:click={() => dispatch("rowclick", row)}
|
||||||
>
|
>
|
||||||
{#each $renderedColumns as column, columnIdx (column.name)}
|
{#each $renderedColumns as column, columnIdx (column.name)}
|
||||||
{@const cellId = `${row._id}-${column.name}`}
|
{@const cellId = `${row._id}-${column.name}`}
|
||||||
|
|
|
@ -17,7 +17,11 @@
|
||||||
|
|
||||||
export let scrollVertically = false
|
export let scrollVertically = false
|
||||||
export let scrollHorizontally = false
|
export let scrollHorizontally = false
|
||||||
export let wheelInteractive = false
|
export let attachHandlers = false
|
||||||
|
|
||||||
|
// Used for tracking touch events
|
||||||
|
let initialTouchX
|
||||||
|
let initialTouchY
|
||||||
|
|
||||||
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
|
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
|
||||||
|
|
||||||
|
@ -27,17 +31,47 @@
|
||||||
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles a wheel even and updates the scroll offsets
|
// Handles a mouse wheel event and updates scroll state
|
||||||
const handleWheel = e => {
|
const handleWheel = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
updateScroll(e.deltaX, e.deltaY, e.clientY)
|
||||||
|
|
||||||
// If a context menu was visible, hide it
|
// If a context menu was visible, hide it
|
||||||
if ($menu.visible) {
|
if ($menu.visible) {
|
||||||
menu.actions.close()
|
menu.actions.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
|
||||||
|
// Handles touch start events
|
||||||
|
const handleTouchStart = e => {
|
||||||
|
if (!e.touches?.[0]) return
|
||||||
|
initialTouchX = e.touches[0].clientX
|
||||||
|
initialTouchY = e.touches[0].clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles touch move events and updates scroll state
|
||||||
|
const handleTouchMove = e => {
|
||||||
|
if (!e.touches?.[0]) return
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Compute delta from previous event, and update scroll
|
||||||
|
const deltaX = initialTouchX - e.touches[0].clientX
|
||||||
|
const deltaY = initialTouchY - e.touches[0].clientY
|
||||||
|
updateScroll(deltaX, deltaY)
|
||||||
|
|
||||||
|
// Store position to reference in next event
|
||||||
|
initialTouchX = e.touches[0].clientX
|
||||||
|
initialTouchY = e.touches[0].clientY
|
||||||
|
|
||||||
|
// If a context menu was visible, hide it
|
||||||
|
if ($menu.visible) {
|
||||||
|
menu.actions.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the scroll offset by a certain delta, and ensure scrolling
|
||||||
|
// stays within sensible bounds. Debounced for performance.
|
||||||
|
const updateScroll = domDebounce((deltaX, deltaY, clientY) => {
|
||||||
const { top, left } = $scroll
|
const { top, left } = $scroll
|
||||||
|
|
||||||
// Calculate new scroll top
|
// Calculate new scroll top
|
||||||
|
@ -55,15 +89,19 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hover row under cursor
|
// Hover row under cursor
|
||||||
const y = clientY - $bounds.top + (newScrollTop % $rowHeight)
|
if (clientY != null) {
|
||||||
const hoveredRow = $renderedRows[Math.floor(y / $rowHeight)]
|
const y = clientY - $bounds.top + (newScrollTop % $rowHeight)
|
||||||
hoveredRowId.set(hoveredRow?._id)
|
const hoveredRow = $renderedRows[Math.floor(y / $rowHeight)]
|
||||||
|
hoveredRowId.set(hoveredRow?._id)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="outer"
|
class="outer"
|
||||||
on:wheel={wheelInteractive ? handleWheel : null}
|
on:wheel={attachHandlers ? handleWheel : null}
|
||||||
|
on:touchstart={attachHandlers ? handleTouchStart : null}
|
||||||
|
on:touchmove={attachHandlers ? handleTouchMove : null}
|
||||||
on:click|self={() => ($focusedCellId = null)}
|
on:click|self={() => ($focusedCellId = null)}
|
||||||
>
|
>
|
||||||
<div {style} class="inner">
|
<div {style} class="inner">
|
||||||
|
|
|
@ -205,7 +205,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||||
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#each $renderedColumns as column, columnIdx}
|
{#each $renderedColumns as column, columnIdx}
|
||||||
{@const cellId = `new-${column.name}`}
|
{@const cellId = `new-${column.name}`}
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
|
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
|
||||||
<GridScrollWrapper scrollVertically wheelInteractive>
|
<GridScrollWrapper scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
{@const rowSelected = !!$selectedRows[row._id]}
|
||||||
{@const rowHovered = $hoveredRowId === row._id}
|
{@const rowHovered = $hoveredRowId === row._id}
|
||||||
|
@ -74,6 +74,7 @@
|
||||||
class="row"
|
class="row"
|
||||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
|
on:click={() => dispatch("rowclick", row)}
|
||||||
>
|
>
|
||||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
|
|
|
@ -53,18 +53,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getLocation = e => {
|
||||||
|
return {
|
||||||
|
y: e.touches?.[0]?.clientY ?? e.clientY,
|
||||||
|
x: e.touches?.[0]?.clientX ?? e.clientX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// V scrollbar drag handlers
|
// V scrollbar drag handlers
|
||||||
const startVDragging = e => {
|
const startVDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
initialMouse = e.clientY
|
initialMouse = getLocation(e).y
|
||||||
initialScroll = $scrollTop
|
initialScroll = $scrollTop
|
||||||
document.addEventListener("mousemove", moveVDragging)
|
document.addEventListener("mousemove", moveVDragging)
|
||||||
|
document.addEventListener("touchmove", moveVDragging)
|
||||||
document.addEventListener("mouseup", stopVDragging)
|
document.addEventListener("mouseup", stopVDragging)
|
||||||
|
document.addEventListener("touchend", stopVDragging)
|
||||||
isDraggingV = true
|
isDraggingV = true
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
const moveVDragging = domDebounce(e => {
|
const moveVDragging = domDebounce(e => {
|
||||||
const delta = e.clientY - initialMouse
|
const delta = getLocation(e).y - initialMouse
|
||||||
const weight = delta / availHeight
|
const weight = delta / availHeight
|
||||||
const newScrollTop = initialScroll + weight * $maxScrollTop
|
const newScrollTop = initialScroll + weight * $maxScrollTop
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
|
@ -74,22 +83,26 @@
|
||||||
})
|
})
|
||||||
const stopVDragging = () => {
|
const stopVDragging = () => {
|
||||||
document.removeEventListener("mousemove", moveVDragging)
|
document.removeEventListener("mousemove", moveVDragging)
|
||||||
|
document.removeEventListener("touchmove", moveVDragging)
|
||||||
document.removeEventListener("mouseup", stopVDragging)
|
document.removeEventListener("mouseup", stopVDragging)
|
||||||
|
document.removeEventListener("touchend", stopVDragging)
|
||||||
isDraggingV = false
|
isDraggingV = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// H scrollbar drag handlers
|
// H scrollbar drag handlers
|
||||||
const startHDragging = e => {
|
const startHDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
initialMouse = e.clientX
|
initialMouse = getLocation(e).x
|
||||||
initialScroll = $scrollLeft
|
initialScroll = $scrollLeft
|
||||||
document.addEventListener("mousemove", moveHDragging)
|
document.addEventListener("mousemove", moveHDragging)
|
||||||
|
document.addEventListener("touchmove", moveHDragging)
|
||||||
document.addEventListener("mouseup", stopHDragging)
|
document.addEventListener("mouseup", stopHDragging)
|
||||||
|
document.addEventListener("touchend", stopHDragging)
|
||||||
isDraggingH = true
|
isDraggingH = true
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
const moveHDragging = domDebounce(e => {
|
const moveHDragging = domDebounce(e => {
|
||||||
const delta = e.clientX - initialMouse
|
const delta = getLocation(e).x - initialMouse
|
||||||
const weight = delta / availWidth
|
const weight = delta / availWidth
|
||||||
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
|
@ -99,7 +112,9 @@
|
||||||
})
|
})
|
||||||
const stopHDragging = () => {
|
const stopHDragging = () => {
|
||||||
document.removeEventListener("mousemove", moveHDragging)
|
document.removeEventListener("mousemove", moveHDragging)
|
||||||
|
document.removeEventListener("touchmove", moveHDragging)
|
||||||
document.removeEventListener("mouseup", stopHDragging)
|
document.removeEventListener("mouseup", stopHDragging)
|
||||||
|
document.removeEventListener("touchend", stopHDragging)
|
||||||
isDraggingH = false
|
isDraggingH = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -109,6 +124,7 @@
|
||||||
class="v-scrollbar"
|
class="v-scrollbar"
|
||||||
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
||||||
on:mousedown={startVDragging}
|
on:mousedown={startVDragging}
|
||||||
|
on:touchstart={startVDragging}
|
||||||
class:dragging={isDraggingV}
|
class:dragging={isDraggingV}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -117,6 +133,7 @@
|
||||||
class="h-scrollbar"
|
class="h-scrollbar"
|
||||||
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
||||||
on:mousedown={startHDragging}
|
on:mousedown={startHDragging}
|
||||||
|
on:touchstart={startHDragging}
|
||||||
class:dragging={isDraggingH}
|
class:dragging={isDraggingH}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const copiedCell = writable(null)
|
const copiedCell = writable(null)
|
||||||
|
@ -12,7 +13,16 @@ export const createActions = context => {
|
||||||
const { copiedCell, focusedCellAPI } = context
|
const { copiedCell, focusedCellAPI } = context
|
||||||
|
|
||||||
const copy = () => {
|
const copy = () => {
|
||||||
copiedCell.set(get(focusedCellAPI)?.getValue())
|
const value = get(focusedCellAPI)?.getValue()
|
||||||
|
copiedCell.set(value)
|
||||||
|
|
||||||
|
// Also copy a stringified version to the clipboard
|
||||||
|
let stringified = ""
|
||||||
|
if (value != null && value !== "") {
|
||||||
|
// Only conditionally stringify to avoid redundant quotes around text
|
||||||
|
stringified = typeof value === "object" ? JSON.stringify(value) : value
|
||||||
|
}
|
||||||
|
Helpers.copyToClipboard(stringified)
|
||||||
}
|
}
|
||||||
|
|
||||||
const paste = () => {
|
const paste = () => {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 044bec6447066b215932d6726c437e7ec5a9e42e
|
Subproject commit 570d14aa44aa88f4d053856322210f0008ba5c76
|
|
@ -11,15 +11,14 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf dist/",
|
"prebuild": "rimraf dist/",
|
||||||
"build": "node ./scripts/build.js",
|
"build": "node ./scripts/build.js",
|
||||||
|
"postbuild": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && copyfiles -f ../../yarn.lock ./dist/",
|
||||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||||
"postbuild": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
|
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
||||||
"test": "bash scripts/test.sh",
|
"test": "bash scripts/test.sh",
|
||||||
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
|
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && yarn build && cp ../../yarn.lock ./dist/",
|
"build:docker": "yarn build && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
||||||
"build:docker": "yarn predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
|
||||||
"run:docker": "node dist/index.js",
|
"run:docker": "node dist/index.js",
|
||||||
"run:docker:cluster": "pm2-runtime start pm2.config.js",
|
"run:docker:cluster": "pm2-runtime start pm2.config.js",
|
||||||
"dev:stack:up": "node scripts/dev/manage.js up",
|
"dev:stack:up": "node scripts/dev/manage.js up",
|
||||||
|
@ -54,9 +53,8 @@
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
"@google-cloud/firestore": "5.0.2",
|
"@google-cloud/firestore": "6.8.0",
|
||||||
"@koa/router": "8.0.8",
|
"@koa/router": "8.0.8",
|
||||||
"@sentry/node": "6.17.7",
|
|
||||||
"@socket.io/redis-adapter": "^8.2.1",
|
"@socket.io/redis-adapter": "^8.2.1",
|
||||||
"airtable": "0.10.1",
|
"airtable": "0.10.1",
|
||||||
"arangojs": "7.2.0",
|
"arangojs": "7.2.0",
|
||||||
|
@ -70,7 +68,6 @@
|
||||||
"curlconverter": "3.21.0",
|
"curlconverter": "3.21.0",
|
||||||
"dd-trace": "3.13.2",
|
"dd-trace": "3.13.2",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
"fix-path": "3.0.0",
|
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"global-agent": "3.0.0",
|
"global-agent": "3.0.0",
|
||||||
"google-auth-library": "7.12.0",
|
"google-auth-library": "7.12.0",
|
||||||
|
@ -96,12 +93,11 @@
|
||||||
"object-sizeof": "2.6.1",
|
"object-sizeof": "2.6.1",
|
||||||
"open": "8.4.0",
|
"open": "8.4.0",
|
||||||
"openai": "^3.2.1",
|
"openai": "^3.2.1",
|
||||||
|
"openapi-types": "9.3.1",
|
||||||
"pg": "8.10.0",
|
"pg": "8.10.0",
|
||||||
"posthog-node": "1.3.0",
|
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-all-dbs": "1.0.2",
|
"pouchdb-all-dbs": "1.1.1",
|
||||||
"pouchdb-find": "7.2.2",
|
"pouchdb-find": "7.2.2",
|
||||||
"pouchdb-replication-stream": "1.2.9",
|
|
||||||
"redis": "4",
|
"redis": "4",
|
||||||
"server-destroy": "1.0.1",
|
"server-destroy": "1.0.1",
|
||||||
"snowflake-promise": "^4.5.0",
|
"snowflake-promise": "^4.5.0",
|
||||||
|
@ -113,8 +109,7 @@
|
||||||
"validate.js": "0.13.1",
|
"validate.js": "0.13.1",
|
||||||
"vm2": "^3.9.19",
|
"vm2": "^3.9.19",
|
||||||
"worker-farm": "1.7.0",
|
"worker-farm": "1.7.0",
|
||||||
"xml2js": "0.5.0",
|
"xml2js": "0.5.0"
|
||||||
"yargs": "13.2.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.17.4",
|
"@babel/core": "7.17.4",
|
||||||
|
@ -144,7 +139,6 @@
|
||||||
"jest-runner": "29.6.2",
|
"jest-runner": "29.6.2",
|
||||||
"jest-serial-runner": "1.2.1",
|
"jest-serial-runner": "1.2.1",
|
||||||
"nodemon": "2.0.15",
|
"nodemon": "2.0.15",
|
||||||
"openapi-types": "9.3.1",
|
|
||||||
"openapi-typescript": "5.2.0",
|
"openapi-typescript": "5.2.0",
|
||||||
"path-to-regexp": "6.2.0",
|
"path-to-regexp": "6.2.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
|
@ -154,7 +148,8 @@
|
||||||
"ts-node": "10.8.1",
|
"ts-node": "10.8.1",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"update-dotenv": "1.1.1"
|
"update-dotenv": "1.1.1",
|
||||||
|
"yargs": "13.2.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"oracledb": "5.3.0"
|
"oracledb": "5.3.0"
|
||||||
|
@ -171,6 +166,22 @@
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/builder",
|
||||||
|
"{projectRoot}/client",
|
||||||
|
"{projectRoot}/dist"
|
||||||
|
],
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/client",
|
||||||
|
"@budibase/builder"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
getTableParams,
|
getTableParams,
|
||||||
} from "../../db/utils"
|
} from "../../db/utils"
|
||||||
import { destroy as tableDestroy } from "./table/internal"
|
import { destroy as tableDestroy } from "./table/internal"
|
||||||
import { BuildSchemaErrors, InvalidColumns } from "../../constants"
|
|
||||||
import { getIntegration } from "../../integrations"
|
import { getIntegration } from "../../integrations"
|
||||||
import { invalidateDynamicVariables } from "../../threads/utils"
|
import { invalidateDynamicVariables } from "../../threads/utils"
|
||||||
import { context, db as dbCore, events } from "@budibase/backend-core"
|
import { context, db as dbCore, events } from "@budibase/backend-core"
|
||||||
|
@ -14,10 +13,13 @@ import {
|
||||||
CreateDatasourceResponse,
|
CreateDatasourceResponse,
|
||||||
Datasource,
|
Datasource,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
ExternalTable,
|
||||||
FetchDatasourceInfoRequest,
|
FetchDatasourceInfoRequest,
|
||||||
FetchDatasourceInfoResponse,
|
FetchDatasourceInfoResponse,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
|
Schema,
|
||||||
SourceName,
|
SourceName,
|
||||||
|
Table,
|
||||||
UpdateDatasourceResponse,
|
UpdateDatasourceResponse,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
VerifyDatasourceRequest,
|
VerifyDatasourceRequest,
|
||||||
|
@ -27,23 +29,6 @@ import sdk from "../../sdk"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
|
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
|
||||||
|
|
||||||
function getErrorTables(errors: any, errorType: string) {
|
|
||||||
return Object.entries(errors)
|
|
||||||
.filter(entry => entry[1] === errorType)
|
|
||||||
.map(([name]) => name)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateError(error: any, newError: any, tables: string[]) {
|
|
||||||
if (!error) {
|
|
||||||
error = ""
|
|
||||||
}
|
|
||||||
if (error.length > 0) {
|
|
||||||
error += "\n"
|
|
||||||
}
|
|
||||||
error += `${newError} ${tables.join(", ")}`
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getConnector(
|
async function getConnector(
|
||||||
datasource: Datasource
|
datasource: Datasource
|
||||||
): Promise<IntegrationBase | DatasourcePlus> {
|
): Promise<IntegrationBase | DatasourcePlus> {
|
||||||
|
@ -71,48 +56,36 @@ async function getAndMergeDatasource(datasource: Datasource) {
|
||||||
return await sdk.datasources.enrich(enrichedDatasource)
|
return await sdk.datasources.enrich(enrichedDatasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildSchemaHelper(datasource: Datasource) {
|
async function buildSchemaHelper(datasource: Datasource): Promise<Schema> {
|
||||||
const connector = (await getConnector(datasource)) as DatasourcePlus
|
const connector = (await getConnector(datasource)) as DatasourcePlus
|
||||||
await connector.buildSchema(datasource._id!, datasource.entities!)
|
return await connector.buildSchema(
|
||||||
|
datasource._id!,
|
||||||
const errors = connector.schemaErrors
|
datasource.entities! as Record<string, ExternalTable>
|
||||||
let error = null
|
)
|
||||||
if (errors && Object.keys(errors).length > 0) {
|
|
||||||
const noKey = getErrorTables(errors, BuildSchemaErrors.NO_KEY)
|
|
||||||
const invalidCol = getErrorTables(errors, BuildSchemaErrors.INVALID_COLUMN)
|
|
||||||
if (noKey.length) {
|
|
||||||
error = updateError(
|
|
||||||
error,
|
|
||||||
"No primary key constraint found for the following:",
|
|
||||||
noKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (invalidCol.length) {
|
|
||||||
const invalidCols = Object.values(InvalidColumns).join(", ")
|
|
||||||
error = updateError(
|
|
||||||
error,
|
|
||||||
`Cannot use columns ${invalidCols} found in following:`,
|
|
||||||
invalidCol
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { tables: connector.tables, error }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildFilteredSchema(datasource: Datasource, filter?: string[]) {
|
async function buildFilteredSchema(
|
||||||
let { tables, error } = await buildSchemaHelper(datasource)
|
datasource: Datasource,
|
||||||
let finalTables = tables
|
filter?: string[]
|
||||||
if (filter) {
|
): Promise<Schema> {
|
||||||
finalTables = {}
|
let schema = await buildSchemaHelper(datasource)
|
||||||
for (let key in tables) {
|
if (!filter) {
|
||||||
if (
|
return schema
|
||||||
filter.some((filter: any) => filter.toLowerCase() === key.toLowerCase())
|
}
|
||||||
) {
|
|
||||||
finalTables[key] = tables[key]
|
let filteredSchema: Schema = { tables: {}, errors: {} }
|
||||||
}
|
for (let key in schema.tables) {
|
||||||
|
if (filter.some(filter => filter.toLowerCase() === key.toLowerCase())) {
|
||||||
|
filteredSchema.tables[key] = schema.tables[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { tables: finalTables, error }
|
|
||||||
|
for (let key in schema.errors) {
|
||||||
|
if (filter.some(filter => filter.toLowerCase() === key.toLowerCase())) {
|
||||||
|
filteredSchema.errors[key] = schema.errors[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
|
@ -156,7 +129,7 @@ export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||||
const tablesFilter = ctx.request.body.tablesFilter
|
const tablesFilter = ctx.request.body.tablesFilter
|
||||||
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||||
|
|
||||||
const { tables, error } = await buildFilteredSchema(datasource, tablesFilter)
|
const { tables, errors } = await buildFilteredSchema(datasource, tablesFilter)
|
||||||
datasource.entities = tables
|
datasource.entities = tables
|
||||||
|
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
|
@ -164,13 +137,11 @@ export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||||
sdk.tables.populateExternalTableSchemas(datasource)
|
sdk.tables.populateExternalTableSchemas(datasource)
|
||||||
)
|
)
|
||||||
datasource._rev = dbResp.rev
|
datasource._rev = dbResp.rev
|
||||||
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
|
|
||||||
|
|
||||||
const res: any = { datasource: cleanedDatasource }
|
ctx.body = {
|
||||||
if (error) {
|
datasource: await sdk.datasources.removeSecretSingle(datasource),
|
||||||
res.error = error
|
errors,
|
||||||
}
|
}
|
||||||
ctx.body = res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -298,15 +269,12 @@ export async function save(
|
||||||
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
|
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
|
||||||
}
|
}
|
||||||
|
|
||||||
let schemaError = null
|
let errors: Record<string, string> = {}
|
||||||
if (fetchSchema) {
|
if (fetchSchema) {
|
||||||
const { tables, error } = await buildFilteredSchema(
|
const schema = await buildFilteredSchema(datasource, tablesFilter)
|
||||||
datasource,
|
datasource.entities = schema.tables
|
||||||
tablesFilter
|
|
||||||
)
|
|
||||||
schemaError = error
|
|
||||||
datasource.entities = tables
|
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
|
errors = schema.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preSaveAction[datasource.source]) {
|
if (preSaveAction[datasource.source]) {
|
||||||
|
@ -327,13 +295,10 @@ export async function save(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: CreateDatasourceResponse = {
|
ctx.body = {
|
||||||
datasource: await sdk.datasources.removeSecretSingle(datasource),
|
datasource: await sdk.datasources.removeSecretSingle(datasource),
|
||||||
|
errors,
|
||||||
}
|
}
|
||||||
if (schemaError) {
|
|
||||||
response.error = schemaError
|
|
||||||
}
|
|
||||||
ctx.body = response
|
|
||||||
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Routing {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the full routing structure by querying the routing view and processing the result into the tree.
|
* Gets the full routing structure by querying the routing view and processing the result into the tree.
|
||||||
* @returns {Promise<object>} The routing structure, this is the full structure designed for use in the builder,
|
* @returns The routing structure, this is the full structure designed for use in the builder,
|
||||||
* if the client routing is required then the updateRoutingStructureForUserRole should be used.
|
* if the client routing is required then the updateRoutingStructureForUserRole should be used.
|
||||||
*/
|
*/
|
||||||
async function getRoutingStructure() {
|
async function getRoutingStructure() {
|
||||||
|
|
|
@ -280,17 +280,8 @@ function isEditableColumn(column: FieldSchema) {
|
||||||
return !(isExternalAutoColumn || isFormula)
|
return !(isExternalAutoColumn || isFormula)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExternalRequestReturnType<T> = T extends Operation.READ
|
export type ExternalRequestReturnType<T extends Operation> =
|
||||||
?
|
T extends Operation.READ ? Row[] : { row: Row; table: Table }
|
||||||
| Row[]
|
|
||||||
| {
|
|
||||||
row: Row
|
|
||||||
table: Table
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
row: Row
|
|
||||||
table: Table
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExternalRequest<T extends Operation> {
|
export class ExternalRequest<T extends Operation> {
|
||||||
private readonly operation: T
|
private readonly operation: T
|
||||||
|
@ -857,11 +848,12 @@ export class ExternalRequest<T extends Operation> {
|
||||||
}
|
}
|
||||||
const output = this.outputProcessing(response, table, relationships)
|
const output = this.outputProcessing(response, table, relationships)
|
||||||
// if reading it'll just be an array of rows, return whole thing
|
// if reading it'll just be an array of rows, return whole thing
|
||||||
const result = (
|
if (operation === Operation.READ) {
|
||||||
operation === Operation.READ && Array.isArray(response)
|
return (
|
||||||
? output
|
Array.isArray(output) ? output : [output]
|
||||||
: { row: output[0], table }
|
) as ExternalRequestReturnType<T>
|
||||||
) as ExternalRequestReturnType<T>
|
} else {
|
||||||
return result
|
return { row: output[0], table } as ExternalRequestReturnType<T>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ export async function handleRequest<T extends Operation>(
|
||||||
return [] as any
|
return [] as any
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ExternalRequest(operation, tableId, opts?.datasource).run(
|
return new ExternalRequest<T>(operation, tableId, opts?.datasource).run(
|
||||||
opts || {}
|
opts || {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -148,17 +148,17 @@ export async function find(ctx: UserCtx): Promise<Row> {
|
||||||
export async function destroy(ctx: UserCtx) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
const _id = ctx.request.body._id
|
const _id = ctx.request.body._id
|
||||||
const { row } = (await handleRequest(Operation.DELETE, tableId, {
|
const { row } = await handleRequest(Operation.DELETE, tableId, {
|
||||||
id: breakRowIdField(_id),
|
id: breakRowIdField(_id),
|
||||||
includeSqlRelationships: IncludeRelationship.EXCLUDE,
|
includeSqlRelationships: IncludeRelationship.EXCLUDE,
|
||||||
})) as { row: Row }
|
})
|
||||||
return { response: { ok: true, id: _id }, row }
|
return { response: { ok: true, id: _id }, row }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkDestroy(ctx: UserCtx) {
|
export async function bulkDestroy(ctx: UserCtx) {
|
||||||
const { rows } = ctx.request.body
|
const { rows } = ctx.request.body
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
let promises: Promise<Row[] | { row: Row; table: Table }>[] = []
|
let promises: Promise<{ row: Row; table: Table }>[] = []
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
promises.push(
|
promises.push(
|
||||||
handleRequest(Operation.DELETE, tableId, {
|
handleRequest(Operation.DELETE, tableId, {
|
||||||
|
@ -167,7 +167,7 @@ export async function bulkDestroy(ctx: UserCtx) {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const responses = (await Promise.all(promises)) as { row: Row }[]
|
const responses = await Promise.all(promises)
|
||||||
return { response: { ok: true }, rows: responses.map(resp => resp.row) }
|
return { response: { ok: true }, rows: responses.map(resp => resp.row) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,11 +183,11 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
|
||||||
ctx.throw(400, "Datasource has not been configured for plus API.")
|
ctx.throw(400, "Datasource has not been configured for plus API.")
|
||||||
}
|
}
|
||||||
const tables = datasource.entities
|
const tables = datasource.entities
|
||||||
const response = (await handleRequest(Operation.READ, tableId, {
|
const response = await handleRequest(Operation.READ, tableId, {
|
||||||
id,
|
id,
|
||||||
datasource,
|
datasource,
|
||||||
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
||||||
})) as Row[]
|
})
|
||||||
const table: Table = tables[tableName]
|
const table: Table = tables[tableName]
|
||||||
const row = response[0]
|
const row = response[0]
|
||||||
// this seems like a lot of work, but basically we need to dig deeper for the enrich
|
// this seems like a lot of work, but basically we need to dig deeper for the enrich
|
||||||
|
|
|
@ -88,8 +88,8 @@ const SCHEMA_MAP: Record<string, any> = {
|
||||||
/**
|
/**
|
||||||
* Iterates through the array of filters to create a JS
|
* Iterates through the array of filters to create a JS
|
||||||
* expression that gets used in a CouchDB view.
|
* expression that gets used in a CouchDB view.
|
||||||
* @param {Array} filters - an array of filter objects
|
* @param filters - an array of filter objects
|
||||||
* @returns {String} JS Expression
|
* @returns JS Expression
|
||||||
*/
|
*/
|
||||||
function parseFilterExpression(filters: ViewFilter[]) {
|
function parseFilterExpression(filters: ViewFilter[]) {
|
||||||
const expression = []
|
const expression = []
|
||||||
|
@ -125,8 +125,8 @@ function parseFilterExpression(filters: ViewFilter[]) {
|
||||||
/**
|
/**
|
||||||
* Returns a CouchDB compliant emit() expression that is used to emit the
|
* Returns a CouchDB compliant emit() expression that is used to emit the
|
||||||
* correct key/value pairs for custom views.
|
* correct key/value pairs for custom views.
|
||||||
* @param {String?} field - field to use for calculations, if any
|
* @param field - field to use for calculations, if any
|
||||||
* @param {String?} groupBy - field to group calculation results on, if any
|
* @param groupBy - field to group calculation results on, if any
|
||||||
*/
|
*/
|
||||||
function parseEmitExpression(field: string, groupBy: string) {
|
function parseEmitExpression(field: string, groupBy: string) {
|
||||||
return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
|
return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
|
||||||
|
@ -136,7 +136,7 @@ function parseEmitExpression(field: string, groupBy: string) {
|
||||||
* Return a fully parsed CouchDB compliant view definition
|
* Return a fully parsed CouchDB compliant view definition
|
||||||
* that will be stored in the design document in the database.
|
* that will be stored in the design document in the database.
|
||||||
*
|
*
|
||||||
* @param {Object} viewDefinition - the JSON definition for a custom view.
|
* @param viewDefinition - the JSON definition for a custom view.
|
||||||
* field: field that calculations will be performed on
|
* field: field that calculations will be performed on
|
||||||
* tableId: tableId of the table this view was created from
|
* tableId: tableId of the table this view was created from
|
||||||
* groupBy: field that calculations will be grouped by. Field must be present for this to be useful
|
* groupBy: field that calculations will be grouped by. Field must be present for this to be useful
|
||||||
|
|
|
@ -23,7 +23,10 @@ describe("/applications/:appId/import", () => {
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.message).toBe("app updated")
|
const appPackage = await config.api.application.get(appId!)
|
||||||
|
expect(appPackage.navigation?.links?.length).toBe(2)
|
||||||
|
expect(expect(appPackage.navigation?.links?.[0].url).toBe("/blank"))
|
||||||
|
expect(expect(appPackage.navigation?.links?.[1].url).toBe("/derp"))
|
||||||
const screens = await config.api.screen.list()
|
const screens = await config.api.screen.list()
|
||||||
expect(screens.length).toBe(2)
|
expect(screens.length).toBe(2)
|
||||||
expect(screens[0].routing.route).toBe("/derp")
|
expect(screens[0].routing.route).toBe("/derp")
|
||||||
|
|
|
@ -37,7 +37,7 @@ describe("/datasources", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.datasource.name).toEqual("Test")
|
expect(res.body.datasource.name).toEqual("Test")
|
||||||
expect(res.body.errors).toBeUndefined()
|
expect(res.body.errors).toEqual({})
|
||||||
expect(events.datasource.created).toBeCalledTimes(1)
|
expect(events.datasource.created).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import Sentry from "@sentry/node"
|
|
||||||
|
|
||||||
if (process.env.DD_APM_ENABLED) {
|
if (process.env.DD_APM_ENABLED) {
|
||||||
require("./ddApm")
|
require("./ddApm")
|
||||||
}
|
}
|
||||||
|
|
||||||
// need to load environment first
|
|
||||||
import env from "./environment"
|
|
||||||
import * as db from "./db"
|
import * as db from "./db"
|
||||||
db.init()
|
db.init()
|
||||||
import { ServiceType } from "@budibase/types"
|
import { ServiceType } from "@budibase/types"
|
||||||
|
@ -28,10 +24,6 @@ async function start() {
|
||||||
}
|
}
|
||||||
// startup includes automation runner - if enabled
|
// startup includes automation runner - if enabled
|
||||||
await startup(app, server)
|
await startup(app, server)
|
||||||
if (env.isProd()) {
|
|
||||||
env._set("NODE_ENV", "production")
|
|
||||||
Sentry.init()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start().catch(err => {
|
start().catch(err => {
|
||||||
|
|
|
@ -14,13 +14,13 @@ import { LoopStep, LoopStepType, LoopInput } from "../definitions/automations"
|
||||||
* make sure that the post template statement can be cast into the correct type, this function does this for numbers
|
* make sure that the post template statement can be cast into the correct type, this function does this for numbers
|
||||||
* and booleans.
|
* and booleans.
|
||||||
*
|
*
|
||||||
* @param {object} inputs An object of inputs, please note this will not recurse down into any objects within, it simply
|
* @param inputs An object of inputs, please note this will not recurse down into any objects within, it simply
|
||||||
* cleanses the top level inputs, however it can be used by recursively calling it deeper into the object structures if
|
* cleanses the top level inputs, however it can be used by recursively calling it deeper into the object structures if
|
||||||
* the schema is known.
|
* the schema is known.
|
||||||
* @param {object} schema The defined schema of the inputs, in the form of JSON schema. The schema definition of an
|
* @param schema The defined schema of the inputs, in the form of JSON schema. The schema definition of an
|
||||||
* automation is the likely use case of this, however validate.js syntax can be converted closely enough to use this by
|
* automation is the likely use case of this, however validate.js syntax can be converted closely enough to use this by
|
||||||
* wrapping the schema properties in a top level "properties" object.
|
* wrapping the schema properties in a top level "properties" object.
|
||||||
* @returns {object} The inputs object which has had all the various types supported by this function converted to their
|
* @returns The inputs object which has had all the various types supported by this function converted to their
|
||||||
* primitive types.
|
* primitive types.
|
||||||
*/
|
*/
|
||||||
export function cleanInputValues(inputs: Record<string, any>, schema?: any) {
|
export function cleanInputValues(inputs: Record<string, any>, schema?: any) {
|
||||||
|
@ -74,9 +74,9 @@ export function cleanInputValues(inputs: Record<string, any>, schema?: any) {
|
||||||
* the automation but is instead part of the Table/Table. This function will get the table schema and use it to instead
|
* the automation but is instead part of the Table/Table. This function will get the table schema and use it to instead
|
||||||
* perform the cleanInputValues function on the input row.
|
* perform the cleanInputValues function on the input row.
|
||||||
*
|
*
|
||||||
* @param {string} tableId The ID of the Table/Table which the schema is to be retrieved for.
|
* @param tableId The ID of the Table/Table which the schema is to be retrieved for.
|
||||||
* @param {object} row The input row structure which requires clean-up after having been through template statements.
|
* @param row The input row structure which requires clean-up after having been through template statements.
|
||||||
* @returns {Promise<Object>} The cleaned up rows object, will should now have all the required primitive types.
|
* @returns The cleaned up rows object, will should now have all the required primitive types.
|
||||||
*/
|
*/
|
||||||
export async function cleanUpRow(tableId: string, row: Row) {
|
export async function cleanUpRow(tableId: string, row: Row) {
|
||||||
let table = await sdk.tables.getTable(tableId)
|
let table = await sdk.tables.getTable(tableId)
|
||||||
|
|
|
@ -148,8 +148,8 @@ export function isRebootTrigger(auto: Automation) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function handles checking of any cron jobs that need to be enabled/updated.
|
* This function handles checking of any cron jobs that need to be enabled/updated.
|
||||||
* @param {string} appId The ID of the app in which we are checking for webhooks
|
* @param appId The ID of the app in which we are checking for webhooks
|
||||||
* @param {object|undefined} automation The automation object to be updated.
|
* @param automation The automation object to be updated.
|
||||||
*/
|
*/
|
||||||
export async function enableCronTrigger(appId: any, automation: Automation) {
|
export async function enableCronTrigger(appId: any, automation: Automation) {
|
||||||
const trigger = automation ? automation.definition.trigger : null
|
const trigger = automation ? automation.definition.trigger : null
|
||||||
|
@ -187,10 +187,10 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function handles checking if any webhooks need to be created or deleted for automations.
|
* This function handles checking if any webhooks need to be created or deleted for automations.
|
||||||
* @param {string} appId The ID of the app in which we are checking for webhooks
|
* @param appId The ID of the app in which we are checking for webhooks
|
||||||
* @param {object|undefined} oldAuto The old automation object if updating/deleting
|
* @param oldAuto The old automation object if updating/deleting
|
||||||
* @param {object|undefined} newAuto The new automation object if creating/updating
|
* @param newAuto The new automation object if creating/updating
|
||||||
* @returns {Promise<object|undefined>} After this is complete the new automation object may have been updated and should be
|
* @returns After this is complete the new automation object may have been updated and should be
|
||||||
* written to DB (this does not write to DB as it would be wasteful to repeat).
|
* written to DB (this does not write to DB as it would be wasteful to repeat).
|
||||||
*/
|
*/
|
||||||
export async function checkForWebhooks({ oldAuto, newAuto }: any) {
|
export async function checkForWebhooks({ oldAuto, newAuto }: any) {
|
||||||
|
@ -257,8 +257,8 @@ export async function checkForWebhooks({ oldAuto, newAuto }: any) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When removing an app/unpublishing it need to make sure automations are cleaned up (cron).
|
* When removing an app/unpublishing it need to make sure automations are cleaned up (cron).
|
||||||
* @param appId {string} the app that is being removed.
|
* @param appId the app that is being removed.
|
||||||
* @return {Promise<void>} clean is complete if this succeeds.
|
* @return clean is complete if this succeeds.
|
||||||
*/
|
*/
|
||||||
export async function cleanupAutomations(appId: any) {
|
export async function cleanupAutomations(appId: any) {
|
||||||
await disableAllCrons(appId)
|
await disableAllCrons(appId)
|
||||||
|
@ -267,7 +267,7 @@ export async function cleanupAutomations(appId: any) {
|
||||||
/**
|
/**
|
||||||
* Checks if the supplied automation is of a recurring type.
|
* Checks if the supplied automation is of a recurring type.
|
||||||
* @param automation The automation to check.
|
* @param automation The automation to check.
|
||||||
* @return {boolean} if it is recurring (cron).
|
* @return if it is recurring (cron).
|
||||||
*/
|
*/
|
||||||
export function isRecurring(automation: Automation) {
|
export function isRecurring(automation: Automation) {
|
||||||
return automation.definition.trigger.stepId === definitions.CRON.stepId
|
return automation.definition.trigger.stepId === definitions.CRON.stepId
|
||||||
|
|
|
@ -159,11 +159,6 @@ export enum InvalidColumns {
|
||||||
TABLE_ID = "tableId",
|
TABLE_ID = "tableId",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BuildSchemaErrors {
|
|
||||||
NO_KEY = "no_key",
|
|
||||||
INVALID_COLUMN = "invalid_column",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AutomationErrors {
|
export enum AutomationErrors {
|
||||||
INCORRECT_TYPE = "INCORRECT_TYPE",
|
INCORRECT_TYPE = "INCORRECT_TYPE",
|
||||||
MAX_ITERATIONS = "MAX_ITERATIONS_REACHED",
|
MAX_ITERATIONS = "MAX_ITERATIONS_REACHED",
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { IncludeDocs, getLinkDocuments } from "./linkUtils"
|
import { IncludeDocs, getLinkDocuments } from "./linkUtils"
|
||||||
import { InternalTables, getUserMetadataParams } from "../utils"
|
import { InternalTables, getUserMetadataParams } from "../utils"
|
||||||
import Sentry from "@sentry/node"
|
|
||||||
import { FieldTypes } from "../../constants"
|
import { FieldTypes } from "../../constants"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context, logging } from "@budibase/backend-core"
|
||||||
import LinkDocument from "./LinkDocument"
|
import LinkDocument from "./LinkDocument"
|
||||||
import {
|
import {
|
||||||
Database,
|
Database,
|
||||||
|
@ -39,7 +38,7 @@ class LinkController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the table, if it was not already found in the eventData.
|
* Retrieves the table, if it was not already found in the eventData.
|
||||||
* @returns {Promise<object>} This will return a table based on the event data, either
|
* @returns This will return a table based on the event data, either
|
||||||
* if it was in the event already, or it uses the specified tableId to get it.
|
* if it was in the event already, or it uses the specified tableId to get it.
|
||||||
*/
|
*/
|
||||||
async table() {
|
async table() {
|
||||||
|
@ -53,8 +52,8 @@ class LinkController {
|
||||||
/**
|
/**
|
||||||
* Checks if the table this was constructed with has any linking columns currently.
|
* Checks if the table this was constructed with has any linking columns currently.
|
||||||
* If the table has not been retrieved this will retrieve it based on the eventData.
|
* If the table has not been retrieved this will retrieve it based on the eventData.
|
||||||
* @params {object|null} table If a table that is not known to the link controller is to be tested.
|
* @params table If a table that is not known to the link controller is to be tested.
|
||||||
* @returns {Promise<boolean>} True if there are any linked fields, otherwise it will return
|
* @returns True if there are any linked fields, otherwise it will return
|
||||||
* false.
|
* false.
|
||||||
*/
|
*/
|
||||||
async doesTableHaveLinkedFields(table?: Table) {
|
async doesTableHaveLinkedFields(table?: Table) {
|
||||||
|
@ -160,7 +159,7 @@ class LinkController {
|
||||||
/**
|
/**
|
||||||
* When a row is saved this will carry out the necessary operations to make sure
|
* When a row is saved this will carry out the necessary operations to make sure
|
||||||
* the link has been created/updated.
|
* the link has been created/updated.
|
||||||
* @returns {Promise<object>} returns the row that has been cleaned and prepared to be written to the DB - links
|
* @returns returns the row that has been cleaned and prepared to be written to the DB - links
|
||||||
* have also been created.
|
* have also been created.
|
||||||
*/
|
*/
|
||||||
async rowSaved() {
|
async rowSaved() {
|
||||||
|
@ -272,7 +271,7 @@ class LinkController {
|
||||||
/**
|
/**
|
||||||
* When a row is deleted this will carry out the necessary operations to make sure
|
* When a row is deleted this will carry out the necessary operations to make sure
|
||||||
* any links that existed have been removed.
|
* any links that existed have been removed.
|
||||||
* @returns {Promise<object>} The operation has been completed and the link documents should now
|
* @returns The operation has been completed and the link documents should now
|
||||||
* be accurate. This also returns the row that was deleted.
|
* be accurate. This also returns the row that was deleted.
|
||||||
*/
|
*/
|
||||||
async rowDeleted() {
|
async rowDeleted() {
|
||||||
|
@ -294,8 +293,8 @@ class LinkController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a field from a table as well as any linked rows that pertained to it.
|
* Remove a field from a table as well as any linked rows that pertained to it.
|
||||||
* @param {string} fieldName The field to be removed from the table.
|
* @param fieldName The field to be removed from the table.
|
||||||
* @returns {Promise<void>} The table has now been updated.
|
* @returns The table has now been updated.
|
||||||
*/
|
*/
|
||||||
async removeFieldFromTable(fieldName: string) {
|
async removeFieldFromTable(fieldName: string) {
|
||||||
let oldTable = this._oldTable
|
let oldTable = this._oldTable
|
||||||
|
@ -334,7 +333,7 @@ class LinkController {
|
||||||
/**
|
/**
|
||||||
* When a table is saved this will carry out the necessary operations to make sure
|
* When a table is saved this will carry out the necessary operations to make sure
|
||||||
* any linked tables are notified and updated correctly.
|
* any linked tables are notified and updated correctly.
|
||||||
* @returns {Promise<object>} The operation has been completed and the link documents should now
|
* @returns The operation has been completed and the link documents should now
|
||||||
* be accurate. Also returns the table that was operated on.
|
* be accurate. Also returns the table that was operated on.
|
||||||
*/
|
*/
|
||||||
async tableSaved() {
|
async tableSaved() {
|
||||||
|
@ -395,7 +394,7 @@ class LinkController {
|
||||||
/**
|
/**
|
||||||
* Update a table, this means if a field is removed need to handle removing from other table and removing
|
* Update a table, this means if a field is removed need to handle removing from other table and removing
|
||||||
* any link docs that pertained to it.
|
* any link docs that pertained to it.
|
||||||
* @returns {Promise<Object>} The table which has been saved, same response as with the tableSaved function.
|
* @returns The table which has been saved, same response as with the tableSaved function.
|
||||||
*/
|
*/
|
||||||
async tableUpdated() {
|
async tableUpdated() {
|
||||||
const oldTable = this._oldTable
|
const oldTable = this._oldTable
|
||||||
|
@ -419,7 +418,7 @@ class LinkController {
|
||||||
* When a table is deleted this will carry out the necessary operations to make sure
|
* When a table is deleted this will carry out the necessary operations to make sure
|
||||||
* any linked tables have the joining column correctly removed as well as removing any
|
* any linked tables have the joining column correctly removed as well as removing any
|
||||||
* now stale linking documents.
|
* now stale linking documents.
|
||||||
* @returns {Promise<object>} The operation has been completed and the link documents should now
|
* @returns The operation has been completed and the link documents should now
|
||||||
* be accurate. Also returns the table that was operated on.
|
* be accurate. Also returns the table that was operated on.
|
||||||
*/
|
*/
|
||||||
async tableDeleted() {
|
async tableDeleted() {
|
||||||
|
@ -433,9 +432,8 @@ class LinkController {
|
||||||
delete linkedTable.schema[field.fieldName]
|
delete linkedTable.schema[field.fieldName]
|
||||||
await this._db.put(linkedTable)
|
await this._db.put(linkedTable)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
/* istanbul ignore next */
|
logging.logWarn(err?.message, err)
|
||||||
Sentry.captureException(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// need to get the full link docs to delete them
|
// need to get the full link docs to delete them
|
||||||
|
|
|
@ -6,12 +6,12 @@ import { LinkDocument } from "@budibase/types"
|
||||||
* Creates a new link document structure which can be put to the database. It is important to
|
* Creates a new link document structure which can be put to the database. It is important to
|
||||||
* note that while this talks about linker/linked the link is bi-directional and for all intent
|
* note that while this talks about linker/linked the link is bi-directional and for all intent
|
||||||
* and purposes it does not matter from which direction the link was initiated.
|
* and purposes it does not matter from which direction the link was initiated.
|
||||||
* @param {string} tableId1 The ID of the first table (the linker).
|
* @param tableId1 The ID of the first table (the linker).
|
||||||
* @param {string} tableId2 The ID of the second table (the linked).
|
* @param tableId2 The ID of the second table (the linked).
|
||||||
* @param {string} fieldName1 The name of the field in the linker table.
|
* @param fieldName1 The name of the field in the linker table.
|
||||||
* @param {string} fieldName2 The name of the field in the linked table.
|
* @param fieldName2 The name of the field in the linked table.
|
||||||
* @param {string} rowId1 The ID of the row which is acting as the linker.
|
* @param rowId1 The ID of the row which is acting as the linker.
|
||||||
* @param {string} rowId2 The ID of the row which is acting as the linked.
|
* @param rowId2 The ID of the row which is acting as the linked.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
class LinkDocumentImpl implements LinkDocument {
|
class LinkDocumentImpl implements LinkDocument {
|
||||||
|
|
|
@ -90,13 +90,13 @@ async function getFullLinkedDocs(links: LinkDocumentValue[]) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update link documents for a row or table - this is to be called by the API controller when a change is occurring.
|
* Update link documents for a row or table - this is to be called by the API controller when a change is occurring.
|
||||||
* @param {string} args.eventType states what type of change which is occurring, means this can be expanded upon in the
|
* @param args.eventType states what type of change which is occurring, means this can be expanded upon in the
|
||||||
* future quite easily (all updates go through one function).
|
* future quite easily (all updates go through one function).
|
||||||
* @param {string} args.tableId The ID of the of the table which is being changed.
|
* @param args.tableId The ID of the of the table which is being changed.
|
||||||
* @param {object|undefined} args.row The row which is changing, e.g. created, updated or deleted.
|
* @param args.row The row which is changing, e.g. created, updated or deleted.
|
||||||
* @param {object|undefined} args.table If the table has already been retrieved this can be used to reduce database gets.
|
* @param args.table If the table has already been retrieved this can be used to reduce database gets.
|
||||||
* @param {object|undefined} args.oldTable If the table is being updated then the old table can be provided for differencing.
|
* @param args.oldTable If the table is being updated then the old table can be provided for differencing.
|
||||||
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the row for
|
* @returns When the update is complete this will respond successfully. Returns the row for
|
||||||
* row operations and the table for table operations.
|
* row operations and the table for table operations.
|
||||||
*/
|
*/
|
||||||
export async function updateLinks(args: {
|
export async function updateLinks(args: {
|
||||||
|
@ -144,10 +144,10 @@ export async function updateLinks(args: {
|
||||||
/**
|
/**
|
||||||
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
|
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
|
||||||
* This is required for formula fields, this may only be utilised internally (for now).
|
* This is required for formula fields, this may only be utilised internally (for now).
|
||||||
* @param {object} table The table from which the rows originated.
|
* @param table The table from which the rows originated.
|
||||||
* @param {array<object>} rows The rows which are to be enriched.
|
* @param rows The rows which are to be enriched.
|
||||||
* @param {object} opts optional - options like passing in a base row to use for enrichment.
|
* @param opts optional - options like passing in a base row to use for enrichment.
|
||||||
* @return {Promise<*>} returns the rows with all of the enriched relationships on it.
|
* @return returns the rows with all of the enriched relationships on it.
|
||||||
*/
|
*/
|
||||||
export async function attachFullLinkedDocs(
|
export async function attachFullLinkedDocs(
|
||||||
table: Table,
|
table: Table,
|
||||||
|
@ -208,9 +208,9 @@ export async function attachFullLinkedDocs(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function will take the given enriched rows and squash the links to only contain the primary display field.
|
* This function will take the given enriched rows and squash the links to only contain the primary display field.
|
||||||
* @param {object} table The table from which the rows originated.
|
* @param table The table from which the rows originated.
|
||||||
* @param {array<object>} enriched The pre-enriched rows (full docs) which are to be squashed.
|
* @param enriched The pre-enriched rows (full docs) which are to be squashed.
|
||||||
* @returns {Promise<Array>} The rows after having their links squashed to only contain the ID and primary display.
|
* @returns The rows after having their links squashed to only contain the ID and primary display.
|
||||||
*/
|
*/
|
||||||
export async function squashLinksToPrimaryDisplay(
|
export async function squashLinksToPrimaryDisplay(
|
||||||
table: Table,
|
table: Table,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue