Merge branch 'master' into BUDI-7655/migration-backend
This commit is contained in:
commit
e5d27181f2
4
LICENSE
4
LICENSE
|
@ -1,7 +1,9 @@
|
||||||
Copyright 2019-2021, Budibase Ltd.
|
Copyright 2019-2023, Budibase Ltd.
|
||||||
|
|
||||||
Each Budibase package has its own license, please check the license file in each package.
|
Each Budibase package has its own license, please check the license file in each package.
|
||||||
|
|
||||||
You can consider Budibase to be GPLv3 licensed overall.
|
You can consider Budibase to be GPLv3 licensed overall.
|
||||||
|
|
||||||
The apps that you build with Budibase do not package any GPLv3 licensed code, thus do not fall under those restrictions.
|
The apps that you build with Budibase do not package any GPLv3 licensed code, thus do not fall under those restrictions.
|
||||||
|
|
||||||
|
Budibase ships with Structured Query Server, by The Neighbourhoodie Software GmbH. This license for this can be found at ./SQS_LICENSE
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
FORM OF CUSTOMER LICENCE
|
||||||
|
|
||||||
|
Budibase hereby grants the Customer a worldwide, royalty free, non-exclusive,
|
||||||
|
perpetual (for the lifetime of the intellectual property rights contained in the Product)
|
||||||
|
right and title to utilise the binary code of the The Neighbourhoodie Software GmbH
|
||||||
|
Structured Query Server software product (Product) for its own internal business
|
||||||
|
purposes (the Purpose) only (the Licence). The Product has the function of bringing a
|
||||||
|
CouchDB database (NoSQL database) into an SQL database form (SQLite) and thereby
|
||||||
|
making it usable for complex queries - which originally could only be displayed in an
|
||||||
|
SQL database. By indexing in SQLite and a server that is tailored to it, the Product
|
||||||
|
enables the use of CouchDB with SQL queries.
|
||||||
|
The Licence shall not permit sub-licensing, resale or transfer of the Product to third
|
||||||
|
parties, other than sub-licensing to the Customer’s direct contractors for the purposes
|
||||||
|
of utilizing the Product as contemplated above.
|
||||||
|
The Licence shall not permit the adaptation, modification, decompilation, reverse
|
||||||
|
engineering or similar activities with respect to the Product.
|
||||||
|
This licence is granted to the Customer only, although Customer and its Affiliates’
|
||||||
|
employees, servants and agents shall be entitled to utilize the Product within the scope
|
||||||
|
of the Licence for the Customer’s Purpose only.
|
||||||
|
Reproduction is not permitted to users, except for reproductions that are necessary for
|
||||||
|
the use of the product under the licence described above. These conditions apply to the
|
||||||
|
product regardless of the form in which we make the product available and on which
|
||||||
|
devices it is installed and/or with which devices it is ultimately used. Depending on the
|
||||||
|
product variant or intended use, certain technical requirements in the IT infrastructure
|
||||||
|
must be satisfied as a prerequisite for use.
|
||||||
|
The law of the Northern Ireland applies exclusively to this licence, and the courts of
|
||||||
|
Northern Ireland shall have exclusive jurisdiction, save that we reserve a right to sue
|
||||||
|
you in the jurisdiction in which you are based. The application of the UN Sales
|
||||||
|
Convention (CISG) is excluded.
|
||||||
|
The invalidity of any part of this licence does not affect the validity of the remaining
|
||||||
|
regulations.
|
|
@ -87,6 +87,7 @@ couchdb:
|
||||||
storageClass: "nfs-client"
|
storageClass: "nfs-client"
|
||||||
adminPassword: admin
|
adminPassword: admin
|
||||||
|
|
||||||
|
services:
|
||||||
objectStore:
|
objectStore:
|
||||||
storageClass: "nfs-client"
|
storageClass: "nfs-client"
|
||||||
redis:
|
redis:
|
||||||
|
|
|
@ -86,6 +86,7 @@ couchdb:
|
||||||
storageClass: "nfs-client"
|
storageClass: "nfs-client"
|
||||||
adminPassword: admin
|
adminPassword: admin
|
||||||
|
|
||||||
|
services:
|
||||||
objectStore:
|
objectStore:
|
||||||
storageClass: "nfs-client"
|
storageClass: "nfs-client"
|
||||||
redis:
|
redis:
|
||||||
|
|
|
@ -16,6 +16,7 @@ spec:
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app.kubernetes.io/name: budibase-proxy
|
app.kubernetes.io/name: budibase-proxy
|
||||||
|
minReadySeconds: 10
|
||||||
strategy:
|
strategy:
|
||||||
type: RollingUpdate
|
type: RollingUpdate
|
||||||
template:
|
template:
|
||||||
|
|
|
@ -30,10 +30,18 @@ elif [[ "${TARGETBUILD}" = "single" ]]; then
|
||||||
# mount, so we use that for all persistent data.
|
# mount, so we use that for all persistent data.
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||||
|
elif [[ "${TARGETBUILD}" = "docker-compose" ]]; then
|
||||||
|
# We remove the database_dir and view_index_dir settings from the local.ini
|
||||||
|
# in docker-compose because it will default to /opt/couchdb/data which is what
|
||||||
|
# our docker-compose was using prior to us switching to using our own CouchDB
|
||||||
|
# image.
|
||||||
|
sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||||
|
sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||||
|
sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
||||||
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
|
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
|
||||||
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
|
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
|
||||||
# mount for storing database data.
|
# mount for storing database data.
|
||||||
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
||||||
|
|
||||||
# We remove the database_dir and view_index_dir settings from the local.ini
|
# We remove the database_dir and view_index_dir settings from the local.ini
|
||||||
# in Kubernetes because it will default to /opt/couchdb/data which is what
|
# in Kubernetes because it will default to /opt/couchdb/data which is what
|
||||||
|
|
|
@ -57,7 +57,6 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis-service
|
- redis-service
|
||||||
- minio-service
|
- minio-service
|
||||||
- couch-init
|
|
||||||
|
|
||||||
minio-service:
|
minio-service:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -70,7 +69,7 @@ services:
|
||||||
MINIO_BROWSER: "off"
|
MINIO_BROWSER: "off"
|
||||||
command: server /data --console-address ":9001"
|
command: server /data --console-address ":9001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
test: "timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1"
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 20s
|
timeout: 20s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
@ -98,26 +97,15 @@ services:
|
||||||
|
|
||||||
couchdb-service:
|
couchdb-service:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: ibmcom/couchdb3
|
image: budibase/couchdb
|
||||||
|
pull_policy: always
|
||||||
environment:
|
environment:
|
||||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||||
- COUCHDB_USER=${COUCH_DB_USER}
|
- COUCHDB_USER=${COUCH_DB_USER}
|
||||||
|
- TARGETBUILD=docker-compose
|
||||||
volumes:
|
volumes:
|
||||||
- couchdb3_data:/opt/couchdb/data
|
- couchdb3_data:/opt/couchdb/data
|
||||||
|
|
||||||
couch-init:
|
|
||||||
image: curlimages/curl
|
|
||||||
environment:
|
|
||||||
PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984"
|
|
||||||
depends_on:
|
|
||||||
- couchdb-service
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"sh",
|
|
||||||
"-c",
|
|
||||||
"sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;",
|
|
||||||
]
|
|
||||||
|
|
||||||
redis-service:
|
redis-service:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: redis
|
image: redis
|
||||||
|
|
|
@ -42,7 +42,7 @@ http {
|
||||||
server {
|
server {
|
||||||
listen 10000 default_server;
|
listen 10000 default_server;
|
||||||
server_name _;
|
server_name _;
|
||||||
client_max_body_size 1000m;
|
client_max_body_size 50000m;
|
||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
|
|
||||||
|
|
|
@ -249,4 +249,30 @@ http {
|
||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# From https://docs.datadoghq.com/integrations/nginx/?tab=kubernetes
|
||||||
|
server {
|
||||||
|
listen 81;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
allow 127.0.0.1;
|
||||||
|
deny all;
|
||||||
|
|
||||||
|
location /nginx_status {
|
||||||
|
# Choose your status module
|
||||||
|
|
||||||
|
# freely available with open source NGINX
|
||||||
|
stub_status;
|
||||||
|
|
||||||
|
# for open source NGINX < version 1.7.5
|
||||||
|
# stub_status on;
|
||||||
|
|
||||||
|
# available only with NGINX Plus
|
||||||
|
# status;
|
||||||
|
|
||||||
|
# ensures the version information can be retrieved
|
||||||
|
server_tokens on;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.13.31",
|
"version": "2.13.35",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
GoogleInnerConfig,
|
GoogleInnerConfig,
|
||||||
OIDCInnerConfig,
|
OIDCInnerConfig,
|
||||||
PlatformLogoutOpts,
|
PlatformLogoutOpts,
|
||||||
|
SessionCookie,
|
||||||
SSOProviderType,
|
SSOProviderType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as events from "../events"
|
import * as events from "../events"
|
||||||
|
@ -44,7 +45,6 @@ export const buildAuthMiddleware = authenticated
|
||||||
export const buildTenancyMiddleware = tenancy
|
export const buildTenancyMiddleware = tenancy
|
||||||
export const buildCsrfMiddleware = csrf
|
export const buildCsrfMiddleware = csrf
|
||||||
export const passport = _passport
|
export const passport = _passport
|
||||||
export const jwt = require("jsonwebtoken")
|
|
||||||
|
|
||||||
// Strategies
|
// Strategies
|
||||||
_passport.use(new LocalStrategy(local.options, local.authenticate))
|
_passport.use(new LocalStrategy(local.options, local.authenticate))
|
||||||
|
@ -191,10 +191,10 @@ export async function platformLogout(opts: PlatformLogoutOpts) {
|
||||||
|
|
||||||
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
||||||
|
|
||||||
const currentSession = getCookie(ctx, Cookie.Auth)
|
const currentSession = getCookie<SessionCookie>(ctx, Cookie.Auth)
|
||||||
let sessions = await getSessionsForUser(userId)
|
let sessions = await getSessionsForUser(userId)
|
||||||
|
|
||||||
if (keepActiveSession) {
|
if (currentSession && keepActiveSession) {
|
||||||
sessions = sessions.filter(
|
sessions = sessions.filter(
|
||||||
session => session.sessionId !== currentSession.sessionId
|
session => session.sessionId !== currentSession.sessionId
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
||||||
import { decrypt } from "../security/encryption"
|
import { decrypt } from "../security/encryption"
|
||||||
import * as identity from "../context/identity"
|
import * as identity from "../context/identity"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { Ctx, EndpointMatcher } from "@budibase/types"
|
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
||||||
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||||
|
|
||||||
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
||||||
|
@ -98,7 +98,9 @@ export default function (
|
||||||
// check the actual user is authenticated first, try header or cookie
|
// check the actual user is authenticated first, try header or cookie
|
||||||
let headerToken = ctx.request.headers[Header.TOKEN]
|
let headerToken = ctx.request.headers[Header.TOKEN]
|
||||||
|
|
||||||
const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken)
|
const authCookie =
|
||||||
|
getCookie<SessionCookie>(ctx, Cookie.Auth) ||
|
||||||
|
openJwt<SessionCookie>(headerToken)
|
||||||
let apiKey = ctx.request.headers[Header.API_KEY]
|
let apiKey = ctx.request.headers[Header.API_KEY]
|
||||||
|
|
||||||
if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) {
|
if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Cookie } from "../../../constants"
|
||||||
import * as configs from "../../../configs"
|
import * as configs from "../../../configs"
|
||||||
import * as cache from "../../../cache"
|
import * as cache from "../../../cache"
|
||||||
import * as utils from "../../../utils"
|
import * as utils from "../../../utils"
|
||||||
import { UserCtx, SSOProfile } from "@budibase/types"
|
import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types"
|
||||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||||
|
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
@ -58,7 +58,14 @@ export async function postAuth(
|
||||||
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||||
|
|
||||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth)
|
const authStateCookie = utils.getCookie<{ appId: string }>(
|
||||||
|
ctx,
|
||||||
|
Cookie.DatasourceAuth
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!authStateCookie) {
|
||||||
|
throw new Error("Unable to fetch datasource auth cookie")
|
||||||
|
}
|
||||||
|
|
||||||
return passport.authenticate(
|
return passport.authenticate(
|
||||||
new GoogleStrategy(
|
new GoogleStrategy(
|
||||||
|
|
|
@ -305,20 +305,33 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
||||||
let writePath = join(budibaseTempDir(), v4())
|
let writePath = join(budibaseTempDir(), v4())
|
||||||
fs.mkdirSync(writePath)
|
fs.mkdirSync(writePath)
|
||||||
const objects = await listAllObjects(bucketName, path)
|
const objects = await listAllObjects(bucketName, path)
|
||||||
let fullObjects = await Promise.all(
|
let streams = await Promise.all(
|
||||||
objects.map(obj => retrieve(bucketName, obj.Key!))
|
objects.map(obj => getReadStream(bucketName, obj.Key!))
|
||||||
)
|
)
|
||||||
let count = 0
|
let count = 0
|
||||||
|
const writePromises: Promise<Error>[] = []
|
||||||
for (let obj of objects) {
|
for (let obj of objects) {
|
||||||
const filename = obj.Key!
|
const filename = obj.Key!
|
||||||
const data = fullObjects[count++]
|
const stream = streams[count++]
|
||||||
const possiblePath = filename.split("/")
|
const possiblePath = filename.split("/")
|
||||||
if (possiblePath.length > 1) {
|
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
||||||
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
const possibleDir = join(writePath, ...dirs)
|
||||||
fs.mkdirSync(join(writePath, ...dirs), { recursive: true })
|
if (possiblePath.length > 1 && !fs.existsSync(possibleDir)) {
|
||||||
|
fs.mkdirSync(possibleDir, { recursive: true })
|
||||||
}
|
}
|
||||||
fs.writeFileSync(join(writePath, ...possiblePath), data)
|
const writeStream = fs.createWriteStream(join(writePath, ...possiblePath), {
|
||||||
|
mode: 0o644,
|
||||||
|
})
|
||||||
|
stream.pipe(writeStream)
|
||||||
|
writePromises.push(
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
stream.on("finish", resolve)
|
||||||
|
stream.on("error", reject)
|
||||||
|
writeStream.on("error", reject)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
await Promise.all(writePromises)
|
||||||
return writePath
|
return writePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,9 @@ export async function encryptFile(
|
||||||
const outputFileName = `${filename}.enc`
|
const outputFileName = `${filename}.enc`
|
||||||
|
|
||||||
const filePath = join(dir, filename)
|
const filePath = join(dir, filename)
|
||||||
|
if (fs.lstatSync(filePath).isDirectory()) {
|
||||||
|
throw new Error("Unable to encrypt directory")
|
||||||
|
}
|
||||||
const inputFile = fs.createReadStream(filePath)
|
const inputFile = fs.createReadStream(filePath)
|
||||||
const outputFile = fs.createWriteStream(join(dir, outputFileName))
|
const outputFile = fs.createWriteStream(join(dir, outputFileName))
|
||||||
|
|
||||||
|
@ -110,6 +113,9 @@ export async function decryptFile(
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
secret: string
|
secret: string
|
||||||
) {
|
) {
|
||||||
|
if (fs.lstatSync(inputPath).isDirectory()) {
|
||||||
|
throw new Error("Unable to encrypt directory")
|
||||||
|
}
|
||||||
const { salt, iv } = await getSaltAndIV(inputPath)
|
const { salt, iv } = await getSaltAndIV(inputPath)
|
||||||
const inputFile = fs.createReadStream(inputPath, {
|
const inputFile = fs.createReadStream(inputPath, {
|
||||||
start: SALT_LENGTH + IV_LENGTH,
|
start: SALT_LENGTH + IV_LENGTH,
|
||||||
|
|
|
@ -11,8 +11,7 @@ import {
|
||||||
TenantResolutionStrategy,
|
TenantResolutionStrategy,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import type { SetOption } from "cookies"
|
import type { SetOption } from "cookies"
|
||||||
|
import jwt, { Secret } from "jsonwebtoken"
|
||||||
const jwt = require("jsonwebtoken")
|
|
||||||
|
|
||||||
const APP_PREFIX = DocumentType.APP + SEPARATOR
|
const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||||
const PROD_APP_PREFIX = "/app/"
|
const PROD_APP_PREFIX = "/app/"
|
||||||
|
@ -60,10 +59,7 @@ export function isServingApp(ctx: Ctx) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// prod app
|
// prod app
|
||||||
if (ctx.path.startsWith(PROD_APP_PREFIX)) {
|
return ctx.path.startsWith(PROD_APP_PREFIX)
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isServingBuilder(ctx: Ctx): boolean {
|
export function isServingBuilder(ctx: Ctx): boolean {
|
||||||
|
@ -138,16 +134,16 @@ function parseAppIdFromUrl(url?: string) {
|
||||||
* opens the contents of the specified encrypted JWT.
|
* opens the contents of the specified encrypted JWT.
|
||||||
* @return the contents of the token.
|
* @return the contents of the token.
|
||||||
*/
|
*/
|
||||||
export function openJwt(token: string) {
|
export function openJwt<T>(token?: string): T | undefined {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return token
|
return undefined
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, env.JWT_SECRET)
|
return jwt.verify(token, env.JWT_SECRET as Secret) as T
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (env.JWT_SECRET_FALLBACK) {
|
if (env.JWT_SECRET_FALLBACK) {
|
||||||
// fallback to enable rotation
|
// fallback to enable rotation
|
||||||
return jwt.verify(token, env.JWT_SECRET_FALLBACK)
|
return jwt.verify(token, env.JWT_SECRET_FALLBACK) as T
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
@ -159,13 +155,9 @@ export function isValidInternalAPIKey(apiKey: string) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// fallback to enable rotation
|
// fallback to enable rotation
|
||||||
if (
|
return !!(
|
||||||
env.INTERNAL_API_KEY_FALLBACK &&
|
env.INTERNAL_API_KEY_FALLBACK && env.INTERNAL_API_KEY_FALLBACK === apiKey
|
||||||
env.INTERNAL_API_KEY_FALLBACK === apiKey
|
)
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -173,14 +165,14 @@ export function isValidInternalAPIKey(apiKey: string) {
|
||||||
* @param ctx The request which is to be manipulated.
|
* @param ctx The request which is to be manipulated.
|
||||||
* @param name The name of the cookie to get.
|
* @param name The name of the cookie to get.
|
||||||
*/
|
*/
|
||||||
export function getCookie(ctx: Ctx, name: string) {
|
export function getCookie<T>(ctx: Ctx, name: string) {
|
||||||
const cookie = ctx.cookies.get(name)
|
const cookie = ctx.cookies.get(name)
|
||||||
|
|
||||||
if (!cookie) {
|
if (!cookie) {
|
||||||
return cookie
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return openJwt(cookie)
|
return openJwt<T>(cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,7 +189,7 @@ export function setCookie(
|
||||||
opts = { sign: true }
|
opts = { sign: true }
|
||||||
) {
|
) {
|
||||||
if (value && opts && opts.sign) {
|
if (value && opts && opts.sign) {
|
||||||
value = jwt.sign(value, env.JWT_SECRET)
|
value = jwt.sign(value, env.JWT_SECRET as Secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: SetOption = {
|
const config: SetOption = {
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
|
|
||||||
export let name
|
export let name
|
||||||
export let show = false
|
export let initiallyShow = false
|
||||||
export let collapsible = true
|
export let collapsible = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
let show = initiallyShow
|
||||||
|
|
||||||
const onHeaderClick = () => {
|
const onHeaderClick = () => {
|
||||||
if (!collapsible) {
|
if (!collapsible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
show = !show
|
show = !show
|
||||||
if (show) {
|
|
||||||
dispatch("open")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
$: {
|
$: {
|
||||||
if (selectedImage?.url) {
|
if (selectedImage?.url) {
|
||||||
selectedUrl = selectedImage?.url
|
selectedUrl = selectedImage?.url
|
||||||
} else if (selectedImage) {
|
} else if (selectedImage && isImage) {
|
||||||
try {
|
try {
|
||||||
let reader = new FileReader()
|
let reader = new FileReader()
|
||||||
reader.readAsDataURL(selectedImage)
|
reader.readAsDataURL(selectedImage)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { derived, get } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createHistoryStore } from "builderStore/store/history"
|
import { createHistoryStore } from "builderStore/store/history"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
|
@ -69,7 +70,14 @@ export const selectedComponent = derived(
|
||||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return findComponent($selectedScreen?.props, $store.selectedComponentId)
|
const selected = findComponent(
|
||||||
|
$selectedScreen?.props,
|
||||||
|
$store.selectedComponentId
|
||||||
|
)
|
||||||
|
|
||||||
|
const clone = selected ? cloneDeep(selected) : selected
|
||||||
|
store.actions.components.migrateSettings(clone)
|
||||||
|
return clone
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
selectedLayoutId: null,
|
selectedLayoutId: null,
|
||||||
|
hoverComponentId: null,
|
||||||
|
|
||||||
// Client state
|
// Client state
|
||||||
selectedComponentInstance: null,
|
selectedComponentInstance: null,
|
||||||
|
@ -112,7 +113,7 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
let clone = cloneDeep(screen)
|
let clone = cloneDeep(screen)
|
||||||
const result = patchFn(clone)
|
const result = patchFn(clone)
|
||||||
|
// An explicit false result means skip this change
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -601,6 +602,36 @@ export const getFrontendStore = () => {
|
||||||
// Finally try an external table
|
// Finally try an external table
|
||||||
return validTables.find(table => table.sourceType === DB_TYPE_EXTERNAL)
|
return validTables.find(table => table.sourceType === DB_TYPE_EXTERNAL)
|
||||||
},
|
},
|
||||||
|
migrateSettings: enrichedComponent => {
|
||||||
|
const componentPrefix = "@budibase/standard-components"
|
||||||
|
let migrated = false
|
||||||
|
|
||||||
|
if (enrichedComponent?._component == `${componentPrefix}/formblock`) {
|
||||||
|
// Use default config if the 'buttons' prop has never been initialised
|
||||||
|
if (!("buttons" in enrichedComponent)) {
|
||||||
|
enrichedComponent["buttons"] =
|
||||||
|
Utils.buildDynamicButtonConfig(enrichedComponent)
|
||||||
|
migrated = true
|
||||||
|
} else if (enrichedComponent["buttons"] == null) {
|
||||||
|
// Ignore legacy config if 'buttons' has been reset by 'resetOn'
|
||||||
|
const { _id, actionType, dataSource } = enrichedComponent
|
||||||
|
enrichedComponent["buttons"] = Utils.buildDynamicButtonConfig({
|
||||||
|
_id,
|
||||||
|
actionType,
|
||||||
|
dataSource,
|
||||||
|
})
|
||||||
|
migrated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure existing Formblocks position their buttons at the top.
|
||||||
|
if (!("buttonPosition" in enrichedComponent)) {
|
||||||
|
enrichedComponent["buttonPosition"] = "top"
|
||||||
|
migrated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrated
|
||||||
|
},
|
||||||
enrichEmptySettings: (component, opts) => {
|
enrichEmptySettings: (component, opts) => {
|
||||||
if (!component?._component) {
|
if (!component?._component) {
|
||||||
return
|
return
|
||||||
|
@ -672,7 +703,6 @@ export const getFrontendStore = () => {
|
||||||
component[setting.key] = setting.defaultValue
|
component[setting.key] = setting.defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate non-empty settings
|
// Validate non-empty settings
|
||||||
else {
|
else {
|
||||||
if (setting.type === "dataProvider") {
|
if (setting.type === "dataProvider") {
|
||||||
|
@ -722,6 +752,9 @@ export const getFrontendStore = () => {
|
||||||
useDefaultValues: true,
|
useDefaultValues: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Migrate nested component settings
|
||||||
|
store.actions.components.migrateSettings(instance)
|
||||||
|
|
||||||
// Add any extra properties the component needs
|
// Add any extra properties the component needs
|
||||||
let extras = {}
|
let extras = {}
|
||||||
if (definition.hasChildren) {
|
if (definition.hasChildren) {
|
||||||
|
@ -845,7 +878,16 @@ export const getFrontendStore = () => {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return patchFn(component, screen)
|
|
||||||
|
// Mutates the fetched component with updates
|
||||||
|
const patchResult = patchFn(component, screen)
|
||||||
|
|
||||||
|
// Mutates the component with any required settings updates
|
||||||
|
const migrated = store.actions.components.migrateSettings(component)
|
||||||
|
|
||||||
|
// Returning an explicit false signifies that we should skip this
|
||||||
|
// update. If we migrated something, ensure we never skip.
|
||||||
|
return migrated ? null : patchResult
|
||||||
}
|
}
|
||||||
await store.actions.screens.patch(patchScreen, screenId)
|
await store.actions.screens.patch(patchScreen, screenId)
|
||||||
},
|
},
|
||||||
|
@ -1247,9 +1289,13 @@ export const getFrontendStore = () => {
|
||||||
const settings = getComponentSettings(component._component)
|
const settings = getComponentSettings(component._component)
|
||||||
const updatedSetting = settings.find(setting => setting.key === name)
|
const updatedSetting = settings.find(setting => setting.key === name)
|
||||||
|
|
||||||
const resetFields = settings.filter(
|
// Can be a single string or array of strings
|
||||||
setting => name === setting.resetOn
|
const resetFields = settings.filter(setting => {
|
||||||
)
|
return (
|
||||||
|
name === setting.resetOn ||
|
||||||
|
(Array.isArray(setting.resetOn) && setting.resetOn.includes(name))
|
||||||
|
)
|
||||||
|
})
|
||||||
resetFields?.forEach(setting => {
|
resetFields?.forEach(setting => {
|
||||||
component[setting.key] = null
|
component[setting.key] = null
|
||||||
})
|
})
|
||||||
|
@ -1271,6 +1317,7 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
component[name] = value
|
component[name] = value
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestEjectBlock: componentId => {
|
requestEjectBlock: componentId => {
|
||||||
|
@ -1278,7 +1325,6 @@ export const getFrontendStore = () => {
|
||||||
},
|
},
|
||||||
handleEjectBlock: async (componentId, ejectedDefinition) => {
|
handleEjectBlock: async (componentId, ejectedDefinition) => {
|
||||||
let nextSelectedComponentId
|
let nextSelectedComponentId
|
||||||
|
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
const block = findComponent(screen.props, componentId)
|
const block = findComponent(screen.props, componentId)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
|
@ -57,16 +57,11 @@
|
||||||
}}
|
}}
|
||||||
class="buttons"
|
class="buttons"
|
||||||
>
|
>
|
||||||
<Icon hoverable size="M" name="Play" />
|
<Icon size="M" name="Play" />
|
||||||
<div>Run test</div>
|
<div>Run test</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Icon
|
<Icon disabled={!$automationStore.testResults} size="M" name="Multiple" />
|
||||||
disabled={!$automationStore.testResults}
|
|
||||||
hoverable
|
|
||||||
size="M"
|
|
||||||
name="Multiple"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class:disabled={!$automationStore.testResults}
|
class:disabled={!$automationStore.testResults}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
class:typing={typing && !automationNameError}
|
class:typing={typing && !automationNameError}
|
||||||
class:typing-error={automationNameError}
|
class:typing-error={automationNameError}
|
||||||
class="blockSection"
|
class="blockSection"
|
||||||
|
on:click={() => dispatch("toggle")}
|
||||||
>
|
>
|
||||||
<div class="splitHeader">
|
<div class="splitHeader">
|
||||||
<div class="center-items">
|
<div class="center-items">
|
||||||
|
@ -138,7 +139,20 @@
|
||||||
on:input={e => {
|
on:input={e => {
|
||||||
automationName = e.target.value.trim()
|
automationName = e.target.value.trim()
|
||||||
}}
|
}}
|
||||||
on:click={startTyping}
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
startTyping()
|
||||||
|
}}
|
||||||
|
on:keydown={async e => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
typing = false
|
||||||
|
if (automationNameError) {
|
||||||
|
automationName = stepNames[block.id] || block?.name
|
||||||
|
} else {
|
||||||
|
await saveName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
on:blur={async () => {
|
on:blur={async () => {
|
||||||
typing = false
|
typing = false
|
||||||
if (automationNameError) {
|
if (automationNameError) {
|
||||||
|
@ -168,7 +182,11 @@
|
||||||
</StatusLight>
|
</StatusLight>
|
||||||
</div>
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
on:click={() => dispatch("toggle")}
|
e.stopPropagation()
|
||||||
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
dispatch("toggle")
|
||||||
|
}}
|
||||||
hoverable
|
hoverable
|
||||||
name={open ? "ChevronUp" : "ChevronDown"}
|
name={open ? "ChevronUp" : "ChevronDown"}
|
||||||
/>
|
/>
|
||||||
|
@ -195,7 +213,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if !showTestStatus}
|
{#if !showTestStatus}
|
||||||
<Icon
|
<Icon
|
||||||
on:click={() => dispatch("toggle")}
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
dispatch("toggle")
|
||||||
|
}}
|
||||||
hoverable
|
hoverable
|
||||||
name={open ? "ChevronUp" : "ChevronDown"}
|
name={open ? "ChevronUp" : "ChevronDown"}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
TextArea,
|
TextArea,
|
||||||
Label,
|
|
||||||
notifications,
|
notifications,
|
||||||
|
ActionButton,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
|
@ -55,50 +53,69 @@
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
selectedValues = !selectedValues
|
||||||
|
selectedJSON = !selectedJSON
|
||||||
|
}
|
||||||
|
let selectedValues = true
|
||||||
|
let selectedJSON = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Add test data"
|
title="Add test data"
|
||||||
confirmText="Test"
|
confirmText="Run test"
|
||||||
size="M"
|
size="L"
|
||||||
showConfirmButton={true}
|
showConfirmButton={true}
|
||||||
disabled={isError}
|
disabled={isError}
|
||||||
onConfirm={testAutomation}
|
onConfirm={testAutomation}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
>
|
>
|
||||||
<Tabs selected="Form" quiet>
|
<div class="size">
|
||||||
<Tab icon="Form" title="Form">
|
<div class="options">
|
||||||
<div class="tab-content-padding">
|
<ActionButton quiet selected={selectedValues} on:click={toggle}
|
||||||
<AutomationBlockSetup
|
>Use values</ActionButton
|
||||||
{testData}
|
>
|
||||||
{schemaProperties}
|
<ActionButton quiet selected={selectedJSON} on:click={toggle}
|
||||||
isTestModal
|
>Use JSON</ActionButton
|
||||||
block={trigger}
|
>
|
||||||
/>
|
</div>
|
||||||
</div></Tab
|
</div>
|
||||||
>
|
|
||||||
<Tab icon="FileJson" title="JSON">
|
{#if selectedValues}
|
||||||
<div class="tab-content-padding">
|
<div class="tab-content-padding">
|
||||||
<Label>JSON</Label>
|
<AutomationBlockSetup
|
||||||
<div class="text-area-container">
|
{testData}
|
||||||
<TextArea
|
{schemaProperties}
|
||||||
value={JSON.stringify($selectedAutomation.testData, null, 2)}
|
isTestModal
|
||||||
error={failedParse}
|
block={trigger}
|
||||||
on:change={e => parseTestJSON(e)}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
{#if selectedJSON}
|
||||||
</Tab>
|
<div class="text-area-container">
|
||||||
</Tabs>
|
<TextArea
|
||||||
|
value={JSON.stringify($selectedAutomation.testData, null, 2)}
|
||||||
|
error={failedParse}
|
||||||
|
on:change={e => parseTestJSON(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.text-area-container :global(textarea) {
|
.text-area-container :global(textarea) {
|
||||||
min-height: 200px;
|
min-height: 300px;
|
||||||
height: 200px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content-padding {
|
.tab-content-padding {
|
||||||
padding: 0 var(--spacing-xl);
|
padding: 0 var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="title-text">
|
<div class="title-text">
|
||||||
<Icon name="MultipleCheck" />
|
<Icon name="MultipleCheck" />
|
||||||
<div style="padding-left: var(--spacing-l)">Test Details</div>
|
<div style="padding-left: var(--spacing-l); ">Test Details</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding-right: var(--spacing-xl)">
|
<div style="padding-right: var(--spacing-xl)">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -40,6 +40,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title :global(h1) {
|
.title :global(h1) {
|
||||||
|
|
|
@ -1,20 +1,44 @@
|
||||||
<script>
|
<script>
|
||||||
import AutomationList from "./AutomationList.svelte"
|
import AutomationList from "./AutomationList.svelte"
|
||||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||||
import { Modal, Button, Layout } from "@budibase/bbui"
|
import { Modal, Icon } from "@budibase/bbui"
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
|
||||||
export let modal
|
export let modal
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel title="Automations" borderRight>
|
<Panel title="Automations" borderRight noHeaderBorder titleCSS={false}>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<span class="panel-title-content" slot="panel-title-content">
|
||||||
<Button cta on:click={modal.show}>Add automation</Button>
|
<div class="header">
|
||||||
</Layout>
|
<div>Automations</div>
|
||||||
|
<div on:click={modal.show} class="add-automation-button">
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
<AutomationList />
|
<AutomationList />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateAutomationModal {webhookModal} />
|
<CreateAutomationModal {webhookModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-automation-button {
|
||||||
|
margin-left: 130px;
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-automation-button:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -149,7 +149,6 @@
|
||||||
}
|
}
|
||||||
const initialiseField = (field, savingColumn) => {
|
const initialiseField = (field, savingColumn) => {
|
||||||
isCreating = !field
|
isCreating = !field
|
||||||
|
|
||||||
if (field && !savingColumn) {
|
if (field && !savingColumn) {
|
||||||
editableColumn = cloneDeep(field)
|
editableColumn = cloneDeep(field)
|
||||||
originalName = editableColumn.name ? editableColumn.name + "" : null
|
originalName = editableColumn.name ? editableColumn.name + "" : null
|
||||||
|
@ -171,7 +170,8 @@
|
||||||
relationshipPart2 = part2
|
relationshipPart2 = part2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!savingColumn) {
|
}
|
||||||
|
if (!savingColumn && !originalName) {
|
||||||
let highestNumber = 0
|
let highestNumber = 0
|
||||||
Object.keys(table.schema).forEach(columnName => {
|
Object.keys(table.schema).forEach(columnName => {
|
||||||
const columnNumber = extractColumnNumber(columnName)
|
const columnNumber = extractColumnNumber(columnName)
|
||||||
|
@ -307,12 +307,6 @@
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
gridDispatch("close-edit-column")
|
gridDispatch("close-edit-column")
|
||||||
|
|
||||||
if (saveColumn.type === LINK_TYPE) {
|
|
||||||
// Fetching the new tables
|
|
||||||
tables.fetch()
|
|
||||||
// Fetching the new relationships
|
|
||||||
datasources.fetch()
|
|
||||||
}
|
|
||||||
if (originalName) {
|
if (originalName) {
|
||||||
notifications.success("Column updated successfully")
|
notifications.success("Column updated successfully")
|
||||||
} else {
|
} else {
|
||||||
|
@ -339,11 +333,6 @@
|
||||||
confirmDeleteDialog.hide()
|
confirmDeleteDialog.hide()
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
gridDispatch("close-edit-column")
|
gridDispatch("close-edit-column")
|
||||||
|
|
||||||
if (editableColumn.type === LINK_TYPE) {
|
|
||||||
// Updating the relationships
|
|
||||||
datasources.fetch()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Error deleting column: ${error.message}`)
|
notifications.error(`Error deleting column: ${error.message}`)
|
||||||
|
@ -540,8 +529,16 @@
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#if mounted}
|
{#if mounted}
|
||||||
<Input
|
<Input
|
||||||
|
value={editableColumn.name}
|
||||||
autofocus
|
autofocus
|
||||||
bind:value={editableColumn.name}
|
on:input={e => {
|
||||||
|
if (
|
||||||
|
!uneditable &&
|
||||||
|
!(linkEditDisabled && editableColumn.type === LINK_TYPE)
|
||||||
|
) {
|
||||||
|
editableColumn.name = e.target.value
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={uneditable ||
|
disabled={uneditable ||
|
||||||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
||||||
error={errors?.name}
|
error={errors?.name}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
export let showTooltip = false
|
export let showTooltip = false
|
||||||
export let selectedBy = null
|
export let selectedBy = null
|
||||||
export let compact = false
|
export let compact = false
|
||||||
|
export let hovering = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -61,6 +62,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
|
class:hovering
|
||||||
class:border
|
class:border
|
||||||
class:selected
|
class:selected
|
||||||
class:withActions
|
class:withActions
|
||||||
|
@ -71,6 +73,8 @@
|
||||||
on:dragstart
|
on:dragstart
|
||||||
on:dragover
|
on:dragover
|
||||||
on:drop
|
on:drop
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
ondragover="return false"
|
ondragover="return false"
|
||||||
ondragenter="return false"
|
ondragenter="return false"
|
||||||
|
@ -152,15 +156,17 @@
|
||||||
--avatars-background: var(--spectrum-global-color-gray-200);
|
--avatars-background: var(--spectrum-global-color-gray-200);
|
||||||
}
|
}
|
||||||
.nav-item.selected {
|
.nav-item.selected {
|
||||||
background-color: var(--spectrum-global-color-gray-300);
|
background-color: var(--spectrum-global-color-gray-300) !important;
|
||||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
.nav-item:hover {
|
.nav-item:hover,
|
||||||
background-color: var(--spectrum-global-color-gray-300);
|
.hovering {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
.nav-item:hover .actions {
|
.nav-item:hover .actions,
|
||||||
|
.hovering .actions {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
.nav-item-content {
|
.nav-item-content {
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
export let wide = false
|
export let wide = false
|
||||||
export let extraWide = false
|
export let extraWide = false
|
||||||
export let closeButtonIcon = "Close"
|
export let closeButtonIcon = "Close"
|
||||||
|
export let noHeaderBorder = false
|
||||||
|
export let titleCSS = true
|
||||||
$: customHeaderContent = $$slots["panel-header-content"]
|
$: customHeaderContent = $$slots["panel-header-content"]
|
||||||
$: customTitleContent = $$slots["panel-title-content"]
|
$: customTitleContent = $$slots["panel-title-content"]
|
||||||
</script>
|
</script>
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
class="header"
|
class="header"
|
||||||
class:custom={customHeaderContent}
|
class:custom={customHeaderContent}
|
||||||
class:borderBottom={borderBottomHeader}
|
class:borderBottom={borderBottomHeader}
|
||||||
|
class:noHeaderBorder
|
||||||
>
|
>
|
||||||
{#if showBackButton}
|
{#if showBackButton}
|
||||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
|
@ -41,7 +43,7 @@
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
</AbsTooltip>
|
</AbsTooltip>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="title">
|
<div class:title={titleCSS}>
|
||||||
{#if customTitleContent}
|
{#if customTitleContent}
|
||||||
<slot name="panel-title-content" />
|
<slot name="panel-title-content" />
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -106,6 +108,10 @@
|
||||||
padding: 0 var(--spacing-l);
|
padding: 0 var(--spacing-l);
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noHeaderBorder {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
.header.borderBottom {
|
.header.borderBottom {
|
||||||
border-bottom: var(--border-light);
|
border-bottom: var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,15 @@
|
||||||
<div class="field-label">{item.label || item.field}</div>
|
<div class="field-label">{item.label || item.field}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-item-right">
|
<div class="list-item-right">
|
||||||
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
<Toggle
|
||||||
|
on:click={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
on:change={onToggle(item)}
|
||||||
|
text=""
|
||||||
|
value={item.active}
|
||||||
|
thin
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
export let app
|
export let app
|
||||||
export let published
|
export let published
|
||||||
let includeInternalTablesRows = true
|
let includeInternalTablesRows = true
|
||||||
let encypt = true
|
let encrypt = true
|
||||||
|
|
||||||
let password = null
|
let password = null
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
|
@ -27,9 +27,9 @@
|
||||||
$: stepConfig = {
|
$: stepConfig = {
|
||||||
[Step.CONFIG]: {
|
[Step.CONFIG]: {
|
||||||
title: published ? "Export published app" : "Export latest app",
|
title: published ? "Export published app" : "Export latest app",
|
||||||
confirmText: encypt ? "Continue" : exportButtonText,
|
confirmText: encrypt ? "Continue" : exportButtonText,
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
if (!encypt) {
|
if (!encrypt) {
|
||||||
exportApp()
|
exportApp()
|
||||||
} else {
|
} else {
|
||||||
currentStep = Step.SET_PASSWORD
|
currentStep = Step.SET_PASSWORD
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
if (!$validation.valid) {
|
if (!$validation.valid) {
|
||||||
return keepOpen
|
return keepOpen
|
||||||
}
|
}
|
||||||
exportApp(password)
|
await exportApp(password)
|
||||||
},
|
},
|
||||||
isValid: $validation.valid,
|
isValid: $validation.valid,
|
||||||
},
|
},
|
||||||
|
@ -109,13 +109,13 @@
|
||||||
text="Export rows from internal tables"
|
text="Export rows from internal tables"
|
||||||
bind:value={includeInternalTablesRows}
|
bind:value={includeInternalTablesRows}
|
||||||
/>
|
/>
|
||||||
<Toggle text="Encrypt my export" bind:value={encypt} />
|
<Toggle text="Encrypt my export" bind:value={encrypt} />
|
||||||
</Body>
|
</Body>
|
||||||
{#if !encypt}
|
<InlineAlert
|
||||||
<InlineAlert
|
header={encrypt
|
||||||
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
? "Please note Budibase does not encrypt attachments during the export process to ensure efficient export of large attachments."
|
||||||
/>
|
: "Do not share your Budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."}
|
||||||
{/if}
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if currentStep === Step.SET_PASSWORD}
|
{#if currentStep === Step.SET_PASSWORD}
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup {
|
.setup {
|
||||||
padding-top: var(--spectrum-global-dimension-size-200);
|
padding-top: 9px;
|
||||||
border-left: var(--border-light);
|
border-left: var(--border-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
const generalSettings = settings.filter(
|
const generalSettings = settings.filter(
|
||||||
setting => !setting.section && setting.tag === tag
|
setting => !setting.section && setting.tag === tag
|
||||||
)
|
)
|
||||||
|
|
||||||
const customSections = settings.filter(
|
const customSections = settings.filter(
|
||||||
setting => setting.section && setting.tag === tag
|
setting => setting.section && setting.tag === tag
|
||||||
)
|
)
|
||||||
|
@ -151,7 +152,7 @@
|
||||||
{#if section.visible}
|
{#if section.visible}
|
||||||
<DetailSummary
|
<DetailSummary
|
||||||
name={showSectionTitle ? section.name : ""}
|
name={showSectionTitle ? section.name : ""}
|
||||||
show={section.collapsed !== true}
|
initiallyShow={section.collapsed !== true}
|
||||||
>
|
>
|
||||||
{#if section.info}
|
{#if section.info}
|
||||||
<div class="section-info">
|
<div class="section-info">
|
||||||
|
|
|
@ -36,12 +36,14 @@
|
||||||
|
|
||||||
// Determine selected component ID
|
// Determine selected component ID
|
||||||
$: selectedComponentId = $store.selectedComponentId
|
$: selectedComponentId = $store.selectedComponentId
|
||||||
|
$: hoverComponentId = $store.hoverComponentId
|
||||||
|
|
||||||
$: previewData = {
|
$: previewData = {
|
||||||
appId: $store.appId,
|
appId: $store.appId,
|
||||||
layout,
|
layout,
|
||||||
screen,
|
screen,
|
||||||
selectedComponentId,
|
selectedComponentId,
|
||||||
|
hoverComponentId,
|
||||||
theme: $store.theme,
|
theme: $store.theme,
|
||||||
customTheme: $store.customTheme,
|
customTheme: $store.customTheme,
|
||||||
previewDevice: $store.previewDevice,
|
previewDevice: $store.previewDevice,
|
||||||
|
@ -117,6 +119,8 @@
|
||||||
error = event.error || "An unknown error occurred"
|
error = event.error || "An unknown error occurred"
|
||||||
} else if (type === "select-component" && data.id) {
|
} else if (type === "select-component" && data.id) {
|
||||||
$store.selectedComponentId = data.id
|
$store.selectedComponentId = data.id
|
||||||
|
} else if (type === "hover-component" && data.id) {
|
||||||
|
$store.hoverComponentId = data.id
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
} else if (type === "update-styles") {
|
} else if (type === "update-styles") {
|
||||||
|
|
|
@ -89,6 +89,17 @@
|
||||||
}
|
}
|
||||||
return findComponentPath($selectedComponent, component._id)?.length > 0
|
return findComponentPath($selectedComponent, component._id)?.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMouseover = componentId => {
|
||||||
|
if ($store.hoverComponentId !== componentId) {
|
||||||
|
$store.hoverComponentId = componentId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleMouseout = componentId => {
|
||||||
|
if ($store.hoverComponentId === componentId) {
|
||||||
|
$store.hoverComponentId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -109,6 +120,9 @@
|
||||||
on:dragover={dragover(component, index)}
|
on:dragover={dragover(component, index)}
|
||||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
on:iconClick={() => toggleNodeOpen(component._id)}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
|
hovering={$store.hoverComponentId === component._id}
|
||||||
|
on:mouseenter={() => handleMouseover(component._id)}
|
||||||
|
on:mouseleave={() => handleMouseout(component._id)}
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
icon={getComponentIcon(component)}
|
icon={getComponentIcon(component)}
|
||||||
iconTooltip={getComponentName(component)}
|
iconTooltip={getComponentName(component)}
|
||||||
|
|
|
@ -32,6 +32,17 @@
|
||||||
const handleScroll = e => {
|
const handleScroll = e => {
|
||||||
scrolling = e.target.scrollTop !== 0
|
scrolling = e.target.scrollTop !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMouseover = componentId => {
|
||||||
|
if ($store.hoverComponentId !== componentId) {
|
||||||
|
$store.hoverComponentId = componentId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleMouseout = componentId => {
|
||||||
|
if ($store.hoverComponentId === componentId) {
|
||||||
|
$store.hoverComponentId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="components">
|
<div class="components">
|
||||||
|
@ -57,6 +68,12 @@
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$store.selectedComponentId = `${$store.selectedScreenId}-screen`
|
$store.selectedComponentId = `${$store.selectedScreenId}-screen`
|
||||||
}}
|
}}
|
||||||
|
hovering={$store.hoverComponentId ===
|
||||||
|
`${$store.selectedScreenId}-screen`}
|
||||||
|
on:mouseenter={() =>
|
||||||
|
handleMouseover(`${$store.selectedScreenId}-screen`)}
|
||||||
|
on:mouseleave={() =>
|
||||||
|
handleMouseout(`${$store.selectedScreenId}-screen`)}
|
||||||
id={`component-screen`}
|
id={`component-screen`}
|
||||||
selectedBy={$userSelectedResourceMap[
|
selectedBy={$userSelectedResourceMap[
|
||||||
`${$store.selectedScreenId}-screen`
|
`${$store.selectedScreenId}-screen`
|
||||||
|
@ -78,6 +95,12 @@
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$store.selectedComponentId = `${$store.selectedScreenId}-navigation`
|
$store.selectedComponentId = `${$store.selectedScreenId}-navigation`
|
||||||
}}
|
}}
|
||||||
|
hovering={$store.hoverComponentId ===
|
||||||
|
`${$store.selectedScreenId}-navigation`}
|
||||||
|
on:mouseenter={() =>
|
||||||
|
handleMouseover(`${$store.selectedScreenId}-navigation`)}
|
||||||
|
on:mouseleave={() =>
|
||||||
|
handleMouseout(`${$store.selectedScreenId}-navigation`)}
|
||||||
id={`component-nav`}
|
id={`component-nav`}
|
||||||
selectedBy={$userSelectedResourceMap[
|
selectedBy={$userSelectedResourceMap[
|
||||||
`${$store.selectedScreenId}-navigation`
|
`${$store.selectedScreenId}-navigation`
|
||||||
|
|
|
@ -81,13 +81,21 @@ export function createTablesStore() {
|
||||||
replaceTable(savedTable._id, savedTable)
|
replaceTable(savedTable._id, savedTable)
|
||||||
select(savedTable._id)
|
select(savedTable._id)
|
||||||
// make sure tables up to date (related)
|
// make sure tables up to date (related)
|
||||||
let tableIdsToFetch = []
|
let newTableIds = []
|
||||||
for (let column of Object.values(updatedTable?.schema || {})) {
|
for (let column of Object.values(updatedTable?.schema || {})) {
|
||||||
if (column.type === FIELDS.LINK.type) {
|
if (column.type === FIELDS.LINK.type) {
|
||||||
tableIdsToFetch.push(column.tableId)
|
newTableIds.push(column.tableId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tableIdsToFetch = [...new Set(tableIdsToFetch)]
|
|
||||||
|
let oldTableIds = []
|
||||||
|
for (let column of Object.values(oldTable?.schema || {})) {
|
||||||
|
if (column.type === FIELDS.LINK.type) {
|
||||||
|
oldTableIds.push(column.tableId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableIdsToFetch = [...new Set([...newTableIds, ...oldTableIds])]
|
||||||
// too many tables to fetch, just get all
|
// too many tables to fetch, just get all
|
||||||
if (tableIdsToFetch.length > 3) {
|
if (tableIdsToFetch.length > 3) {
|
||||||
await fetch()
|
await fetch()
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
"pkg": "pkg . --out-path build --no-bytecode --public --public-packages \"*\" -C GZip",
|
"pkg": "pkg . --out-path build --no-bytecode --public --public-packages \"*\" -C GZip",
|
||||||
"build": "yarn prebuild && yarn rename && yarn tsc && yarn pkg && yarn postbuild",
|
"build": "yarn prebuild && yarn rename && yarn tsc && yarn pkg && yarn postbuild",
|
||||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||||
"postbuild": "rm -rf prebuilds 2> /dev/null"
|
"postbuild": "rm -rf prebuilds 2> /dev/null",
|
||||||
|
"start": "ts-node ./src/index.ts"
|
||||||
},
|
},
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|
|
@ -6112,54 +6112,32 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tag": "style",
|
||||||
|
"type": "select",
|
||||||
|
"label": "Button position",
|
||||||
|
"key": "buttonPosition",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Bottom",
|
||||||
|
"value": "bottom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Top",
|
||||||
|
"value": "top"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultValue": "bottom"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Buttons",
|
"name": "Buttons",
|
||||||
"dependsOn": {
|
|
||||||
"setting": "actionType",
|
|
||||||
"value": "View",
|
|
||||||
"invert": true
|
|
||||||
},
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "buttonConfiguration",
|
||||||
"key": "saveButtonLabel",
|
"key": "buttons",
|
||||||
"label": "Save button",
|
|
||||||
"nested": true,
|
"nested": true,
|
||||||
"defaultValue": "Save"
|
"resetOn": ["actionType", "dataSource"]
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"key": "deleteButtonLabel",
|
|
||||||
"label": "Delete button",
|
|
||||||
"nested": true,
|
|
||||||
"defaultValue": "Delete",
|
|
||||||
"dependsOn": {
|
|
||||||
"setting": "actionType",
|
|
||||||
"value": "Update"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "url",
|
|
||||||
"label": "Navigate after button press",
|
|
||||||
"key": "actionUrl",
|
|
||||||
"placeholder": "Choose a screen",
|
|
||||||
"dependsOn": {
|
|
||||||
"setting": "actionType",
|
|
||||||
"value": "View",
|
|
||||||
"invert": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "boolean",
|
|
||||||
"label": "Hide notifications",
|
|
||||||
"key": "notificationOverride",
|
|
||||||
"defaultValue": false,
|
|
||||||
"dependsOn": {
|
|
||||||
"setting": "actionType",
|
|
||||||
"value": "View",
|
|
||||||
"invert": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
export let notificationOverride
|
export let notificationOverride
|
||||||
|
|
||||||
const { fetchDatasourceSchema, API } = getContext("sdk")
|
const { fetchDatasourceSchema, API } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
const stateKey = `ID_${generate()}`
|
const stateKey = `ID_${generate()}`
|
||||||
|
|
||||||
let formId
|
let formId
|
||||||
|
@ -259,16 +261,25 @@
|
||||||
name="Details form block"
|
name="Details form block"
|
||||||
type="formblock"
|
type="formblock"
|
||||||
bind:id={detailsFormBlockId}
|
bind:id={detailsFormBlockId}
|
||||||
|
context="form-edit"
|
||||||
props={{
|
props={{
|
||||||
dataSource,
|
dataSource,
|
||||||
saveButtonLabel: sidePanelSaveLabel || "Save", //always show
|
buttonPosition: "top",
|
||||||
deleteButtonLabel: deleteLabel,
|
buttons: Utils.buildDynamicButtonConfig({
|
||||||
|
_id: $component.id + "-form-edit",
|
||||||
|
showDeleteButton: deleteLabel !== "",
|
||||||
|
showSaveButton: true,
|
||||||
|
saveButtonLabel: sidePanelSaveLabel || "Save",
|
||||||
|
deleteButtonLabel: deleteLabel,
|
||||||
|
notificationOverride,
|
||||||
|
actionType: "Update",
|
||||||
|
dataSource,
|
||||||
|
}),
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
||||||
fields: sidePanelFields || normalFields,
|
fields: sidePanelFields || normalFields,
|
||||||
title: editTitle,
|
title: editTitle,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
notificationOverride,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
|
@ -284,16 +295,23 @@
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
name="New row form block"
|
name="New row form block"
|
||||||
type="formblock"
|
type="formblock"
|
||||||
|
context="form-new"
|
||||||
props={{
|
props={{
|
||||||
dataSource,
|
dataSource,
|
||||||
showSaveButton: true,
|
buttonPosition: "top",
|
||||||
showDeleteButton: false,
|
buttons: Utils.buildDynamicButtonConfig({
|
||||||
saveButtonLabel: sidePanelSaveLabel || "Save", //always show
|
_id: $component.id + "-form-new",
|
||||||
|
showDeleteButton: false,
|
||||||
|
showSaveButton: true,
|
||||||
|
saveButtonLabel: "Save",
|
||||||
|
notificationOverride,
|
||||||
|
actionType: "Create",
|
||||||
|
dataSource,
|
||||||
|
}),
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
fields: sidePanelFields || normalFields,
|
fields: sidePanelFields || normalFields,
|
||||||
title: "Create Row",
|
title: "Create Row",
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
notificationOverride,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
|
|
|
@ -4,28 +4,31 @@
|
||||||
import Block from "components/Block.svelte"
|
import Block from "components/Block.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import InnerFormBlock from "./InnerFormBlock.svelte"
|
import InnerFormBlock from "./InnerFormBlock.svelte"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let actionType
|
export let actionType
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let size
|
export let size
|
||||||
export let disabled
|
export let disabled
|
||||||
export let fields
|
export let fields
|
||||||
|
export let buttons
|
||||||
|
export let buttonPosition
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let description
|
export let description
|
||||||
export let showDeleteButton
|
|
||||||
export let showSaveButton
|
|
||||||
export let saveButtonLabel
|
|
||||||
export let deleteButtonLabel
|
|
||||||
export let rowId
|
export let rowId
|
||||||
export let actionUrl
|
export let actionUrl
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
export let notificationOverride
|
export let notificationOverride
|
||||||
|
|
||||||
// Accommodate old config to ensure delete button does not reappear
|
// Legacy
|
||||||
$: deleteLabel = showDeleteButton === false ? "" : deleteButtonLabel?.trim()
|
export let showDeleteButton
|
||||||
$: saveLabel = showSaveButton === false ? "" : saveButtonLabel?.trim()
|
export let showSaveButton
|
||||||
|
export let saveButtonLabel
|
||||||
|
export let deleteButtonLabel
|
||||||
|
|
||||||
const { fetchDatasourceSchema } = getContext("sdk")
|
const { fetchDatasourceSchema } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
const convertOldFieldFormat = fields => {
|
const convertOldFieldFormat = fields => {
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
|
@ -98,11 +101,23 @@
|
||||||
fields: fieldsOrDefault,
|
fields: fieldsOrDefault,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
saveButtonLabel: saveLabel,
|
|
||||||
deleteButtonLabel: deleteLabel,
|
|
||||||
schema,
|
schema,
|
||||||
repeaterId,
|
repeaterId,
|
||||||
notificationOverride,
|
notificationOverride,
|
||||||
|
buttons:
|
||||||
|
buttons ||
|
||||||
|
Utils.buildDynamicButtonConfig({
|
||||||
|
_id: $component.id,
|
||||||
|
showDeleteButton,
|
||||||
|
showSaveButton,
|
||||||
|
saveButtonLabel,
|
||||||
|
deleteButtonLabel,
|
||||||
|
notificationOverride,
|
||||||
|
actionType,
|
||||||
|
actionUrl,
|
||||||
|
dataSource,
|
||||||
|
}),
|
||||||
|
buttonPosition: buttons ? buttonPosition : "top",
|
||||||
}
|
}
|
||||||
const fetchSchema = async () => {
|
const fetchSchema = async () => {
|
||||||
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "components/app/Placeholder.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let actionUrl
|
|
||||||
export let actionType
|
export let actionType
|
||||||
export let size
|
export let size
|
||||||
export let disabled
|
export let disabled
|
||||||
export let fields
|
export let fields
|
||||||
export let title
|
export let title
|
||||||
export let description
|
export let description
|
||||||
export let saveButtonLabel
|
export let buttons
|
||||||
export let deleteButtonLabel
|
export let buttonPosition = "bottom"
|
||||||
export let schema
|
export let schema
|
||||||
export let repeaterId
|
|
||||||
export let notificationOverride
|
|
||||||
|
|
||||||
const FieldTypeToComponentMap = {
|
const FieldTypeToComponentMap = {
|
||||||
string: "stringfield",
|
string: "stringfield",
|
||||||
|
@ -37,74 +33,7 @@
|
||||||
|
|
||||||
let formId
|
let formId
|
||||||
|
|
||||||
$: onSave = [
|
$: renderHeader = buttons || title
|
||||||
{
|
|
||||||
"##eventHandlerType": "Validate Form",
|
|
||||||
parameters: {
|
|
||||||
componentId: formId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Save Row",
|
|
||||||
parameters: {
|
|
||||||
providerId: formId,
|
|
||||||
tableId: dataSource?.resourceId,
|
|
||||||
notificationOverride,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Close Screen Modal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Close Side Panel",
|
|
||||||
},
|
|
||||||
// Clear a create form once submitted
|
|
||||||
...(actionType !== "Create"
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Clear Form",
|
|
||||||
parameters: {
|
|
||||||
componentId: formId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Navigate To",
|
|
||||||
parameters: {
|
|
||||||
url: actionUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
$: onDelete = [
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Delete Row",
|
|
||||||
parameters: {
|
|
||||||
confirm: true,
|
|
||||||
tableId: dataSource?.resourceId,
|
|
||||||
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
|
||||||
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
|
||||||
notificationOverride,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Close Screen Modal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Close Side Panel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Navigate To",
|
|
||||||
parameters: {
|
|
||||||
url: actionUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
$: renderDeleteButton = deleteButtonLabel && actionType === "Update"
|
|
||||||
$: renderSaveButton = saveButtonLabel && actionType !== "View"
|
|
||||||
$: renderButtons = renderDeleteButton || renderSaveButton
|
|
||||||
$: renderHeader = renderButtons || title
|
|
||||||
|
|
||||||
const getComponentForField = field => {
|
const getComponentForField = field => {
|
||||||
const fieldSchemaName = field.field || field.name
|
const fieldSchemaName = field.field || field.name
|
||||||
|
@ -184,42 +113,14 @@
|
||||||
props={{ text: title || "" }}
|
props={{ text: title || "" }}
|
||||||
order={0}
|
order={0}
|
||||||
/>
|
/>
|
||||||
{#if renderButtons}
|
{#if buttonPosition == "top"}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="container"
|
type="buttongroup"
|
||||||
props={{
|
props={{
|
||||||
direction: "row",
|
buttons,
|
||||||
hAlign: "stretch",
|
|
||||||
vAlign: "center",
|
|
||||||
gap: "M",
|
|
||||||
wrap: true,
|
|
||||||
}}
|
}}
|
||||||
order={1}
|
order={0}
|
||||||
>
|
/>
|
||||||
{#if renderDeleteButton}
|
|
||||||
<BlockComponent
|
|
||||||
type="button"
|
|
||||||
props={{
|
|
||||||
text: deleteButtonLabel,
|
|
||||||
onClick: onDelete,
|
|
||||||
quiet: true,
|
|
||||||
type: "secondary",
|
|
||||||
}}
|
|
||||||
order={0}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if renderSaveButton}
|
|
||||||
<BlockComponent
|
|
||||||
type="button"
|
|
||||||
props={{
|
|
||||||
text: saveButtonLabel,
|
|
||||||
onClick: onSave,
|
|
||||||
type: "cta",
|
|
||||||
}}
|
|
||||||
order={1}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</BlockComponent>
|
|
||||||
{/if}
|
{/if}
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
|
@ -245,6 +146,20 @@
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{/key}
|
{/key}
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
|
{#if buttonPosition === "bottom"}
|
||||||
|
<BlockComponent
|
||||||
|
type="buttongroup"
|
||||||
|
props={{
|
||||||
|
buttons,
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
normal: {
|
||||||
|
"margin-top": "16",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
order={1}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{:else}
|
{:else}
|
||||||
<Placeholder
|
<Placeholder
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
import { builderStore, dndIsDragging } from "stores"
|
import { builderStore, dndIsDragging } from "stores"
|
||||||
|
|
||||||
let componentId
|
$: componentId = $builderStore.hoverComponentId
|
||||||
|
|
||||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = e => {
|
||||||
|
@ -24,12 +23,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
componentId = newId
|
builderStore.actions.hoverComponent(newId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
componentId = null
|
builderStore.actions.hoverComponent(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
|
@ -32,6 +32,7 @@ const loadBudibase = async () => {
|
||||||
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
||||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||||
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||||
|
hoverComponentId: window["##BUDIBASE_HOVER_COMPONENT_ID##"],
|
||||||
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||||
theme: window["##BUDIBASE_PREVIEW_THEME##"],
|
theme: window["##BUDIBASE_PREVIEW_THEME##"],
|
||||||
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
|
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
|
||||||
|
|
|
@ -8,6 +8,7 @@ const createBuilderStore = () => {
|
||||||
inBuilder: false,
|
inBuilder: false,
|
||||||
screen: null,
|
screen: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
|
hoverComponentId: null,
|
||||||
editMode: false,
|
editMode: false,
|
||||||
previewId: null,
|
previewId: null,
|
||||||
theme: null,
|
theme: null,
|
||||||
|
@ -23,6 +24,16 @@ const createBuilderStore = () => {
|
||||||
}
|
}
|
||||||
const store = writable(initialState)
|
const store = writable(initialState)
|
||||||
const actions = {
|
const actions = {
|
||||||
|
hoverComponent: id => {
|
||||||
|
if (id === get(store).hoverComponentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
hoverComponentId: id,
|
||||||
|
}))
|
||||||
|
eventStore.actions.dispatchEvent("hover-component", { id })
|
||||||
|
},
|
||||||
selectComponent: id => {
|
selectComponent: id => {
|
||||||
if (id === get(store).selectedComponentId) {
|
if (id === get(store).selectedComponentId) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to wrap an async function and ensure all invocations happen
|
* Utility to wrap an async function and ensure all invocations happen
|
||||||
* sequentially.
|
* sequentially.
|
||||||
|
@ -106,3 +109,135 @@ export const domDebounce = callback => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the default FormBlock button configs per actionType
|
||||||
|
* Parse any legacy button config and mirror its the outcome
|
||||||
|
*
|
||||||
|
* @param {any} props
|
||||||
|
* */
|
||||||
|
export const buildDynamicButtonConfig = props => {
|
||||||
|
const {
|
||||||
|
_id,
|
||||||
|
actionType,
|
||||||
|
dataSource,
|
||||||
|
notificationOverride,
|
||||||
|
actionUrl,
|
||||||
|
showDeleteButton,
|
||||||
|
deleteButtonLabel,
|
||||||
|
showSaveButton,
|
||||||
|
saveButtonLabel,
|
||||||
|
} = props || {}
|
||||||
|
|
||||||
|
if (!_id) {
|
||||||
|
console.log("MISSING ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const formId = `${_id}-form`
|
||||||
|
const repeaterId = `${_id}-repeater`
|
||||||
|
const resourceId = dataSource?.resourceId
|
||||||
|
|
||||||
|
// Accommodate old config to ensure delete button does not reappear
|
||||||
|
const deleteText = showDeleteButton === false ? "" : deleteButtonLabel?.trim()
|
||||||
|
const saveText = showSaveButton === false ? "" : saveButtonLabel?.trim()
|
||||||
|
|
||||||
|
const onSave = [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
parameters: {
|
||||||
|
componentId: formId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Save Row",
|
||||||
|
parameters: {
|
||||||
|
providerId: formId,
|
||||||
|
tableId: resourceId,
|
||||||
|
notificationOverride,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
},
|
||||||
|
// Clear a create form once submitted
|
||||||
|
...(actionType !== "Create"
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Clear Form",
|
||||||
|
parameters: {
|
||||||
|
componentId: formId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
...(actionUrl
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
url: actionUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
|
||||||
|
const onDelete = [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Delete Row",
|
||||||
|
parameters: {
|
||||||
|
confirm: true,
|
||||||
|
tableId: resourceId,
|
||||||
|
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
||||||
|
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
||||||
|
notificationOverride,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Screen Modal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Side Panel",
|
||||||
|
},
|
||||||
|
|
||||||
|
...(actionUrl
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
parameters: {
|
||||||
|
url: actionUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultButtons = []
|
||||||
|
|
||||||
|
if (["Update", "Create"].includes(actionType) && showSaveButton !== false) {
|
||||||
|
defaultButtons.push({
|
||||||
|
text: saveText || "Save",
|
||||||
|
_id: Helpers.uuid(),
|
||||||
|
_component: "@budibase/standard-components/button",
|
||||||
|
onClick: onSave,
|
||||||
|
type: "cta",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionType == "Update" && showDeleteButton !== false) {
|
||||||
|
defaultButtons.push({
|
||||||
|
text: deleteText || "Delete",
|
||||||
|
_id: Helpers.uuid(),
|
||||||
|
_component: "@budibase/standard-components/button",
|
||||||
|
onClick: onDelete,
|
||||||
|
quiet: true,
|
||||||
|
type: "secondary",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultButtons
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { quotas } from "@budibase/pro"
|
||||||
import { events, context, utils, constants } from "@budibase/backend-core"
|
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { QueryEvent } from "../../../threads/definitions"
|
import { QueryEvent } from "../../../threads/definitions"
|
||||||
import { ConfigType, Query, UserCtx } from "@budibase/types"
|
import { ConfigType, Query, UserCtx, SessionCookie } from "@budibase/types"
|
||||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||||
|
|
||||||
const Runner = new Thread(ThreadType.QUERY, {
|
const Runner = new Thread(ThreadType.QUERY, {
|
||||||
|
@ -113,7 +113,7 @@ function getOAuthConfigCookieId(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthConfig(ctx: UserCtx) {
|
function getAuthConfig(ctx: UserCtx) {
|
||||||
const authCookie = utils.getCookie(ctx, constants.Cookie.Auth)
|
const authCookie = utils.getCookie<SessionCookie>(ctx, constants.Cookie.Auth)
|
||||||
let authConfigCtx: any = {}
|
let authConfigCtx: any = {}
|
||||||
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
|
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
|
||||||
authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null
|
authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as linkRows from "../../../db/linkedRows"
|
||||||
import { generateRowID, InternalTables } from "../../../db/utils"
|
import { generateRowID, InternalTables } from "../../../db/utils"
|
||||||
import * as userController from "../user"
|
import * as userController from "../user"
|
||||||
import {
|
import {
|
||||||
cleanupAttachments,
|
AttachmentCleanup,
|
||||||
inputProcessing,
|
inputProcessing,
|
||||||
outputProcessing,
|
outputProcessing,
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
|
@ -79,7 +79,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
table,
|
table,
|
||||||
})) as Row
|
})) as Row
|
||||||
// check if any attachments removed
|
// check if any attachments removed
|
||||||
await cleanupAttachments(table, { oldRow, row })
|
await AttachmentCleanup.rowUpdate(table, { row, oldRow })
|
||||||
|
|
||||||
if (isUserTable) {
|
if (isUserTable) {
|
||||||
// the row has been updated, need to put it into the ctx
|
// the row has been updated, need to put it into the ctx
|
||||||
|
@ -119,7 +119,7 @@ export async function save(ctx: UserCtx) {
|
||||||
throw { validation: validateResult.errors }
|
throw { validation: validateResult.errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure link rows are up to date
|
// make sure link rows are up-to-date
|
||||||
row = (await linkRows.updateLinks({
|
row = (await linkRows.updateLinks({
|
||||||
eventType: linkRows.EventType.ROW_SAVE,
|
eventType: linkRows.EventType.ROW_SAVE,
|
||||||
row,
|
row,
|
||||||
|
@ -165,7 +165,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
tableId,
|
tableId,
|
||||||
})
|
})
|
||||||
// remove any attachments that were on the row from object storage
|
// remove any attachments that were on the row from object storage
|
||||||
await cleanupAttachments(table, { row })
|
await AttachmentCleanup.rowDelete(table, [row])
|
||||||
// remove any static formula
|
// remove any static formula
|
||||||
await updateRelatedFormula(table, row)
|
await updateRelatedFormula(table, row)
|
||||||
|
|
||||||
|
@ -216,7 +216,7 @@ export async function bulkDestroy(ctx: UserCtx) {
|
||||||
await db.bulkDocs(processedRows.map(row => ({ ...row, _deleted: true })))
|
await db.bulkDocs(processedRows.map(row => ({ ...row, _deleted: true })))
|
||||||
}
|
}
|
||||||
// remove any attachments that were on the rows from object storage
|
// remove any attachments that were on the rows from object storage
|
||||||
await cleanupAttachments(table, { rows: processedRows })
|
await AttachmentCleanup.rowDelete(table, processedRows)
|
||||||
await updateRelatedFormula(table, processedRows)
|
await updateRelatedFormula(table, processedRows)
|
||||||
await Promise.all(updates)
|
await Promise.all(updates)
|
||||||
return { response: { ok: true }, rows: processedRows }
|
return { response: { ok: true }, rows: processedRows }
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
// Extract data from message
|
// Extract data from message
|
||||||
const {
|
const {
|
||||||
selectedComponentId,
|
selectedComponentId,
|
||||||
|
hoverComponentId,
|
||||||
layout,
|
layout,
|
||||||
screen,
|
screen,
|
||||||
appId,
|
appId,
|
||||||
|
@ -81,6 +82,7 @@
|
||||||
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
|
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
|
||||||
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
||||||
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
||||||
|
window["##BUDIBASE_HOVER_COMPONENT_ID##"] = hoverComponentId
|
||||||
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
||||||
window["##BUDIBASE_PREVIEW_THEME##"] = theme
|
window["##BUDIBASE_PREVIEW_THEME##"] = theme
|
||||||
window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme
|
window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme
|
||||||
|
@ -108,4 +110,4 @@
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "../../../constants"
|
} from "../../../constants"
|
||||||
import {
|
import {
|
||||||
inputProcessing,
|
inputProcessing,
|
||||||
cleanupAttachments,
|
AttachmentCleanup,
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import { getViews, saveView } from "../view/utils"
|
import { getViews, saveView } from "../view/utils"
|
||||||
import viewTemplate from "../view/viewBuilder"
|
import viewTemplate from "../view/viewBuilder"
|
||||||
|
@ -82,7 +82,10 @@ export async function checkForColumnUpdates(
|
||||||
})
|
})
|
||||||
|
|
||||||
// cleanup any attachments from object storage for deleted attachment columns
|
// cleanup any attachments from object storage for deleted attachment columns
|
||||||
await cleanupAttachments(updatedTable, { oldTable, rows: rawRows })
|
await AttachmentCleanup.tableUpdate(updatedTable, rawRows, {
|
||||||
|
oldTable,
|
||||||
|
rename: columnRename,
|
||||||
|
})
|
||||||
// Update views
|
// Update views
|
||||||
await checkForViewUpdates(updatedTable, deletedColumns, columnRename)
|
await checkForViewUpdates(updatedTable, deletedColumns, columnRename)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ const environment = {
|
||||||
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
||||||
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
|
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
|
||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
|
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
||||||
// flags
|
// flags
|
||||||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
buildExternalTableId,
|
buildExternalTableId,
|
||||||
convertSqlType,
|
generateColumnDefinition,
|
||||||
finaliseExternalTables,
|
finaliseExternalTables,
|
||||||
SqlClient,
|
SqlClient,
|
||||||
checkExternalTables,
|
checkExternalTables,
|
||||||
|
@ -429,15 +429,12 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
||||||
const hasDefault = def.COLUMN_DEFAULT
|
const hasDefault = def.COLUMN_DEFAULT
|
||||||
const isAuto = !!autoColumns.find(col => col === name)
|
const isAuto = !!autoColumns.find(col => col === name)
|
||||||
const required = !!requiredColumns.find(col => col === name)
|
const required = !!requiredColumns.find(col => col === name)
|
||||||
schema[name] = {
|
schema[name] = generateColumnDefinition({
|
||||||
autocolumn: isAuto,
|
autocolumn: isAuto,
|
||||||
name: name,
|
name,
|
||||||
constraints: {
|
presence: required && !isAuto && !hasDefault,
|
||||||
presence: required && !isAuto && !hasDefault,
|
|
||||||
},
|
|
||||||
...convertSqlType(def.DATA_TYPE),
|
|
||||||
externalType: def.DATA_TYPE,
|
externalType: def.DATA_TYPE,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
tables[tableName] = {
|
tables[tableName] = {
|
||||||
_id: buildExternalTableId(datasourceId, tableName),
|
_id: buildExternalTableId(datasourceId, tableName),
|
||||||
|
|
|
@ -12,12 +12,13 @@ import {
|
||||||
SourceName,
|
SourceName,
|
||||||
Schema,
|
Schema,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
|
FieldType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
SqlClient,
|
SqlClient,
|
||||||
buildExternalTableId,
|
buildExternalTableId,
|
||||||
convertSqlType,
|
generateColumnDefinition,
|
||||||
finaliseExternalTables,
|
finaliseExternalTables,
|
||||||
checkExternalTables,
|
checkExternalTables,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
|
@ -305,16 +306,17 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
(column.Extra === "auto_increment" ||
|
(column.Extra === "auto_increment" ||
|
||||||
column.Extra.toLowerCase().includes("generated"))
|
column.Extra.toLowerCase().includes("generated"))
|
||||||
const required = column.Null !== "YES"
|
const required = column.Null !== "YES"
|
||||||
const constraints = {
|
schema[columnName] = generateColumnDefinition({
|
||||||
presence: required && !isAuto && !hasDefault,
|
|
||||||
}
|
|
||||||
schema[columnName] = {
|
|
||||||
name: columnName,
|
name: columnName,
|
||||||
autocolumn: isAuto,
|
autocolumn: isAuto,
|
||||||
constraints,
|
presence: required && !isAuto && !hasDefault,
|
||||||
...convertSqlType(column.Type),
|
|
||||||
externalType: column.Type,
|
externalType: column.Type,
|
||||||
}
|
options: column.Type.startsWith("enum")
|
||||||
|
? column.Type.substring(5, column.Type.length - 1)
|
||||||
|
.split(",")
|
||||||
|
.map(str => str.replace(/^'(.*)'$/, "$1"))
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (!tables[tableName]) {
|
if (!tables[tableName]) {
|
||||||
tables[tableName] = {
|
tables[tableName] = {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import {
|
import {
|
||||||
buildExternalTableId,
|
buildExternalTableId,
|
||||||
checkExternalTables,
|
checkExternalTables,
|
||||||
convertSqlType,
|
generateColumnDefinition,
|
||||||
finaliseExternalTables,
|
finaliseExternalTables,
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
SqlClient,
|
SqlClient,
|
||||||
|
@ -250,14 +250,6 @@ class OracleIntegration extends Sql implements DatasourcePlus {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private internalConvertType(column: OracleColumn) {
|
|
||||||
if (this.isBooleanType(column)) {
|
|
||||||
return { type: FieldTypes.BOOLEAN }
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertSqlType(column.type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the tables from the oracle table and assigns them to the datasource.
|
* Fetches the tables from the oracle table and assigns them to the datasource.
|
||||||
* @param datasourceId - datasourceId to fetch
|
* @param datasourceId - datasourceId to fetch
|
||||||
|
@ -302,13 +294,15 @@ class OracleIntegration extends Sql implements DatasourcePlus {
|
||||||
const columnName = oracleColumn.name
|
const columnName = oracleColumn.name
|
||||||
let fieldSchema = table.schema[columnName]
|
let fieldSchema = table.schema[columnName]
|
||||||
if (!fieldSchema) {
|
if (!fieldSchema) {
|
||||||
fieldSchema = {
|
fieldSchema = generateColumnDefinition({
|
||||||
autocolumn: OracleIntegration.isAutoColumn(oracleColumn),
|
autocolumn: OracleIntegration.isAutoColumn(oracleColumn),
|
||||||
name: columnName,
|
name: columnName,
|
||||||
constraints: {
|
presence: false,
|
||||||
presence: false,
|
externalType: oracleColumn.type,
|
||||||
},
|
})
|
||||||
...this.internalConvertType(oracleColumn),
|
|
||||||
|
if (this.isBooleanType(oracleColumn)) {
|
||||||
|
fieldSchema.type = FieldTypes.BOOLEAN
|
||||||
}
|
}
|
||||||
|
|
||||||
table.schema[columnName] = fieldSchema
|
table.schema[columnName] = fieldSchema
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
buildExternalTableId,
|
buildExternalTableId,
|
||||||
convertSqlType,
|
generateColumnDefinition,
|
||||||
finaliseExternalTables,
|
finaliseExternalTables,
|
||||||
SqlClient,
|
SqlClient,
|
||||||
checkExternalTables,
|
checkExternalTables,
|
||||||
|
@ -162,6 +162,14 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
WHERE pg_namespace.nspname = '${this.config.schema}';
|
WHERE pg_namespace.nspname = '${this.config.schema}';
|
||||||
`
|
`
|
||||||
|
|
||||||
|
ENUM_VALUES = () => `
|
||||||
|
SELECT t.typname,
|
||||||
|
e.enumlabel
|
||||||
|
FROM pg_type t
|
||||||
|
JOIN pg_enum e on t.oid = e.enumtypid
|
||||||
|
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace;
|
||||||
|
`
|
||||||
|
|
||||||
constructor(config: PostgresConfig) {
|
constructor(config: PostgresConfig) {
|
||||||
super(SqlClient.POSTGRES)
|
super(SqlClient.POSTGRES)
|
||||||
this.config = config
|
this.config = config
|
||||||
|
@ -303,6 +311,18 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
|
|
||||||
const tables: { [key: string]: Table } = {}
|
const tables: { [key: string]: Table } = {}
|
||||||
|
|
||||||
|
// Fetch enum values
|
||||||
|
const enumsResponse = await this.client.query(this.ENUM_VALUES())
|
||||||
|
const enumValues = enumsResponse.rows?.reduce((acc, row) => {
|
||||||
|
if (!acc[row.typname]) {
|
||||||
|
return {
|
||||||
|
[row.typname]: [row.enumlabel],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc[row.typname].push(row.enumlabel)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
for (let column of columnsResponse.rows) {
|
for (let column of columnsResponse.rows) {
|
||||||
const tableName: string = column.table_name
|
const tableName: string = column.table_name
|
||||||
const columnName: string = column.column_name
|
const columnName: string = column.column_name
|
||||||
|
@ -333,16 +353,13 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
column.is_generated && column.is_generated !== "NEVER"
|
column.is_generated && column.is_generated !== "NEVER"
|
||||||
const isAuto: boolean = hasNextVal || identity || isGenerated
|
const isAuto: boolean = hasNextVal || identity || isGenerated
|
||||||
const required = column.is_nullable === "NO"
|
const required = column.is_nullable === "NO"
|
||||||
const constraints = {
|
tables[tableName].schema[columnName] = generateColumnDefinition({
|
||||||
presence: required && !hasDefault && !isGenerated,
|
|
||||||
}
|
|
||||||
tables[tableName].schema[columnName] = {
|
|
||||||
autocolumn: isAuto,
|
autocolumn: isAuto,
|
||||||
name: columnName,
|
name: columnName,
|
||||||
constraints,
|
presence: required && !hasDefault && !isGenerated,
|
||||||
...convertSqlType(column.data_type),
|
|
||||||
externalType: column.data_type,
|
externalType: column.data_type,
|
||||||
}
|
options: enumValues?.[column.udt_name],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let finalizedTables = finaliseExternalTables(tables, entities)
|
let finalizedTables = finaliseExternalTables(tables, entities)
|
||||||
|
|
|
@ -67,6 +67,10 @@ const SQL_BOOLEAN_TYPE_MAP = {
|
||||||
tinyint: FieldType.BOOLEAN,
|
tinyint: FieldType.BOOLEAN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SQL_OPTIONS_TYPE_MAP = {
|
||||||
|
"user-defined": FieldType.OPTIONS,
|
||||||
|
}
|
||||||
|
|
||||||
const SQL_MISC_TYPE_MAP = {
|
const SQL_MISC_TYPE_MAP = {
|
||||||
json: FieldType.JSON,
|
json: FieldType.JSON,
|
||||||
bigint: FieldType.BIGINT,
|
bigint: FieldType.BIGINT,
|
||||||
|
@ -78,6 +82,7 @@ const SQL_TYPE_MAP = {
|
||||||
...SQL_STRING_TYPE_MAP,
|
...SQL_STRING_TYPE_MAP,
|
||||||
...SQL_BOOLEAN_TYPE_MAP,
|
...SQL_BOOLEAN_TYPE_MAP,
|
||||||
...SQL_MISC_TYPE_MAP,
|
...SQL_MISC_TYPE_MAP,
|
||||||
|
...SQL_OPTIONS_TYPE_MAP,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SqlClient {
|
export enum SqlClient {
|
||||||
|
@ -178,25 +183,49 @@ export function breakRowIdField(_id: string | { _id: string }): any[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertSqlType(type: string) {
|
export function generateColumnDefinition(config: {
|
||||||
|
externalType: string
|
||||||
|
autocolumn: boolean
|
||||||
|
name: string
|
||||||
|
presence: boolean
|
||||||
|
options?: string[]
|
||||||
|
}) {
|
||||||
|
let { externalType, autocolumn, name, presence, options } = config
|
||||||
let foundType = FieldType.STRING
|
let foundType = FieldType.STRING
|
||||||
const lcType = type.toLowerCase()
|
const lowerCaseType = externalType.toLowerCase()
|
||||||
let matchingTypes = []
|
let matchingTypes = []
|
||||||
for (let [external, internal] of Object.entries(SQL_TYPE_MAP)) {
|
for (let [external, internal] of Object.entries(SQL_TYPE_MAP)) {
|
||||||
if (lcType.includes(external)) {
|
if (lowerCaseType.includes(external)) {
|
||||||
matchingTypes.push({ external, internal })
|
matchingTypes.push({ external, internal })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Set the foundType based the longest match
|
// Set the foundType based the longest match
|
||||||
if (matchingTypes.length > 0) {
|
if (matchingTypes.length > 0) {
|
||||||
foundType = matchingTypes.reduce((acc, val) => {
|
foundType = matchingTypes.reduce((acc, val) => {
|
||||||
return acc.external.length >= val.external.length ? acc : val
|
return acc.external.length >= val.external.length ? acc : val
|
||||||
}).internal
|
}).internal
|
||||||
}
|
}
|
||||||
const schema: any = { type: foundType }
|
|
||||||
|
const constraints: {
|
||||||
|
presence: boolean
|
||||||
|
inclusion?: string[]
|
||||||
|
} = {
|
||||||
|
presence,
|
||||||
|
}
|
||||||
|
if (foundType === FieldType.OPTIONS) {
|
||||||
|
constraints.inclusion = options
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema: any = {
|
||||||
|
type: foundType,
|
||||||
|
externalType,
|
||||||
|
autocolumn,
|
||||||
|
name,
|
||||||
|
constraints,
|
||||||
|
}
|
||||||
if (foundType === FieldType.DATETIME) {
|
if (foundType === FieldType.DATETIME) {
|
||||||
schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lcType)
|
schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lowerCaseType)
|
||||||
schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lcType)
|
schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lowerCaseType)
|
||||||
}
|
}
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import env from "./environment"
|
import env from "./environment"
|
||||||
import Koa, { ExtendableContext } from "koa"
|
import Koa from "koa"
|
||||||
import koaBody from "koa-body"
|
import koaBody from "koa-body"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import * as api from "./api"
|
import * as api from "./api"
|
||||||
|
@ -27,6 +27,9 @@ export default function createKoaApp() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
enableTypes: ["json", "form", "text"],
|
enableTypes: ["json", "form", "text"],
|
||||||
parsedMethods: ["POST", "PUT", "PATCH", "DELETE"],
|
parsedMethods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||||
|
formidable: {
|
||||||
|
maxFileSize: parseInt(env.MAX_IMPORT_SIZE_MB || "100") * 1024 * 1024,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export const DB_EXPORT_FILE = "db.txt"
|
export const DB_EXPORT_FILE = "db.txt"
|
||||||
export const GLOBAL_DB_EXPORT_FILE = "global.txt"
|
export const GLOBAL_DB_EXPORT_FILE = "global.txt"
|
||||||
export const STATIC_APP_FILES = ["manifest.json", "budibase-client.js"]
|
export const STATIC_APP_FILES = ["manifest.json", "budibase-client.js"]
|
||||||
|
export const ATTACHMENT_DIRECTORY = "attachments"
|
||||||
|
|
|
@ -8,13 +8,15 @@ import {
|
||||||
TABLE_ROW_PREFIX,
|
TABLE_ROW_PREFIX,
|
||||||
USER_METDATA_PREFIX,
|
USER_METDATA_PREFIX,
|
||||||
} from "../../../db/utils"
|
} from "../../../db/utils"
|
||||||
import { DB_EXPORT_FILE, STATIC_APP_FILES } from "./constants"
|
import {
|
||||||
|
DB_EXPORT_FILE,
|
||||||
|
STATIC_APP_FILES,
|
||||||
|
ATTACHMENT_DIRECTORY,
|
||||||
|
} from "./constants"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
const uuid = require("uuid/v4")
|
|
||||||
|
|
||||||
import tar from "tar"
|
import tar from "tar"
|
||||||
|
|
||||||
const MemoryStream = require("memorystream")
|
const MemoryStream = require("memorystream")
|
||||||
|
@ -30,12 +32,11 @@ export interface ExportOpts extends DBDumpOpts {
|
||||||
encryptPassword?: string
|
encryptPassword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function tarFilesToTmp(tmpDir: string, files: string[]) {
|
async function tarFilesToTmp(tmpDir: string, files: string[]) {
|
||||||
const fileName = `${uuid()}.tar.gz`
|
const fileName = `${uuid()}.tar.gz`
|
||||||
const exportFile = join(budibaseTempDir(), fileName)
|
const exportFile = join(budibaseTempDir(), fileName)
|
||||||
tar.create(
|
await tar.create(
|
||||||
{
|
{
|
||||||
sync: true,
|
|
||||||
gzip: true,
|
gzip: true,
|
||||||
file: exportFile,
|
file: exportFile,
|
||||||
noDirRecurse: false,
|
noDirRecurse: false,
|
||||||
|
@ -150,19 +151,21 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
||||||
for (let file of fs.readdirSync(tmpPath)) {
|
for (let file of fs.readdirSync(tmpPath)) {
|
||||||
const path = join(tmpPath, file)
|
const path = join(tmpPath, file)
|
||||||
|
|
||||||
await encryption.encryptFile(
|
// skip the attachments - too big to encrypt
|
||||||
{ dir: tmpPath, filename: file },
|
if (file !== ATTACHMENT_DIRECTORY) {
|
||||||
config.encryptPassword
|
await encryption.encryptFile(
|
||||||
)
|
{ dir: tmpPath, filename: file },
|
||||||
|
config.encryptPassword
|
||||||
fs.rmSync(path)
|
)
|
||||||
|
fs.rmSync(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if tar requested, return where the tarball is
|
// if tar requested, return where the tarball is
|
||||||
if (config?.tar) {
|
if (config?.tar) {
|
||||||
// now the tmpPath contains both the DB export and attachments, tar this
|
// now the tmpPath contains both the DB export and attachments, tar this
|
||||||
const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
const tarPath = await tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
||||||
// cleanup the tmp export files as tarball returned
|
// cleanup the tmp export files as tarball returned
|
||||||
fs.rmSync(tmpPath, { recursive: true, force: true })
|
fs.rmSync(tmpPath, { recursive: true, force: true })
|
||||||
|
|
||||||
|
|
|
@ -6,17 +6,20 @@ import {
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
RowAttachment,
|
RowAttachment,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
|
import { getAutomationParams } from "../../../db/utils"
|
||||||
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
||||||
import { DB_EXPORT_FILE, GLOBAL_DB_EXPORT_FILE } from "./constants"
|
import {
|
||||||
|
DB_EXPORT_FILE,
|
||||||
|
GLOBAL_DB_EXPORT_FILE,
|
||||||
|
ATTACHMENT_DIRECTORY,
|
||||||
|
} from "./constants"
|
||||||
import { downloadTemplate } from "../../../utilities/fileSystem"
|
import { downloadTemplate } from "../../../utilities/fileSystem"
|
||||||
import { ObjectStoreBuckets } from "../../../constants"
|
import { ObjectStoreBuckets } from "../../../constants"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import sdk from "../../"
|
import sdk from "../../"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
const uuid = require("uuid/v4")
|
import tar from "tar"
|
||||||
const tar = require("tar")
|
|
||||||
|
|
||||||
type TemplateType = {
|
type TemplateType = {
|
||||||
file?: {
|
file?: {
|
||||||
|
@ -114,12 +117,11 @@ async function getTemplateStream(template: TemplateType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function untarFile(file: { path: string }) {
|
export async function untarFile(file: { path: string }) {
|
||||||
const tmpPath = join(budibaseTempDir(), uuid())
|
const tmpPath = join(budibaseTempDir(), uuid())
|
||||||
fs.mkdirSync(tmpPath)
|
fs.mkdirSync(tmpPath)
|
||||||
// extract the tarball
|
// extract the tarball
|
||||||
tar.extract({
|
await tar.extract({
|
||||||
sync: true,
|
|
||||||
cwd: tmpPath,
|
cwd: tmpPath,
|
||||||
file: file.path,
|
file: file.path,
|
||||||
})
|
})
|
||||||
|
@ -130,9 +132,11 @@ async function decryptFiles(path: string, password: string) {
|
||||||
try {
|
try {
|
||||||
for (let file of fs.readdirSync(path)) {
|
for (let file of fs.readdirSync(path)) {
|
||||||
const inputPath = join(path, file)
|
const inputPath = join(path, file)
|
||||||
const outputPath = inputPath.replace(/\.enc$/, "")
|
if (!inputPath.endsWith(ATTACHMENT_DIRECTORY)) {
|
||||||
await encryption.decryptFile(inputPath, outputPath, password)
|
const outputPath = inputPath.replace(/\.enc$/, "")
|
||||||
fs.rmSync(inputPath)
|
await encryption.decryptFile(inputPath, outputPath, password)
|
||||||
|
fs.rmSync(inputPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.message === "incorrect header check") {
|
if (err.message === "incorrect header check") {
|
||||||
|
@ -162,7 +166,7 @@ export async function importApp(
|
||||||
const isDirectory =
|
const isDirectory =
|
||||||
template.file && fs.lstatSync(template.file.path).isDirectory()
|
template.file && fs.lstatSync(template.file.path).isDirectory()
|
||||||
if (template.file && (isTar || isDirectory)) {
|
if (template.file && (isTar || isDirectory)) {
|
||||||
const tmpPath = isTar ? untarFile(template.file) : template.file.path
|
const tmpPath = isTar ? await untarFile(template.file) : template.file.path
|
||||||
if (isTar && template.file.password) {
|
if (isTar && template.file.password) {
|
||||||
await decryptFiles(tmpPath, template.file.password)
|
await decryptFiles(tmpPath, template.file.password)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,10 @@ import { context } from "@budibase/backend-core"
|
||||||
import { getTable } from "../getters"
|
import { getTable } from "../getters"
|
||||||
import { checkAutoColumns } from "./utils"
|
import { checkAutoColumns } from "./utils"
|
||||||
import * as viewsSdk from "../../views"
|
import * as viewsSdk from "../../views"
|
||||||
import sdk from "../../../index"
|
|
||||||
import { getRowParams } from "../../../../db/utils"
|
import { getRowParams } from "../../../../db/utils"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import env from "../../../../environment"
|
import env from "../../../../environment"
|
||||||
import { cleanupAttachments } from "../../../../utilities/rowProcessor"
|
import { AttachmentCleanup } from "../../../../utilities/rowProcessor"
|
||||||
|
|
||||||
export async function save(
|
export async function save(
|
||||||
table: Table,
|
table: Table,
|
||||||
|
@ -164,9 +163,10 @@ export async function destroy(table: Table) {
|
||||||
await runStaticFormulaChecks(table, {
|
await runStaticFormulaChecks(table, {
|
||||||
deletion: true,
|
deletion: true,
|
||||||
})
|
})
|
||||||
await cleanupAttachments(table, {
|
await AttachmentCleanup.tableDelete(
|
||||||
rows: rowsData.rows.map((row: any) => row.doc),
|
table,
|
||||||
})
|
rowsData.rows.map((row: any) => row.doc)
|
||||||
|
)
|
||||||
|
|
||||||
return { table }
|
return { table }
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ import {
|
||||||
|
|
||||||
import API from "./api"
|
import API from "./api"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
|
import jwt, { Secret } from "jsonwebtoken"
|
||||||
|
|
||||||
mocks.licenses.init(pro)
|
mocks.licenses.init(pro)
|
||||||
|
|
||||||
|
@ -391,7 +392,7 @@ class TestConfiguration {
|
||||||
sessionId: "sessionid",
|
sessionId: "sessionid",
|
||||||
tenantId: this.getTenantId(),
|
tenantId: this.getTenantId(),
|
||||||
}
|
}
|
||||||
const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET)
|
const authToken = jwt.sign(authObj, coreEnv.JWT_SECRET as Secret)
|
||||||
|
|
||||||
// returning necessary request headers
|
// returning necessary request headers
|
||||||
await cache.user.invalidateUser(userId)
|
await cache.user.invalidateUser(userId)
|
||||||
|
@ -412,7 +413,7 @@ class TestConfiguration {
|
||||||
sessionId: "sessionid",
|
sessionId: "sessionid",
|
||||||
tenantId,
|
tenantId,
|
||||||
}
|
}
|
||||||
const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET)
|
const authToken = jwt.sign(authObj, coreEnv.JWT_SECRET as Secret)
|
||||||
|
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { FieldTypes, ObjectStoreBuckets } from "../../constants"
|
||||||
|
import { context, db as dbCore, objectStore } from "@budibase/backend-core"
|
||||||
|
import { RenameColumn, Row, RowAttachment, Table } from "@budibase/types"
|
||||||
|
|
||||||
|
export class AttachmentCleanup {
|
||||||
|
static async coreCleanup(fileListFn: () => string[]): Promise<void> {
|
||||||
|
const appId = context.getAppId()
|
||||||
|
if (!dbCore.isProdAppID(appId)) {
|
||||||
|
const prodAppId = dbCore.getProdAppID(appId!)
|
||||||
|
// if prod exists, then don't allow deleting
|
||||||
|
const exists = await dbCore.dbExists(prodAppId)
|
||||||
|
if (exists) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const files = fileListFn()
|
||||||
|
if (files.length > 0) {
|
||||||
|
await objectStore.deleteFiles(ObjectStoreBuckets.APPS, files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async tableChange(
|
||||||
|
table: Table,
|
||||||
|
rows: Row[],
|
||||||
|
opts: { oldTable?: Table; rename?: RenameColumn; deleting?: boolean }
|
||||||
|
) {
|
||||||
|
return AttachmentCleanup.coreCleanup(() => {
|
||||||
|
let files: string[] = []
|
||||||
|
const tableSchema = opts.oldTable?.schema || table.schema
|
||||||
|
for (let [key, schema] of Object.entries(tableSchema)) {
|
||||||
|
if (schema.type !== FieldTypes.ATTACHMENT) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const columnRemoved = opts.oldTable && !table.schema[key]
|
||||||
|
const renaming = opts.rename?.old === key
|
||||||
|
// old table had this column, new table doesn't - delete it
|
||||||
|
if ((columnRemoved && !renaming) || opts.deleting) {
|
||||||
|
rows.forEach(row => {
|
||||||
|
files = files.concat(
|
||||||
|
row[key].map((attachment: any) => attachment.key)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async tableDelete(table: Table, rows: Row[]) {
|
||||||
|
return AttachmentCleanup.tableChange(table, rows, { deleting: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
static async tableUpdate(
|
||||||
|
table: Table,
|
||||||
|
rows: Row[],
|
||||||
|
opts: { oldTable?: Table; rename?: RenameColumn }
|
||||||
|
) {
|
||||||
|
return AttachmentCleanup.tableChange(table, rows, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async rowDelete(table: Table, rows: Row[]) {
|
||||||
|
return AttachmentCleanup.coreCleanup(() => {
|
||||||
|
let files: string[] = []
|
||||||
|
for (let [key, schema] of Object.entries(table.schema)) {
|
||||||
|
if (schema.type !== FieldTypes.ATTACHMENT) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows.forEach(row => {
|
||||||
|
files = files.concat(
|
||||||
|
row[key].map((attachment: any) => attachment.key)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static rowUpdate(table: Table, opts: { row: Row; oldRow: Row }) {
|
||||||
|
return AttachmentCleanup.coreCleanup(() => {
|
||||||
|
let files: string[] = []
|
||||||
|
for (let [key, schema] of Object.entries(table.schema)) {
|
||||||
|
if (schema.type !== FieldTypes.ATTACHMENT) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const oldKeys =
|
||||||
|
opts.oldRow[key]?.map(
|
||||||
|
(attachment: RowAttachment) => attachment.key
|
||||||
|
) || []
|
||||||
|
const newKeys =
|
||||||
|
opts.row[key]?.map((attachment: RowAttachment) => attachment.key) ||
|
||||||
|
[]
|
||||||
|
files = files.concat(
|
||||||
|
oldKeys.filter((key: string) => newKeys.indexOf(key) === -1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,7 @@
|
||||||
import * as linkRows from "../../db/linkedRows"
|
import * as linkRows from "../../db/linkedRows"
|
||||||
import {
|
import { FieldTypes, AutoFieldSubTypes } from "../../constants"
|
||||||
FieldTypes,
|
|
||||||
AutoFieldSubTypes,
|
|
||||||
ObjectStoreBuckets,
|
|
||||||
} from "../../constants"
|
|
||||||
import { processFormulas, fixAutoColumnSubType } from "./utils"
|
import { processFormulas, fixAutoColumnSubType } from "./utils"
|
||||||
import {
|
import { objectStore, utils } from "@budibase/backend-core"
|
||||||
context,
|
|
||||||
db as dbCore,
|
|
||||||
objectStore,
|
|
||||||
utils,
|
|
||||||
} from "@budibase/backend-core"
|
|
||||||
import { InternalTables } from "../../db/utils"
|
import { InternalTables } from "../../db/utils"
|
||||||
import { TYPE_TRANSFORM_MAP } from "./map"
|
import { TYPE_TRANSFORM_MAP } from "./map"
|
||||||
import { FieldSubtype, Row, RowAttachment, Table } from "@budibase/types"
|
import { FieldSubtype, Row, RowAttachment, Table } from "@budibase/types"
|
||||||
|
@ -22,6 +13,7 @@ import {
|
||||||
import { isExternalTableID } from "../../integrations/utils"
|
import { isExternalTableID } from "../../integrations/utils"
|
||||||
|
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
|
export * from "./attachments"
|
||||||
|
|
||||||
type AutoColumnProcessingOpts = {
|
type AutoColumnProcessingOpts = {
|
||||||
reprocessing?: boolean
|
reprocessing?: boolean
|
||||||
|
@ -30,27 +22,6 @@ type AutoColumnProcessingOpts = {
|
||||||
|
|
||||||
const BASE_AUTO_ID = 1
|
const BASE_AUTO_ID = 1
|
||||||
|
|
||||||
/**
|
|
||||||
* Given the old state of the row and the new one after an update, this will
|
|
||||||
* find the keys that have been removed in the updated row.
|
|
||||||
*/
|
|
||||||
function getRemovedAttachmentKeys(
|
|
||||||
oldRow: Row,
|
|
||||||
row: Row,
|
|
||||||
attachmentKey: string
|
|
||||||
) {
|
|
||||||
if (!oldRow[attachmentKey]) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const oldKeys = oldRow[attachmentKey].map((attachment: any) => attachment.key)
|
|
||||||
// no attachments in new row, all removed
|
|
||||||
if (!row[attachmentKey]) {
|
|
||||||
return oldKeys
|
|
||||||
}
|
|
||||||
const newKeys = row[attachmentKey].map((attachment: any) => attachment.key)
|
|
||||||
return oldKeys.filter((key: string) => newKeys.indexOf(key) === -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will update any auto columns that are found on the row/table with the correct information based on
|
* This will update any auto columns that are found on the row/table with the correct information based on
|
||||||
* time now and the current logged in user making the request.
|
* time now and the current logged in user making the request.
|
||||||
|
@ -249,7 +220,9 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
row[property].forEach((attachment: RowAttachment) => {
|
row[property].forEach((attachment: RowAttachment) => {
|
||||||
attachment.url ??= objectStore.getAppFileUrl(attachment.key)
|
if (!attachment.url) {
|
||||||
|
attachment.url = objectStore.getAppFileUrl(attachment.key)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -286,59 +259,3 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
}
|
}
|
||||||
return (wasArray ? enriched : enriched[0]) as T
|
return (wasArray ? enriched : enriched[0]) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up any attachments that were attached to a row.
|
|
||||||
* @param table The table from which a row is being removed.
|
|
||||||
* @param row optional - the row being removed.
|
|
||||||
* @param rows optional - if multiple rows being deleted can do this in bulk.
|
|
||||||
* @param oldRow optional - if updating a row this will determine the difference.
|
|
||||||
* @param oldTable optional - if updating a table, can supply the old table to look for
|
|
||||||
* deleted attachment columns.
|
|
||||||
* @return When all attachments have been removed this will return.
|
|
||||||
*/
|
|
||||||
export async function cleanupAttachments(
|
|
||||||
table: Table,
|
|
||||||
{
|
|
||||||
row,
|
|
||||||
rows,
|
|
||||||
oldRow,
|
|
||||||
oldTable,
|
|
||||||
}: { row?: Row; rows?: Row[]; oldRow?: Row; oldTable?: Table }
|
|
||||||
): Promise<any> {
|
|
||||||
const appId = context.getAppId()
|
|
||||||
if (!dbCore.isProdAppID(appId)) {
|
|
||||||
const prodAppId = dbCore.getProdAppID(appId!)
|
|
||||||
// if prod exists, then don't allow deleting
|
|
||||||
const exists = await dbCore.dbExists(prodAppId)
|
|
||||||
if (exists) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let files: string[] = []
|
|
||||||
function addFiles(row: Row, key: string) {
|
|
||||||
if (row[key]) {
|
|
||||||
files = files.concat(row[key].map((attachment: any) => attachment.key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const schemaToUse = oldTable ? oldTable.schema : table.schema
|
|
||||||
for (let [key, schema] of Object.entries(schemaToUse)) {
|
|
||||||
if (schema.type !== FieldTypes.ATTACHMENT) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// old table had this column, new table doesn't - delete it
|
|
||||||
if (rows && oldTable && !table.schema[key]) {
|
|
||||||
rows.forEach(row => addFiles(row, key))
|
|
||||||
} else if (oldRow && row) {
|
|
||||||
// if updating, need to manage the differences
|
|
||||||
files = files.concat(getRemovedAttachmentKeys(oldRow, row, key))
|
|
||||||
} else if (row) {
|
|
||||||
addFiles(row, key)
|
|
||||||
} else if (rows) {
|
|
||||||
rows.forEach(row => addFiles(row, key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (files.length > 0) {
|
|
||||||
await objectStore.deleteFiles(ObjectStoreBuckets.APPS, files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { AttachmentCleanup } from "../attachments"
|
||||||
|
import { FieldType, Table, Row, TableSourceType } from "@budibase/types"
|
||||||
|
import { DEFAULT_BB_DATASOURCE_ID } from "../../../constants"
|
||||||
|
import { objectStore } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
const BUCKET = "prod-budi-app-assets"
|
||||||
|
const FILE_NAME = "file/thing.jpg"
|
||||||
|
|
||||||
|
jest.mock("@budibase/backend-core", () => {
|
||||||
|
const actual = jest.requireActual("@budibase/backend-core")
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
objectStore: {
|
||||||
|
deleteFiles: jest.fn(),
|
||||||
|
ObjectStoreBuckets: actual.objectStore.ObjectStoreBuckets,
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
isProdAppID: () => jest.fn(() => false),
|
||||||
|
dbExists: () => jest.fn(() => false),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockedDeleteFiles = objectStore.deleteFiles as jest.MockedFunction<
|
||||||
|
typeof objectStore.deleteFiles
|
||||||
|
>
|
||||||
|
|
||||||
|
function table(): Table {
|
||||||
|
return {
|
||||||
|
name: "table",
|
||||||
|
sourceId: DEFAULT_BB_DATASOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
attach: {
|
||||||
|
name: "attach",
|
||||||
|
type: FieldType.ATTACHMENT,
|
||||||
|
constraints: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function row(fileKey: string = FILE_NAME): Row {
|
||||||
|
return {
|
||||||
|
attach: [
|
||||||
|
{
|
||||||
|
size: 1,
|
||||||
|
extension: "jpg",
|
||||||
|
key: fileKey,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("attachment cleanup", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedDeleteFiles.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to cleanup a table update", async () => {
|
||||||
|
const originalTable = table()
|
||||||
|
delete originalTable.schema["attach"]
|
||||||
|
await AttachmentCleanup.tableUpdate(originalTable, [row()], {
|
||||||
|
oldTable: table(),
|
||||||
|
})
|
||||||
|
expect(mockedDeleteFiles).toBeCalledWith(BUCKET, [FILE_NAME])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to cleanup a table deletion", async () => {
|
||||||
|
await AttachmentCleanup.tableDelete(table(), [row()])
|
||||||
|
expect(mockedDeleteFiles).toBeCalledWith(BUCKET, [FILE_NAME])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle table column renaming", async () => {
|
||||||
|
const updatedTable = table()
|
||||||
|
updatedTable.schema.attach2 = updatedTable.schema.attach
|
||||||
|
delete updatedTable.schema.attach
|
||||||
|
await AttachmentCleanup.tableUpdate(updatedTable, [row()], {
|
||||||
|
oldTable: table(),
|
||||||
|
rename: { old: "attach", updated: "attach2" },
|
||||||
|
})
|
||||||
|
expect(mockedDeleteFiles).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't cleanup if no table changes", async () => {
|
||||||
|
await AttachmentCleanup.tableUpdate(table(), [row()], { oldTable: table() })
|
||||||
|
expect(mockedDeleteFiles).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle row updates", async () => {
|
||||||
|
const updatedRow = row()
|
||||||
|
delete updatedRow.attach
|
||||||
|
await AttachmentCleanup.rowUpdate(table(), {
|
||||||
|
row: updatedRow,
|
||||||
|
oldRow: row(),
|
||||||
|
})
|
||||||
|
expect(mockedDeleteFiles).toBeCalledWith(BUCKET, [FILE_NAME])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle row deletion", async () => {
|
||||||
|
await AttachmentCleanup.rowDelete(table(), [row()])
|
||||||
|
expect(mockedDeleteFiles).toBeCalledWith(BUCKET, [FILE_NAME])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't cleanup attachments if row not updated", async () => {
|
||||||
|
await AttachmentCleanup.rowUpdate(table(), { row: row(), oldRow: row() })
|
||||||
|
expect(mockedDeleteFiles).not.toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -3,6 +3,7 @@ import {
|
||||||
FieldType,
|
FieldType,
|
||||||
FieldTypeSubtypes,
|
FieldTypeSubtypes,
|
||||||
INTERNAL_TABLE_SOURCE_ID,
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
RowAttachment,
|
||||||
Table,
|
Table,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -70,6 +71,49 @@ describe("rowProcessor - outputProcessing", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should handle attachments correctly", async () => {
|
||||||
|
const table: Table = {
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
schema: {
|
||||||
|
attach: {
|
||||||
|
type: FieldType.ATTACHMENT,
|
||||||
|
name: "attach",
|
||||||
|
constraints: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const row: { attach: RowAttachment[] } = {
|
||||||
|
attach: [
|
||||||
|
{
|
||||||
|
size: 10,
|
||||||
|
name: "test",
|
||||||
|
extension: "jpg",
|
||||||
|
key: "test.jpg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = await outputProcessing(table, row, { squash: false })
|
||||||
|
expect(output.attach[0].url).toBe(
|
||||||
|
"/files/signed/prod-budi-app-assets/test.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
row.attach[0].url = ""
|
||||||
|
const output2 = await outputProcessing(table, row, { squash: false })
|
||||||
|
expect(output2.attach[0].url).toBe(
|
||||||
|
"/files/signed/prod-budi-app-assets/test.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
row.attach[0].url = "aaaa"
|
||||||
|
const output3 = await outputProcessing(table, row, { squash: false })
|
||||||
|
expect(output3.attach[0].url).toBe("aaaa")
|
||||||
|
})
|
||||||
|
|
||||||
it("process output even when the field is not empty", async () => {
|
it("process output even when the field is not empty", async () => {
|
||||||
const table: Table = {
|
const table: Table = {
|
||||||
_id: generator.guid(),
|
_id: generator.guid(),
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export interface DatasourceAuthCookie {
|
||||||
|
appId: string
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionCookie {
|
||||||
|
sessionId: string
|
||||||
|
userId: string
|
||||||
|
}
|
|
@ -9,3 +9,4 @@ export * from "./app"
|
||||||
export * from "./global"
|
export * from "./global"
|
||||||
export * from "./pagination"
|
export * from "./pagination"
|
||||||
export * from "./searchFilter"
|
export * from "./searchFilter"
|
||||||
|
export * from "./cookies"
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
PasswordResetUpdateRequest,
|
PasswordResetUpdateRequest,
|
||||||
GoogleInnerConfig,
|
GoogleInnerConfig,
|
||||||
|
DatasourceAuthCookie,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
|
|
||||||
|
@ -148,7 +149,13 @@ export const datasourcePreAuth = async (ctx: any, next: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const datasourceAuth = async (ctx: any, next: any) => {
|
export const datasourceAuth = async (ctx: any, next: any) => {
|
||||||
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
|
const authStateCookie = getCookie<DatasourceAuthCookie>(
|
||||||
|
ctx,
|
||||||
|
Cookie.DatasourceAuth
|
||||||
|
)
|
||||||
|
if (!authStateCookie) {
|
||||||
|
throw new Error("Unable to retrieve datasource authentication cookie")
|
||||||
|
}
|
||||||
const provider = authStateCookie.provider
|
const provider = authStateCookie.provider
|
||||||
const { middleware } = require(`@budibase/backend-core`)
|
const { middleware } = require(`@budibase/backend-core`)
|
||||||
const handler = middleware.datasource[provider]
|
const handler = middleware.datasource[provider]
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
ConfigType,
|
ConfigType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import API from "./api"
|
import API from "./api"
|
||||||
|
import jwt, { Secret } from "jsonwebtoken"
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
server: any
|
server: any
|
||||||
|
@ -209,7 +210,7 @@ class TestConfiguration {
|
||||||
sessionId: "sessionid",
|
sessionId: "sessionid",
|
||||||
tenantId: user.tenantId,
|
tenantId: user.tenantId,
|
||||||
}
|
}
|
||||||
const authCookie = auth.jwt.sign(authToken, coreEnv.JWT_SECRET)
|
const authCookie = jwt.sign(authToken, coreEnv.JWT_SECRET as Secret)
|
||||||
return {
|
return {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
...this.cookieHeader([`${constants.Cookie.Auth}=${authCookie}`]),
|
...this.cookieHeader([`${constants.Cookie.Auth}=${authCookie}`]),
|
||||||
|
@ -327,7 +328,7 @@ class TestConfiguration {
|
||||||
// CONFIGS - OIDC
|
// CONFIGS - OIDC
|
||||||
|
|
||||||
getOIDConfigCookie(configId: string) {
|
getOIDConfigCookie(configId: string) {
|
||||||
const token = auth.jwt.sign(configId, coreEnv.JWT_SECRET)
|
const token = jwt.sign(configId, coreEnv.JWT_SECRET as Secret)
|
||||||
return this.cookieHeader([[`${constants.Cookie.OIDC_CONFIG}=${token}`]])
|
return this.cookieHeader([[`${constants.Cookie.OIDC_CONFIG}=${token}`]])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue