mirror of https://github.com/Budibase/budibase.git
Browse Source
# Conflicts: # packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.sveltepull/4639/head
764 changed files with 34601 additions and 13226 deletions
@ -0,0 +1,9 @@ |
|||
packages/server/node_modules |
|||
packages/builder |
|||
packages/frontend-core |
|||
packages/backend-core |
|||
packages/worker/node_modules |
|||
packages/cli |
|||
packages/client |
|||
packages/bbui |
|||
packages/string-templates |
|||
@ -0,0 +1,94 @@ |
|||
{ |
|||
"version": "2", |
|||
"templates": [ |
|||
{ |
|||
"type": 3, |
|||
"title": "Budibase", |
|||
"categories": ["Tools"], |
|||
"description": "Build modern business apps in minutes", |
|||
"logo": "https://budibase.com/favicon.ico", |
|||
"platform": "linux", |
|||
"repository": { |
|||
"url": "https://github.com/Budibase/budibase", |
|||
"stackfile": "hosting/docker-compose.yaml" |
|||
}, |
|||
"env": [ |
|||
{ |
|||
"name": "MAIN_PORT", |
|||
"label": "Main port", |
|||
"default": "10000" |
|||
}, |
|||
{ |
|||
"name": "JWT_SECRET", |
|||
"label": "JWT secret", |
|||
"default": "change-me" |
|||
}, |
|||
{ |
|||
"name": "MINIO_ACCESS_KEY", |
|||
"label": "MinIO access key", |
|||
"default": "change-me" |
|||
}, |
|||
{ |
|||
"name": "MINIO_SECRET_KEY", |
|||
"label": "MinIO secret key", |
|||
"default": "change-me" |
|||
}, |
|||
{ |
|||
"name": "COUCH_DB_USER", |
|||
"default": "budibase", |
|||
"preset": true |
|||
}, |
|||
{ |
|||
"name": "COUCH_DB_PASSWORD", |
|||
"label": "Couch DB password", |
|||
"default": "change-me" |
|||
}, |
|||
{ |
|||
"name": "REDIS_PASSWORD", |
|||
"label": "Redis password", |
|||
"default": "change-me" |
|||
}, |
|||
{ |
|||
"name": "INTERNAL_API_KEY", |
|||
"label": "Internal API key", |
|||
"default": "change-me" |
|||
}, |
|||
{ |
|||
"name": "APP_PORT", |
|||
"default": "4002", |
|||
"preset": true |
|||
}, |
|||
{ |
|||
"name": "WORKER_PORT", |
|||
"default": "4003", |
|||
"preset": true |
|||
}, |
|||
{ |
|||
"name": "MINIO_PORT", |
|||
"default": "4004", |
|||
"preset": true |
|||
}, |
|||
{ |
|||
"name": "COUCH_DB_PORT", |
|||
"default": "4005", |
|||
"preset": true |
|||
}, |
|||
{ |
|||
"name": "REDIS_PORT", |
|||
"default": "6379", |
|||
"preset": true |
|||
}, |
|||
{ |
|||
"name": "WATCHTOWER_PORT", |
|||
"default": "6161", |
|||
"preset": true |
|||
}, |
|||
{ |
|||
"name": "BUDIBASE_ENVIRONMENT", |
|||
"default": "PRODUCTION", |
|||
"preset": true |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
@ -1,2 +1,3 @@ |
|||
FROM nginx:latest |
|||
COPY .generated-nginx.prod.conf /etc/nginx/nginx.conf |
|||
COPY .generated-nginx.prod.conf /etc/nginx/nginx.conf |
|||
COPY error.html /usr/share/nginx/html/error.html |
|||
@ -0,0 +1,175 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>Budibase</title> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1"> |
|||
</head> |
|||
|
|||
<script> |
|||
function checkStatusButton() { |
|||
if (window.location.href.includes("budibase.app")) { |
|||
var button = document.getElementById("statusButton") |
|||
button.removeAttribute("hidden") |
|||
} |
|||
} |
|||
|
|||
function goToStatus() { |
|||
window.location.href = "https://status.budibase.com"; |
|||
} |
|||
function goHome() { |
|||
window.location.href = window.location.origin; |
|||
} |
|||
function getStatus() { |
|||
var http = new XMLHttpRequest() |
|||
var url = window.location.href |
|||
http.open('GET', url, true) |
|||
http.send() |
|||
http.onreadystatechange = (e) => { |
|||
var status = http.status |
|||
document.getElementById("status").innerHTML = status |
|||
|
|||
var message |
|||
if (status === 502) { |
|||
message = "Bad gateway. Please try again later." |
|||
} else if (status === 503) { |
|||
message = "Service Unavailable. Please try again later." |
|||
} else if (status === 504) { |
|||
message = "Gateway timeout. Please try again later." |
|||
} else { |
|||
message = "Please try again later." |
|||
} |
|||
|
|||
document.getElementById("message").innerHTML = message |
|||
} |
|||
} |
|||
|
|||
window.onload = function() { |
|||
checkStatusButton() |
|||
getStatus() |
|||
}; |
|||
|
|||
</script> |
|||
|
|||
<style> |
|||
|
|||
:root { |
|||
--spectrum-global-color-gray-600: rgb(144,144,144); |
|||
--spectrum-global-color-gray-900: rgb(255,255,255); |
|||
--spectrum-global-color-gray-800: rgb(227,227,227); |
|||
--spectrum-global-color-static-blue-600: rgb(20,115,230); |
|||
--spectrum-global-color-static-blue-hover: rgb( 18, 103, 207); |
|||
} |
|||
|
|||
html, body { |
|||
background-color: #1a1a1a; |
|||
padding: 0; |
|||
margin: 0; |
|||
overflow: hidden; |
|||
color: #e7e7e7; |
|||
font-family: 'Roboto', sans-serif; |
|||
} |
|||
button { |
|||
color: #e7e7e7; |
|||
font-family: 'Roboto', sans-serif; |
|||
border: none; |
|||
font-size: 15px; |
|||
border-radius: 15px; |
|||
padding: 8px 22px; |
|||
} |
|||
button:hover { |
|||
cursor: pointer; |
|||
} |
|||
.main { |
|||
height: 100vh; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
.info { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: left; |
|||
} |
|||
|
|||
@media only screen and (max-width: 600px) { |
|||
.info { |
|||
align-items: center; |
|||
} |
|||
} |
|||
|
|||
.status { |
|||
color: var(--spectrum-global-color-gray-600) |
|||
} |
|||
.title { |
|||
font-weight: 400; |
|||
color: var(--spectrum-global-color-gray-900) |
|||
} |
|||
.message { |
|||
font-weight: 200; |
|||
color: var(--spectrum-global-color-gray-800) |
|||
} |
|||
.buttons { |
|||
display: flex; |
|||
flex-direction: row; |
|||
margin-top: 15px; |
|||
} |
|||
.homeButton { |
|||
background-color: var(--spectrum-global-color-static-blue-600); |
|||
} |
|||
.homeButton:hover { |
|||
background-color: var(--spectrum-global-color-static-blue-hover); |
|||
} |
|||
.statusButton { |
|||
background-color: transparent; |
|||
margin-left: 20px; |
|||
border: none; |
|||
} |
|||
.hero { |
|||
height: 160px; |
|||
width: 160px; |
|||
margin-right: 80px; |
|||
} |
|||
.content { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: flex-end; |
|||
justify-content: center; |
|||
} |
|||
|
|||
@media only screen and (max-width: 600px) { |
|||
.content { |
|||
flex-direction: column; |
|||
} |
|||
} |
|||
</style> |
|||
|
|||
<script src=""> |
|||
</script> |
|||
|
|||
<body> |
|||
<div class="main"> |
|||
<div class="content"> |
|||
<div class="hero"> |
|||
<img src="https://raw.githubusercontent.com/Budibase/budibase/master/packages/builder/assets/bb-space-man.svg" alt="Budibase Logo"> |
|||
</div> |
|||
<div class="info"> |
|||
<div> |
|||
<h4 id="status" class="status"></h4> |
|||
<h1 class="title"> |
|||
Houston we have a problem! |
|||
</h1> |
|||
<h3 id="message" class="message"> |
|||
</h3> |
|||
</div> |
|||
<div class="buttons"> |
|||
<button class="homeButton" onclick=goHome()>Return home</button> |
|||
<button id="statusButton" class="statusButton" hidden="true" onclick=goToStatus()>Check out status</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
|
|||
</html> |
|||
@ -0,0 +1,99 @@ |
|||
FROM couchdb |
|||
|
|||
ENV DEPLOYMENT_ENVIRONMENT=docker |
|||
ENV POSTHOG_TOKEN=phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS |
|||
ENV COUCHDB_PASSWORD=budibase |
|||
ENV COUCHDB_USER=budibase |
|||
ENV COUCH_DB_URL=http://budibase:budibase@localhost:5984 |
|||
ENV BUDIBASE_ENVIRONMENT=PRODUCTION |
|||
ENV MINIO_URL=http://localhost:9000 |
|||
ENV REDIS_URL=localhost:6379 |
|||
ENV WORKER_URL=http://localhost:4002 |
|||
ENV INTERNAL_API_KEY=budibase |
|||
ENV JWT_SECRET=testsecret |
|||
ENV MINIO_ACCESS_KEY=budibase |
|||
ENV MINIO_SECRET_KEY=budibase |
|||
ENV SELF_HOSTED=1 |
|||
ENV CLUSTER_PORT=10000 |
|||
ENV REDIS_PASSWORD=budibase |
|||
ENV ARCHITECTURE=amd |
|||
ENV APP_PORT=4001 |
|||
ENV WORKER_PORT=4002 |
|||
|
|||
RUN apt-get update |
|||
RUN apt-get install software-properties-common wget nginx -y |
|||
RUN apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' |
|||
RUN apt-get update |
|||
|
|||
# setup nginx |
|||
ADD hosting/single/nginx.conf /etc/nginx |
|||
RUN mkdir /etc/nginx/logs |
|||
RUN useradd www |
|||
RUN touch /etc/nginx/logs/error.log |
|||
RUN touch /etc/nginx/logs/nginx.pid |
|||
|
|||
# install java |
|||
RUN apt-get install openjdk-8-jdk -y |
|||
|
|||
# setup nodejs |
|||
WORKDIR /nodejs |
|||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh |
|||
RUN bash /tmp/nodesource_setup.sh |
|||
RUN apt-get install nodejs |
|||
RUN npm install --global yarn |
|||
RUN npm install --global pm2 |
|||
|
|||
# setup redis |
|||
RUN apt install redis-server -y |
|||
|
|||
# setup server |
|||
WORKDIR /app |
|||
ADD packages/server . |
|||
RUN ls -al |
|||
RUN yarn |
|||
RUN yarn build |
|||
# Install client for oracle datasource |
|||
RUN apt-get install unzip libaio1 |
|||
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh |
|||
|
|||
# setup worker |
|||
WORKDIR /worker |
|||
ADD packages/worker . |
|||
RUN yarn |
|||
RUN yarn build |
|||
|
|||
# setup clouseau |
|||
WORKDIR / |
|||
RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip |
|||
RUN unzip clouseau-2.21.0-dist.zip |
|||
RUN mv clouseau-2.21.0 /opt/clouseau |
|||
RUN rm clouseau-2.21.0-dist.zip |
|||
|
|||
WORKDIR /opt/clouseau |
|||
RUN mkdir ./bin |
|||
ADD hosting/single/clouseau ./bin/ |
|||
ADD hosting/single/log4j.properties . |
|||
ADD hosting/single/clouseau.ini . |
|||
RUN chmod +x ./bin/clouseau |
|||
|
|||
# setup CouchDB |
|||
WORKDIR /opt/couchdb |
|||
ADD hosting/single/vm.args ./etc/ |
|||
|
|||
# setup minio |
|||
WORKDIR /minio |
|||
RUN wget https://dl.min.io/server/minio/release/linux-${ARCHITECTURE}64/minio |
|||
RUN chmod +x minio |
|||
|
|||
# setup runner file |
|||
WORKDIR / |
|||
ADD hosting/single/runner.sh . |
|||
RUN chmod +x ./runner.sh |
|||
|
|||
EXPOSE 10000 |
|||
VOLUME /opt/couchdb/data |
|||
VOLUME /minio |
|||
|
|||
# must set this just before running |
|||
ENV NODE_ENV=production |
|||
CMD ["./runner.sh"] |
|||
@ -0,0 +1,12 @@ |
|||
#!/bin/sh |
|||
/usr/bin/java -server \ |
|||
-Xmx2G \ |
|||
-Dsun.net.inetaddr.ttl=30 \ |
|||
-Dsun.net.inetaddr.negative.ttl=30 \ |
|||
-Dlog4j.configuration=file:/opt/clouseau/log4j.properties \ |
|||
-XX:OnOutOfMemoryError="kill -9 %p" \ |
|||
-XX:+UseConcMarkSweepGC \ |
|||
-XX:+CMSParallelRemarkEnabled \ |
|||
-classpath '/opt/clouseau/*' \ |
|||
com.cloudant.clouseau.Main \ |
|||
/opt/clouseau/clouseau.ini |
|||
@ -0,0 +1,13 @@ |
|||
[clouseau] |
|||
|
|||
; the name of the Erlang node created by the service, leave this unchanged |
|||
name=clouseau@127.0.0.1 |
|||
|
|||
; set this to the same distributed Erlang cookie used by the CouchDB nodes |
|||
cookie=monster |
|||
|
|||
; the path where you would like to store the search index files |
|||
dir=/opt/couchdb/data/search |
|||
|
|||
; the number of search indexes that can be open simultaneously |
|||
max_indexes_open=500 |
|||
@ -0,0 +1,4 @@ |
|||
log4j.rootLogger=debug, CONSOLE |
|||
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender |
|||
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout |
|||
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %c [%p] %m%n |
|||
@ -0,0 +1,116 @@ |
|||
user www www; |
|||
error_log /etc/nginx/logs/error.log; |
|||
pid /etc/nginx/logs/nginx.pid; |
|||
worker_processes auto; |
|||
worker_rlimit_nofile 8192; |
|||
|
|||
events { |
|||
worker_connections 1024; |
|||
} |
|||
|
|||
http { |
|||
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s; |
|||
proxy_set_header Host $host; |
|||
charset utf-8; |
|||
sendfile on; |
|||
tcp_nopush on; |
|||
tcp_nodelay on; |
|||
server_tokens off; |
|||
types_hash_max_size 2048; |
|||
|
|||
# buffering |
|||
client_header_buffer_size 1k; |
|||
client_max_body_size 20M; |
|||
ignore_invalid_headers off; |
|||
proxy_buffering off; |
|||
|
|||
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
|||
'$status $body_bytes_sent "$http_referer" ' |
|||
'"$http_user_agent" "$http_x_forwarded_for"'; |
|||
|
|||
map $http_upgrade $connection_upgrade { |
|||
default "upgrade"; |
|||
} |
|||
|
|||
server { |
|||
listen 10000 default_server; |
|||
listen [::]:10000 default_server; |
|||
server_name _; |
|||
client_max_body_size 1000m; |
|||
ignore_invalid_headers off; |
|||
proxy_buffering off; |
|||
# port_in_redirect off; |
|||
|
|||
location /app { |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location = / { |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location ~ ^/(builder|app_) { |
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection $connection_upgrade; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location ~ ^/api/(system|admin|global)/ { |
|||
proxy_pass http://127.0.0.1:4002; |
|||
} |
|||
|
|||
location /worker/ { |
|||
proxy_pass http://127.0.0.1:4002; |
|||
rewrite ^/worker/(.*)$ /$1 break; |
|||
} |
|||
|
|||
location /api/ { |
|||
# calls to the API are rate limited with bursting |
|||
limit_req zone=ratelimit burst=20 nodelay; |
|||
|
|||
# 120s timeout on API requests |
|||
proxy_read_timeout 120s; |
|||
proxy_connect_timeout 120s; |
|||
proxy_send_timeout 120s; |
|||
|
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection $connection_upgrade; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
|
|||
proxy_pass http://127.0.0.1:4001; |
|||
} |
|||
|
|||
location /db/ { |
|||
proxy_pass http://127.0.0.1:5984; |
|||
rewrite ^/db/(.*)$ /$1 break; |
|||
} |
|||
|
|||
location / { |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_set_header X-Forwarded-Proto $scheme; |
|||
|
|||
proxy_connect_timeout 300; |
|||
proxy_http_version 1.1; |
|||
proxy_set_header Connection ""; |
|||
chunked_transfer_encoding off; |
|||
proxy_pass http://127.0.0.1:9000; |
|||
} |
|||
|
|||
client_header_timeout 60; |
|||
client_body_timeout 60; |
|||
keepalive_timeout 60; |
|||
|
|||
# gzip |
|||
gzip on; |
|||
gzip_vary on; |
|||
gzip_proxied any; |
|||
gzip_comp_level 6; |
|||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
redis-server --requirepass $REDIS_PASSWORD & |
|||
/opt/clouseau/bin/clouseau & |
|||
/minio/minio server /minio & |
|||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb & |
|||
/etc/init.d/nginx restart |
|||
pushd app |
|||
pm2 start --name app "yarn run:docker" |
|||
popd |
|||
pushd worker |
|||
pm2 start --name worker "yarn run:docker" |
|||
popd |
|||
sleep 10 |
|||
URL=http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984 |
|||
curl -X PUT ${URL}/_users |
|||
curl -X PUT ${URL}/_replicator |
|||
sleep infinity |
|||
@ -0,0 +1,32 @@ |
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not |
|||
# use this file except in compliance with the License. You may obtain a copy of |
|||
# the License at |
|||
# |
|||
# http://www.apache.org/licenses/LICENSE-2.0 |
|||
# |
|||
# Unless required by applicable law or agreed to in writing, software |
|||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|||
# License for the specific language governing permissions and limitations under |
|||
# the License. |
|||
|
|||
# erlang cookie for clouseau security |
|||
-name couchdb@127.0.0.1 |
|||
-setcookie monster |
|||
|
|||
# Ensure that the Erlang VM listens on a known port |
|||
-kernel inet_dist_listen_min 9100 |
|||
-kernel inet_dist_listen_max 9100 |
|||
|
|||
# Tell kernel and SASL not to log anything |
|||
-kernel error_logger silent |
|||
-sasl sasl_error_logger false |
|||
|
|||
# Use kernel poll functionality if supported by emulator |
|||
+K true |
|||
|
|||
# Start a pool of asynchronous IO threads |
|||
+A 16 |
|||
|
|||
# Comment this line out to enable the interactive Erlang shell on startup |
|||
+Bd -noinput |
|||
@ -1,4 +1,7 @@ |
|||
const generic = require("./src/cache/generic") |
|||
|
|||
module.exports = { |
|||
user: require("./src/cache/user"), |
|||
app: require("./src/cache/appMetadata"), |
|||
...generic, |
|||
} |
|||
|
|||
@ -0,0 +1 @@ |
|||
module.exports = require("./src/logging") |
|||
@ -1,45 +1,80 @@ |
|||
{ |
|||
"name": "@budibase/backend-core", |
|||
"version": "1.0.98-alpha.1", |
|||
"version": "1.0.206", |
|||
"description": "Budibase backend core libraries used in server and worker", |
|||
"main": "src/index.js", |
|||
"main": "dist/src/index.js", |
|||
"types": "dist/src/index.d.ts", |
|||
"exports": { |
|||
".": "./dist/src/index.js", |
|||
"./tests": "./dist/tests/index.js", |
|||
"./*": "./dist/*.js" |
|||
}, |
|||
"author": "Budibase", |
|||
"license": "GPL-3.0", |
|||
"scripts": { |
|||
"prebuild": "rimraf dist/", |
|||
"prepack": "cp package.json dist", |
|||
"build": "tsc -p tsconfig.build.json", |
|||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", |
|||
"test": "jest", |
|||
"test:watch": "jest --watchAll" |
|||
}, |
|||
"dependencies": { |
|||
"@techpass/passport-openidconnect": "^0.3.0", |
|||
"aws-sdk": "^2.901.0", |
|||
"bcryptjs": "^2.4.3", |
|||
"cls-hooked": "^4.2.2", |
|||
"ioredis": "^4.27.1", |
|||
"jsonwebtoken": "^8.5.1", |
|||
"koa-passport": "^4.1.4", |
|||
"lodash": "^4.17.21", |
|||
"lodash.isarguments": "^3.1.0", |
|||
"node-fetch": "^2.6.1", |
|||
"passport-google-auth": "^1.0.2", |
|||
"passport-google-oauth": "^2.0.0", |
|||
"passport-jwt": "^4.0.0", |
|||
"passport-local": "^1.0.0", |
|||
"sanitize-s3-objectkey": "^0.0.1", |
|||
"tar-fs": "^2.1.1", |
|||
"uuid": "^8.3.2", |
|||
"zlib": "^1.0.5" |
|||
"@techpass/passport-openidconnect": "0.3.2", |
|||
"aws-sdk": "2.1030.0", |
|||
"bcrypt": "5.0.1", |
|||
"dotenv": "16.0.1", |
|||
"emitter-listener": "1.1.2", |
|||
"ioredis": "4.28.0", |
|||
"jsonwebtoken": "8.5.1", |
|||
"koa-passport": "4.1.4", |
|||
"lodash": "4.17.21", |
|||
"lodash.isarguments": "3.1.0", |
|||
"node-fetch": "2.6.7", |
|||
"passport-google-auth": "1.0.2", |
|||
"passport-google-oauth": "2.0.0", |
|||
"passport-jwt": "4.0.0", |
|||
"passport-local": "1.0.0", |
|||
"posthog-node": "1.3.0", |
|||
"pouchdb": "7.3.0", |
|||
"pouchdb-find": "7.2.2", |
|||
"pouchdb-replication-stream": "1.2.9", |
|||
"redlock": "4.2.0", |
|||
"sanitize-s3-objectkey": "0.0.1", |
|||
"semver": "7.3.7", |
|||
"tar-fs": "2.1.1", |
|||
"uuid": "8.3.2", |
|||
"zlib": "1.0.5" |
|||
}, |
|||
"jest": { |
|||
"preset": "ts-jest", |
|||
"testEnvironment": "node", |
|||
"moduleNameMapper": { |
|||
"@budibase/types": "<rootDir>/../types/src" |
|||
}, |
|||
"setupFiles": [ |
|||
"./scripts/jestSetup.js" |
|||
"./scripts/jestSetup.ts" |
|||
] |
|||
}, |
|||
"devDependencies": { |
|||
"ioredis-mock": "^5.5.5", |
|||
"jest": "^26.6.3", |
|||
"pouchdb": "^7.2.1", |
|||
"pouchdb-adapter-memory": "^7.2.2", |
|||
"pouchdb-all-dbs": "^1.0.2" |
|||
"@budibase/types": "^1.0.206", |
|||
"@shopify/jest-koa-mocks": "3.1.5", |
|||
"@types/jest": "27.5.1", |
|||
"@types/koa": "2.0.52", |
|||
"@types/node": "14.18.20", |
|||
"@types/node-fetch": "2.6.1", |
|||
"@types/redlock": "4.0.3", |
|||
"@types/semver": "7.3.7", |
|||
"@types/tar-fs": "2.0.1", |
|||
"@types/uuid": "8.3.4", |
|||
"ioredis-mock": "5.8.0", |
|||
"jest": "27.5.1", |
|||
"koa": "2.7.0", |
|||
"nodemon": "2.0.16", |
|||
"pouchdb-adapter-memory": "7.2.2", |
|||
"timekeeper": "2.2.0", |
|||
"ts-jest": "27.1.5", |
|||
"typescript": "4.7.3" |
|||
}, |
|||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" |
|||
} |
|||
|
|||
@ -1,4 +1,5 @@ |
|||
module.exports = { |
|||
Client: require("./src/redis"), |
|||
utils: require("./src/redis/utils"), |
|||
clients: require("./src/redis/authRedis"), |
|||
} |
|||
|
|||
@ -1,6 +0,0 @@ |
|||
const env = require("../src/environment") |
|||
|
|||
env._set("SELF_HOSTED", "1") |
|||
env._set("NODE_ENV", "jest") |
|||
env._set("JWT_SECRET", "test-jwtsecret") |
|||
env._set("LOG_LEVEL", "silent") |
|||
@ -0,0 +1,12 @@ |
|||
import env from "../src/environment" |
|||
import { mocks } from "../tests/utilities" |
|||
|
|||
// mock all dates to 2020-01-01T00:00:00.000Z
|
|||
// use tk.reset() to use real dates in individual tests
|
|||
import tk from "timekeeper" |
|||
tk.freeze(mocks.date.MOCK_DATE) |
|||
|
|||
env._set("SELF_HOSTED", "1") |
|||
env._set("NODE_ENV", "jest") |
|||
env._set("JWT_SECRET", "test-jwtsecret") |
|||
env._set("LOG_LEVEL", "silent") |
|||
@ -0,0 +1,82 @@ |
|||
const redis = require("../redis/authRedis") |
|||
const { getTenantId } = require("../context") |
|||
|
|||
exports.CacheKeys = { |
|||
CHECKLIST: "checklist", |
|||
INSTALLATION: "installation", |
|||
ANALYTICS_ENABLED: "analyticsEnabled", |
|||
UNIQUE_TENANT_ID: "uniqueTenantId", |
|||
EVENTS: "events", |
|||
BACKFILL_METADATA: "backfillMetadata", |
|||
} |
|||
|
|||
exports.TTL = { |
|||
ONE_MINUTE: 600, |
|||
ONE_HOUR: 3600, |
|||
ONE_DAY: 86400, |
|||
} |
|||
|
|||
function generateTenantKey(key) { |
|||
const tenantId = getTenantId() |
|||
return `${key}:${tenantId}` |
|||
} |
|||
|
|||
exports.keys = async pattern => { |
|||
const client = await redis.getCacheClient() |
|||
return client.keys(pattern) |
|||
} |
|||
|
|||
/** |
|||
* Read only from the cache. |
|||
*/ |
|||
exports.get = async (key, opts = { useTenancy: true }) => { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await redis.getCacheClient() |
|||
const value = await client.get(key) |
|||
return value |
|||
} |
|||
|
|||
/** |
|||
* Write to the cache. |
|||
*/ |
|||
exports.store = async (key, value, ttl, opts = { useTenancy: true }) => { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await redis.getCacheClient() |
|||
await client.store(key, value, ttl) |
|||
} |
|||
|
|||
exports.delete = async (key, opts = { useTenancy: true }) => { |
|||
key = opts.useTenancy ? generateTenantKey(key) : key |
|||
const client = await redis.getCacheClient() |
|||
return client.delete(key) |
|||
} |
|||
|
|||
/** |
|||
* Read from the cache. Write to the cache if not exists. |
|||
*/ |
|||
exports.withCache = async (key, ttl, fetchFn, opts = { useTenancy: true }) => { |
|||
const cachedValue = await exports.get(key, opts) |
|||
if (cachedValue) { |
|||
return cachedValue |
|||
} |
|||
|
|||
try { |
|||
const fetchedValue = await fetchFn() |
|||
|
|||
await exports.store(key, fetchedValue, ttl, opts) |
|||
return fetchedValue |
|||
} catch (err) { |
|||
console.error("Error fetching before cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
exports.bustCache = async key => { |
|||
const client = await redis.getCacheClient() |
|||
try { |
|||
await client.delete(generateTenantKey(key)) |
|||
} catch (err) { |
|||
console.error("Error busting cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
const API = require("./api") |
|||
const env = require("../environment") |
|||
const { Headers } = require("../constants") |
|||
|
|||
const api = new API(env.ACCOUNT_PORTAL_URL) |
|||
|
|||
exports.getAccount = async email => { |
|||
const payload = { |
|||
email, |
|||
} |
|||
const response = await api.post(`/api/accounts/search`, { |
|||
body: payload, |
|||
headers: { |
|||
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, |
|||
}, |
|||
}) |
|||
const json = await response.json() |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error(`Error getting account by email ${email}`, json) |
|||
} |
|||
|
|||
return json[0] |
|||
} |
|||
|
|||
exports.getStatus = async () => { |
|||
const response = await api.get(`/api/status`, { |
|||
headers: { |
|||
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, |
|||
}, |
|||
}) |
|||
const json = await response.json() |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error(`Error getting status`) |
|||
} |
|||
|
|||
return json |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
import API from "./api" |
|||
import env from "../environment" |
|||
import { Headers } from "../constants" |
|||
import { CloudAccount } from "@budibase/types" |
|||
|
|||
const api = new API(env.ACCOUNT_PORTAL_URL) |
|||
|
|||
export const getAccount = async ( |
|||
email: string |
|||
): Promise<CloudAccount | undefined> => { |
|||
const payload = { |
|||
email, |
|||
} |
|||
const response = await api.post(`/api/accounts/search`, { |
|||
body: payload, |
|||
headers: { |
|||
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, |
|||
}, |
|||
}) |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error(`Error getting account by email ${email}`) |
|||
} |
|||
|
|||
const json: CloudAccount[] = await response.json() |
|||
return json[0] |
|||
} |
|||
|
|||
export const getAccountByTenantId = async ( |
|||
tenantId: string |
|||
): Promise<CloudAccount | undefined> => { |
|||
const payload = { |
|||
tenantId, |
|||
} |
|||
const response = await api.post(`/api/accounts/search`, { |
|||
body: payload, |
|||
headers: { |
|||
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, |
|||
}, |
|||
}) |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error(`Error getting account by tenantId ${tenantId}`) |
|||
} |
|||
|
|||
const json: CloudAccount[] = await response.json() |
|||
return json[0] |
|||
} |
|||
|
|||
export const getStatus = async () => { |
|||
const response = await api.get(`/api/status`, { |
|||
headers: { |
|||
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, |
|||
}, |
|||
}) |
|||
const json = await response.json() |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error(`Error getting status`) |
|||
} |
|||
|
|||
return json |
|||
} |
|||
@ -0,0 +1,650 @@ |
|||
const util = require("util") |
|||
const assert = require("assert") |
|||
const wrapEmitter = require("emitter-listener") |
|||
const async_hooks = require("async_hooks") |
|||
|
|||
const CONTEXTS_SYMBOL = "cls@contexts" |
|||
const ERROR_SYMBOL = "error@context" |
|||
|
|||
const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED |
|||
|
|||
let currentUid = -1 |
|||
|
|||
module.exports = { |
|||
getNamespace: getNamespace, |
|||
createNamespace: createNamespace, |
|||
destroyNamespace: destroyNamespace, |
|||
reset: reset, |
|||
ERROR_SYMBOL: ERROR_SYMBOL, |
|||
} |
|||
|
|||
function Namespace(name) { |
|||
this.name = name |
|||
// changed in 2.7: no default context
|
|||
this.active = null |
|||
this._set = [] |
|||
this.id = null |
|||
this._contexts = new Map() |
|||
this._indent = 0 |
|||
this._hook = null |
|||
} |
|||
|
|||
Namespace.prototype.set = function set(key, value) { |
|||
if (!this.active) { |
|||
throw new Error( |
|||
"No context available. ns.run() or ns.bind() must be called first." |
|||
) |
|||
} |
|||
|
|||
this.active[key] = value |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
indentStr + |
|||
"CONTEXT-SET KEY:" + |
|||
key + |
|||
"=" + |
|||
value + |
|||
" in ns:" + |
|||
this.name + |
|||
" currentUid:" + |
|||
currentUid + |
|||
" active:" + |
|||
util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) |
|||
) |
|||
} |
|||
|
|||
return value |
|||
} |
|||
|
|||
Namespace.prototype.get = function get(key) { |
|||
if (!this.active) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.currentId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}` |
|||
) |
|||
} |
|||
return undefined |
|||
} |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
indentStr + |
|||
"CONTEXT-GETTING KEY:" + |
|||
key + |
|||
"=" + |
|||
this.active[key] + |
|||
" (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" active:" + |
|||
util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) |
|||
) |
|||
debug2( |
|||
`${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${ |
|||
this.active[key] |
|||
} currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ |
|||
this._set.length |
|||
} active:${util.inspect(this.active)}` |
|||
) |
|||
} |
|||
return this.active[key] |
|||
} |
|||
|
|||
Namespace.prototype.createContext = function createContext() { |
|||
// Prototype inherit existing context if created a new child context within existing context.
|
|||
let context = Object.create(this.active ? this.active : Object.prototype) |
|||
context._ns_name = this.name |
|||
context.id = currentUid |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-CREATED Context: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ |
|||
this._set.length |
|||
} context:${util.inspect(context, { |
|||
showHidden: true, |
|||
depth: 2, |
|||
colors: true, |
|||
})}` |
|||
) |
|||
} |
|||
|
|||
return context |
|||
} |
|||
|
|||
Namespace.prototype.run = function run(fn) { |
|||
let context = this.createContext() |
|||
this.enter(context) |
|||
|
|||
try { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-RUN BEGIN: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} context:${util.inspect(context)}` |
|||
) |
|||
} |
|||
fn(context) |
|||
return context |
|||
} catch (exception) { |
|||
if (exception) { |
|||
exception[ERROR_SYMBOL] = context |
|||
} |
|||
throw exception |
|||
} finally { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-RUN END: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} ${util.inspect(context)}` |
|||
) |
|||
} |
|||
this.exit(context) |
|||
} |
|||
} |
|||
|
|||
Namespace.prototype.runAndReturn = function runAndReturn(fn) { |
|||
let value |
|||
this.run(function (context) { |
|||
value = fn(context) |
|||
}) |
|||
return value |
|||
} |
|||
|
|||
/** |
|||
* Uses global Promise and assumes Promise is cls friendly or wrapped already. |
|||
* @param {function} fn |
|||
* @returns {*} |
|||
*/ |
|||
Namespace.prototype.runPromise = function runPromise(fn) { |
|||
let context = this.createContext() |
|||
this.enter(context) |
|||
|
|||
let promise = fn(context) |
|||
if (!promise || !promise.then || !promise.catch) { |
|||
throw new Error("fn must return a promise.") |
|||
} |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"CONTEXT-runPromise BEFORE: (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" len:" + |
|||
this._set.length + |
|||
" " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
|
|||
return promise |
|||
.then(result => { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"CONTEXT-runPromise AFTER then: (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" len:" + |
|||
this._set.length + |
|||
" " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
this.exit(context) |
|||
return result |
|||
}) |
|||
.catch(err => { |
|||
err[ERROR_SYMBOL] = context |
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"CONTEXT-runPromise AFTER catch: (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" len:" + |
|||
this._set.length + |
|||
" " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
this.exit(context) |
|||
throw err |
|||
}) |
|||
} |
|||
|
|||
Namespace.prototype.bind = function bindFactory(fn, context) { |
|||
if (!context) { |
|||
if (!this.active) { |
|||
context = this.createContext() |
|||
} else { |
|||
context = this.active |
|||
} |
|||
} |
|||
|
|||
let self = this |
|||
return function clsBind() { |
|||
self.enter(context) |
|||
try { |
|||
return fn.apply(this, arguments) |
|||
} catch (exception) { |
|||
if (exception) { |
|||
exception[ERROR_SYMBOL] = context |
|||
} |
|||
throw exception |
|||
} finally { |
|||
self.exit(context) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Namespace.prototype.enter = function enter(context) { |
|||
assert.ok(context, "context must be provided for entering") |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-ENTER: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} ${util.inspect(context)}` |
|||
) |
|||
} |
|||
|
|||
this._set.push(this.active) |
|||
this.active = context |
|||
} |
|||
|
|||
Namespace.prototype.exit = function exit(context) { |
|||
assert.ok(context, "context must be provided for exiting") |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-EXIT: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} ${util.inspect(context)}` |
|||
) |
|||
} |
|||
|
|||
// Fast path for most exits that are at the top of the stack
|
|||
if (this.active === context) { |
|||
assert.ok(this._set.length, "can't remove top context") |
|||
this.active = this._set.pop() |
|||
return |
|||
} |
|||
|
|||
// Fast search in the stack using lastIndexOf
|
|||
let index = this._set.lastIndexOf(context) |
|||
|
|||
if (index < 0) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"??ERROR?? context exiting but not entered - ignoring: " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
assert.ok( |
|||
index >= 0, |
|||
"context not currently entered; can't exit. \n" + |
|||
util.inspect(this) + |
|||
"\n" + |
|||
util.inspect(context) |
|||
) |
|||
} else { |
|||
assert.ok(index, "can't remove top context") |
|||
this._set.splice(index, 1) |
|||
} |
|||
} |
|||
|
|||
Namespace.prototype.bindEmitter = function bindEmitter(emitter) { |
|||
assert.ok( |
|||
emitter.on && emitter.addListener && emitter.emit, |
|||
"can only bind real EEs" |
|||
) |
|||
|
|||
let namespace = this |
|||
let thisSymbol = "context@" + this.name |
|||
|
|||
// Capture the context active at the time the emitter is bound.
|
|||
function attach(listener) { |
|||
if (!listener) { |
|||
return |
|||
} |
|||
if (!listener[CONTEXTS_SYMBOL]) { |
|||
listener[CONTEXTS_SYMBOL] = Object.create(null) |
|||
} |
|||
|
|||
listener[CONTEXTS_SYMBOL][thisSymbol] = { |
|||
namespace: namespace, |
|||
context: namespace.active, |
|||
} |
|||
} |
|||
|
|||
// At emit time, bind the listener within the correct context.
|
|||
function bind(unwrapped) { |
|||
if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) { |
|||
return unwrapped |
|||
} |
|||
|
|||
let wrapped = unwrapped |
|||
let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL] |
|||
Object.keys(unwrappedContexts).forEach(function (name) { |
|||
let thunk = unwrappedContexts[name] |
|||
wrapped = thunk.namespace.bind(wrapped, thunk.context) |
|||
}) |
|||
return wrapped |
|||
} |
|||
|
|||
wrapEmitter(emitter, attach, bind) |
|||
} |
|||
|
|||
/** |
|||
* If an error comes out of a namespace, it will have a context attached to it. |
|||
* This function knows how to find it. |
|||
* |
|||
* @param {Error} exception Possibly annotated error. |
|||
*/ |
|||
Namespace.prototype.fromException = function fromException(exception) { |
|||
return exception[ERROR_SYMBOL] |
|||
} |
|||
|
|||
function getNamespace(name) { |
|||
return process.namespaces[name] |
|||
} |
|||
|
|||
function createNamespace(name) { |
|||
assert.ok(name, "namespace must be given a name.") |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2(`NS-CREATING NAMESPACE (${name})`) |
|||
} |
|||
let namespace = new Namespace(name) |
|||
namespace.id = currentUid |
|||
|
|||
const hook = async_hooks.createHook({ |
|||
init(asyncId, type, triggerId, resource) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
|
|||
//CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec
|
|||
// let initContext = namespace.active;
|
|||
// if(!initContext && triggerId) {
|
|||
// let parentContext = namespace._contexts.get(triggerId);
|
|||
// if (parentContext) {
|
|||
// namespace.active = parentContext;
|
|||
// namespace._contexts.set(currentUid, parentContext);
|
|||
// if (DEBUG_CLS_HOOKED) {
|
|||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|||
// debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|||
// }
|
|||
// } else if (DEBUG_CLS_HOOKED) {
|
|||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|||
// debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|||
// }
|
|||
// }else {
|
|||
// namespace._contexts.set(currentUid, namespace.active);
|
|||
// if (DEBUG_CLS_HOOKED) {
|
|||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|||
// debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|||
// }
|
|||
// }
|
|||
if (namespace.active) { |
|||
namespace._contexts.set(asyncId, namespace.active) |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
} else if (currentUid === 0) { |
|||
// CurrentId will be 0 when triggered from C++. Promise events
|
|||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const triggerIdContext = namespace._contexts.get(triggerId) |
|||
if (triggerIdContext) { |
|||
namespace._contexts.set(asyncId, triggerIdContext) |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
} else if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
} |
|||
|
|||
if (DEBUG_CLS_HOOKED && type === "PROMISE") { |
|||
debug2(util.inspect(resource, { showHidden: true })) |
|||
const parentId = resource.parentId |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
}, |
|||
before(asyncId) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
let context |
|||
|
|||
/* |
|||
if(currentUid === 0){ |
|||
// CurrentId will be 0 when triggered from C++. Promise events
|
|||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|||
//const triggerId = async_hooks.triggerAsyncId();
|
|||
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
|||
}else{ |
|||
context = namespace._contexts.get(currentUid); |
|||
} |
|||
*/ |
|||
|
|||
//HACK to work with promises until they are fixed in node > 8.1.1
|
|||
context = |
|||
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) |
|||
|
|||
if (context) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(context)}` |
|||
) |
|||
namespace._indent += 2 |
|||
} |
|||
|
|||
namespace.enter(context) |
|||
} else if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} namespace._contexts:${util.inspect(namespace._contexts, { |
|||
showHidden: true, |
|||
depth: 2, |
|||
colors: true, |
|||
})}` |
|||
) |
|||
namespace._indent += 2 |
|||
} |
|||
}, |
|||
after(asyncId) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
let context // = namespace._contexts.get(currentUid);
|
|||
/* |
|||
if(currentUid === 0){ |
|||
// CurrentId will be 0 when triggered from C++. Promise events
|
|||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|||
//const triggerId = async_hooks.triggerAsyncId();
|
|||
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
|||
}else{ |
|||
context = namespace._contexts.get(currentUid); |
|||
} |
|||
*/ |
|||
//HACK to work with promises until they are fixed in node > 8.1.1
|
|||
context = |
|||
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) |
|||
|
|||
if (context) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
namespace._indent -= 2 |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(context)}` |
|||
) |
|||
} |
|||
|
|||
namespace.exit(context) |
|||
} else if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
namespace._indent -= 2 |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(context)}` |
|||
) |
|||
} |
|||
}, |
|||
destroy(asyncId) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(namespace._contexts.get(currentUid))}` |
|||
) |
|||
} |
|||
|
|||
namespace._contexts.delete(asyncId) |
|||
}, |
|||
}) |
|||
|
|||
hook.enable() |
|||
namespace._hook = hook |
|||
|
|||
process.namespaces[name] = namespace |
|||
return namespace |
|||
} |
|||
|
|||
function destroyNamespace(name) { |
|||
let namespace = getNamespace(name) |
|||
|
|||
assert.ok(namespace, "can't delete nonexistent namespace! \"" + name + '"') |
|||
assert.ok( |
|||
namespace.id, |
|||
"don't assign to process.namespaces directly! " + util.inspect(namespace) |
|||
) |
|||
|
|||
namespace._hook.disable() |
|||
namespace._contexts = null |
|||
process.namespaces[name] = null |
|||
} |
|||
|
|||
function reset() { |
|||
// must unregister async listeners
|
|||
if (process.namespaces) { |
|||
Object.keys(process.namespaces).forEach(function (name) { |
|||
destroyNamespace(name) |
|||
}) |
|||
} |
|||
process.namespaces = Object.create(null) |
|||
} |
|||
|
|||
process.namespaces = process.namespaces || {} |
|||
|
|||
//const fs = require('fs');
|
|||
function debug2(...args) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
//fs.writeSync(1, `${util.format(...args)}\n`);
|
|||
process._rawDebug(`${util.format(...args)}`) |
|||
} |
|||
} |
|||
|
|||
/*function getFunctionName(fn) { |
|||
if (!fn) { |
|||
return fn; |
|||
} |
|||
if (typeof fn === 'function') { |
|||
if (fn.name) { |
|||
return fn.name; |
|||
} |
|||
return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; |
|||
} else if (fn.constructor && fn.constructor.name) { |
|||
return fn.constructor.name; |
|||
} |
|||
}*/ |
|||
@ -1,73 +1,47 @@ |
|||
const cls = require("cls-hooked") |
|||
const cls = require("../clshooked") |
|||
const { newid } = require("../hashing") |
|||
|
|||
const REQUEST_ID_KEY = "requestId" |
|||
|
|||
class FunctionContext { |
|||
static getMiddleware(updateCtxFn = null, contextName = "session") { |
|||
const namespace = this.createNamespace(contextName) |
|||
|
|||
return async function (ctx, next) { |
|||
await new Promise( |
|||
namespace.bind(function (resolve, reject) { |
|||
// store a contextual request ID that can be used anywhere (audit logs)
|
|||
namespace.set(REQUEST_ID_KEY, newid()) |
|||
namespace.bindEmitter(ctx.req) |
|||
namespace.bindEmitter(ctx.res) |
|||
|
|||
if (updateCtxFn) { |
|||
updateCtxFn(ctx) |
|||
} |
|||
next().then(resolve).catch(reject) |
|||
}) |
|||
) |
|||
} |
|||
const MAIN_CTX = cls.createNamespace("main") |
|||
|
|||
function getContextStorage(namespace) { |
|||
if (namespace && namespace.active) { |
|||
let contextData = namespace.active |
|||
delete contextData.id |
|||
delete contextData._ns_name |
|||
return contextData |
|||
} |
|||
return {} |
|||
} |
|||
|
|||
static run(callback, contextName = "session") { |
|||
const namespace = this.createNamespace(contextName) |
|||
|
|||
return namespace.runAndReturn(callback) |
|||
class FunctionContext { |
|||
static run(callback) { |
|||
return MAIN_CTX.runAndReturn(async () => { |
|||
const namespaceId = newid() |
|||
MAIN_CTX.set(REQUEST_ID_KEY, namespaceId) |
|||
const namespace = cls.createNamespace(namespaceId) |
|||
let response = await namespace.runAndReturn(callback) |
|||
cls.destroyNamespace(namespaceId) |
|||
return response |
|||
}) |
|||
} |
|||
|
|||
static setOnContext(key, value, contextName = "session") { |
|||
const namespace = this.createNamespace(contextName) |
|||
static setOnContext(key, value) { |
|||
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) |
|||
const namespace = cls.getNamespace(namespaceId) |
|||
namespace.set(key, value) |
|||
} |
|||
|
|||
static getContextStorage() { |
|||
if (this._namespace && this._namespace.active) { |
|||
let contextData = this._namespace.active |
|||
delete contextData.id |
|||
delete contextData._ns_name |
|||
return contextData |
|||
} |
|||
|
|||
return {} |
|||
} |
|||
|
|||
static getFromContext(key) { |
|||
const context = this.getContextStorage() |
|||
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) |
|||
const namespace = cls.getNamespace(namespaceId) |
|||
const context = getContextStorage(namespace) |
|||
if (context) { |
|||
return context[key] |
|||
} else { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
static destroyNamespace(name = "session") { |
|||
if (this._namespace) { |
|||
cls.destroyNamespace(name) |
|||
this._namespace = null |
|||
} |
|||
} |
|||
|
|||
static createNamespace(name = "session") { |
|||
if (!this._namespace) { |
|||
this._namespace = cls.createNamespace(name) |
|||
} |
|||
return this._namespace |
|||
} |
|||
} |
|||
|
|||
module.exports = FunctionContext |
|||
|
|||
@ -0,0 +1,50 @@ |
|||
import { |
|||
IdentityContext, |
|||
IdentityType, |
|||
User, |
|||
UserContext, |
|||
isCloudAccount, |
|||
Account, |
|||
AccountUserContext, |
|||
} from "@budibase/types" |
|||
import * as context from "." |
|||
|
|||
export const getIdentity = (): IdentityContext | undefined => { |
|||
return context.getIdentity() |
|||
} |
|||
|
|||
export const doInIdentityContext = (identity: IdentityContext, task: any) => { |
|||
return context.doInIdentityContext(identity, task) |
|||
} |
|||
|
|||
export const doInUserContext = (user: User, task: any) => { |
|||
const userContext: UserContext = { |
|||
...user, |
|||
_id: user._id as string, |
|||
type: IdentityType.USER, |
|||
} |
|||
return doInIdentityContext(userContext, task) |
|||
} |
|||
|
|||
export const doInAccountContext = (account: Account, task: any) => { |
|||
const _id = getAccountUserId(account) |
|||
const tenantId = account.tenantId |
|||
const accountContext: AccountUserContext = { |
|||
_id, |
|||
type: IdentityType.USER, |
|||
tenantId, |
|||
account, |
|||
} |
|||
return doInIdentityContext(accountContext, task) |
|||
} |
|||
|
|||
export const getAccountUserId = (account: Account) => { |
|||
let userId: string |
|||
if (isCloudAccount(account)) { |
|||
userId = account.budibaseUserId |
|||
} else { |
|||
// use account id as user id for self hosting
|
|||
userId = account.accountId |
|||
} |
|||
return userId |
|||
} |
|||
@ -1,13 +1,77 @@ |
|||
let Pouch |
|||
const pouch = require("./pouch") |
|||
const env = require("../environment") |
|||
|
|||
module.exports.setDB = pouch => { |
|||
Pouch = pouch |
|||
let PouchDB |
|||
let initialised = false |
|||
const dbList = new Set() |
|||
|
|||
const put = |
|||
dbPut => |
|||
async (doc, options = {}) => { |
|||
if (!doc.createdAt) { |
|||
doc.createdAt = new Date().toISOString() |
|||
} |
|||
doc.updatedAt = new Date().toISOString() |
|||
return dbPut(doc, options) |
|||
} |
|||
|
|||
const checkInitialised = () => { |
|||
if (!initialised) { |
|||
throw new Error("init has not been called") |
|||
} |
|||
} |
|||
|
|||
exports.init = opts => { |
|||
PouchDB = pouch.getPouch(opts) |
|||
initialised = true |
|||
} |
|||
|
|||
// NOTE: THIS IS A DANGEROUS FUNCTION - USE WITH CAUTION
|
|||
// this function is prone to leaks, should only be used
|
|||
// in situations that using the function doWithDB does not work
|
|||
exports.dangerousGetDB = (dbName, opts) => { |
|||
checkInitialised() |
|||
if (env.isTest()) { |
|||
dbList.add(dbName) |
|||
} |
|||
const db = new PouchDB(dbName, opts) |
|||
const dbPut = db.put |
|||
db.put = put(dbPut) |
|||
return db |
|||
} |
|||
|
|||
// use this function if you have called dangerousGetDB - close
|
|||
// the databases you've opened once finished
|
|||
exports.closeDB = async db => { |
|||
if (!db || env.isTest()) { |
|||
return |
|||
} |
|||
try { |
|||
// specifically await so that if there is an error, it can be ignored
|
|||
return await db.close() |
|||
} catch (err) { |
|||
// ignore error, already closed
|
|||
} |
|||
} |
|||
|
|||
module.exports.getDB = dbName => { |
|||
return new Pouch(dbName) |
|||
// we have to use a callback for this so that we can close
|
|||
// the DB when we're done, without this manual requests would
|
|||
// need to close the database when done with it to avoid memory leaks
|
|||
exports.doWithDB = async (dbName, cb, opts = {}) => { |
|||
const db = exports.dangerousGetDB(dbName, opts) |
|||
// need this to be async so that we can correctly close DB after all
|
|||
// async operations have been completed
|
|||
try { |
|||
return await cb(db) |
|||
} finally { |
|||
await exports.closeDB(db) |
|||
} |
|||
} |
|||
|
|||
module.exports.getCouch = () => { |
|||
return Pouch |
|||
exports.allDbs = () => { |
|||
if (!env.isTest()) { |
|||
throw new Error("Cannot be used outside test environment.") |
|||
} |
|||
checkInitialised() |
|||
return [...dbList] |
|||
} |
|||
|
|||
@ -0,0 +1,96 @@ |
|||
const PouchDB = require("pouchdb") |
|||
const env = require("../environment") |
|||
|
|||
function getUrlInfo() { |
|||
let url = env.COUCH_DB_URL |
|||
let username, password, host |
|||
const [protocol, rest] = url.split("://") |
|||
if (url.includes("@")) { |
|||
const hostParts = rest.split("@") |
|||
host = hostParts[1] |
|||
const authParts = hostParts[0].split(":") |
|||
username = authParts[0] |
|||
password = authParts[1] |
|||
} else { |
|||
host = rest |
|||
} |
|||
return { |
|||
url: `${protocol}://${host}`, |
|||
auth: { |
|||
username, |
|||
password, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
exports.getCouchInfo = () => { |
|||
const urlInfo = getUrlInfo() |
|||
let username |
|||
let password |
|||
if (env.COUCH_DB_USERNAME) { |
|||
// set from env
|
|||
username = env.COUCH_DB_USERNAME |
|||
} else if (urlInfo.auth.username) { |
|||
// set from url
|
|||
username = urlInfo.auth.username |
|||
} else if (!env.isTest()) { |
|||
throw new Error("CouchDB username not set") |
|||
} |
|||
if (env.COUCH_DB_PASSWORD) { |
|||
// set from env
|
|||
password = env.COUCH_DB_PASSWORD |
|||
} else if (urlInfo.auth.password) { |
|||
// set from url
|
|||
password = urlInfo.auth.password |
|||
} else if (!env.isTest()) { |
|||
throw new Error("CouchDB password not set") |
|||
} |
|||
const authCookie = Buffer.from(`${username}:${password}`).toString("base64") |
|||
return { |
|||
url: urlInfo.url, |
|||
auth: { |
|||
username: username, |
|||
password: password, |
|||
}, |
|||
cookie: `Basic ${authCookie}`, |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Return a constructor for PouchDB. |
|||
* This should be rarely used outside of the main application config. |
|||
* Exposed for exceptional cases such as in-memory views. |
|||
*/ |
|||
exports.getPouch = (opts = {}) => { |
|||
let { url, cookie } = exports.getCouchInfo() |
|||
let POUCH_DB_DEFAULTS = { |
|||
prefix: url, |
|||
fetch: (url, opts) => { |
|||
// use a specific authorization cookie - be very explicit about how we authenticate
|
|||
opts.headers.set("Authorization", cookie) |
|||
return PouchDB.fetch(url, opts) |
|||
}, |
|||
} |
|||
|
|||
if (opts.inMemory) { |
|||
const inMemory = require("pouchdb-adapter-memory") |
|||
PouchDB.plugin(inMemory) |
|||
POUCH_DB_DEFAULTS = { |
|||
prefix: undefined, |
|||
adapter: "memory", |
|||
} |
|||
} |
|||
|
|||
if (opts.replication) { |
|||
const replicationStream = require("pouchdb-replication-stream") |
|||
PouchDB.plugin(replicationStream.plugin) |
|||
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream) |
|||
} |
|||
|
|||
if (opts.find) { |
|||
const find = require("pouchdb-find") |
|||
PouchDB.plugin(find) |
|||
} |
|||
|
|||
return PouchDB.defaults(POUCH_DB_DEFAULTS) |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
require("../../../tests/utilities/TestConfiguration") |
|||
const { dangerousGetDB } = require("../") |
|||
|
|||
describe("db", () => { |
|||
|
|||
describe("getDB", () => { |
|||
it("returns a db", async () => { |
|||
const db = dangerousGetDB("test") |
|||
expect(db).toBeDefined() |
|||
expect(db._adapter).toBe("memory") |
|||
expect(db.prefix).toBe("_pouch_") |
|||
expect(db.name).toBe("test") |
|||
}) |
|||
|
|||
it("uses the custom put function", async () => { |
|||
const db = dangerousGetDB("test") |
|||
let doc = { _id: "test" } |
|||
await db.put(doc) |
|||
doc = await db.get(doc._id) |
|||
expect(doc.createdAt).toBe(new Date().toISOString()) |
|||
expect(doc.updatedAt).toBe(new Date().toISOString()) |
|||
await db.destroy() |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
@ -0,0 +1,194 @@ |
|||
require("../../../tests/utilities/TestConfiguration"); |
|||
const { |
|||
generateAppID, |
|||
getDevelopmentAppID, |
|||
getProdAppID, |
|||
isDevAppID, |
|||
isProdAppID, |
|||
getPlatformUrl, |
|||
getScopedConfig |
|||
} = require("../utils") |
|||
const tenancy = require("../../tenancy"); |
|||
const { Configs, DEFAULT_TENANT_ID } = require("../../constants"); |
|||
const env = require("../../environment") |
|||
|
|||
describe("utils", () => { |
|||
describe("app ID manipulation", () => { |
|||
|
|||
function getID() { |
|||
const appId = generateAppID() |
|||
const split = appId.split("_") |
|||
const uuid = split[split.length - 1] |
|||
const devAppId = `app_dev_${uuid}` |
|||
return { appId, devAppId, split, uuid } |
|||
} |
|||
|
|||
it("should be able to generate a new app ID", () => { |
|||
expect(generateAppID().startsWith("app_")).toEqual(true) |
|||
}) |
|||
|
|||
it("should be able to convert a production app ID to development", () => { |
|||
const { appId, uuid } = getID() |
|||
expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to convert a development app ID to development", () => { |
|||
const { devAppId, uuid } = getID() |
|||
expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to convert a development ID to a production", () => { |
|||
const { devAppId, uuid } = getID() |
|||
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to convert a production ID to production", () => { |
|||
const { appId, uuid } = getID() |
|||
expect(getProdAppID(appId)).toEqual(`app_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to confirm dev app ID is development", () => { |
|||
const { devAppId } = getID() |
|||
expect(isDevAppID(devAppId)).toEqual(true) |
|||
}) |
|||
|
|||
it("should be able to confirm prod app ID is not development", () => { |
|||
const { appId } = getID() |
|||
expect(isDevAppID(appId)).toEqual(false) |
|||
}) |
|||
|
|||
it("should be able to confirm prod app ID is prod", () => { |
|||
const { appId } = getID() |
|||
expect(isProdAppID(appId)).toEqual(true) |
|||
}) |
|||
|
|||
it("should be able to confirm dev app ID is not prod", () => { |
|||
const { devAppId } = getID() |
|||
expect(isProdAppID(devAppId)).toEqual(false) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
const DB_URL = "http://dburl.com" |
|||
const DEFAULT_URL = "http://localhost:10000" |
|||
const ENV_URL = "http://env.com" |
|||
|
|||
const setDbPlatformUrl = async () => { |
|||
const db = tenancy.getGlobalDB() |
|||
db.put({ |
|||
_id: "config_settings", |
|||
type: Configs.SETTINGS, |
|||
config: { |
|||
platformUrl: DB_URL |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const clearSettingsConfig = async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const db = tenancy.getGlobalDB() |
|||
try { |
|||
const config = await db.get("config_settings") |
|||
await db.remove("config_settings", config._rev) |
|||
} catch (e) { |
|||
if (e.status !== 404) { |
|||
throw e |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
describe("getPlatformUrl", () => { |
|||
describe("self host", () => { |
|||
|
|||
beforeEach(async () => { |
|||
env._set("SELF_HOST", 1) |
|||
await clearSettingsConfig() |
|||
}) |
|||
|
|||
it("gets the default url", async () => { |
|||
await tenancy.doInTenant(null, async () => { |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(DEFAULT_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("gets the platform url from the environment", async () => { |
|||
await tenancy.doInTenant(null, async () => { |
|||
env._set("PLATFORM_URL", ENV_URL) |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(ENV_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("gets the platform url from the database", async () => { |
|||
await tenancy.doInTenant(null, async () => { |
|||
await setDbPlatformUrl() |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(DB_URL) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
|
|||
describe("cloud", () => { |
|||
const TENANT_AWARE_URL = "http://default.env.com" |
|||
|
|||
beforeEach(async () => { |
|||
env._set("SELF_HOSTED", 0) |
|||
env._set("MULTI_TENANCY", 1) |
|||
env._set("PLATFORM_URL", ENV_URL) |
|||
await clearSettingsConfig() |
|||
}) |
|||
|
|||
it("gets the platform url from the environment without tenancy", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const url = await getPlatformUrl({ tenantAware: false }) |
|||
expect(url).toBe(ENV_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("gets the platform url from the environment with tenancy", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(TENANT_AWARE_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("never gets the platform url from the database", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
await setDbPlatformUrl() |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(TENANT_AWARE_URL) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("getScopedConfig", () => { |
|||
describe("settings config", () => { |
|||
|
|||
beforeEach(async () => { |
|||
env._set("SELF_HOSTED", 1) |
|||
env._set("PLATFORM_URL", "") |
|||
await clearSettingsConfig() |
|||
}) |
|||
|
|||
it("returns the platform url with an existing config", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
await setDbPlatformUrl() |
|||
const db = tenancy.getGlobalDB() |
|||
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) |
|||
expect(config.platformUrl).toBe(DB_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("returns the platform url without an existing config", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const db = tenancy.getGlobalDB() |
|||
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) |
|||
expect(config.platformUrl).toBe(DEFAULT_URL) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -1,36 +0,0 @@ |
|||
function isTest() { |
|||
return ( |
|||
process.env.NODE_ENV === "jest" || |
|||
process.env.NODE_ENV === "cypress" || |
|||
process.env.JEST_WORKER_ID != null |
|||
) |
|||
} |
|||
|
|||
module.exports = { |
|||
JWT_SECRET: process.env.JWT_SECRET, |
|||
COUCH_DB_URL: process.env.COUCH_DB_URL, |
|||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER, |
|||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, |
|||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, |
|||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, |
|||
SALT_ROUNDS: process.env.SALT_ROUNDS, |
|||
REDIS_URL: process.env.REDIS_URL, |
|||
REDIS_PASSWORD: process.env.REDIS_PASSWORD, |
|||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, |
|||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, |
|||
AWS_REGION: process.env.AWS_REGION, |
|||
MINIO_URL: process.env.MINIO_URL, |
|||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, |
|||
MULTI_TENANCY: process.env.MULTI_TENANCY, |
|||
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL, |
|||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY, |
|||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, |
|||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), |
|||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, |
|||
PLATFORM_URL: process.env.PLATFORM_URL, |
|||
isTest, |
|||
_set(key, value) { |
|||
process.env[key] = value |
|||
module.exports[key] = value |
|||
}, |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
function isTest() { |
|||
return ( |
|||
process.env.NODE_ENV === "jest" || |
|||
process.env.NODE_ENV === "cypress" || |
|||
process.env.JEST_WORKER_ID != null |
|||
) |
|||
} |
|||
|
|||
function isDev() { |
|||
return process.env.NODE_ENV !== "production" |
|||
} |
|||
|
|||
let LOADED = false |
|||
if (!LOADED && isDev() && !isTest()) { |
|||
require("dotenv").config() |
|||
LOADED = true |
|||
} |
|||
|
|||
const env: any = { |
|||
isTest, |
|||
isDev, |
|||
JWT_SECRET: process.env.JWT_SECRET, |
|||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", |
|||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER, |
|||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, |
|||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, |
|||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, |
|||
SALT_ROUNDS: process.env.SALT_ROUNDS, |
|||
REDIS_URL: process.env.REDIS_URL, |
|||
REDIS_PASSWORD: process.env.REDIS_PASSWORD, |
|||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, |
|||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, |
|||
AWS_REGION: process.env.AWS_REGION, |
|||
MINIO_URL: process.env.MINIO_URL, |
|||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, |
|||
MULTI_TENANCY: process.env.MULTI_TENANCY, |
|||
ACCOUNT_PORTAL_URL: |
|||
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", |
|||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY, |
|||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, |
|||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), |
|||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, |
|||
PLATFORM_URL: process.env.PLATFORM_URL, |
|||
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, |
|||
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, |
|||
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS, |
|||
BACKUPS_BUCKET_NAME: process.env.BACKUPS_BUCKET_NAME || "backups", |
|||
APPS_BUCKET_NAME: process.env.APPS_BUCKET_NAME || "prod-budi-app-assets", |
|||
TEMPLATES_BUCKET_NAME: process.env.TEMPLATES_BUCKET_NAME || "templates", |
|||
GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global", |
|||
GLOBAL_CLOUD_BUCKET_NAME: |
|||
process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads", |
|||
USE_COUCH: process.env.USE_COUCH || true, |
|||
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, |
|||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, |
|||
SERVICE: process.env.SERVICE || "budibase", |
|||
DEPLOYMENT_ENVIRONMENT: |
|||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", |
|||
_set(key: any, value: any) { |
|||
process.env[key] = value |
|||
module.exports[key] = value |
|||
}, |
|||
} |
|||
|
|||
// clean up any environment variable edge cases
|
|||
for (let [key, value] of Object.entries(env)) { |
|||
// handle the edge case of "0" to disable an environment variable
|
|||
if (value === "0") { |
|||
env[key] = 0 |
|||
} |
|||
} |
|||
|
|||
export = env |
|||
@ -0,0 +1,11 @@ |
|||
class BudibaseError extends Error { |
|||
constructor(message, code, type) { |
|||
super(message) |
|||
this.code = code |
|||
this.type = type |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
BudibaseError, |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
const { BudibaseError } = require("./base") |
|||
|
|||
class GenericError extends BudibaseError { |
|||
constructor(message, code, type) { |
|||
super(message, code, type ? type : "generic") |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
GenericError, |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
const { GenericError } = require("./generic") |
|||
|
|||
class HTTPError extends GenericError { |
|||
constructor(message, httpStatus, code = "http", type = "generic") { |
|||
super(message, code, type) |
|||
this.status = httpStatus |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
HTTPError, |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
const http = require("./http") |
|||
const licensing = require("./licensing") |
|||
|
|||
const codes = { |
|||
...licensing.codes, |
|||
} |
|||
|
|||
const types = [licensing.type] |
|||
|
|||
const context = { |
|||
...licensing.context, |
|||
} |
|||
|
|||
const getPublicError = err => { |
|||
let error |
|||
if (err.code || err.type) { |
|||
// add generic error information
|
|||
error = { |
|||
code: err.code, |
|||
type: err.type, |
|||
} |
|||
|
|||
if (err.code && context[err.code]) { |
|||
error = { |
|||
...error, |
|||
// get any additional context from this error
|
|||
...context[err.code](err), |
|||
} |
|||
} |
|||
} |
|||
|
|||
return error |
|||
} |
|||
|
|||
module.exports = { |
|||
codes, |
|||
types, |
|||
errors: { |
|||
UsageLimitError: licensing.UsageLimitError, |
|||
HTTPError: http.HTTPError, |
|||
}, |
|||
getPublicError, |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
const { HTTPError } = require("./http") |
|||
|
|||
const type = "license_error" |
|||
|
|||
const codes = { |
|||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", |
|||
} |
|||
|
|||
const context = { |
|||
[codes.USAGE_LIMIT_EXCEEDED]: err => { |
|||
return { |
|||
limitName: err.limitName, |
|||
} |
|||
}, |
|||
} |
|||
|
|||
class UsageLimitError extends HTTPError { |
|||
constructor(message, limitName) { |
|||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type) |
|||
this.limitName = limitName |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
type, |
|||
codes, |
|||
context, |
|||
UsageLimitError, |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
import env from "../environment" |
|||
import tenancy from "../tenancy" |
|||
import * as dbUtils from "../db/utils" |
|||
import { Configs } from "../constants" |
|||
import { withCache, TTL, CacheKeys } from "../cache/generic" |
|||
|
|||
export const enabled = async () => { |
|||
// cloud - always use the environment variable
|
|||
if (!env.SELF_HOSTED) { |
|||
return !!env.ENABLE_ANALYTICS |
|||
} |
|||
|
|||
// self host - prefer the settings doc
|
|||
// use cache as events have high throughput
|
|||
const enabledInDB = await withCache( |
|||
CacheKeys.ANALYTICS_ENABLED, |
|||
TTL.ONE_DAY, |
|||
async () => { |
|||
const settings = await getSettingsDoc() |
|||
|
|||
// need to do explicit checks in case the field is not set
|
|||
if (settings?.config?.analyticsEnabled === false) { |
|||
return false |
|||
} else if (settings?.config?.analyticsEnabled === true) { |
|||
return true |
|||
} |
|||
} |
|||
) |
|||
|
|||
if (enabledInDB !== undefined) { |
|||
return enabledInDB |
|||
} |
|||
|
|||
// fallback to the environment variable
|
|||
// explicitly check for 0 or false here, undefined or otherwise is treated as true
|
|||
const envEnabled: any = env.ENABLE_ANALYTICS |
|||
if (envEnabled === 0 || envEnabled === false) { |
|||
return false |
|||
} else { |
|||
return true |
|||
} |
|||
} |
|||
|
|||
const getSettingsDoc = async () => { |
|||
const db = tenancy.getGlobalDB() |
|||
let settings |
|||
try { |
|||
settings = await db.get( |
|||
dbUtils.generateConfigID({ type: Configs.SETTINGS }) |
|||
) |
|||
} catch (e: any) { |
|||
if (e.status !== 404) { |
|||
throw e |
|||
} |
|||
} |
|||
return settings |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
import { |
|||
Event, |
|||
BackfillMetadata, |
|||
CachedEvent, |
|||
SSOCreatedEvent, |
|||
AutomationCreatedEvent, |
|||
AutomationStepCreatedEvent, |
|||
DatasourceCreatedEvent, |
|||
LayoutCreatedEvent, |
|||
QueryCreatedEvent, |
|||
RoleCreatedEvent, |
|||
ScreenCreatedEvent, |
|||
TableCreatedEvent, |
|||
ViewCreatedEvent, |
|||
ViewCalculationCreatedEvent, |
|||
ViewFilterCreatedEvent, |
|||
AppPublishedEvent, |
|||
UserCreatedEvent, |
|||
RoleAssignedEvent, |
|||
UserPermissionAssignedEvent, |
|||
AppCreatedEvent, |
|||
} from "@budibase/types" |
|||
import * as context from "../context" |
|||
import { CacheKeys } from "../cache/generic" |
|||
import * as cache from "../cache/generic" |
|||
|
|||
// LIFECYCLE
|
|||
|
|||
export const start = async (events: Event[]) => { |
|||
const metadata: BackfillMetadata = { |
|||
eventWhitelist: events, |
|||
} |
|||
return saveBackfillMetadata(metadata) |
|||
} |
|||
|
|||
export const recordEvent = async (event: Event, properties: any) => { |
|||
const eventKey = getEventKey(event, properties) |
|||
// don't use a ttl - cleaned up by migration
|
|||
// don't use tenancy - already in the key
|
|||
await cache.store(eventKey, properties, undefined, { useTenancy: false }) |
|||
} |
|||
|
|||
export const end = async () => { |
|||
await deleteBackfillMetadata() |
|||
await clearEvents() |
|||
} |
|||
|
|||
// CRUD
|
|||
|
|||
const getBackfillMetadata = async (): Promise<BackfillMetadata | null> => { |
|||
return cache.get(CacheKeys.BACKFILL_METADATA) |
|||
} |
|||
|
|||
const saveBackfillMetadata = async ( |
|||
backfill: BackfillMetadata |
|||
): Promise<void> => { |
|||
// no TTL - deleted by backfill
|
|||
return cache.store(CacheKeys.BACKFILL_METADATA, backfill) |
|||
} |
|||
|
|||
const deleteBackfillMetadata = async (): Promise<void> => { |
|||
await cache.delete(CacheKeys.BACKFILL_METADATA) |
|||
} |
|||
|
|||
const clearEvents = async () => { |
|||
// wildcard
|
|||
const pattern = getEventKey() |
|||
const keys = await cache.keys(pattern) |
|||
|
|||
for (const key of keys) { |
|||
// delete each key
|
|||
// don't use tenancy, already in the key
|
|||
await cache.delete(key, { useTenancy: false }) |
|||
} |
|||
} |
|||
|
|||
// HELPERS
|
|||
|
|||
export const isBackfillingEvent = async (event: Event) => { |
|||
const backfill = await getBackfillMetadata() |
|||
const events = backfill?.eventWhitelist |
|||
if (events && events.includes(event)) { |
|||
return true |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
export const isAlreadySent = async (event: Event, properties: any) => { |
|||
const eventKey = getEventKey(event, properties) |
|||
const cachedEvent: CachedEvent = await cache.get(eventKey, { |
|||
useTenancy: false, |
|||
}) |
|||
return !!cachedEvent |
|||
} |
|||
|
|||
const CUSTOM_PROPERTY_SUFFIX: any = { |
|||
// APP EVENTS
|
|||
[Event.AUTOMATION_CREATED]: (properties: AutomationCreatedEvent) => { |
|||
return properties.automationId |
|||
}, |
|||
[Event.AUTOMATION_STEP_CREATED]: (properties: AutomationStepCreatedEvent) => { |
|||
return properties.stepId |
|||
}, |
|||
[Event.DATASOURCE_CREATED]: (properties: DatasourceCreatedEvent) => { |
|||
return properties.datasourceId |
|||
}, |
|||
[Event.LAYOUT_CREATED]: (properties: LayoutCreatedEvent) => { |
|||
return properties.layoutId |
|||
}, |
|||
[Event.QUERY_CREATED]: (properties: QueryCreatedEvent) => { |
|||
return properties.queryId |
|||
}, |
|||
[Event.ROLE_CREATED]: (properties: RoleCreatedEvent) => { |
|||
return properties.roleId |
|||
}, |
|||
[Event.SCREEN_CREATED]: (properties: ScreenCreatedEvent) => { |
|||
return properties.screenId |
|||
}, |
|||
[Event.TABLE_CREATED]: (properties: TableCreatedEvent) => { |
|||
return properties.tableId |
|||
}, |
|||
[Event.VIEW_CREATED]: (properties: ViewCreatedEvent) => { |
|||
return properties.tableId // best uniqueness
|
|||
}, |
|||
[Event.VIEW_CALCULATION_CREATED]: ( |
|||
properties: ViewCalculationCreatedEvent |
|||
) => { |
|||
return properties.tableId // best uniqueness
|
|||
}, |
|||
[Event.VIEW_FILTER_CREATED]: (properties: ViewFilterCreatedEvent) => { |
|||
return properties.tableId // best uniqueness
|
|||
}, |
|||
[Event.APP_CREATED]: (properties: AppCreatedEvent) => { |
|||
return properties.appId // best uniqueness
|
|||
}, |
|||
[Event.APP_PUBLISHED]: (properties: AppPublishedEvent) => { |
|||
return properties.appId // best uniqueness
|
|||
}, |
|||
// GLOBAL EVENTS
|
|||
[Event.AUTH_SSO_CREATED]: (properties: SSOCreatedEvent) => { |
|||
return properties.type |
|||
}, |
|||
[Event.AUTH_SSO_ACTIVATED]: (properties: SSOCreatedEvent) => { |
|||
return properties.type |
|||
}, |
|||
[Event.USER_CREATED]: (properties: UserCreatedEvent) => { |
|||
return properties.userId |
|||
}, |
|||
[Event.USER_PERMISSION_ADMIN_ASSIGNED]: ( |
|||
properties: UserPermissionAssignedEvent |
|||
) => { |
|||
return properties.userId |
|||
}, |
|||
[Event.USER_PERMISSION_BUILDER_ASSIGNED]: ( |
|||
properties: UserPermissionAssignedEvent |
|||
) => { |
|||
return properties.userId |
|||
}, |
|||
[Event.ROLE_ASSIGNED]: (properties: RoleAssignedEvent) => { |
|||
return `${properties.roleId}-${properties.userId}` |
|||
}, |
|||
} |
|||
|
|||
const getEventKey = (event?: Event, properties?: any) => { |
|||
let eventKey: string |
|||
|
|||
const tenantId = context.getTenantId() |
|||
if (event) { |
|||
eventKey = `${CacheKeys.EVENTS}:${tenantId}:${event}` |
|||
|
|||
// use some properties to make the key more unique
|
|||
const custom = CUSTOM_PROPERTY_SUFFIX[event] |
|||
const suffix = custom ? custom(properties) : undefined |
|||
if (suffix) { |
|||
eventKey = `${eventKey}:${suffix}` |
|||
} |
|||
} else { |
|||
eventKey = `${CacheKeys.EVENTS}:${tenantId}:*` |
|||
} |
|||
|
|||
return eventKey |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
import { Event } from "@budibase/types" |
|||
import { processors } from "./processors" |
|||
import * as identification from "./identification" |
|||
import * as backfill from "./backfill" |
|||
|
|||
export const publishEvent = async ( |
|||
event: Event, |
|||
properties: any, |
|||
timestamp?: string | number |
|||
) => { |
|||
// in future this should use async events via a distributed queue.
|
|||
const identity = await identification.getCurrentIdentity() |
|||
|
|||
const backfilling = await backfill.isBackfillingEvent(event) |
|||
// no backfill - send the event and exit
|
|||
if (!backfilling) { |
|||
await processors.processEvent(event, identity, properties, timestamp) |
|||
return |
|||
} |
|||
|
|||
// backfill active - check if the event has been sent already
|
|||
const alreadySent = await backfill.isAlreadySent(event, properties) |
|||
if (alreadySent) { |
|||
// do nothing
|
|||
return |
|||
} else { |
|||
// send and record the event
|
|||
await processors.processEvent(event, identity, properties, timestamp) |
|||
await backfill.recordEvent(event, properties) |
|||
} |
|||
} |
|||
@ -0,0 +1,302 @@ |
|||
import * as context from "../context" |
|||
import * as identityCtx from "../context/identity" |
|||
import env from "../environment" |
|||
import { |
|||
Hosting, |
|||
User, |
|||
Identity, |
|||
IdentityType, |
|||
Account, |
|||
isCloudAccount, |
|||
isSSOAccount, |
|||
TenantGroup, |
|||
SettingsConfig, |
|||
CloudAccount, |
|||
UserIdentity, |
|||
InstallationGroup, |
|||
UserContext, |
|||
Group, |
|||
} from "@budibase/types" |
|||
import { processors } from "./processors" |
|||
import * as dbUtils from "../db/utils" |
|||
import { Configs } from "../constants" |
|||
import * as hashing from "../hashing" |
|||
import * as installation from "../installation" |
|||
import { withCache, TTL, CacheKeys } from "../cache/generic" |
|||
|
|||
const pkg = require("../../package.json") |
|||
|
|||
/** |
|||
* An identity can be: |
|||
* - account user (Self host) |
|||
* - budibase user |
|||
* - tenant |
|||
* - installation |
|||
*/ |
|||
export const getCurrentIdentity = async (): Promise<Identity> => { |
|||
let identityContext = identityCtx.getIdentity() |
|||
const environment = getDeploymentEnvironment() |
|||
|
|||
let identityType |
|||
|
|||
if (!identityContext) { |
|||
identityType = IdentityType.TENANT |
|||
} else { |
|||
identityType = identityContext.type |
|||
} |
|||
|
|||
if (identityType === IdentityType.INSTALLATION) { |
|||
const installationId = await getInstallationId() |
|||
const hosting = getHostingFromEnv() |
|||
return { |
|||
id: formatDistinctId(installationId, identityType), |
|||
hosting, |
|||
type: identityType, |
|||
installationId, |
|||
environment, |
|||
} |
|||
} else if (identityType === IdentityType.TENANT) { |
|||
const installationId = await getInstallationId() |
|||
const tenantId = await getEventTenantId(context.getTenantId()) |
|||
const hosting = getHostingFromEnv() |
|||
|
|||
return { |
|||
id: formatDistinctId(tenantId, identityType), |
|||
type: identityType, |
|||
hosting, |
|||
installationId, |
|||
tenantId, |
|||
environment, |
|||
} |
|||
} else if (identityType === IdentityType.USER) { |
|||
const userContext = identityContext as UserContext |
|||
const tenantId = await getEventTenantId(context.getTenantId()) |
|||
const installationId = await getInstallationId() |
|||
|
|||
const account = userContext.account |
|||
let hosting |
|||
if (account) { |
|||
hosting = account.hosting |
|||
} else { |
|||
hosting = getHostingFromEnv() |
|||
} |
|||
|
|||
return { |
|||
id: userContext._id, |
|||
type: identityType, |
|||
hosting, |
|||
installationId, |
|||
tenantId, |
|||
environment, |
|||
} |
|||
} else { |
|||
throw new Error("Unknown identity type") |
|||
} |
|||
} |
|||
|
|||
export const identifyInstallationGroup = async ( |
|||
installId: string, |
|||
timestamp?: string | number |
|||
): Promise<void> => { |
|||
const id = installId |
|||
const type = IdentityType.INSTALLATION |
|||
const hosting = getHostingFromEnv() |
|||
const version = pkg.version |
|||
const environment = getDeploymentEnvironment() |
|||
|
|||
const group: InstallationGroup = { |
|||
id, |
|||
type, |
|||
hosting, |
|||
version, |
|||
environment, |
|||
} |
|||
|
|||
await identifyGroup(group, timestamp) |
|||
// need to create a normal identity for the group to be able to query it globally
|
|||
// match the posthog syntax to link this identity to the empty auto generated one
|
|||
await identify({ ...group, id: `$${type}_${id}` }, timestamp) |
|||
} |
|||
|
|||
export const identifyTenantGroup = async ( |
|||
tenantId: string, |
|||
account: Account | undefined, |
|||
timestamp?: string | number |
|||
): Promise<void> => { |
|||
const id = await getEventTenantId(tenantId) |
|||
const type = IdentityType.TENANT |
|||
const installationId = await getInstallationId() |
|||
const environment = getDeploymentEnvironment() |
|||
|
|||
let hosting: Hosting |
|||
let profession: string | undefined |
|||
let companySize: string | undefined |
|||
|
|||
if (account) { |
|||
profession = account.profession |
|||
companySize = account.size |
|||
hosting = account.hosting |
|||
} else { |
|||
hosting = getHostingFromEnv() |
|||
} |
|||
|
|||
const group: TenantGroup = { |
|||
id, |
|||
type, |
|||
hosting, |
|||
environment, |
|||
installationId, |
|||
profession, |
|||
companySize, |
|||
} |
|||
|
|||
await identifyGroup(group, timestamp) |
|||
// need to create a normal identity for the group to be able to query it globally
|
|||
// match the posthog syntax to link this identity to the auto generated one
|
|||
await identify({ ...group, id: `$${type}_${id}` }, timestamp) |
|||
} |
|||
|
|||
export const identifyUser = async ( |
|||
user: User, |
|||
account: CloudAccount | undefined, |
|||
timestamp?: string | number |
|||
) => { |
|||
const id = user._id as string |
|||
const tenantId = await getEventTenantId(user.tenantId) |
|||
const type = IdentityType.USER |
|||
let builder = user.builder?.global || false |
|||
let admin = user.admin?.global || false |
|||
let providerType = user.providerType |
|||
const accountHolder = account?.budibaseUserId === user._id || false |
|||
const verified = |
|||
account && account?.budibaseUserId === user._id ? account.verified : false |
|||
const installationId = await getInstallationId() |
|||
const hosting = account ? account.hosting : getHostingFromEnv() |
|||
const environment = getDeploymentEnvironment() |
|||
|
|||
const identity: UserIdentity = { |
|||
id, |
|||
type, |
|||
hosting, |
|||
installationId, |
|||
tenantId, |
|||
verified, |
|||
accountHolder, |
|||
providerType, |
|||
builder, |
|||
admin, |
|||
environment, |
|||
} |
|||
|
|||
await identify(identity, timestamp) |
|||
} |
|||
|
|||
export const identifyAccount = async (account: Account) => { |
|||
let id = account.accountId |
|||
const tenantId = account.tenantId |
|||
let type = IdentityType.USER |
|||
let providerType = isSSOAccount(account) ? account.providerType : undefined |
|||
const verified = account.verified |
|||
const accountHolder = true |
|||
const hosting = account.hosting |
|||
const installationId = await getInstallationId() |
|||
const environment = getDeploymentEnvironment() |
|||
|
|||
if (isCloudAccount(account)) { |
|||
if (account.budibaseUserId) { |
|||
// use the budibase user as the id if set
|
|||
id = account.budibaseUserId |
|||
} |
|||
} |
|||
|
|||
const identity: UserIdentity = { |
|||
id, |
|||
type, |
|||
hosting, |
|||
installationId, |
|||
tenantId, |
|||
providerType, |
|||
verified, |
|||
accountHolder, |
|||
environment, |
|||
} |
|||
|
|||
await identify(identity) |
|||
} |
|||
|
|||
export const identify = async ( |
|||
identity: Identity, |
|||
timestamp?: string | number |
|||
) => { |
|||
await processors.identify(identity, timestamp) |
|||
} |
|||
|
|||
export const identifyGroup = async ( |
|||
group: Group, |
|||
timestamp?: string | number |
|||
) => { |
|||
await processors.identifyGroup(group, timestamp) |
|||
} |
|||
|
|||
const getDeploymentEnvironment = () => { |
|||
if (env.isDev()) { |
|||
return "development" |
|||
} else { |
|||
return env.DEPLOYMENT_ENVIRONMENT |
|||
} |
|||
} |
|||
|
|||
const getHostingFromEnv = () => { |
|||
return env.SELF_HOSTED ? Hosting.SELF : Hosting.CLOUD |
|||
} |
|||
|
|||
export const getInstallationId = async () => { |
|||
if (isAccountPortal()) { |
|||
return "account-portal" |
|||
} |
|||
const install = await installation.getInstall() |
|||
return install.installId |
|||
} |
|||
|
|||
const getEventTenantId = async (tenantId: string): Promise<string> => { |
|||
if (env.SELF_HOSTED) { |
|||
return getUniqueTenantId(tenantId) |
|||
} else { |
|||
// tenant id's in the cloud are already unique
|
|||
return tenantId |
|||
} |
|||
} |
|||
|
|||
const getUniqueTenantId = async (tenantId: string): Promise<string> => { |
|||
// make sure this tenantId always matches the tenantId in context
|
|||
return context.doInTenant(tenantId, () => { |
|||
return withCache(CacheKeys.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { |
|||
const db = context.getGlobalDB() |
|||
const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { |
|||
type: Configs.SETTINGS, |
|||
}) |
|||
|
|||
let uniqueTenantId: string |
|||
if (config.config.uniqueTenantId) { |
|||
return config.config.uniqueTenantId |
|||
} else { |
|||
uniqueTenantId = `${hashing.newid()}_${tenantId}` |
|||
config.config.uniqueTenantId = uniqueTenantId |
|||
await db.put(config) |
|||
return uniqueTenantId |
|||
} |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
const isAccountPortal = () => { |
|||
return env.SERVICE === "account-portal" |
|||
} |
|||
|
|||
const formatDistinctId = (id: string, type: IdentityType) => { |
|||
if (type === IdentityType.INSTALLATION || type === IdentityType.TENANT) { |
|||
return `$${type}_${id}` |
|||
} else { |
|||
return id |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
export * from "./publishers" |
|||
export * as processors from "./processors" |
|||
export * as analytics from "./analytics" |
|||
export * as identification from "./identification" |
|||
export * as backfillCache from "./backfill" |
|||
|
|||
import { processors } from "./processors" |
|||
|
|||
export const shutdown = () => { |
|||
processors.shutdown() |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
import { Event, Identity, Group, IdentityType } from "@budibase/types" |
|||
import { EventProcessor } from "./types" |
|||
import env from "../../environment" |
|||
import * as analytics from "../analytics" |
|||
import PosthogProcessor from "./PosthogProcessor" |
|||
|
|||
/** |
|||
* Events that are always captured. |
|||
*/ |
|||
const EVENT_WHITELIST = [ |
|||
Event.INSTALLATION_VERSION_UPGRADED, |
|||
Event.INSTALLATION_VERSION_DOWNGRADED, |
|||
] |
|||
const IDENTITY_WHITELIST = [IdentityType.INSTALLATION, IdentityType.TENANT] |
|||
|
|||
export default class AnalyticsProcessor implements EventProcessor { |
|||
posthog: PosthogProcessor | undefined |
|||
|
|||
constructor() { |
|||
if (env.POSTHOG_TOKEN && !env.isTest()) { |
|||
this.posthog = new PosthogProcessor(env.POSTHOG_TOKEN) |
|||
} |
|||
} |
|||
|
|||
async processEvent( |
|||
event: Event, |
|||
identity: Identity, |
|||
properties: any, |
|||
timestamp?: string | number |
|||
): Promise<void> { |
|||
if (!EVENT_WHITELIST.includes(event) && !(await analytics.enabled())) { |
|||
return |
|||
} |
|||
if (this.posthog) { |
|||
this.posthog.processEvent(event, identity, properties, timestamp) |
|||
} |
|||
} |
|||
|
|||
async identify(identity: Identity, timestamp?: string | number) { |
|||
// Group indentifications (tenant and installation) always on
|
|||
if ( |
|||
!IDENTITY_WHITELIST.includes(identity.type) && |
|||
!(await analytics.enabled()) |
|||
) { |
|||
return |
|||
} |
|||
if (this.posthog) { |
|||
this.posthog.identify(identity, timestamp) |
|||
} |
|||
} |
|||
|
|||
async identifyGroup(group: Group, timestamp?: string | number) { |
|||
// Group indentifications (tenant and installation) always on
|
|||
if (this.posthog) { |
|||
this.posthog.identifyGroup(group, timestamp) |
|||
} |
|||
} |
|||
|
|||
shutdown() { |
|||
if (this.posthog) { |
|||
this.posthog.shutdown() |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
import { Event, Identity, Group } from "@budibase/types" |
|||
import { EventProcessor } from "./types" |
|||
import env from "../../environment" |
|||
|
|||
const getTimestampString = (timestamp?: string | number) => { |
|||
let timestampString = "" |
|||
if (timestamp) { |
|||
timestampString = `[timestamp=${new Date(timestamp).toISOString()}]` |
|||
} |
|||
return timestampString |
|||
} |
|||
|
|||
const skipLogging = env.SELF_HOSTED && !env.isDev() |
|||
|
|||
export default class LoggingProcessor implements EventProcessor { |
|||
async processEvent( |
|||
event: Event, |
|||
identity: Identity, |
|||
properties: any, |
|||
timestamp?: string |
|||
): Promise<void> { |
|||
if (skipLogging) { |
|||
return |
|||
} |
|||
let timestampString = getTimestampString(timestamp) |
|||
console.log( |
|||
`[audit] [tenant=${identity.tenantId}] [identityType=${identity.type}] [identity=${identity.id}] ${timestampString} ${event} ` |
|||
) |
|||
} |
|||
|
|||
async identify(identity: Identity, timestamp?: string | number) { |
|||
if (skipLogging) { |
|||
return |
|||
} |
|||
let timestampString = getTimestampString(timestamp) |
|||
console.log( |
|||
`[audit] [${JSON.stringify(identity)}] ${timestampString} identified` |
|||
) |
|||
} |
|||
|
|||
async identifyGroup(group: Group, timestamp?: string | number) { |
|||
if (skipLogging) { |
|||
return |
|||
} |
|||
let timestampString = getTimestampString(timestamp) |
|||
console.log( |
|||
`[audit] [${JSON.stringify(group)}] ${timestampString} group identified` |
|||
) |
|||
} |
|||
|
|||
shutdown(): void { |
|||
// no-op
|
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
import PostHog from "posthog-node" |
|||
import { Event, Identity, Group, BaseEvent } from "@budibase/types" |
|||
import { EventProcessor } from "./types" |
|||
import env from "../../environment" |
|||
import context from "../../context" |
|||
const pkg = require("../../../package.json") |
|||
|
|||
export default class PosthogProcessor implements EventProcessor { |
|||
posthog: PostHog |
|||
|
|||
constructor(token: string | undefined) { |
|||
if (!token) { |
|||
throw new Error("Posthog token is not defined") |
|||
} |
|||
this.posthog = new PostHog(token) |
|||
} |
|||
|
|||
async processEvent( |
|||
event: Event, |
|||
identity: Identity, |
|||
properties: BaseEvent, |
|||
timestamp?: string | number |
|||
): Promise<void> { |
|||
properties.version = pkg.version |
|||
properties.service = env.SERVICE |
|||
properties.environment = identity.environment |
|||
properties.hosting = identity.hosting |
|||
|
|||
const appId = context.getAppId() |
|||
if (appId) { |
|||
properties.appId = appId |
|||
} |
|||
|
|||
const payload: any = { distinctId: identity.id, event, properties } |
|||
|
|||
if (timestamp) { |
|||
payload.timestamp = new Date(timestamp) |
|||
} |
|||
|
|||
// add groups to the event
|
|||
if (identity.installationId || identity.tenantId) { |
|||
payload.groups = {} |
|||
if (identity.installationId) { |
|||
payload.groups.installation = identity.installationId |
|||
payload.properties.installationId = identity.installationId |
|||
} |
|||
if (identity.tenantId) { |
|||
payload.groups.tenant = identity.tenantId |
|||
payload.properties.tenantId = identity.tenantId |
|||
} |
|||
} |
|||
|
|||
this.posthog.capture(payload) |
|||
} |
|||
|
|||
async identify(identity: Identity, timestamp?: string | number) { |
|||
const payload: any = { distinctId: identity.id, properties: identity } |
|||
if (timestamp) { |
|||
payload.timestamp = new Date(timestamp) |
|||
} |
|||
this.posthog.identify(payload) |
|||
} |
|||
|
|||
async identifyGroup(group: Group, timestamp?: string | number) { |
|||
const payload: any = { |
|||
distinctId: group.id, |
|||
groupType: group.type, |
|||
groupKey: group.id, |
|||
properties: group, |
|||
} |
|||
|
|||
if (timestamp) { |
|||
payload.timestamp = new Date(timestamp) |
|||
} |
|||
this.posthog.groupIdentify(payload) |
|||
} |
|||
|
|||
shutdown() { |
|||
this.posthog.shutdown() |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
import { Event, Identity, Group } from "@budibase/types" |
|||
import { EventProcessor } from "./types" |
|||
|
|||
export default class Processor implements EventProcessor { |
|||
initialised: boolean = false |
|||
processors: EventProcessor[] = [] |
|||
|
|||
constructor(processors: EventProcessor[]) { |
|||
this.processors = processors |
|||
} |
|||
|
|||
async processEvent( |
|||
event: Event, |
|||
identity: Identity, |
|||
properties: any, |
|||
timestamp?: string | number |
|||
): Promise<void> { |
|||
for (const eventProcessor of this.processors) { |
|||
await eventProcessor.processEvent(event, identity, properties, timestamp) |
|||
} |
|||
} |
|||
|
|||
async identify( |
|||
identity: Identity, |
|||
timestamp?: string | number |
|||
): Promise<void> { |
|||
for (const eventProcessor of this.processors) { |
|||
await eventProcessor.identify(identity, timestamp) |
|||
} |
|||
} |
|||
|
|||
async identifyGroup( |
|||
identity: Group, |
|||
timestamp?: string | number |
|||
): Promise<void> { |
|||
for (const eventProcessor of this.processors) { |
|||
await eventProcessor.identifyGroup(identity, timestamp) |
|||
} |
|||
} |
|||
|
|||
shutdown() { |
|||
for (const eventProcessor of this.processors) { |
|||
eventProcessor.shutdown() |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import AnalyticsProcessor from "./AnalyticsProcessor" |
|||
import LoggingProcessor from "./LoggingProcessor" |
|||
import Processors from "./Processors" |
|||
|
|||
export const analyticsProcessor = new AnalyticsProcessor() |
|||
const loggingProcessor = new LoggingProcessor() |
|||
|
|||
export const processors = new Processors([analyticsProcessor, loggingProcessor]) |
|||
@ -0,0 +1,18 @@ |
|||
import { Event, Identity, Group } from "@budibase/types" |
|||
|
|||
export enum EventProcessorType { |
|||
POSTHOG = "posthog", |
|||
LOGGING = "logging", |
|||
} |
|||
|
|||
export interface EventProcessor { |
|||
processEvent( |
|||
event: Event, |
|||
identity: Identity, |
|||
properties: any, |
|||
timestamp?: string | number |
|||
): Promise<void> |
|||
identify(identity: Identity, timestamp?: string | number): Promise<void> |
|||
identifyGroup(group: Group, timestamp?: string | number): Promise<void> |
|||
shutdown(): void |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
import { publishEvent } from "../events" |
|||
import { |
|||
Event, |
|||
Account, |
|||
AccountCreatedEvent, |
|||
AccountDeletedEvent, |
|||
AccountVerifiedEvent, |
|||
} from "@budibase/types" |
|||
|
|||
export async function created(account: Account) { |
|||
const properties: AccountCreatedEvent = { |
|||
tenantId: account.tenantId, |
|||
} |
|||
await publishEvent(Event.ACCOUNT_CREATED, properties) |
|||
} |
|||
|
|||
export async function deleted(account: Account) { |
|||
const properties: AccountDeletedEvent = { |
|||
tenantId: account.tenantId, |
|||
} |
|||
await publishEvent(Event.ACCOUNT_DELETED, properties) |
|||
} |
|||
|
|||
export async function verified(account: Account) { |
|||
const properties: AccountVerifiedEvent = { |
|||
tenantId: account.tenantId, |
|||
} |
|||
await publishEvent(Event.ACCOUNT_VERIFIED, properties) |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
import { publishEvent } from "../events" |
|||
import { |
|||
Event, |
|||
App, |
|||
AppCreatedEvent, |
|||
AppUpdatedEvent, |
|||
AppDeletedEvent, |
|||
AppPublishedEvent, |
|||
AppUnpublishedEvent, |
|||
AppFileImportedEvent, |
|||
AppTemplateImportedEvent, |
|||
AppVersionUpdatedEvent, |
|||
AppVersionRevertedEvent, |
|||
AppRevertedEvent, |
|||
AppExportedEvent, |
|||
} from "@budibase/types" |
|||
|
|||
export const created = async (app: App, timestamp?: string | number) => { |
|||
const properties: AppCreatedEvent = { |
|||
appId: app.appId, |
|||
version: app.version, |
|||
} |
|||
await publishEvent(Event.APP_CREATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function updated(app: App) { |
|||
const properties: AppUpdatedEvent = { |
|||
appId: app.appId, |
|||
version: app.version, |
|||
} |
|||
await publishEvent(Event.APP_UPDATED, properties) |
|||
} |
|||
|
|||
export async function deleted(app: App) { |
|||
const properties: AppDeletedEvent = { |
|||
appId: app.appId, |
|||
} |
|||
await publishEvent(Event.APP_DELETED, properties) |
|||
} |
|||
|
|||
export async function published(app: App, timestamp?: string | number) { |
|||
const properties: AppPublishedEvent = { |
|||
appId: app.appId, |
|||
} |
|||
await publishEvent(Event.APP_PUBLISHED, properties, timestamp) |
|||
} |
|||
|
|||
export async function unpublished(app: App) { |
|||
const properties: AppUnpublishedEvent = { |
|||
appId: app.appId, |
|||
} |
|||
await publishEvent(Event.APP_UNPUBLISHED, properties) |
|||
} |
|||
|
|||
export async function fileImported(app: App) { |
|||
const properties: AppFileImportedEvent = { |
|||
appId: app.appId, |
|||
} |
|||
await publishEvent(Event.APP_FILE_IMPORTED, properties) |
|||
} |
|||
|
|||
export async function templateImported(app: App, templateKey: string) { |
|||
const properties: AppTemplateImportedEvent = { |
|||
appId: app.appId, |
|||
templateKey, |
|||
} |
|||
await publishEvent(Event.APP_TEMPLATE_IMPORTED, properties) |
|||
} |
|||
|
|||
export async function versionUpdated( |
|||
app: App, |
|||
currentVersion: string, |
|||
updatedToVersion: string |
|||
) { |
|||
const properties: AppVersionUpdatedEvent = { |
|||
appId: app.appId, |
|||
currentVersion, |
|||
updatedToVersion, |
|||
} |
|||
await publishEvent(Event.APP_VERSION_UPDATED, properties) |
|||
} |
|||
|
|||
export async function versionReverted( |
|||
app: App, |
|||
currentVersion: string, |
|||
revertedToVersion: string |
|||
) { |
|||
const properties: AppVersionRevertedEvent = { |
|||
appId: app.appId, |
|||
currentVersion, |
|||
revertedToVersion, |
|||
} |
|||
await publishEvent(Event.APP_VERSION_REVERTED, properties) |
|||
} |
|||
|
|||
export async function reverted(app: App) { |
|||
const properties: AppRevertedEvent = { |
|||
appId: app.appId, |
|||
} |
|||
await publishEvent(Event.APP_REVERTED, properties) |
|||
} |
|||
|
|||
export async function exported(app: App) { |
|||
const properties: AppExportedEvent = { |
|||
appId: app.appId, |
|||
} |
|||
await publishEvent(Event.APP_EXPORTED, properties) |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
import { publishEvent } from "../events" |
|||
import { |
|||
Event, |
|||
LoginEvent, |
|||
LoginSource, |
|||
LogoutEvent, |
|||
SSOActivatedEvent, |
|||
SSOCreatedEvent, |
|||
SSODeactivatedEvent, |
|||
SSOType, |
|||
SSOUpdatedEvent, |
|||
} from "@budibase/types" |
|||
import { identification } from ".." |
|||
|
|||
export async function login(source: LoginSource) { |
|||
const identity = await identification.getCurrentIdentity() |
|||
const properties: LoginEvent = { |
|||
userId: identity.id, |
|||
source, |
|||
} |
|||
await publishEvent(Event.AUTH_LOGIN, properties) |
|||
} |
|||
|
|||
export async function logout() { |
|||
const identity = await identification.getCurrentIdentity() |
|||
const properties: LogoutEvent = { |
|||
userId: identity.id, |
|||
} |
|||
await publishEvent(Event.AUTH_LOGOUT, properties) |
|||
} |
|||
|
|||
export async function SSOCreated(type: SSOType, timestamp?: string | number) { |
|||
const properties: SSOCreatedEvent = { |
|||
type, |
|||
} |
|||
await publishEvent(Event.AUTH_SSO_CREATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function SSOUpdated(type: SSOType) { |
|||
const properties: SSOUpdatedEvent = { |
|||
type, |
|||
} |
|||
await publishEvent(Event.AUTH_SSO_UPDATED, properties) |
|||
} |
|||
|
|||
export async function SSOActivated(type: SSOType, timestamp?: string | number) { |
|||
const properties: SSOActivatedEvent = { |
|||
type, |
|||
} |
|||
await publishEvent(Event.AUTH_SSO_ACTIVATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function SSODeactivated(type: SSOType) { |
|||
const properties: SSODeactivatedEvent = { |
|||
type, |
|||
} |
|||
await publishEvent(Event.AUTH_SSO_DEACTIVATED, properties) |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
import { publishEvent } from "../events" |
|||
import { |
|||
Automation, |
|||
Event, |
|||
AutomationStep, |
|||
AutomationCreatedEvent, |
|||
AutomationDeletedEvent, |
|||
AutomationTestedEvent, |
|||
AutomationStepCreatedEvent, |
|||
AutomationStepDeletedEvent, |
|||
AutomationTriggerUpdatedEvent, |
|||
AutomationsRunEvent, |
|||
} from "@budibase/types" |
|||
|
|||
export async function created( |
|||
automation: Automation, |
|||
timestamp?: string | number |
|||
) { |
|||
const properties: AutomationCreatedEvent = { |
|||
appId: automation.appId, |
|||
automationId: automation._id as string, |
|||
triggerId: automation.definition?.trigger?.id, |
|||
triggerType: automation.definition?.trigger?.stepId, |
|||
} |
|||
await publishEvent(Event.AUTOMATION_CREATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function triggerUpdated(automation: Automation) { |
|||
const properties: AutomationTriggerUpdatedEvent = { |
|||
appId: automation.appId, |
|||
automationId: automation._id as string, |
|||
triggerId: automation.definition?.trigger?.id, |
|||
triggerType: automation.definition?.trigger?.stepId, |
|||
} |
|||
await publishEvent(Event.AUTOMATION_TRIGGER_UPDATED, properties) |
|||
} |
|||
|
|||
export async function deleted(automation: Automation) { |
|||
const properties: AutomationDeletedEvent = { |
|||
appId: automation.appId, |
|||
automationId: automation._id as string, |
|||
triggerId: automation.definition?.trigger?.id, |
|||
triggerType: automation.definition?.trigger?.stepId, |
|||
} |
|||
await publishEvent(Event.AUTOMATION_DELETED, properties) |
|||
} |
|||
|
|||
export async function tested(automation: Automation) { |
|||
const properties: AutomationTestedEvent = { |
|||
appId: automation.appId, |
|||
automationId: automation._id as string, |
|||
triggerId: automation.definition?.trigger?.id, |
|||
triggerType: automation.definition?.trigger?.stepId, |
|||
} |
|||
await publishEvent(Event.AUTOMATION_TESTED, properties) |
|||
} |
|||
|
|||
export const run = async (count: number, timestamp?: string | number) => { |
|||
const properties: AutomationsRunEvent = { |
|||
count, |
|||
} |
|||
await publishEvent(Event.AUTOMATIONS_RUN, properties, timestamp) |
|||
} |
|||
|
|||
export async function stepCreated( |
|||
automation: Automation, |
|||
step: AutomationStep, |
|||
timestamp?: string | number |
|||
) { |
|||
const properties: AutomationStepCreatedEvent = { |
|||
appId: automation.appId, |
|||
automationId: automation._id as string, |
|||
triggerId: automation.definition?.trigger?.id, |
|||
triggerType: automation.definition?.trigger?.stepId, |
|||
stepId: step.id, |
|||
stepType: step.stepId, |
|||
} |
|||
await publishEvent(Event.AUTOMATION_STEP_CREATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function stepDeleted( |
|||
automation: Automation, |
|||
step: AutomationStep |
|||
) { |
|||
const properties: AutomationStepDeletedEvent = { |
|||
appId: automation.appId, |
|||
automationId: automation._id as string, |
|||
triggerId: automation.definition?.trigger?.id, |
|||
triggerType: automation.definition?.trigger?.stepId, |
|||
stepId: step.id, |
|||
stepType: step.stepId, |
|||
} |
|||
await publishEvent(Event.AUTOMATION_STEP_DELETED, properties) |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
import { publishEvent } from "../events" |
|||
import { |
|||
Event, |
|||
AppBackfillSucceededEvent, |
|||
AppBackfillFailedEvent, |
|||
TenantBackfillSucceededEvent, |
|||
TenantBackfillFailedEvent, |
|||
InstallationBackfillSucceededEvent, |
|||
InstallationBackfillFailedEvent, |
|||
} from "@budibase/types" |
|||
const env = require("../../environment") |
|||
|
|||
const shouldSkip = !env.SELF_HOSTED && !env.isDev() |
|||
|
|||
export async function appSucceeded(properties: AppBackfillSucceededEvent) { |
|||
if (shouldSkip) { |
|||
return |
|||
} |
|||
await publishEvent(Event.APP_BACKFILL_SUCCEEDED, properties) |
|||
} |
|||
|
|||
export async function appFailed(error: any) { |
|||
if (shouldSkip) { |
|||
return |
|||
} |
|||
const properties: AppBackfillFailedEvent = { |
|||
error: JSON.stringify(error, Object.getOwnPropertyNames(error)), |
|||
} |
|||
await publishEvent(Event.APP_BACKFILL_FAILED, properties) |
|||
} |
|||
|
|||
export async function tenantSucceeded( |
|||
properties: TenantBackfillSucceededEvent |
|||
) { |
|||
if (shouldSkip) { |
|||
return |
|||
} |
|||
await publishEvent(Event.TENANT_BACKFILL_SUCCEEDED, properties) |
|||
} |
|||
|
|||
export async function tenantFailed(error: any) { |
|||
if (shouldSkip) { |
|||
return |
|||
} |
|||
const properties: TenantBackfillFailedEvent = { |
|||
error: JSON.stringify(error, Object.getOwnPropertyNames(error)), |
|||
} |
|||
await publishEvent(Event.TENANT_BACKFILL_FAILED, properties) |
|||
} |
|||
|
|||
export async function installationSucceeded() { |
|||
if (shouldSkip) { |
|||
return |
|||
} |
|||
const properties: InstallationBackfillSucceededEvent = {} |
|||
await publishEvent(Event.INSTALLATION_BACKFILL_SUCCEEDED, properties) |
|||
} |
|||
|
|||
export async function installationFailed(error: any) { |
|||
if (shouldSkip) { |
|||
return |
|||
} |
|||
const properties: InstallationBackfillFailedEvent = { |
|||
error: JSON.stringify(error, Object.getOwnPropertyNames(error)), |
|||
} |
|||
await publishEvent(Event.INSTALLATION_BACKFILL_FAILED, properties) |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
import { publishEvent } from "../events" |
|||
import { |
|||
Event, |
|||
Datasource, |
|||
DatasourceCreatedEvent, |
|||
DatasourceUpdatedEvent, |
|||
DatasourceDeletedEvent, |
|||
} from "@budibase/types" |
|||
|
|||
export async function created( |
|||
datasource: Datasource, |
|||
timestamp?: string | number |
|||
) { |
|||
const properties: DatasourceCreatedEvent = { |
|||
datasourceId: datasource._id as string, |
|||
source: datasource.source, |
|||
} |
|||
await publishEvent(Event.DATASOURCE_CREATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function updated(datasource: Datasource) { |
|||
const properties: DatasourceUpdatedEvent = { |
|||
datasourceId: datasource._id as string, |
|||
source: datasource.source, |
|||
} |
|||
await publishEvent(Event.DATASOURCE_UPDATED, properties) |
|||
} |
|||
|
|||
export async function deleted(datasource: Datasource) { |
|||
const properties: DatasourceDeletedEvent = { |
|||
datasourceId: datasource._id as string, |
|||
source: datasource.source, |
|||
} |
|||
await publishEvent(Event.DATASOURCE_DELETED, properties) |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
import { publishEvent } from "../events" |
|||
import { Event, SMTPCreatedEvent, SMTPUpdatedEvent } from "@budibase/types" |
|||
|
|||
export async function SMTPCreated(timestamp?: string | number) { |
|||
const properties: SMTPCreatedEvent = {} |
|||
await publishEvent(Event.EMAIL_SMTP_CREATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function SMTPUpdated() { |
|||
const properties: SMTPUpdatedEvent = {} |
|||
await publishEvent(Event.EMAIL_SMTP_UPDATED, properties) |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
export * as account from "./account" |
|||
export * as app from "./app" |
|||
export * as auth from "./auth" |
|||
export * as automation from "./automation" |
|||
export * as datasource from "./datasource" |
|||
export * as email from "./email" |
|||
export * as license from "./license" |
|||
export * as layout from "./layout" |
|||
export * as org from "./org" |
|||
export * as query from "./query" |
|||
export * as role from "./role" |
|||
export * as screen from "./screen" |
|||
export * as rows from "./rows" |
|||
export * as table from "./table" |
|||
export * as serve from "./serve" |
|||
export * as user from "./user" |
|||
export * as view from "./view" |
|||
export * as installation from "./installation" |
|||
export * as backfill from "./backfill" |
|||
@ -0,0 +1,31 @@ |
|||
import { publishEvent } from "../events" |
|||
import { Event, VersionCheckedEvent, VersionChangeEvent } from "@budibase/types" |
|||
|
|||
export async function versionChecked(version: string) { |
|||
const properties: VersionCheckedEvent = { |
|||
currentVersion: version, |
|||
} |
|||
await publishEvent(Event.INSTALLATION_VERSION_CHECKED, properties) |
|||
} |
|||
|
|||
export async function upgraded(from: string, to: string) { |
|||
const properties: VersionChangeEvent = { |
|||
from, |
|||
to, |
|||
} |
|||
|
|||
await publishEvent(Event.INSTALLATION_VERSION_UPGRADED, properties) |
|||
} |
|||
|
|||
export async function downgraded(from: string, to: string) { |
|||
const properties: VersionChangeEvent = { |
|||
from, |
|||
to, |
|||
} |
|||
await publishEvent(Event.INSTALLATION_VERSION_DOWNGRADED, properties) |
|||
} |
|||
|
|||
export async function firstStartup() { |
|||
const properties = {} |
|||
await publishEvent(Event.INSTALLATION_FIRST_STARTUP, properties) |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import { publishEvent } from "../events" |
|||
import { |
|||
Event, |
|||
Layout, |
|||
LayoutCreatedEvent, |
|||
LayoutDeletedEvent, |
|||
} from "@budibase/types" |
|||
|
|||
export async function created(layout: Layout, timestamp?: string | number) { |
|||
const properties: LayoutCreatedEvent = { |
|||
layoutId: layout._id as string, |
|||
} |
|||
await publishEvent(Event.LAYOUT_CREATED, properties, timestamp) |
|||
} |
|||
|
|||
export async function deleted(layoutId: string) { |
|||
const properties: LayoutDeletedEvent = { |
|||
layoutId, |
|||
} |
|||
await publishEvent(Event.LAYOUT_DELETED, properties) |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue