diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d1e373003a..2a57d6f388 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -137,7 +137,7 @@ If you wish to delete all the apps created in development and reset the environm ### Backend -For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [Envoy](https://www.envoyproxy.io/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker. +For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [NGINX](https://www.nginx.com/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker. ### Data Storage diff --git a/.gitignore b/.gitignore index 6ba2f61ed7..d98e8e8fce 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,7 @@ typings/ # dotenv environment variables file .env !hosting/.env -hosting/.generated-envoy.dev.yaml +hosting/.generated-nginx.dev.conf # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/charts/budibase/Chart.yaml b/charts/budibase/Chart.yaml index 830c1b9f1d..766657769b 100644 --- a/charts/budibase/Chart.yaml +++ b/charts/budibase/Chart.yaml @@ -12,7 +12,7 @@ sources: - https://budibase.com type: application version: 0.2.6 -appVersion: 1.0.47 +appVersion: 1.0.48 dependencies: - name: couchdb version: 3.3.4 diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index 2e453d1c5b..9ea7df1608 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -25,7 +25,7 @@ spec: app.kubernetes.io/name: budibase-proxy spec: containers: - - image: budibase/proxy + - image: budibase/proxy:k8s imagePullPolicy: Always name: proxy-service ports: diff --git a/hosting/.env b/hosting/.env deleted file mode 120000 index bb1b54ad77..0000000000 --- a/hosting/.env +++ /dev/null @@ -1 +0,0 @@ -hosting.properties \ No newline at end of file diff --git a/hosting/.env b/hosting/.env new file mode 100644 index 0000000000..39df76d01e --- /dev/null +++ b/hosting/.env @@ -0,0 +1,21 @@ +# Use the main port in the builder for your self hosting URL, e.g. localhost:10000 +MAIN_PORT=10000 + +# This section contains all secrets pertaining to the system +# These should be updated +JWT_SECRET=testsecret +MINIO_ACCESS_KEY=budibase +MINIO_SECRET_KEY=budibase +COUCH_DB_PASSWORD=budibase +COUCH_DB_USER=budibase +REDIS_PASSWORD=budibase +INTERNAL_API_KEY=budibase + +# This section contains variables that do not need to be altered under normal circumstances +APP_PORT=4002 +WORKER_PORT=4003 +MINIO_PORT=4004 +COUCH_DB_PORT=4005 +REDIS_PORT=6379 +WATCHTOWER_PORT=6161 +BUDIBASE_ENVIRONMENT=PRODUCTION \ No newline at end of file diff --git a/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot b/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot index e5a883ac81..ffa63ad670 100755 --- a/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot +++ b/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot @@ -3,9 +3,8 @@ # go into the app dir cd /root -# fetch envoy and docker-compose files +# fetch nginx and docker-compose files wget https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml -wget https://raw.githubusercontent.com/Budibase/budibase/master/hosting/envoy.yaml wget https://raw.githubusercontent.com/Budibase/budibase/master/hosting/hosting.properties # Create .env file from hosting.properties using bash and then remove it diff --git a/hosting/docker-compose.dev.yaml b/hosting/docker-compose.dev.yaml index eaced64e06..df403c0a22 100644 --- a/hosting/docker-compose.dev.yaml +++ b/hosting/docker-compose.dev.yaml @@ -22,18 +22,21 @@ services: retries: 3 proxy-service: - container_name: budi-envoy-dev + container_name: budi-nginx-dev restart: always - image: envoyproxy/envoy:v1.16-latest + image: nginx:latest volumes: - - ./.generated-envoy.dev.yaml:/etc/envoy/envoy.yaml + - ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf ports: - "${MAIN_PORT}:10000" depends_on: - minio-service - couchdb-service + extra_hosts: + - "host.docker.internal:host-gateway" couchdb-service: + # platform: linux/amd64 container_name: budi-couchdb-dev restart: always image: ibmcom/couchdb3 diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index c94d1520a1..17ed12a13d 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -80,9 +80,8 @@ services: proxy-service: restart: always - image: envoyproxy/envoy:v1.16-latest - volumes: - - ./envoy.yaml:/etc/envoy/envoy.yaml + container_name: bbproxy + image: budibase/proxy ports: - "${MAIN_PORT}:10000" depends_on: @@ -125,7 +124,7 @@ services: - "${WATCHTOWER_PORT}:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock - command: --debug --http-api-update bbapps bbworker + command: --debug --http-api-update bbapps bbworker bbproxy environment: - WATCHTOWER_HTTP_API=true - WATCHTOWER_HTTP_API_TOKEN=budibase diff --git a/hosting/envoy.dev.yaml.hbs b/hosting/envoy.dev.yaml.hbs deleted file mode 100644 index 59363fab5e..0000000000 --- a/hosting/envoy.dev.yaml.hbs +++ /dev/null @@ -1,149 +0,0 @@ -static_resources: - listeners: - - name: main_listener - address: - socket_address: { address: 0.0.0.0, port_value: 10000 } - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress - codec_type: auto - route_config: - name: local_route - virtual_hosts: - - name: local_services - domains: ["*"] - routes: - # special case to redirect specifically the route path - # to the builder, if this were a prefix then it would break minio - - match: { path: "/" } - redirect: { path_redirect: "/builder/" } - - - match: { prefix: "/db/" } - route: - cluster: couchdb-service - prefix_rewrite: "/" - - - match: { prefix: "/api/system/" } - route: - cluster: worker-dev - - - match: { prefix: "/api/admin/" } - route: - cluster: worker-dev - - - match: { prefix: "/api/global/" } - route: - cluster: worker-dev - - - match: { prefix: "/api/" } - route: - cluster: server-dev - timeout: 120s - - - match: { prefix: "/app_" } - route: - cluster: server-dev - - - match: { prefix: "/app/" } - route: - cluster: server-dev - prefix_rewrite: "/" - - # the below three cases are needed to make sure - # all traffic prefixed for the builder is passed through - # correctly. - - match: { path: "/" } - route: - cluster: builder-dev - - - match: { prefix: "/builder/" } - route: - cluster: builder-dev - - - match: { prefix: "/builder" } - route: - cluster: builder-dev - prefix_rewrite: "/builder/" - - # minio is on the default route because this works - # best, minio + AWS SDK doesn't handle path proxy - - match: { prefix: "/" } - route: - cluster: minio-service - - http_filters: - - name: envoy.filters.http.router - - clusters: - - name: minio-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: minio-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: minio-service - port_value: 9000 - - - name: couchdb-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: couchdb-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: couchdb-service - port_value: 5984 - - - name: server-dev - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: server-dev - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: {{ address }} - port_value: 4001 - - - name: builder-dev - connect_timeout: 15s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: builder-dev - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: {{ address }} - port_value: 3000 - - - name: worker-dev - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: worker-dev - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: {{ address }} - port_value: 4002 diff --git a/hosting/envoy.yaml b/hosting/envoy.yaml deleted file mode 100644 index d9f8384688..0000000000 --- a/hosting/envoy.yaml +++ /dev/null @@ -1,152 +0,0 @@ -static_resources: - listeners: - - name: main_listener - address: - socket_address: { address: 0.0.0.0, port_value: 10000 } - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress - codec_type: auto - route_config: - name: local_route - virtual_hosts: - - name: local_services - domains: ["*"] - routes: - - match: { prefix: "/app/" } - route: - cluster: app-service - prefix_rewrite: "/" - - - match: { path: "/v1/update" } - route: - cluster: watchtower-service - - - match: { prefix: "/builder/" } - route: - cluster: app-service - - - match: { prefix: "/builder" } - route: - cluster: app-service - - - match: { prefix: "/app_" } - route: - cluster: app-service - - # special cases for worker admin (deprecated), global and system API - - match: { prefix: "/api/global/" } - route: - cluster: worker-service - - - match: { prefix: "/api/admin/" } - route: - cluster: worker-service - - - match: { prefix: "/api/system/" } - route: - cluster: worker-service - - - match: { path: "/" } - route: - cluster: app-service - - # special case for when API requests are made, can just forward, not to minio - - match: { prefix: "/api/" } - route: - cluster: app-service - timeout: 120s - - - match: { prefix: "/worker/" } - route: - cluster: worker-service - prefix_rewrite: "/" - - - match: { prefix: "/db/" } - route: - cluster: couchdb-service - prefix_rewrite: "/" - - # minio is on the default route because this works - # best, minio + AWS SDK doesn't handle path proxy - - match: { prefix: "/" } - route: - cluster: minio-service - - http_filters: - - name: envoy.filters.http.router - - clusters: - - name: app-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: app-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: app-service - port_value: 4002 - - - name: minio-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: minio-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: minio-service - port_value: 9000 - - - name: worker-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: worker-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: worker-service - port_value: 4003 - - - name: couchdb-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: couchdb-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: couchdb-service - port_value: 5984 - - - name: watchtower-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: watchtower-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: watchtower-service - port_value: 8080 - diff --git a/hosting/hosting.properties b/hosting/hosting.properties deleted file mode 100644 index c8e2f5c606..0000000000 --- a/hosting/hosting.properties +++ /dev/null @@ -1,21 +0,0 @@ -# Use the main port in the builder for your self hosting URL, e.g. localhost:10000 -MAIN_PORT=10000 - -# This section contains all secrets pertaining to the system -# These should be updated -JWT_SECRET=testsecret -MINIO_ACCESS_KEY=budibase -MINIO_SECRET_KEY=budibase -COUCH_DB_PASSWORD=budibase -COUCH_DB_USER=budibase -REDIS_PASSWORD=budibase -INTERNAL_API_KEY=budibase - -# This section contains variables that do not need to be altered under normal circumstances -APP_PORT=4002 -WORKER_PORT=4003 -MINIO_PORT=4004 -COUCH_DB_PORT=4005 -REDIS_PORT=6379 -WATCHTOWER_PORT=6161 -BUDIBASE_ENVIRONMENT=PRODUCTION diff --git a/hosting/kubernetes/envoy/Dockerfile b/hosting/kubernetes/envoy/Dockerfile deleted file mode 100644 index 96334fa723..0000000000 --- a/hosting/kubernetes/envoy/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM envoyproxy/envoy:v1.16-latest -COPY envoy.yaml /etc/envoy/envoy.yaml -RUN chmod go+r /etc/envoy/envoy.yaml - diff --git a/hosting/kubernetes/envoy/envoy.yaml b/hosting/kubernetes/envoy/envoy.yaml deleted file mode 100644 index bab1f25c02..0000000000 --- a/hosting/kubernetes/envoy/envoy.yaml +++ /dev/null @@ -1,146 +0,0 @@ -static_resources: - listeners: - - name: main_listener - address: - socket_address: { address: 0.0.0.0, port_value: 10000 } - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress - codec_type: auto - route_config: - name: local_route - virtual_hosts: - - name: local_services - domains: ["*"] - routes: - - match: { prefix: "/app/" } - route: - cluster: app-service - prefix_rewrite: "/" - - - match: { prefix: "/builder/" } - route: - cluster: app-service - - - match: { prefix: "/builder" } - route: - cluster: app-service - - - match: { prefix: "/app_" } - route: - cluster: app-service - - # special cases for worker admin (deprecated), global and system API - - match: { prefix: "/api/global/" } - route: - cluster: worker-service - - - match: { prefix: "/api/admin/" } - route: - cluster: worker-service - - - match: { prefix: "/api/system/" } - route: - cluster: worker-service - - - match: { path: "/" } - route: - cluster: app-service - - - match: - safe_regex: - google_re2: {} - regex: "/api/.*/export" - route: - timeout: 0s - cluster: app-service - - - match: { path: "/api/deploy" } - route: - timeout: 60s - cluster: app-service - - # special case for when API requests are made, can just forward, not to minio - - match: { prefix: "/api/" } - route: - cluster: app-service - - - match: { prefix: "/worker/" } - route: - cluster: worker-service - prefix_rewrite: "/" - - - match: { prefix: "/db/" } - route: - cluster: couchdb-service - prefix_rewrite: "/" - - # minio is on the default route because this works - # best, minio + AWS SDK doesn't handle path proxy - - match: { prefix: "/" } - route: - cluster: minio-service - - http_filters: - - name: envoy.filters.http.router - - clusters: - - name: app-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: app-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: app-service.budibase.svc.cluster.local - port_value: 4002 - - - name: minio-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: minio-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: minio-service.budibase.svc.cluster.local - port_value: 9000 - - - name: worker-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: worker-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: worker-service.budibase.svc.cluster.local - port_value: 4001 - - - name: couchdb-service - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: couchdb-service - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: budibase-prod-svc-couchdb - port_value: 5984 - diff --git a/hosting/kubernetes/nginx/Dockerfile b/hosting/kubernetes/nginx/Dockerfile new file mode 100644 index 0000000000..754f9f9be3 --- /dev/null +++ b/hosting/kubernetes/nginx/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:latest +COPY nginx.conf /etc/nginx/nginx.conf diff --git a/hosting/kubernetes/nginx/nginx.conf b/hosting/kubernetes/nginx/nginx.conf new file mode 100644 index 0000000000..2bf512964b --- /dev/null +++ b/hosting/kubernetes/nginx/nginx.conf @@ -0,0 +1,127 @@ +user nginx; +error_log /var/log/nginx/error.log debug; +pid /var/run/nginx.pid; +worker_processes auto; +worker_rlimit_nofile 33282; + +events { + worker_connections 1024; +} + +http { + limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=10r/s; + include /etc/nginx/mime.types; + default_type application/octet-stream; + charset utf-8; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + server_tokens off; + types_hash_max_size 2048; + + # buffering + client_body_buffer_size 1K; + client_header_buffer_size 1k; + client_max_body_size 1k; + 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; + server_name _; + + # Security Headers + add_header X-Frame-Options SAMEORIGIN always; + add_header X-Content-Type-Options nosniff always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me; frame-src 'self'; img-src https: data:; manifest-src 'self'; media-src 'self'; worker-src 'none';" always; + + location /app { + proxy_pass http://app-service:4002; + rewrite ^/app/(.*)$ /$1 break; + } + + location = / { + proxy_pass http://app-service.budibase.svc.cluster.local:4002; + } + + location /builder/ { + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://app-service.budibase.svc.cluster.local:4002; + } + + location ^/(builder|app_) { + proxy_pass http://app-service.budibase.svc.cluster.local:4002; + } + + location ~ ^/api/(system|admin|global)/ { + proxy_pass http://worker-service.budibase.svc.cluster.local:4003; + } + + location /worker/ { + proxy_pass http://worker-service.budibase.svc.cluster.local:4003; + rewrite ^/worker/(.*)$ /$1 break; + } + + location /api/ { + # calls to the API are rate limited with bursting + limit_req zone=ratelimit burst=10 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 Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://app-service.budibase.svc.cluster.local:4002; + } + + location /db/ { + proxy_pass http://budibase-prod-svc-couchdb: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_set_header Host $http_host; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + proxy_pass http://minio-service.budibase.svc.cluster.local: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; + } +} diff --git a/hosting/nginx.dev.conf.hbs b/hosting/nginx.dev.conf.hbs new file mode 100644 index 0000000000..51b55cd49b --- /dev/null +++ b/hosting/nginx.dev.conf.hbs @@ -0,0 +1,91 @@ +user nginx; +error_log /var/log/nginx/error.log debug; +pid /var/run/nginx.pid; +worker_processes auto; +worker_rlimit_nofile 33282; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + 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; + server_name _; + client_max_body_size 1000m; + ignore_invalid_headers off; + proxy_buffering off; + + location /db/ { + proxy_pass http://couchdb-service:5984; + rewrite ^/db/(.*)$ /$1 break; + } + + location ~ ^/api/(system|admin|global)/ { + proxy_pass http://{{ address }}:4002; + } + + location /api/ { + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + proxy_pass http://{{ address }}:4001; + } + + location /app_ { + proxy_pass http://{{ address }}:4001; + } + + location /app/ { + proxy_pass http://{{ address }}:4001; + rewrite ^/app/(.*)$ /$1 break; + } + + location /builder { + proxy_pass http://{{ address }}:3000; + rewrite ^/builder(.*)$ /builder/$1 break; + } + + location /builder/ { + proxy_pass http://{{ address }}:3000; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + 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_set_header Host $http_host; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + + proxy_pass http://minio-service:9000; + } + + client_header_timeout 60; + client_body_timeout 60; + keepalive_timeout 60; + gzip off; + gzip_comp_level 4; + } +} \ No newline at end of file diff --git a/hosting/proxy/Dockerfile b/hosting/proxy/Dockerfile new file mode 100644 index 0000000000..2fd64a9d68 --- /dev/null +++ b/hosting/proxy/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:latest +COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/hosting/proxy/nginx.conf b/hosting/proxy/nginx.conf new file mode 100644 index 0000000000..7a8a44e2d8 --- /dev/null +++ b/hosting/proxy/nginx.conf @@ -0,0 +1,135 @@ +user nginx; +error_log /var/log/nginx/error.log debug; +pid /var/run/nginx.pid; +worker_processes auto; +worker_rlimit_nofile 33282; + +events { + worker_connections 1024; +} + +http { + limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=10r/s; + include /etc/nginx/mime.types; + default_type application/octet-stream; + charset utf-8; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + server_tokens off; + types_hash_max_size 2048; + + # buffering + client_body_buffer_size 1K; + client_header_buffer_size 1k; + client_max_body_size 1k; + 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; + server_name _; + client_max_body_size 1000m; + ignore_invalid_headers off; + proxy_buffering off; + + # Security Headers + add_header X-Frame-Options SAMEORIGIN always; + add_header X-Content-Type-Options nosniff always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval' 'self' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me; frame-src 'self'; img-src https: data:; manifest-src 'self'; media-src 'self'; worker-src 'none';" always; + + location /app { + proxy_pass http://app-service:4002; + rewrite ^/app/(.*)$ /$1 break; + } + + location = / { + proxy_pass http://app-service:4002; + } + + location = /v1/update { + proxy_pass http://watchtower-service:8080; + } + + location /builder/ { + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://app-service:4002; + } + + location ^/(builder|app_) { + proxy_pass http://app-service:4002; + } + + location ~ ^/api/(system|admin|global)/ { + proxy_pass http://worker-service:4003; + } + + location /worker/ { + proxy_pass http://worker-service:4003; + rewrite ^/worker/(.*)$ /$1 break; + } + + location /api/ { + # calls to the API are rate limited with bursting + limit_req zone=ratelimit burst=10 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 Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://app-service:4002; + } + + location /db/ { + proxy_pass http://couchdb-service: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_set_header Host $http_host; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + proxy_pass http://minio-service: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; + } +} \ No newline at end of file diff --git a/hosting/scripts/airgapped/airgappedDockerBuild.js b/hosting/scripts/airgapped/airgappedDockerBuild.js index 5be19fdeb8..4bd324364c 100755 --- a/hosting/scripts/airgapped/airgappedDockerBuild.js +++ b/hosting/scripts/airgapped/airgappedDockerBuild.js @@ -5,7 +5,7 @@ const path = require("path") const IMAGES = { worker: "budibase/worker", apps: "budibase/apps", - proxy: "envoyproxy/envoy:v1.16-latest", + proxy: "budibase/proxy", minio: "minio/minio", couch: "ibmcom/couchdb3", curl: "curlimages/curl", @@ -15,8 +15,7 @@ const IMAGES = { const FILES = { COMPOSE: "docker-compose.yaml", - ENVOY: "envoy.yaml", - PROPERTIES: "hosting.properties" + NGINX: "nginx.conf" } const OUTPUT_DIR = path.join(__dirname, "../", "bb-airgapped") diff --git a/hosting/scripts/linux/release-to-docker-hub.sh b/hosting/scripts/linux/release-to-docker-hub.sh index 642a8682fb..599a10f914 100755 --- a/hosting/scripts/linux/release-to-docker-hub.sh +++ b/hosting/scripts/linux/release-to-docker-hub.sh @@ -9,8 +9,10 @@ fi echo "Tagging images with tag: $tag" +docker tag proxy-service budibase/proxy:$tag docker tag app-service budibase/apps:$tag docker tag worker-service budibase/worker:$tag docker push --all-tags budibase/apps docker push --all-tags budibase/worker +docker push --all-tags budibase/proxy diff --git a/lerna.json b/lerna.json index 832eb90564..6602c97ea5 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.57", + "version": "1.0.58-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 5960d15e75..9ab9a4411f 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,10 @@ "lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint", "test:e2e": "lerna run cy:test", "test:e2e:ci": "lerna run cy:ci", - "build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -", + "build:docker": "lerna run build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -", + "build:docker:proxy": "docker build hosting/proxy -t proxy-service", "build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -", - "build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", + "build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", "build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", "build:docs": "lerna run build:docs", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index a53a132f31..e20988bfa3 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.57", + "version": "1.0.58-alpha.0", "description": "Budibase backend core libraries used in server and worker", "main": "src/index.js", "author": "Budibase", diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index f95c3a173e..2149bd3e18 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -8,7 +8,7 @@ const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") const { getTenantId } = require("../../tenancy") -const INVALID_ERR = "Invalid Credentials" +const INVALID_ERR = "Invalid credentials" const SSO_NO_PASSWORD = "SSO user does not have a password set" const EXPIRED = "This account has expired. Please reset your password" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index fe4ee39617..1f529e5115 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.0.57", + "version": "1.0.58-alpha.0", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index da4d405f02..67930b8030 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -1,5 +1,6 @@ - - {#if icon} - - - + + (showTooltip = true)} + on:mouseleave={() => (showTooltip = false)} + > + {#if icon} + + + + {/if} + {#if $$slots} + + {/if} + {#if !disabled && tooltip} + + + + + + {/if} + + {#if showTooltip && tooltip} + + + + + {/if} - {#if $$slots} - - {/if} - + diff --git a/packages/bbui/src/ColorPicker/ColorPicker.svelte b/packages/bbui/src/ColorPicker/ColorPicker.svelte index ff6a292d1b..1fa950fadc 100644 --- a/packages/bbui/src/ColorPicker/ColorPicker.svelte +++ b/packages/bbui/src/ColorPicker/ColorPicker.svelte @@ -5,7 +5,7 @@ import { fly } from "svelte/transition" import Icon from "../Icon/Icon.svelte" import Input from "../Form/Input.svelte" - import { capitalise } from "../utils/helpers" + import { capitalise } from "../helpers" export let value export let size = "M" diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 8edb68a38e..c1c4cc866f 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -5,7 +5,7 @@ import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/picker/dist/index-vars.css" import { createEventDispatcher } from "svelte" - import { generateID } from "../../utils/helpers" + import { uuid } from "../../helpers" export let id = null export let disabled = false @@ -14,16 +14,20 @@ export let value = null export let placeholder = null export let appendTo = undefined + export let timeOnly = false const dispatch = createEventDispatcher() - const flatpickrId = `${generateID()}-wrapper` + const flatpickrId = `${uuid()}-wrapper` let open = false - let flatpickr + let flatpickr, flatpickrOptions, isTimeOnly + + $: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly $: flatpickrOptions = { element: `#${flatpickrId}`, - enableTime: enableTime || false, + enableTime: isTimeOnly || enableTime || false, + noCalendar: isTimeOnly || false, altInput: true, - altFormat: enableTime ? "F j Y, H:i" : "F j, Y", + altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y", wrap: true, appendTo, disableMobile: "true", @@ -35,6 +39,11 @@ if (newValue) { newValue = newValue.toISOString() } + // if time only set date component to today + if (timeOnly) { + const todayDate = new Date().toISOString().split("T")[0] + newValue = `${todayDate}T${newValue.split("T")[1]}` + } dispatch("change", newValue) } @@ -67,7 +76,11 @@ return null } let date - if (val instanceof Date) { + let time = new Date(`0-${val}`) + // it is a string like 00:00:00, just time + if (timeOnly || (typeof val === "string" && !isNaN(time))) { + date = time + } else if (val instanceof Date) { // Use real date obj if already parsed date = val } else if (isNaN(val)) { @@ -77,7 +90,7 @@ // Treat as numerical timestamp date = new Date(parseInt(val)) } - const time = date.getTime() + time = date.getTime() if (isNaN(time)) { return null } @@ -88,69 +101,71 @@ } - - - {#if !!error} + + {#if !!error} + + + + {/if} + + + - + - {/if} - + - - - - - - - + +{/key} {#if open} {/if} diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index 6b8022a36c..d739e751c9 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -3,7 +3,7 @@ import "@spectrum-css/typography/dist/index-vars.css" import "@spectrum-css/illustratedmessage/dist/index-vars.css" import { createEventDispatcher } from "svelte" - import { generateID } from "../../utils/helpers" + import { uuid } from "../../helpers" import Icon from "../../Icon/Icon.svelte" import Link from "../../Link/Link.svelte" import Tag from "../../Tags/Tag.svelte" @@ -37,7 +37,7 @@ "jfif", ] - const fieldId = id || generateID() + const fieldId = id || uuid() let selectedImageIdx = 0 let fileDragged = false let selectedUrl diff --git a/packages/bbui/src/Form/DatePicker.svelte b/packages/bbui/src/Form/DatePicker.svelte index 7d5656a22d..9298c49177 100644 --- a/packages/bbui/src/Form/DatePicker.svelte +++ b/packages/bbui/src/Form/DatePicker.svelte @@ -9,6 +9,7 @@ export let disabled = false export let error = null export let enableTime = true + export let timeOnly = false export let placeholder = null export let appendTo = undefined @@ -27,6 +28,7 @@ {value} {placeholder} {enableTime} + {timeOnly} {appendTo} on:change={onChange} /> diff --git a/packages/bbui/src/Table/DateTimeRenderer.svelte b/packages/bbui/src/Table/DateTimeRenderer.svelte index 8a06082d58..ff750cecd8 100644 --- a/packages/bbui/src/Table/DateTimeRenderer.svelte +++ b/packages/bbui/src/Table/DateTimeRenderer.svelte @@ -2,9 +2,18 @@ import dayjs from "dayjs" export let value + + // adding the 0- will turn a string like 00:00:00 into a valid ISO + // date, but will make actual ISO dates invalid + $: time = new Date(`0-${value}`) + $: isTime = !isNaN(time) -{dayjs(value).format("MMMM D YYYY, HH:mm")} + + {dayjs(isTime ? time : value).format( + isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm" + )} + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte index e303729d0b..11d19edf7c 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte @@ -12,7 +12,7 @@ import { dndzone } from "svelte-dnd-action" import { generate } from "shortid" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" - import { OperatorOptions, getValidOperatorsForType } from "constants/lucene" + import { LuceneUtils, Constants } from "@budibase/frontend-core" import { selectedComponent } from "builderStore" import { getComponentForSettingType } from "./componentSettings" import PropertyControl from "./PropertyControl.svelte" @@ -83,7 +83,7 @@ valueType: "string", id: generate(), action: "hide", - operator: OperatorOptions.Equals.value, + operator: Constants.OperatorOptions.Equals.value, }, ] } @@ -108,13 +108,13 @@ } const getOperatorOptions = condition => { - return getValidOperatorsForType(condition.valueType) + return LuceneUtils.getValidOperatorsForType(condition.valueType) } const onOperatorChange = (condition, newOperator) => { const noValueOptions = [ - OperatorOptions.Empty.value, - OperatorOptions.NotEmpty.value, + Constants.OperatorOptions.Empty.value, + Constants.OperatorOptions.NotEmpty.value, ] condition.noValue = noValueOptions.includes(newOperator) if (condition.noValue) { @@ -127,9 +127,12 @@ condition.referenceValue = null // Ensure a valid operator is set - const validOperators = getValidOperatorsForType(newType).map(x => x.value) + const validOperators = LuceneUtils.getValidOperatorsForType(newType).map( + x => x.value + ) if (!validOperators.includes(condition.operator)) { - condition.operator = validOperators[0] ?? OperatorOptions.Equals.value + condition.operator = + validOperators[0] ?? Constants.OperatorOptions.Equals.value onOperatorChange(condition, condition.operator) } } diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte index ac97bf6065..ef56c610bd 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte @@ -13,7 +13,7 @@ import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import { generate } from "shortid" - import { getValidOperatorsForType, OperatorOptions } from "constants/lucene" + import { LuceneUtils, Constants } from "@budibase/frontend-core" import { getFields } from "helpers/searchFields" export let schemaFields @@ -32,7 +32,7 @@ { id: generate(), field: null, - operator: OperatorOptions.Equals.value, + operator: Constants.OperatorOptions.Equals.value, value: null, valueType: "Value", }, @@ -54,11 +54,12 @@ expression.type = enrichedSchemaFields.find(x => x.name === field)?.type // Ensure a valid operator is set - const validOperators = getValidOperatorsForType(expression.type).map( - x => x.value - ) + const validOperators = LuceneUtils.getValidOperatorsForType( + expression.type + ).map(x => x.value) if (!validOperators.includes(expression.operator)) { - expression.operator = validOperators[0] ?? OperatorOptions.Equals.value + expression.operator = + validOperators[0] ?? Constants.OperatorOptions.Equals.value onOperatorChange(expression, expression.operator) } @@ -73,8 +74,8 @@ const onOperatorChange = (expression, operator) => { const noValueOptions = [ - OperatorOptions.Empty.value, - OperatorOptions.NotEmpty.value, + Constants.OperatorOptions.Empty.value, + Constants.OperatorOptions.NotEmpty.value, ] expression.noValue = noValueOptions.includes(operator) if (expression.noValue) { @@ -110,7 +111,7 @@ /> onOperatorChange(filter, e.detail)} placeholder={null} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte index fa2a0d6088..a76a93d7f6 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte @@ -1,5 +1,5 @@ diff --git a/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte b/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte index 4a7c77746e..ded80a7d5c 100644 --- a/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte @@ -1,7 +1,7 @@ @@ -34,7 +42,7 @@ control={prop.control} key={prop.key} value={style[prop.key]} - onChange={val => store.actions.components.updateStyle(prop.key, val)} + onChange={val => updateStyle(prop.key, val)} props={getControlProps(prop)} {bindings} /> diff --git a/packages/builder/src/components/feedback/NPSFeedbackForm.svelte b/packages/builder/src/components/feedback/NPSFeedbackForm.svelte index 4c5bb46c63..6a6e52ec74 100644 --- a/packages/builder/src/components/feedback/NPSFeedbackForm.svelte +++ b/packages/builder/src/components/feedback/NPSFeedbackForm.svelte @@ -13,6 +13,7 @@ Detail, Divider, Layout, + notifications, } from "@budibase/bbui" import { auth } from "stores/portal" @@ -45,20 +46,28 @@ improvements, comment, }) - auth.updateSelf({ - flags: { - feedbackSubmitted: true, - }, - }) + try { + auth.updateSelf({ + flags: { + feedbackSubmitted: true, + }, + }) + } catch (error) { + notifications.error("Error updating user") + } dispatch("complete") } function cancelFeedback() { - auth.updateSelf({ - flags: { - feedbackSubmitted: true, - }, - }) + try { + auth.updateSelf({ + flags: { + feedbackSubmitted: true, + }, + }) + } catch (error) { + notifications.error("Error updating user") + } dispatch("complete") } diff --git a/packages/builder/src/components/integration/AccessLevelSelect.svelte b/packages/builder/src/components/integration/AccessLevelSelect.svelte index 86065893d4..59f6b8a105 100644 --- a/packages/builder/src/components/integration/AccessLevelSelect.svelte +++ b/packages/builder/src/components/integration/AccessLevelSelect.svelte @@ -1,5 +1,5 @@ diff --git a/packages/builder/src/components/start/ChooseIconModal.svelte b/packages/builder/src/components/start/ChooseIconModal.svelte index 4efb679a51..b2f68c6ce7 100644 --- a/packages/builder/src/components/start/ChooseIconModal.svelte +++ b/packages/builder/src/components/start/ChooseIconModal.svelte @@ -1,5 +1,12 @@ diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index 3efd0231aa..91c4807dc8 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -2,8 +2,8 @@ import { writable, get as svelteGet } from "svelte/store" import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui" import { store, automationStore } from "builderStore" + import { API } from "api" import { apps, admin, auth } from "stores/portal" - import api, { get, post } from "builderStore/api" import analytics, { Events } from "analytics" import { onMount } from "svelte" import { goto } from "@roxi/routify" @@ -45,43 +45,27 @@ } // Create App - const appResp = await post("/api/applications", data, {}) - const appJson = await appResp.json() - if (!appResp.ok) { - throw new Error(appJson.message) - } - + const createdApp = await API.createApp(data) analytics.captureEvent(Events.APP.CREATED, { name: $values.name, - appId: appJson.instance._id, + appId: createdApp.instance._id, templateToUse: template, }) // Select Correct Application/DB in prep for creating user - const applicationPkg = await get( - `/api/applications/${appJson.instance._id}/appPackage` - ) - const pkg = await applicationPkg.json() - if (applicationPkg.ok) { - await store.actions.initialise(pkg) - await automationStore.actions.fetch() - // update checklist - incase first app - await admin.init() - } else { - throw new Error(pkg) - } + const pkg = await API.fetchAppPackage(createdApp.instance._id) + await store.actions.initialise(pkg) + await automationStore.actions.fetch() + // Update checklist - in case first app + await admin.init() // Create user - const user = { - roleId: $values.roleId, - } - const userResp = await api.post(`/api/users/metadata/self`, user) - await userResp.json() + await API.updateOwnMetadata({ roleId: $values.roleId }) await auth.setInitInfo({}) - $goto(`/builder/app/${appJson.instance._id}`) + $goto(`/builder/app/${createdApp.instance._id}`) } catch (error) { console.error(error) - notifications.error(error) + notifications.error("Error creating app") } } diff --git a/packages/builder/src/components/start/UpdateAppModal.svelte b/packages/builder/src/components/start/UpdateAppModal.svelte index 7549876fc0..1ce699b834 100644 --- a/packages/builder/src/components/start/UpdateAppModal.svelte +++ b/packages/builder/src/components/start/UpdateAppModal.svelte @@ -38,7 +38,7 @@ await apps.update(app.instance._id, body) } catch (error) { console.error(error) - notifications.error(error) + notifications.error("Error updating app") } } diff --git a/packages/builder/src/constants/lucene.js b/packages/builder/src/constants/lucene.js deleted file mode 100644 index 8a6bf57b5f..0000000000 --- a/packages/builder/src/constants/lucene.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Operator options for lucene queries - */ -export const OperatorOptions = { - Equals: { - value: "equal", - label: "Equals", - }, - NotEquals: { - value: "notEqual", - label: "Not equals", - }, - Empty: { - value: "empty", - label: "Is empty", - }, - NotEmpty: { - value: "notEmpty", - label: "Is not empty", - }, - StartsWith: { - value: "string", - label: "Starts with", - }, - Like: { - value: "fuzzy", - label: "Like", - }, - MoreThan: { - value: "rangeLow", - label: "More than", - }, - LessThan: { - value: "rangeHigh", - label: "Less than", - }, - Contains: { - value: "equal", - label: "Contains", - }, - NotContains: { - value: "notEqual", - label: "Does Not Contain", - }, -} - -export const NoEmptyFilterStrings = [ - OperatorOptions.StartsWith.value, - OperatorOptions.Like.value, - OperatorOptions.Equals.value, - OperatorOptions.NotEquals.value, - OperatorOptions.Contains.value, - OperatorOptions.NotContains.value, -] - -/** - * Returns the valid operator options for a certain data type - * @param type the data type - */ -export const getValidOperatorsForType = type => { - const Op = OperatorOptions - const stringOps = [ - Op.Equals, - Op.NotEquals, - Op.StartsWith, - Op.Like, - Op.Empty, - Op.NotEmpty, - ] - const numOps = [ - Op.Equals, - Op.NotEquals, - Op.MoreThan, - Op.LessThan, - Op.Empty, - Op.NotEmpty, - ] - if (type === "string") { - return stringOps - } else if (type === "number") { - return numOps - } else if (type === "options") { - return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] - } else if (type === "array") { - return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty] - } else if (type === "boolean") { - return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] - } else if (type === "longform") { - return stringOps - } else if (type === "datetime") { - return numOps - } else if (type === "formula") { - return stringOps.concat([Op.MoreThan, Op.LessThan]) - } - return [] -} diff --git a/packages/builder/src/helpers/fetchData.js b/packages/builder/src/helpers/fetchData.js index 65061f6b6a..9208419c4e 100644 --- a/packages/builder/src/helpers/fetchData.js +++ b/packages/builder/src/helpers/fetchData.js @@ -1,5 +1,5 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export default function (url) { const store = writable({ status: "LOADING", data: {}, error: {} }) @@ -7,8 +7,8 @@ export default function (url) { async function get() { store.update(u => ({ ...u, status: "LOADING" })) try { - const response = await api.get(url) - store.set({ data: await response.json(), status: "SUCCESS" }) + const data = await API.get({ url }) + store.set({ data, status: "SUCCESS" }) } catch (e) { store.set({ data: {}, error: e, status: "ERROR" }) } diff --git a/packages/builder/src/helpers/fetchTableData.js b/packages/builder/src/helpers/fetchTableData.js deleted file mode 100644 index 6d61ec813e..0000000000 --- a/packages/builder/src/helpers/fetchTableData.js +++ /dev/null @@ -1,210 +0,0 @@ -// Do not use any aliased imports in common files, as these will be bundled -// by multiple bundlers which may not be able to resolve them. -// This will eventually be replaced by the new client implementation when we -// add a core package. -import { writable, derived, get } from "svelte/store" -import * as API from "../builderStore/api" -import { buildLuceneQuery } from "./lucene" - -const defaultOptions = { - tableId: null, - filters: null, - limit: 10, - sortColumn: null, - sortOrder: "ascending", - paginate: true, - schema: null, -} - -export const fetchTableData = opts => { - // Save option set so we can override it later rather than relying on params - let options = { - ...defaultOptions, - ...opts, - } - - // Local non-observable state - let query - let sortType - let lastBookmark - - // Local observable state - const store = writable({ - rows: [], - schema: null, - loading: false, - loaded: false, - bookmarks: [], - pageNumber: 0, - }) - - // Derive certain properties to return - const derivedStore = derived(store, $store => { - return { - ...$store, - hasNextPage: $store.bookmarks[$store.pageNumber + 1] != null, - hasPrevPage: $store.pageNumber > 0, - } - }) - - const fetchPage = async bookmark => { - lastBookmark = bookmark - const { tableId, limit, sortColumn, sortOrder, paginate } = options - const res = await API.post(`/api/${options.tableId}/search`, { - tableId, - query, - limit, - sort: sortColumn, - sortOrder: sortOrder?.toLowerCase() ?? "ascending", - sortType, - paginate, - bookmark, - }) - return await res.json() - } - - // Fetches a fresh set of results from the server - const fetchData = async () => { - const { tableId, schema, sortColumn, filters } = options - - // Ensure table ID exists - if (!tableId) { - return - } - - // Get and enrich schema. - // Ensure there are "name" properties for all fields and that field schema - // are objects - let enrichedSchema = schema - if (!enrichedSchema) { - const definition = await API.get(`/api/tables/${tableId}`) - enrichedSchema = definition?.schema ?? null - } - if (enrichedSchema) { - Object.entries(schema).forEach(([fieldName, fieldSchema]) => { - if (typeof fieldSchema === "string") { - enrichedSchema[fieldName] = { - type: fieldSchema, - name: fieldName, - } - } else { - enrichedSchema[fieldName] = { - ...fieldSchema, - name: fieldName, - } - } - }) - - // Save fixed schema so we can provide it later - options.schema = enrichedSchema - } - - // Ensure schema exists - if (!schema) { - return - } - store.update($store => ({ ...$store, schema, loading: true })) - - // Work out what sort type to use - if (!sortColumn || !schema[sortColumn]) { - sortType = "string" - } - const type = schema?.[sortColumn]?.type - sortType = type === "number" ? "number" : "string" - - // Build the lucene query - query = buildLuceneQuery(filters) - - // Actually fetch data - const page = await fetchPage() - store.update($store => ({ - ...$store, - loading: false, - loaded: true, - pageNumber: 0, - rows: page.rows, - bookmarks: page.hasNextPage ? [null, page.bookmark] : [null], - })) - } - - // Fetches the next page of data - const nextPage = async () => { - const state = get(derivedStore) - if (state.loading || !options.paginate || !state.hasNextPage) { - return - } - - // Fetch next page - store.update($store => ({ ...$store, loading: true })) - const page = await fetchPage(state.bookmarks[state.pageNumber + 1]) - - // Update state - store.update($store => { - let { bookmarks, pageNumber } = $store - if (page.hasNextPage) { - bookmarks[pageNumber + 2] = page.bookmark - } - return { - ...$store, - pageNumber: pageNumber + 1, - rows: page.rows, - bookmarks, - loading: false, - } - }) - } - - // Fetches the previous page of data - const prevPage = async () => { - const state = get(derivedStore) - if (state.loading || !options.paginate || !state.hasPrevPage) { - return - } - - // Fetch previous page - store.update($store => ({ ...$store, loading: true })) - const page = await fetchPage(state.bookmarks[state.pageNumber - 1]) - - // Update state - store.update($store => { - return { - ...$store, - pageNumber: $store.pageNumber - 1, - rows: page.rows, - loading: false, - } - }) - } - - // Resets the data set and updates options - const update = async newOptions => { - if (newOptions) { - options = { - ...options, - ...newOptions, - } - } - await fetchData() - } - - // Loads the same page again - const refresh = async () => { - if (get(store).loading) { - return - } - const page = await fetchPage(lastBookmark) - store.update($store => ({ ...$store, rows: page.rows })) - } - - // Initially fetch data but don't bother waiting for the result - fetchData() - - // Return our derived store which will be updated over time - return { - subscribe: derivedStore.subscribe, - nextPage, - prevPage, - update, - refresh, - } -} diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte index 1d41af15e7..cb760cd165 100644 --- a/packages/builder/src/pages/builder/_layout.svelte +++ b/packages/builder/src/pages/builder/_layout.svelte @@ -2,12 +2,7 @@ import { isActive, redirect, params } from "@roxi/routify" import { admin, auth } from "stores/portal" import { onMount } from "svelte" - import { - Cookies, - getCookie, - removeCookie, - setCookie, - } from "builderStore/cookies" + import { CookieUtils, Constants } from "@budibase/frontend-core" let loaded = false @@ -46,9 +41,12 @@ if (user.tenantId !== urlTenantId) { // user should not be here - play it safe and log them out - await auth.logout() - await auth.setOrganisation(null) - return + try { + await auth.logout() + await auth.setOrganisation(null) + } catch (error) { + // Swallow error and do nothing + } } } else { // no user - set the org according to the url @@ -57,17 +55,23 @@ } onMount(async () => { - if ($params["?template"]) { - await auth.setInitInfo({ init_template: $params["?template"] }) + try { + await auth.getSelf() + await admin.init() + + // Set init info if present + if ($params["?template"]) { + await auth.setInitInfo({ init_template: $params["?template"] }) + } + + // Validate tenant if in a multi-tenant env + if (useAccountPortal && multiTenancyEnabled) { + await validateTenantId() + } + } catch (error) { + // Don't show a notification here, as we might 403 initially due to not + // being logged in } - - await auth.getSelf() - await admin.init() - - if (useAccountPortal && multiTenancyEnabled) { - await validateTenantId() - } - loaded = true }) @@ -79,7 +83,7 @@ loaded && apiReady && !$auth.user && - !getCookie(Cookies.ReturnUrl) && + !CookieUtils.getCookie(Constants.Cookies.ReturnUrl) && // logout triggers a page refresh, so we don't want to set the return url !$auth.postLogout && // don't set the return url on pre-login pages @@ -88,7 +92,7 @@ !$isActive("./admin") ) { const url = window.location.pathname - setCookie(Cookies.ReturnUrl, url) + CookieUtils.setCookie(Constants.Cookies.ReturnUrl, url) } // if tenant is not set go to it @@ -122,9 +126,9 @@ } // lastly, redirect to the return url if it has been set else if (loaded && apiReady && $auth.user) { - const returnUrl = getCookie(Cookies.ReturnUrl) + const returnUrl = CookieUtils.getCookie(Constants.Cookies.ReturnUrl) if (returnUrl) { - removeCookie(Cookies.ReturnUrl) + CookieUtils.removeCookie(Constants.Cookies.ReturnUrl) window.location.href = returnUrl } } diff --git a/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte b/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte index de29e11301..182df63967 100644 --- a/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte +++ b/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte @@ -1,6 +1,6 @@ @@ -36,10 +31,10 @@ onConfirm={importApps} disabled={!value.file} > - Please upload the file that was exported from your Cloud environment to get - started + + Please upload the file that was exported from your Cloud environment to get + started + { if (!cloud) { - await admin.checkImportComplete() + try { + await admin.checkImportComplete() + } catch (error) { + notifications.error("Error checking import status") + } } }) diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index 553125e1a7..1003936214 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -6,25 +6,26 @@ import RevertModal from "components/deploy/RevertModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte" import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte" - import { get, post } from "builderStore/api" + import { API } from "api" import { auth, admin } from "stores/portal" import { isActive, goto, layout, redirect } from "@roxi/routify" import Logo from "assets/bb-emblem.svg" import { capitalise } from "helpers" - import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte" + import UpgradeModal from "components/upgrade/UpgradeModal.svelte" import { onMount, onDestroy } from "svelte" - // Get Package and set store export let application + + // Get Package and set store let promise = getPackage() - // sync once when you load the app + + // Sync once when you load the app let hasSynced = false + let userShouldPostFeedback = false $: selected = capitalise( $layout.children.find(layout => $isActive(layout.path))?.title ?? "data" ) - let userShouldPostFeedback = false - function previewApp() { if (!$auth?.user?.flags?.feedbackSubmitted) { userShouldPostFeedback = true @@ -33,34 +34,24 @@ } async function getPackage() { - const res = await get(`/api/applications/${application}/appPackage`) - const pkg = await res.json() - - if (res.ok) { - try { - await store.actions.initialise(pkg) - // edge case, lock wasn't known to client when it re-directed, or user went directly - } catch (err) { - if (!err.ok && err.reason === "locked") { - $redirect("../../") - } else { - throw err - } - } + try { + const pkg = await API.fetchAppPackage(application) + await store.actions.initialise(pkg) await automationStore.actions.fetch() await roles.fetch() await flags.fetch() return pkg - } else { - throw new Error(pkg) + } catch (error) { + notifications.error(`Error initialising app: ${error?.message}`) + $redirect("../../") } } - // handles navigation between frontend, backend, automation. - // this remembers your last place on each of the sections + // Handles navigation between frontend, backend, automation. + // This remembers your last place on each of the sections // e.g. if one of your screens is selected on front end, then // you browse to backend, when you click frontend, you will be - // brought back to the same screen + // brought back to the same screen. const topItemNavigate = path => () => { const activeTopNav = $layout.children.find(c => $isActive(c.path)) if (!activeTopNav) return @@ -74,8 +65,9 @@ onMount(async () => { if (!hasSynced && application) { - const res = await post(`/api/applications/${application}/sync`) - if (res.status !== 200) { + try { + await API.syncApp(application) + } catch (error) { notifications.error("Failed to sync with production database") } hasSynced = true diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index 808c3a49ec..a0df3a9d07 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -59,6 +59,9 @@ $: schemaReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess $: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly) + $: hasSchema = + Object.keys(schema || {}).length !== 0 || + Object.keys(query?.schema || {}).length !== 0 function getSelectedQuery() { return cloneDeep( @@ -112,14 +115,13 @@ const { _id } = await queries.save(toSave.datasourceId, toSave) saveId = _id query = getSelectedQuery() - notifications.success(`Request saved successfully.`) - + notifications.success(`Request saved successfully`) if (dynamicVariables) { datasource.config.dynamicVariables = rebuildVariables(saveId) datasource = await datasources.save(datasource) } } catch (err) { - notifications.error(`Error saving query. ${err.message}`) + notifications.error(`Error saving query`) } } @@ -127,14 +129,14 @@ try { response = await queries.preview(buildQuery(query)) if (response.rows.length === 0) { - notifications.info("Request did not return any data.") + notifications.info("Request did not return any data") } else { response.info = response.info || { code: 200 } schema = response.schema - notifications.success("Request sent successfully.") + notifications.success("Request sent successfully") } - } catch (err) { - notifications.error(err) + } catch (error) { + notifications.error("Error running query") } } @@ -226,10 +228,24 @@ ) } + const updateFlag = async (flag, value) => { + try { + await flags.updateFlag(flag, value) + } catch (error) { + notifications.error("Error updating flag") + } + } + onMount(async () => { query = getSelectedQuery() - // clear any unsaved changes to the datasource - await datasources.init() + + try { + // Clear any unsaved changes to the datasource + await datasources.init() + } catch (error) { + notifications.error("Error getting datasources") + } + datasource = $datasources.list.find(ds => ds._id === query?.datasourceId) const datasourceUrl = datasource?.config.url const qs = query?.fields.queryString @@ -294,6 +310,7 @@ bind:value={query.name} defaultValue="Untitled" on:change={() => (query.flags.urlName = false)} + on:save={saveQuery} /> Access level @@ -313,7 +330,15 @@ - Send + Send + Save @@ -393,8 +418,7 @@ window.open( "https://docs.budibase.com/building-apps/data/transformers" )} - on:change={() => - flags.updateFlag("queryTransformerBanner", true)} + on:change={() => updateFlag("queryTransformerBanner", true)} > Add a JavaScript function to transform the query result. @@ -527,9 +551,6 @@ >{response?.info.size} - Save query {/if} diff --git a/packages/builder/src/pages/builder/apps/index.svelte b/packages/builder/src/pages/builder/apps/index.svelte index c98e749e45..39cc780ac7 100644 --- a/packages/builder/src/pages/builder/apps/index.svelte +++ b/packages/builder/src/pages/builder/apps/index.svelte @@ -10,6 +10,7 @@ Icon, Body, Modal, + notifications, } from "@budibase/bbui" import { onMount } from "svelte" import { apps, organisation, auth } from "stores/portal" @@ -26,8 +27,12 @@ let changePasswordModal onMount(async () => { - await organisation.init() - await apps.load() + try { + await organisation.init() + await apps.load() + } catch (error) { + notifications.error("Error loading apps") + } loaded = true }) @@ -47,6 +52,14 @@ return `/${app.prodId}` } } + + const logout = async () => { + try { + await auth.logout() + } catch (error) { + // Swallow error and do nothing + } + } {#if $auth.user && loaded} @@ -82,7 +95,7 @@ Open developer mode {/if} - Log out + Log out diff --git a/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte index bae68b6548..27f5bde186 100644 --- a/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte +++ b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte @@ -1,5 +1,5 @@ diff --git a/packages/builder/src/pages/builder/auth/index.svelte b/packages/builder/src/pages/builder/auth/index.svelte index a2a02e65c1..72b3a8c7cf 100644 --- a/packages/builder/src/pages/builder/auth/index.svelte +++ b/packages/builder/src/pages/builder/auth/index.svelte @@ -2,6 +2,7 @@ import { redirect } from "@roxi/routify" import { auth, admin } from "stores/portal" import { onMount } from "svelte" + import { notifications } from "@budibase/bbui" $: tenantSet = $auth.tenantSet $: multiTenancyEnabled = $admin.multiTenancy @@ -17,8 +18,12 @@ } onMount(async () => { - await admin.init() - await auth.checkQueryString() + try { + await admin.init() + await auth.checkQueryString() + } catch (error) { + notifications.error("Error getting checklist") + } loaded = true }) diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 7a13164c51..d9151b4342 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -31,7 +31,6 @@ username, password, }) - if ($auth?.user?.forceResetPassword) { $goto("./reset") } else { @@ -39,8 +38,7 @@ $goto("../portal") } } catch (err) { - console.error(err) - notifications.error(err.message ? err.message : "Invalid Credentials") + notifications.error(err.message ? err.message : "Invalid credentials") } } @@ -49,7 +47,11 @@ } onMount(async () => { - await organisation.init() + try { + await organisation.init() + } catch (error) { + notifications.error("Error getting org config") + } loaded = true }) diff --git a/packages/builder/src/pages/builder/auth/org.svelte b/packages/builder/src/pages/builder/auth/org.svelte index 5a484b6c93..8fd94463d9 100644 --- a/packages/builder/src/pages/builder/auth/org.svelte +++ b/packages/builder/src/pages/builder/auth/org.svelte @@ -1,5 +1,13 @@ diff --git a/packages/builder/src/pages/builder/invite/index.svelte b/packages/builder/src/pages/builder/invite/index.svelte index ddf888ad73..c4745d8737 100644 --- a/packages/builder/src/pages/builder/invite/index.svelte +++ b/packages/builder/src/pages/builder/invite/index.svelte @@ -10,14 +10,11 @@ async function acceptInvite() { try { - const res = await users.acceptInvite(inviteCode, password) - if (!res) { - throw new Error(res.message) - } - notifications.success(`User created.`) + await users.acceptInvite(inviteCode, password) + notifications.success("Invitation accepted successfully") $goto("../auth/login") - } catch (err) { - notifications.error(err) + } catch (error) { + notifications.error("Error accepting invitation") } } diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte index 8fca18d29d..f4679647ff 100644 --- a/packages/builder/src/pages/builder/portal/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/_layout.svelte @@ -10,6 +10,7 @@ MenuItem, Modal, clickOutside, + notifications, } from "@budibase/bbui" import ConfigChecklist from "components/common/ConfigChecklist.svelte" import { organisation, auth } from "stores/portal" @@ -78,6 +79,14 @@ return menu } + const logout = async () => { + try { + await auth.logout() + } catch (error) { + // Swallow error and do nothing + } + } + const showMobileMenu = () => (mobileMenuVisible = true) const hideMobileMenu = () => (mobileMenuVisible = false) @@ -87,7 +96,11 @@ if (!$auth.user?.builder?.global) { $redirect("../") } else { - await organisation.init() + try { + await organisation.init() + } catch (error) { + notifications.error("Error getting org config") + } loaded = true } } @@ -158,7 +171,7 @@ $goto("../apps")}> Close developer mode - Log out + Log out diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index bf783fdb86..b05aa1b659 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -19,7 +19,7 @@ import ChooseIconModal from "components/start/ChooseIconModal.svelte" import { store, automationStore } from "builderStore" - import api, { del, post, get } from "builderStore/api" + import { API } from "api" import { onMount } from "svelte" import { apps, auth, admin, templates } from "stores/portal" import download from "downloadjs" @@ -115,43 +115,29 @@ data.append("templateKey", template.key) // Create App - const appResp = await post("/api/applications", data, {}) - const appJson = await appResp.json() - if (!appResp.ok) { - throw new Error(appJson.message) - } - + const createdApp = await API.createApp(data) analytics.captureEvent(Events.APP.CREATED, { name: appName, - appId: appJson.instance._id, + appId: createdApp.instance._id, template, fromTemplateMarketplace: true, }) // Select Correct Application/DB in prep for creating user - const applicationPkg = await get( - `/api/applications/${appJson.instance._id}/appPackage` - ) - const pkg = await applicationPkg.json() - if (applicationPkg.ok) { - await store.actions.initialise(pkg) - await automationStore.actions.fetch() - // update checklist - incase first app - await admin.init() - } else { - throw new Error(pkg) - } + const pkg = await API.fetchAppPackage(createdApp.instance._id) + await store.actions.initialise(pkg) + await automationStore.actions.fetch() + // Update checklist - in case first app + await admin.init() // Create user - const userResp = await api.post(`/api/users/metadata/self`, { + await API.updateOwnMetadata({ roleId: "BASIC", }) - await userResp.json() await auth.setInitInfo({}) - $goto(`/builder/app/${appJson.instance._id}`) + $goto(`/builder/app/${createdApp.instance._id}`) } catch (error) { - console.error(error) - notifications.error(error) + notifications.error("Error creating app") } } @@ -199,17 +185,11 @@ return } try { - const response = await del( - `/api/applications/${selectedApp.prodId}?unpublish=1` - ) - if (response.status !== 200) { - const json = await response.json() - throw json.message - } + await API.unpublishApp(selectedApp.prodId) await apps.load() notifications.success("App unpublished successfully") } catch (err) { - notifications.error(`Error unpublishing app: ${err}`) + notifications.error("Error unpublishing app") } } @@ -223,17 +203,13 @@ return } try { - const response = await del(`/api/applications/${selectedApp?.devId}`) - if (response.status !== 200) { - const json = await response.json() - throw json.message - } + await API.deleteApp(selectedApp?.devId) await apps.load() - // get checklist, just in case that was the last app + // Get checklist, just in case that was the last app await admin.init() notifications.success("App deleted successfully") } catch (err) { - notifications.error(`Error deleting app: ${err}`) + notifications.error("Error deleting app") } selectedApp = null appName = null @@ -246,15 +222,11 @@ const releaseLock = async app => { try { - const response = await del(`/api/dev/${app.devId}/lock`) - if (response.status !== 200) { - const json = await response.json() - throw json.message - } + await API.releaseAppLock(app.devId) await apps.load() notifications.success("Lock released successfully") } catch (err) { - notifications.error(`Error releasing lock: ${err}`) + notifications.error("Error releasing lock") } } @@ -272,17 +244,23 @@ } onMount(async () => { - await apps.load() - await templates.load() - if ($templates?.length === 0) { - notifications.error("There was a problem loading quick start templates.") - } - // if the portal is loaded from an external URL with a template param - const initInfo = await auth.getInitInfo() - if (initInfo?.init_template) { - creatingFromTemplate = true - createAppFromTemplateUrl(initInfo.init_template) - return + try { + await apps.load() + await templates.load() + if ($templates?.length === 0) { + notifications.error( + "There was a problem loading quick start templates." + ) + } + // If the portal is loaded from an external URL with a template param + const initInfo = await auth.getInitInfo() + if (initInfo?.init_template) { + creatingFromTemplate = true + createAppFromTemplateUrl(initInfo.init_template) + return + } + } catch (error) { + notifications.error("Error loading apps and templates") } loaded = true }) diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte index 20d30fdfbb..b001f02fe9 100644 --- a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte @@ -20,9 +20,9 @@ Toggle, } from "@budibase/bbui" import { onMount } from "svelte" - import api from "builderStore/api" + import { API } from "api" import { organisation, admin } from "stores/portal" - import { uuid } from "builderStore/uuid" + import { Helpers } from "@budibase/bbui" import analytics, { Events } from "analytics" const ConfigTypes = { @@ -137,17 +137,6 @@ providers.oidc?.config?.configs[0].clientID && providers.oidc?.config?.configs[0].clientSecret - async function uploadLogo(file) { - let data = new FormData() - data.append("file", file) - const res = await api.post( - `/api/global/configs/upload/logos_oidc/${file.name}`, - data, - {} - ) - return await res.json() - } - const onFileSelected = e => { let fileName = e.target.files[0].name image = e.target.files[0] @@ -156,17 +145,28 @@ } async function save(docs) { - // only if the user has provided an image, upload it. - image && uploadLogo(image) let calls = [] + + // Only if the user has provided an image, upload it + if (image) { + let data = new FormData() + data.append("file", image) + calls.push( + API.uploadOIDCLogo({ + name: image.name, + data, + }) + ) + } + docs.forEach(element => { if (element.type === ConfigTypes.OIDC) { - //Add a UUID here so each config is distinguishable when it arrives at the login page + // Add a UUID here so each config is distinguishable when it arrives at the login page for (let config of element.config.configs) { if (!config.uuid) { - config.uuid = uuid() + config.uuid = Helpers.uuid() } - // callback urls shouldn't be included + // Callback urls shouldn't be included delete config.callbackURL } if (partialOidc) { @@ -175,8 +175,8 @@ `Please fill in all required ${ConfigTypes.OIDC} fields` ) } else { - calls.push(api.post(`/api/global/configs`, element)) - // turn the save button grey when clicked + calls.push(API.saveConfig(element)) + // Turn the save button grey when clicked oidcSaveButtonDisabled = true originalOidcDoc = cloneDeep(providers.oidc) } @@ -189,71 +189,73 @@ `Please fill in all required ${ConfigTypes.Google} fields` ) } else { - calls.push(api.post(`/api/global/configs`, element)) + calls.push(API.saveConfig(element)) googleSaveButtonDisabled = true originalGoogleDoc = cloneDeep(providers.google) } } } }) - calls.length && + + if (calls.length) { Promise.all(calls) - .then(responses => { - return Promise.all( - responses.map(response => { - return response.json() - }) - ) - }) .then(data => { data.forEach(res => { providers[res.type]._rev = res._rev providers[res.type]._id = res._id }) - notifications.success(`Settings saved.`) + notifications.success(`Settings saved`) analytics.captureEvent(Events.SSO.SAVED) }) - .catch(err => { - notifications.error(`Failed to update auth settings. ${err}`) - throw new Error(err.message) + .catch(() => { + notifications.error("Failed to update auth settings") }) + } } onMount(async () => { - await organisation.init() - // fetch the configs for oauth - const googleResponse = await api.get( - `/api/global/configs/${ConfigTypes.Google}` - ) - const googleDoc = await googleResponse.json() + try { + await organisation.init() + } catch (error) { + notifications.error("Error getting org config") + } - if (!googleDoc._id) { + // Fetch Google config + let googleDoc + try { + googleDoc = await API.getConfig(ConfigTypes.Google) + } catch (error) { + notifications.error("Error fetching Google OAuth config") + } + if (!googleDoc?._id) { providers.google = { type: ConfigTypes.Google, config: { activated: true }, } originalGoogleDoc = cloneDeep(googleDoc) } else { - // default activated to true for older configs + // Default activated to true for older configs if (googleDoc.config.activated === undefined) { googleDoc.config.activated = true } originalGoogleDoc = cloneDeep(googleDoc) providers.google = googleDoc } - googleCallbackUrl = providers?.google?.config?.callbackURL - //Get the list of user uploaded logos and push it to the dropdown options. - //This needs to be done before the config call so they're available when the dropdown renders - const res = await api.get(`/api/global/configs/logos_oidc`) - const configSettings = await res.json() - - if (configSettings.config) { - const logoKeys = Object.keys(configSettings.config) - + // Get the list of user uploaded logos and push it to the dropdown options. + // This needs to be done before the config call so they're available when + // the dropdown renders. + let oidcLogos + try { + oidcLogos = await API.getOIDCLogos() + } catch (error) { + notifications.error("Error fetching OIDC logos") + } + if (oidcLogos?.config) { + const logoKeys = Object.keys(oidcLogos.config) logoKeys.map(logoKey => { - const logoUrl = configSettings.config[logoKey] + const logoUrl = oidcLogos.config[logoKey] iconDropdownOptions.unshift({ label: logoKey, value: logoKey, @@ -261,11 +263,15 @@ }) }) } - const oidcResponse = await api.get( - `/api/global/configs/${ConfigTypes.OIDC}` - ) - const oidcDoc = await oidcResponse.json() - if (!oidcDoc._id) { + + // Fetch OIDC config + let oidcDoc + try { + oidcDoc = await API.getConfig(ConfigTypes.OIDC) + } catch (error) { + notifications.error("Error fetching OIDC config") + } + if (!oidcDoc?._id) { providers.oidc = { type: ConfigTypes.OIDC, config: { configs: [{ activated: true }] }, diff --git a/packages/builder/src/pages/builder/portal/manage/email/[template].svelte b/packages/builder/src/pages/builder/portal/manage/email/[template].svelte index cc00f3d798..33ecca2a10 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/[template].svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/[template].svelte @@ -36,9 +36,9 @@ try { // Save your template config await email.templates.save(selectedTemplate) - notifications.success(`Template saved.`) - } catch (err) { - notifications.error(`Failed to update template settings. ${err}`) + notifications.success("Template saved") + } catch (error) { + notifications.error("Failed to update template settings") } } diff --git a/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte b/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte index 410a7d4ff2..e371c2daae 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte @@ -1,7 +1,15 @@ diff --git a/packages/builder/src/pages/builder/portal/manage/email/index.svelte b/packages/builder/src/pages/builder/portal/manage/email/index.svelte index 5a78623b81..4ef59d2daa 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/index.svelte @@ -14,7 +14,7 @@ Checkbox, } from "@budibase/bbui" import { email } from "stores/portal" - import api from "builderStore/api" + import { API } from "api" import { cloneDeep } from "lodash/fp" import analytics, { Events } from "analytics" @@ -54,55 +54,48 @@ delete smtp.config.auth } // Save your SMTP config - const response = await api.post(`/api/global/configs`, smtp) - - if (response.status !== 200) { - const error = await response.text() - let message - try { - message = JSON.parse(error).message - } catch (err) { - message = error - } - notifications.error(`Failed to save email settings, reason: ${message}`) - } else { - const json = await response.json() - smtpConfig._rev = json._rev - smtpConfig._id = json._id - notifications.success(`Settings saved.`) + try { + const savedConfig = await API.saveConfig(smtp) + smtpConfig._rev = savedConfig._rev + smtpConfig._id = savedConfig._id + notifications.success(`Settings saved`) analytics.captureEvent(Events.SMTP.SAVED) + } catch (error) { + notifications.error( + `Failed to save email settings, reason: ${error?.message || "Unknown"}` + ) } } async function fetchSmtp() { loading = true - // fetch the configs for smtp - const smtpResponse = await api.get( - `/api/global/configs/${ConfigTypes.SMTP}` - ) - const smtpDoc = await smtpResponse.json() - - if (!smtpDoc._id) { - smtpConfig = { - type: ConfigTypes.SMTP, - config: { - secure: true, - }, + try { + // Fetch the configs for smtp + const smtpDoc = await API.getConfig(ConfigTypes.SMTP) + if (!smtpDoc._id) { + smtpConfig = { + type: ConfigTypes.SMTP, + config: { + secure: true, + }, + } + } else { + smtpConfig = smtpDoc } - } else { - smtpConfig = smtpDoc - } - loading = false - requireAuth = smtpConfig.config.auth != null - // always attach the auth for the forms purpose - - // this will be removed later if required - if (!smtpDoc.config) { - smtpDoc.config = {} - } - if (!smtpDoc.config.auth) { - smtpConfig.config.auth = { - type: "login", + loading = false + requireAuth = smtpConfig.config.auth != null + // Always attach the auth for the forms purpose - + // this will be removed later if required + if (!smtpDoc.config) { + smtpDoc.config = {} } + if (!smtpDoc.config.auth) { + smtpConfig.config.auth = { + type: "login", + } + } + } catch (error) { + notifications.error("Error fetching SMTP config") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte index 549d0e4334..a8cb340465 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte @@ -64,31 +64,43 @@ const apps = fetchData(`/api/global/roles`) async function deleteUser() { - const res = await users.delete(userId) - if (res.status === 200) { + try { + await users.delete(userId) notifications.success(`User ${$userFetch?.data?.email} deleted.`) $goto("./") - } else { - notifications.error(res?.message ? res.message : "Failed to delete user.") + } catch (error) { + notifications.error("Error deleting user") } } let toggleDisabled = false async function updateUserFirstName(evt) { - await users.save({ ...$userFetch?.data, firstName: evt.target.value }) - await userFetch.refresh() + try { + await users.save({ ...$userFetch?.data, firstName: evt.target.value }) + await userFetch.refresh() + } catch (error) { + notifications.error("Error updating user") + } } async function updateUserLastName(evt) { - await users.save({ ...$userFetch?.data, lastName: evt.target.value }) - await userFetch.refresh() + try { + await users.save({ ...$userFetch?.data, lastName: evt.target.value }) + await userFetch.refresh() + } catch (error) { + notifications.error("Error updating user") + } } async function toggleFlag(flagName, detail) { toggleDisabled = true - await users.save({ ...$userFetch?.data, [flagName]: { global: detail } }) - await userFetch.refresh() + try { + await users.save({ ...$userFetch?.data, [flagName]: { global: detail } }) + await userFetch.refresh() + } catch (error) { + notifications.error("Error updating user") + } toggleDisabled = false } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte index 25a69af1c8..0255784a7b 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte @@ -21,12 +21,12 @@ const [email, error, touched] = createValidationStore("", emailValidator) async function createUserFlow() { - const res = await users.invite({ email: $email, builder, admin }) - if (res.status) { - notifications.error(res.message) - } else { + try { + const res = await users.invite({ email: $email, builder, admin }) notifications.success(res.message) analytics.captureEvent(Events.USER.INVITE, { type: selected }) + } catch (error) { + notifications.error("Error inviting user") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte index ff958d542b..29e2d56ed0 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte @@ -16,17 +16,17 @@ admin = false async function createUser() { - const res = await users.create({ - email: $email, - password, - builder, - admin, - forceResetPassword: true, - }) - if (res.status) { - notifications.error(res.message) - } else { + try { + await users.create({ + email: $email, + password, + builder, + admin, + forceResetPassword: true, + }) notifications.success("Successfully created user") + } catch (error) { + notifications.error("Error creating user") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte index 6468498df8..a380f0aa65 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte @@ -10,16 +10,16 @@ const password = Math.random().toString(36).substr(2, 20) async function resetPassword() { - const res = await users.save({ - ...user, - password, - forceResetPassword: true, - }) - if (res.status) { - notifications.error(res.message) - } else { - notifications.success("Password reset.") + try { + await users.save({ + ...user, + password, + forceResetPassword: true, + }) + notifications.success("Password reset successfully") dispatch("update") + } catch (error) { + notifications.error("Error resetting password") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte index afa4c84f0e..5a60bfdff8 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte @@ -18,33 +18,31 @@ let selectedRole = user?.roles?.[app?._id] async function updateUserRoles() { - let res - if (selectedRole === NO_ACCESS) { - // remove the user role - const filteredRoles = { ...user.roles } - delete filteredRoles[app?._id] - res = await users.save({ - ...user, - roles: { - ...filteredRoles, - }, - }) - } else { - // add the user role - res = await users.save({ - ...user, - roles: { - ...user.roles, - [app._id]: selectedRole, - }, - }) - } - - if (res.status === 400) { - notifications.error("Failed to update role") - } else { + try { + if (selectedRole === NO_ACCESS) { + // Remove the user role + const filteredRoles = { ...user.roles } + delete filteredRoles[app?._id] + await users.save({ + ...user, + roles: { + ...filteredRoles, + }, + }) + } else { + // Add the user role + await users.save({ + ...user, + roles: { + ...user.roles, + [app._id]: selectedRole, + }, + }) + } notifications.success("Role updated") dispatch("update") + } catch (error) { + notifications.error("Failed to update role") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte index 124115a486..61192063cc 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte @@ -11,13 +11,13 @@ Label, Layout, Modal, + notifications, } from "@budibase/bbui" import TagsRenderer from "./_components/TagsTableRenderer.svelte" import AddUserModal from "./_components/AddUserModal.svelte" import BasicOnboardingModal from "./_components/BasicOnboardingModal.svelte" import { users } from "stores/portal" - - users.init() + import { onMount } from "svelte" const schema = { email: {}, @@ -47,6 +47,14 @@ createUserModal.hide() basicOnboardingModal.show() } + + onMount(async () => { + try { + await users.init() + } catch (error) { + notifications.error("Error getting user list") + } + }) diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index 6903854922..7094a0af01 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -11,7 +11,7 @@ notifications, } from "@budibase/bbui" import { auth, organisation, admin } from "stores/portal" - import { post } from "builderStore/api" + import { API } from "api" import { writable } from "svelte/store" import { redirect } from "@roxi/routify" @@ -32,42 +32,40 @@ let loading = false async function uploadLogo(file) { - let data = new FormData() - data.append("file", file) - const res = await post( - "/api/global/configs/upload/settings/logoUrl", - data, - {} - ) - return await res.json() + try { + let data = new FormData() + data.append("file", file) + await API.uploadLogo(data) + } catch (error) { + notifications.error("Error uploading logo") + } } async function saveConfig() { loading = true - // Upload logo if required - if ($values.logo && !$values.logo.url) { - await uploadLogo($values.logo) - await organisation.init() - } + try { + // Upload logo if required + if ($values.logo && !$values.logo.url) { + await uploadLogo($values.logo) + await organisation.init() + } - const config = { - company: $values.company ?? "", - platformUrl: $values.platformUrl ?? "", - } - // remove logo if required - if (!$values.logo) { - config.logoUrl = "" - } + const config = { + company: $values.company ?? "", + platformUrl: $values.platformUrl ?? "", + } - // Update settings - const res = await organisation.save(config) - if (res.status === 200) { - notifications.success("Settings saved successfully") - } else { - notifications.error(res.message) - } + // Remove logo if required + if (!$values.logo) { + config.logoUrl = "" + } + // Update settings + await organisation.save(config) + } catch (error) { + notifications.error("Error saving org config") + } loading = false } diff --git a/packages/builder/src/pages/builder/portal/settings/update.svelte b/packages/builder/src/pages/builder/portal/settings/update.svelte index 5deb724a7c..d87736144d 100644 --- a/packages/builder/src/pages/builder/portal/settings/update.svelte +++ b/packages/builder/src/pages/builder/portal/settings/update.svelte @@ -9,7 +9,7 @@ notifications, Label, } from "@budibase/bbui" - import api from "builderStore/api" + import { API } from "api" import { auth, admin } from "stores/portal" import { redirect } from "@roxi/routify" @@ -38,8 +38,12 @@ } async function getVersion() { - const response = await api.get("/api/dev/version") - version = await response.text() + try { + version = await API.getBudibaseVersion() + } catch (error) { + notifications.error("Error getting Budibase version") + version = null + } } onMount(() => { diff --git a/packages/builder/src/pages/index.svelte b/packages/builder/src/pages/index.svelte index 477097f726..c6eaba8ff1 100644 --- a/packages/builder/src/pages/index.svelte +++ b/packages/builder/src/pages/index.svelte @@ -2,10 +2,14 @@ import { redirect } from "@roxi/routify" import { auth } from "../stores/portal" import { onMount } from "svelte" + import { notifications } from "@budibase/bbui" - auth.checkQueryString() - - onMount(() => { + onMount(async () => { + try { + await auth.checkQueryString() + } catch (error) { + notifications.error("Error setting org") + } $redirect(`./builder`) }) diff --git a/packages/builder/src/stores/backend/datasources.js b/packages/builder/src/stores/backend/datasources.js index 7810c3a950..2423394c6a 100644 --- a/packages/builder/src/stores/backend/datasources.js +++ b/packages/builder/src/stores/backend/datasources.js @@ -1,6 +1,6 @@ import { writable, get } from "svelte/store" import { queries, tables, views } from "./" -import api from "../../builderStore/api" +import { API } from "api" export const INITIAL_DATASOURCE_VALUES = { list: [], @@ -13,23 +13,20 @@ export function createDatasourcesStore() { const { subscribe, update, set } = store async function updateDatasource(response) { - if (response.status !== 200) { - throw new Error(await response.text()) - } - - const { datasource, error } = await response.json() + const { datasource, error } = response update(state => { const currentIdx = state.list.findIndex(ds => ds._id === datasource._id) - const sources = state.list - if (currentIdx >= 0) { sources.splice(currentIdx, 1, datasource) } else { sources.push(datasource) } - - return { list: sources, selected: datasource._id, schemaError: error } + return { + list: sources, + selected: datasource._id, + schemaError: error, + } }) return datasource } @@ -38,25 +35,25 @@ export function createDatasourcesStore() { subscribe, update, init: async () => { - const response = await api.get(`/api/datasources`) - const json = await response.json() - set({ list: json, selected: null }) + const datasources = await API.getDatasources() + set({ + list: datasources, + selected: null, + }) }, fetch: async () => { - const response = await api.get(`/api/datasources`) - const json = await response.json() + const datasources = await API.getDatasources() // Clear selected if it no longer exists, otherwise keep it const selected = get(store).selected let nextSelected = null - if (selected && json.find(source => source._id === selected)) { + if (selected && datasources.find(source => source._id === selected)) { nextSelected = selected } - update(state => ({ ...state, list: json, selected: nextSelected })) - return json + update(state => ({ ...state, list: datasources, selected: nextSelected })) }, - select: async datasourceId => { + select: datasourceId => { update(state => ({ ...state, selected: datasourceId })) queries.unselect() tables.unselect() @@ -66,37 +63,33 @@ export function createDatasourcesStore() { update(state => ({ ...state, selected: null })) }, updateSchema: async datasource => { - let url = `/api/datasources/${datasource._id}/schema` - - const response = await api.post(url) - return updateDatasource(response) + const response = await API.buildDatasourceSchema(datasource?._id) + return await updateDatasource(response) }, save: async (body, fetchSchema = false) => { let response if (body._id) { - response = await api.put(`/api/datasources/${body._id}`, body) + response = await API.updateDatasource(body) } else { - response = await api.post("/api/datasources", { + response = await API.createDatasource({ datasource: body, fetchSchema, }) } - return updateDatasource(response) }, delete: async datasource => { - const response = await api.delete( - `/api/datasources/${datasource._id}/${datasource._rev}` - ) + await API.deleteDatasource({ + datasourceId: datasource?._id, + datasourceRev: datasource?._rev, + }) update(state => { const sources = state.list.filter( existing => existing._id !== datasource._id ) return { list: sources, selected: null } }) - await queries.fetch() - return response }, removeSchemaError: () => { update(state => { diff --git a/packages/builder/src/stores/backend/flags.js b/packages/builder/src/stores/backend/flags.js index 7e5adcd00f..449d010640 100644 --- a/packages/builder/src/stores/backend/flags.js +++ b/packages/builder/src/stores/backend/flags.js @@ -1,37 +1,27 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createFlagsStore() { const { subscribe, set } = writable({}) - return { - subscribe, + const actions = { fetch: async () => { - const { doc, response } = await getFlags() - set(doc) - return response + const flags = await API.getFlags() + set(flags) }, updateFlag: async (flag, value) => { - const response = await api.post("/api/users/flags", { + await API.updateFlag({ flag, value, }) - if (response.status === 200) { - const { doc } = await getFlags() - set(doc) - } - return response + await actions.fetch() }, } -} -async function getFlags() { - const response = await api.get("/api/users/flags") - let doc = {} - if (response.status === 200) { - doc = await response.json() + return { + subscribe, + ...actions, } - return { doc, response } } export const flags = createFlagsStore() diff --git a/packages/builder/src/stores/backend/integrations.js b/packages/builder/src/stores/backend/integrations.js index d1df818248..717b656c72 100644 --- a/packages/builder/src/stores/backend/integrations.js +++ b/packages/builder/src/stores/backend/integrations.js @@ -1,3 +1,16 @@ import { writable } from "svelte/store" +import { API } from "api" -export const integrations = writable({}) +const createIntegrationsStore = () => { + const store = writable(null) + + return { + ...store, + init: async () => { + const integrations = await API.getIntegrations() + store.set(integrations) + }, + } +} + +export const integrations = createIntegrationsStore() diff --git a/packages/builder/src/stores/backend/permissions.js b/packages/builder/src/stores/backend/permissions.js index 29159494ed..aaab406bc9 100644 --- a/packages/builder/src/stores/backend/permissions.js +++ b/packages/builder/src/stores/backend/permissions.js @@ -1,5 +1,5 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createPermissionStore() { const { subscribe } = writable([]) @@ -7,14 +7,14 @@ export function createPermissionStore() { return { subscribe, save: async ({ level, role, resource }) => { - const response = await api.post( - `/api/permission/${role}/${resource}/${level}` - ) - return await response.json() + return await API.updatePermissionForResource({ + resourceId: resource, + roleId: role, + level, + }) }, forResource: async resourceId => { - const response = await api.get(`/api/permission/${resourceId}`) - return await response.json() + return await API.getPermissionForResource(resourceId) }, } } diff --git a/packages/builder/src/stores/backend/queries.js b/packages/builder/src/stores/backend/queries.js index 2018933ffc..6e30cb21f8 100644 --- a/packages/builder/src/stores/backend/queries.js +++ b/packages/builder/src/stores/backend/queries.js @@ -1,6 +1,6 @@ import { writable, get } from "svelte/store" import { datasources, integrations, tables, views } from "./" -import api from "builderStore/api" +import { API } from "api" import { duplicateName } from "../../helpers/duplicate" const sortQueries = queryList => { @@ -15,23 +15,26 @@ export function createQueriesStore() { const actions = { init: async () => { - const response = await api.get(`/api/queries`) - const json = await response.json() - set({ list: json, selected: null }) + const queries = await API.getQueries() + set({ + list: queries, + selected: null, + }) }, fetch: async () => { - const response = await api.get(`/api/queries`) - const json = await response.json() - sortQueries(json) - update(state => ({ ...state, list: json })) - return json + const queries = await API.getQueries() + sortQueries(queries) + update(state => ({ + ...state, + list: queries, + })) }, save: async (datasourceId, query) => { const _integrations = get(integrations) const dataSource = get(datasources).list.filter( ds => ds._id === datasourceId ) - // check if readable attribute is found + // Check if readable attribute is found if (dataSource.length !== 0) { const integration = _integrations[dataSource[0].source] const readable = integration.query[query.queryVerb].readable @@ -40,34 +43,28 @@ export function createQueriesStore() { } } query.datasourceId = datasourceId - const response = await api.post(`/api/queries`, query) - if (response.status !== 200) { - throw new Error("Failed saving query.") - } - const json = await response.json() + const savedQuery = await API.saveQuery(query) update(state => { - const currentIdx = state.list.findIndex(query => query._id === json._id) - + const idx = state.list.findIndex(query => query._id === savedQuery._id) const queries = state.list - - if (currentIdx >= 0) { - queries.splice(currentIdx, 1, json) + if (idx >= 0) { + queries.splice(idx, 1, savedQuery) } else { - queries.push(json) + queries.push(savedQuery) } sortQueries(queries) - return { list: queries, selected: json._id } + return { + list: queries, + selected: savedQuery._id, + } }) - return json + return savedQuery }, - import: async body => { - const response = await api.post(`/api/queries/import`, body) - - if (response.status !== 200) { - throw new Error(response.message) - } - - return response.json() + import: async (data, datasourceId) => { + return await API.importQueries({ + datasourceId, + data, + }) }, select: query => { update(state => ({ ...state, selected: query._id })) @@ -79,48 +76,37 @@ export function createQueriesStore() { update(state => ({ ...state, selected: null })) }, preview: async query => { - const response = await api.post("/api/queries/preview", { - fields: query.fields, - queryVerb: query.queryVerb, - transformer: query.transformer, - parameters: query.parameters.reduce( - (acc, next) => ({ - ...acc, - [next.name]: next.default, - }), - {} - ), - datasourceId: query.datasourceId, - queryId: query._id || undefined, + const parameters = query.parameters.reduce( + (acc, next) => ({ + ...acc, + [next.name]: next.default, + }), + {} + ) + const result = await API.previewQuery({ + ...query, + parameters, }) - - if (response.status !== 200) { - const error = await response.text() - throw `Query error: ${error}` - } - - const json = await response.json() // Assume all the fields are strings and create a basic schema from the // unique fields returned by the server const schema = {} - for (let field of json.schemaFields) { + for (let field of result.schemaFields) { schema[field] = "string" } - return { ...json, schema, rows: json.rows || [] } + return { ...result, schema, rows: result.rows || [] } }, delete: async query => { - const response = await api.delete( - `/api/queries/${query._id}/${query._rev}` - ) + await API.deleteQuery({ + queryId: query?._id, + queryRev: query?._rev, + }) update(state => { state.list = state.list.filter(existing => existing._id !== query._id) if (state.selected === query._id) { state.selected = null } - return state }) - return response }, duplicate: async query => { let list = get(store).list diff --git a/packages/builder/src/stores/backend/roles.js b/packages/builder/src/stores/backend/roles.js index 1a1a9c04c5..0c3cdbce5a 100644 --- a/packages/builder/src/stores/backend/roles.js +++ b/packages/builder/src/stores/backend/roles.js @@ -1,30 +1,32 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createRolesStore() { const { subscribe, update, set } = writable([]) - return { - subscribe, + const actions = { fetch: async () => { - set(await getRoles()) + const roles = await API.getRoles() + set(roles) }, delete: async role => { - const response = await api.delete(`/api/roles/${role._id}/${role._rev}`) + await API.deleteRole({ + roleId: role?._id, + roleRev: role?._rev, + }) update(state => state.filter(existing => existing._id !== role._id)) - return response }, save: async role => { - const response = await api.post("/api/roles", role) - set(await getRoles()) - return response + const savedRole = await API.saveRole(role) + await actions.fetch() + return savedRole }, } + + return { + subscribe, + ...actions, + } } -async function getRoles() { - const response = await api.get("/api/roles") - return await response.json() -} - export const roles = createRolesStore() diff --git a/packages/builder/src/stores/backend/tables.js b/packages/builder/src/stores/backend/tables.js index 02db48c549..f6d20037cb 100644 --- a/packages/builder/src/stores/backend/tables.js +++ b/packages/builder/src/stores/backend/tables.js @@ -1,7 +1,7 @@ import { get, writable } from "svelte/store" import { datasources, queries, views } from "./" import { cloneDeep } from "lodash/fp" -import api from "builderStore/api" +import { API } from "api" import { SWITCHABLE_TYPES } from "../../constants/backend" export function createTablesStore() { @@ -9,10 +9,11 @@ export function createTablesStore() { const { subscribe, update, set } = store async function fetch() { - const tablesResponse = await api.get(`/api/tables`) - const tables = await tablesResponse.json() - update(state => ({ ...state, list: tables })) - return tables + const tables = await API.getTables() + update(state => ({ + ...state, + list: tables, + })) } async function select(table) { @@ -38,16 +39,16 @@ export function createTablesStore() { const oldTable = get(store).list.filter(t => t._id === table._id)[0] const fieldNames = [] - // update any renamed schema keys to reflect their names + // Update any renamed schema keys to reflect their names for (let key of Object.keys(updatedTable.schema)) { - // if field name has been seen before remove it + // If field name has been seen before remove it if (fieldNames.indexOf(key.toLowerCase()) !== -1) { delete updatedTable.schema[key] continue } const field = updatedTable.schema[key] const oldField = oldTable?.schema[key] - // if the type has changed then revert back to the old field + // If the type has changed then revert back to the old field if ( oldField != null && oldField?.type !== field.type && @@ -55,21 +56,17 @@ export function createTablesStore() { ) { updatedTable.schema[key] = oldField } - // field has been renamed + // Field has been renamed if (field.name && field.name !== key) { updatedTable.schema[field.name] = field updatedTable._rename = { old: key, updated: field.name } delete updatedTable.schema[key] } - // finally record this field has been used + // Finally record this field has been used fieldNames.push(key.toLowerCase()) } - const response = await api.post(`/api/tables`, updatedTable) - if (response.status !== 200) { - throw (await response.json()).message - } - const savedTable = await response.json() + const savedTable = await API.saveTable(updatedTable) await fetch() if (table.type === "external") { await datasources.fetch() @@ -91,21 +88,18 @@ export function createTablesStore() { }, save, init: async () => { - const response = await api.get("/api/tables") - const json = await response.json() + const tables = await API.getTables() set({ - list: json, + list: tables, selected: {}, draft: {}, }) }, delete: async table => { - const response = await api.delete( - `/api/tables/${table._id}/${table._rev}` - ) - if (response.status !== 200) { - throw (await response.json()).message - } + await API.deleteTable({ + tableId: table?._id, + tableRev: table?._rev, + }) update(state => ({ ...state, list: state.list.filter(existing => existing._id !== table._id), @@ -156,12 +150,16 @@ export function createTablesStore() { await promise } }, - deleteField: field => { + deleteField: async field => { + let promise update(state => { delete state.draft.schema[field.name] - save(state.draft) + promise = save(state.draft) return state }) + if (promise) { + await promise + } }, } } diff --git a/packages/builder/src/stores/backend/views.js b/packages/builder/src/stores/backend/views.js index 14c7bf92a4..849a66f671 100644 --- a/packages/builder/src/stores/backend/views.js +++ b/packages/builder/src/stores/backend/views.js @@ -1,6 +1,6 @@ import { writable, get } from "svelte/store" import { tables, datasources, queries } from "./" -import api from "builderStore/api" +import { API } from "api" export function createViewsStore() { const { subscribe, update } = writable({ @@ -11,7 +11,7 @@ export function createViewsStore() { return { subscribe, update, - select: async view => { + select: view => { update(state => ({ ...state, selected: view, @@ -27,16 +27,14 @@ export function createViewsStore() { })) }, delete: async view => { - await api.delete(`/api/views/${view}`) + await API.deleteView(view) await tables.fetch() }, save: async view => { - const response = await api.post(`/api/views`, view) - const json = await response.json() - + const savedView = await API.saveView(view) const viewMeta = { name: view.name, - ...json, + ...savedView, } const viewTable = get(tables).list.find( diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index d98eae8363..dc68c43cc5 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -1,5 +1,5 @@ import { writable, get } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" import { auth } from "stores/portal" export function createAdminStore() { @@ -23,64 +23,37 @@ export function createAdminStore() { const admin = writable(DEFAULT_CONFIG) async function init() { - try { - const tenantId = get(auth).tenantId - const response = await api.get( - `/api/global/configs/checklist?tenantId=${tenantId}` - ) - const json = await response.json() - const totalSteps = Object.keys(json).length - const completedSteps = Object.values(json).filter(x => x?.checked).length - - await getEnvironment() - admin.update(store => { - store.loaded = true - store.checklist = json - store.onboardingProgress = (completedSteps / totalSteps) * 100 - return store - }) - } catch (err) { - admin.update(store => { - store.checklist = null - return store - }) - } + const tenantId = get(auth).tenantId + const checklist = await API.getChecklist(tenantId) + const totalSteps = Object.keys(checklist).length + const completedSteps = Object.values(checklist).filter( + x => x?.checked + ).length + await getEnvironment() + admin.update(store => { + store.loaded = true + store.checklist = checklist + store.onboardingProgress = (completedSteps / totalSteps) * 100 + return store + }) } async function checkImportComplete() { - const response = await api.get(`/api/cloud/import/complete`) - if (response.status === 200) { - const json = await response.json() - admin.update(store => { - store.importComplete = json ? json.imported : false - return store - }) - } + const result = await API.checkImportComplete() + admin.update(store => { + store.importComplete = result ? result.imported : false + return store + }) } async function getEnvironment() { - let multiTenancyEnabled = false - let cloud = false - let disableAccountPortal = false - let accountPortalUrl = "" - let isDev = false - try { - const response = await api.get(`/api/system/environment`) - const json = await response.json() - multiTenancyEnabled = json.multiTenancy - cloud = json.cloud - disableAccountPortal = json.disableAccountPortal - accountPortalUrl = json.accountPortalUrl - isDev = json.isDev - } catch (err) { - // just let it stay disabled - } + const environment = await API.getEnvironment() admin.update(store => { - store.multiTenancy = multiTenancyEnabled - store.cloud = cloud - store.disableAccountPortal = disableAccountPortal - store.accountPortalUrl = accountPortalUrl - store.isDev = isDev + store.multiTenancy = environment.multiTenancy + store.cloud = environment.cloud + store.disableAccountPortal = environment.disableAccountPortal + store.accountPortalUrl = environment.accountPortalUrl + store.isDev = environment.isDev return store }) } diff --git a/packages/builder/src/stores/portal/apps.js b/packages/builder/src/stores/portal/apps.js index de944c057d..b8fb8c5670 100644 --- a/packages/builder/src/stores/portal/apps.js +++ b/packages/builder/src/stores/portal/apps.js @@ -1,7 +1,6 @@ import { writable } from "svelte/store" -import { get } from "builderStore/api" import { AppStatus } from "../../constants" -import api from "../../builderStore/api" +import { API } from "api" const extractAppId = id => { const split = id?.split("_") || [] @@ -12,77 +11,67 @@ export function createAppStore() { const store = writable([]) async function load() { - try { - const res = await get(`/api/applications?status=all`) - const json = await res.json() - if (res.ok && Array.isArray(json)) { - // Merge apps into one sensible list - let appMap = {} - let devApps = json.filter(app => app.status === AppStatus.DEV) - let deployedApps = json.filter(app => app.status === AppStatus.DEPLOYED) + const json = await API.getApps() + if (Array.isArray(json)) { + // Merge apps into one sensible list + let appMap = {} + let devApps = json.filter(app => app.status === AppStatus.DEV) + let deployedApps = json.filter(app => app.status === AppStatus.DEPLOYED) - // First append all dev app version - devApps.forEach(app => { - const id = extractAppId(app.appId) - appMap[id] = { - ...app, - devId: app.appId, - devRev: app._rev, - } - }) + // First append all dev app version + devApps.forEach(app => { + const id = extractAppId(app.appId) + appMap[id] = { + ...app, + devId: app.appId, + devRev: app._rev, + } + }) - // Then merge with all prod app versions - deployedApps.forEach(app => { - const id = extractAppId(app.appId) + // Then merge with all prod app versions + deployedApps.forEach(app => { + const id = extractAppId(app.appId) - // Skip any deployed apps which don't have a dev counterpart - if (!appMap[id]) { - return - } + // Skip any deployed apps which don't have a dev counterpart + if (!appMap[id]) { + return + } - appMap[id] = { - ...appMap[id], - ...app, - prodId: app.appId, - prodRev: app._rev, - } - }) + appMap[id] = { + ...appMap[id], + ...app, + prodId: app.appId, + prodRev: app._rev, + } + }) - // Transform into an array and clean up - const apps = Object.values(appMap) - apps.forEach(app => { - app.appId = extractAppId(app.devId) - delete app._id - delete app._rev - }) - store.set(apps) - } else { - store.set([]) - } - return json - } catch (error) { + // Transform into an array and clean up + const apps = Object.values(appMap) + apps.forEach(app => { + app.appId = extractAppId(app.devId) + delete app._id + delete app._rev + }) + store.set(apps) + } else { store.set([]) } } async function update(appId, value) { - console.log({ value }) - const response = await api.put(`/api/applications/${appId}`, { ...value }) - if (response.status === 200) { - store.update(state => { - const updatedAppIndex = state.findIndex( - app => app.instance._id === appId - ) - if (updatedAppIndex !== -1) { - let updatedApp = state[updatedAppIndex] - updatedApp = { ...updatedApp, ...value } - state.apps = state.splice(updatedAppIndex, 1, updatedApp) - } - return state - }) - } else { - throw new Error("Error updating name") - } + await API.saveAppMetadata({ + appId, + metadata: value, + }) + store.update(state => { + const updatedAppIndex = state.findIndex(app => app.instance._id === appId) + if (updatedAppIndex !== -1) { + let updatedApp = state[updatedAppIndex] + updatedApp = { ...updatedApp, ...value } + state.apps = state.splice(updatedAppIndex, 1, updatedApp) + } + return state + }) } return { diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index c4197a89c0..d66e901163 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -1,5 +1,5 @@ import { derived, writable, get } from "svelte/store" -import api from "../../builderStore/api" +import { API } from "api" import { admin } from "stores/portal" import analytics from "analytics" @@ -54,18 +54,25 @@ export function createAuthStore() { }) if (user) { - analytics.activate().then(() => { - analytics.identify(user._id, user) - analytics.showChat({ - email: user.email, - created_at: (user.createdAt || Date.now()) / 1000, - name: user.account?.name, - user_id: user._id, - tenant: user.tenantId, - "Company size": user.account?.size, - "Job role": user.account?.profession, + analytics + .activate() + .then(() => { + analytics.identify(user._id, user) + analytics.showChat({ + email: user.email, + created_at: (user.createdAt || Date.now()) / 1000, + name: user.account?.name, + user_id: user._id, + tenant: user.tenantId, + "Company size": user.account?.size, + "Job role": user.account?.profession, + }) + }) + .catch(() => { + // This request may fail due to browser extensions blocking requests + // containing the word analytics, so we don't want to spam users with + // an error here. }) - }) } } @@ -83,7 +90,7 @@ export function createAuthStore() { } async function setInitInfo(info) { - await api.post(`/api/global/auth/init`, info) + await API.setInitInfo(info) auth.update(store => { store.initInfo = info return store @@ -91,7 +98,7 @@ export function createAuthStore() { return info } - async function setPostLogout() { + function setPostLogout() { auth.update(store => { store.postLogout = true return store @@ -99,13 +106,12 @@ export function createAuthStore() { } async function getInitInfo() { - const response = await api.get(`/api/global/auth/init`) - const json = response.json() + const info = await API.getInitInfo() auth.update(store => { - store.initInfo = json + store.initInfo = info return store }) - return json + return info } const actions = { @@ -120,76 +126,51 @@ export function createAuthStore() { await setOrganisation(tenantId) }, getSelf: async () => { - const response = await api.get("/api/global/users/self") - if (response.status !== 200) { + // We need to catch this locally as we never want this to fail, even + // though normally we never want to swallow API errors at the store level. + // We're either logged in or we aren't. + // We also need to always update the loaded flag. + try { + const user = await API.fetchBuilderSelf() + setUser(user) + } catch (error) { setUser(null) - } else { - const json = await response.json() - setUser(json) } }, login: async creds => { const tenantId = get(store).tenantId - const response = await api.post( - `/api/global/auth/${tenantId}/login`, - creds - ) - if (response.status === 200) { - await actions.getSelf() - } else { - const json = await response.json() - throw new Error(json.message ? json.message : "Invalid credentials") - } + await API.logIn({ + username: creds.username, + password: creds.password, + tenantId, + }) + await actions.getSelf() }, logout: async () => { - const response = await api.post(`/api/global/auth/logout`) - if (response.status !== 200) { - throw "Unable to create logout" - } - await response.json() - await setInitInfo({}) setUser(null) setPostLogout() + await API.logOut() + await setInitInfo({}) }, updateSelf: async fields => { const newUser = { ...get(auth).user, ...fields } - const response = await api.post("/api/global/users/self", newUser) - if (response.status === 200) { - setUser(newUser) - } else { - throw "Unable to update user details" - } + await API.updateSelf(newUser) + setUser(newUser) }, forgotPassword: async email => { const tenantId = get(store).tenantId - const response = await api.post(`/api/global/auth/${tenantId}/reset`, { + await API.requestForgotPassword({ + tenantId, email, }) - if (response.status !== 200) { - throw "Unable to send email with reset link" - } - await response.json() }, - resetPassword: async (password, code) => { + resetPassword: async (password, resetCode) => { const tenantId = get(store).tenantId - const response = await api.post( - `/api/global/auth/${tenantId}/reset/update`, - { - password, - resetCode: code, - } - ) - if (response.status !== 200) { - throw "Unable to reset password" - } - await response.json() - }, - createUser: async user => { - const response = await api.post(`/api/global/users`, user) - if (response.status !== 200) { - throw "Unable to create user" - } - await response.json() + await API.resetPassword({ + tenantId, + password, + resetCode, + }) }, } diff --git a/packages/builder/src/stores/portal/email.js b/packages/builder/src/stores/portal/email.js index a015480141..2e222d34c4 100644 --- a/packages/builder/src/stores/portal/email.js +++ b/packages/builder/src/stores/portal/email.js @@ -1,5 +1,5 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createEmailStore() { const store = writable({}) @@ -8,14 +8,9 @@ export function createEmailStore() { subscribe: store.subscribe, templates: { fetch: async () => { - // fetch the email template definitions - const response = await api.get(`/api/global/template/definitions`) - const definitions = await response.json() - - // fetch the email templates themselves - const templatesResponse = await api.get(`/api/global/template/email`) - const templates = await templatesResponse.json() - + // Fetch the email template definitions and templates + const definitions = await API.getEmailTemplateDefinitions() + const templates = await API.getEmailTemplates() store.set({ definitions, templates, @@ -23,15 +18,12 @@ export function createEmailStore() { }, save: async template => { // Save your template config - const response = await api.post(`/api/global/template`, template) - const json = await response.json() - if (response.status !== 200) throw new Error(json.message) - template._rev = json._rev - template._id = json._id - + const savedTemplate = await API.saveEmailTemplate(template) + template._rev = savedTemplate._rev + template._id = savedTemplate._id store.update(state => { const currentIdx = state.templates.findIndex( - template => template.purpose === json.purpose + template => template.purpose === savedTemplate.purpose ) state.templates.splice(currentIdx, 1, template) return state diff --git a/packages/builder/src/stores/portal/oidc.js b/packages/builder/src/stores/portal/oidc.js index 3e3a7048ca..3a4b954753 100644 --- a/packages/builder/src/stores/portal/oidc.js +++ b/packages/builder/src/stores/portal/oidc.js @@ -1,5 +1,5 @@ import { writable, get } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" import { auth } from "stores/portal" const OIDC_CONFIG = { @@ -11,26 +11,20 @@ const OIDC_CONFIG = { export function createOidcStore() { const store = writable(OIDC_CONFIG) const { set, subscribe } = store - - async function init() { - const tenantId = get(auth).tenantId - const res = await api.get( - `/api/global/configs/public/oidc?tenantId=${tenantId}` - ) - const json = await res.json() - - if (json.status === 400 || Object.keys(json).length === 0) { - set(OIDC_CONFIG) - } else { - // Just use the first config for now. We will be support multiple logins buttons later on. - set(...json) - } - } - return { subscribe, set, - init, + init: async () => { + const tenantId = get(auth).tenantId + const config = await API.getOIDCConfig(tenantId) + if (Object.keys(config || {}).length) { + // Just use the first config for now. + // We will be support multiple logins buttons later on. + set(...config) + } else { + set(OIDC_CONFIG) + } + }, } } diff --git a/packages/builder/src/stores/portal/organisation.js b/packages/builder/src/stores/portal/organisation.js index 21a110c54a..9709578fa2 100644 --- a/packages/builder/src/stores/portal/organisation.js +++ b/packages/builder/src/stores/portal/organisation.js @@ -1,5 +1,5 @@ import { writable, get } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" import { auth } from "stores/portal" const DEFAULT_CONFIG = { @@ -19,35 +19,23 @@ export function createOrganisationStore() { async function init() { const tenantId = get(auth).tenantId - const res = await api.get(`/api/global/configs/public?tenantId=${tenantId}`) - const json = await res.json() - - if (json.status === 400) { - set(DEFAULT_CONFIG) - } else { - set({ ...DEFAULT_CONFIG, ...json.config, _rev: json._rev }) - } + const tenant = await API.getTenantConfig(tenantId) + set({ ...DEFAULT_CONFIG, ...tenant.config, _rev: tenant._rev }) } async function save(config) { - // delete non-persisted fields + // Delete non-persisted fields const storeConfig = get(store) delete storeConfig.oidc delete storeConfig.google delete storeConfig.oidcCallbackUrl delete storeConfig.googleCallbackUrl - - const res = await api.post("/api/global/configs", { + await API.saveConfig({ type: "settings", config: { ...get(store), ...config }, _rev: get(store)._rev, }) - const json = await res.json() - if (json.status) { - return json - } await init() - return { status: 200 } } return { diff --git a/packages/builder/src/stores/portal/templates.js b/packages/builder/src/stores/portal/templates.js index b82ecd70e2..904e9cfa8e 100644 --- a/packages/builder/src/stores/portal/templates.js +++ b/packages/builder/src/stores/portal/templates.js @@ -1,18 +1,15 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function templatesStore() { const { subscribe, set } = writable([]) - async function load() { - const response = await api.get("/api/templates?type=app") - const json = await response.json() - set(json) - } - return { subscribe, - load, + load: async () => { + const templates = await API.getAppTemplates() + set(templates) + }, } } diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index 9a3df120e0..cebf03d4c0 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -1,38 +1,28 @@ import { writable } from "svelte/store" -import api, { post } from "builderStore/api" +import { API } from "api" import { update } from "lodash" export function createUsersStore() { const { subscribe, set } = writable([]) async function init() { - const response = await api.get(`/api/global/users`) - const json = await response.json() - set(json) + const users = await API.getUsers() + set(users) } async function invite({ email, builder, admin }) { - const body = { email, userInfo: {} } - if (admin) { - body.userInfo.admin = { - global: true, - } - } - if (builder) { - body.userInfo.builder = { - global: true, - } - } - const response = await api.post(`/api/global/users/invite`, body) - return await response.json() + await API.inviteUser({ + email, + builder, + admin, + }) } async function acceptInvite(inviteCode, password) { - const response = await api.post("/api/global/users/invite/accept", { + await API.acceptInvite({ inviteCode, password, }) - return await response.json() } async function create({ @@ -56,29 +46,17 @@ export function createUsersStore() { if (admin) { body.admin = { global: true } } - const response = await api.post("/api/global/users", body) + await API.saveUser(body) await init() - return await response.json() } async function del(id) { - const response = await api.delete(`/api/global/users/${id}`) + await API.deleteUser(id) update(users => users.filter(user => user._id !== id)) - const json = await response.json() - return { - ...json, - status: response.status, - } } async function save(data) { - try { - const res = await post(`/api/global/users`, data) - return await res.json() - } catch (error) { - console.log(error) - return error - } + await API.saveUser(data) } return { diff --git a/packages/builder/vite.config.js b/packages/builder/vite.config.js index d66d677555..b68d265bc5 100644 --- a/packages/builder/vite.config.js +++ b/packages/builder/vite.config.js @@ -56,6 +56,10 @@ export default ({ mode }) => { find: "stores", replacement: path.resolve("./src/stores"), }, + { + find: "api", + replacement: path.resolve("./src/api.js"), + }, { find: "constants", replacement: path.resolve("./src/constants"), diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index c47f898f26..618aa6638a 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -1,6 +1,5 @@ node_modules/ docker-compose.yaml -envoy.yaml -hosting.properties +nginx.conf build/ docker-error.log diff --git a/packages/cli/package.json b/packages/cli/package.json index 17360baeb1..2ff8dfeae6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.0.57", + "version": "1.0.58-alpha.0", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/cli/src/hosting/index.js b/packages/cli/src/hosting/index.js index 05d221435c..ecf3b710b2 100644 --- a/packages/cli/src/hosting/index.js +++ b/packages/cli/src/hosting/index.js @@ -19,7 +19,6 @@ const BUDIBASE_SERVICES = ["app-service", "worker-service"] const ERROR_FILE = "docker-error.log" const FILE_URLS = [ "https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml", - "https://raw.githubusercontent.com/Budibase/budibase/master/hosting/envoy.yaml", ] const DO_USER_DATA_URL = "http://169.254.169.254/metadata/v1/user-data" @@ -141,11 +140,7 @@ async function stop() { async function update() { await checkDockerConfigured() checkInitComplete() - if ( - await confirmation( - "Do you wish to update you docker-compose.yaml and envoy.yaml?" - ) - ) { + if (await confirmation("Do you wish to update you docker-compose.yaml?")) { await downloadFiles() } await handleError(async () => { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index ab48142ad5..06dbaad660 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -2453,6 +2453,12 @@ "key": "enableTime", "defaultValue": true }, + { + "type": "boolean", + "label": "Time Only", + "key": "timeOnly", + "defaultValue": false + }, { "type": "text", "label": "Default value", @@ -2940,7 +2946,7 @@ "settings": [ { "type": "number", - "label": "Row Count", + "label": "Scroll Limit", "key": "rowCount", "defaultValue": 8 }, diff --git a/packages/client/package.json b/packages/client/package.json index d3095ea74c..a584a82833 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.0.57", + "version": "1.0.58-alpha.0", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^1.0.57", - "@budibase/standard-components": "^0.9.139", - "@budibase/string-templates": "^1.0.57", + "@budibase/bbui": "^1.0.58-alpha.0", + "@budibase/frontend-core": "^1.0.58-alpha.0", + "@budibase/string-templates": "^1.0.58-alpha.0", "regexparam": "^1.3.0", "rollup-plugin-polyfill-node": "^0.8.0", "shortid": "^2.2.15", diff --git a/packages/client/rollup.config.js b/packages/client/rollup.config.js index bde9d2325f..1aee91df42 100644 --- a/packages/client/rollup.config.js +++ b/packages/client/rollup.config.js @@ -57,10 +57,6 @@ export default { find: "sdk", replacement: path.resolve("./src/sdk"), }, - { - find: "builder", - replacement: path.resolve("../builder"), - }, ], }), svelte({ diff --git a/packages/client/src/api/analytics.js b/packages/client/src/api/analytics.js deleted file mode 100644 index 5a089eaa21..0000000000 --- a/packages/client/src/api/analytics.js +++ /dev/null @@ -1,10 +0,0 @@ -import API from "./api" - -/** - * Notifies that an end user client app has been loaded. - */ -export const pingEndUser = async () => { - return await API.post({ - url: `/api/analytics/ping`, - }) -} diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.js index 1bb12cca53..591d4a6782 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.js @@ -1,110 +1,50 @@ -import { notificationStore, authStore } from "stores" +import { createAPIClient } from "@budibase/frontend-core" +import { notificationStore, authStore } from "../stores" import { get } from "svelte/store" -import { ApiVersion } from "constants" -/** - * API cache for cached request responses. - */ -let cache = {} +export const API = createAPIClient({ + // Enable caching of cacheable endpoints to speed things up, + enableCaching: true, -/** - * Handler for API errors. - */ -const handleError = error => { - return { error } -} + // Attach client specific headers + attachHeaders: headers => { + // Attach app ID header + headers["x-budibase-app-id"] = window["##BUDIBASE_APP_ID##"] -/** - * Performs an API call to the server. - * App ID header is always correctly set. - */ -const makeApiCall = async ({ method, url, body, json = true }) => { - try { - const requestBody = json ? JSON.stringify(body) : body - const inBuilder = window["##BUDIBASE_IN_BUILDER##"] - const headers = { - Accept: "application/json", - "x-budibase-app-id": window["##BUDIBASE_APP_ID##"], - "x-budibase-api-version": ApiVersion, - ...(json && { "Content-Type": "application/json" }), - ...(!inBuilder && { "x-budibase-type": "client" }), + // Attach client header if not inside the builder preview + if (!window["##BUDIBASE_IN_BUILDER##"]) { + headers["x-budibase-type"] = "client" } - // add csrf token if authenticated + // Add csrf token if authenticated const auth = get(authStore) - if (auth && auth.csrfToken) { + if (auth?.csrfToken) { headers["x-csrf-token"] = auth.csrfToken } + }, - const response = await fetch(url, { - method, - headers, - body: requestBody, - credentials: "same-origin", - }) - switch (response.status) { - case 200: - try { - return await response.json() - } catch (error) { - return null - } - case 401: - notificationStore.actions.error("Invalid credentials") - return handleError(`Invalid credentials`) - case 404: - notificationStore.actions.warning("Not found") - return handleError(`${url}: Not Found`) - case 400: - return handleError(`${url}: Bad Request`) - case 403: - notificationStore.actions.error( - "Your session has expired, or you don't have permission to access that data" - ) - return handleError(`${url}: Forbidden`) - default: - if (response.status >= 200 && response.status < 400) { - return response.json() - } - return handleError(`${url} - ${response.statusText}`) + // Show an error notification for all API failures. + // We could also log these to sentry. + // Or we could check error.status and redirect to login on a 403 etc. + onError: error => { + const { status, method, url, message, handled } = error || {} + + // Log any errors that we haven't manually handled + if (!handled) { + console.error("Unhandled error from API client", error) + return } - } catch (error) { - return handleError(error) - } -} -/** - * Performs an API call to the server and caches the response. - * Future invocation for this URL will return the cached result instead of - * hitting the server again. - */ -const makeCachedApiCall = async params => { - const identifier = params.url - if (!identifier) { - return null - } - if (!cache[identifier]) { - cache[identifier] = makeApiCall(params) - cache[identifier] = await cache[identifier] - } - return await cache[identifier] -} + // Notify all errors + if (message) { + // Don't notify if the URL contains the word analytics as it may be + // blocked by browser extensions + if (!url?.includes("analytics")) { + notificationStore.actions.error(message) + } + } -/** - * Constructs an API call function for a particular HTTP method. - */ -const requestApiCall = method => async params => { - const { external = false, url, cache = false } = params - const fixedUrl = external ? url : `/${url}`.replace("//", "/") - const enrichedParams = { ...params, method, url: fixedUrl } - return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams) -} - -export default { - post: requestApiCall("POST"), - put: requestApiCall("PUT"), - get: requestApiCall("GET"), - patch: requestApiCall("PATCH"), - del: requestApiCall("DELETE"), - error: handleError, -} + // Log all errors to console + console.warn(`[Client] HTTP ${status} on ${method}:${url}\n\t${message}`) + }, +}) diff --git a/packages/client/src/api/app.js b/packages/client/src/api/app.js deleted file mode 100644 index c5ee305cda..0000000000 --- a/packages/client/src/api/app.js +++ /dev/null @@ -1,10 +0,0 @@ -import API from "./api" - -/** - * Fetches screen definition for an app. - */ -export const fetchAppPackage = async appId => { - return await API.get({ - url: `/api/applications/${appId}/appPackage`, - }) -} diff --git a/packages/client/src/api/attachments.js b/packages/client/src/api/attachments.js deleted file mode 100644 index ed9c6fe522..0000000000 --- a/packages/client/src/api/attachments.js +++ /dev/null @@ -1,50 +0,0 @@ -import API from "./api" - -/** - * Uploads an attachment to the server. - */ -export const uploadAttachment = async (data, tableId = "") => { - return await API.post({ - url: `/api/attachments/${tableId}/upload`, - body: data, - json: false, - }) -} - -/** - * Generates a signed URL to upload a file to an external datasource. - */ -export const getSignedDatasourceURL = async (datasourceId, bucket, key) => { - if (!datasourceId) { - return null - } - const res = await API.post({ - url: `/api/attachments/${datasourceId}/url`, - body: { bucket, key }, - }) - if (res.error) { - throw "Could not generate signed upload URL" - } - return res -} - -/** - * Uploads a file to an external datasource. - */ -export const externalUpload = async (datasourceId, bucket, key, data) => { - const { signedUrl, publicUrl } = await getSignedDatasourceURL( - datasourceId, - bucket, - key - ) - const res = await API.put({ - url: signedUrl, - body: data, - json: false, - external: true, - }) - if (res?.error) { - throw "Could not upload file to signed URL" - } - return { publicUrl } -} diff --git a/packages/client/src/api/auth.js b/packages/client/src/api/auth.js deleted file mode 100644 index 9ac09f5571..0000000000 --- a/packages/client/src/api/auth.js +++ /dev/null @@ -1,45 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" -import { TableNames } from "../constants" - -/** - * Performs a log in request. - */ -export const logIn = async ({ email, password }) => { - if (!email) { - return API.error("Please enter your email") - } - if (!password) { - return API.error("Please enter your password") - } - return await API.post({ - url: "/api/global/auth", - body: { username: email, password }, - }) -} - -/** - * Logs the user out and invaidates their session. - */ -export const logOut = async () => { - return await API.post({ - url: "/api/global/auth/logout", - }) -} - -/** - * Fetches the currently logged in user object - */ -export const fetchSelf = async () => { - const user = await API.get({ url: "/api/self" }) - if (user && user._id) { - if (user.roleId === "PUBLIC") { - // Don't try to enrich a public user as it will 403 - return user - } else { - return (await enrichRows([user], TableNames.USERS))[0] - } - } else { - return null - } -} diff --git a/packages/client/src/api/automations.js b/packages/client/src/api/automations.js deleted file mode 100644 index cb3e4623ad..0000000000 --- a/packages/client/src/api/automations.js +++ /dev/null @@ -1,16 +0,0 @@ -import { notificationStore } from "stores/notification" -import API from "./api" - -/** - * Executes an automation. Must have "App Action" trigger. - */ -export const triggerAutomation = async (automationId, fields) => { - const res = await API.post({ - url: `/api/automations/${automationId}/trigger`, - body: { fields }, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Automation triggered") - return res -} diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index d429eb437c..5eb6b2b6f4 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -1,11 +1,9 @@ -export * from "./rows" -export * from "./auth" -export * from "./tables" -export * from "./attachments" -export * from "./views" -export * from "./relationships" -export * from "./routes" -export * from "./queries" -export * from "./app" -export * from "./automations" -export * from "./analytics" +import { API } from "./api.js" +import { patchAPI } from "./patches.js" + +// Certain endpoints which return rows need patched so that they transform +// and enrich the row docs, so that they can be correctly handled by the +// client library +patchAPI(API) + +export { API } diff --git a/packages/client/src/api/patches.js b/packages/client/src/api/patches.js new file mode 100644 index 0000000000..faad9c81ec --- /dev/null +++ b/packages/client/src/api/patches.js @@ -0,0 +1,107 @@ +import { Constants } from "@budibase/frontend-core" +import { FieldTypes } from "../constants" + +export const patchAPI = API => { + /** + * Enriches rows which contain certain field types so that they can + * be properly displayed. + * The ability to create these bindings has been removed, but they will still + * exist in client apps to support backwards compatibility. + */ + const enrichRows = async (rows, tableId) => { + if (!Array.isArray(rows)) { + return [] + } + if (rows.length) { + const tables = {} + for (let row of rows) { + // Fall back to passed in tableId if row doesn't have it specified + let rowTableId = row.tableId || tableId + let table = tables[rowTableId] + if (!table) { + // Fetch table schema so we can check column types + table = await API.fetchTableDefinition(rowTableId) + tables[rowTableId] = table + } + const schema = table?.schema + if (schema) { + const keys = Object.keys(schema) + for (let key of keys) { + const type = schema[key].type + if (type === FieldTypes.LINK && Array.isArray(row[key])) { + // Enrich row a string join of relationship fields + row[`${key}_text`] = + row[key] + ?.map(option => option?.primaryDisplay) + .filter(option => !!option) + .join(", ") || "" + } else if (type === "attachment") { + // Enrich row with the first image URL for any attachment fields + let url = null + if (Array.isArray(row[key]) && row[key][0] != null) { + url = row[key][0].url + } + row[`${key}_first`] = url + } + } + } + } + } + return rows + } + + // Enrich rows so they properly handle client bindings + const fetchSelf = API.fetchSelf + API.fetchSelf = async () => { + const user = await fetchSelf() + if (user && user._id) { + if (user.roleId === "PUBLIC") { + // Don't try to enrich a public user as it will 403 + return user + } else { + return (await enrichRows([user], Constants.TableNames.USERS))[0] + } + } else { + return null + } + } + const fetchRelationshipData = API.fetchRelationshipData + API.fetchRelationshipData = async params => { + const tableId = params?.tableId + const rows = await fetchRelationshipData(params) + return await enrichRows(rows, tableId) + } + const fetchTableData = API.fetchTableData + API.fetchTableData = async tableId => { + const rows = await fetchTableData(tableId) + return await enrichRows(rows, tableId) + } + const searchTable = API.searchTable + API.searchTable = async params => { + const tableId = params?.tableId + const output = await searchTable(params) + return { + ...output, + rows: await enrichRows(output?.rows, tableId), + } + } + const fetchViewData = API.fetchViewData + API.fetchViewData = async params => { + const tableId = params?.tableId + const rows = await fetchViewData(params) + return await enrichRows(rows, tableId) + } + + // Wipe any HBS formulae from table definitions, as these interfere with + // handlebars enrichment + const fetchTableDefinition = API.fetchTableDefinition + API.fetchTableDefinition = async tableId => { + const definition = await fetchTableDefinition(tableId) + Object.keys(definition?.schema || {}).forEach(field => { + if (definition.schema[field]?.type === "formula") { + delete definition.schema[field].formula + } + }) + return definition + } +} diff --git a/packages/client/src/api/queries.js b/packages/client/src/api/queries.js deleted file mode 100644 index e8972f657e..0000000000 --- a/packages/client/src/api/queries.js +++ /dev/null @@ -1,34 +0,0 @@ -import { notificationStore, dataSourceStore } from "stores" -import API from "./api" - -/** - * Executes a query against an external data connector. - */ -export const executeQuery = async ({ queryId, pagination, parameters }) => { - const query = await fetchQueryDefinition(queryId) - if (query?.datasourceId == null) { - notificationStore.actions.error("That query couldn't be found") - return - } - const res = await API.post({ - url: `/api/v2/queries/${queryId}`, - body: { - parameters, - pagination, - }, - }) - if (res.error) { - notificationStore.actions.error("An error has occurred") - } else if (!query.readable) { - notificationStore.actions.success("Query executed successfully") - await dataSourceStore.actions.invalidateDataSource(query.datasourceId) - } - return res -} - -/** - * Fetches the definition of an external query. - */ -export const fetchQueryDefinition = async queryId => { - return await API.get({ url: `/api/queries/${queryId}`, cache: true }) -} diff --git a/packages/client/src/api/relationships.js b/packages/client/src/api/relationships.js deleted file mode 100644 index fe92bfd038..0000000000 --- a/packages/client/src/api/relationships.js +++ /dev/null @@ -1,14 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" - -/** - * Fetches related rows for a certain field of a certain row. - */ -export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => { - if (!tableId || !rowId || !fieldName) { - return [] - } - const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` }) - const rows = response[fieldName] || [] - return await enrichRows(rows, tableId) -} diff --git a/packages/client/src/api/routes.js b/packages/client/src/api/routes.js deleted file mode 100644 index d762461075..0000000000 --- a/packages/client/src/api/routes.js +++ /dev/null @@ -1,10 +0,0 @@ -import API from "./api" - -/** - * Fetches available routes for the client app. - */ -export const fetchRoutes = async () => { - return await API.get({ - url: `/api/routing/client`, - }) -} diff --git a/packages/client/src/api/rows.js b/packages/client/src/api/rows.js deleted file mode 100644 index 2d6df90e83..0000000000 --- a/packages/client/src/api/rows.js +++ /dev/null @@ -1,155 +0,0 @@ -import { notificationStore, dataSourceStore } from "stores" -import API from "./api" -import { fetchTableDefinition } from "./tables" -import { FieldTypes } from "../constants" - -/** - * Fetches data about a certain row in a table. - */ -export const fetchRow = async ({ tableId, rowId }) => { - if (!tableId || !rowId) { - return - } - const row = await API.get({ - url: `/api/${tableId}/rows/${rowId}`, - }) - return (await enrichRows([row], tableId))[0] -} - -/** - * Creates a row in a table. - */ -export const saveRow = async row => { - if (!row?.tableId) { - return - } - const res = await API.post({ - url: `/api/${row.tableId}/rows`, - body: row, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Row saved") - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(row.tableId) - - return res -} - -/** - * Updates a row in a table. - */ -export const updateRow = async row => { - if (!row?.tableId || !row?._id) { - return - } - const res = await API.patch({ - url: `/api/${row.tableId}/rows`, - body: row, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Row updated") - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(row.tableId) - - return res -} - -/** - * Deletes a row from a table. - */ -export const deleteRow = async ({ tableId, rowId, revId }) => { - if (!tableId || !rowId || !revId) { - return - } - const res = await API.del({ - url: `/api/${tableId}/rows`, - body: { - _id: rowId, - _rev: revId, - }, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Row deleted") - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(tableId) - - return res -} - -/** - * Deletes many rows from a table. - */ -export const deleteRows = async ({ tableId, rows }) => { - if (!tableId || !rows) { - return - } - const res = await API.del({ - url: `/api/${tableId}/rows`, - body: { - rows, - }, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success(`${rows.length} row(s) deleted`) - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(tableId) - - return res -} - -/** - * Enriches rows which contain certain field types so that they can - * be properly displayed. - * The ability to create these bindings has been removed, but they will still - * exist in client apps to support backwards compatibility. - */ -export const enrichRows = async (rows, tableId) => { - if (!Array.isArray(rows)) { - return [] - } - if (rows.length) { - // map of tables, incase a row being loaded is not from the same table - const tables = {} - for (let row of rows) { - // fallback to passed in tableId if row doesn't have it specified - let rowTableId = row.tableId || tableId - let table = tables[rowTableId] - if (!table) { - // Fetch table schema so we can check column types - table = await fetchTableDefinition(rowTableId) - tables[rowTableId] = table - } - const schema = table?.schema - if (schema) { - const keys = Object.keys(schema) - for (let key of keys) { - const type = schema[key].type - if (type === FieldTypes.LINK && Array.isArray(row[key])) { - // Enrich row a string join of relationship fields - row[`${key}_text`] = - row[key] - ?.map(option => option?.primaryDisplay) - .filter(option => !!option) - .join(", ") || "" - } else if (type === "attachment") { - // Enrich row with the first image URL for any attachment fields - let url = null - if (Array.isArray(row[key]) && row[key][0] != null) { - url = row[key][0].url - } - row[`${key}_first`] = url - } - } - } - } - } - return rows -} diff --git a/packages/client/src/api/tables.js b/packages/client/src/api/tables.js deleted file mode 100644 index 09f77de6ee..0000000000 --- a/packages/client/src/api/tables.js +++ /dev/null @@ -1,63 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" - -/** - * Fetches a table definition. - * Since definitions cannot change at runtime, the result is cached. - */ -export const fetchTableDefinition = async tableId => { - const res = await API.get({ url: `/api/tables/${tableId}`, cache: true }) - - // Wipe any HBS formulae, as these interfere with handlebars enrichment - Object.keys(res?.schema || {}).forEach(field => { - if (res.schema[field]?.type === "formula") { - delete res.schema[field].formula - } - }) - - return res -} - -/** - * Fetches all rows from a table. - */ -export const fetchTableData = async tableId => { - const rows = await API.get({ url: `/api/${tableId}/rows` }) - return await enrichRows(rows, tableId) -} - -/** - * Searches a table using Lucene. - */ -export const searchTable = async ({ - tableId, - query, - bookmark, - limit, - sort, - sortOrder, - sortType, - paginate, -}) => { - if (!tableId || !query) { - return { - rows: [], - } - } - const res = await API.post({ - url: `/api/${tableId}/search`, - body: { - query, - bookmark, - limit, - sort, - sortOrder, - sortType, - paginate, - }, - }) - return { - ...res, - rows: await enrichRows(res?.rows, tableId), - } -} diff --git a/packages/client/src/api/views.js b/packages/client/src/api/views.js deleted file mode 100644 index d173e53d53..0000000000 --- a/packages/client/src/api/views.js +++ /dev/null @@ -1,30 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" - -/** - * Fetches all rows in a view. - */ -export const fetchViewData = async ({ - name, - field, - groupBy, - calculation, - tableId, -}) => { - const params = new URLSearchParams() - - if (calculation) { - params.set("field", field) - params.set("calculation", calculation) - } - if (groupBy) { - params.set("group", groupBy ? "true" : "false") - } - - const QUERY_VIEW_URL = field - ? `/api/views/${name}?${params}` - : `/api/views/${name}` - - const rows = await API.get({ url: QUERY_VIEW_URL }) - return await enrichRows(rows, tableId) -} diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index 7f5bed210e..5bd5d2d46f 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -2,6 +2,8 @@ import { writable, get } from "svelte/store" import { setContext, onMount } from "svelte" import { Layout, Heading, Body } from "@budibase/bbui" + import ErrorSVG from "@budibase/frontend-core/assets/error.svg" + import { Constants, CookieUtils } from "@budibase/frontend-core" import Component from "./Component.svelte" import SDK from "sdk" import { @@ -24,7 +26,6 @@ import HoverIndicator from "components/preview/HoverIndicator.svelte" import CustomThemeWrapper from "./CustomThemeWrapper.svelte" import DNDHandler from "components/preview/DNDHandler.svelte" - import ErrorSVG from "builder/assets/error.svg" import KeyboardManager from "components/preview/KeyboardManager.svelte" // Provide contexts @@ -63,9 +64,8 @@ } else { // The user is not logged in, redirect them to login const returnUrl = `${window.location.pathname}${window.location.hash}` - // TODO: reuse `Cookies` from builder when frontend-core is added - window.document.cookie = `budibase:returnurl=${returnUrl}; Path=/` - window.location = `/builder/auth/login` + CookieUtils.setCookie(Constants.Cookies.ReturnUrl, returnUrl) + window.location = "/builder/auth/login" } } } diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 8cd1849336..f43c2b30ec 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -9,7 +9,7 @@ import Router from "./Router.svelte" import { enrichProps, propsAreSame } from "utils/componentProps" import { builderStore } from "stores" - import { hashString } from "utils/helpers" + import { Helpers } from "@budibase/bbui" import Manifest from "manifest.json" import { getActiveConditions, reduceConditionActions } from "utils/conditions" import Placeholder from "components/app/Placeholder.svelte" @@ -106,7 +106,7 @@ // Raw settings are all settings excluding internal props and children $: rawSettings = getRawSettings(instance) - $: instanceKey = hashString(JSON.stringify(rawSettings)) + $: instanceKey = Helpers.hashString(JSON.stringify(rawSettings)) // Update and enrich component settings $: updateSettings(rawSettings, instanceKey, settingsDefinition, $context) @@ -118,9 +118,6 @@ // Build up the final settings object to be passed to the component $: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings) - // Render key is used to determine when components need to fully remount - $: renderKey = getRenderKey(id, editing) - // Update component context $: componentStore.set({ id, @@ -276,8 +273,7 @@ // reactive statements as much as possible. const cacheSettings = (enriched, nested, conditional) => { const allSettings = { ...enriched, ...nested, ...conditional } - const mounted = ref?.$$set != null - if (!cachedSettings || !mounted) { + if (!cachedSettings) { cachedSettings = { ...allSettings } initialSettings = cachedSettings } else { @@ -290,51 +286,54 @@ // setting it on initialSettings directly, we avoid a double render. cachedSettings[key] = allSettings[key] - // Programmatically set the prop to avoid svelte reactive statements - // firing inside components. This circumvents the problems caused by - // spreading a props object. - ref.$$set({ [key]: allSettings[key] }) + if (ref?.$$set) { + // Programmatically set the prop to avoid svelte reactive statements + // firing inside components. This circumvents the problems caused by + // spreading a props object. + ref.$$set({ [key]: allSettings[key] }) + } else { + // Sometimes enrichment can occur multiple times before the + // component has mounted and been assigned a ref. + // In these cases, for some reason we need to update the + // initial settings object, even though it is equivalent by + // reference to cached settings. This solves the problem of multiple + // initial enrichments, while also not causing wasted renders for + // any components not affected by this issue. + initialSettings[key] = allSettings[key] + } } }) } } - - // Generates a key used to determine when components need to fully remount. - // Currently only toggling editing requires remounting. - const getRenderKey = (id, editing) => { - return hashString(`${id}-${editing}`) - } -{#key renderKey} - {#if constructor && initialSettings && (visible || inSelectedPath)} - - - - - {#if children.length} - {#each children as child (child._id)} - - {/each} - {:else if emptyState} - - {:else if isBlock} - - {/if} - - - {/if} -{/key} +{#if constructor && initialSettings && (visible || inSelectedPath)} + + + + + {#if children.length} + {#each children as child (child._id)} + + {/each} + {:else if emptyState} + + {:else if isBlock} + + {/if} + + +{/if}