Merge branch 'develop' of github.com:Budibase/budibase into grid-v2
This commit is contained in:
commit
d491a24d3e
|
@ -22,44 +22,64 @@ jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn nx run-many -t=build --configuration=production
|
||||||
- run: yarn build
|
|
||||||
|
|
||||||
test:
|
test-libraries:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||||
- run: yarn build
|
- uses: codecov/codecov-action@v3
|
||||||
- run: yarn test --ignore=@budibase/pro
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
name: codecov-umbrella
|
||||||
|
verbose: true
|
||||||
|
|
||||||
|
test-services:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
- name: Use Node.js 14.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 14.x
|
||||||
|
cache: "yarn"
|
||||||
|
- run: yarn
|
||||||
|
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
@ -69,32 +89,34 @@ jobs:
|
||||||
test-pro:
|
test-pro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
|
||||||
- run: yarn build --scope=@budibase/types --scope=@budibase/shared-core
|
|
||||||
- run: yarn test --scope=@budibase/pro
|
- run: yarn test --scope=@budibase/pro
|
||||||
|
|
||||||
integration-test:
|
integration-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
- run: yarn && yarn bootstrap && yarn build
|
cache: "yarn"
|
||||||
- run: |
|
- run: yarn
|
||||||
|
- run: yarn build
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
cd qa-core
|
cd qa-core
|
||||||
yarn setup
|
yarn setup
|
||||||
yarn test:ci
|
yarn test:ci
|
||||||
|
@ -106,7 +128,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
|
@ -51,7 +51,7 @@ jobs:
|
||||||
node scripts/syncLocalDependencies.js $version
|
node scripts/syncLocalDependencies.js $version
|
||||||
echo "Syncing yarn workspace"
|
echo "Syncing yarn workspace"
|
||||||
yarn
|
yarn
|
||||||
- run: yarn build
|
- run: yarn build --configuration=production
|
||||||
- run: yarn build:sdk
|
- run: yarn build:sdk
|
||||||
|
|
||||||
- name: Publish budibase packages to NPM
|
- name: Publish budibase packages to NPM
|
||||||
|
|
|
@ -57,7 +57,7 @@ jobs:
|
||||||
echo "Syncing yarn workspace"
|
echo "Syncing yarn workspace"
|
||||||
yarn
|
yarn
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
- run: yarn build
|
- run: yarn build --configuration=production
|
||||||
- run: yarn build:sdk
|
- run: yarn build:sdk
|
||||||
|
|
||||||
- name: Publish budibase packages to NPM
|
- name: Publish budibase packages to NPM
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
3.10.0
|
3.10.0
|
||||||
|
|
|
@ -144,8 +144,6 @@ The following commands can be executed to manually get Budibase up and running (
|
||||||
|
|
||||||
`yarn` to install project dependencies
|
`yarn` to install project dependencies
|
||||||
|
|
||||||
`yarn bootstrap` will install all budibase modules and symlink them together using lerna.
|
|
||||||
|
|
||||||
`yarn build` will build all budibase packages.
|
`yarn build` will build all budibase packages.
|
||||||
|
|
||||||
#### 4. Running
|
#### 4. Running
|
||||||
|
@ -243,7 +241,7 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
|
||||||
|
|
||||||
Note that only budibase maintainers will be able to access the pro repo.
|
Note that only budibase maintainers will be able to access the pro repo.
|
||||||
|
|
||||||
The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
# optional ports are specified throughout for more advanced use cases.
|
||||||
|
|
||||||
|
services:
|
||||||
|
app-service:
|
||||||
|
build: ../packages/server
|
||||||
|
container_name: build-bbapps
|
||||||
|
environment:
|
||||||
|
SELF_HOSTED: 1
|
||||||
|
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||||
|
WORKER_URL: http://worker-service:4003
|
||||||
|
MINIO_URL: http://minio-service:9000
|
||||||
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||||
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
|
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
||||||
|
PORT: 4002
|
||||||
|
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
LOG_LEVEL: info
|
||||||
|
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||||
|
ENABLE_ANALYTICS: "true"
|
||||||
|
REDIS_URL: redis-service:6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||||
|
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||||
|
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||||
|
depends_on:
|
||||||
|
- worker-service
|
||||||
|
- redis-service
|
||||||
|
# volumes:
|
||||||
|
# - /some/path/to/plugins:/plugins
|
||||||
|
|
||||||
|
worker-service:
|
||||||
|
build: ../packages/worker
|
||||||
|
container_name: build-bbworker
|
||||||
|
environment:
|
||||||
|
SELF_HOSTED: 1
|
||||||
|
PORT: 4003
|
||||||
|
CLUSTER_PORT: ${MAIN_PORT}
|
||||||
|
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||||
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
|
MINIO_URL: http://minio-service:9000
|
||||||
|
APPS_URL: http://app-service:4002
|
||||||
|
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||||
|
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||||
|
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}
|
||||||
|
REDIS_URL: redis-service:6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
depends_on:
|
||||||
|
- redis-service
|
||||||
|
- minio-service
|
||||||
|
|
||||||
|
proxy-service-docker:
|
||||||
|
ports:
|
||||||
|
- "${MAIN_PORT}:10000"
|
||||||
|
container_name: build-bbproxy
|
||||||
|
image: budibase/proxy
|
||||||
|
environment:
|
||||||
|
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
|
||||||
|
- PROXY_RATE_LIMIT_API_PER_SECOND=20
|
||||||
|
- APPS_UPSTREAM_URL=http://app-service:4002
|
||||||
|
- WORKER_UPSTREAM_URL=http://worker-service:4003
|
||||||
|
- MINIO_UPSTREAM_URL=http://minio-service:9000
|
||||||
|
- COUCHDB_UPSTREAM_URL=http://couchdb-service:5984
|
||||||
|
- WATCHTOWER_UPSTREAM_URL=http://watchtower-service:8080
|
||||||
|
- RESOLVER=127.0.0.11
|
||||||
|
depends_on:
|
||||||
|
- minio-service
|
||||||
|
- worker-service
|
||||||
|
- app-service
|
||||||
|
- couchdb-service
|
|
@ -1,22 +1,22 @@
|
||||||
FROM node:14-slim as build
|
FROM node:16-slim as build
|
||||||
|
|
||||||
# install node-gyp dependencies
|
# install node-gyp dependencies
|
||||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
||||||
|
|
||||||
# add pin script
|
# add pin script
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
ADD scripts/pinVersions.js scripts/cleanup.sh ./
|
ADD scripts/cleanup.sh ./
|
||||||
RUN chmod +x /cleanup.sh
|
RUN chmod +x /cleanup.sh
|
||||||
|
|
||||||
# build server
|
# build server
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD packages/server .
|
ADD packages/server .
|
||||||
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
||||||
|
|
||||||
# build worker
|
# build worker
|
||||||
WORKDIR /worker
|
WORKDIR /worker
|
||||||
ADD packages/worker .
|
ADD packages/worker .
|
||||||
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
||||||
|
|
||||||
FROM budibase/couchdb
|
FROM budibase/couchdb
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
@ -31,9 +31,7 @@ COPY --from=build /worker /worker
|
||||||
|
|
||||||
# install base dependencies
|
# install base dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
|
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
|
||||||
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
|
||||||
apt-get update
|
|
||||||
|
|
||||||
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||||
WORKDIR /nodejs
|
WORKDIR /nodejs
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.6.16-alpha.0",
|
"version": "2.6.19-alpha.7",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/backend-core",
|
||||||
|
|
10
nx.json
10
nx.json
|
@ -6,5 +6,15 @@
|
||||||
"cacheableOperations": ["build", "test"]
|
"cacheableOperations": ["build", "test"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"targetDefaults": {
|
||||||
|
"dev:builder": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": ["@budibase/string-templates"],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
package.json
30
package.json
|
@ -2,17 +2,23 @@
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esbuild-plugins/node-resolve": "^0.2.2",
|
||||||
|
"@nx/esbuild": "16.2.1",
|
||||||
|
"@nx/js": "16.2.1",
|
||||||
"@rollup/plugin-json": "^4.0.2",
|
"@rollup/plugin-json": "^4.0.2",
|
||||||
"@typescript-eslint/parser": "5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
|
"esbuild": "^0.17.18",
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-plugin-cypress": "^2.11.3",
|
"eslint-plugin-cypress": "^2.11.3",
|
||||||
"eslint-plugin-svelte3": "^3.2.0",
|
"eslint-plugin-svelte3": "^3.2.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "^6.6.1",
|
"lerna": "7.0.0-alpha.0",
|
||||||
"madge": "^6.0.0",
|
"madge": "^6.0.0",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"nx": "^16.2.1",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
@ -23,9 +29,9 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "node scripts/syncProPackage.js",
|
"preinstall": "node scripts/syncProPackage.js",
|
||||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
||||||
"bootstrap": "./scripts/bootstrap.sh && lerna link && ./scripts/link-dependencies.sh",
|
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
||||||
"build": "lerna run --stream build",
|
"build": "yarn nx run-many -t=build",
|
||||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||||
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||||
|
@ -41,10 +47,11 @@
|
||||||
"kill-builder": "kill-port 3000",
|
"kill-builder": "kill-port 3000",
|
||||||
"kill-server": "kill-port 4001 4002",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||||
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
|
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
|
||||||
"dev:noserver": "yarn run kill-builder && lerna link && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --concurrency 1 --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 --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --stream --parallel 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 --parallel dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||||
|
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.dev.yaml -f hosting/docker-compose.build.yaml up --build --scale proxy-service=0 ",
|
||||||
"test": "lerna run --stream test --stream",
|
"test": "lerna run --stream test --stream",
|
||||||
"lint:eslint": "eslint packages && eslint qa-core",
|
"lint:eslint": "eslint packages && eslint qa-core",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
|
@ -53,16 +60,16 @@
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
"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 && npm run 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": "lerna run --stream build && lerna run --stream predocker",
|
"build:docker:pre": "lerna run --stream 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 && npm run 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 -",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "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:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||||
"build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image",
|
"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",
|
||||||
|
@ -101,5 +108,6 @@
|
||||||
"packages/worker",
|
"packages/worker",
|
||||||
"packages/pro/packages/pro"
|
"packages/pro/packages/pro"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,5 +88,19 @@
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
},
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/types"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,6 +104,22 @@ async function newContext(updates: ContextMap, task: any) {
|
||||||
return Context.run(context, task)
|
return Context.run(context, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function doInAutomationContext(params: {
|
||||||
|
appId: string
|
||||||
|
automationId: string
|
||||||
|
task: any
|
||||||
|
}): Promise<any> {
|
||||||
|
const tenantId = getTenantIDFromAppID(params.appId)
|
||||||
|
return newContext(
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
appId: params.appId,
|
||||||
|
automationId: params.automationId,
|
||||||
|
},
|
||||||
|
params.task
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function doInContext(appId: string, task: any): Promise<any> {
|
export async function doInContext(appId: string, task: any): Promise<any> {
|
||||||
const tenantId = getTenantIDFromAppID(appId)
|
const tenantId = getTenantIDFromAppID(appId)
|
||||||
return newContext(
|
return newContext(
|
||||||
|
@ -187,6 +203,11 @@ export function getTenantId(): string {
|
||||||
return tenantId
|
return tenantId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAutomationId(): string | undefined {
|
||||||
|
const context = Context.get()
|
||||||
|
return context?.automationId
|
||||||
|
}
|
||||||
|
|
||||||
export function getAppId(): string | undefined {
|
export function getAppId(): string | undefined {
|
||||||
const context = Context.get()
|
const context = Context.get()
|
||||||
const foundId = context?.appId
|
const foundId = context?.appId
|
||||||
|
|
|
@ -7,4 +7,5 @@ export type ContextMap = {
|
||||||
identity?: IdentityContext
|
identity?: IdentityContext
|
||||||
environmentVariables?: Record<string, string>
|
environmentVariables?: Record<string, string>
|
||||||
isScim?: boolean
|
isScim?: boolean
|
||||||
|
automationId?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
isDocument,
|
isDocument,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getCouchInfo } from "./connections"
|
import { getCouchInfo } from "./connections"
|
||||||
import { directCouchCall } from "./utils"
|
import { directCouchUrlCall } from "./utils"
|
||||||
import { getPouchDB } from "./pouchDB"
|
import { getPouchDB } from "./pouchDB"
|
||||||
import { WriteStream, ReadStream } from "fs"
|
import { WriteStream, ReadStream } from "fs"
|
||||||
import { newid } from "../../docIds/newid"
|
import { newid } from "../../docIds/newid"
|
||||||
|
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
|
||||||
private readonly instanceNano?: Nano.ServerScope
|
private readonly instanceNano?: Nano.ServerScope
|
||||||
private readonly pouchOpts: DatabaseOpts
|
private readonly pouchOpts: DatabaseOpts
|
||||||
|
|
||||||
|
private readonly couchInfo = getCouchInfo()
|
||||||
|
|
||||||
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
||||||
if (dbName == null) {
|
if (dbName == null) {
|
||||||
throw new Error("Database name cannot be undefined.")
|
throw new Error("Database name cannot be undefined.")
|
||||||
|
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
|
||||||
this.name = dbName
|
this.name = dbName
|
||||||
this.pouchOpts = opts || {}
|
this.pouchOpts = opts || {}
|
||||||
if (connection) {
|
if (connection) {
|
||||||
const couchInfo = getCouchInfo(connection)
|
this.couchInfo = getCouchInfo(connection)
|
||||||
this.instanceNano = buildNano(couchInfo)
|
this.instanceNano = buildNano(this.couchInfo)
|
||||||
}
|
}
|
||||||
if (!DatabaseImpl.nano) {
|
if (!DatabaseImpl.nano) {
|
||||||
DatabaseImpl.init()
|
DatabaseImpl.init()
|
||||||
|
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists() {
|
async exists() {
|
||||||
let response = await directCouchCall(`/${this.name}`, "HEAD")
|
const response = await directCouchUrlCall({
|
||||||
|
url: `${this.couchInfo.url}/${this.name}`,
|
||||||
|
method: "HEAD",
|
||||||
|
cookie: this.couchInfo.cookie,
|
||||||
|
})
|
||||||
return response.status === 200
|
return response.status === 200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
|
||||||
const urlInfo = getUrlInfo(connection)
|
const urlInfo = getUrlInfo(connection)
|
||||||
let username
|
let username
|
||||||
let password
|
let password
|
||||||
if (env.COUCH_DB_USERNAME) {
|
if (urlInfo.auth?.username) {
|
||||||
// set from env
|
|
||||||
username = env.COUCH_DB_USERNAME
|
|
||||||
} else if (urlInfo.auth.username) {
|
|
||||||
// set from url
|
// set from url
|
||||||
username = urlInfo.auth.username
|
username = urlInfo.auth.username
|
||||||
|
} else if (env.COUCH_DB_USERNAME) {
|
||||||
|
// set from env
|
||||||
|
username = env.COUCH_DB_USERNAME
|
||||||
} else if (!env.isTest()) {
|
} else if (!env.isTest()) {
|
||||||
throw new Error("CouchDB username not set")
|
throw new Error("CouchDB username not set")
|
||||||
}
|
}
|
||||||
if (env.COUCH_DB_PASSWORD) {
|
if (urlInfo.auth?.password) {
|
||||||
// set from env
|
|
||||||
password = env.COUCH_DB_PASSWORD
|
|
||||||
} else if (urlInfo.auth.password) {
|
|
||||||
// set from url
|
// set from url
|
||||||
password = urlInfo.auth.password
|
password = urlInfo.auth.password
|
||||||
|
} else if (env.COUCH_DB_PASSWORD) {
|
||||||
|
// set from env
|
||||||
|
password = env.COUCH_DB_PASSWORD
|
||||||
} else if (!env.isTest()) {
|
} else if (!env.isTest()) {
|
||||||
throw new Error("CouchDB password not set")
|
throw new Error("CouchDB password not set")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,20 @@ export async function directCouchCall(
|
||||||
) {
|
) {
|
||||||
let { url, cookie } = getCouchInfo()
|
let { url, cookie } = getCouchInfo()
|
||||||
const couchUrl = `${url}/${path}`
|
const couchUrl = `${url}/${path}`
|
||||||
|
return await directCouchUrlCall({ url: couchUrl, cookie, method, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function directCouchUrlCall({
|
||||||
|
url,
|
||||||
|
cookie,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
cookie: string
|
||||||
|
method: string
|
||||||
|
body?: any
|
||||||
|
}) {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -19,7 +33,7 @@ export async function directCouchCall(
|
||||||
params.body = JSON.stringify(body)
|
params.body = JSON.stringify(body)
|
||||||
params.headers["Content-Type"] = "application/json"
|
params.headers["Content-Type"] = "application/json"
|
||||||
}
|
}
|
||||||
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
|
return await fetch(checkSlashesInUrl(encodeURI(url)), params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function directCouchQuery(
|
export async function directCouchQuery(
|
||||||
|
|
|
@ -39,6 +39,7 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
objects?: any[]
|
objects?: any[]
|
||||||
tenantId?: string
|
tenantId?: string
|
||||||
appId?: string
|
appId?: string
|
||||||
|
automationId?: string
|
||||||
identityId?: string
|
identityId?: string
|
||||||
identityType?: IdentityType
|
identityType?: IdentityType
|
||||||
correlationId?: string
|
correlationId?: string
|
||||||
|
@ -86,18 +87,44 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
contextObject = {
|
contextObject = {
|
||||||
tenantId: getTenantId(),
|
tenantId: getTenantId(),
|
||||||
appId: getAppId(),
|
appId: getAppId(),
|
||||||
|
automationId: getAutomationId(),
|
||||||
identityId: identity?._id,
|
identityId: identity?._id,
|
||||||
identityType: identity?.type,
|
identityType: identity?.type,
|
||||||
correlationId: correlation.getId(),
|
correlationId: correlation.getId(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergingObject = {
|
const mergingObject: any = {
|
||||||
objects: objects.length ? objects : undefined,
|
|
||||||
err: error,
|
err: error,
|
||||||
...contextObject,
|
...contextObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (objects.length) {
|
||||||
|
// init generic data object for params supplied that don't have a
|
||||||
|
// '_logKey' field. This prints an object using argument index as the key
|
||||||
|
// e.g. { 0: {}, 1: {} }
|
||||||
|
const data: any = {}
|
||||||
|
let dataIndex = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < objects.length; i++) {
|
||||||
|
const object = objects[i]
|
||||||
|
// the object has specified a log key
|
||||||
|
// use this instead of generic key
|
||||||
|
const logKey = object._logKey
|
||||||
|
if (logKey) {
|
||||||
|
delete object._logKey
|
||||||
|
mergingObject[logKey] = object
|
||||||
|
} else {
|
||||||
|
data[dataIndex] = object
|
||||||
|
dataIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(data).length) {
|
||||||
|
mergingObject.data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [mergingObject, message]
|
return [mergingObject, message]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,6 +186,16 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
return appId
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAutomationId = () => {
|
||||||
|
let appId
|
||||||
|
try {
|
||||||
|
appId = context.getAutomationId()
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
|
||||||
const getIdentity = () => {
|
const getIdentity = () => {
|
||||||
let identity
|
let identity
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -128,6 +128,7 @@ class InMemoryQueue {
|
||||||
|
|
||||||
on() {
|
on() {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForCompletion() {
|
async waitForCompletion() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Job, JobId, Queue } from "bull"
|
import { Job, JobId, Queue } from "bull"
|
||||||
import { JobQueue } from "./constants"
|
import { JobQueue } from "./constants"
|
||||||
|
import * as context from "../context"
|
||||||
|
|
||||||
export type StalledFn = (job: Job) => Promise<void>
|
export type StalledFn = (job: Job) => Promise<void>
|
||||||
|
|
||||||
|
@ -31,77 +32,164 @@ function handleStalled(queue: Queue, removeStalledCb?: StalledFn) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function logging(queue: Queue, jobQueue: JobQueue) {
|
function getLogParams(
|
||||||
let eventType: string
|
eventType: QueueEventType,
|
||||||
switch (jobQueue) {
|
event: BullEvent,
|
||||||
case JobQueue.AUTOMATION:
|
opts: {
|
||||||
eventType = "automation-event"
|
job?: Job
|
||||||
break
|
jobId?: JobId
|
||||||
case JobQueue.APP_BACKUP:
|
error?: Error
|
||||||
eventType = "app-backup-event"
|
} = {},
|
||||||
break
|
extra: any = {}
|
||||||
case JobQueue.AUDIT_LOG:
|
) {
|
||||||
eventType = "audit-log-event"
|
const message = `[BULL] ${eventType}=${event}`
|
||||||
break
|
const err = opts.error
|
||||||
case JobQueue.SYSTEM_EVENT_QUEUE:
|
|
||||||
eventType = "system-event"
|
const bullLog = {
|
||||||
break
|
_logKey: "bull",
|
||||||
|
eventType,
|
||||||
|
event,
|
||||||
|
job: opts.job,
|
||||||
|
jobId: opts.jobId || opts.job?.id,
|
||||||
|
...extra,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let automationLog
|
||||||
|
if (opts.job?.data?.automation) {
|
||||||
|
automationLog = {
|
||||||
|
_logKey: "automation",
|
||||||
|
trigger: opts.job
|
||||||
|
? opts.job.data.automation.definition.trigger.event
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [message, err, bullLog, automationLog]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BullEvent {
|
||||||
|
ERROR = "error",
|
||||||
|
WAITING = "waiting",
|
||||||
|
ACTIVE = "active",
|
||||||
|
STALLED = "stalled",
|
||||||
|
PROGRESS = "progress",
|
||||||
|
COMPLETED = "completed",
|
||||||
|
FAILED = "failed",
|
||||||
|
PAUSED = "paused",
|
||||||
|
RESUMED = "resumed",
|
||||||
|
CLEANED = "cleaned",
|
||||||
|
DRAINED = "drained",
|
||||||
|
REMOVED = "removed",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QueueEventType {
|
||||||
|
AUTOMATION_EVENT = "automation-event",
|
||||||
|
APP_BACKUP_EVENT = "app-backup-event",
|
||||||
|
AUDIT_LOG_EVENT = "audit-log-event",
|
||||||
|
SYSTEM_EVENT = "system-event",
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventTypeMap: { [key in JobQueue]: QueueEventType } = {
|
||||||
|
[JobQueue.AUTOMATION]: QueueEventType.AUTOMATION_EVENT,
|
||||||
|
[JobQueue.APP_BACKUP]: QueueEventType.APP_BACKUP_EVENT,
|
||||||
|
[JobQueue.AUDIT_LOG]: QueueEventType.AUDIT_LOG_EVENT,
|
||||||
|
[JobQueue.SYSTEM_EVENT_QUEUE]: QueueEventType.SYSTEM_EVENT,
|
||||||
|
}
|
||||||
|
|
||||||
|
function logging(queue: Queue, jobQueue: JobQueue) {
|
||||||
|
const eventType = EventTypeMap[jobQueue]
|
||||||
|
|
||||||
|
function doInJobContext(job: Job, task: any) {
|
||||||
|
// if this is an automation job try to get the app id
|
||||||
|
const appId = job.data.event?.appId
|
||||||
|
if (appId) {
|
||||||
|
return context.doInContext(appId, task)
|
||||||
|
} else {
|
||||||
|
task()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queue
|
||||||
|
.on(BullEvent.STALLED, async (job: Job) => {
|
||||||
|
// A job has been marked as stalled. This is useful for debugging job
|
||||||
|
// workers that crash or pause the event loop.
|
||||||
|
await doInJobContext(job, () => {
|
||||||
|
console.error(...getLogParams(eventType, BullEvent.STALLED, { job }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on(BullEvent.ERROR, (error: any) => {
|
||||||
|
// An error occurred.
|
||||||
|
console.error(...getLogParams(eventType, BullEvent.ERROR, { error }))
|
||||||
|
})
|
||||||
|
|
||||||
if (process.env.NODE_DEBUG?.includes("bull")) {
|
if (process.env.NODE_DEBUG?.includes("bull")) {
|
||||||
queue
|
queue
|
||||||
.on("error", (error: any) => {
|
.on(BullEvent.WAITING, (jobId: JobId) => {
|
||||||
// An error occurred.
|
|
||||||
console.error(`${eventType}=error error=${JSON.stringify(error)}`)
|
|
||||||
})
|
|
||||||
.on("waiting", (jobId: JobId) => {
|
|
||||||
// A Job is waiting to be processed as soon as a worker is idling.
|
// A Job is waiting to be processed as soon as a worker is idling.
|
||||||
console.log(`${eventType}=waiting jobId=${jobId}`)
|
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
|
||||||
})
|
})
|
||||||
.on("active", (job: Job, jobPromise: any) => {
|
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
|
||||||
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
||||||
console.log(`${eventType}=active jobId=${job.id}`)
|
await doInJobContext(job, () => {
|
||||||
|
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.on("stalled", (job: Job) => {
|
.on(BullEvent.PROGRESS, async (job: Job, progress: any) => {
|
||||||
// A job has been marked as stalled. This is useful for debugging job
|
// A job's progress was updated
|
||||||
// workers that crash or pause the event loop.
|
await doInJobContext(job, () => {
|
||||||
console.error(
|
console.info(
|
||||||
`${eventType}=stalled jobId=${job.id} job=${JSON.stringify(job)}`
|
...getLogParams(
|
||||||
)
|
eventType,
|
||||||
|
BullEvent.PROGRESS,
|
||||||
|
{ job },
|
||||||
|
{ progress }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.on("progress", (job: Job, progress: any) => {
|
.on(BullEvent.COMPLETED, async (job: Job, result) => {
|
||||||
// A job's progress was updated!
|
|
||||||
console.log(
|
|
||||||
`${eventType}=progress jobId=${job.id} progress=${progress}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on("completed", (job: Job, result) => {
|
|
||||||
// A job successfully completed with a `result`.
|
// A job successfully completed with a `result`.
|
||||||
console.log(`${eventType}=completed jobId=${job.id} result=${result}`)
|
await doInJobContext(job, () => {
|
||||||
|
console.info(
|
||||||
|
...getLogParams(eventType, BullEvent.COMPLETED, { job }, { result })
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.on("failed", (job, err: any) => {
|
.on(BullEvent.FAILED, async (job: Job, error: any) => {
|
||||||
// A job failed with reason `err`!
|
// A job failed with reason `err`!
|
||||||
console.log(`${eventType}=failed jobId=${job.id} error=${err}`)
|
await doInJobContext(job, () => {
|
||||||
|
console.error(
|
||||||
|
...getLogParams(eventType, BullEvent.FAILED, { job, error })
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.on("paused", () => {
|
.on(BullEvent.PAUSED, () => {
|
||||||
// The queue has been paused.
|
// The queue has been paused.
|
||||||
console.log(`${eventType}=paused`)
|
console.info(...getLogParams(eventType, BullEvent.PAUSED))
|
||||||
})
|
})
|
||||||
.on("resumed", (job: Job) => {
|
.on(BullEvent.RESUMED, () => {
|
||||||
// The queue has been resumed.
|
// The queue has been resumed.
|
||||||
console.log(`${eventType}=paused jobId=${job.id}`)
|
console.info(...getLogParams(eventType, BullEvent.RESUMED))
|
||||||
})
|
})
|
||||||
.on("cleaned", (jobs: Job[], type: string) => {
|
.on(BullEvent.CLEANED, (jobs: Job[], type: string) => {
|
||||||
// Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
|
// Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
|
||||||
// jobs, and `type` is the type of jobs cleaned.
|
// jobs, and `type` is the type of jobs cleaned.
|
||||||
console.log(`${eventType}=cleaned length=${jobs.length} type=${type}`)
|
console.info(
|
||||||
|
...getLogParams(
|
||||||
|
eventType,
|
||||||
|
BullEvent.CLEANED,
|
||||||
|
{},
|
||||||
|
{ length: jobs.length, type }
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on("drained", () => {
|
.on(BullEvent.DRAINED, () => {
|
||||||
// Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed)
|
// Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed)
|
||||||
console.log(`${eventType}=drained`)
|
console.info(...getLogParams(eventType, BullEvent.DRAINED))
|
||||||
})
|
})
|
||||||
.on("removed", (job: Job) => {
|
.on(BullEvent.REMOVED, (job: Job) => {
|
||||||
// A job successfully removed.
|
// A job successfully removed.
|
||||||
console.log(`${eventType}=removed jobId=${job.id}`)
|
console.info(...getLogParams(eventType, BullEvent.REMOVED, { job }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,11 +84,25 @@
|
||||||
"@spectrum-css/vars": "3.0.1",
|
"@spectrum-css/vars": "3.0.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
"svelte-flatpickr": "^3.3.2",
|
"svelte-flatpickr": "3.2.3",
|
||||||
"svelte-portal": "^1.0.0"
|
"svelte-portal": "^1.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"loader-utils": "1.4.1"
|
"loader-utils": "1.4.1"
|
||||||
},
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/string-templates"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,9 @@
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
transition: color ease-out 130ms;
|
transition: color ease-out 130ms;
|
||||||
}
|
}
|
||||||
.is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) {
|
.is-selected:not(.spectrum-ActionButton--emphasized):not(
|
||||||
|
.spectrum-ActionButton--quiet
|
||||||
|
) {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
border-color: var(--spectrum-global-color-gray-500);
|
border-color: var(--spectrum-global-color-gray-500);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import "@spectrum-css/button/dist/index-vars.css"
|
import "@spectrum-css/button/dist/index-vars.css"
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
|
||||||
|
export let type
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let cta = false
|
export let cta = false
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
{id}
|
{id}
|
||||||
|
{type}
|
||||||
class:spectrum-Button--cta={cta}
|
class:spectrum-Button--cta={cta}
|
||||||
class:spectrum-Button--primary={primary}
|
class:spectrum-Button--primary={primary}
|
||||||
class:spectrum-Button--secondary={secondary}
|
class:spectrum-Button--secondary={secondary}
|
||||||
|
@ -73,6 +75,7 @@
|
||||||
button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-Button-label {
|
.spectrum-Button-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script>
|
||||||
|
import { slide } from "svelte/transition"
|
||||||
|
|
||||||
|
export let error = null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error-message {
|
||||||
|
background: var(--spectrum-global-color-red-400);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { slide } from "svelte/transition"
|
import ErrorMessage from "./ErrorMessage.svelte"
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
|
@ -55,9 +55,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if error}
|
{#if error}
|
||||||
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
<ErrorMessage {error} />
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -110,13 +108,6 @@
|
||||||
.field {
|
.field {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.error-message {
|
|
||||||
background: var(--spectrum-global-color-red-400);
|
|
||||||
color: white;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.error-icon {
|
.error-icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
||||||
export { default as FancyButton } from "./FancyButton.svelte"
|
export { default as FancyButton } from "./FancyButton.svelte"
|
||||||
export { default as FancyForm } from "./FancyForm.svelte"
|
export { default as FancyForm } from "./FancyForm.svelte"
|
||||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||||
|
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"dev:builder": "routify -c dev:vite",
|
"dev:builder": "routify -c dev:vite",
|
||||||
"dev:vite": "vite --host 0.0.0.0",
|
"dev:vite": "vite --host 0.0.0.0",
|
||||||
"rollup": "rollup -c -w",
|
"rollup": "rollup -c -w",
|
||||||
"test": "vitest"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"globals": {
|
"globals": {
|
||||||
|
@ -62,6 +62,7 @@
|
||||||
"@budibase/frontend-core": "0.0.1",
|
"@budibase/frontend-core": "0.0.1",
|
||||||
"@budibase/shared-core": "0.0.1",
|
"@budibase/shared-core": "0.0.1",
|
||||||
"@budibase/string-templates": "0.0.1",
|
"@budibase/string-templates": "0.0.1",
|
||||||
|
"@budibase/types": "0.0.1",
|
||||||
"@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",
|
||||||
|
@ -116,5 +117,31 @@
|
||||||
"vite": "^3.0.8",
|
"vite": "^3.0.8",
|
||||||
"vitest": "^0.29.2"
|
"vitest": "^0.29.2"
|
||||||
},
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/string-templates",
|
||||||
|
"@budibase/shared-core"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/shared-core",
|
||||||
|
"@budibase/string-templates"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,15 +146,18 @@
|
||||||
|
|
||||||
/* Override default active line highlight colour in dark theme */
|
/* Override default active line highlight colour in dark theme */
|
||||||
div
|
div
|
||||||
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties
|
:global(
|
||||||
.CodeMirror-activeline-background) {
|
.CodeMirror-focused.cm-s-tomorrow-night-eighties
|
||||||
|
.CodeMirror-activeline-background
|
||||||
|
) {
|
||||||
background: rgba(255, 255, 255, 0.075);
|
background: rgba(255, 255, 255, 0.075);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove active line styling when not focused */
|
/* Remove active line styling when not focused */
|
||||||
div
|
div
|
||||||
:global(.CodeMirror:not(.CodeMirror-focused)
|
:global(
|
||||||
.CodeMirror-activeline-background) {
|
.CodeMirror:not(.CodeMirror-focused) .CodeMirror-activeline-background
|
||||||
|
) {
|
||||||
background: unset;
|
background: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
|
let modal
|
||||||
|
|
||||||
|
export let onConfirm
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
modal.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal} on:hide={modal}>
|
||||||
|
<ModalContent
|
||||||
|
title="Your account is currently de-activated"
|
||||||
|
size="S"
|
||||||
|
showCancelButton={true}
|
||||||
|
showCloseIcon={false}
|
||||||
|
confirmText={"View plans"}
|
||||||
|
{onConfirm}
|
||||||
|
>
|
||||||
|
<Body size="S"
|
||||||
|
>Due to the free plan user limit being exceeded, your account has been
|
||||||
|
de-activated. Upgrade your plan to re-activate your account.</Body
|
||||||
|
>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
|
@ -3,7 +3,6 @@ import { temporalStore } from "builderStore"
|
||||||
import { admin, auth, licensing } from "stores/portal"
|
import { admin, auth, licensing } from "stores/portal"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { BANNER_TYPES } from "@budibase/bbui"
|
import { BANNER_TYPES } from "@budibase/bbui"
|
||||||
import { capitalise } from "helpers"
|
|
||||||
|
|
||||||
const oneDayInSeconds = 86400
|
const oneDayInSeconds = 86400
|
||||||
|
|
||||||
|
@ -146,23 +145,19 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
|
||||||
const userLicensing = get(licensing)
|
const userLicensing = get(licensing)
|
||||||
return {
|
return {
|
||||||
key: EXPIRY_KEY,
|
key: EXPIRY_KEY,
|
||||||
type: BANNER_TYPES.WARNING,
|
type: BANNER_TYPES.NEGATIVE,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
defaultCacheFn(EXPIRY_KEY)
|
defaultCacheFn(EXPIRY_KEY)
|
||||||
},
|
},
|
||||||
criteria: () => {
|
criteria: () => {
|
||||||
return userLicensing.warnUserLimit
|
return userLicensing.errUserLimit
|
||||||
},
|
},
|
||||||
message: `${capitalise(
|
message: "Your Budibase account is de-activated. Upgrade your plan",
|
||||||
userLicensing.license.plan.type
|
|
||||||
)} plan changes - Users will be limited to ${
|
|
||||||
userLicensing.userLimit
|
|
||||||
} users in ${userLicensing.userLimitDays}`,
|
|
||||||
...{
|
...{
|
||||||
extraButtonText: "Find out more",
|
extraButtonText: "View plans",
|
||||||
extraButtonAction: () => {
|
extraButtonAction: () => {
|
||||||
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
||||||
window.location.href = "/builder/portal/users/users"
|
window.location.href = "https://budibase.com/pricing/"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
|
||||||
|
export let lockedAction
|
||||||
|
|
||||||
const handleDefaultClick = () => {
|
const handleDefaultClick = () => {
|
||||||
if (window.innerWidth < 640) {
|
if (window.innerWidth < 640) {
|
||||||
goToOverview()
|
goToOverview()
|
||||||
|
@ -29,7 +31,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-row" on:click={handleDefaultClick}>
|
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="app-icon">
|
<div class="app-icon">
|
||||||
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
||||||
|
@ -58,8 +60,11 @@
|
||||||
|
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
<AppLockModal {app} buttonSize="M" />
|
<AppLockModal {app} buttonSize="M" />
|
||||||
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
|
<Button size="S" secondary on:click={lockedAction || goToOverview}
|
||||||
<Button size="S" primary on:click={goToBuilder}>Edit</Button>
|
>Manage</Button
|
||||||
|
>
|
||||||
|
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if $licensing.usageMetrics?.dayPasses >= 100}
|
{#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
|
||||||
<div>
|
<div>
|
||||||
<Layout gap="S" justifyItems="center">
|
<Layout gap="S" justifyItems="center">
|
||||||
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
|
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
|
||||||
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
let template
|
let template
|
||||||
let creationModal
|
let creationModal
|
||||||
let appLimitModal
|
let appLimitModal
|
||||||
|
let accountLockedModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let creatingFromTemplate = false
|
let creatingFromTemplate = false
|
||||||
|
@ -48,6 +50,11 @@
|
||||||
: true)
|
: true)
|
||||||
)
|
)
|
||||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
|
const usersLimitLockAction = $licensing?.errUserLimit
|
||||||
|
? () => accountLockedModal.show()
|
||||||
|
: null
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
|
@ -189,6 +196,9 @@
|
||||||
creatingFromTemplate = true
|
creatingFromTemplate = true
|
||||||
createAppFromTemplateUrl(initInfo.init_template)
|
createAppFromTemplateUrl(initInfo.init_template)
|
||||||
}
|
}
|
||||||
|
if (usersLimitLockAction) {
|
||||||
|
usersLimitLockAction()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting init info")
|
notifications.error("Error getting init info")
|
||||||
}
|
}
|
||||||
|
@ -230,20 +240,30 @@
|
||||||
<Layout noPadding gap="L">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button size="M" cta on:click={initiateAppCreation}>
|
<Button
|
||||||
|
size="M"
|
||||||
|
cta
|
||||||
|
on:click={usersLimitLockAction || initiateAppCreation}
|
||||||
|
>
|
||||||
Create new app
|
Create new app
|
||||||
</Button>
|
</Button>
|
||||||
{#if $apps?.length > 0}
|
{#if $apps?.length > 0}
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
secondary
|
secondary
|
||||||
on:click={$goto("/builder/portal/apps/templates")}
|
on:click={usersLimitLockAction ||
|
||||||
|
$goto("/builder/portal/apps/templates")}
|
||||||
>
|
>
|
||||||
View templates
|
View templates
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !$apps?.length}
|
{#if !$apps?.length}
|
||||||
<Button size="L" quiet secondary on:click={initiateAppImport}>
|
<Button
|
||||||
|
size="L"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={usersLimitLockAction || initiateAppImport}
|
||||||
|
>
|
||||||
Import app
|
Import app
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -267,7 +287,7 @@
|
||||||
|
|
||||||
<div class="app-table">
|
<div class="app-table">
|
||||||
{#each filteredApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<AppRow {app} />
|
<AppRow {app} lockedAction={usersLimitLockAction} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -294,6 +314,11 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<AppLimitModal bind:this={appLimitModal} />
|
<AppLimitModal bind:this={appLimitModal} />
|
||||||
|
<AccountLockedModal
|
||||||
|
bind:this={accountLockedModal}
|
||||||
|
onConfirm={() =>
|
||||||
|
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title {
|
.title {
|
||||||
|
|
|
@ -115,27 +115,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="file"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.sso-link-icon {
|
|
||||||
padding-top: 4px;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
.sso-link {
|
|
||||||
margin-top: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.enforce-sso-title {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.enforce-sso-heading-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.provider-title {
|
.provider-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -143,9 +122,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.provider-title span {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.inputContainer {
|
.inputContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -30,8 +30,8 @@
|
||||||
$: hasError = userData.find(x => x.error != null)
|
$: hasError = userData.find(x => x.error != null)
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userData.length
|
$: userCount = $licensing.userCount + userData.length
|
||||||
$: willReach = licensing.willReachUserLimit(userCount)
|
$: reached = licensing.usersLimitReached(userCount)
|
||||||
$: willExceed = licensing.willExceedUserLimit(userCount)
|
$: exceeded = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
function removeInput(idx) {
|
function removeInput(idx) {
|
||||||
userData = userData.filter((e, i) => i !== idx)
|
userData = userData.filter((e, i) => i !== idx)
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
confirmDisabled={disabled}
|
confirmDisabled={disabled}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
disabled={hasError || !userData.length || willExceed}
|
disabled={hasError || !userData.length || exceeded}
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Email address</Label>
|
<Label>Email address</Label>
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if willReach}
|
{#if reached}
|
||||||
<div class="user-notification">
|
<div class="user-notification">
|
||||||
<Icon name="Info" />
|
<Icon name="Info" />
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -25,10 +25,10 @@
|
||||||
$: invalidEmails = []
|
$: invalidEmails = []
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userEmails.length
|
$: userCount = $licensing.userCount + userEmails.length
|
||||||
$: willExceed = licensing.willExceedUserLimit(userCount)
|
$: exceed = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
$: importDisabled =
|
$: importDisabled =
|
||||||
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed
|
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
||||||
|
|
||||||
const validEmails = userEmails => {
|
const validEmails = userEmails => {
|
||||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if willExceed}
|
{#if exceed}
|
||||||
<div class="user-notification">
|
<div class="user-notification">
|
||||||
<Icon name="Info" />
|
<Icon name="Info" />
|
||||||
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}
|
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}
|
||||||
|
|
|
@ -268,8 +268,6 @@
|
||||||
notifications.error("Error fetching user group data")
|
notifications.error("Error fetching user group data")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let staticUserLimit = $licensing.license.quotas.usage.static.users.value
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="M">
|
<Layout noPadding gap="M">
|
||||||
|
@ -278,7 +276,7 @@
|
||||||
<Body>Add users and control who gets access to your published apps</Body>
|
<Body>Add users and control who gets access to your published apps</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if $licensing.warnUserLimit}
|
{#if $licensing.errUserLimit}
|
||||||
<InlineAlert
|
<InlineAlert
|
||||||
type="error"
|
type="error"
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
|
@ -290,13 +288,9 @@
|
||||||
}}
|
}}
|
||||||
buttonText={isOwner ? "Upgrade" : "View plans"}
|
buttonText={isOwner ? "Upgrade" : "View plans"}
|
||||||
cta
|
cta
|
||||||
header={`Users will soon be limited to ${staticUserLimit}`}
|
header="Account de-activated"
|
||||||
message={`Our free plan is going to be limited to ${staticUserLimit} users in ${$licensing.userLimitDays}.
|
message="Due to the free plan user limit being exceeded, your account has been de-activated.
|
||||||
|
Upgrade your plan to re-activate your account."
|
||||||
This means any users exceeding the limit will be de-activated.
|
|
||||||
|
|
||||||
De-activated users will not able to access the builder or any published apps until you upgrade to one of our paid plans.
|
|
||||||
`}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|
|
@ -25,6 +25,8 @@ export function createDatasourcesStore() {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
selectedDatasourceId: id,
|
selectedDatasourceId: id,
|
||||||
|
// Remove any possible schema error
|
||||||
|
schemaError: null,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { auth, admin } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { StripeStatus } from "components/portal/licensing/constants"
|
import { StripeStatus } from "components/portal/licensing/constants"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||||
import dayjs from "dayjs"
|
import { PlanModel } from "@budibase/types"
|
||||||
|
|
||||||
const UNLIMITED = -1
|
const UNLIMITED = -1
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export const createLicensingStore = () => {
|
||||||
const DEFAULT = {
|
const DEFAULT = {
|
||||||
// navigation
|
// navigation
|
||||||
goToUpgradePage: () => {},
|
goToUpgradePage: () => {},
|
||||||
|
goToPricingPage: () => {},
|
||||||
// the top level license
|
// the top level license
|
||||||
license: undefined,
|
license: undefined,
|
||||||
isFreePlan: true,
|
isFreePlan: true,
|
||||||
|
@ -37,29 +38,37 @@ export const createLicensingStore = () => {
|
||||||
// user limits
|
// user limits
|
||||||
userCount: undefined,
|
userCount: undefined,
|
||||||
userLimit: undefined,
|
userLimit: undefined,
|
||||||
userLimitDays: undefined,
|
|
||||||
userLimitReached: false,
|
userLimitReached: false,
|
||||||
warnUserLimit: false,
|
errUserLimit: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneDayInMilliseconds = 86400000
|
const oneDayInMilliseconds = 86400000
|
||||||
|
|
||||||
const store = writable(DEFAULT)
|
const store = writable(DEFAULT)
|
||||||
|
|
||||||
function willReachUserLimit(userCount, userLimit) {
|
function usersLimitReached(userCount, userLimit) {
|
||||||
if (userLimit === UNLIMITED) {
|
if (userLimit === UNLIMITED) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return userCount >= userLimit
|
return userCount >= userLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
function willExceedUserLimit(userCount, userLimit) {
|
function usersLimitExceeded(userCount, userLimit) {
|
||||||
if (userLimit === UNLIMITED) {
|
if (userLimit === UNLIMITED) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return userCount > userLimit
|
return userCount > userLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isCloud() {
|
||||||
|
let adminStore = get(admin)
|
||||||
|
if (!adminStore.loaded) {
|
||||||
|
await admin.init()
|
||||||
|
adminStore = get(admin)
|
||||||
|
}
|
||||||
|
return adminStore.cloud
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
init: async () => {
|
init: async () => {
|
||||||
actions.setNavigation()
|
actions.setNavigation()
|
||||||
|
@ -71,10 +80,14 @@ export const createLicensingStore = () => {
|
||||||
const goToUpgradePage = () => {
|
const goToUpgradePage = () => {
|
||||||
window.location.href = upgradeUrl
|
window.location.href = upgradeUrl
|
||||||
}
|
}
|
||||||
|
const goToPricingPage = () => {
|
||||||
|
window.open("https://budibase.com/pricing/", "_blank")
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
goToUpgradePage,
|
goToUpgradePage,
|
||||||
|
goToPricingPage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -128,15 +141,15 @@ export const createLicensingStore = () => {
|
||||||
quotaUsage,
|
quotaUsage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
actions.setUsageMetrics()
|
await actions.setUsageMetrics()
|
||||||
},
|
},
|
||||||
willReachUserLimit: userCount => {
|
usersLimitReached: userCount => {
|
||||||
return willReachUserLimit(userCount, get(store).userLimit)
|
return usersLimitReached(userCount, get(store).userLimit)
|
||||||
},
|
},
|
||||||
willExceedUserLimit(userCount) {
|
usersLimitExceeded(userCount) {
|
||||||
return willExceedUserLimit(userCount, get(store).userLimit)
|
return usersLimitExceeded(userCount, get(store).userLimit)
|
||||||
},
|
},
|
||||||
setUsageMetrics: () => {
|
setUsageMetrics: async () => {
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||||
const usage = get(store).quotaUsage
|
const usage = get(store).quotaUsage
|
||||||
const license = get(auth).user.license
|
const license = get(auth).user.license
|
||||||
|
@ -198,11 +211,13 @@ export const createLicensingStore = () => {
|
||||||
const userQuota = license.quotas.usage.static.users
|
const userQuota = license.quotas.usage.static.users
|
||||||
const userLimit = userQuota?.value
|
const userLimit = userQuota?.value
|
||||||
const userCount = usage.usageQuota.users
|
const userCount = usage.usageQuota.users
|
||||||
const userLimitReached = willReachUserLimit(userCount, userLimit)
|
const userLimitReached = usersLimitReached(userCount, userLimit)
|
||||||
const userLimitExceeded = willExceedUserLimit(userCount, userLimit)
|
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
|
||||||
const days = dayjs(userQuota?.startDate).diff(dayjs(), "day")
|
const isCloudAccount = await isCloud()
|
||||||
const userLimitDays = days > 1 ? `${days} days` : "1 day"
|
const errUserLimit =
|
||||||
const warnUserLimit = userQuota?.startDate && userLimitExceeded
|
isCloudAccount &&
|
||||||
|
license.plan.model === PlanModel.PER_USER &&
|
||||||
|
userLimitExceeded
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
|
@ -217,9 +232,8 @@ export const createLicensingStore = () => {
|
||||||
// user limits
|
// user limits
|
||||||
userCount,
|
userCount,
|
||||||
userLimit,
|
userLimit,
|
||||||
userLimitDays,
|
|
||||||
userLimitReached,
|
userLimitReached,
|
||||||
warnUserLimit,
|
errUserLimit,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,7 @@
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": "."
|
||||||
"paths": {
|
|
||||||
"@budibase/types": ["../types/src"],
|
|
||||||
"@budibase/backend-core": ["../backend-core/src"],
|
|
||||||
"@budibase/backend-core/*": ["../backend-core/*.js"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"require": ["tsconfig-paths/register"]
|
"require": ["tsconfig-paths/register"]
|
||||||
|
|
|
@ -63,5 +63,19 @@
|
||||||
"renamer": "^4.0.0",
|
"renamer": "^4.0.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/backend-core"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,5 +65,20 @@
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"loader-utils": "1.4.1"
|
"loader-utils": "1.4.1"
|
||||||
},
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/string-templates",
|
||||||
|
"@budibase/shared-core"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,9 @@
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
div
|
div
|
||||||
:global(.apexcharts-theme-dark
|
:global(
|
||||||
.apexcharts-tooltip-series-group.apexcharts-active) {
|
.apexcharts-theme-dark .apexcharts-tooltip-series-group.apexcharts-active
|
||||||
|
) {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -72,9 +72,11 @@
|
||||||
:global(.spectrum-Form-itemField .spectrum-Textfield--multiline) {
|
:global(.spectrum-Form-itemField .spectrum-Textfield--multiline) {
|
||||||
min-height: calc(var(--height) - 24px);
|
min-height: calc(var(--height) - 24px);
|
||||||
}
|
}
|
||||||
:global(.spectrum-Form--labelsAbove
|
:global(
|
||||||
.spectrum-Form-itemField
|
.spectrum-Form--labelsAbove
|
||||||
.spectrum-Textfield--multiline) {
|
.spectrum-Form-itemField
|
||||||
|
.spectrum-Textfield--multiline
|
||||||
|
) {
|
||||||
min-height: calc(var(--height) - 24px);
|
min-height: calc(var(--height) - 24px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script>
|
||||||
|
import { Layout } from "@budibase/bbui"
|
||||||
|
import Bulgaria from "../../assets/bulgaria.png"
|
||||||
|
import Covanta from "../../assets/covanta.png"
|
||||||
|
import Schnellecke from "../../assets/schnellecke.png"
|
||||||
|
|
||||||
|
const testimonials = [
|
||||||
|
{
|
||||||
|
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
|
||||||
|
name: "Charles Link",
|
||||||
|
role: "Senior Director, Data and Analytics",
|
||||||
|
image: Covanta,
|
||||||
|
imageSize: 105,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
|
||||||
|
name: "Bozhidar Bozhanov",
|
||||||
|
role: "Government of Bulgaria",
|
||||||
|
image: Bulgaria,
|
||||||
|
imageSize: 49,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and it’s now used daily for internal development for those apps that you know you need but don’t feel value in losing days of development to reinvent the wheel.",
|
||||||
|
name: "Davide Lenzarini",
|
||||||
|
role: "IT manager",
|
||||||
|
image: Schnellecke,
|
||||||
|
imageSize: 141,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const testimonial = testimonials[Math.floor(Math.random() * 3)]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="testimonial">
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<img
|
||||||
|
width={testimonial.imageSize}
|
||||||
|
alt="a-happy-budibase-user"
|
||||||
|
src={testimonial.image}
|
||||||
|
/>
|
||||||
|
<div class="text">
|
||||||
|
"{testimonial.text}"
|
||||||
|
</div>
|
||||||
|
<div class="author">
|
||||||
|
<div class="name">{testimonial.name}</div>
|
||||||
|
<div class="company">{testimonial.role}</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.testimonial {
|
||||||
|
width: 380px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
}
|
||||||
|
.company {
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,58 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import SplitPage from "./SplitPage.svelte"
|
import SplitPage from "./SplitPage.svelte"
|
||||||
import { Layout } from "@budibase/bbui"
|
import Testimonial from "./Testimonial.svelte"
|
||||||
import Bulgaria from "../../assets/bulgaria.png"
|
|
||||||
import Covanta from "../../assets/covanta.png"
|
|
||||||
import Schnellecke from "../../assets/schnellecke.png"
|
|
||||||
|
|
||||||
export let enabled = true
|
export let enabled = true
|
||||||
|
|
||||||
const testimonials = [
|
|
||||||
{
|
|
||||||
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
|
|
||||||
name: "Charles Link",
|
|
||||||
role: "Senior Director, Data and Analytics",
|
|
||||||
image: Covanta,
|
|
||||||
imageSize: 105,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
|
|
||||||
name: "Bozhidar Bozhanov",
|
|
||||||
role: "Government of Bulgaria",
|
|
||||||
image: Bulgaria,
|
|
||||||
imageSize: 49,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and it’s now used daily for internal development for those apps that you know you need but don’t feel value in losing days of development to reinvent the wheel.",
|
|
||||||
name: "Davide Lenzarini",
|
|
||||||
role: "IT manager",
|
|
||||||
image: Schnellecke,
|
|
||||||
imageSize: 141,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const testimonial = testimonials[Math.floor(Math.random() * 3)]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
<slot />
|
<slot />
|
||||||
<div class:wrapper={enabled} slot="right">
|
<div class:wrapper={enabled} slot="right">
|
||||||
{#if enabled}
|
{#if enabled}
|
||||||
<div class="testimonial">
|
<Testimonial />
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<img
|
|
||||||
width={testimonial.imageSize}
|
|
||||||
alt="a-happy-budibase-user"
|
|
||||||
src={testimonial.image}
|
|
||||||
/>
|
|
||||||
<div class="text">
|
|
||||||
"{testimonial.text}"
|
|
||||||
</div>
|
|
||||||
<div class="author">
|
|
||||||
<div class="name">{testimonial.name}</div>
|
|
||||||
<div class="company">{testimonial.role}</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SplitPage>
|
</SplitPage>
|
||||||
|
@ -64,20 +21,4 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
.testimonial {
|
|
||||||
width: 380px;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
font-size: var(--font-size-l);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.name {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--spectrum-global-color-gray-900);
|
|
||||||
font-size: var(--font-size-l);
|
|
||||||
}
|
|
||||||
.company {
|
|
||||||
color: var(--spectrum-global-color-gray-700);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
let flatpickr
|
let flatpickr
|
||||||
let isOpen
|
let isOpen
|
||||||
|
|
||||||
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
// Adding the 0- will turn a string like 00:00:00 into a valid ISO
|
||||||
// date, but will make actual ISO dates invalid
|
// date, but will make actual ISO dates invalid
|
||||||
$: time = new Date(`0-${value}`)
|
$: isTimeValue = !isNaN(new Date(`0-${value}`))
|
||||||
$: timeOnly = !isNaN(time) || schema?.timeOnly
|
$: timeOnly = isTimeValue || schema?.timeOnly
|
||||||
$: dateOnly = schema?.dateOnly
|
$: dateOnly = schema?.dateOnly
|
||||||
$: format = timeOnly
|
$: format = timeOnly
|
||||||
? "HH:mm:ss"
|
? "HH:mm:ss"
|
||||||
|
@ -24,6 +24,19 @@
|
||||||
? "MMM D YYYY"
|
? "MMM D YYYY"
|
||||||
: "MMM D YYYY, HH:mm"
|
: "MMM D YYYY, HH:mm"
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
|
$: displayValue = getDisplayValue(value, format, timeOnly, isTimeValue)
|
||||||
|
|
||||||
|
const getDisplayValue = (value, format, timeOnly, isTimeValue) => {
|
||||||
|
if (!value) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Parse full date strings
|
||||||
|
if (!timeOnly || !isTimeValue) {
|
||||||
|
return dayjs(value).format(format)
|
||||||
|
}
|
||||||
|
// Otherwise must be a time string
|
||||||
|
return dayjs(`0-${value}`).format(format)
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure we close flatpickr when unselected
|
// Ensure we close flatpickr when unselected
|
||||||
$: {
|
$: {
|
||||||
|
@ -49,7 +62,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{#if value}
|
{#if value}
|
||||||
{dayjs(timeOnly ? time : value).format(format)}
|
{displayValue}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if editable}
|
{#if editable}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export { default as SplitPage } from "./SplitPage.svelte"
|
export { default as SplitPage } from "./SplitPage.svelte"
|
||||||
export { default as TestimonialPage } from "./TestimonialPage.svelte"
|
export { default as TestimonialPage } from "./TestimonialPage.svelte"
|
||||||
|
export { default as Testimonial } from "./Testimonial.svelte"
|
||||||
export { Grid } from "./grid"
|
export { Grid } from "./grid"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 64a2025727c25d5813832c92eb360de3947b7aa6
|
Subproject commit aea8a4acb0bae6a1036520bf4c6d8cae428cc7d9
|
|
@ -1,6 +1,7 @@
|
||||||
node_modules
|
*
|
||||||
npm-debug.log
|
!/dist/
|
||||||
Dockerfile
|
!/scripts/integrations/oracle/
|
||||||
.dockerignore
|
!/package.json
|
||||||
.git
|
!/docker_run.sh
|
||||||
.gitignore
|
!/builder/
|
||||||
|
!/client/
|
|
@ -15,22 +15,28 @@ ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
||||||
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||||
|
|
||||||
# copy files and install dependencies
|
|
||||||
COPY . ./
|
|
||||||
# handle node-gyp
|
# handle node-gyp
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends g++ make python \
|
&& apt-get install -y --no-install-recommends g++ make python
|
||||||
&& yarn \
|
|
||||||
&& yarn cache clean \
|
|
||||||
&& apt-get remove -y --purge --auto-remove g++ make python \
|
|
||||||
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
|
||||||
RUN yarn global add pm2
|
RUN yarn global add pm2
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
# Install client for oracle datasource
|
# Install client for oracle datasource
|
||||||
RUN apt-get install unzip libaio1
|
RUN apt-get install unzip libaio1
|
||||||
|
COPY scripts/integrations/oracle/ scripts/integrations/oracle/
|
||||||
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
RUN yarn install --frozen-lockfile --production=true
|
||||||
|
# Remove unneeded data from file system to reduce image size
|
||||||
|
RUN yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python \
|
||||||
|
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
||||||
|
|
||||||
|
COPY dist/ dist/
|
||||||
|
COPY docker_run.sh .
|
||||||
|
COPY builder/ builder/
|
||||||
|
COPY client/ client/
|
||||||
|
|
||||||
EXPOSE 4001
|
EXPOSE 4001
|
||||||
|
|
||||||
# have to add node environment production after install
|
# have to add node environment production after install
|
||||||
|
@ -38,4 +44,6 @@ EXPOSE 4001
|
||||||
# which are actually needed to get this environment up and running
|
# which are actually needed to get this environment up and running
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV CLUSTER_MODE=${CLUSTER_MODE}
|
ENV CLUSTER_MODE=${CLUSTER_MODE}
|
||||||
|
ENV TOP_LEVEL_PATH=/app
|
||||||
|
|
||||||
CMD ["./docker_run.sh"]
|
CMD ["./docker_run.sh"]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Config } from "@jest/types"
|
import { Config } from "@jest/types"
|
||||||
|
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
|
import { join } from "path"
|
||||||
const preset = require("ts-jest/jest-preset")
|
const preset = require("ts-jest/jest-preset")
|
||||||
|
|
||||||
const baseConfig: Config.InitialProjectOptions = {
|
const baseConfig: Config.InitialProjectOptions = {
|
||||||
|
@ -49,4 +50,6 @@ const config: Config.InitialOptions = {
|
||||||
coverageReporters: ["lcov", "json", "clover"],
|
coverageReporters: ["lcov", "json", "clover"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.env.TOP_LEVEL_PATH = join(__dirname, "..", "..")
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"src/**/*.spec.js",
|
"src/**/*.spec.js",
|
||||||
"../backend-core/dist/**/*"
|
"../backend-core/dist/**/*"
|
||||||
],
|
],
|
||||||
"exec": "ts-node src/index.ts"
|
"exec": "node ./scripts/build.js && node ./dist/index.js"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,22 +10,22 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf dist/",
|
"prebuild": "rimraf dist/",
|
||||||
"build": "tsc -p tsconfig.build.json && mv dist/src/* dist/ && rimraf dist/src/",
|
"build": "node ./scripts/build.js",
|
||||||
|
"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",
|
||||||
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
|
|
||||||
"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",
|
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && yarn build --configuration=production",
|
||||||
"build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
"build:docker": "yarn predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
||||||
"build:docs": "node ./scripts/docs/generate.js open",
|
"build:docs": "node ./scripts/docs/generate.js open",
|
||||||
"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",
|
||||||
"dev:stack:down": "node scripts/dev/manage.js down",
|
"dev:stack:down": "node scripts/dev/manage.js down",
|
||||||
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
|
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
|
||||||
"dev:builder": "yarn run dev:stack:up && nodemon",
|
"dev:builder": "yarn run dev:stack:up && rimraf dist/ && nodemon",
|
||||||
"dev:built": "yarn run dev:stack:up && yarn run run:docker",
|
"dev:built": "yarn run dev:stack:up && yarn run run:docker",
|
||||||
"specs": "ts-node specs/generate.ts && openapi-typescript specs/openapi.yaml --output src/definitions/openapi.ts",
|
"specs": "ts-node specs/generate.ts && openapi-typescript specs/openapi.yaml --output src/definitions/openapi.ts",
|
||||||
"initialise": "node scripts/initialise.js",
|
"initialise": "node scripts/initialise.js",
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
"mysql2": "2.3.3",
|
"mysql2": "2.3.3",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
"open": "8.4.0",
|
"open": "8.4.0",
|
||||||
"pg": "8.5.1",
|
"pg": "8.10.0",
|
||||||
"posthog-node": "1.3.0",
|
"posthog-node": "1.3.0",
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-adapter-memory": "7.2.2",
|
"pouchdb-adapter-memory": "7.2.2",
|
||||||
|
@ -118,8 +118,8 @@
|
||||||
"validate.js": "0.13.1",
|
"validate.js": "0.13.1",
|
||||||
"vm2": "3.9.17",
|
"vm2": "3.9.17",
|
||||||
"worker-farm": "1.7.0",
|
"worker-farm": "1.7.0",
|
||||||
"xml2js": "0.5.0",
|
"yargs": "13.2.4",
|
||||||
"yargs": "13.2.4"
|
"xml2js": "0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.17.4",
|
"@babel/core": "7.17.4",
|
||||||
|
@ -141,6 +141,7 @@
|
||||||
"@types/node": "14.18.20",
|
"@types/node": "14.18.20",
|
||||||
"@types/node-fetch": "2.6.1",
|
"@types/node-fetch": "2.6.1",
|
||||||
"@types/oracledb": "5.2.2",
|
"@types/oracledb": "5.2.2",
|
||||||
|
"@types/pg": "8.6.6",
|
||||||
"@types/pouchdb": "6.4.0",
|
"@types/pouchdb": "6.4.0",
|
||||||
"@types/redis": "4.0.11",
|
"@types/redis": "4.0.11",
|
||||||
"@types/server-destroy": "1.0.1",
|
"@types/server-destroy": "1.0.1",
|
||||||
|
@ -176,5 +177,20 @@
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"oracledb": "5.3.0"
|
"oracledb": "5.3.0"
|
||||||
},
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"test": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/string-templates",
|
||||||
|
"@budibase/shared-core"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
script: "dist/index.js",
|
script: "./dist/index.js",
|
||||||
instances: "max",
|
instances: "max",
|
||||||
exec_mode: "cluster",
|
exec_mode: "cluster",
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/usr/bin/node
|
||||||
|
const { join } = require("path")
|
||||||
|
const fs = require("fs")
|
||||||
|
const coreBuild = require("../../../scripts/build")
|
||||||
|
|
||||||
|
const dir = join(__dirname, "..")
|
||||||
|
const entryPath = join(dir, "src")
|
||||||
|
const outfilePath = join(dir, "dist")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reasoning for this is that now our built version is simple
|
||||||
|
* dist/index.js - any kind of threaded approach in Node.js requires
|
||||||
|
* a runner file to work from - I played around with a lot of
|
||||||
|
* different methods, but we really want to be able to use forks.
|
||||||
|
*
|
||||||
|
* Rather than trying to rewrite so that forks run the whole system,
|
||||||
|
* I instead went down a path of building the individual threads so
|
||||||
|
* that we have runner files for each of them e.g. dist/automations.js
|
||||||
|
* and dist/query.js - these can be ran totally independently and then
|
||||||
|
* the parent process can pass down data for processing to them.
|
||||||
|
*
|
||||||
|
* The ignoring is simply to remove the files which really don't need
|
||||||
|
* to be built - they could be built and it wouldn't cause any issues,
|
||||||
|
* but this just means if any further threads are added in future
|
||||||
|
* they will naturally work (rather than including, which would mean
|
||||||
|
* adjustments to the build files).
|
||||||
|
*/
|
||||||
|
const ignoredFiles = ["definitions", "index", "utils"]
|
||||||
|
const threadNames = fs
|
||||||
|
.readdirSync(join(dir, "src", "threads"))
|
||||||
|
.filter(path => !ignoredFiles.find(file => path.includes(file)))
|
||||||
|
.map(path => path.replace(".ts", ""))
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
entry: join(entryPath, "index.ts"),
|
||||||
|
out: join(outfilePath, "index.js"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for (let name of threadNames) {
|
||||||
|
files.push({
|
||||||
|
entry: join(entryPath, "threads", `${name}.ts`),
|
||||||
|
out: join(outfilePath, `${name}.js`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let file of files) {
|
||||||
|
coreBuild(file.entry, file.out)
|
||||||
|
}
|
|
@ -18,11 +18,72 @@ import {
|
||||||
Row,
|
Row,
|
||||||
CreateDatasourceResponse,
|
CreateDatasourceResponse,
|
||||||
UpdateDatasourceResponse,
|
UpdateDatasourceResponse,
|
||||||
UpdateDatasourceRequest,
|
|
||||||
CreateDatasourceRequest,
|
CreateDatasourceRequest,
|
||||||
|
VerifyDatasourceRequest,
|
||||||
|
VerifyDatasourceResponse,
|
||||||
|
FetchDatasourceInfoResponse,
|
||||||
|
IntegrationBase,
|
||||||
|
DatasourcePlus,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
|
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(
|
||||||
|
datasource: Datasource
|
||||||
|
): Promise<IntegrationBase | DatasourcePlus> {
|
||||||
|
const Connector = await getIntegration(datasource.source)
|
||||||
|
// can't enrich if it doesn't have an ID yet
|
||||||
|
if (datasource._id) {
|
||||||
|
datasource = await sdk.datasources.enrich(datasource)
|
||||||
|
}
|
||||||
|
// Connect to the DB and build the schema
|
||||||
|
return new Connector(datasource.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildSchemaHelper(datasource: Datasource) {
|
||||||
|
const connector = (await getConnector(datasource)) as DatasourcePlus
|
||||||
|
await connector.buildSchema(datasource._id!, datasource.entities!)
|
||||||
|
|
||||||
|
const errors = connector.schemaErrors
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
// Get internal tables
|
// Get internal tables
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -66,6 +127,48 @@ export async function fetch(ctx: UserCtx) {
|
||||||
ctx.body = [bbInternalDb, ...datasources]
|
ctx.body = [bbInternalDb, ...datasources]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verify(
|
||||||
|
ctx: UserCtx<VerifyDatasourceRequest, VerifyDatasourceResponse>
|
||||||
|
) {
|
||||||
|
const { datasource } = ctx.request.body
|
||||||
|
let existingDatasource: undefined | Datasource
|
||||||
|
if (datasource._id) {
|
||||||
|
existingDatasource = await sdk.datasources.get(datasource._id)
|
||||||
|
}
|
||||||
|
let enrichedDatasource = datasource
|
||||||
|
if (existingDatasource) {
|
||||||
|
enrichedDatasource = sdk.datasources.mergeConfigs(
|
||||||
|
datasource,
|
||||||
|
existingDatasource
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const connector = await getConnector(enrichedDatasource)
|
||||||
|
if (!connector.testConnection) {
|
||||||
|
ctx.throw(400, "Connection information verification not supported")
|
||||||
|
}
|
||||||
|
const response = await connector.testConnection()
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
connected: response.connected,
|
||||||
|
error: response.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function information(
|
||||||
|
ctx: UserCtx<void, FetchDatasourceInfoResponse>
|
||||||
|
) {
|
||||||
|
const datasourceId = ctx.params.datasourceId
|
||||||
|
const datasource = await sdk.datasources.get(datasourceId, { enriched: true })
|
||||||
|
const connector = (await getConnector(datasource)) as DatasourcePlus
|
||||||
|
if (!connector.getTableNames) {
|
||||||
|
ctx.throw(400, "Table name fetching not supported by datasource")
|
||||||
|
}
|
||||||
|
const tableNames = await connector.getTableNames()
|
||||||
|
ctx.body = {
|
||||||
|
tableNames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function buildSchemaFromDb(ctx: UserCtx) {
|
export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||||
|
@ -311,51 +414,3 @@ export async function query(ctx: UserCtx) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 buildSchemaHelper(datasource: Datasource) {
|
|
||||||
const Connector = await getIntegration(datasource.source)
|
|
||||||
datasource = await sdk.datasources.enrich(datasource)
|
|
||||||
// Connect to the DB and build the schema
|
|
||||||
const connector = new Connector(datasource.config)
|
|
||||||
await connector.buildSchema(datasource._id, datasource.entities)
|
|
||||||
|
|
||||||
const errors = connector.schemaErrors
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getDefinitions } from "../../integrations"
|
import { getDefinition, getDefinitions } from "../../integrations"
|
||||||
import { BBContext } from "@budibase/types"
|
import { BBContext } from "@budibase/types"
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: BBContext) {
|
||||||
|
@ -7,7 +7,7 @@ export async function fetch(ctx: BBContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: BBContext) {
|
export async function find(ctx: BBContext) {
|
||||||
const defs = await getDefinitions()
|
const def = await getDefinition(ctx.params.type)
|
||||||
|
ctx.body = def
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = defs[ctx.params.type]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,7 @@ export const serveApp = async function (ctx: any) {
|
||||||
? objectStore.getGlobalFileUrl("settings", "logoUrl")
|
? objectStore.getGlobalFileUrl("settings", "logoUrl")
|
||||||
: "",
|
: "",
|
||||||
})
|
})
|
||||||
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
|
const appHbs = loadHandlebarsFile(`${__dirname}/app.hbs`)
|
||||||
ctx.body = await processString(appHbs, {
|
ctx.body = await processString(appHbs, {
|
||||||
head,
|
head,
|
||||||
body: html,
|
body: html,
|
||||||
|
@ -161,7 +161,7 @@ export const serveApp = async function (ctx: any) {
|
||||||
: "",
|
: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
|
const appHbs = loadHandlebarsFile(`${__dirname}/app.hbs`)
|
||||||
ctx.body = await processString(appHbs, {
|
ctx.body = await processString(appHbs, {
|
||||||
head,
|
head,
|
||||||
body: html,
|
body: html,
|
||||||
|
@ -177,7 +177,7 @@ export const serveBuilderPreview = async function (ctx: any) {
|
||||||
|
|
||||||
if (!env.isJest()) {
|
if (!env.isJest()) {
|
||||||
let appId = context.getAppId()
|
let appId = context.getAppId()
|
||||||
const previewHbs = loadHandlebarsFile(`${__dirname}/templates/preview.hbs`)
|
const previewHbs = loadHandlebarsFile(`${__dirname}/preview.hbs`)
|
||||||
ctx.body = await processString(previewHbs, {
|
ctx.body = await processString(previewHbs, {
|
||||||
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,16 @@ router
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
datasourceController.fetch
|
datasourceController.fetch
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/api/datasources/verify",
|
||||||
|
authorized(permissions.BUILDER),
|
||||||
|
datasourceController.verify
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/api/datasources/:datasourceId/info",
|
||||||
|
authorized(permissions.BUILDER),
|
||||||
|
datasourceController.information
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/datasources/:datasourceId",
|
"/api/datasources/:datasourceId",
|
||||||
authorized(
|
authorized(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import authorized from "../../middleware/authorized"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { permissions } from "@budibase/backend-core"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import { paramResource } from "../../middleware/resourceId"
|
import { paramResource } from "../../middleware/resourceId"
|
||||||
|
import { devClientLibPath } from "../../utilities/fileSystem"
|
||||||
const { BUILDER, PermissionType, PermissionLevel } = permissions
|
const { BUILDER, PermissionType, PermissionLevel } = permissions
|
||||||
|
|
||||||
const router: Router = new Router()
|
const router: Router = new Router()
|
||||||
|
@ -17,7 +18,8 @@ router.param("file", async (file: any, ctx: any, next: any) => {
|
||||||
}
|
}
|
||||||
// test serves from require
|
// test serves from require
|
||||||
if (env.isTest()) {
|
if (env.isTest()) {
|
||||||
ctx.devPath = require.resolve("@budibase/client").split(ctx.file)[0]
|
const path = devClientLibPath()
|
||||||
|
ctx.devPath = path.split(ctx.file)[0]
|
||||||
} else if (env.isDev()) {
|
} else if (env.isDev()) {
|
||||||
// Serving the client library from your local dir in dev
|
// Serving the client library from your local dir in dev
|
||||||
ctx.devPath = budibaseTempDir()
|
ctx.devPath = budibaseTempDir()
|
||||||
|
|
|
@ -87,7 +87,7 @@ describe("/datasources", () => {
|
||||||
expect(contents.rows.length).toEqual(1)
|
expect(contents.rows.length).toEqual(1)
|
||||||
|
|
||||||
// update the datasource to remove the variables
|
// update the datasource to remove the variables
|
||||||
datasource.config.dynamicVariables = []
|
datasource.config!.dynamicVariables = []
|
||||||
const res = await request
|
const res = await request
|
||||||
.put(`/api/datasources/${datasource._id}`)
|
.put(`/api/datasources/${datasource._id}`)
|
||||||
.send(datasource)
|
.send(datasource)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { checkTestFlag } from "../utilities/redis"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { context, db as dbCore } from "@budibase/backend-core"
|
import { context, db as dbCore } from "@budibase/backend-core"
|
||||||
import { Automation, Row } from "@budibase/types"
|
import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types"
|
||||||
|
|
||||||
export const TRIGGER_DEFINITIONS = definitions
|
export const TRIGGER_DEFINITIONS = definitions
|
||||||
const JOB_OPTS = {
|
const JOB_OPTS = {
|
||||||
|
@ -109,14 +109,16 @@ export async function externalTrigger(
|
||||||
}
|
}
|
||||||
params.fields = coercedFields
|
params.fields = coercedFields
|
||||||
}
|
}
|
||||||
const data: Record<string, any> = { automation, event: params }
|
|
||||||
|
const data: AutomationData = { automation, event: params as any }
|
||||||
if (getResponses) {
|
if (getResponses) {
|
||||||
data.event = {
|
data.event = {
|
||||||
...data.event,
|
...data.event,
|
||||||
appId: context.getAppId(),
|
appId: context.getAppId(),
|
||||||
automation,
|
automation,
|
||||||
}
|
}
|
||||||
return utils.processEvent({ data })
|
const job = { data } as AutomationJob
|
||||||
|
return utils.processEvent(job)
|
||||||
} else {
|
} else {
|
||||||
return automationQueue.add(data, JOB_OPTS)
|
return automationQueue.add(data, JOB_OPTS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { db as dbCore, context } from "@budibase/backend-core"
|
||||||
import { getAutomationMetadataParams } from "../db/utils"
|
import { getAutomationMetadataParams } from "../db/utils"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { Automation, WebhookActionType } from "@budibase/types"
|
import { Automation, AutomationJob, WebhookActionType } from "@budibase/types"
|
||||||
import sdk from "../sdk"
|
import sdk from "../sdk"
|
||||||
|
|
||||||
const REBOOT_CRON = "@reboot"
|
const REBOOT_CRON = "@reboot"
|
||||||
|
@ -16,27 +16,40 @@ const WH_STEP_ID = definitions.WEBHOOK.stepId
|
||||||
const CRON_STEP_ID = definitions.CRON.stepId
|
const CRON_STEP_ID = definitions.CRON.stepId
|
||||||
const Runner = new Thread(ThreadType.AUTOMATION)
|
const Runner = new Thread(ThreadType.AUTOMATION)
|
||||||
|
|
||||||
const jobMessage = (job: any, message: string) => {
|
function loggingArgs(job: AutomationJob) {
|
||||||
return `app=${job.data.event.appId} automation=${job.data.automation._id} jobId=${job.id} trigger=${job.data.automation.definition.trigger.event} : ${message}`
|
return [
|
||||||
|
{
|
||||||
|
_logKey: "automation",
|
||||||
|
trigger: job.data.automation.definition.trigger.event,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_logKey: "bull",
|
||||||
|
jobId: job.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processEvent(job: any) {
|
export async function processEvent(job: AutomationJob) {
|
||||||
try {
|
const appId = job.data.event.appId!
|
||||||
const automationId = job.data.automation._id
|
const automationId = job.data.automation._id!
|
||||||
console.log(jobMessage(job, "running"))
|
const task = async () => {
|
||||||
// need to actually await these so that an error can be captured properly
|
try {
|
||||||
return await context.doInContext(job.data.event.appId, async () => {
|
// need to actually await these so that an error can be captured properly
|
||||||
|
console.log("automation running", ...loggingArgs(job))
|
||||||
|
|
||||||
const runFn = () => Runner.run(job)
|
const runFn = () => Runner.run(job)
|
||||||
return quotas.addAutomation(runFn, {
|
const result = await quotas.addAutomation(runFn, {
|
||||||
automationId,
|
automationId,
|
||||||
})
|
})
|
||||||
})
|
console.log("automation completed", ...loggingArgs(job))
|
||||||
} catch (err) {
|
return result
|
||||||
const errJson = JSON.stringify(err)
|
} catch (err) {
|
||||||
console.error(jobMessage(job, `was unable to run - ${errJson}`))
|
console.error(`automation was unable to run`, err, ...loggingArgs(job))
|
||||||
console.trace(err)
|
return { err }
|
||||||
return { err }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await context.doInAutomationContext({ appId, automationId, task })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateTestHistory(
|
export async function updateTestHistory(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { objectStore, roles, constants } from "@budibase/backend-core"
|
import { objectStore, roles, constants } from "@budibase/backend-core"
|
||||||
|
import { FieldType as FieldTypes } from "@budibase/types"
|
||||||
export { FieldType as FieldTypes, RelationshipTypes } from "@budibase/types"
|
export { FieldType as FieldTypes, RelationshipTypes } from "@budibase/types"
|
||||||
|
|
||||||
export enum FilterTypes {
|
export enum FilterTypes {
|
||||||
|
@ -24,14 +25,14 @@ export const NoEmptyFilterStrings = [
|
||||||
]
|
]
|
||||||
|
|
||||||
export const CanSwitchTypes = [
|
export const CanSwitchTypes = [
|
||||||
[exports.FieldTypes.JSON, exports.FieldTypes.ARRAY],
|
[FieldTypes.JSON, FieldTypes.ARRAY],
|
||||||
[
|
[
|
||||||
exports.FieldTypes.STRING,
|
FieldTypes.STRING,
|
||||||
exports.FieldTypes.OPTIONS,
|
FieldTypes.OPTIONS,
|
||||||
exports.FieldTypes.LONGFORM,
|
FieldTypes.LONGFORM,
|
||||||
exports.FieldTypes.BARCODEQR,
|
FieldTypes.BARCODEQR,
|
||||||
],
|
],
|
||||||
[exports.FieldTypes.BOOLEAN, exports.FieldTypes.NUMBER],
|
[FieldTypes.BOOLEAN, FieldTypes.NUMBER],
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SwitchableTypes = CanSwitchTypes.reduce((prev, current) =>
|
export const SwitchableTypes = CanSwitchTypes.reduce((prev, current) =>
|
||||||
|
@ -77,9 +78,9 @@ export const USERS_TABLE_SCHEMA = {
|
||||||
// TODO: ADMIN PANEL - when implemented this doesn't need to be carried out
|
// TODO: ADMIN PANEL - when implemented this doesn't need to be carried out
|
||||||
schema: {
|
schema: {
|
||||||
email: {
|
email: {
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
email: true,
|
email: true,
|
||||||
length: {
|
length: {
|
||||||
maximum: "",
|
maximum: "",
|
||||||
|
@ -92,27 +93,27 @@ export const USERS_TABLE_SCHEMA = {
|
||||||
firstName: {
|
firstName: {
|
||||||
name: "firstName",
|
name: "firstName",
|
||||||
fieldName: "firstName",
|
fieldName: "firstName",
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lastName: {
|
lastName: {
|
||||||
name: "lastName",
|
name: "lastName",
|
||||||
fieldName: "lastName",
|
fieldName: "lastName",
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roleId: {
|
roleId: {
|
||||||
fieldName: "roleId",
|
fieldName: "roleId",
|
||||||
name: "roleId",
|
name: "roleId",
|
||||||
type: exports.FieldTypes.OPTIONS,
|
type: FieldTypes.OPTIONS,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
presence: false,
|
presence: false,
|
||||||
inclusion: Object.values(roles.BUILTIN_ROLE_IDS),
|
inclusion: Object.values(roles.BUILTIN_ROLE_IDS),
|
||||||
},
|
},
|
||||||
|
@ -120,9 +121,9 @@ export const USERS_TABLE_SCHEMA = {
|
||||||
status: {
|
status: {
|
||||||
fieldName: "status",
|
fieldName: "status",
|
||||||
name: "status",
|
name: "status",
|
||||||
type: exports.FieldTypes.OPTIONS,
|
type: FieldTypes.OPTIONS,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: exports.FieldTypes.STRING,
|
type: FieldTypes.STRING,
|
||||||
presence: false,
|
presence: false,
|
||||||
inclusion: Object.values(constants.UserStatus),
|
inclusion: Object.values(constants.UserStatus),
|
||||||
},
|
},
|
||||||
|
|
|
@ -140,7 +140,7 @@ export function init(endpoint: string) {
|
||||||
docClient = new AWS.DynamoDB.DocumentClient(docClientParams)
|
docClient = new AWS.DynamoDB.DocumentClient(docClientParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!env.isProd()) {
|
if (!env.isProd() && !env.isJest()) {
|
||||||
env._set("AWS_ACCESS_KEY_ID", "KEY_ID")
|
env._set("AWS_ACCESS_KEY_ID", "KEY_ID")
|
||||||
env._set("AWS_SECRET_ACCESS_KEY", "SECRET_KEY")
|
env._set("AWS_SECRET_ACCESS_KEY", "SECRET_KEY")
|
||||||
init("http://localhost:8333")
|
init("http://localhost:8333")
|
||||||
|
|
|
@ -25,7 +25,9 @@ export async function runView(
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
let fn = (doc: Document, emit: any) => emit(doc._id)
|
let fn = (doc: Document, emit: any) => emit(doc._id)
|
||||||
eval("fn = " + view?.map?.replace("function (doc)", "function (doc, emit)"))
|
;(0, eval)(
|
||||||
|
"fn = " + view?.map?.replace("function (doc)", "function (doc, emit)")
|
||||||
|
)
|
||||||
const queryFns: any = {
|
const queryFns: any = {
|
||||||
meta: view.meta,
|
meta: view.meta,
|
||||||
map: fn,
|
map: fn,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AutomationResults, AutomationStep, Document } from "@budibase/types"
|
import { AutomationResults, AutomationStep } from "@budibase/types"
|
||||||
|
|
||||||
export enum LoopStepType {
|
export enum LoopStepType {
|
||||||
ARRAY = "Array",
|
ARRAY = "Array",
|
||||||
|
@ -27,7 +27,3 @@ export interface AutomationContext extends AutomationResults {
|
||||||
env?: Record<string, string>
|
env?: Record<string, string>
|
||||||
trigger: any
|
trigger: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationMetadata extends Document {
|
|
||||||
errorCount?: number
|
|
||||||
}
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ const environment = {
|
||||||
isInThread: () => {
|
isInThread: () => {
|
||||||
return process.env.FORKED_PROCESS
|
return process.env.FORKED_PROCESS
|
||||||
},
|
},
|
||||||
|
TOP_LEVEL_PATH: process.env.TOP_LEVEL_PATH,
|
||||||
}
|
}
|
||||||
|
|
||||||
// threading can cause memory issues with node-ts in development
|
// threading can cause memory issues with node-ts in development
|
||||||
|
|
|
@ -19,7 +19,6 @@ import _ from "lodash"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
import { utils } from "@budibase/backend-core"
|
import { utils } from "@budibase/backend-core"
|
||||||
import { GenericContainer } from "testcontainers"
|
import { GenericContainer } from "testcontainers"
|
||||||
import { generateRowIdField } from "../integrations/utils"
|
|
||||||
|
|
||||||
const config = setup.getConfig()!
|
const config = setup.getConfig()!
|
||||||
|
|
||||||
|
@ -27,7 +26,7 @@ jest.setTimeout(30000)
|
||||||
|
|
||||||
jest.unmock("pg")
|
jest.unmock("pg")
|
||||||
|
|
||||||
describe("row api - postgres", () => {
|
describe("postgres integrations", () => {
|
||||||
let makeRequest: MakeRequestResponse,
|
let makeRequest: MakeRequestResponse,
|
||||||
postgresDatasource: Datasource,
|
postgresDatasource: Datasource,
|
||||||
primaryPostgresTable: Table,
|
primaryPostgresTable: Table,
|
||||||
|
@ -53,8 +52,8 @@ describe("row api - postgres", () => {
|
||||||
makeRequest = generateMakeRequest(apiKey, true)
|
makeRequest = generateMakeRequest(apiKey, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
function pgDatasourceConfig() {
|
||||||
postgresDatasource = await config.createDatasource({
|
return {
|
||||||
datasource: {
|
datasource: {
|
||||||
type: "datasource",
|
type: "datasource",
|
||||||
source: SourceName.POSTGRES,
|
source: SourceName.POSTGRES,
|
||||||
|
@ -71,7 +70,11 @@ describe("row api - postgres", () => {
|
||||||
ca: false,
|
ca: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
postgresDatasource = await config.createDatasource(pgDatasourceConfig())
|
||||||
|
|
||||||
async function createAuxTable(prefix: string) {
|
async function createAuxTable(prefix: string) {
|
||||||
return await config.createTable({
|
return await config.createTable({
|
||||||
|
@ -1025,4 +1028,43 @@ describe("row api - postgres", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("POST /api/datasources/verify", () => {
|
||||||
|
it("should be able to verify the connection", async () => {
|
||||||
|
const config = pgDatasourceConfig()
|
||||||
|
const response = await makeRequest(
|
||||||
|
"post",
|
||||||
|
"/api/datasources/verify",
|
||||||
|
config
|
||||||
|
)
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.body.connected).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should state an invalid datasource cannot connect", async () => {
|
||||||
|
const config = pgDatasourceConfig()
|
||||||
|
config.datasource.config.password = "wrongpassword"
|
||||||
|
const response = await makeRequest(
|
||||||
|
"post",
|
||||||
|
"/api/datasources/verify",
|
||||||
|
config
|
||||||
|
)
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.body.connected).toBe(false)
|
||||||
|
expect(response.body.error).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /api/datasources/:datasourceId/info", () => {
|
||||||
|
it("should fetch information about postgres datasource", async () => {
|
||||||
|
const primaryName = primaryPostgresTable.name
|
||||||
|
const response = await makeRequest(
|
||||||
|
"get",
|
||||||
|
`/api/datasources/${postgresDatasource._id}/info`
|
||||||
|
)
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.body.tableNames).toBeDefined()
|
||||||
|
expect(response.body.tableNames.indexOf(primaryName)).not.toBe(-1)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import {
|
import {
|
||||||
Integration,
|
ConnectionInfo,
|
||||||
|
DatasourceFeature,
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
QueryType,
|
Integration,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
|
QueryType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
const Airtable = require("airtable")
|
import Airtable from "airtable"
|
||||||
|
|
||||||
interface AirtableConfig {
|
interface AirtableConfig {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
|
@ -18,6 +20,7 @@ const SCHEMA: Integration = {
|
||||||
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.",
|
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.",
|
||||||
friendlyName: "Airtable",
|
friendlyName: "Airtable",
|
||||||
type: "Spreadsheet",
|
type: "Spreadsheet",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
apiKey: {
|
apiKey: {
|
||||||
type: DatasourceFieldType.PASSWORD,
|
type: DatasourceFieldType.PASSWORD,
|
||||||
|
@ -81,13 +84,37 @@ const SCHEMA: Integration = {
|
||||||
|
|
||||||
class AirtableIntegration implements IntegrationBase {
|
class AirtableIntegration implements IntegrationBase {
|
||||||
private config: AirtableConfig
|
private config: AirtableConfig
|
||||||
private client: any
|
private client
|
||||||
|
|
||||||
constructor(config: AirtableConfig) {
|
constructor(config: AirtableConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.client = new Airtable(config).base(config.base)
|
this.client = new Airtable(config).base(config.base)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
|
const mockTable = Date.now().toString()
|
||||||
|
try {
|
||||||
|
await this.client.makeRequest({
|
||||||
|
path: `/${mockTable}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { connected: true }
|
||||||
|
} catch (e: any) {
|
||||||
|
if (
|
||||||
|
e.message ===
|
||||||
|
`Could not find table ${mockTable} in application ${this.config.base}`
|
||||||
|
) {
|
||||||
|
// The request managed to check the application, so the credentials are valid
|
||||||
|
return { connected: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
error: e.message as string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(query: { table: any; json: any }) {
|
async create(query: { table: any; json: any }) {
|
||||||
const { table, json } = query
|
const { table, json } = query
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,11 @@ import {
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
QueryType,
|
QueryType,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
const { Database, aql } = require("arangojs")
|
import { Database, aql } from "arangojs"
|
||||||
|
|
||||||
interface ArangodbConfig {
|
interface ArangodbConfig {
|
||||||
url: string
|
url: string
|
||||||
|
@ -21,6 +23,7 @@ const SCHEMA: Integration = {
|
||||||
type: "Non-relational",
|
type: "Non-relational",
|
||||||
description:
|
description:
|
||||||
"ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ",
|
"ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -58,7 +61,7 @@ const SCHEMA: Integration = {
|
||||||
|
|
||||||
class ArangoDBIntegration implements IntegrationBase {
|
class ArangoDBIntegration implements IntegrationBase {
|
||||||
private config: ArangodbConfig
|
private config: ArangodbConfig
|
||||||
private client: any
|
private client
|
||||||
|
|
||||||
constructor(config: ArangodbConfig) {
|
constructor(config: ArangodbConfig) {
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
|
@ -74,6 +77,19 @@ class ArangoDBIntegration implements IntegrationBase {
|
||||||
this.client = new Database(newConfig)
|
this.client = new Database(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.client.get()
|
||||||
|
response.connected = true
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
async read(query: { sql: any }) {
|
async read(query: { sql: any }) {
|
||||||
try {
|
try {
|
||||||
const result = await this.client.query(query.sql)
|
const result = await this.client.query(query.sql)
|
||||||
|
|
|
@ -206,4 +206,3 @@ class SqlTableQueryBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SqlTableQueryBuilder
|
export default SqlTableQueryBuilder
|
||||||
module.exports = SqlTableQueryBuilder
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import {
|
import {
|
||||||
|
ConnectionInfo,
|
||||||
|
DatasourceFeature,
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
Document,
|
Document,
|
||||||
Integration,
|
Integration,
|
||||||
|
@ -18,6 +20,7 @@ const SCHEMA: Integration = {
|
||||||
type: "Non-relational",
|
type: "Non-relational",
|
||||||
description:
|
description:
|
||||||
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
|
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -61,21 +64,32 @@ const SCHEMA: Integration = {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CouchDBIntegration implements IntegrationBase {
|
class CouchDBIntegration implements IntegrationBase {
|
||||||
private config: CouchDBConfig
|
private readonly client: dbCore.DatabaseImpl
|
||||||
private readonly client: any
|
|
||||||
|
|
||||||
constructor(config: CouchDBConfig) {
|
constructor(config: CouchDBConfig) {
|
||||||
this.config = config
|
|
||||||
this.client = dbCore.DatabaseWithConnection(config.database, config.url)
|
this.client = dbCore.DatabaseWithConnection(config.database, config.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await this.query("exists", "validation error", {})
|
||||||
|
response.connected = result === true
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
async query(
|
async query(
|
||||||
command: string,
|
command: string,
|
||||||
errorMsg: string,
|
errorMsg: string,
|
||||||
query: { json?: object; id?: string }
|
query: { json?: object; id?: string }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return await this.client[command](query.id || query.json)
|
return await (this.client as any)[command](query.id || query.json)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(errorMsg, err)
|
console.error(errorMsg, err)
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -3,10 +3,13 @@ import {
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
QueryType,
|
QueryType,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
import AWS from "aws-sdk"
|
import AWS from "aws-sdk"
|
||||||
import { AWS_REGION } from "../db/dynamoClient"
|
import { AWS_REGION } from "../db/dynamoClient"
|
||||||
|
import { DocumentClient } from "aws-sdk/clients/dynamodb"
|
||||||
|
|
||||||
interface DynamoDBConfig {
|
interface DynamoDBConfig {
|
||||||
region: string
|
region: string
|
||||||
|
@ -22,6 +25,7 @@ const SCHEMA: Integration = {
|
||||||
"Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.",
|
"Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.",
|
||||||
friendlyName: "DynamoDB",
|
friendlyName: "DynamoDB",
|
||||||
type: "Non-relational",
|
type: "Non-relational",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
region: {
|
region: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -128,7 +132,7 @@ const SCHEMA: Integration = {
|
||||||
|
|
||||||
class DynamoDBIntegration implements IntegrationBase {
|
class DynamoDBIntegration implements IntegrationBase {
|
||||||
private config: DynamoDBConfig
|
private config: DynamoDBConfig
|
||||||
private client: any
|
private client
|
||||||
|
|
||||||
constructor(config: DynamoDBConfig) {
|
constructor(config: DynamoDBConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
@ -148,7 +152,23 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
this.client = new AWS.DynamoDB.DocumentClient(this.config)
|
this.client = new AWS.DynamoDB.DocumentClient(this.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(query: { table: string; json: object }) {
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const scanRes = await new AWS.DynamoDB(this.config).listTables().promise()
|
||||||
|
response.connected = !!scanRes.$response
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(query: {
|
||||||
|
table: string
|
||||||
|
json: Omit<DocumentClient.PutItemInput, "TableName">
|
||||||
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
|
@ -189,7 +209,10 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
return new AWS.DynamoDB(this.config).describeTable(params).promise()
|
return new AWS.DynamoDB(this.config).describeTable(params).promise()
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(query: { table: string; json: object }) {
|
async get(query: {
|
||||||
|
table: string
|
||||||
|
json: Omit<DocumentClient.GetItemInput, "TableName">
|
||||||
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
|
@ -197,7 +220,10 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
return this.client.get(params).promise()
|
return this.client.get(params).promise()
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(query: { table: string; json: object }) {
|
async update(query: {
|
||||||
|
table: string
|
||||||
|
json: Omit<DocumentClient.UpdateItemInput, "TableName">
|
||||||
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
|
@ -205,7 +231,10 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
return this.client.update(params).promise()
|
return this.client.update(params).promise()
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(query: { table: string; json: object }) {
|
async delete(query: {
|
||||||
|
table: string
|
||||||
|
json: Omit<DocumentClient.DeleteItemInput, "TableName">
|
||||||
|
}) {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
...query.json,
|
...query.json,
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
QueryType,
|
QueryType,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
import { Client, ClientOptions } from "@elastic/elasticsearch"
|
import { Client, ClientOptions } from "@elastic/elasticsearch"
|
||||||
|
@ -20,6 +22,7 @@ const SCHEMA: Integration = {
|
||||||
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.",
|
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.",
|
||||||
friendlyName: "ElasticSearch",
|
friendlyName: "ElasticSearch",
|
||||||
type: "Non-relational",
|
type: "Non-relational",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -95,7 +98,7 @@ const SCHEMA: Integration = {
|
||||||
|
|
||||||
class ElasticSearchIntegration implements IntegrationBase {
|
class ElasticSearchIntegration implements IntegrationBase {
|
||||||
private config: ElasticsearchConfig
|
private config: ElasticsearchConfig
|
||||||
private client: any
|
private client
|
||||||
|
|
||||||
constructor(config: ElasticsearchConfig) {
|
constructor(config: ElasticsearchConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
@ -114,6 +117,18 @@ class ElasticSearchIntegration implements IntegrationBase {
|
||||||
this.client = new Client(clientConfig)
|
this.client = new Client(clientConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
|
try {
|
||||||
|
await this.client.info()
|
||||||
|
return { connected: true }
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
error: e.message as string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(query: { index: string; json: object }) {
|
async create(query: { index: string; json: object }) {
|
||||||
const { index, json } = query
|
const { index, json } = query
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
QueryType,
|
QueryType,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Firestore, WhereFilterOp } from "@google-cloud/firestore"
|
import { Firestore, WhereFilterOp } from "@google-cloud/firestore"
|
||||||
|
|
||||||
|
@ -18,6 +20,7 @@ const SCHEMA: Integration = {
|
||||||
type: "Non-relational",
|
type: "Non-relational",
|
||||||
description:
|
description:
|
||||||
"Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.",
|
"Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
email: {
|
email: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -99,6 +102,18 @@ class FirebaseIntegration implements IntegrationBase {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
|
try {
|
||||||
|
await this.client.listCollections()
|
||||||
|
return { connected: true }
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
error: e.message as string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(query: { json: object; extra: { [key: string]: string } }) {
|
async create(query: { json: object; extra: { [key: string]: string } }) {
|
||||||
try {
|
try {
|
||||||
const documentReference = this.client
|
const documentReference = this.client
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import {
|
import {
|
||||||
|
ConnectionInfo,
|
||||||
|
DatasourceFeature,
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
@ -61,9 +63,13 @@ const SCHEMA: Integration = {
|
||||||
relationships: false,
|
relationships: false,
|
||||||
docs: "https://developers.google.com/sheets/api/quickstart/nodejs",
|
docs: "https://developers.google.com/sheets/api/quickstart/nodejs",
|
||||||
description:
|
description:
|
||||||
"Create and collaborate on online spreadsheets in real-time and from any device. ",
|
"Create and collaborate on online spreadsheets in real-time and from any device.",
|
||||||
friendlyName: "Google Sheets",
|
friendlyName: "Google Sheets",
|
||||||
type: "Spreadsheet",
|
type: "Spreadsheet",
|
||||||
|
features: [
|
||||||
|
DatasourceFeature.CONNECTION_CHECKING,
|
||||||
|
DatasourceFeature.FETCH_TABLE_NAMES,
|
||||||
|
],
|
||||||
datasource: {
|
datasource: {
|
||||||
spreadsheetId: {
|
spreadsheetId: {
|
||||||
display: "Google Sheet URL",
|
display: "Google Sheet URL",
|
||||||
|
@ -139,6 +145,18 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
this.client = new GoogleSpreadsheet(spreadsheetId)
|
this.client = new GoogleSpreadsheet(spreadsheetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
|
try {
|
||||||
|
await this.connect()
|
||||||
|
return { connected: true }
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
error: e.message as string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getBindingIdentifier() {
|
getBindingIdentifier() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -224,6 +242,12 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTableNames(): Promise<string[]> {
|
||||||
|
await this.connect()
|
||||||
|
const sheets = this.client.sheetsByIndex
|
||||||
|
return sheets.map(s => s.title)
|
||||||
|
}
|
||||||
|
|
||||||
getTableSchema(title: string, headerValues: string[], id?: string) {
|
getTableSchema(title: string, headerValues: string[], id?: string) {
|
||||||
// base table
|
// base table
|
||||||
const table: Table = {
|
const table: Table = {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import env from "../environment"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
import sdk from "../sdk"
|
import sdk from "../sdk"
|
||||||
|
|
||||||
const DEFINITIONS: { [key: string]: Integration } = {
|
const DEFINITIONS: Record<SourceName, Integration | undefined> = {
|
||||||
[SourceName.POSTGRES]: postgres.schema,
|
[SourceName.POSTGRES]: postgres.schema,
|
||||||
[SourceName.DYNAMODB]: dynamodb.schema,
|
[SourceName.DYNAMODB]: dynamodb.schema,
|
||||||
[SourceName.MONGODB]: mongodb.schema,
|
[SourceName.MONGODB]: mongodb.schema,
|
||||||
|
@ -36,9 +36,10 @@ const DEFINITIONS: { [key: string]: Integration } = {
|
||||||
[SourceName.GOOGLE_SHEETS]: googlesheets.schema,
|
[SourceName.GOOGLE_SHEETS]: googlesheets.schema,
|
||||||
[SourceName.REDIS]: redis.schema,
|
[SourceName.REDIS]: redis.schema,
|
||||||
[SourceName.SNOWFLAKE]: snowflake.schema,
|
[SourceName.SNOWFLAKE]: snowflake.schema,
|
||||||
|
[SourceName.ORACLE]: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const INTEGRATIONS: { [key: string]: any } = {
|
const INTEGRATIONS: Record<SourceName, any> = {
|
||||||
[SourceName.POSTGRES]: postgres.integration,
|
[SourceName.POSTGRES]: postgres.integration,
|
||||||
[SourceName.DYNAMODB]: dynamodb.integration,
|
[SourceName.DYNAMODB]: dynamodb.integration,
|
||||||
[SourceName.MONGODB]: mongodb.integration,
|
[SourceName.MONGODB]: mongodb.integration,
|
||||||
|
@ -55,6 +56,7 @@ const INTEGRATIONS: { [key: string]: any } = {
|
||||||
[SourceName.REDIS]: redis.integration,
|
[SourceName.REDIS]: redis.integration,
|
||||||
[SourceName.FIRESTORE]: firebase.integration,
|
[SourceName.FIRESTORE]: firebase.integration,
|
||||||
[SourceName.SNOWFLAKE]: snowflake.integration,
|
[SourceName.SNOWFLAKE]: snowflake.integration,
|
||||||
|
[SourceName.ORACLE]: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionally add oracle integration if the oracle binary can be installed
|
// optionally add oracle integration if the oracle binary can be installed
|
||||||
|
@ -67,10 +69,13 @@ if (
|
||||||
INTEGRATIONS[SourceName.ORACLE] = oracle.integration
|
INTEGRATIONS[SourceName.ORACLE] = oracle.integration
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDefinition(source: SourceName): Promise<Integration> {
|
export async function getDefinition(
|
||||||
|
source: SourceName
|
||||||
|
): Promise<Integration | undefined> {
|
||||||
// check if its integrated, faster
|
// check if its integrated, faster
|
||||||
if (DEFINITIONS[source]) {
|
const definition = DEFINITIONS[source]
|
||||||
return DEFINITIONS[source]
|
if (definition) {
|
||||||
|
return definition
|
||||||
}
|
}
|
||||||
const allDefinitions = await getDefinitions()
|
const allDefinitions = await getDefinitions()
|
||||||
return allDefinitions[source]
|
return allDefinitions[source]
|
||||||
|
@ -98,7 +103,7 @@ export async function getDefinitions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getIntegration(integration: string) {
|
export async function getIntegration(integration: SourceName) {
|
||||||
if (INTEGRATIONS[integration]) {
|
if (INTEGRATIONS[integration]) {
|
||||||
return INTEGRATIONS[integration]
|
return INTEGRATIONS[integration]
|
||||||
}
|
}
|
||||||
|
@ -107,7 +112,7 @@ export async function getIntegration(integration: string) {
|
||||||
for (let plugin of plugins) {
|
for (let plugin of plugins) {
|
||||||
if (plugin.name === integration) {
|
if (plugin.name === integration) {
|
||||||
// need to use commonJS require due to its dynamic runtime nature
|
// need to use commonJS require due to its dynamic runtime nature
|
||||||
const retrieved: any = await getDatasourcePlugin(plugin)
|
const retrieved = await getDatasourcePlugin(plugin)
|
||||||
if (retrieved.integration) {
|
if (retrieved.integration) {
|
||||||
return retrieved.integration
|
return retrieved.integration
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
QueryType,
|
QueryType,
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
|
@ -18,7 +20,6 @@ import {
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import Sql from "./base/sql"
|
import Sql from "./base/sql"
|
||||||
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
|
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
|
||||||
|
|
||||||
const sqlServer = require("mssql")
|
const sqlServer = require("mssql")
|
||||||
const DEFAULT_SCHEMA = "dbo"
|
const DEFAULT_SCHEMA = "dbo"
|
||||||
|
|
||||||
|
@ -39,6 +40,10 @@ const SCHEMA: Integration = {
|
||||||
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
|
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
|
||||||
friendlyName: "MS SQL Server",
|
friendlyName: "MS SQL Server",
|
||||||
type: "Relational",
|
type: "Relational",
|
||||||
|
features: [
|
||||||
|
DatasourceFeature.CONNECTION_CHECKING,
|
||||||
|
DatasourceFeature.FETCH_TABLE_NAMES,
|
||||||
|
],
|
||||||
datasource: {
|
datasource: {
|
||||||
user: {
|
user: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -121,6 +126,19 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.connect()
|
||||||
|
response.connected = true
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
getBindingIdentifier(): string {
|
getBindingIdentifier(): string {
|
||||||
return `@p${this.index++}`
|
return `@p${this.index++}`
|
||||||
}
|
}
|
||||||
|
@ -268,6 +286,20 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
||||||
this.schemaErrors = final.errors
|
this.schemaErrors = final.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async queryTableNames() {
|
||||||
|
let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
|
||||||
|
const schema = this.config.schema || DEFAULT_SCHEMA
|
||||||
|
return tableInfo
|
||||||
|
.filter((record: any) => record.TABLE_SCHEMA === schema)
|
||||||
|
.map((record: any) => record.TABLE_NAME)
|
||||||
|
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTableNames() {
|
||||||
|
await this.connect()
|
||||||
|
return this.queryTableNames()
|
||||||
|
}
|
||||||
|
|
||||||
async read(query: SqlQuery | string) {
|
async read(query: SqlQuery | string) {
|
||||||
await this.connect()
|
await this.connect()
|
||||||
const response = await this.internalQuery(getSqlQuery(query))
|
const response = await this.internalQuery(getSqlQuery(query))
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
QueryType,
|
QueryType,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
MongoClient,
|
MongoClient,
|
||||||
|
@ -38,6 +40,7 @@ const getSchema = () => {
|
||||||
type: "Non-relational",
|
type: "Non-relational",
|
||||||
description:
|
description:
|
||||||
"MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era.",
|
"MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era.",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
connectionString: {
|
connectionString: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -358,6 +361,19 @@ class MongoIntegration implements IntegrationBase {
|
||||||
this.client = new MongoClient(config.connectionString, options)
|
this.client = new MongoClient(config.connectionString, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.connect()
|
||||||
|
response.connected = true
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
return this.client.connect()
|
return this.client.connect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
Table,
|
Table,
|
||||||
TableSchema,
|
TableSchema,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
|
@ -20,18 +22,11 @@ import { NUMBER_REGEX } from "../utilities"
|
||||||
import Sql from "./base/sql"
|
import Sql from "./base/sql"
|
||||||
import { MySQLColumn } from "./base/types"
|
import { MySQLColumn } from "./base/types"
|
||||||
|
|
||||||
const mysql = require("mysql2/promise")
|
import mysql from "mysql2/promise"
|
||||||
|
|
||||||
interface MySQLConfig {
|
interface MySQLConfig extends mysql.ConnectionOptions {
|
||||||
host: string
|
|
||||||
port: number
|
|
||||||
user: string
|
|
||||||
password: string
|
|
||||||
database: string
|
database: string
|
||||||
ssl?: { [key: string]: any }
|
|
||||||
rejectUnauthorized: boolean
|
rejectUnauthorized: boolean
|
||||||
typeCast: Function
|
|
||||||
multipleStatements: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA: Integration = {
|
const SCHEMA: Integration = {
|
||||||
|
@ -41,6 +36,10 @@ const SCHEMA: Integration = {
|
||||||
type: "Relational",
|
type: "Relational",
|
||||||
description:
|
description:
|
||||||
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
|
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
|
||||||
|
features: [
|
||||||
|
DatasourceFeature.CONNECTION_CHECKING,
|
||||||
|
DatasourceFeature.FETCH_TABLE_NAMES,
|
||||||
|
],
|
||||||
datasource: {
|
datasource: {
|
||||||
host: {
|
host: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -92,8 +91,6 @@ const SCHEMA: Integration = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimezoneAwareDateTypes = ["timestamp"]
|
|
||||||
|
|
||||||
function bindingTypeCoerce(bindings: any[]) {
|
function bindingTypeCoerce(bindings: any[]) {
|
||||||
for (let i = 0; i < bindings.length; i++) {
|
for (let i = 0; i < bindings.length; i++) {
|
||||||
const binding = bindings[i]
|
const binding = bindings[i]
|
||||||
|
@ -120,7 +117,7 @@ function bindingTypeCoerce(bindings: any[]) {
|
||||||
|
|
||||||
class MySQLIntegration extends Sql implements DatasourcePlus {
|
class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
private config: MySQLConfig
|
private config: MySQLConfig
|
||||||
private client: any
|
private client?: mysql.Connection
|
||||||
public tables: Record<string, Table> = {}
|
public tables: Record<string, Table> = {}
|
||||||
public schemaErrors: Record<string, string> = {}
|
public schemaErrors: Record<string, string> = {}
|
||||||
|
|
||||||
|
@ -134,7 +131,8 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
if (
|
if (
|
||||||
config.rejectUnauthorized != null &&
|
config.rejectUnauthorized != null &&
|
||||||
!config.rejectUnauthorized &&
|
!config.rejectUnauthorized &&
|
||||||
config.ssl
|
config.ssl &&
|
||||||
|
typeof config.ssl !== "string"
|
||||||
) {
|
) {
|
||||||
config.ssl.rejectUnauthorized = config.rejectUnauthorized
|
config.ssl.rejectUnauthorized = config.rejectUnauthorized
|
||||||
}
|
}
|
||||||
|
@ -160,6 +158,22 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [result] = await this.internalQuery(
|
||||||
|
{ sql: "SELECT 1+1 AS checkRes" },
|
||||||
|
{ connect: true }
|
||||||
|
)
|
||||||
|
response.connected = result?.checkRes == 2
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
getBindingIdentifier(): string {
|
getBindingIdentifier(): string {
|
||||||
return "?"
|
return "?"
|
||||||
}
|
}
|
||||||
|
@ -173,7 +187,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
await this.client.end()
|
await this.client!.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
async internalQuery(
|
async internalQuery(
|
||||||
|
@ -192,10 +206,10 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
? baseBindings
|
? baseBindings
|
||||||
: bindingTypeCoerce(baseBindings)
|
: bindingTypeCoerce(baseBindings)
|
||||||
// Node MySQL is callback based, so we must wrap our call in a promise
|
// Node MySQL is callback based, so we must wrap our call in a promise
|
||||||
const response = await this.client.query(query.sql, bindings)
|
const response = await this.client!.query(query.sql, bindings)
|
||||||
return response[0]
|
return response[0]
|
||||||
} finally {
|
} finally {
|
||||||
if (opts?.connect) {
|
if (opts?.connect && this.client) {
|
||||||
await this.disconnect()
|
await this.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,20 +217,11 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
|
|
||||||
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
||||||
const tables: { [key: string]: Table } = {}
|
const tables: { [key: string]: Table } = {}
|
||||||
const database = this.config.database
|
|
||||||
await this.connect()
|
await this.connect()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// get the tables first
|
// get the tables first
|
||||||
const tablesResp: Record<string, string>[] = await this.internalQuery(
|
const tableNames = await this.queryTableNames()
|
||||||
{ sql: "SHOW TABLES;" },
|
|
||||||
{ connect: false }
|
|
||||||
)
|
|
||||||
const tableNames: string[] = tablesResp.map(
|
|
||||||
(obj: any) =>
|
|
||||||
obj[`Tables_in_${database}`] ||
|
|
||||||
obj[`Tables_in_${database.toLowerCase()}`]
|
|
||||||
)
|
|
||||||
for (let tableName of tableNames) {
|
for (let tableName of tableNames) {
|
||||||
const primaryKeys = []
|
const primaryKeys = []
|
||||||
const schema: TableSchema = {}
|
const schema: TableSchema = {}
|
||||||
|
@ -263,6 +268,28 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
this.schemaErrors = final.errors
|
this.schemaErrors = final.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async queryTableNames() {
|
||||||
|
const database = this.config.database
|
||||||
|
const tablesResp: Record<string, string>[] = await this.internalQuery(
|
||||||
|
{ sql: "SHOW TABLES;" },
|
||||||
|
{ connect: false }
|
||||||
|
)
|
||||||
|
return tablesResp.map(
|
||||||
|
(obj: any) =>
|
||||||
|
obj[`Tables_in_${database}`] ||
|
||||||
|
obj[`Tables_in_${database.toLowerCase()}`]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTableNames() {
|
||||||
|
await this.connect()
|
||||||
|
try {
|
||||||
|
return this.queryTableNames()
|
||||||
|
} finally {
|
||||||
|
await this.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(query: SqlQuery | string) {
|
async create(query: SqlQuery | string) {
|
||||||
const results = await this.internalQuery(getSqlQuery(query))
|
const results = await this.internalQuery(getSqlQuery(query))
|
||||||
return results.length ? results : [{ created: true }]
|
return results.length ? results : [{ created: true }]
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
Table,
|
Table,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
buildExternalTableId,
|
buildExternalTableId,
|
||||||
|
@ -24,12 +26,7 @@ import {
|
||||||
ExecuteOptions,
|
ExecuteOptions,
|
||||||
Result,
|
Result,
|
||||||
} from "oracledb"
|
} from "oracledb"
|
||||||
import {
|
import { OracleTable, OracleColumn, OracleColumnsResponse } from "./base/types"
|
||||||
OracleTable,
|
|
||||||
OracleColumn,
|
|
||||||
OracleColumnsResponse,
|
|
||||||
OracleConstraint,
|
|
||||||
} from "./base/types"
|
|
||||||
let oracledb: any
|
let oracledb: any
|
||||||
try {
|
try {
|
||||||
oracledb = require("oracledb")
|
oracledb = require("oracledb")
|
||||||
|
@ -53,6 +50,10 @@ const SCHEMA: Integration = {
|
||||||
type: "Relational",
|
type: "Relational",
|
||||||
description:
|
description:
|
||||||
"Oracle Database is an object-relational database management system developed by Oracle Corporation",
|
"Oracle Database is an object-relational database management system developed by Oracle Corporation",
|
||||||
|
features: [
|
||||||
|
DatasourceFeature.CONNECTION_CHECKING,
|
||||||
|
DatasourceFeature.FETCH_TABLE_NAMES,
|
||||||
|
],
|
||||||
datasource: {
|
datasource: {
|
||||||
host: {
|
host: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -325,6 +326,37 @@ class OracleIntegration extends Sql implements DatasourcePlus {
|
||||||
this.schemaErrors = final.errors
|
this.schemaErrors = final.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTableNames() {
|
||||||
|
const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
|
||||||
|
sql: this.COLUMNS_SQL,
|
||||||
|
})
|
||||||
|
return (columnsResponse.rows || []).map(row => row.TABLE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
let connection
|
||||||
|
try {
|
||||||
|
connection = await this.getConnection()
|
||||||
|
response.connected = true
|
||||||
|
} catch (err: any) {
|
||||||
|
response.connected = false
|
||||||
|
response.error = err.message
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
try {
|
||||||
|
await connection.close()
|
||||||
|
} catch (err: any) {
|
||||||
|
response.connected = false
|
||||||
|
response.error = err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> {
|
private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> {
|
||||||
let connection
|
let connection
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
Table,
|
Table,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
|
@ -18,7 +20,7 @@ import Sql from "./base/sql"
|
||||||
import { PostgresColumn } from "./base/types"
|
import { PostgresColumn } from "./base/types"
|
||||||
import { escapeDangerousCharacters } from "../utilities"
|
import { escapeDangerousCharacters } from "../utilities"
|
||||||
|
|
||||||
const { Client, types } = require("pg")
|
import { Client, types } from "pg"
|
||||||
|
|
||||||
// Return "date" and "timestamp" types as plain strings.
|
// Return "date" and "timestamp" types as plain strings.
|
||||||
// This lets us reference the original stored timezone.
|
// This lets us reference the original stored timezone.
|
||||||
|
@ -50,6 +52,10 @@ const SCHEMA: Integration = {
|
||||||
type: "Relational",
|
type: "Relational",
|
||||||
description:
|
description:
|
||||||
"PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
|
"PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
|
||||||
|
features: [
|
||||||
|
DatasourceFeature.CONNECTION_CHECKING,
|
||||||
|
DatasourceFeature.FETCH_TABLE_NAMES,
|
||||||
|
],
|
||||||
datasource: {
|
datasource: {
|
||||||
host: {
|
host: {
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
|
@ -114,7 +120,7 @@ const SCHEMA: Integration = {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostgresIntegration extends Sql implements DatasourcePlus {
|
class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
private readonly client: any
|
private readonly client: Client
|
||||||
private readonly config: PostgresConfig
|
private readonly config: PostgresConfig
|
||||||
private index: number = 1
|
private index: number = 1
|
||||||
private open: boolean
|
private open: boolean
|
||||||
|
@ -123,14 +129,15 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
|
|
||||||
COLUMNS_SQL!: string
|
COLUMNS_SQL!: string
|
||||||
|
|
||||||
PRIMARY_KEYS_SQL = `
|
PRIMARY_KEYS_SQL = () => `
|
||||||
select tc.table_schema, tc.table_name, kc.column_name as primary_key
|
SELECT pg_namespace.nspname table_schema
|
||||||
from information_schema.table_constraints tc
|
, pg_class.relname table_name
|
||||||
join
|
, pg_attribute.attname primary_key
|
||||||
information_schema.key_column_usage kc on kc.table_name = tc.table_name
|
FROM pg_class
|
||||||
and kc.table_schema = tc.table_schema
|
JOIN pg_index ON pg_class.oid = pg_index.indrelid AND pg_index.indisprimary
|
||||||
and kc.constraint_name = tc.constraint_name
|
JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = ANY(pg_index.indkey)
|
||||||
where tc.constraint_type = 'PRIMARY KEY';
|
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
|
||||||
|
WHERE pg_namespace.nspname = '${this.config.schema}';
|
||||||
`
|
`
|
||||||
|
|
||||||
constructor(config: PostgresConfig) {
|
constructor(config: PostgresConfig) {
|
||||||
|
@ -150,6 +157,21 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
this.open = false
|
this.open = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.openConnection()
|
||||||
|
response.connected = true
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
} finally {
|
||||||
|
await this.closeConnection()
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
getBindingIdentifier(): string {
|
getBindingIdentifier(): string {
|
||||||
return `$${this.index++}`
|
return `$${this.index++}`
|
||||||
}
|
}
|
||||||
|
@ -163,7 +185,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
if (!this.config.schema) {
|
if (!this.config.schema) {
|
||||||
this.config.schema = "public"
|
this.config.schema = "public"
|
||||||
}
|
}
|
||||||
this.client.query(`SET search_path TO ${this.config.schema}`)
|
await this.client.query(`SET search_path TO ${this.config.schema}`)
|
||||||
this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`
|
this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`
|
||||||
this.open = true
|
this.open = true
|
||||||
}
|
}
|
||||||
|
@ -221,7 +243,9 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
let tableKeys: { [key: string]: string[] } = {}
|
let tableKeys: { [key: string]: string[] } = {}
|
||||||
await this.openConnection()
|
await this.openConnection()
|
||||||
try {
|
try {
|
||||||
const primaryKeysResponse = await this.client.query(this.PRIMARY_KEYS_SQL)
|
const primaryKeysResponse = await this.client.query(
|
||||||
|
this.PRIMARY_KEYS_SQL()
|
||||||
|
)
|
||||||
for (let table of primaryKeysResponse.rows) {
|
for (let table of primaryKeysResponse.rows) {
|
||||||
const tableName = table.table_name
|
const tableName = table.table_name
|
||||||
if (!tableKeys[tableName]) {
|
if (!tableKeys[tableName]) {
|
||||||
|
@ -293,6 +317,17 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTableNames() {
|
||||||
|
try {
|
||||||
|
await this.openConnection()
|
||||||
|
const columnsResponse: { rows: PostgresColumn[] } =
|
||||||
|
await this.client.query(this.COLUMNS_SQL)
|
||||||
|
return columnsResponse.rows.map(row => row.table_name)
|
||||||
|
} finally {
|
||||||
|
await this.closeConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(query: SqlQuery | string) {
|
async create(query: SqlQuery | string) {
|
||||||
const response = await this.internalQuery(getSqlQuery(query))
|
const response = await this.internalQuery(getSqlQuery(query))
|
||||||
return response.rows.length ? response.rows : [{ created: true }]
|
return response.rows.length ? response.rows : [{ created: true }]
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { DatasourceFieldType, Integration, QueryType } from "@budibase/types"
|
import {
|
||||||
|
ConnectionInfo,
|
||||||
|
DatasourceFeature,
|
||||||
|
DatasourceFieldType,
|
||||||
|
Integration,
|
||||||
|
QueryType,
|
||||||
|
} from "@budibase/types"
|
||||||
import Redis from "ioredis"
|
import Redis from "ioredis"
|
||||||
|
|
||||||
interface RedisConfig {
|
interface RedisConfig {
|
||||||
|
@ -11,9 +17,11 @@ interface RedisConfig {
|
||||||
|
|
||||||
const SCHEMA: Integration = {
|
const SCHEMA: Integration = {
|
||||||
docs: "https://redis.io/docs/",
|
docs: "https://redis.io/docs/",
|
||||||
description: "",
|
description:
|
||||||
|
"Redis is a caching tool, providing powerful key-value store capabilities.",
|
||||||
friendlyName: "Redis",
|
friendlyName: "Redis",
|
||||||
type: "Non-relational",
|
type: "Non-relational",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
host: {
|
host: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -86,7 +94,7 @@ const SCHEMA: Integration = {
|
||||||
|
|
||||||
class RedisIntegration {
|
class RedisIntegration {
|
||||||
private readonly config: RedisConfig
|
private readonly config: RedisConfig
|
||||||
private client: any
|
private client
|
||||||
|
|
||||||
constructor(config: RedisConfig) {
|
constructor(config: RedisConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
@ -99,6 +107,21 @@ class RedisIntegration {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.client.ping()
|
||||||
|
response.connected = true
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
} finally {
|
||||||
|
await this.disconnect()
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
return this.client.quit()
|
return this.client.quit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,12 @@ import {
|
||||||
QueryType,
|
QueryType,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
|
DatasourceFeature,
|
||||||
|
ConnectionInfo,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
const AWS = require("aws-sdk")
|
import AWS from "aws-sdk"
|
||||||
const csv = require("csvtojson")
|
import csv from "csvtojson"
|
||||||
|
|
||||||
interface S3Config {
|
interface S3Config {
|
||||||
region: string
|
region: string
|
||||||
|
@ -22,6 +24,7 @@ const SCHEMA: Integration = {
|
||||||
"Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.",
|
"Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.",
|
||||||
friendlyName: "Amazon S3",
|
friendlyName: "Amazon S3",
|
||||||
type: "Object store",
|
type: "Object store",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
region: {
|
region: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -152,7 +155,7 @@ const SCHEMA: Integration = {
|
||||||
|
|
||||||
class S3Integration implements IntegrationBase {
|
class S3Integration implements IntegrationBase {
|
||||||
private readonly config: S3Config
|
private readonly config: S3Config
|
||||||
private client: any
|
private client
|
||||||
|
|
||||||
constructor(config: S3Config) {
|
constructor(config: S3Config) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
@ -165,6 +168,19 @@ class S3Integration implements IntegrationBase {
|
||||||
this.client = new AWS.S3(this.config)
|
this.client = new AWS.S3(this.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
const response: ConnectionInfo = {
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.client.listBuckets().promise()
|
||||||
|
response.connected = true
|
||||||
|
} catch (e: any) {
|
||||||
|
response.error = e.message as string
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
async create(query: {
|
async create(query: {
|
||||||
bucket: string
|
bucket: string
|
||||||
location: string
|
location: string
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { Integration, QueryType, SqlQuery } from "@budibase/types"
|
import {
|
||||||
|
ConnectionInfo,
|
||||||
|
DatasourceFeature,
|
||||||
|
Integration,
|
||||||
|
QueryType,
|
||||||
|
SqlQuery,
|
||||||
|
} from "@budibase/types"
|
||||||
import { Snowflake } from "snowflake-promise"
|
import { Snowflake } from "snowflake-promise"
|
||||||
|
|
||||||
interface SnowflakeConfig {
|
interface SnowflakeConfig {
|
||||||
|
@ -16,6 +22,7 @@ const SCHEMA: Integration = {
|
||||||
"Snowflake is a solution for data warehousing, data lakes, data engineering, data science, data application development, and securely sharing and consuming shared data.",
|
"Snowflake is a solution for data warehousing, data lakes, data engineering, data science, data application development, and securely sharing and consuming shared data.",
|
||||||
friendlyName: "Snowflake",
|
friendlyName: "Snowflake",
|
||||||
type: "Relational",
|
type: "Relational",
|
||||||
|
features: [DatasourceFeature.CONNECTION_CHECKING],
|
||||||
datasource: {
|
datasource: {
|
||||||
account: {
|
account: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -65,6 +72,18 @@ class SnowflakeIntegration {
|
||||||
this.client = new Snowflake(config)
|
this.client = new Snowflake(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
|
try {
|
||||||
|
await this.client.connect()
|
||||||
|
return { connected: true }
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
error: e.message as string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async internalQuery(query: SqlQuery) {
|
async internalQuery(query: SqlQuery) {
|
||||||
await this.client.connect()
|
await this.client.connect()
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -17,14 +17,15 @@ jest.mock("google-spreadsheet")
|
||||||
const { GoogleSpreadsheet } = require("google-spreadsheet")
|
const { GoogleSpreadsheet } = require("google-spreadsheet")
|
||||||
|
|
||||||
const sheetsByTitle: { [title: string]: GoogleSpreadsheetWorksheet } = {}
|
const sheetsByTitle: { [title: string]: GoogleSpreadsheetWorksheet } = {}
|
||||||
|
const sheetsByIndex: GoogleSpreadsheetWorksheet[] = []
|
||||||
|
const mockGoogleIntegration = {
|
||||||
|
useOAuth2Client: jest.fn(),
|
||||||
|
loadInfo: jest.fn(),
|
||||||
|
sheetsByTitle,
|
||||||
|
sheetsByIndex,
|
||||||
|
}
|
||||||
|
|
||||||
GoogleSpreadsheet.mockImplementation(() => {
|
GoogleSpreadsheet.mockImplementation(() => mockGoogleIntegration)
|
||||||
return {
|
|
||||||
useOAuth2Client: jest.fn(),
|
|
||||||
loadInfo: jest.fn(),
|
|
||||||
sheetsByTitle,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
import { structures } from "@budibase/backend-core/tests"
|
import { structures } from "@budibase/backend-core/tests"
|
||||||
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||||
|
@ -53,6 +54,8 @@ describe("Google Sheets Integration", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await config.init()
|
await config.init()
|
||||||
|
|
||||||
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
function createBasicTable(name: string, columns: string[]): Table {
|
function createBasicTable(name: string, columns: string[]): Table {
|
||||||
|
@ -88,7 +91,7 @@ describe("Google Sheets Integration", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("update table", () => {
|
describe("update table", () => {
|
||||||
test("adding a new field will be adding a new header row", async () => {
|
it("adding a new field will be adding a new header row", async () => {
|
||||||
await config.doInContext(structures.uuid(), async () => {
|
await config.doInContext(structures.uuid(), async () => {
|
||||||
const tableColumns = ["name", "description", "new field"]
|
const tableColumns = ["name", "description", "new field"]
|
||||||
const table = createBasicTable(structures.uuid(), tableColumns)
|
const table = createBasicTable(structures.uuid(), tableColumns)
|
||||||
|
@ -103,7 +106,7 @@ describe("Google Sheets Integration", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("removing an existing field will remove the header from the google sheet", async () => {
|
it("removing an existing field will remove the header from the google sheet", async () => {
|
||||||
const sheet = await config.doInContext(structures.uuid(), async () => {
|
const sheet = await config.doInContext(structures.uuid(), async () => {
|
||||||
const tableColumns = ["name"]
|
const tableColumns = ["name"]
|
||||||
const table = createBasicTable(structures.uuid(), tableColumns)
|
const table = createBasicTable(structures.uuid(), tableColumns)
|
||||||
|
@ -123,4 +126,33 @@ describe("Google Sheets Integration", () => {
|
||||||
expect((sheet.setHeaderRow as any).mock.calls[0][0]).toHaveLength(1)
|
expect((sheet.setHeaderRow as any).mock.calls[0][0]).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getTableNames", () => {
|
||||||
|
it("can fetch table names", async () => {
|
||||||
|
await config.doInContext(structures.uuid(), async () => {
|
||||||
|
const sheetNames: string[] = []
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const sheet = createSheet({ headerValues: [] })
|
||||||
|
sheetsByIndex.push(sheet)
|
||||||
|
sheetNames.push(sheet.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await integration.getTableNames()
|
||||||
|
|
||||||
|
expect(mockGoogleIntegration.loadInfo).toBeCalledTimes(1)
|
||||||
|
expect(res).toEqual(sheetNames)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("testConnection", () => {
|
||||||
|
it("can test successful connections", async () => {
|
||||||
|
await config.doInContext(structures.uuid(), async () => {
|
||||||
|
const res = await integration.testConnection()
|
||||||
|
|
||||||
|
expect(mockGoogleIntegration.loadInfo).toBeCalledTimes(1)
|
||||||
|
expect(res).toEqual({ connected: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const backfill = async (appDb: any, timestamp: string | number) => {
|
||||||
datasource = {
|
datasource = {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
_id: query.datasourceId,
|
_id: query.datasourceId,
|
||||||
source: SourceName.UNKNOWN,
|
source: "unknown" as SourceName,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { getEnvironmentVariables } from "../../utils"
|
import { getEnvironmentVariables } from "../../utils"
|
||||||
import { getDefinitions, getDefinition } from "../../../integrations"
|
import { getDefinitions, getDefinition } from "../../../integrations"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
const ENV_VAR_PREFIX = "env."
|
const ENV_VAR_PREFIX = "env."
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ async function enrichDatasourceWithValues(datasource: Datasource) {
|
||||||
{ onlyFound: true }
|
{ onlyFound: true }
|
||||||
) as Datasource
|
) as Datasource
|
||||||
const definition = await getDefinition(processed.source)
|
const definition = await getDefinition(processed.source)
|
||||||
processed.config = checkDatasourceTypes(definition, processed.config)
|
processed.config = checkDatasourceTypes(definition!, processed.config)
|
||||||
return {
|
return {
|
||||||
datasource: processed,
|
datasource: processed,
|
||||||
envVars: env as Record<string, string>,
|
envVars: env as Record<string, string>,
|
||||||
|
@ -147,6 +148,11 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (old.config?.auth) {
|
||||||
|
update.config = _.merge(old.config, update.config)
|
||||||
|
}
|
||||||
|
|
||||||
// update back to actual passwords for everything else
|
// update back to actual passwords for everything else
|
||||||
for (let [key, value] of Object.entries(update.config)) {
|
for (let [key, value] of Object.entries(update.config)) {
|
||||||
if (value !== PASSWORD_REPLACEMENT) {
|
if (value !== PASSWORD_REPLACEMENT) {
|
||||||
|
|
|
@ -13,13 +13,18 @@ import { generateAutomationMetadataID, isProdAppID } from "../db/utils"
|
||||||
import { definitions as triggerDefs } from "../automations/triggerInfo"
|
import { definitions as triggerDefs } from "../automations/triggerInfo"
|
||||||
import { AutomationErrors, MAX_AUTOMATION_RECURRING_ERRORS } from "../constants"
|
import { AutomationErrors, MAX_AUTOMATION_RECURRING_ERRORS } from "../constants"
|
||||||
import { storeLog } from "../automations/logging"
|
import { storeLog } from "../automations/logging"
|
||||||
import { Automation, AutomationStep, AutomationStatus } from "@budibase/types"
|
import {
|
||||||
|
Automation,
|
||||||
|
AutomationStep,
|
||||||
|
AutomationStatus,
|
||||||
|
AutomationMetadata,
|
||||||
|
AutomationJob,
|
||||||
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
LoopStep,
|
LoopStep,
|
||||||
LoopInput,
|
LoopInput,
|
||||||
TriggerOutput,
|
TriggerOutput,
|
||||||
AutomationContext,
|
AutomationContext,
|
||||||
AutomationMetadata,
|
|
||||||
} from "../definitions/automations"
|
} from "../definitions/automations"
|
||||||
import { WorkerCallback } from "./definitions"
|
import { WorkerCallback } from "./definitions"
|
||||||
import { context, logging } from "@budibase/backend-core"
|
import { context, logging } from "@budibase/backend-core"
|
||||||
|
@ -60,11 +65,11 @@ class Orchestrator {
|
||||||
_job: Job
|
_job: Job
|
||||||
executionOutput: AutomationContext
|
executionOutput: AutomationContext
|
||||||
|
|
||||||
constructor(job: Job) {
|
constructor(job: AutomationJob) {
|
||||||
let automation = job.data.automation,
|
let automation = job.data.automation
|
||||||
triggerOutput = job.data.event
|
let triggerOutput = job.data.event
|
||||||
const metadata = triggerOutput.metadata
|
const metadata = triggerOutput.metadata
|
||||||
this._chainCount = metadata ? metadata.automationChainCount : 0
|
this._chainCount = metadata ? metadata.automationChainCount! : 0
|
||||||
this._appId = triggerOutput.appId as string
|
this._appId = triggerOutput.appId as string
|
||||||
this._job = job
|
this._job = job
|
||||||
const triggerStepId = automation.definition.trigger.stepId
|
const triggerStepId = automation.definition.trigger.stepId
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { EnvironmentVariablesDecrypted } from "@budibase/types"
|
|
||||||
|
|
||||||
export type WorkerCallback = (error: any, response?: any) => void
|
export type WorkerCallback = (error: any, response?: any) => void
|
||||||
|
|
||||||
export interface QueryEvent {
|
export interface QueryEvent {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import workerFarm from "worker-farm"
|
import workerFarm from "worker-farm"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { AutomationJob } from "@budibase/types"
|
||||||
|
import { QueryEvent } from "./definitions"
|
||||||
|
|
||||||
export const ThreadType = {
|
export const ThreadType = {
|
||||||
QUERY: "query",
|
QUERY: "query",
|
||||||
|
@ -64,11 +66,11 @@ export class Thread {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
run(data: any) {
|
run(job: AutomationJob | QueryEvent) {
|
||||||
const timeout = this.timeoutMs
|
const timeout = this.timeoutMs
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
function fire(worker: any) {
|
function fire(worker: any) {
|
||||||
worker.execute(data, (err: any, response: any) => {
|
worker.execute(job, (err: any, response: any) => {
|
||||||
if (err && err.type === "TimeoutError") {
|
if (err && err.type === "TimeoutError") {
|
||||||
reject(
|
reject(
|
||||||
new Error(`Query response time exceeded ${timeout}ms timeout.`)
|
new Error(`Query response time exceeded ${timeout}ms timeout.`)
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const getComponentLibraryManifest = async (library: string) => {
|
||||||
const filename = "manifest.json"
|
const filename = "manifest.json"
|
||||||
|
|
||||||
if (env.isDev() || env.isTest()) {
|
if (env.isDev() || env.isTest()) {
|
||||||
const path = join(NODE_MODULES_PATH, "@budibase", "client", filename)
|
const path = join(TOP_LEVEL_PATH, "packages/client", filename)
|
||||||
// always load from new so that updates are refreshed
|
// always load from new so that updates are refreshed
|
||||||
delete require.cache[require.resolve(path)]
|
delete require.cache[require.resolve(path)]
|
||||||
return require(path)
|
return require(path)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { join } from "path"
|
import path, { join } from "path"
|
||||||
import { ObjectStoreBuckets } from "../../constants"
|
import { ObjectStoreBuckets } from "../../constants"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { objectStore } from "@budibase/backend-core"
|
import { objectStore } from "@budibase/backend-core"
|
||||||
|
@ -6,6 +6,10 @@ import { resolve } from "../centralPath"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import { TOP_LEVEL_PATH } from "./filesystem"
|
import { TOP_LEVEL_PATH } from "./filesystem"
|
||||||
|
|
||||||
|
export function devClientLibPath() {
|
||||||
|
return require.resolve("@budibase/client")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client library paths in the object store:
|
* Client library paths in the object store:
|
||||||
* Previously, the entire client library package was downloaded from NPM
|
* Previously, the entire client library package was downloaded from NPM
|
||||||
|
@ -89,9 +93,10 @@ export async function updateClientLibrary(appId: string) {
|
||||||
let manifest, client
|
let manifest, client
|
||||||
|
|
||||||
if (env.isDev()) {
|
if (env.isDev()) {
|
||||||
|
const clientPath = devClientLibPath()
|
||||||
// Load the symlinked version in dev which is always the newest
|
// Load the symlinked version in dev which is always the newest
|
||||||
manifest = require.resolve("@budibase/client/manifest.json")
|
manifest = join(path.dirname(path.dirname(clientPath)), "manifest.json")
|
||||||
client = require.resolve("@budibase/client")
|
client = clientPath
|
||||||
} else {
|
} else {
|
||||||
// Load the bundled version in prod
|
// Load the bundled version in prod
|
||||||
manifest = resolve(TOP_LEVEL_PATH, "client", "manifest.json")
|
manifest = resolve(TOP_LEVEL_PATH, "client", "manifest.json")
|
||||||
|
|
|
@ -4,9 +4,11 @@ import { budibaseTempDir } from "../budibaseDir"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import tar from "tar"
|
import tar from "tar"
|
||||||
|
import environment from "../../environment"
|
||||||
const uuid = require("uuid/v4")
|
const uuid = require("uuid/v4")
|
||||||
|
|
||||||
export const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..")
|
export const TOP_LEVEL_PATH =
|
||||||
|
environment.TOP_LEVEL_PATH || join(__dirname, "..", "..", "..")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upon first startup of instance there may not be everything we need in tmp directory, set it up.
|
* Upon first startup of instance there may not be everything we need in tmp directory, set it up.
|
||||||
|
|
|
@ -10,7 +10,15 @@
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"types": ["node", "jest"],
|
"types": ["node", "jest"],
|
||||||
"outDir": "dist/src",
|
"outDir": "dist/src",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@budibase/types": ["../types/src"],
|
||||||
|
"@budibase/backend-core": ["../backend-core/src"],
|
||||||
|
"@budibase/backend-core/*": ["../backend-core/*"],
|
||||||
|
"@budibase/shared-core": ["../shared-core/src"],
|
||||||
|
"@budibase/pro": ["../pro/packages/pro/src"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
|
@ -18,12 +18,6 @@
|
||||||
"require": ["tsconfig-paths/register"],
|
"require": ["tsconfig-paths/register"],
|
||||||
"swc": true
|
"swc": true
|
||||||
},
|
},
|
||||||
"references": [
|
|
||||||
{ "path": "../types" },
|
|
||||||
{ "path": "../backend-core" },
|
|
||||||
{ "path": "../shared-core" },
|
|
||||||
{ "path": "../../../budibase-pro/packages/pro" }
|
|
||||||
],
|
|
||||||
"include": ["src/**/*", "specs"],
|
"include": ["src/**/*", "specs"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,5 +26,19 @@
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
"@budibase/types"
|
||||||
|
],
|
||||||
|
"target": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -328,8 +328,8 @@ export const runLuceneQuery = (docs: any[], query?: Query) => {
|
||||||
return (
|
return (
|
||||||
docValue == null ||
|
docValue == null ||
|
||||||
docValue === "" ||
|
docValue === "" ||
|
||||||
docValue < testValue.low ||
|
+docValue < testValue.low ||
|
||||||
docValue > testValue.high
|
+docValue > testValue.high
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue