Merge remote-tracking branch 'origin/develop' into fix/copy-id-and-rev
This commit is contained in:
commit
e28fde815c
|
@ -9,7 +9,7 @@ env:
|
|||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN_SELF_HOST }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: Budibase Release
|
||||
name: Budibase Release Selfhost
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -7,7 +7,7 @@ env:
|
|||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN_SELF_HOST }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
@ -28,12 +28,7 @@ jobs:
|
|||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: 'Get Previous tag'
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
|
||||
- name: Build/release Docker images (Self Host)
|
||||
if: ${{ github.event.inputs.release_self_host == 'Y' }}
|
||||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
yarn build
|
||||
|
@ -41,7 +36,7 @@ jobs:
|
|||
env:
|
||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
||||
BUDIBASE_RELEASE_VERSION: latest
|
||||
|
||||
- uses: azure/setup-helm@v1
|
||||
id: install
|
||||
|
|
Binary file not shown.
|
@ -1,9 +1,35 @@
|
|||
apiVersion: v1
|
||||
entries:
|
||||
budibase:
|
||||
- apiVersion: v2
|
||||
appVersion: 0.9.163
|
||||
created: "2021-10-12T21:58:00.515555+01:00"
|
||||
dependencies:
|
||||
- condition: services.couchdb.enabled
|
||||
name: couchdb
|
||||
repository: https://apache.github.io/couchdb-helm
|
||||
version: 3.3.4
|
||||
- condition: ingress.nginx
|
||||
name: ingress-nginx
|
||||
repository: https://github.com/kubernetes/ingress-nginx
|
||||
version: 3.35.0
|
||||
description: Budibase is an open source low-code platform, helping thousands of teams build apps for their workplace in minutes.
|
||||
digest: f369536c0eac1f6959d51e8ce6d74a87a7a9df29ae84fb9cbed0a273ab77429b
|
||||
keywords:
|
||||
- low-code
|
||||
- database
|
||||
- cluster
|
||||
name: budibase
|
||||
sources:
|
||||
- https://github.com/Budibase/budibase
|
||||
- https://budibase.com
|
||||
type: application
|
||||
urls:
|
||||
- https://budibase.github.io/budibase/budibase-0.2.0.tgz
|
||||
version: 0.2.0
|
||||
- apiVersion: v2
|
||||
appVersion: 0.9.56
|
||||
created: "2021-08-18T18:41:52.640176+01:00"
|
||||
created: "2021-10-12T21:58:00.512062+01:00"
|
||||
dependencies:
|
||||
- condition: services.couchdb.enabled
|
||||
name: couchdb
|
||||
|
@ -28,7 +54,7 @@ entries:
|
|||
version: 0.1.1
|
||||
- apiVersion: v2
|
||||
appVersion: 0.9.56
|
||||
created: "2021-08-18T18:41:52.635603+01:00"
|
||||
created: "2021-10-12T21:58:00.507257+01:00"
|
||||
dependencies:
|
||||
- condition: services.couchdb.enabled
|
||||
name: couchdb
|
||||
|
@ -51,4 +77,4 @@ entries:
|
|||
urls:
|
||||
- https://budibase.github.io/budibase/budibase-0.1.0.tgz
|
||||
version: 0.1.0
|
||||
generated: "2021-08-18T18:41:52.629415+01:00"
|
||||
generated: "2021-10-12T21:58:00.503447+01:00"
|
||||
|
|
|
@ -21,7 +21,7 @@ services:
|
|||
PORT: 4002
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_LEVEL: info
|
||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||
SENTRY_DSN: https://cc54bb0358fd4300ae97ef2273fbaf9f@o420233.ingest.sentry.io/6007553
|
||||
ENABLE_ANALYTICS: "true"
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
|
@ -51,7 +51,6 @@ services:
|
|||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
ACCOUNT_PORTAL_URL: https://portal.budi.live
|
||||
volumes:
|
||||
- ./logs:/logs
|
||||
depends_on:
|
||||
|
|
|
@ -22,13 +22,13 @@ type: application
|
|||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.1
|
||||
version: 0.2.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.9.56"
|
||||
appVersion: "0.9.163"
|
||||
|
||||
dependencies:
|
||||
- name: couchdb
|
||||
|
@ -37,5 +37,5 @@ dependencies:
|
|||
condition: services.couchdb.enabled
|
||||
- name: ingress-nginx
|
||||
version: 3.35.0
|
||||
repository: https://kubernetes.github.io/ingress-nginx
|
||||
condition: services.ingress.nginx
|
||||
repository: https://github.com/kubernetes/ingress-nginx
|
||||
condition: ingress.nginx
|
||||
|
|
|
@ -7,6 +7,8 @@ metadata:
|
|||
kubernetes.io/ingress.class: alb
|
||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||
alb.ingress.kubernetes.io/target-type: ip
|
||||
alb.ingress.kubernetes.io/success-codes: 200,301
|
||||
alb.ingress.kubernetes.io/healthcheck-path: /
|
||||
{{- if .Values.ingress.certificateArn }}
|
||||
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
|
||||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
|
||||
|
|
|
@ -14,7 +14,7 @@ spec:
|
|||
matchLabels:
|
||||
io.kompose.service: app-service
|
||||
strategy:
|
||||
type: Recreate
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
|
@ -73,13 +73,11 @@ spec:
|
|||
name: {{ template "budibase.fullname" . }}
|
||||
key: objectStoreSecret
|
||||
- name: MINIO_URL
|
||||
{{ if .Values.services.objectStore.url }}
|
||||
value: {{ .Values.services.objectStore.url }}
|
||||
{{ else }}
|
||||
value: http://minio-service:{{ .Values.services.objectStore.port }}
|
||||
{{ end }}
|
||||
- name: PORT
|
||||
value: {{ .Values.services.apps.port | quote }}
|
||||
- name: MULTI_TENANCY
|
||||
value: "1"
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.services.redis.password }}
|
||||
- name: REDIS_URL
|
||||
|
@ -92,14 +90,20 @@ spec:
|
|||
value: {{ .Values.globals.selfHosted | quote }}
|
||||
- name: SENTRY_DSN
|
||||
value: {{ .Values.globals.sentryDSN }}
|
||||
- name: POSTHOG_TOKEN
|
||||
value: {{ .Values.globals.posthogToken }}
|
||||
- name: WORKER_URL
|
||||
value: worker-service:{{ .Values.services.worker.port }}
|
||||
- name: COOKIE_DOMAIN
|
||||
value: {{ .Values.globals.cookieDomain | quote }}
|
||||
value: http://worker-service:{{ .Values.services.worker.port }}
|
||||
- name: PLATFORM_URL
|
||||
value: {{ .Values.globals.platformUrl | quote }}
|
||||
- name: USE_QUOTAS
|
||||
value: "1"
|
||||
- name: ACCOUNT_PORTAL_URL
|
||||
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||
- name: ACCOUNT_PORTAL_API_KEY
|
||||
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
||||
- name: COOKIE_DOMAIN
|
||||
value: {{ .Values.globals.cookieDomain | quote }}
|
||||
image: budibase/apps
|
||||
imagePullPolicy: Always
|
||||
name: bbapps
|
||||
|
|
|
@ -14,7 +14,7 @@ spec:
|
|||
matchLabels:
|
||||
app.kubernetes.io/name: budibase-proxy
|
||||
strategy:
|
||||
type: Recreate
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
|
@ -26,7 +26,7 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- image: budibase/proxy
|
||||
imagePullPolicy: ""
|
||||
imagePullPolicy: Always
|
||||
name: proxy-service
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.proxy.port }}
|
||||
|
|
|
@ -15,7 +15,7 @@ spec:
|
|||
matchLabels:
|
||||
io.kompose.service: worker-service
|
||||
strategy:
|
||||
type: Recreate
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
|
@ -70,13 +70,11 @@ spec:
|
|||
name: {{ template "budibase.fullname" . }}
|
||||
key: objectStoreSecret
|
||||
- name: MINIO_URL
|
||||
{{ if .Values.services.objectStore.url }}
|
||||
value: {{ .Values.services.objectStore.url }}
|
||||
{{ else }}
|
||||
value: http://minio-service:{{ .Values.services.objectStore.port }}
|
||||
{{ end }}
|
||||
- name: PORT
|
||||
value: {{ .Values.services.worker.port | quote }}
|
||||
- name: MULTI_TENANCY
|
||||
value: "1"
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.services.redis.password | quote }}
|
||||
- name: REDIS_URL
|
||||
|
@ -91,8 +89,22 @@ spec:
|
|||
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||
- name: ACCOUNT_PORTAL_API_KEY
|
||||
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
||||
- name: PLATFORM_URL
|
||||
value: {{ .Values.globals.platformUrl | quote }}
|
||||
- name: COOKIE_DOMAIN
|
||||
value: {{ .Values.globals.cookieDomain | quote }}
|
||||
- name: SMTP_FALLBACK_ENABLED
|
||||
value: {{ .Values.globals.smtp.enabled | quote }}
|
||||
- name: SMTP_USER
|
||||
value: {{ .Values.globals.smtp.user | quote }}
|
||||
- name: SMTP_PASSWORD
|
||||
value: {{ .Values.globals.smtp.password | quote }}
|
||||
- name: SMTP_HOST
|
||||
value: {{ .Values.globals.smtp.host | quote }}
|
||||
- name: SMTP_PORT
|
||||
value: {{ .Values.globals.smtp.port | quote }}
|
||||
- name: SMTP_FROM_ADDRESS
|
||||
value: {{ .Values.globals.smtp.from | quote }}
|
||||
image: budibase/worker
|
||||
imagePullPolicy: Always
|
||||
name: bbworker
|
||||
|
|
|
@ -40,11 +40,12 @@ service:
|
|||
port: 10000
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
nginx: true
|
||||
certificateArn: ""
|
||||
enabled: false
|
||||
aws: false
|
||||
nginx: true
|
||||
certificateArn: ""
|
||||
className: ""
|
||||
annotations:
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
hosts:
|
||||
- host: # change if using custom domain
|
||||
|
@ -55,7 +56,7 @@ ingress:
|
|||
service:
|
||||
name: proxy-service
|
||||
port:
|
||||
number: 10000
|
||||
number: 10000
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
|
@ -84,20 +85,24 @@ affinity: {}
|
|||
|
||||
globals:
|
||||
budibaseEnv: PRODUCTION
|
||||
enableAnalytics: false
|
||||
posthogToken: ""
|
||||
enableAnalytics: true
|
||||
sentryDSN: ""
|
||||
posthogToken: ""
|
||||
logLevel: info
|
||||
selfHosted: 1
|
||||
accountPortalUrL: ""
|
||||
selfHosted: ""
|
||||
accountPortalUrl: ""
|
||||
accountPortalApiKey: ""
|
||||
cookieDomain: ""
|
||||
cookieDomain: ""
|
||||
platformUrl: ""
|
||||
|
||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||
|
||||
# if createSecrets is set to false, you can hard-code your secrets here
|
||||
internalApiKey: ""
|
||||
jwtSecret: ""
|
||||
|
||||
smtp:
|
||||
enabled: false
|
||||
|
||||
services:
|
||||
dns: cluster.local
|
||||
|
@ -118,12 +123,12 @@ services:
|
|||
couchdb:
|
||||
enabled: true
|
||||
replicaCount: 3
|
||||
url: "" # only change if pointing to existing couch server
|
||||
user: "" # only change if pointing to existing couch server
|
||||
password: "" # only change if pointing to existing couch server
|
||||
# url: "" # only change if pointing to existing couch server
|
||||
# user: "" # only change if pointing to existing couch server
|
||||
# password: "" # only change if pointing to existing couch server
|
||||
port: 5984
|
||||
storage: 100Mi
|
||||
|
||||
|
||||
redis:
|
||||
enabled: true # disable if using external redis
|
||||
port: 6379
|
||||
|
@ -131,7 +136,7 @@ services:
|
|||
url: "" # only change if pointing to existing redis cluster and enabled: false
|
||||
password: "budibase" # recommended to override if using built-in redis
|
||||
storage: 100Mi
|
||||
|
||||
|
||||
objectStore:
|
||||
minio: true
|
||||
browser: true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -42,8 +42,9 @@ module.exports = (
|
|||
internal = false
|
||||
if (authCookie) {
|
||||
let error = null
|
||||
const sessionId = authCookie.sessionId,
|
||||
userId = authCookie.userId
|
||||
const sessionId = authCookie.sessionId
|
||||
const userId = authCookie.userId
|
||||
|
||||
const session = await getSession(userId, sessionId)
|
||||
if (!session) {
|
||||
error = "No session found"
|
||||
|
|
|
@ -24,17 +24,24 @@ exports.createASession = async (userId, session) => {
|
|||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||
}
|
||||
|
||||
exports.invalidateSessions = async (userId, sessionId = null) => {
|
||||
exports.invalidateSessions = async (userId, sessionIds = null) => {
|
||||
let sessions = []
|
||||
if (sessionId) {
|
||||
sessions.push({ key: makeSessionID(userId, sessionId) })
|
||||
} else {
|
||||
|
||||
// If no sessionIds, get all the sessions for the user
|
||||
if (!sessionIds) {
|
||||
sessions = await getSessionsForUser(userId)
|
||||
sessions.forEach(
|
||||
session =>
|
||||
(session.key = makeSessionID(session.userId, session.sessionId))
|
||||
)
|
||||
} else {
|
||||
// use the passed array of sessionIds
|
||||
sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
||||
sessions = sessions.map(sessionId => ({
|
||||
key: makeSessionID(userId, sessionId),
|
||||
}))
|
||||
}
|
||||
|
||||
const client = await redis.getSessionClient()
|
||||
const promises = []
|
||||
for (let session of sessions) {
|
||||
|
|
|
@ -7,7 +7,7 @@ const {
|
|||
const jwt = require("jsonwebtoken")
|
||||
const { options } = require("./middleware/passport/jwt")
|
||||
const { createUserEmailView } = require("./db/views")
|
||||
const { Headers, UserStatus } = require("./constants")
|
||||
const { Headers, UserStatus, Cookies } = require("./constants")
|
||||
const {
|
||||
getGlobalDB,
|
||||
updateTenantId,
|
||||
|
@ -19,6 +19,7 @@ const accounts = require("./cloud/accounts")
|
|||
const { hash } = require("./hashing")
|
||||
const userCache = require("./cache/user")
|
||||
const env = require("./environment")
|
||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||
|
||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
|
||||
|
@ -235,3 +236,28 @@ exports.saveUser = async (
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a user out from budibase. Re-used across account portal and builder.
|
||||
*/
|
||||
exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
||||
|
||||
const currentSession = this.getCookie(ctx, Cookies.Auth)
|
||||
let sessions = await getUserSessions(userId)
|
||||
|
||||
if (keepActiveSession) {
|
||||
sessions = sessions.filter(
|
||||
session => session.sessionId !== currentSession.sessionId
|
||||
)
|
||||
} else {
|
||||
// clear cookies
|
||||
this.clearCookie(ctx, Cookies.Auth)
|
||||
this.clearCookie(ctx, Cookies.CurrentApp)
|
||||
}
|
||||
|
||||
await invalidateSessions(
|
||||
userId,
|
||||
sessions.map(({ sessionId }) => sessionId)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"license": "AGPL-3.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import RelationshipRenderer from "./RelationshipRenderer.svelte"
|
||||
import AttachmentRenderer from "./AttachmentRenderer.svelte"
|
||||
import ArrayRenderer from "./ArrayRenderer.svelte"
|
||||
import InternalRenderer from "./InternalRenderer.svelte"
|
||||
|
||||
export let row
|
||||
export let schema
|
||||
|
@ -23,9 +22,7 @@
|
|||
number: StringRenderer,
|
||||
longform: StringRenderer,
|
||||
array: ArrayRenderer,
|
||||
internal: InternalRenderer,
|
||||
}
|
||||
|
||||
$: type = schema?.type ?? "string"
|
||||
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
||||
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
|
||||
|
|
|
@ -8,11 +8,19 @@
|
|||
const selected = getContext("tab")
|
||||
let tab
|
||||
let tabInfo
|
||||
|
||||
const setTabInfo = () => {
|
||||
tabInfo = tab.getBoundingClientRect()
|
||||
if ($selected.title === title) {
|
||||
$selected.info = tabInfo
|
||||
}
|
||||
// If the tabs are being rendered inside a component which uses
|
||||
// a svelte transition to enter, then this initial getBoundingClientRect
|
||||
// will return an incorrect position.
|
||||
// We just need to get this off the main thread to fix this, by using
|
||||
// a 0ms timeout.
|
||||
setTimeout(() => {
|
||||
tabInfo = tab.getBoundingClientRect()
|
||||
if ($selected.title === title) {
|
||||
$selected.info = tabInfo
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
@ -31,7 +31,7 @@ context("Create a Table", () => {
|
|||
cy.contains("nameupdated ").should("contain", "nameupdated")
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
it("edits a row", () => {
|
||||
cy.contains("button", "Edit").click({ force: true })
|
||||
cy.wait(1000)
|
||||
|
@ -40,7 +40,7 @@ context("Create a Table", () => {
|
|||
cy.contains("Save").click()
|
||||
cy.contains("Updated").should("have.text", "Updated")
|
||||
})
|
||||
*/
|
||||
|
||||
it("deletes a row", () => {
|
||||
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
||||
cy.contains("Delete 1 row(s)").click()
|
||||
|
|
|
@ -36,18 +36,11 @@ Cypress.Commands.add("createApp", name => {
|
|||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
||||
cy.wait(500)
|
||||
cy.contains(/Start from scratch/).click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.within(() => {
|
||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||
cy.wait(7000)
|
||||
})
|
||||
.then(() => {
|
||||
// Because we show the datasource modal on entry, we need to create a table to get rid of the modal in the future
|
||||
cy.createInitialDatasource("initialTable")
|
||||
cy.expandBudibaseConnection()
|
||||
cy.get(".nav-item.selected > .content").should("be.visible")
|
||||
})
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||
cy.wait(7000)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("deleteApp", () => {
|
||||
|
@ -77,22 +70,6 @@ Cypress.Commands.add("createTestTableWithData", () => {
|
|||
cy.addColumn("dog", "age", "Number")
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createInitialDatasource", tableName => {
|
||||
// Enter table name
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.contains("Budibase DB").trigger("mouseover").click().click()
|
||||
cy.wait(1000)
|
||||
cy.contains("Continue").click()
|
||||
})
|
||||
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.wait(1000)
|
||||
cy.get("input").first().type(tableName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||
})
|
||||
cy.contains(tableName).should("be.visible")
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createTable", tableName => {
|
||||
cy.contains("Budibase DB").click()
|
||||
cy.contains("Create new table").click()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.160-alpha.4",
|
||||
"@budibase/client": "^0.9.160-alpha.4",
|
||||
"@budibase/bbui": "^0.9.167-alpha.12",
|
||||
"@budibase/client": "^0.9.167-alpha.12",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^0.9.160-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.167-alpha.12",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -15,7 +15,7 @@ const apiCall =
|
|||
if (resp.status === 403) {
|
||||
removeCookie(Cookies.Auth)
|
||||
// reload after removing cookie, go to login
|
||||
if (!url.includes("self")) {
|
||||
if (!url.includes("self") && !url.includes("login")) {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,17 @@ import {
|
|||
} from "./storeUtils"
|
||||
import { store } from "builderStore"
|
||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import {
|
||||
makePropSafe,
|
||||
isJSBinding,
|
||||
decodeJSBinding,
|
||||
encodeJSBinding,
|
||||
} from "@budibase/string-templates"
|
||||
import { TableNames } from "../constants"
|
||||
|
||||
// Regex to match all instances of template strings
|
||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
|
||||
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
|
||||
|
||||
/**
|
||||
|
@ -430,6 +436,15 @@ function replaceBetween(string, start, end, replacement) {
|
|||
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
||||
*/
|
||||
function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
||||
// Decide from base64 if using JS
|
||||
const isJS = isJSBinding(textWithBindings)
|
||||
if (isJS) {
|
||||
textWithBindings = decodeJSBinding(textWithBindings)
|
||||
}
|
||||
|
||||
// Determine correct regex to find bindings to replace
|
||||
const regex = isJS ? CAPTURE_VAR_INSIDE_JS : CAPTURE_VAR_INSIDE_TEMPLATE
|
||||
|
||||
const convertFrom =
|
||||
convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding"
|
||||
if (typeof textWithBindings !== "string") {
|
||||
|
@ -441,7 +456,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
|||
.sort((a, b) => {
|
||||
return b.length - a.length
|
||||
})
|
||||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
|
||||
const boundValues = textWithBindings.match(regex) || []
|
||||
let result = textWithBindings
|
||||
for (let boundValue of boundValues) {
|
||||
let newBoundValue = boundValue
|
||||
|
@ -449,7 +464,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
|||
// in the search, working from longest to shortest so always use best match first
|
||||
let searchString = newBoundValue
|
||||
for (let from of convertFromProps) {
|
||||
if (shouldReplaceBinding(newBoundValue, from, convertTo)) {
|
||||
if (isJS || shouldReplaceBinding(newBoundValue, from, convertTo)) {
|
||||
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
||||
let idx
|
||||
do {
|
||||
|
@ -472,6 +487,12 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
|||
}
|
||||
result = result.replace(boundValue, newBoundValue)
|
||||
}
|
||||
|
||||
// Re-encode to base64 if using JS
|
||||
if (isJS) {
|
||||
result = encodeJSBinding(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||
</div>
|
||||
</div>
|
||||
{#if testResult}
|
||||
{#if testResult && testResult[0]}
|
||||
<span on:click={() => resultsModal.show()}>
|
||||
<StatusLight
|
||||
positive={isTrigger || testResult[0].outputs?.success}
|
||||
|
|
|
@ -194,6 +194,7 @@
|
|||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
/>
|
||||
{/if}
|
||||
{:else if value.customType === "query"}
|
||||
|
@ -259,6 +260,7 @@
|
|||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
type="string"
|
||||
{bindings}
|
||||
fillWidth={true}
|
||||
allowJS={false}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { Select, Toggle, DatePicker, Multiselect } from "@budibase/bbui"
|
||||
import {
|
||||
Select,
|
||||
Toggle,
|
||||
DatePicker,
|
||||
Multiselect,
|
||||
TextArea,
|
||||
} from "@budibase/bbui"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
@ -52,7 +58,6 @@
|
|||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
/>
|
||||
|
||||
{#if schemaFields.length}
|
||||
<div class="schema-fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
|
@ -82,6 +87,8 @@
|
|||
label={field}
|
||||
options={schema.constraints.inclusion}
|
||||
/>
|
||||
{:else if schema.type === "longform"}
|
||||
<TextArea label={field} bind:value={value[field]} />
|
||||
{:else if schema.type === "link"}
|
||||
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
|
||||
{:else if schema.type === "string" || schema.type === "number"}
|
||||
|
@ -103,6 +110,7 @@
|
|||
type="string"
|
||||
{bindings}
|
||||
fillWidth={true}
|
||||
allowJS={false}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
const LINK_TYPE = FIELDS.LINK.type
|
||||
const dispatch = createEventDispatcher()
|
||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||
const { hide } = getContext(Context.Modal)
|
||||
let fieldDefinitions = cloneDeep(FIELDS)
|
||||
|
||||
|
@ -66,7 +67,11 @@
|
|||
(field.type === LINK_TYPE && !field.tableId) ||
|
||||
Object.keys($tables.draft?.schema ?? {}).some(
|
||||
key => key !== originalName && key === field.name
|
||||
)
|
||||
) ||
|
||||
columnNameInvalid
|
||||
$: columnNameInvalid = PROHIBITED_COLUMN_NAMES.some(
|
||||
name => field.name === name
|
||||
)
|
||||
|
||||
// used to select what different options can be displayed for column type
|
||||
$: canBeSearched =
|
||||
|
@ -200,6 +205,9 @@
|
|||
label="Name"
|
||||
bind:value={field.name}
|
||||
disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)}
|
||||
error={columnNameInvalid
|
||||
? `${PROHIBITED_COLUMN_NAMES.join(", ")} are not allowed as column names`
|
||||
: ""}
|
||||
/>
|
||||
|
||||
<Select
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
<script>
|
||||
import { Label, Input, Layout, Toggle, Button } from "@budibase/bbui"
|
||||
import {
|
||||
Label,
|
||||
Input,
|
||||
Layout,
|
||||
Toggle,
|
||||
Button,
|
||||
TextArea,
|
||||
} from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let integration
|
||||
export let schema
|
||||
|
||||
let addButton
|
||||
</script>
|
||||
|
||||
|
@ -29,6 +37,15 @@
|
|||
<Label>{capitalise(configKey)}</Label>
|
||||
<Toggle text="" bind:value={integration[configKey]} />
|
||||
</div>
|
||||
{:else if schema[configKey].type === "longForm"}
|
||||
<div class="form-row">
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
<TextArea
|
||||
type={schema[configKey].type}
|
||||
on:change
|
||||
bind:value={integration[configKey]}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="form-row">
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={externalDatasourceModal}>
|
||||
<DatasourceConfigModal {integration} />
|
||||
<DatasourceConfigModal {integration} {modal} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
? "Fetch tables from database"
|
||||
: "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
size="M"
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS"
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
<script context="module">
|
||||
import { Label } from "@budibase/bbui"
|
||||
|
||||
export const EditorModes = {
|
||||
JS: {
|
||||
name: "javascript",
|
||||
json: false,
|
||||
},
|
||||
JSON: {
|
||||
name: "javascript",
|
||||
json: true,
|
||||
},
|
||||
SQL: {
|
||||
name: "sql",
|
||||
},
|
||||
Handlebars: {
|
||||
name: "handlebars",
|
||||
base: "text/html",
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import CodeMirror from "components/integration/codemirror"
|
||||
import { themeStore } from "builderStore"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
|
||||
export let mode = EditorModes.JS
|
||||
export let value = ""
|
||||
export let height = 300
|
||||
export let resize = "none"
|
||||
export let readonly = false
|
||||
export let hints = []
|
||||
export let label
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let textarea
|
||||
let editor
|
||||
|
||||
// Keep editor up to date with value
|
||||
$: editor?.setValue(value || "")
|
||||
|
||||
// Creates an instance of a code mirror editor
|
||||
async function createEditor(mode, value) {
|
||||
if (!CodeMirror || !textarea || editor) {
|
||||
return
|
||||
}
|
||||
|
||||
// Configure CM options
|
||||
const lightTheme = $themeStore.theme.includes("light")
|
||||
const options = {
|
||||
mode,
|
||||
value: value || "",
|
||||
readOnly: readonly,
|
||||
theme: lightTheme ? "default" : "tomorrow-night-eighties",
|
||||
|
||||
// Style
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
indentWithTabs: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
|
||||
// QOL addons
|
||||
extraKeys: { "Ctrl-Space": "autocomplete" },
|
||||
styleActiveLine: { nonEmpty: true },
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
}
|
||||
|
||||
// Register hints plugin if desired
|
||||
if (hints?.length) {
|
||||
CodeMirror.registerHelper("hint", "dictionaryHint", function (editor) {
|
||||
const cursor = editor.getCursor()
|
||||
return {
|
||||
list: hints,
|
||||
from: CodeMirror.Pos(cursor.line, cursor.ch),
|
||||
to: CodeMirror.Pos(cursor.line, cursor.ch),
|
||||
}
|
||||
})
|
||||
CodeMirror.commands.autocomplete = function (cm) {
|
||||
CodeMirror.showHint(cm, CodeMirror.hint.dictionaryHint)
|
||||
}
|
||||
}
|
||||
|
||||
// Construct CM instance
|
||||
editor = CodeMirror.fromTextArea(textarea, options)
|
||||
|
||||
// Use a blur handler to update the value
|
||||
editor.on("blur", instance => {
|
||||
dispatch("change", instance.getValue())
|
||||
})
|
||||
}
|
||||
|
||||
// Export a function to expose caret position
|
||||
export const getCaretPosition = () => {
|
||||
const cursor = editor.getCursor()
|
||||
return {
|
||||
start: cursor.ch,
|
||||
end: cursor.ch,
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Create the editor with initial value
|
||||
createEditor(mode, value)
|
||||
|
||||
// Clean up editor on unmount
|
||||
return () => {
|
||||
if (editor) {
|
||||
editor.toTextArea()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if label}
|
||||
<div style="margin-bottom: var(--spacing-s)">
|
||||
<Label small>{label}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
style={`--code-mirror-height: ${height}px; --code-mirror-resize: ${resize}`}
|
||||
>
|
||||
<textarea tabindex="0" bind:this={textarea} readonly {value} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div :global(.CodeMirror) {
|
||||
height: var(--code-mirror-height);
|
||||
min-height: var(--code-mirror-height);
|
||||
font-family: monospace;
|
||||
line-height: 1.3;
|
||||
border: var(--spectrum-alias-border-size-thin) solid;
|
||||
border-color: var(--spectrum-alias-border-color);
|
||||
border-radius: var(--border-radius-s);
|
||||
resize: var(--code-mirror-resize);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Override default active line highlight colour in dark theme */
|
||||
div
|
||||
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties
|
||||
.CodeMirror-activeline-background) {
|
||||
background: rgba(255, 255, 255, 0.075);
|
||||
}
|
||||
|
||||
/* Remove active line styling when not focused */
|
||||
div
|
||||
:global(.CodeMirror:not(.CodeMirror-focused)
|
||||
.CodeMirror-activeline-background) {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
/* Add a spectrum themed border when focused */
|
||||
div :global(.CodeMirror-focused) {
|
||||
border-color: var(--spectrum-alias-border-color-mouse-focus);
|
||||
}
|
||||
</style>
|
|
@ -1,32 +1,98 @@
|
|||
<script>
|
||||
import groupBy from "lodash/fp/groupBy"
|
||||
import { Search, TextArea, DrawerContent } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { isValid } from "@budibase/string-templates"
|
||||
import {
|
||||
Search,
|
||||
TextArea,
|
||||
DrawerContent,
|
||||
Tabs,
|
||||
Tab,
|
||||
Body,
|
||||
Layout,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import {
|
||||
isValid,
|
||||
decodeJSBinding,
|
||||
encodeJSBinding,
|
||||
} from "@budibase/string-templates"
|
||||
import { readableToRuntimeBinding } from "builderStore/dataBinding"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
import { addToText } from "./utils"
|
||||
import { addHBSBinding, addJSBinding } from "./utils"
|
||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let bindableProperties
|
||||
export let value = ""
|
||||
export let valid
|
||||
export let allowJS = false
|
||||
|
||||
let helpers = handlebarsCompletions()
|
||||
let getCaretPosition
|
||||
let search = ""
|
||||
let initialValueJS = value?.startsWith("{{ js ")
|
||||
let mode = initialValueJS ? "JavaScript" : "Handlebars"
|
||||
let jsValue = initialValueJS ? value : null
|
||||
let hbsValue = initialValueJS ? null : value
|
||||
|
||||
$: valid = isValid(readableToRuntimeBinding(bindableProperties, value))
|
||||
$: dispatch("change", value)
|
||||
$: usingJS = mode === "JavaScript"
|
||||
$: ({ context } = groupBy("type", bindableProperties))
|
||||
$: searchRgx = new RegExp(search, "ig")
|
||||
$: filteredColumns = context?.filter(context => {
|
||||
$: filteredBindings = context?.filter(context => {
|
||||
return context.readableBinding.match(searchRgx)
|
||||
})
|
||||
$: filteredHelpers = helpers?.filter(helper => {
|
||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
||||
})
|
||||
|
||||
const updateValue = value => {
|
||||
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
|
||||
if (valid) {
|
||||
dispatch("change", value)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a HBS helper to the expression
|
||||
const addHelper = helper => {
|
||||
hbsValue = addHBSBinding(value, getCaretPosition(), helper.text)
|
||||
updateValue(hbsValue)
|
||||
}
|
||||
|
||||
// Adds a data binding to the expression
|
||||
const addBinding = binding => {
|
||||
if (usingJS) {
|
||||
let js = decodeJSBinding(jsValue)
|
||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
||||
jsValue = encodeJSBinding(js)
|
||||
updateValue(jsValue)
|
||||
} else {
|
||||
hbsValue = addHBSBinding(
|
||||
hbsValue,
|
||||
getCaretPosition(),
|
||||
binding.readableBinding
|
||||
)
|
||||
updateValue(hbsValue)
|
||||
}
|
||||
}
|
||||
|
||||
const onChangeMode = e => {
|
||||
mode = e.detail
|
||||
updateValue(mode === "JavaScript" ? jsValue : hbsValue)
|
||||
}
|
||||
|
||||
const onChangeHBSValue = e => {
|
||||
hbsValue = e.detail
|
||||
updateValue(hbsValue)
|
||||
}
|
||||
|
||||
const onChangeJSValue = e => {
|
||||
jsValue = encodeJSBinding(e.detail)
|
||||
updateValue(jsValue)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
|
||||
})
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
|
@ -36,32 +102,24 @@
|
|||
<div class="heading">Search</div>
|
||||
<Search placeholder="Search" bind:value={search} />
|
||||
</section>
|
||||
{#if filteredColumns?.length}
|
||||
{#if filteredBindings?.length}
|
||||
<section>
|
||||
<div class="heading">Bindable Values</div>
|
||||
<ul>
|
||||
{#each filteredColumns as { readableBinding }}
|
||||
<li
|
||||
on:click={() => {
|
||||
value = addToText(value, getCaretPosition(), readableBinding)
|
||||
}}
|
||||
>
|
||||
{readableBinding}
|
||||
{#each filteredBindings as binding}
|
||||
<li on:click={() => addBinding(binding)}>
|
||||
{binding.readableBinding}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
{#if filteredHelpers?.length}
|
||||
{#if filteredHelpers?.length && !usingJS}
|
||||
<section>
|
||||
<div class="heading">Helpers</div>
|
||||
<ul>
|
||||
{#each filteredHelpers as helper}
|
||||
<li
|
||||
on:click={() => {
|
||||
value = addToText(value, getCaretPosition(), helper.text)
|
||||
}}
|
||||
>
|
||||
<li on:click={() => addHelper(helper)}>
|
||||
<div class="helper">
|
||||
<div class="helper__name">{helper.displayText}</div>
|
||||
<div class="helper__description">
|
||||
|
@ -77,24 +135,56 @@
|
|||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="main">
|
||||
<TextArea
|
||||
bind:getCaretPosition
|
||||
bind:value
|
||||
placeholder="Add text, or click the objects on the left to add them to the textbox."
|
||||
/>
|
||||
{#if !valid}
|
||||
<p class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the guide
|
||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||
for more details.
|
||||
</p>
|
||||
{/if}
|
||||
<Tabs selected={mode} on:select={onChangeMode}>
|
||||
<Tab title="Handlebars">
|
||||
<div class="main-content">
|
||||
<TextArea
|
||||
bind:getCaretPosition
|
||||
value={hbsValue}
|
||||
on:change={onChangeHBSValue}
|
||||
placeholder="Add text, or click the objects on the left to add them to the textbox."
|
||||
/>
|
||||
{#if !valid}
|
||||
<p class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the guide
|
||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||
for more details.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
{#if allowJS}
|
||||
<Tab title="JavaScript">
|
||||
<div class="main-content">
|
||||
<Layout noPadding gap="XS">
|
||||
<CodeMirrorEditor
|
||||
bind:getCaretPosition
|
||||
height={200}
|
||||
value={decodeJSBinding(jsValue)}
|
||||
on:change={onChangeJSValue}
|
||||
hints={context?.map(x => `$("${x.readableBinding}")`)}
|
||||
/>
|
||||
<Body size="S">
|
||||
JavaScript expressions are executed as functions, so ensure that
|
||||
your expression returns a value.
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
|
||||
<style>
|
||||
.main :global(textarea) {
|
||||
min-height: 150px !important;
|
||||
min-height: 202px !important;
|
||||
}
|
||||
.main {
|
||||
margin: calc(-1 * var(--spacing-xl));
|
||||
}
|
||||
.main-content {
|
||||
padding: var(--spacing-s) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.container {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
} from "builderStore/dataBinding"
|
||||
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let panel = BindingPanel
|
||||
export let value = ""
|
||||
|
@ -15,11 +16,14 @@
|
|||
export let label
|
||||
export let disabled = false
|
||||
export let options
|
||||
export let allowJS = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
|
||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||
$: tempValue = readableValue
|
||||
$: isJS = isJSBinding(value)
|
||||
|
||||
const handleClose = () => {
|
||||
onChange(tempValue)
|
||||
|
@ -35,7 +39,7 @@
|
|||
<Combobox
|
||||
{label}
|
||||
{disabled}
|
||||
value={readableValue}
|
||||
value={isJS ? "(JavaScript function)" : readableValue}
|
||||
on:change={event => onChange(event.detail)}
|
||||
{placeholder}
|
||||
{options}
|
||||
|
@ -58,6 +62,7 @@
|
|||
close={handleClose}
|
||||
on:change={event => (tempValue = event.detail)}
|
||||
bindableProperties={bindings}
|
||||
{allowJS}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
} from "builderStore/dataBinding"
|
||||
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let panel = BindingPanel
|
||||
export let value = ""
|
||||
|
@ -15,12 +16,15 @@
|
|||
export let label
|
||||
export let disabled = false
|
||||
export let fillWidth
|
||||
export let allowJS = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
let valid = true
|
||||
|
||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||
$: tempValue = readableValue
|
||||
$: isJS = isJSBinding(value)
|
||||
|
||||
const saveBinding = () => {
|
||||
onChange(tempValue)
|
||||
|
@ -36,7 +40,7 @@
|
|||
<Input
|
||||
{label}
|
||||
{disabled}
|
||||
value={readableValue}
|
||||
value={isJS ? "(JavaScript function)" : readableValue}
|
||||
on:change={event => onChange(event.detail)}
|
||||
{placeholder}
|
||||
/>
|
||||
|
@ -60,6 +64,7 @@
|
|||
value={readableValue}
|
||||
on:change={event => (tempValue = event.detail)}
|
||||
bindableProperties={bindings}
|
||||
{allowJS}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { isValid } from "@budibase/string-templates"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
import { readableToRuntimeBinding } from "builderStore/dataBinding"
|
||||
import { addToText } from "./utils"
|
||||
import { addHBSBinding } from "./utils"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
|||
{#each bindings as binding}
|
||||
<li
|
||||
on:click={() => {
|
||||
value = addToText(value, getCaretPosition(), binding)
|
||||
value = addHBSBinding(value, getCaretPosition(), binding)
|
||||
}}
|
||||
>
|
||||
<span class="binding__label">{binding.label}</span>
|
||||
|
@ -71,7 +71,7 @@
|
|||
{#each filteredHelpers as helper}
|
||||
<li
|
||||
on:click={() => {
|
||||
value = addToText(value, getCaretPosition(), helper.text)
|
||||
value = addHBSBinding(value, getCaretPosition(), helper.text)
|
||||
}}
|
||||
>
|
||||
<div class="helper">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function addToText(value, caretPos, binding) {
|
||||
export function addHBSBinding(value, caretPos, binding) {
|
||||
binding = typeof binding === "string" ? binding : binding.path
|
||||
value = value == null ? "" : value
|
||||
if (!value.includes("{{") && !value.includes("}}")) {
|
||||
|
@ -14,3 +14,18 @@ export function addToText(value, caretPos, binding) {
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function addJSBinding(value, caretPos, binding) {
|
||||
binding = typeof binding === "string" ? binding : binding.path
|
||||
value = value == null ? "" : value
|
||||
binding = `$("${binding}")`
|
||||
if (caretPos.start) {
|
||||
value =
|
||||
value.substring(0, caretPos.start) +
|
||||
binding +
|
||||
value.substring(caretPos.end, value.length)
|
||||
} else {
|
||||
value += binding
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import { Input } from "@budibase/bbui"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let value
|
||||
|
||||
$: isJS = isJSBinding(value)
|
||||
</script>
|
||||
|
||||
<Input
|
||||
{...$$props}
|
||||
value={isJS ? "(JavaScript function)" : value}
|
||||
readonly={isJS}
|
||||
on:change
|
||||
/>
|
|
@ -105,6 +105,7 @@
|
|||
value={safeValue}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
bindableProperties={bindings}
|
||||
allowJS
|
||||
/>
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Checkbox, Input, Select, Stepper } from "@budibase/bbui"
|
||||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||
import EventsEditor from "./EventsEditor"
|
||||
|
@ -15,6 +15,7 @@ import URLSelect from "./URLSelect.svelte"
|
|||
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
|
||||
import Input from "./Input.svelte"
|
||||
|
||||
const componentMap = {
|
||||
text: Input,
|
||||
|
|
|
@ -21,12 +21,15 @@
|
|||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import { datasources, integrations, queries } from "stores/backend"
|
||||
import { capitalise } from "../../helpers"
|
||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||
|
||||
export let query
|
||||
export let fields = []
|
||||
|
||||
let parameters
|
||||
let data = []
|
||||
const transformerDocs =
|
||||
"https://docs.budibase.com/building-apps/data/transformers"
|
||||
const typeOptions = [
|
||||
{ label: "Text", value: "STRING" },
|
||||
{ label: "Number", value: "NUMBER" },
|
||||
|
@ -52,6 +55,11 @@
|
|||
$: readQuery = query.queryVerb === "read" || query.readable
|
||||
$: queryInvalid = !query.name || (readQuery && data.length === 0)
|
||||
|
||||
// seed the transformer
|
||||
if (query && !query.transformer) {
|
||||
query.transformer = "return data"
|
||||
}
|
||||
|
||||
function newField() {
|
||||
fields = [...fields, {}]
|
||||
}
|
||||
|
@ -74,6 +82,7 @@
|
|||
const response = await api.post(`/api/queries/preview`, {
|
||||
fields: query.fields,
|
||||
queryVerb: query.queryVerb,
|
||||
transformer: query.transformer,
|
||||
parameters: query.parameters.reduce(
|
||||
(acc, next) => ({
|
||||
...acc,
|
||||
|
@ -160,12 +169,34 @@
|
|||
<IntegrationQueryEditor
|
||||
{datasource}
|
||||
{query}
|
||||
height={300}
|
||||
height={200}
|
||||
schema={queryConfig[query.queryVerb]}
|
||||
bind:parameters
|
||||
/>
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="help-heading">
|
||||
<Heading size="S">Transformer</Heading>
|
||||
<Icon
|
||||
on:click={() => window.open(transformerDocs)}
|
||||
hoverable
|
||||
name="Help"
|
||||
size="L"
|
||||
/>
|
||||
</div>
|
||||
<Body size="S"
|
||||
>Add a JavaScript function to transform the query result.</Body
|
||||
>
|
||||
<CodeMirrorEditor
|
||||
height={200}
|
||||
label="Transformer"
|
||||
value={query.transformer}
|
||||
resize="vertical"
|
||||
on:change={e => (query.transformer = e.detail)}
|
||||
/>
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="viewer-controls">
|
||||
<Heading size="S">Results</Heading>
|
||||
<ButtonGroup>
|
||||
|
@ -220,6 +251,7 @@
|
|||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.config-field {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
|
@ -227,6 +259,11 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.help-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5%;
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
import CodeMirror from "codemirror"
|
||||
import "codemirror/lib/codemirror.css"
|
||||
import "codemirror/theme/tomorrow-night-eighties.css"
|
||||
import "codemirror/addon/hint/show-hint.css"
|
||||
import "codemirror/theme/neo.css"
|
||||
|
||||
// Modes
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
import "codemirror/mode/sql/sql"
|
||||
import "codemirror/mode/css/css"
|
||||
import "codemirror/mode/handlebars/handlebars"
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
|
||||
// Hints
|
||||
import "codemirror/addon/hint/show-hint"
|
||||
import "codemirror/addon/hint/show-hint.css"
|
||||
|
||||
// Theming
|
||||
import "codemirror/theme/tomorrow-night-eighties.css"
|
||||
|
||||
// Functional addons
|
||||
import "codemirror/addon/selection/active-line"
|
||||
import "codemirror/addon/edit/closebrackets"
|
||||
import "codemirror/addon/edit/matchbrackets"
|
||||
|
||||
export default CodeMirror
|
||||
|
|
|
@ -150,7 +150,6 @@
|
|||
showCancelButton={false}
|
||||
showCloseIcon={false}
|
||||
>
|
||||
<Body size="M">Select a template below, or start from scratch.</Body>
|
||||
<TemplateList
|
||||
onSelect={selected => {
|
||||
if (!selected) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Heading, Layout, Icon } from "@budibase/bbui"
|
||||
import { Heading, Layout, Icon, Body } from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import api from "builderStore/api"
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
|||
|
||||
async function fetchTemplates() {
|
||||
const response = await api.get("/api/templates?type=app")
|
||||
console.log("Responded")
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
|
@ -19,6 +20,11 @@
|
|||
<Spinner size="30" />
|
||||
</div>
|
||||
{:then templates}
|
||||
{#if templates?.length > 0}
|
||||
<Body size="M">Select a template below, or start from scratch.</Body>
|
||||
{:else}
|
||||
<Body size="M">Start your app from scratch below.</Body>
|
||||
{/if}
|
||||
<div class="templates">
|
||||
{#each templates as template}
|
||||
<div class="template" on:click={() => onSelect(template)}>
|
||||
|
|
|
@ -48,7 +48,6 @@ export const fetchTableData = opts => {
|
|||
const fetchPage = async bookmark => {
|
||||
lastBookmark = bookmark
|
||||
const { tableId, limit, sortColumn, sortOrder, paginate } = options
|
||||
store.update($store => ({ ...$store, loading: true }))
|
||||
const res = await API.post(`/api/${options.tableId}/search`, {
|
||||
tableId,
|
||||
query,
|
||||
|
@ -59,7 +58,6 @@ export const fetchTableData = opts => {
|
|||
paginate,
|
||||
bookmark,
|
||||
})
|
||||
store.update($store => ({ ...$store, loading: false, loaded: true }))
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
|
@ -103,7 +101,7 @@ export const fetchTableData = opts => {
|
|||
if (!schema) {
|
||||
return
|
||||
}
|
||||
store.update($store => ({ ...$store, schema }))
|
||||
store.update($store => ({ ...$store, schema, loading: true }))
|
||||
|
||||
// Work out what sort type to use
|
||||
if (!sortColumn || !schema[sortColumn]) {
|
||||
|
@ -135,6 +133,7 @@ export const fetchTableData = opts => {
|
|||
}
|
||||
|
||||
// Fetch next page
|
||||
store.update($store => ({ ...$store, loading: true }))
|
||||
const page = await fetchPage(state.bookmarks[state.pageNumber + 1])
|
||||
|
||||
// Update state
|
||||
|
@ -148,6 +147,7 @@ export const fetchTableData = opts => {
|
|||
pageNumber: pageNumber + 1,
|
||||
rows: page.rows,
|
||||
bookmarks,
|
||||
loading: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -160,6 +160,7 @@ export const fetchTableData = opts => {
|
|||
}
|
||||
|
||||
// Fetch previous page
|
||||
store.update($store => ({ ...$store, loading: true }))
|
||||
const page = await fetchPage(state.bookmarks[state.pageNumber - 1])
|
||||
|
||||
// Update state
|
||||
|
@ -168,6 +169,7 @@ export const fetchTableData = opts => {
|
|||
...$store,
|
||||
pageNumber: $store.pageNumber - 1,
|
||||
rows: page.rows,
|
||||
loading: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
|
||||
import { post } from "builderStore/api"
|
||||
import { admin } from "stores/portal"
|
||||
|
||||
let submitting = false
|
||||
|
||||
|
@ -20,8 +21,8 @@
|
|||
if (!importResp.ok) {
|
||||
throw new Error(importJson.message)
|
||||
}
|
||||
// now reload to get to login
|
||||
window.location.reload()
|
||||
await admin.checkImportComplete()
|
||||
notifications.success("Import complete, please finish registration!")
|
||||
} catch (error) {
|
||||
notifications.error(error)
|
||||
submitting = false
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let adminUser = {}
|
||||
let error
|
||||
|
@ -23,6 +24,7 @@
|
|||
$: tenantId = $auth.tenantId
|
||||
$: multiTenancyEnabled = $admin.multiTenancy
|
||||
$: cloud = $admin.cloud
|
||||
$: imported = $admin.importComplete
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
|
@ -40,6 +42,12 @@
|
|||
notifications.error(`Failed to create admin user`)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!cloud) {
|
||||
await admin.checkImportComplete()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} padding={false} width="600px">
|
||||
|
@ -73,7 +81,7 @@
|
|||
>
|
||||
Change organisation
|
||||
</ActionButton>
|
||||
{:else if !cloud}
|
||||
{:else if !cloud && !imported}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { onMount } from "svelte"
|
||||
import { admin } from "stores/portal"
|
||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||
import { datasources } from "stores/backend"
|
||||
|
||||
|
@ -10,7 +11,7 @@
|
|||
$datasources.list.length > 1
|
||||
|
||||
onMount(() => {
|
||||
if (!setupComplete) {
|
||||
if (!setupComplete && !$admin.isDev) {
|
||||
modal.show()
|
||||
} else {
|
||||
$goto("./table")
|
||||
|
|
|
@ -34,6 +34,12 @@
|
|||
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
|
||||
|
||||
$: publishedApps = $apps.filter(publishedAppsOnly)
|
||||
|
||||
$: userApps = $auth.user?.builder?.global
|
||||
? publishedApps
|
||||
: publishedApps.filter(app =>
|
||||
Object.keys($auth.user?.roles).includes(app.prodId)
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if $auth.user && loaded}
|
||||
|
@ -82,11 +88,11 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
{#if publishedApps.length}
|
||||
{#if userApps.length}
|
||||
<Heading>Apps</Heading>
|
||||
<div class="group">
|
||||
<Layout gap="S" noPadding>
|
||||
{#each publishedApps as app, idx (app.appId)}
|
||||
{#each userApps as app, idx (app.appId)}
|
||||
<a class="app" target="_blank" href={`/${app.prodId}`}>
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div class="app-info">
|
||||
|
|
|
@ -34,9 +34,13 @@
|
|||
role: {},
|
||||
}
|
||||
|
||||
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "BASIC"
|
||||
const noRoleSchema = {
|
||||
name: { displayName: "App" },
|
||||
}
|
||||
|
||||
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : ""
|
||||
// Merge the Apps list and the roles response to get something that makes sense for the table
|
||||
$: appList = Object.keys($apps?.data).map(id => {
|
||||
$: allAppList = Object.keys($apps?.data).map(id => {
|
||||
const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId
|
||||
const role = $apps?.data?.[id].roles.find(role => role._id === roleId)
|
||||
return {
|
||||
|
@ -45,6 +49,15 @@
|
|||
role: [role],
|
||||
}
|
||||
})
|
||||
|
||||
$: appList = allAppList.filter(app => !!app.role[0])
|
||||
$: noRoleAppList = allAppList
|
||||
.filter(app => !app.role[0])
|
||||
.map(app => {
|
||||
delete app.role
|
||||
return app
|
||||
})
|
||||
|
||||
let selectedApp
|
||||
|
||||
const userFetch = fetchData(`/api/global/users/${userId}`)
|
||||
|
@ -173,6 +186,7 @@
|
|||
<Divider size="S" />
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="S">Configure roles</Heading>
|
||||
<Body>Specify a role to grant access to an app.</Body>
|
||||
<Table
|
||||
on:click={openUpdateRolesModal}
|
||||
schema={roleSchema}
|
||||
|
@ -183,6 +197,21 @@
|
|||
customRenderers={[{ column: "role", component: TagsRenderer }]}
|
||||
/>
|
||||
</Layout>
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="XS">No Access</Heading>
|
||||
<Body
|
||||
>Apps do not appear in the users portal. Public pages may still be viewed
|
||||
if visited directly.</Body
|
||||
>
|
||||
<Table
|
||||
on:click={openUpdateRolesModal}
|
||||
schema={noRoleSchema}
|
||||
data={noRoleAppList}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
/>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Delete user</Heading>
|
||||
|
|
|
@ -6,22 +6,38 @@
|
|||
export let app
|
||||
export let user
|
||||
|
||||
const NO_ACCESS = "NO_ACCESS"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const roles = app.roles
|
||||
let options = roles
|
||||
.filter(role => role._id !== "PUBLIC")
|
||||
.map(role => ({ value: role._id, label: role.name }))
|
||||
let options = roles.map(role => ({ value: role._id, label: role.name }))
|
||||
options.push({ value: NO_ACCESS, label: "No Access" })
|
||||
let selectedRole = user?.roles?.[app?._id]
|
||||
|
||||
async function updateUserRoles() {
|
||||
const res = await users.save({
|
||||
...user,
|
||||
roles: {
|
||||
...user.roles,
|
||||
[app._id]: selectedRole,
|
||||
},
|
||||
})
|
||||
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 {
|
||||
|
|
|
@ -7,8 +7,10 @@ export function createAdminStore() {
|
|||
loaded: false,
|
||||
multiTenancy: false,
|
||||
cloud: false,
|
||||
isDev: false,
|
||||
disableAccountPortal: false,
|
||||
accountPortalUrl: "",
|
||||
importComplete: false,
|
||||
onboardingProgress: 0,
|
||||
checklist: {
|
||||
apps: { checked: false },
|
||||
|
@ -45,11 +47,23 @@ export function createAdminStore() {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -57,6 +71,7 @@ export function createAdminStore() {
|
|||
cloud = json.cloud
|
||||
disableAccountPortal = json.disableAccountPortal
|
||||
accountPortalUrl = json.accountPortalUrl
|
||||
isDev = json.isDev
|
||||
} catch (err) {
|
||||
// just let it stay disabled
|
||||
}
|
||||
|
@ -65,6 +80,7 @@ export function createAdminStore() {
|
|||
store.cloud = cloud
|
||||
store.disableAccountPortal = disableAccountPortal
|
||||
store.accountPortalUrl = accountPortalUrl
|
||||
store.isDev = isDev
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
@ -79,6 +95,7 @@ export function createAdminStore() {
|
|||
return {
|
||||
subscribe: admin.subscribe,
|
||||
init,
|
||||
checkImportComplete,
|
||||
unload,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -2385,7 +2385,7 @@
|
|||
},
|
||||
"dataprovider": {
|
||||
"name": "Data Provider",
|
||||
"info": "Pagination is only available for data stored in internal tables.",
|
||||
"info": "Pagination is only available for data stored in tables.",
|
||||
"icon": "Data",
|
||||
"illegalChildren": ["section"],
|
||||
"hasChildren": true,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"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": "^0.9.160-alpha.4",
|
||||
"@budibase/bbui": "^0.9.167-alpha.12",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^0.9.160-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.167-alpha.12",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
/* Buttons */
|
||||
--spectrum-semantic-cta-color-background-default: var(--primaryColor);
|
||||
--spectrum-semantic-cta-color-background-hover: var(--primaryColorHover);
|
||||
--spectrum-button-primary-s-border-radius: var(--buttonBorderRadius);
|
||||
--spectrum-button-primary-m-border-radius: var(--buttonBorderRadius);
|
||||
--spectrum-button-primary-l-border-radius: var(--buttonBorderRadius);
|
||||
--spectrum-button-primary-xl-border-radius: var(--buttonBorderRadius);
|
||||
|
||||
/* Loading spinners */
|
||||
--spectrum-progresscircle-medium-track-fill-color: var(--primaryColor);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -68,9 +68,9 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^0.9.160-alpha.4",
|
||||
"@budibase/client": "^0.9.160-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.160-alpha.4",
|
||||
"@budibase/auth": "^0.9.167-alpha.12",
|
||||
"@budibase/client": "^0.9.167-alpha.12",
|
||||
"@budibase/string-templates": "^0.9.167-alpha.12",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
"@koa/router": "8.0.0",
|
||||
"@sendgrid/mail": "7.1.1",
|
||||
|
@ -119,6 +119,7 @@
|
|||
"to-json-schema": "0.2.5",
|
||||
"uuid": "3.3.2",
|
||||
"validate.js": "0.13.1",
|
||||
"vm2": "^3.9.3",
|
||||
"yargs": "13.2.4",
|
||||
"zlib": "1.0.5"
|
||||
},
|
||||
|
|
|
@ -86,6 +86,7 @@ async function getAppUrlIfNotInUse(ctx) {
|
|||
if (
|
||||
url &&
|
||||
deployedApps[url] != null &&
|
||||
ctx.params != null &&
|
||||
deployedApps[url].appId !== ctx.params.appId
|
||||
) {
|
||||
ctx.throw(400, "App name/URL is already in use.")
|
||||
|
|
|
@ -28,15 +28,18 @@ exports.exportApps = async ctx => {
|
|||
ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.")
|
||||
}
|
||||
const apps = await getAllApps(CouchDB, { all: true })
|
||||
const globalDBString = await exportDB(getGlobalDBName())
|
||||
const globalDBString = await exportDB(getGlobalDBName(), {
|
||||
filter: doc => !doc._id.startsWith(DocumentTypes.USER),
|
||||
})
|
||||
let allDBs = {
|
||||
global: globalDBString,
|
||||
}
|
||||
for (let app of apps) {
|
||||
const appId = app.appId || app._id
|
||||
// only export the dev apps as they will be the latest, the user can republish the apps
|
||||
// in their self hosted environment
|
||||
if (isDevAppID(app._id)) {
|
||||
allDBs[app.name] = await exportDB(app._id)
|
||||
if (isDevAppID(appId)) {
|
||||
allDBs[app.name] = await exportDB(appId)
|
||||
}
|
||||
}
|
||||
const filename = `cloud-export-${new Date().getTime()}.txt`
|
||||
|
@ -53,16 +56,26 @@ async function getAllDocType(db, docType) {
|
|||
return response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
async function hasBeenImported() {
|
||||
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
|
||||
return true
|
||||
}
|
||||
const apps = await getAllApps(CouchDB, { all: true })
|
||||
return apps.length !== 0
|
||||
}
|
||||
|
||||
exports.hasBeenImported = async ctx => {
|
||||
ctx.body = {
|
||||
imported: await hasBeenImported(),
|
||||
}
|
||||
}
|
||||
|
||||
exports.importApps = async ctx => {
|
||||
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
|
||||
ctx.throw(400, "Importing only allowed in self hosted environments.")
|
||||
}
|
||||
const apps = await getAllApps(CouchDB, { all: true })
|
||||
if (
|
||||
apps.length !== 0 ||
|
||||
!ctx.request.files ||
|
||||
!ctx.request.files.importFile
|
||||
) {
|
||||
const beenImported = await hasBeenImported()
|
||||
if (beenImported || !ctx.request.files || !ctx.request.files.importFile) {
|
||||
ctx.throw(
|
||||
400,
|
||||
"Import file is required and environment must be fresh to import apps."
|
||||
|
@ -80,11 +93,17 @@ exports.importApps = async ctx => {
|
|||
for (let [appName, appImport] of Object.entries(dbs)) {
|
||||
await createApp(appName, appImport)
|
||||
}
|
||||
// once apps are created clean up the global db
|
||||
|
||||
// if there are any users make sure to remove them
|
||||
let users = await getAllDocType(globalDb, DocumentTypes.USER)
|
||||
let userDeletionPromises = []
|
||||
for (let user of users) {
|
||||
delete user.tenantId
|
||||
userDeletionPromises.push(globalDb.remove(user._id, user._rev))
|
||||
}
|
||||
if (userDeletionPromises.length > 0) {
|
||||
await Promise.all(userDeletionPromises)
|
||||
}
|
||||
|
||||
await globalDb.bulkDocs(users)
|
||||
ctx.body = {
|
||||
message: "Apps successfully imported.",
|
||||
|
|
|
@ -7,11 +7,13 @@ const { clearLock } = require("../../utilities/redis")
|
|||
const { Replication } = require("@budibase/auth").db
|
||||
const { DocumentTypes } = require("../../db/utils")
|
||||
|
||||
async function redirect(ctx, method) {
|
||||
async function redirect(ctx, method, path = "global") {
|
||||
const { devPath } = ctx.params
|
||||
const queryString = ctx.originalUrl.split("?")[1] || ""
|
||||
const response = await fetch(
|
||||
checkSlashesInUrl(`${env.WORKER_URL}/api/global/${devPath}?${queryString}`),
|
||||
checkSlashesInUrl(
|
||||
`${env.WORKER_URL}/api/${path}/${devPath}?${queryString}`
|
||||
),
|
||||
request(
|
||||
ctx,
|
||||
{
|
||||
|
@ -41,16 +43,22 @@ async function redirect(ctx, method) {
|
|||
ctx.cookies
|
||||
}
|
||||
|
||||
exports.redirectGet = async ctx => {
|
||||
await redirect(ctx, "GET")
|
||||
exports.buildRedirectGet = path => {
|
||||
return async ctx => {
|
||||
await redirect(ctx, "GET", path)
|
||||
}
|
||||
}
|
||||
|
||||
exports.redirectPost = async ctx => {
|
||||
await redirect(ctx, "POST")
|
||||
exports.buildRedirectPost = path => {
|
||||
return async ctx => {
|
||||
await redirect(ctx, "POST", path)
|
||||
}
|
||||
}
|
||||
|
||||
exports.redirectDelete = async ctx => {
|
||||
await redirect(ctx, "DELETE")
|
||||
exports.buildRedirectDelete = path => {
|
||||
return async ctx => {
|
||||
await redirect(ctx, "DELETE", path)
|
||||
}
|
||||
}
|
||||
|
||||
exports.clearLock = async ctx => {
|
||||
|
|
|
@ -4,6 +4,7 @@ const { generateQueryID, getQueryParams } = require("../../db/utils")
|
|||
const { integrations } = require("../../integrations")
|
||||
const { BaseQueryVerbs } = require("../../constants")
|
||||
const env = require("../../environment")
|
||||
const ScriptRunner = require("../../utilities/scriptRunner")
|
||||
|
||||
// simple function to append "readable" to all read queries
|
||||
function enrichQueries(input) {
|
||||
|
@ -28,12 +29,39 @@ function formatResponse(resp) {
|
|||
resp = { response: resp }
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(resp)) {
|
||||
resp = [resp]
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
async function runAndTransform(
|
||||
integration,
|
||||
queryVerb,
|
||||
enrichedQuery,
|
||||
transformer
|
||||
) {
|
||||
let rows = formatResponse(await integration[queryVerb](enrichedQuery))
|
||||
|
||||
// transform as required
|
||||
if (transformer) {
|
||||
const runner = new ScriptRunner(transformer, { data: rows })
|
||||
rows = runner.execute()
|
||||
}
|
||||
|
||||
// needs to an array for next step
|
||||
if (!Array.isArray(rows)) {
|
||||
rows = [rows]
|
||||
}
|
||||
|
||||
// map into JSON if just raw primitive here
|
||||
if (rows.find(row => typeof row !== "object")) {
|
||||
rows = rows.map(value => ({ value }))
|
||||
}
|
||||
|
||||
// get all the potential fields in the schema
|
||||
let keys = rows.flatMap(Object.keys)
|
||||
|
||||
return { rows, keys }
|
||||
}
|
||||
|
||||
exports.fetch = async function (ctx) {
|
||||
const db = new CouchDB(ctx.appId)
|
||||
|
||||
|
@ -122,15 +150,16 @@ exports.preview = async function (ctx) {
|
|||
ctx.throw(400, "Integration type does not exist.")
|
||||
}
|
||||
|
||||
const { fields, parameters, queryVerb } = ctx.request.body
|
||||
|
||||
const { fields, parameters, queryVerb, transformer } = ctx.request.body
|
||||
const enrichedQuery = await enrichQueryFields(fields, parameters)
|
||||
|
||||
const integration = new Integration(datasource.config)
|
||||
const rows = formatResponse(await integration[queryVerb](enrichedQuery))
|
||||
|
||||
// get all the potential fields in the schema
|
||||
const keys = rows.flatMap(Object.keys)
|
||||
const { rows, keys } = await runAndTransform(
|
||||
integration,
|
||||
queryVerb,
|
||||
enrichedQuery,
|
||||
transformer
|
||||
)
|
||||
|
||||
ctx.body = {
|
||||
rows,
|
||||
|
@ -158,10 +187,16 @@ exports.execute = async function (ctx) {
|
|||
query.fields,
|
||||
ctx.request.body.parameters
|
||||
)
|
||||
|
||||
const integration = new Integration(datasource.config)
|
||||
|
||||
// call the relevant CRUD method on the integration class
|
||||
ctx.body = formatResponse(await integration[query.queryVerb](enrichedQuery))
|
||||
const { rows } = await runAndTransform(
|
||||
integration,
|
||||
query.queryVerb,
|
||||
enrichedQuery,
|
||||
query.transformer
|
||||
)
|
||||
ctx.body = rows
|
||||
// cleanup
|
||||
if (integration.end) {
|
||||
integration.end()
|
||||
|
|
|
@ -1,24 +1,9 @@
|
|||
const fetch = require("node-fetch")
|
||||
const vm = require("vm")
|
||||
|
||||
class ScriptExecutor {
|
||||
constructor(body) {
|
||||
const code = `let fn = () => {\n${body.script}\n}; out = fn();`
|
||||
this.script = new vm.Script(code)
|
||||
this.context = vm.createContext(body.context)
|
||||
this.context.fetch = fetch
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.script.runInContext(this.context)
|
||||
return this.context.out
|
||||
}
|
||||
}
|
||||
const ScriptRunner = require("../../utilities/scriptRunner")
|
||||
|
||||
exports.execute = async function (ctx) {
|
||||
const executor = new ScriptExecutor(ctx.request.body)
|
||||
|
||||
ctx.body = executor.execute()
|
||||
const { script, context } = ctx.request.body
|
||||
const runner = new ScriptRunner(script, context)
|
||||
ctx.body = runner.execute()
|
||||
}
|
||||
|
||||
exports.save = async function (ctx) {
|
||||
|
|
|
@ -91,6 +91,9 @@ exports.save = async function (ctx) {
|
|||
for (let propKey of Object.keys(tableToSave.schema)) {
|
||||
let column = tableToSave.schema[propKey]
|
||||
let oldColumn = oldTable.schema[propKey]
|
||||
if (oldColumn && oldColumn.type === "internal") {
|
||||
oldColumn.type = "auto"
|
||||
}
|
||||
if (oldColumn && oldColumn.type !== column.type) {
|
||||
ctx.throw(400, "Cannot change the type of a column")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const linkRows = require("../../../db/linkedRows")
|
||||
const csvParser = require("../../../utilities/csvParser")
|
||||
const {
|
||||
getRowParams,
|
||||
|
@ -93,19 +92,10 @@ exports.handleDataImport = async (appId, user, table, dataImport) => {
|
|||
}
|
||||
}
|
||||
|
||||
// make sure link rows are up to date
|
||||
finalData.push(
|
||||
linkRows.updateLinks({
|
||||
appId,
|
||||
eventType: linkRows.EventType.ROW_SAVE,
|
||||
row,
|
||||
tableId: row.tableId,
|
||||
table,
|
||||
})
|
||||
)
|
||||
finalData.push(row)
|
||||
}
|
||||
|
||||
await db.bulkDocs(await Promise.all(finalData))
|
||||
await db.bulkDocs(finalData)
|
||||
let response = await db.put(table)
|
||||
table._rev = response._rev
|
||||
}
|
||||
|
|
|
@ -7,11 +7,23 @@ const DEFAULT_TEMPLATES_BUCKET =
|
|||
|
||||
exports.fetch = async function (ctx) {
|
||||
const { type = "app" } = ctx.query
|
||||
const response = await fetch(
|
||||
`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json`
|
||||
)
|
||||
const json = await response.json()
|
||||
ctx.body = Object.values(json.templates[type])
|
||||
let response,
|
||||
error = false
|
||||
try {
|
||||
response = await fetch(`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json`)
|
||||
if (response.status !== 200) {
|
||||
error = true
|
||||
}
|
||||
} catch (err) {
|
||||
error = true
|
||||
}
|
||||
// if there is an error, simply return no templates
|
||||
if (!error && response) {
|
||||
const json = await response.json()
|
||||
ctx.body = Object.values(json.templates[type])
|
||||
} else {
|
||||
ctx.body = []
|
||||
}
|
||||
}
|
||||
|
||||
// can't currently test this, have to ignore from coverage
|
||||
|
|
|
@ -9,5 +9,6 @@ router
|
|||
.get("/api/cloud/export", authorized(BUILDER), controller.exportApps)
|
||||
// has to be public, only run if apps don't exist
|
||||
.post("/api/cloud/import", controller.importApps)
|
||||
.get("/api/cloud/import/complete", controller.hasBeenImported)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -6,11 +6,16 @@ const { BUILDER } = require("@budibase/auth/permissions")
|
|||
|
||||
const router = Router()
|
||||
|
||||
if (env.isDev() || env.isTest()) {
|
||||
function redirectPath(path) {
|
||||
router
|
||||
.get("/api/global/:devPath(.*)", controller.redirectGet)
|
||||
.post("/api/global/:devPath(.*)", controller.redirectPost)
|
||||
.delete("/api/global/:devPath(.*)", controller.redirectDelete)
|
||||
.get(`/api/${path}/:devPath(.*)`, controller.buildRedirectGet(path))
|
||||
.post(`/api/${path}/:devPath(.*)`, controller.buildRedirectPost(path))
|
||||
.delete(`/api/${path}/:devPath(.*)`, controller.buildRedirectDelete(path))
|
||||
}
|
||||
|
||||
if (env.isDev() || env.isTest()) {
|
||||
redirectPath("global")
|
||||
redirectPath("system")
|
||||
}
|
||||
|
||||
router
|
||||
|
|
|
@ -31,7 +31,8 @@ function generateQueryValidation() {
|
|||
})),
|
||||
queryVerb: Joi.string().allow().required(),
|
||||
extra: Joi.object().optional(),
|
||||
schema: Joi.object({}).required().unknown(true)
|
||||
schema: Joi.object({}).required().unknown(true),
|
||||
transformer: Joi.string().optional(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -42,6 +43,7 @@ function generateQueryPreviewValidation() {
|
|||
queryVerb: Joi.string().allow().required(),
|
||||
extra: Joi.object().optional(),
|
||||
datasourceId: Joi.string().required(),
|
||||
transformer: Joi.string().optional(),
|
||||
parameters: Joi.object({}).required().unknown(true)
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ const createRow = require("./steps/createRow")
|
|||
const updateRow = require("./steps/updateRow")
|
||||
const deleteRow = require("./steps/deleteRow")
|
||||
const executeScript = require("./steps/executeScript")
|
||||
const bash = require("./steps/bash")
|
||||
const executeQuery = require("./steps/executeQuery")
|
||||
const outgoingWebhook = require("./steps/outgoingWebhook")
|
||||
const serverLog = require("./steps/serverLog")
|
||||
|
@ -14,6 +13,7 @@ const integromat = require("./steps/integromat")
|
|||
let filter = require("./steps/filter")
|
||||
let delay = require("./steps/delay")
|
||||
let queryRow = require("./steps/queryRows")
|
||||
const env = require("../environment")
|
||||
|
||||
const ACTION_IMPLS = {
|
||||
SEND_EMAIL_SMTP: sendSmtpEmail.run,
|
||||
|
@ -22,7 +22,6 @@ const ACTION_IMPLS = {
|
|||
DELETE_ROW: deleteRow.run,
|
||||
OUTGOING_WEBHOOK: outgoingWebhook.run,
|
||||
EXECUTE_SCRIPT: executeScript.run,
|
||||
EXECUTE_BASH: bash.run,
|
||||
EXECUTE_QUERY: executeQuery.run,
|
||||
SERVER_LOG: serverLog.run,
|
||||
DELAY: delay.run,
|
||||
|
@ -42,7 +41,6 @@ const ACTION_DEFINITIONS = {
|
|||
OUTGOING_WEBHOOK: outgoingWebhook.definition,
|
||||
EXECUTE_SCRIPT: executeScript.definition,
|
||||
EXECUTE_QUERY: executeQuery.definition,
|
||||
EXECUTE_BASH: bash.definition,
|
||||
SERVER_LOG: serverLog.definition,
|
||||
DELAY: delay.definition,
|
||||
FILTER: filter.definition,
|
||||
|
@ -54,6 +52,15 @@ const ACTION_DEFINITIONS = {
|
|||
integromat: integromat.definition,
|
||||
}
|
||||
|
||||
// don't add the bash script/definitions unless in self host
|
||||
// the fact this isn't included in any definitions means it cannot be
|
||||
// ran at all
|
||||
if (env.SELF_HOSTED) {
|
||||
const bash = require("./steps/bash")
|
||||
ACTION_IMPLS["EXECUTE_BASH"] = bash.run
|
||||
ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
exports.getAction = async function (actionName) {
|
||||
if (ACTION_IMPLS[actionName] != null) {
|
||||
|
|
|
@ -77,7 +77,7 @@ exports.run = async function ({ inputs }) {
|
|||
const { status, message } = await getFetchResponse(response)
|
||||
return {
|
||||
httpStatus: status,
|
||||
success: status === 200,
|
||||
success: status === 200 || status === 204,
|
||||
response: message,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ const env = require("../environment")
|
|||
const usage = require("../utilities/usageQuota")
|
||||
|
||||
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
|
||||
const STOPPED_STATUS = { success: false, status: "STOPPED" }
|
||||
|
||||
/**
|
||||
* The automation orchestrator is a class responsible for executing automations.
|
||||
|
@ -68,7 +69,13 @@ class Orchestrator {
|
|||
async execute() {
|
||||
let automation = this._automation
|
||||
const app = await this.getApp()
|
||||
let stopped = false
|
||||
for (let step of automation.definition.steps) {
|
||||
// execution stopped, record state for that
|
||||
if (stopped) {
|
||||
this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS)
|
||||
continue
|
||||
}
|
||||
let stepFn = await this.getStepFunctionality(step.stepId)
|
||||
step.inputs = await processObject(step.inputs, this._context)
|
||||
step.inputs = automationUtils.cleanInputValues(
|
||||
|
@ -86,10 +93,17 @@ class Orchestrator {
|
|||
context: this._context,
|
||||
})
|
||||
})
|
||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
||||
break
|
||||
}
|
||||
this._context.steps.push(outputs)
|
||||
// if filter causes us to stop execution don't break the loop, set a var
|
||||
// so that we can finish iterating through the steps and record that it stopped
|
||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
||||
stopped = true
|
||||
this.updateExecutionOutput(step.id, step.stepId, step.inputs, {
|
||||
...outputs,
|
||||
...STOPPED_STATUS,
|
||||
})
|
||||
continue
|
||||
}
|
||||
this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs)
|
||||
} catch (err) {
|
||||
console.error(`Automation error - ${step.stepId} - ${err}`)
|
||||
|
@ -99,7 +113,7 @@ class Orchestrator {
|
|||
|
||||
// Increment quota for automation runs
|
||||
if (!env.SELF_HOSTED && !isDevAppID(this._appId)) {
|
||||
usage.update(usage.Properties.AUTOMATION, 1)
|
||||
await usage.update(usage.Properties.AUTOMATION, 1)
|
||||
}
|
||||
return this.executionOutput
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ exports.FieldTypes = {
|
|||
FORMULA: "formula",
|
||||
AUTO: "auto",
|
||||
JSON: "json",
|
||||
INTERNAL: "internal",
|
||||
}
|
||||
|
||||
exports.RelationshipTypes = {
|
||||
|
|
|
@ -20,12 +20,14 @@ export enum QueryTypes {
|
|||
|
||||
export enum DatasourceFieldTypes {
|
||||
STRING = "string",
|
||||
LONGFORM = "longForm",
|
||||
BOOLEAN = "boolean",
|
||||
NUMBER = "number",
|
||||
PASSWORD = "password",
|
||||
LIST = "list",
|
||||
OBJECT = "object",
|
||||
JSON = "json",
|
||||
FILE = "file",
|
||||
}
|
||||
|
||||
export enum SourceNames {
|
||||
|
|
|
@ -74,9 +74,10 @@ module.exports = {
|
|||
},
|
||||
}
|
||||
|
||||
// convert any strings to numbers if required, like "0" would be true otherwise
|
||||
// clean up any environment variable edge cases
|
||||
for (let [key, value] of Object.entries(module.exports)) {
|
||||
if (typeof value === "string" && !isNaN(parseInt(value))) {
|
||||
module.exports[key] = parseInt(value)
|
||||
// handle the edge case of "0" to disable an environment variable
|
||||
if (value === "0") {
|
||||
module.exports[key] = 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,9 @@ function addFilters(
|
|||
}
|
||||
|
||||
function addRelationships(
|
||||
knex: Knex,
|
||||
query: KnexQuery,
|
||||
fields: string | string[],
|
||||
fromTable: string,
|
||||
relationships: RelationshipsJson[] | undefined
|
||||
): KnexQuery {
|
||||
|
@ -114,7 +116,7 @@ function addRelationships(
|
|||
query = query.leftJoin(
|
||||
toTable,
|
||||
`${fromTable}.${from}`,
|
||||
`${relationship.tableName}.${to}`
|
||||
`${toTable}.${to}`
|
||||
)
|
||||
} else {
|
||||
const throughTable = relationship.through
|
||||
|
@ -130,7 +132,7 @@ function addRelationships(
|
|||
.leftJoin(toTable, `${toTable}.${toPrimary}`, `${throughTable}.${to}`)
|
||||
}
|
||||
}
|
||||
return query
|
||||
return query.limit(BASE_LIMIT)
|
||||
}
|
||||
|
||||
function buildCreate(
|
||||
|
@ -199,7 +201,7 @@ function buildRead(knex: Knex, json: QueryJson, limit: number): KnexQuery {
|
|||
[tableName]: query,
|
||||
}).select(selectStatement)
|
||||
// handle joins
|
||||
return addRelationships(preQuery, tableName, relationships)
|
||||
return addRelationships(knex, preQuery, selectStatement, tableName, relationships)
|
||||
}
|
||||
|
||||
function buildUpdate(
|
||||
|
|
|
@ -28,6 +28,8 @@ module PostgresModule {
|
|||
user: string
|
||||
password: string
|
||||
ssl?: boolean
|
||||
ca?: string
|
||||
rejectUnauthorized?: boolean
|
||||
}
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
|
@ -67,6 +69,16 @@ module PostgresModule {
|
|||
default: false,
|
||||
required: false,
|
||||
},
|
||||
rejectUnauthorized: {
|
||||
type: DatasourceFieldTypes.BOOLEAN,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
ca: {
|
||||
type: DatasourceFieldTypes.LONGFORM,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
create: {
|
||||
|
@ -144,7 +156,12 @@ module PostgresModule {
|
|||
|
||||
let newConfig = {
|
||||
...this.config,
|
||||
ssl: this.config.ssl ? { rejectUnauthorized: true } : undefined,
|
||||
ssl: this.config.ssl
|
||||
? {
|
||||
rejectUnauthorized: this.config.rejectUnauthorized,
|
||||
ca: this.config.ca,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
if (!this.pool) {
|
||||
this.pool = new Pool(newConfig)
|
||||
|
|
|
@ -27,7 +27,7 @@ describe("MongoDB Integration", () => {
|
|||
const body = {
|
||||
name: "Hello"
|
||||
}
|
||||
const response = await config.integration.create({
|
||||
await config.integration.create({
|
||||
index: indexName,
|
||||
json: body,
|
||||
extra: { collection: 'testCollection', actionTypes: 'insertOne'}
|
||||
|
@ -54,7 +54,7 @@ describe("MongoDB Integration", () => {
|
|||
},
|
||||
extra: { collection: 'testCollection', actionTypes: 'deleteOne'}
|
||||
}
|
||||
const response = await config.integration.delete(query)
|
||||
await config.integration.delete(query)
|
||||
expect(config.integration.client.deleteOne).toHaveBeenCalledWith(query.json)
|
||||
})
|
||||
|
||||
|
@ -65,7 +65,7 @@ describe("MongoDB Integration", () => {
|
|||
},
|
||||
extra: { collection: 'testCollection', actionTypes: 'updateOne'}
|
||||
}
|
||||
const response = await config.integration.update(query)
|
||||
await config.integration.update(query)
|
||||
expect(config.integration.client.updateOne).toHaveBeenCalledWith(query.json)
|
||||
})
|
||||
|
||||
|
@ -75,10 +75,14 @@ describe("MongoDB Integration", () => {
|
|||
const query = {
|
||||
extra: { collection: 'testCollection', actionTypes: 'deleteOne'}
|
||||
}
|
||||
// Weird, need to do an IIFE for jest to recognize that it throws
|
||||
expect(() => config.integration.read(query)()).toThrow(expect.any(Object))
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await config.integration.read(query)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
expect(error).toBeDefined()
|
||||
restore()
|
||||
})
|
||||
|
||||
})
|
|
@ -45,7 +45,7 @@ module.exports = async (ctx, next) => {
|
|||
const globalUser = await getCachedSelf(ctx, requestAppId)
|
||||
appId = requestAppId
|
||||
// retrieving global user gets the right role
|
||||
roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC
|
||||
roleId = globalUser.roleId || roleId
|
||||
}
|
||||
|
||||
// nothing more to do
|
||||
|
|
|
@ -127,8 +127,8 @@ describe("Current app middleware", () => {
|
|||
} else {
|
||||
expect(cookieFn).not.toHaveBeenCalled()
|
||||
}
|
||||
expect(config.ctx.roleId).toEqual("BASIC")
|
||||
expect(config.ctx.user.role._id).toEqual("BASIC")
|
||||
expect(config.ctx.roleId).toEqual("PUBLIC")
|
||||
expect(config.ctx.user.role._id).toEqual("PUBLIC")
|
||||
expect(config.ctx.appId).toEqual("app_test")
|
||||
expect(config.next).toHaveBeenCalled()
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ describe("Current app middleware", () => {
|
|||
return "app_test"
|
||||
},
|
||||
setCookie: jest.fn(),
|
||||
getCookie: () => ({appId: "app_test", roleId: "BASIC"}),
|
||||
getCookie: () => ({appId: "app_test", roleId: "PUBLIC"}),
|
||||
},
|
||||
constants: { Cookies: {} },
|
||||
}))
|
||||
|
|
|
@ -26,7 +26,7 @@ exports.updateAppRole = (appId, user) => {
|
|||
if (!user.roleId && user.builder && user.builder.global) {
|
||||
user.roleId = BUILTIN_ROLE_IDS.ADMIN
|
||||
} else if (!user.roleId) {
|
||||
user.roleId = BUILTIN_ROLE_IDS.BASIC
|
||||
user.roleId = BUILTIN_ROLE_IDS.PUBLIC
|
||||
}
|
||||
delete user.roles
|
||||
return user
|
||||
|
|
|
@ -150,6 +150,10 @@ exports.processAutoColumn = processAutoColumn
|
|||
* @returns {object} The coerced value
|
||||
*/
|
||||
exports.coerce = (row, type) => {
|
||||
// no coercion specified for type, skip it
|
||||
if (!TYPE_TRANSFORM_MAP[type]) {
|
||||
return row
|
||||
}
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
|
||||
return TYPE_TRANSFORM_MAP[type][row]
|
||||
|
@ -196,6 +200,12 @@ exports.inputProcessing = (
|
|||
clonedRow[key] = exports.coerce(value, field.type)
|
||||
}
|
||||
}
|
||||
|
||||
if (!clonedRow._id || !clonedRow._rev) {
|
||||
clonedRow._id = row._id
|
||||
clonedRow._rev = row._rev
|
||||
}
|
||||
|
||||
// handle auto columns - this returns an object like {table, row}
|
||||
return processAutoColumn(user, copiedTable, clonedRow, opts)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
const fetch = require("node-fetch")
|
||||
const { VM, VMScript } = require("vm2")
|
||||
|
||||
class ScriptRunner {
|
||||
constructor(script, context) {
|
||||
const code = `let fn = () => {\n${script}\n}; results.out = fn();`
|
||||
this.vm = new VM()
|
||||
this.results = { out: "" }
|
||||
this.vm.setGlobals(context)
|
||||
this.vm.setGlobal("fetch", fetch)
|
||||
this.vm.setGlobal("results", this.results)
|
||||
this.script = new VMScript(code)
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.vm.run(this.script)
|
||||
return this.results.out
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ScriptRunner
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
@ -24,7 +24,8 @@
|
|||
"dayjs": "^1.10.4",
|
||||
"handlebars": "^4.7.6",
|
||||
"handlebars-utils": "^1.0.6",
|
||||
"lodash": "^4.17.20"
|
||||
"lodash": "^4.17.20",
|
||||
"vm2": "^3.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.1.0",
|
||||
|
|
|
@ -7,18 +7,6 @@ import globals from "rollup-plugin-node-globals"
|
|||
|
||||
const production = !process.env.ROLLUP_WATCH
|
||||
|
||||
const plugins = [
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: true,
|
||||
}),
|
||||
commonjs(),
|
||||
globals(),
|
||||
builtins(),
|
||||
json(),
|
||||
production && terser(),
|
||||
]
|
||||
|
||||
export default [
|
||||
{
|
||||
input: "src/index.mjs",
|
||||
|
@ -27,18 +15,16 @@ export default [
|
|||
format: "esm",
|
||||
file: "./dist/bundle.mjs",
|
||||
},
|
||||
plugins,
|
||||
plugins: [
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: true,
|
||||
}),
|
||||
commonjs(),
|
||||
globals(),
|
||||
builtins(),
|
||||
json(),
|
||||
production && terser(),
|
||||
],
|
||||
},
|
||||
// This is the valid configuration for a CommonJS bundle, but since we have
|
||||
// no use for this, it's better to leave it out.
|
||||
// {
|
||||
// input: "src/index.cjs",
|
||||
// output: {
|
||||
// sourcemap: !production,
|
||||
// format: "cjs",
|
||||
// file: "./dist/bundle.cjs",
|
||||
// exports: "named",
|
||||
// },
|
||||
// plugins,
|
||||
// },
|
||||
]
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
class Helper {
|
||||
constructor(name, fn) {
|
||||
constructor(name, fn, useValueFallback = true) {
|
||||
this.name = name
|
||||
this.fn = fn
|
||||
this.useValueFallback = useValueFallback
|
||||
}
|
||||
|
||||
register(handlebars) {
|
||||
// wrap the function so that no helper can cause handlebars to break
|
||||
handlebars.registerHelper(this.name, value => {
|
||||
return this.fn(value) || value
|
||||
handlebars.registerHelper(this.name, (value, info) => {
|
||||
let context = {}
|
||||
if (info && info.data && info.data.root) {
|
||||
context = info.data.root
|
||||
}
|
||||
const result = this.fn(value, context)
|
||||
if (result == null) {
|
||||
return this.useValueFallback ? value : null
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ module.exports.HelperFunctionNames = {
|
|||
OBJECT: "object",
|
||||
ALL: "all",
|
||||
LITERAL: "literal",
|
||||
JS: "js",
|
||||
}
|
||||
|
||||
module.exports.LITERAL_MARKER = "%LITERAL%"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const Helper = require("./Helper")
|
||||
const { SafeString } = require("handlebars")
|
||||
const externalHandlebars = require("./external")
|
||||
const { processJS } = require("./javascript")
|
||||
const {
|
||||
HelperFunctionNames,
|
||||
HelperFunctionBuiltin,
|
||||
|
@ -17,6 +18,8 @@ const HELPERS = [
|
|||
new Helper(HelperFunctionNames.OBJECT, value => {
|
||||
return new SafeString(JSON.stringify(value))
|
||||
}),
|
||||
// javascript helper
|
||||
new Helper(HelperFunctionNames.JS, processJS, false),
|
||||
// this help is applied to all statements
|
||||
new Helper(HelperFunctionNames.ALL, value => {
|
||||
if (
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
const { atob } = require("../utilities")
|
||||
|
||||
// The method of executing JS scripts depends on the bundle being built.
|
||||
// This setter is used in the entrypoint (either index.cjs or index.mjs).
|
||||
let runJS
|
||||
module.exports.setJSRunner = runner => (runJS = runner)
|
||||
|
||||
// Helper utility to strip square brackets from a value
|
||||
const removeSquareBrackets = value => {
|
||||
if (!value || typeof value !== "string") {
|
||||
return value
|
||||
}
|
||||
const regex = /\[+(.+)]+/
|
||||
const matches = value.match(regex)
|
||||
if (matches && matches[1]) {
|
||||
return matches[1]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Our context getter function provided to JS code as $.
|
||||
// Extracts a value from context.
|
||||
const getContextValue = (path, context) => {
|
||||
let data = context
|
||||
path.split(".").forEach(key => {
|
||||
if (data == null || typeof data !== "object") {
|
||||
return null
|
||||
}
|
||||
data = data[removeSquareBrackets(key)]
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// Evaluates JS code against a certain context
|
||||
module.exports.processJS = (handlebars, context) => {
|
||||
try {
|
||||
// Wrap JS in a function and immediately invoke it.
|
||||
// This is required to allow the final `return` statement to be valid.
|
||||
const js = `function run(){${atob(handlebars)}};run();`
|
||||
|
||||
// Our $ context function gets a value from context
|
||||
const sandboxContext = { $: path => getContextValue(path, context) }
|
||||
|
||||
// Create a sandbox with out context and run the JS
|
||||
return runJS(js, sandboxContext)
|
||||
} catch (error) {
|
||||
return "Error while executing JS"
|
||||
}
|
||||
}
|
|
@ -1,161 +1,28 @@
|
|||
const handlebars = require("handlebars")
|
||||
const { registerAll } = require("./helpers/index")
|
||||
const processors = require("./processors")
|
||||
const { removeHandlebarsStatements } = require("./utilities")
|
||||
const manifest = require("../manifest.json")
|
||||
|
||||
const hbsInstance = handlebars.create()
|
||||
registerAll(hbsInstance)
|
||||
const { VM } = require("vm2")
|
||||
const templates = require("./index.js")
|
||||
const { setJSRunner } = require("./helpers/javascript")
|
||||
|
||||
/**
|
||||
* utility function to check if the object is valid
|
||||
* CJS entrypoint for rollup
|
||||
*/
|
||||
function testObject(object) {
|
||||
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
||||
try {
|
||||
JSON.stringify(object)
|
||||
} catch (err) {
|
||||
throw "Unable to process inputs to JSON, cannot recurse"
|
||||
}
|
||||
}
|
||||
module.exports.isValid = templates.isValid
|
||||
module.exports.makePropSafe = templates.makePropSafe
|
||||
module.exports.getManifest = templates.getManifest
|
||||
module.exports.isJSBinding = templates.isJSBinding
|
||||
module.exports.encodeJSBinding = templates.encodeJSBinding
|
||||
module.exports.decodeJSBinding = templates.decodeJSBinding
|
||||
module.exports.processStringSync = templates.processStringSync
|
||||
module.exports.processObjectSync = templates.processObjectSync
|
||||
module.exports.processString = templates.processString
|
||||
module.exports.processObject = templates.processObject
|
||||
|
||||
/**
|
||||
* Given an input object this will recurse through all props to try and update any handlebars statements within.
|
||||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||
* if the structure contains any cycles then this will fail.
|
||||
* @param {object} context The context that handlebars should fill data from.
|
||||
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||
* Use vm2 to run JS scripts in a node env
|
||||
*/
|
||||
module.exports.processObject = async (object, context) => {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
if (object[key] != null) {
|
||||
let val = object[key]
|
||||
if (typeof val === "string") {
|
||||
object[key] = await module.exports.processString(object[key], context)
|
||||
} else if (typeof val === "object") {
|
||||
object[key] = await module.exports.processObject(object[key], context)
|
||||
}
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
/**
|
||||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||
* then nothing will occur.
|
||||
* @param {string} string The template string which is the filled from the context object.
|
||||
* @param {object} context An object of information which will be used to enrich the string.
|
||||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processString = async (string, context) => {
|
||||
// TODO: carry out any async calls before carrying out async call
|
||||
return module.exports.processStringSync(string, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is
|
||||
* a pure sync call and therefore does not have the full functionality of the async call.
|
||||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||
* if the structure contains any cycles then this will fail.
|
||||
* @param {object} context The context that handlebars should fill data from.
|
||||
* @returns {object|array} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObjectSync = (object, context) => {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
let val = object[key]
|
||||
if (typeof val === "string") {
|
||||
object[key] = module.exports.processStringSync(object[key], context)
|
||||
} else if (typeof val === "object") {
|
||||
object[key] = module.exports.processObjectSync(object[key], context)
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
/**
|
||||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
||||
* @param {string} string The template string which is the filled from the context object.
|
||||
* @param {object} context An object of information which will be used to enrich the string.
|
||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processStringSync = (string, context) => {
|
||||
if (!exports.isValid(string)) {
|
||||
return string
|
||||
}
|
||||
// take a copy of input incase error
|
||||
const input = string
|
||||
if (typeof string !== "string") {
|
||||
throw "Cannot process non-string types."
|
||||
}
|
||||
try {
|
||||
string = processors.preprocess(string)
|
||||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
||||
const template = hbsInstance.compile(string, {
|
||||
strict: false,
|
||||
})
|
||||
return processors.postprocess(template({
|
||||
now: new Date().toISOString(),
|
||||
...context,
|
||||
}))
|
||||
} catch (err) {
|
||||
return removeHandlebarsStatements(input)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
|
||||
* @param {string} property The property which is to be wrapped.
|
||||
* @returns {string} The wrapped property ready to be added to a templating string.
|
||||
*/
|
||||
module.exports.makePropSafe = property => {
|
||||
return `[${property}]`.replace("[[", "[").replace("]]", "]")
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
|
||||
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
|
||||
* @returns {boolean} Whether or not the input string is valid.
|
||||
*/
|
||||
module.exports.isValid = string => {
|
||||
const validCases = [
|
||||
"string",
|
||||
"number",
|
||||
"object",
|
||||
"array",
|
||||
"cannot read property",
|
||||
"undefined",
|
||||
]
|
||||
// this is a portion of a specific string always output by handlebars in the case of a syntax error
|
||||
const invalidCases = [`expecting '`]
|
||||
// don't really need a real context to check if its valid
|
||||
const context = {}
|
||||
try {
|
||||
hbsInstance.compile(processors.preprocess(string, false))(context)
|
||||
return true
|
||||
} catch (err) {
|
||||
const msg = err && err.message ? err.message : err
|
||||
if (!msg) {
|
||||
return false
|
||||
}
|
||||
const invalidCase = invalidCases.some(invalidCase =>
|
||||
msg.toLowerCase().includes(invalidCase)
|
||||
)
|
||||
const validCase = validCases.some(validCase =>
|
||||
msg.toLowerCase().includes(validCase)
|
||||
)
|
||||
// special case for maths functions - don't have inputs yet
|
||||
return validCase && !invalidCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We have generated a static manifest file from the helpers that this string templating package makes use of.
|
||||
* This manifest provides information about each of the helpers and how it can be used.
|
||||
* @returns The manifest JSON which has been generated from the helpers.
|
||||
*/
|
||||
module.exports.getManifest = () => {
|
||||
return manifest
|
||||
}
|
||||
setJSRunner((js, context) => {
|
||||
const vm = new VM({
|
||||
sandbox: context,
|
||||
timeout: 1000
|
||||
})
|
||||
return vm.run(js)
|
||||
})
|
|
@ -0,0 +1,204 @@
|
|||
const handlebars = require("handlebars")
|
||||
const { registerAll } = require("./helpers/index")
|
||||
const processors = require("./processors")
|
||||
const { removeHandlebarsStatements, atob, btoa } = require("./utilities")
|
||||
const manifest = require("../manifest.json")
|
||||
|
||||
const hbsInstance = handlebars.create()
|
||||
registerAll(hbsInstance)
|
||||
|
||||
/**
|
||||
* utility function to check if the object is valid
|
||||
*/
|
||||
function testObject(object) {
|
||||
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
||||
try {
|
||||
JSON.stringify(object)
|
||||
} catch (err) {
|
||||
throw "Unable to process inputs to JSON, cannot recurse"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an input object this will recurse through all props to try and update any handlebars statements within.
|
||||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||
* if the structure contains any cycles then this will fail.
|
||||
* @param {object} context The context that handlebars should fill data from.
|
||||
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObject = async (object, context) => {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
if (object[key] != null) {
|
||||
let val = object[key]
|
||||
if (typeof val === "string") {
|
||||
object[key] = await module.exports.processString(object[key], context)
|
||||
} else if (typeof val === "object") {
|
||||
object[key] = await module.exports.processObject(object[key], context)
|
||||
}
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
/**
|
||||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||
* then nothing will occur.
|
||||
* @param {string} string The template string which is the filled from the context object.
|
||||
* @param {object} context An object of information which will be used to enrich the string.
|
||||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processString = async (string, context) => {
|
||||
// TODO: carry out any async calls before carrying out async call
|
||||
return module.exports.processStringSync(string, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is
|
||||
* a pure sync call and therefore does not have the full functionality of the async call.
|
||||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||
* if the structure contains any cycles then this will fail.
|
||||
* @param {object} context The context that handlebars should fill data from.
|
||||
* @returns {object|array} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObjectSync = (object, context) => {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
let val = object[key]
|
||||
if (typeof val === "string") {
|
||||
object[key] = module.exports.processStringSync(object[key], context)
|
||||
} else if (typeof val === "object") {
|
||||
object[key] = module.exports.processObjectSync(object[key], context)
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
/**
|
||||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
||||
* @param {string} string The template string which is the filled from the context object.
|
||||
* @param {object} context An object of information which will be used to enrich the string.
|
||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processStringSync = (string, context) => {
|
||||
if (!exports.isValid(string)) {
|
||||
return string
|
||||
}
|
||||
// take a copy of input incase error
|
||||
const input = string
|
||||
if (typeof string !== "string") {
|
||||
throw "Cannot process non-string types."
|
||||
}
|
||||
try {
|
||||
string = processors.preprocess(string)
|
||||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
||||
const template = hbsInstance.compile(string, {
|
||||
strict: false,
|
||||
})
|
||||
return processors.postprocess(
|
||||
template({
|
||||
now: new Date().toISOString(),
|
||||
...context,
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
return removeHandlebarsStatements(input)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
|
||||
* @param {string} property The property which is to be wrapped.
|
||||
* @returns {string} The wrapped property ready to be added to a templating string.
|
||||
*/
|
||||
module.exports.makePropSafe = property => {
|
||||
return `[${property}]`.replace("[[", "[").replace("]]", "]")
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
|
||||
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
|
||||
* @returns {boolean} Whether or not the input string is valid.
|
||||
*/
|
||||
module.exports.isValid = string => {
|
||||
const validCases = [
|
||||
"string",
|
||||
"number",
|
||||
"object",
|
||||
"array",
|
||||
"cannot read property",
|
||||
"undefined",
|
||||
]
|
||||
// this is a portion of a specific string always output by handlebars in the case of a syntax error
|
||||
const invalidCases = [`expecting '`]
|
||||
// don't really need a real context to check if its valid
|
||||
const context = {}
|
||||
try {
|
||||
hbsInstance.compile(processors.preprocess(string, false))(context)
|
||||
return true
|
||||
} catch (err) {
|
||||
const msg = err && err.message ? err.message : err
|
||||
if (!msg) {
|
||||
return false
|
||||
}
|
||||
const invalidCase = invalidCases.some(invalidCase =>
|
||||
msg.toLowerCase().includes(invalidCase)
|
||||
)
|
||||
const validCase = validCases.some(validCase =>
|
||||
msg.toLowerCase().includes(validCase)
|
||||
)
|
||||
// special case for maths functions - don't have inputs yet
|
||||
return validCase && !invalidCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We have generated a static manifest file from the helpers that this string templating package makes use of.
|
||||
* This manifest provides information about each of the helpers and how it can be used.
|
||||
* @returns The manifest JSON which has been generated from the helpers.
|
||||
*/
|
||||
module.exports.getManifest = () => {
|
||||
return manifest
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a HBS expression is a valid JS HBS expression
|
||||
* @param handlebars the HBS expression to check
|
||||
* @returns {boolean} whether the expression is JS or not
|
||||
*/
|
||||
module.exports.isJSBinding = handlebars => {
|
||||
return module.exports.decodeJSBinding(handlebars) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a raw JS string as a JS HBS expression
|
||||
* @param javascript the JS code to encode
|
||||
* @returns {string} the JS HBS expression
|
||||
*/
|
||||
module.exports.encodeJSBinding = javascript => {
|
||||
return `{{ js "${btoa(javascript)}" }}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a JS HBS expression to the raw JS code
|
||||
* @param handlebars the JS HBS expression
|
||||
* @returns {string|null} the raw JS code
|
||||
*/
|
||||
module.exports.decodeJSBinding = handlebars => {
|
||||
if (!handlebars || typeof handlebars !== "string") {
|
||||
return null
|
||||
}
|
||||
|
||||
// JS is only valid if it is the only HBS expression
|
||||
if (!handlebars.trim().startsWith("{{ js ")) {
|
||||
return null
|
||||
}
|
||||
|
||||
const captureJSRegex = new RegExp(/{{ js "(.*)" }}/)
|
||||
const match = handlebars.match(captureJSRegex)
|
||||
if (!match || match.length < 2) {
|
||||
return null
|
||||
}
|
||||
return atob(match[1])
|
||||
}
|
|
@ -1,12 +1,31 @@
|
|||
import templates from "./index.cjs"
|
||||
import vm from "vm"
|
||||
import templates from "./index.js"
|
||||
import { setJSRunner } from "./helpers/javascript"
|
||||
|
||||
/**
|
||||
* This file is simply an entrypoint for rollup - makes a lot of cjs problems go away
|
||||
* ES6 entrypoint for rollup
|
||||
*/
|
||||
export const isValid = templates.isValid
|
||||
export const makePropSafe = templates.makePropSafe
|
||||
export const getManifest = templates.getManifest
|
||||
export const isJSBinding = templates.isJSBinding
|
||||
export const encodeJSBinding = templates.encodeJSBinding
|
||||
export const decodeJSBinding = templates.decodeJSBinding
|
||||
export const processStringSync = templates.processStringSync
|
||||
export const processObjectSync = templates.processObjectSync
|
||||
export const processString = templates.processString
|
||||
export const processObject = templates.processObject
|
||||
|
||||
/**
|
||||
* Use polyfilled vm to run JS scripts in a browser Env
|
||||
*/
|
||||
setJSRunner((js, context) => {
|
||||
context = {
|
||||
...context,
|
||||
alert: undefined,
|
||||
setInterval: undefined,
|
||||
setTimeout: undefined,
|
||||
}
|
||||
vm.createContext(context)
|
||||
return vm.runInNewContext(js, context, { timeout: 1000 })
|
||||
})
|
|
@ -22,3 +22,11 @@ module.exports.removeHandlebarsStatements = string => {
|
|||
}
|
||||
return string
|
||||
}
|
||||
|
||||
module.exports.btoa = plainText => {
|
||||
return Buffer.from(plainText, "utf-8").toString("base64")
|
||||
}
|
||||
|
||||
module.exports.atob = base64 => {
|
||||
return Buffer.from(base64, "base64").toString("utf-8")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
const { processStringSync, encodeJSBinding } = require("../src/index.cjs")
|
||||
|
||||
const processJS = (js, context) => {
|
||||
return processStringSync(encodeJSBinding(js), context)
|
||||
}
|
||||
|
||||
describe("Test the JavaScript helper", () => {
|
||||
it("should execute a simple expression", () => {
|
||||
const output = processJS(`return 1 + 2`)
|
||||
expect(output).toBe("3")
|
||||
})
|
||||
|
||||
it("should be able to use primitive bindings", () => {
|
||||
const output = processJS(`return $("foo")`, {
|
||||
foo: "bar",
|
||||
})
|
||||
expect(output).toBe("bar")
|
||||
})
|
||||
|
||||
it("should be able to use an object binding", () => {
|
||||
const output = processJS(`return $("foo").bar`, {
|
||||
foo: {
|
||||
bar: "baz",
|
||||
},
|
||||
})
|
||||
expect(output).toBe("baz")
|
||||
})
|
||||
|
||||
it("should be able to use a complex object binding", () => {
|
||||
const output = processJS(`return $("foo").bar[0].baz`, {
|
||||
foo: {
|
||||
bar: [
|
||||
{
|
||||
baz: "shazbat",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(output).toBe("shazbat")
|
||||
})
|
||||
|
||||
it("should be able to use a deep binding", () => {
|
||||
const output = processJS(`return $("foo.bar.baz")`, {
|
||||
foo: {
|
||||
bar: {
|
||||
baz: "shazbat",
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(output).toBe("shazbat")
|
||||
})
|
||||
|
||||
it("should be able to use a deep array binding", () => {
|
||||
const output = processJS(`return $("foo.0.bar")`, {
|
||||
foo: [
|
||||
{
|
||||
bar: "baz",
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(output).toBe("baz")
|
||||
})
|
||||
|
||||
it("should handle errors", () => {
|
||||
const output = processJS(`throw "Error"`)
|
||||
expect(output).toBe("Error while executing JS")
|
||||
})
|
||||
|
||||
it("should timeout after one second", () => {
|
||||
const output = processJS(`while (true) {}`)
|
||||
expect(output).toBe("Error while executing JS")
|
||||
})
|
||||
|
||||
it("should prevent access to the process global", () => {
|
||||
const output = processJS(`return process`)
|
||||
expect(output).toBe("Error while executing JS")
|
||||
})
|
||||
|
||||
it("should prevent sandbox escape", () => {
|
||||
const output = processJS(
|
||||
`return this.constructor.constructor("return process")()`
|
||||
)
|
||||
expect(output).toBe("Error while executing JS")
|
||||
})
|
||||
})
|
|
@ -4572,6 +4572,11 @@ vlq@^0.2.2:
|
|||
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
|
||||
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
|
||||
|
||||
vm2@^3.9.4:
|
||||
version "3.9.4"
|
||||
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.4.tgz#2e118290fefe7bd8ea09ebe2f5faf53730dbddaa"
|
||||
integrity sha512-sOdharrJ7KEePIpHekiWaY1DwgueuiBeX/ZBJUPgETsVlJsXuEx0K0/naATq2haFvJrvZnRiORQRubR0b7Ye6g==
|
||||
|
||||
w3c-hr-time@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.9.160-alpha.4",
|
||||
"version": "0.9.167-alpha.12",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -29,8 +29,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^0.9.160-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.160-alpha.4",
|
||||
"@budibase/auth": "^0.9.167-alpha.12",
|
||||
"@budibase/string-templates": "^0.9.167-alpha.12",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
"aws-sdk": "^2.811.0",
|
||||
|
|
|
@ -3,8 +3,14 @@ const { google } = require("@budibase/auth/src/middleware")
|
|||
const { oidc } = require("@budibase/auth/src/middleware")
|
||||
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||
const { setCookie, getCookie, clearCookie, getGlobalUserByEmail, hash } =
|
||||
authPkg.utils
|
||||
const {
|
||||
setCookie,
|
||||
getCookie,
|
||||
clearCookie,
|
||||
getGlobalUserByEmail,
|
||||
hash,
|
||||
platformLogout,
|
||||
} = authPkg.utils
|
||||
const { Cookies } = authPkg.constants
|
||||
const { passport } = authPkg.auth
|
||||
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
||||
|
@ -121,8 +127,7 @@ exports.resetUpdate = async ctx => {
|
|||
}
|
||||
|
||||
exports.logout = async ctx => {
|
||||
clearCookie(ctx, Cookies.Auth)
|
||||
clearCookie(ctx, Cookies.CurrentApp)
|
||||
await platformLogout({ ctx, userId: ctx.user._id })
|
||||
ctx.body = { message: "User logged out." }
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ const {
|
|||
StaticDatabases,
|
||||
generateNewUsageQuotaDoc,
|
||||
} = require("@budibase/auth/db")
|
||||
const { hash, getGlobalUserByEmail, saveUser } = require("@budibase/auth").utils
|
||||
const { hash, getGlobalUserByEmail, saveUser, platformLogout } =
|
||||
require("@budibase/auth").utils
|
||||
const { EmailTemplatePurpose } = require("../../../constants")
|
||||
const { checkInviteCode } = require("../../../utilities/redis")
|
||||
const { sendEmail } = require("../../../utilities/email")
|
||||
|
@ -173,7 +174,14 @@ exports.updateSelf = async ctx => {
|
|||
const db = getGlobalDB()
|
||||
const user = await db.get(ctx.user._id)
|
||||
if (ctx.request.body.password) {
|
||||
// changing password
|
||||
ctx.request.body.password = await hash(ctx.request.body.password)
|
||||
// Log all other sessions out apart from the current one
|
||||
await platformLogout({
|
||||
ctx,
|
||||
userId: ctx.user._id,
|
||||
keepActiveSession: true,
|
||||
})
|
||||
}
|
||||
// don't allow sending up an ID/Rev, always use the existing one
|
||||
delete ctx.request.body._id
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue