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 |
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 = { |
module.exports = { |
||||
user: require("./src/cache/user"), |
user: require("./src/cache/user"), |
||||
app: require("./src/cache/appMetadata"), |
app: require("./src/cache/appMetadata"), |
||||
|
...generic, |
||||
} |
} |
||||
|
|||||
@ -0,0 +1 @@ |
|||||
|
module.exports = require("./src/logging") |
||||
@ -1,45 +1,80 @@ |
|||||
{ |
{ |
||||
"name": "@budibase/backend-core", |
"name": "@budibase/backend-core", |
||||
"version": "1.0.98-alpha.1", |
"version": "1.0.206", |
||||
"description": "Budibase backend core libraries used in server and worker", |
"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", |
"author": "Budibase", |
||||
"license": "GPL-3.0", |
"license": "GPL-3.0", |
||||
"scripts": { |
"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": "jest", |
||||
"test:watch": "jest --watchAll" |
"test:watch": "jest --watchAll" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@techpass/passport-openidconnect": "^0.3.0", |
"@techpass/passport-openidconnect": "0.3.2", |
||||
"aws-sdk": "^2.901.0", |
"aws-sdk": "2.1030.0", |
||||
"bcryptjs": "^2.4.3", |
"bcrypt": "5.0.1", |
||||
"cls-hooked": "^4.2.2", |
"dotenv": "16.0.1", |
||||
"ioredis": "^4.27.1", |
"emitter-listener": "1.1.2", |
||||
"jsonwebtoken": "^8.5.1", |
"ioredis": "4.28.0", |
||||
"koa-passport": "^4.1.4", |
"jsonwebtoken": "8.5.1", |
||||
"lodash": "^4.17.21", |
"koa-passport": "4.1.4", |
||||
"lodash.isarguments": "^3.1.0", |
"lodash": "4.17.21", |
||||
"node-fetch": "^2.6.1", |
"lodash.isarguments": "3.1.0", |
||||
"passport-google-auth": "^1.0.2", |
"node-fetch": "2.6.7", |
||||
"passport-google-oauth": "^2.0.0", |
"passport-google-auth": "1.0.2", |
||||
"passport-jwt": "^4.0.0", |
"passport-google-oauth": "2.0.0", |
||||
"passport-local": "^1.0.0", |
"passport-jwt": "4.0.0", |
||||
"sanitize-s3-objectkey": "^0.0.1", |
"passport-local": "1.0.0", |
||||
"tar-fs": "^2.1.1", |
"posthog-node": "1.3.0", |
||||
"uuid": "^8.3.2", |
"pouchdb": "7.3.0", |
||||
"zlib": "^1.0.5" |
"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": { |
"jest": { |
||||
|
"preset": "ts-jest", |
||||
|
"testEnvironment": "node", |
||||
|
"moduleNameMapper": { |
||||
|
"@budibase/types": "<rootDir>/../types/src" |
||||
|
}, |
||||
"setupFiles": [ |
"setupFiles": [ |
||||
"./scripts/jestSetup.js" |
"./scripts/jestSetup.ts" |
||||
] |
] |
||||
}, |
}, |
||||
"devDependencies": { |
"devDependencies": { |
||||
"ioredis-mock": "^5.5.5", |
"@budibase/types": "^1.0.206", |
||||
"jest": "^26.6.3", |
"@shopify/jest-koa-mocks": "3.1.5", |
||||
"pouchdb": "^7.2.1", |
"@types/jest": "27.5.1", |
||||
"pouchdb-adapter-memory": "^7.2.2", |
"@types/koa": "2.0.52", |
||||
"pouchdb-all-dbs": "^1.0.2" |
"@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" |
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" |
||||
} |
} |
||||
|
|||||
@ -1,4 +1,5 @@ |
|||||
module.exports = { |
module.exports = { |
||||
Client: require("./src/redis"), |
Client: require("./src/redis"), |
||||
utils: require("./src/redis/utils"), |
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 { newid } = require("../hashing") |
||||
|
|
||||
const REQUEST_ID_KEY = "requestId" |
const REQUEST_ID_KEY = "requestId" |
||||
|
const MAIN_CTX = cls.createNamespace("main") |
||||
class FunctionContext { |
|
||||
static getMiddleware(updateCtxFn = null, contextName = "session") { |
function getContextStorage(namespace) { |
||||
const namespace = this.createNamespace(contextName) |
if (namespace && namespace.active) { |
||||
|
let contextData = namespace.active |
||||
return async function (ctx, next) { |
delete contextData.id |
||||
await new Promise( |
delete contextData._ns_name |
||||
namespace.bind(function (resolve, reject) { |
return contextData |
||||
// 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) |
|
||||
}) |
|
||||
) |
|
||||
} |
|
||||
} |
} |
||||
|
return {} |
||||
|
} |
||||
|
|
||||
static run(callback, contextName = "session") { |
class FunctionContext { |
||||
const namespace = this.createNamespace(contextName) |
static run(callback) { |
||||
|
return MAIN_CTX.runAndReturn(async () => { |
||||
return namespace.runAndReturn(callback) |
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") { |
static setOnContext(key, value) { |
||||
const namespace = this.createNamespace(contextName) |
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) |
||||
|
const namespace = cls.getNamespace(namespaceId) |
||||
namespace.set(key, value) |
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) { |
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) { |
if (context) { |
||||
return context[key] |
return context[key] |
||||
} else { |
} else { |
||||
return null |
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 |
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 => { |
let PouchDB |
||||
Pouch = pouch |
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 => { |
// we have to use a callback for this so that we can close
|
||||
return new Pouch(dbName) |
// 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 = () => { |
exports.allDbs = () => { |
||||
return Pouch |
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