@@ -258,8 +260,6 @@
bind:this={iframe}
src="/app/preview"
class:hidden={loading || error}
- class:tablet={$previewStore.previewDevice === "tablet"}
- class:mobile={$previewStore.previewDevice === "mobile"}
/>
diff --git a/packages/frontend-core/src/components/index.js b/packages/frontend-core/src/components/index.js
index f724e1e4d9..f71420b12b 100644
--- a/packages/frontend-core/src/components/index.js
+++ b/packages/frontend-core/src/components/index.js
@@ -5,3 +5,4 @@ export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte"
export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid"
+export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
diff --git a/packages/frontend-core/src/themes/midnight.css b/packages/frontend-core/src/themes/midnight.css
index e311452262..cf6a4fbd13 100644
--- a/packages/frontend-core/src/themes/midnight.css
+++ b/packages/frontend-core/src/themes/midnight.css
@@ -17,5 +17,8 @@
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
+
+ --spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
}
diff --git a/packages/frontend-core/src/themes/nord.css b/packages/frontend-core/src/themes/nord.css
index d47dbe8aa8..bc142db0fd 100644
--- a/packages/frontend-core/src/themes/nord.css
+++ b/packages/frontend-core/src/themes/nord.css
@@ -50,4 +50,7 @@
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.15) !important;
--spectrum-global-color-blue-100: rgb(56, 65, 84) !important;
+
+ --spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
}
diff --git a/packages/server/package.json b/packages/server/package.json
index 771f73ac13..02db1138b5 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -52,6 +52,7 @@
"@budibase/pro": "0.0.0",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
+ "@budibase/frontend-core": "0.0.0",
"@budibase/types": "0.0.0",
"@bull-board/api": "5.10.2",
"@bull-board/koa": "5.10.2",
diff --git a/packages/server/scripts/integrations/postgres/reset.sh b/packages/server/scripts/integrations/postgres/reset.sh
index 32778bd11f..8deb01cdf8 100755
--- a/packages/server/scripts/integrations/postgres/reset.sh
+++ b/packages/server/scripts/integrations/postgres/reset.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-docker-compose down
+docker-compose down -v
docker volume prune -f
diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh
index 9efef05526..3ecf8bb794 100644
--- a/packages/server/scripts/test.sh
+++ b/packages/server/scripts/test.sh
@@ -3,12 +3,12 @@ set -e
if [[ -n $CI ]]
then
- export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot"
+ export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS"
echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@"
jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@
else
# --maxWorkers performs better in development
- export NODE_OPTIONS="--no-node-snapshot"
+ export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS"
echo "jest --coverage --maxWorkers=2 --forceExit $@"
jest --coverage --maxWorkers=2 --forceExit $@
fi
\ No newline at end of file
diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts
index 33582cf656..0bc93888ae 100644
--- a/packages/server/src/api/controllers/application.ts
+++ b/packages/server/src/api/controllers/application.ts
@@ -47,6 +47,9 @@ import {
PlanType,
Screen,
UserCtx,
+ CreateAppRequest,
+ FetchAppDefinitionResponse,
+ FetchAppPackageResponse,
} from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk"
@@ -58,23 +61,23 @@ import * as appMigrations from "../../appMigrations"
async function getLayouts() {
const db = context.getAppDB()
return (
- await db.allDocs(
+ await db.allDocs
(
getLayoutParams(null, {
include_docs: true,
})
)
- ).rows.map((row: any) => row.doc)
+ ).rows.map(row => row.doc!)
}
async function getScreens() {
const db = context.getAppDB()
return (
- await db.allDocs(
+ await db.allDocs(
getScreenParams(null, {
include_docs: true,
})
)
- ).rows.map((row: any) => row.doc)
+ ).rows.map(row => row.doc!)
}
function getUserRoleId(ctx: UserCtx) {
@@ -116,8 +119,8 @@ function checkAppName(
}
interface AppTemplate {
- templateString: string
- useTemplate: string
+ templateString?: string
+ useTemplate?: string
file?: {
type: string
path: string
@@ -174,14 +177,16 @@ export const addSampleData = async (ctx: UserCtx) => {
ctx.status = 200
}
-export async function fetch(ctx: UserCtx) {
+export async function fetch(ctx: UserCtx) {
ctx.body = await sdk.applications.fetch(
ctx.query.status as AppStatus,
ctx.user
)
}
-export async function fetchAppDefinition(ctx: UserCtx) {
+export async function fetchAppDefinition(
+ ctx: UserCtx
+) {
const layouts = await getLayouts()
const userRoleId = getUserRoleId(ctx)
const accessController = new roles.AccessController()
@@ -196,10 +201,12 @@ export async function fetchAppDefinition(ctx: UserCtx) {
}
}
-export async function fetchAppPackage(ctx: UserCtx) {
+export async function fetchAppPackage(
+ ctx: UserCtx
+) {
const db = context.getAppDB()
const appId = context.getAppId()
- let application = await db.get(DocumentType.APP_METADATA)
+ let application = await db.get(DocumentType.APP_METADATA)
const layouts = await getLayouts()
let screens = await getScreens()
const license = await licensing.cache.getCachedLicense()
@@ -231,17 +238,21 @@ export async function fetchAppPackage(ctx: UserCtx) {
}
}
-async function performAppCreate(ctx: UserCtx) {
+async function performAppCreate(ctx: UserCtx) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
- const name = ctx.request.body.name,
- possibleUrl = ctx.request.body.url,
- encryptionPassword = ctx.request.body.encryptionPassword
+ const {
+ name,
+ url,
+ encryptionPassword,
+ useTemplate,
+ templateKey,
+ templateString,
+ } = ctx.request.body
checkAppName(ctx, apps, name)
- const url = sdk.applications.getAppUrl({ name, url: possibleUrl })
- checkAppUrl(ctx, apps, url)
+ const appUrl = sdk.applications.getAppUrl({ name, url })
+ checkAppUrl(ctx, apps, appUrl)
- const { useTemplate, templateKey, templateString } = ctx.request.body
const instanceConfig: AppTemplate = {
useTemplate,
key: templateKey,
@@ -268,7 +279,7 @@ async function performAppCreate(ctx: UserCtx) {
version: envCore.VERSION,
componentLibraries: ["@budibase/standard-components"],
name: name,
- url: url,
+ url: appUrl,
template: templateKey,
instance,
tenantId: tenancy.getTenantId(),
@@ -420,7 +431,9 @@ export async function create(ctx: UserCtx) {
// This endpoint currently operates as a PATCH rather than a PUT
// Thus name and url fields are handled only if present
-export async function update(ctx: UserCtx) {
+export async function update(
+ ctx: UserCtx<{ name?: string; url?: string }, App>
+) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
// validation
const name = ctx.request.body.name,
@@ -493,7 +506,7 @@ export async function revertClient(ctx: UserCtx) {
const revertedToVersion = application.revertableVersion
const appPackageUpdates = {
version: revertedToVersion,
- revertableVersion: null,
+ revertableVersion: undefined,
}
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
await events.app.versionReverted(app, currentVersion, revertedToVersion)
@@ -613,12 +626,15 @@ export async function importToApp(ctx: UserCtx) {
ctx.body = { message: "app updated" }
}
-export async function updateAppPackage(appPackage: any, appId: any) {
+export async function updateAppPackage(
+ appPackage: Partial,
+ appId: string
+) {
return context.doInAppContext(appId, async () => {
const db = context.getAppDB()
const application = await db.get(DocumentType.APP_METADATA)
- const newAppPackage = { ...application, ...appPackage }
+ const newAppPackage: App = { ...application, ...appPackage }
if (appPackage._rev !== application._rev) {
newAppPackage._rev = application._rev
}
diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts
index 6bb886f0e5..b83132702b 100644
--- a/packages/server/src/api/controllers/row/ExternalRequest.ts
+++ b/packages/server/src/api/controllers/row/ExternalRequest.ts
@@ -11,12 +11,12 @@ import {
PaginationJson,
RelationshipFieldMetadata,
RelationshipsJson,
- RelationshipType,
Row,
SearchFilters,
SortJson,
SortType,
Table,
+ isManyToOne,
} from "@budibase/types"
import {
breakExternalTableId,
@@ -24,6 +24,7 @@ import {
convertRowId,
isRowId,
isSQL,
+ generateRowIdField,
} from "../../../integrations/utils"
import {
buildExternalRelationships,
@@ -33,13 +34,16 @@ import {
updateRelationshipColumns,
fixArrayTypes,
isManyToMany,
+ processRelationshipFields,
} from "./utils"
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp"
import { db as dbCore } from "@budibase/backend-core"
-import { processDates, processFormulas } from "../../../utilities/rowProcessor"
+import { processDates } from "../../../utilities/rowProcessor"
+import AliasTables from "./alias"
import sdk from "../../../sdk"
+import env from "../../../environment"
export interface ManyRelationship {
tableId?: string
@@ -108,6 +112,39 @@ function buildFilters(
}
}
+async function removeManyToManyRelationships(
+ rowId: string,
+ table: Table,
+ colName: string
+) {
+ const tableId = table._id!
+ const filters = buildFilters(rowId, {}, table)
+ // safety check, if there are no filters on deletion bad things happen
+ if (Object.keys(filters).length !== 0) {
+ return getDatasourceAndQuery({
+ endpoint: getEndpoint(tableId, Operation.DELETE),
+ body: { [colName]: null },
+ filters,
+ })
+ } else {
+ return []
+ }
+}
+
+async function removeOneToManyRelationships(rowId: string, table: Table) {
+ const tableId = table._id!
+ const filters = buildFilters(rowId, {}, table)
+ // safety check, if there are no filters on deletion bad things happen
+ if (Object.keys(filters).length !== 0) {
+ return getDatasourceAndQuery({
+ endpoint: getEndpoint(tableId, Operation.UPDATE),
+ filters,
+ })
+ } else {
+ return []
+ }
+}
+
/**
* This function checks the incoming parameters to make sure all the inputs are
* valid based on on the table schema. The main thing this is looking for is when a
@@ -158,13 +195,13 @@ function cleanupConfig(config: RunConfig, table: Table): RunConfig {
function getEndpoint(tableId: string | undefined, operation: string) {
if (!tableId) {
- return {}
+ throw new Error("Cannot get endpoint information - no table ID specified")
}
const { datasourceId, tableName } = breakExternalTableId(tableId)
return {
- datasourceId,
- entityId: tableName,
- operation,
+ datasourceId: datasourceId!,
+ entityId: tableName!,
+ operation: operation as Operation,
}
}
@@ -264,6 +301,18 @@ export class ExternalRequest {
}
}
+ async getRow(table: Table, rowId: string): Promise {
+ const response = await getDatasourceAndQuery({
+ endpoint: getEndpoint(table._id!, Operation.READ),
+ filters: buildFilters(rowId, {}, table),
+ })
+ if (Array.isArray(response) && response.length > 0) {
+ return response[0]
+ } else {
+ throw new Error(`Cannot fetch row by ID "${rowId}"`)
+ }
+ }
+
inputProcessing(row: Row | undefined, table: Table) {
if (!row) {
return { row, manyRelationships: [] }
@@ -348,33 +397,6 @@ export class ExternalRequest {
return { row: newRow, manyRelationships }
}
- processRelationshipFields(
- table: Table,
- row: Row,
- relationships: RelationshipsJson[]
- ): Row {
- for (let relationship of relationships) {
- const linkedTable = this.tables[relationship.tableName]
- if (!linkedTable || !row[relationship.column]) {
- continue
- }
- for (let key of Object.keys(row[relationship.column])) {
- let relatedRow: Row = row[relationship.column][key]
- // add this row as context for the relationship
- for (let col of Object.values(linkedTable.schema)) {
- if (col.type === FieldType.LINK && col.tableId === table._id) {
- relatedRow[col.name] = [row]
- }
- }
- // process additional types
- relatedRow = processDates(table, relatedRow)
- relatedRow = processFormulas(linkedTable, relatedRow)
- row[relationship.column][key] = relatedRow
- }
- }
- return row
- }
-
outputProcessing(
rows: Row[] = [],
table: Table,
@@ -419,7 +441,7 @@ export class ExternalRequest {
// make sure all related rows are correct
let finalRowArray = Object.values(finalRows).map(row =>
- this.processRelationshipFields(table, row, relationships)
+ processRelationshipFields(table, this.tables, row, relationships)
)
// process some additional types
@@ -432,7 +454,9 @@ export class ExternalRequest {
* information.
*/
async lookupRelations(tableId: string, row: Row) {
- const related: { [key: string]: any } = {}
+ const related: {
+ [key: string]: { rows: Row[]; isMany: boolean; tableId: string }
+ } = {}
const { tableName } = breakExternalTableId(tableId)
if (!tableName) {
return related
@@ -450,14 +474,26 @@ export class ExternalRequest {
) {
continue
}
- const isMany = field.relationshipType === RelationshipType.MANY_TO_MANY
- const tableId = isMany ? field.through : field.tableId
+ let tableId: string | undefined,
+ lookupField: string | undefined,
+ fieldName: string | undefined
+ if (isManyToMany(field)) {
+ tableId = field.through
+ lookupField = primaryKey
+ fieldName = field.throughTo || primaryKey
+ } else if (isManyToOne(field)) {
+ tableId = field.tableId
+ lookupField = field.foreignKey
+ fieldName = field.fieldName
+ }
+ if (!tableId || !lookupField || !fieldName) {
+ throw new Error(
+ "Unable to lookup relationships - undefined column properties."
+ )
+ }
const { tableName: relatedTableName } = breakExternalTableId(tableId)
// @ts-ignore
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
-
- const lookupField = isMany ? primaryKey : field.foreignKey
- const fieldName = isMany ? field.throughTo || primaryKey : field.fieldName
if (!lookupField || !row[lookupField]) {
continue
}
@@ -470,9 +506,12 @@ export class ExternalRequest {
},
})
// this is the response from knex if no rows found
- const rows = !response[0].read ? response : []
- const storeTo = isMany ? field.throughFrom || linkPrimaryKey : fieldName
- related[storeTo] = { rows, isMany, tableId }
+ const rows: Row[] =
+ !Array.isArray(response) || response?.[0].read ? [] : response
+ const storeTo = isManyToMany(field)
+ ? field.throughFrom || linkPrimaryKey
+ : fieldName
+ related[storeTo] = { rows, isMany: isManyToMany(field), tableId }
}
return related
}
@@ -558,24 +597,43 @@ export class ExternalRequest {
continue
}
for (let row of rows) {
- const filters = buildFilters(generateIdForRow(row, table), {}, table)
- // safety check, if there are no filters on deletion bad things happen
- if (Object.keys(filters).length !== 0) {
- const op = isMany ? Operation.DELETE : Operation.UPDATE
- const body = isMany ? null : { [colName]: null }
- promises.push(
- getDatasourceAndQuery({
- endpoint: getEndpoint(tableId, op),
- body,
- filters,
- })
- )
+ const rowId = generateIdForRow(row, table)
+ const promise: Promise = isMany
+ ? removeManyToManyRelationships(rowId, table, colName)
+ : removeOneToManyRelationships(rowId, table)
+ if (promise) {
+ promises.push(promise)
}
}
}
await Promise.all(promises)
}
+ async removeRelationshipsToRow(table: Table, rowId: string) {
+ const row = await this.getRow(table, rowId)
+ const related = await this.lookupRelations(table._id!, row)
+ for (let column of Object.values(table.schema)) {
+ const relationshipColumn = column as RelationshipFieldMetadata
+ if (!isManyToOne(relationshipColumn)) {
+ continue
+ }
+ const { rows, isMany, tableId } = related[relationshipColumn.fieldName]
+ const table = this.getTable(tableId)!
+ await Promise.all(
+ rows.map(row => {
+ const rowId = generateIdForRow(row, table)
+ return isMany
+ ? removeManyToManyRelationships(
+ rowId,
+ table,
+ relationshipColumn.fieldName
+ )
+ : removeOneToManyRelationships(rowId, table)
+ })
+ )
+ }
+ }
+
async run(config: RunConfig): Promise> {
const { operation, tableId } = this
let { datasourceId, tableName } = breakExternalTableId(tableId)
@@ -632,7 +690,7 @@ export class ExternalRequest {
}
let json = {
endpoint: {
- datasourceId,
+ datasourceId: datasourceId!,
entityId: tableName,
operation,
},
@@ -658,13 +716,26 @@ export class ExternalRequest {
},
}
- // can't really use response right now
- const response = await getDatasourceAndQuery(json)
- // handle many to many relationships now if we know the ID (could be auto increment)
+ // remove any relationships that could block deletion
+ if (operation === Operation.DELETE && id) {
+ await this.removeRelationshipsToRow(table, generateRowIdField(id))
+ }
+
+ // aliasing can be disabled fully if desired
+ let response
+ if (env.SQL_ALIASING_DISABLE) {
+ response = await getDatasourceAndQuery(json)
+ } else {
+ const aliasing = new AliasTables(Object.keys(this.tables))
+ response = await aliasing.queryWithAliasing(json)
+ }
+
+ const responseRows = Array.isArray(response) ? response : []
+ // handle many-to-many relationships now if we know the ID (could be auto increment)
if (operation !== Operation.READ) {
await this.handleManyRelationships(
table._id || "",
- response[0],
+ responseRows[0],
processed.manyRelationships
)
}
diff --git a/packages/server/src/api/controllers/row/alias.ts b/packages/server/src/api/controllers/row/alias.ts
new file mode 100644
index 0000000000..7c5db51792
--- /dev/null
+++ b/packages/server/src/api/controllers/row/alias.ts
@@ -0,0 +1,168 @@
+import {
+ QueryJson,
+ SearchFilters,
+ Table,
+ Row,
+ DatasourcePlusQueryResponse,
+} from "@budibase/types"
+import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
+import { cloneDeep } from "lodash"
+
+class CharSequence {
+ static alphabet = "abcdefghijklmnopqrstuvwxyz"
+ counters: number[]
+
+ constructor() {
+ this.counters = [0]
+ }
+
+ getCharacter(): string {
+ const char = this.counters.map(i => CharSequence.alphabet[i]).join("")
+ for (let i = this.counters.length - 1; i >= 0; i--) {
+ if (this.counters[i] < CharSequence.alphabet.length - 1) {
+ this.counters[i]++
+ return char
+ }
+ this.counters[i] = 0
+ }
+ this.counters.unshift(0)
+ return char
+ }
+}
+
+export default class AliasTables {
+ aliases: Record
+ tableAliases: Record
+ tableNames: string[]
+ charSeq: CharSequence
+
+ constructor(tableNames: string[]) {
+ this.tableNames = tableNames
+ this.aliases = {}
+ this.tableAliases = {}
+ this.charSeq = new CharSequence()
+ }
+
+ getAlias(tableName: string) {
+ if (this.aliases[tableName]) {
+ return this.aliases[tableName]
+ }
+ const char = this.charSeq.getCharacter()
+ this.aliases[tableName] = char
+ this.tableAliases[char] = tableName
+ return char
+ }
+
+ aliasField(field: string) {
+ const tableNames = this.tableNames
+ if (field.includes(".")) {
+ const [tableName, column] = field.split(".")
+ const foundTableName = tableNames.find(name => {
+ const idx = tableName.indexOf(name)
+ if (idx === -1 || idx > 1) {
+ return
+ }
+ return Math.abs(tableName.length - name.length) <= 2
+ })
+ if (foundTableName) {
+ const aliasedTableName = tableName.replace(
+ foundTableName,
+ this.getAlias(foundTableName)
+ )
+ field = `${aliasedTableName}.${column}`
+ }
+ }
+ return field
+ }
+
+ reverse(rows: T): T {
+ const process = (row: Row) => {
+ const final: Row = {}
+ for (let [key, value] of Object.entries(row)) {
+ if (!key.includes(".")) {
+ final[key] = value
+ } else {
+ const [alias, column] = key.split(".")
+ const tableName = this.tableAliases[alias] || alias
+ final[`${tableName}.${column}`] = value
+ }
+ }
+ return final
+ }
+ if (Array.isArray(rows)) {
+ return rows.map(row => process(row)) as T
+ } else {
+ return process(rows) as T
+ }
+ }
+
+ aliasMap(tableNames: (string | undefined)[]) {
+ const map: Record = {}
+ for (let tableName of tableNames) {
+ if (tableName) {
+ map[tableName] = this.getAlias(tableName)
+ }
+ }
+ return map
+ }
+
+ async queryWithAliasing(
+ json: QueryJson
+ ): Promise {
+ json = cloneDeep(json)
+ const aliasTable = (table: Table) => ({
+ ...table,
+ name: this.getAlias(table.name),
+ })
+ // run through the query json to update anywhere a table may be used
+ if (json.resource?.fields) {
+ json.resource.fields = json.resource.fields.map(field =>
+ this.aliasField(field)
+ )
+ }
+ if (json.filters) {
+ for (let [filterKey, filter] of Object.entries(json.filters)) {
+ if (typeof filter !== "object") {
+ continue
+ }
+ const aliasedFilters: typeof filter = {}
+ for (let key of Object.keys(filter)) {
+ aliasedFilters[this.aliasField(key)] = filter[key]
+ }
+ json.filters[filterKey as keyof SearchFilters] = aliasedFilters
+ }
+ }
+ if (json.relationships) {
+ json.relationships = json.relationships.map(relationship => ({
+ ...relationship,
+ aliases: this.aliasMap([
+ relationship.through,
+ relationship.tableName,
+ json.endpoint.entityId,
+ ]),
+ }))
+ }
+ if (json.meta?.table) {
+ json.meta.table = aliasTable(json.meta.table)
+ }
+ if (json.meta?.tables) {
+ const aliasedTables: Record = {}
+ for (let [tableName, table] of Object.entries(json.meta.tables)) {
+ aliasedTables[this.getAlias(tableName)] = aliasTable(table)
+ }
+ json.meta.tables = aliasedTables
+ }
+ // invert and return
+ const invertedTableAliases: Record = {}
+ for (let [key, value] of Object.entries(this.tableAliases)) {
+ invertedTableAliases[value] = key
+ }
+ json.tableAliases = invertedTableAliases
+ const response = await getDatasourceAndQuery(json)
+ if (Array.isArray(response)) {
+ return this.reverse(response)
+ } else {
+ return response
+ }
+ }
+}
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 1ad8a2a695..ec56919d12 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -223,7 +223,8 @@ export const exportRows = async (
const format = ctx.query.format
- const { rows, columns, query, sort, sortOrder } = ctx.request.body
+ const { rows, columns, query, sort, sortOrder, delimiter, customHeaders } =
+ ctx.request.body
if (typeof format !== "string" || !exporters.isFormat(format)) {
ctx.throw(
400,
@@ -241,6 +242,8 @@ export const exportRows = async (
query,
sort,
sortOrder,
+ delimiter,
+ customHeaders,
})
ctx.attachment(fileName)
ctx.body = apiFileReturn(content)
diff --git a/packages/server/src/api/controllers/row/utils/utils.ts b/packages/server/src/api/controllers/row/utils/utils.ts
index eb87dc8c93..8197fba0f6 100644
--- a/packages/server/src/api/controllers/row/utils/utils.ts
+++ b/packages/server/src/api/controllers/row/utils/utils.ts
@@ -1,7 +1,15 @@
import { InternalTables } from "../../../../db/utils"
import * as userController from "../../user"
import { context } from "@budibase/backend-core"
-import { Ctx, RelationshipsJson, Row, Table, UserCtx } from "@budibase/types"
+import {
+ Ctx,
+ DatasourcePlusQueryResponse,
+ FieldType,
+ RelationshipsJson,
+ Row,
+ Table,
+ UserCtx,
+} from "@budibase/types"
import {
processDates,
processFormulas,
@@ -25,6 +33,34 @@ validateJs.extend(validateJs.validators.datetime, {
},
})
+export function processRelationshipFields(
+ table: Table,
+ tables: Record,
+ row: Row,
+ relationships: RelationshipsJson[]
+): Row {
+ for (let relationship of relationships) {
+ const linkedTable = tables[relationship.tableName]
+ if (!linkedTable || !row[relationship.column]) {
+ continue
+ }
+ for (let key of Object.keys(row[relationship.column])) {
+ let relatedRow: Row = row[relationship.column][key]
+ // add this row as context for the relationship
+ for (let col of Object.values(linkedTable.schema)) {
+ if (col.type === FieldType.LINK && col.tableId === table._id) {
+ relatedRow[col.name] = [row]
+ }
+ }
+ // process additional types
+ relatedRow = processDates(table, relatedRow)
+ relatedRow = processFormulas(linkedTable, relatedRow)
+ row[relationship.column][key] = relatedRow
+ }
+ }
+ return row
+}
+
export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
const db = context.getAppDB()
let row: Row
@@ -80,17 +116,17 @@ export async function validate(
}
export function sqlOutputProcessing(
- rows: Row[] = [],
+ rows: DatasourcePlusQueryResponse,
table: Table,
tables: Record,
relationships: RelationshipsJson[],
opts?: { internal?: boolean }
) {
- if (!rows || rows.length === 0 || rows[0].read === true) {
+ if (!Array.isArray(rows) || rows.length === 0 || rows[0].read === true) {
return []
}
let finalRows: { [key: string]: Row } = {}
- for (let row of rows) {
+ for (let row of rows as Row[]) {
let rowId = row._id
if (!rowId) {
rowId = generateIdForRow(row, table)
@@ -103,7 +139,8 @@ export function sqlOutputProcessing(
tables,
row,
finalRows,
- relationships
+ relationships,
+ opts
)
continue
}
@@ -126,19 +163,18 @@ export function sqlOutputProcessing(
tables,
row,
finalRows,
- relationships,
- opts
+ relationships
)
}
- // Process some additional data types
- let finalRowArray = Object.values(finalRows)
- finalRowArray = processDates(table, finalRowArray)
- finalRowArray = processFormulas(table, finalRowArray) as Row[]
-
- return finalRowArray.map((row: Row) =>
- squashRelationshipColumns(table, tables, row, relationships)
+ // make sure all related rows are correct
+ let finalRowArray = Object.values(finalRows).map(row =>
+ processRelationshipFields(table, tables, row, relationships)
)
+
+ // process some additional types
+ finalRowArray = processDates(table, finalRowArray)
+ return finalRowArray
}
export function isUserMetadataTable(tableId: string) {
diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts
index bf93893c8d..06c731f1a6 100644
--- a/packages/server/src/api/controllers/static/index.ts
+++ b/packages/server/src/api/controllers/static/index.ts
@@ -1,7 +1,5 @@
import { InvalidFileExtensions } from "@budibase/shared-core"
-
import AppComponent from "./templates/BudibaseApp.svelte"
-
import { join } from "../../../utilities/centralPath"
import * as uuid from "uuid"
import { ObjectStoreBuckets } from "../../../constants"
@@ -24,10 +22,12 @@ import fs from "fs"
import sdk from "../../../sdk"
import * as pro from "@budibase/pro"
import {
+ DocumentType,
+ UserCtx,
App,
Ctx,
- DocumentType,
ProcessAttachmentResponse,
+ Feature,
} from "@budibase/types"
import {
getAppMigrationVersion,
@@ -36,6 +36,61 @@ import {
import send from "koa-send"
+const getThemeVariables = (theme: string) => {
+ if (theme === "spectrum--lightest") {
+ return `
+ --spectrum-global-color-gray-50: rgb(255, 255, 255);
+ --spectrum-global-color-gray-200: rgb(244, 244, 244);
+ --spectrum-global-color-gray-300: rgb(234, 234, 234);
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
+ `
+ }
+ if (theme === "spectrum--light") {
+ return `
+ --spectrum-global-color-gray-50: rgb(255, 255, 255);
+ --spectrum-global-color-gray-200: rgb(234, 234, 234);
+ --spectrum-global-color-gray-300: rgb(225, 225, 225);
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
+
+ `
+ }
+ if (theme === "spectrum--dark") {
+ return `
+ --spectrum-global-color-gray-100: rgb(50, 50, 50);
+ --spectrum-global-color-gray-200: rgb(62, 62, 62);
+ --spectrum-global-color-gray-300: rgb(74, 74, 74);
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
+ `
+ }
+ if (theme === "spectrum--darkest") {
+ return `
+ --spectrum-global-color-gray-100: rgb(30, 30, 30);
+ --spectrum-global-color-gray-200: rgb(44, 44, 44);
+ --spectrum-global-color-gray-300: rgb(57, 57, 57);
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
+ `
+ }
+ if (theme === "spectrum--nord") {
+ return `
+ --spectrum-global-color-gray-100: #3b4252;
+
+ --spectrum-global-color-gray-200: #424a5c;
+ --spectrum-global-color-gray-300: #4c566a;
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
+ `
+ }
+ if (theme === "spectrum--midnight") {
+ return `
+ --hue: 220;
+ --sat: 10%;
+ --spectrum-global-color-gray-100: hsl(var(--hue), var(--sat), 17%);
+ --spectrum-global-color-gray-200: hsl(var(--hue), var(--sat), 20%);
+ --spectrum-global-color-gray-300: hsl(var(--hue), var(--sat), 24%);
+ --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
+ `
+ }
+}
+
export const toggleBetaUiFeature = async function (ctx: Ctx) {
const cookieName = `beta:${ctx.params.feature}`
@@ -150,7 +205,7 @@ const requiresMigration = async (ctx: Ctx) => {
return requiresMigrations
}
-export const serveApp = async function (ctx: Ctx) {
+export const serveApp = async function (ctx: UserCtx) {
const needMigrations = await requiresMigration(ctx)
const bbHeaderEmbed =
@@ -171,9 +226,19 @@ export const serveApp = async function (ctx: Ctx) {
const appInfo = await db.get(DocumentType.APP_METADATA)
let appId = context.getAppId()
+ const hideDevTools = !!ctx.params.appUrl
+ const sideNav = appInfo.navigation.navigation === "Left"
+ const hideFooter =
+ ctx?.user?.license?.features?.includes(Feature.BRANDING) || false
+ const themeVariables = getThemeVariables(appInfo?.theme)
+
if (!env.isJest()) {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
+
const { head, html, css } = AppComponent.render({
+ hideDevTools,
+ sideNav,
+ hideFooter,
metaImage:
branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
@@ -198,7 +263,7 @@ export const serveApp = async function (ctx: Ctx) {
ctx.body = await processString(appHbs, {
head,
body: html,
- style: css.code,
+ css: `:root{${themeVariables}} ${css.code}`,
appId,
embedded: bbHeaderEmbed,
})
diff --git a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte
index 7819368fc0..63b293b4ca 100644
--- a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte
+++ b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte
@@ -1,4 +1,6 @@
@@ -96,6 +102,7 @@
+
{#if clientLibPath}
There was an error loading your app
diff --git a/packages/server/src/api/controllers/static/templates/app.hbs b/packages/server/src/api/controllers/static/templates/app.hbs
index 8c445158a0..b01b723c3e 100644
--- a/packages/server/src/api/controllers/static/templates/app.hbs
+++ b/packages/server/src/api/controllers/static/templates/app.hbs
@@ -1,8 +1,12 @@
-
+
{{{head}}}
-
+