diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
index 7d77942815..0ade6ea2ab 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
@@ -33,7 +33,7 @@
import { getBindings } from "components/backend/DataTable/formula"
import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core"
- import { FieldSubtype, FieldType } from "@budibase/types"
+ import { FieldType } from "@budibase/types"
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
const AUTO_TYPE = "auto"
@@ -43,11 +43,7 @@
const NUMBER_TYPE = FIELDS.NUMBER.type
const JSON_TYPE = FIELDS.JSON.type
const DATE_TYPE = FIELDS.DATETIME.type
- const BB_REFERENCE_TYPE = FieldType.BB_REFERENCE
- const BB_USER_REFERENCE_TYPE = composeType(
- BB_REFERENCE_TYPE,
- FieldSubtype.USER
- )
+ const USER_REFRENCE_TYPE = FIELDS.BB_REFERENCE_USER.compositeType
const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
@@ -80,33 +76,6 @@
fieldName: $tables.selected.name,
}
- const bbRefTypeMapping = {}
-
- function composeType(fieldType, subtype) {
- return `${fieldType}_${subtype}`
- }
-
- // Handling fields with subtypes
- fieldDefinitions = Object.entries(fieldDefinitions).reduce(
- (p, [key, field]) => {
- if (field.type === BB_REFERENCE_TYPE) {
- const composedType = composeType(field.type, field.subtype)
- p[key] = {
- ...field,
- type: composedType,
- }
- bbRefTypeMapping[composedType] = {
- type: field.type,
- subtype: field.subtype,
- }
- } else {
- p[key] = field
- }
- return p
- },
- {}
- )
-
$: if (primaryDisplay) {
editableColumn.constraints.presence = { allowEmpty: false }
}
@@ -149,12 +118,8 @@
$tables.selected.primaryDisplay == null ||
$tables.selected.primaryDisplay === editableColumn.name
- const mapped = Object.entries(bbRefTypeMapping).find(
- ([_, v]) => v.type === field.type && v.subtype === field.subtype
- )
- if (mapped) {
- editableColumn.type = mapped[0]
- delete editableColumn.subtype
+ if (editableColumn.type === FieldType.BB_REFERENCE) {
+ editableColumn.type = `${editableColumn.type}_${editableColumn.subtype}`
}
} else if (!savingColumn) {
let highestNumber = 0
@@ -188,8 +153,6 @@
$: initialiseField(field, savingColumn)
- $: isBBReference = !!bbRefTypeMapping[editableColumn.type]
-
$: checkConstraints(editableColumn)
$: required = !!editableColumn?.constraints?.presence || primaryDisplay
$: uneditable =
@@ -265,11 +228,12 @@
let saveColumn = cloneDeep(editableColumn)
- if (bbRefTypeMapping[saveColumn.type]) {
- saveColumn = {
- ...saveColumn,
- ...bbRefTypeMapping[saveColumn.type],
- }
+ // Handle types on composite types
+ const definition = fieldDefinitions[saveColumn.type.toUpperCase()]
+ if (definition && saveColumn.type === definition.compositeType) {
+ saveColumn.type = definition.type
+ saveColumn.subtype = definition.subtype
+ delete saveColumn.compositeType
}
if (saveColumn.type === AUTO_TYPE) {
@@ -352,7 +316,7 @@
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
} else if (editableColumn.type === FORMULA_TYPE) {
editableColumn.formulaType = "dynamic"
- } else if (editableColumn.type === BB_USER_REFERENCE_TYPE) {
+ } else if (editableColumn.type === USER_REFRENCE_TYPE) {
editableColumn.relationshipType = RelationshipType.ONE_TO_MANY
}
}
@@ -410,14 +374,12 @@
FIELDS.BOOLEAN,
FIELDS.FORMULA,
FIELDS.BIGINT,
+ FIELDS.BB_REFERENCE_USER,
]
// no-sql or a spreadsheet
if (!external || table.sql) {
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
}
- if (fieldDefinitions.USER) {
- fields.push(fieldDefinitions.USER)
- }
return fields
}
}
@@ -426,8 +388,9 @@
if (!fieldToCheck) {
return
}
+
// most types need this, just make sure its always present
- if (fieldToCheck && !fieldToCheck.constraints) {
+ if (!fieldToCheck.constraints) {
fieldToCheck.constraints = {}
}
// some string types may have been built by server, may not always have constraints
@@ -507,7 +470,7 @@
on:change={handleTypeChange}
options={allowedTypes}
getOptionLabel={field => field.name}
- getOptionValue={field => field.type}
+ getOptionValue={field => field.compositeType || field.type}
getOptionIcon={field => field.icon}
isOptionEnabled={option => {
if (option.type == AUTO_TYPE) {
@@ -671,7 +634,7 @@
- {:else if isBBReference}
+ {:else if editableColumn.type === USER_REFRENCE_TYPE}
diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js
index 047152eeed..8b76207822 100644
--- a/packages/builder/src/constants/backend/index.js
+++ b/packages/builder/src/constants/backend/index.js
@@ -120,10 +120,11 @@ export const FIELDS = {
presence: false,
},
},
- USER: {
+ BB_REFERENCE_USER: {
name: "User",
type: "bb_reference",
subtype: "user",
+ compositeType: "bb_reference_user", // Used for working with the subtype on CreateEditColumn as is it was a primary type
icon: "User",
},
}
diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts
index 52dfcfd97a..1e57416cd1 100644
--- a/packages/server/src/api/controllers/row/external.ts
+++ b/packages/server/src/api/controllers/row/external.ts
@@ -18,7 +18,10 @@ import {
import sdk from "../../../sdk"
import * as utils from "./utils"
import { dataFilters } from "@budibase/shared-core"
-import { inputProcessing } from "../../../utilities/rowProcessor"
+import {
+ inputProcessing,
+ outputProcessing,
+} from "../../../utilities/rowProcessor"
import { cloneDeep, isEqual } from "lodash"
export async function handleRequest(
@@ -46,24 +49,31 @@ export async function patch(ctx: UserCtx) {
const tableId = utils.getTableId(ctx)
const { _id, ...rowData } = ctx.request.body
+ const table = await sdk.tables.getTable(tableId)
+ const { row: dataToUpdate } = await inputProcessing(
+ ctx.user?._id,
+ cloneDeep(table),
+ rowData
+ )
+
const validateResult = await sdk.rows.utils.validate({
- row: rowData,
+ row: dataToUpdate,
tableId,
})
if (!validateResult.valid) {
throw { validation: validateResult.errors }
}
+
const response = await handleRequest(Operation.UPDATE, tableId, {
id: breakRowIdField(_id),
- row: rowData,
+ row: dataToUpdate,
})
const row = await sdk.rows.external.getRow(tableId, _id, {
relationships: true,
})
- const table = await sdk.tables.getTable(tableId)
return {
...response,
- row,
+ row: await outputProcessing(table, row),
table,
}
}
@@ -71,13 +81,6 @@ export async function patch(ctx: UserCtx) {
export async function save(ctx: UserCtx) {
const inputs = ctx.request.body
const tableId = utils.getTableId(ctx)
- const validateResult = await sdk.rows.utils.validate({
- row: inputs,
- tableId,
- })
- if (!validateResult.valid) {
- throw { validation: validateResult.errors }
- }
const table = await sdk.tables.getTable(tableId)
const { table: updatedTable, row } = await inputProcessing(
@@ -86,6 +89,14 @@ export async function save(ctx: UserCtx) {
inputs
)
+ const validateResult = await sdk.rows.utils.validate({
+ row,
+ tableId,
+ })
+ if (!validateResult.valid) {
+ throw { validation: validateResult.errors }
+ }
+
const response = await handleRequest(Operation.CREATE, tableId, {
row,
})
@@ -103,7 +114,7 @@ export async function save(ctx: UserCtx) {
})
return {
...response,
- row,
+ row: await outputProcessing(table, row),
}
} else {
return response
@@ -121,7 +132,12 @@ export async function find(ctx: UserCtx): Promise {
ctx.throw(404)
}
- return row
+ const table = await sdk.tables.getTable(tableId)
+ // Preserving links, as the outputProcessing does not support external rows yet and we don't need it in this use case
+ return await outputProcessing(table, row, {
+ squash: false,
+ preserveLinks: true,
+ })
}
export async function destroy(ctx: UserCtx) {
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index 6a021460ac..b4a33efdde 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -7,6 +7,7 @@ import { context, InternalTable, roles, tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro"
import {
FieldType,
+ FieldTypeSubtypes,
MonthlyQuotaName,
PermissionLevel,
QuotaUsageType,
@@ -17,6 +18,7 @@ import {
SortType,
StaticQuotaName,
Table,
+ User,
} from "@budibase/types"
import {
expectAnyExternalColsAttributes,
@@ -25,6 +27,7 @@ import {
mocks,
structures,
} from "@budibase/backend-core/tests"
+import _ from "lodash"
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
tk.freeze(timestamp)
@@ -34,7 +37,7 @@ const { basicRow } = setup.structures
describe.each([
["internal", undefined],
["postgres", databaseTestProviders.postgres],
-])("/rows (%s)", (_, dsProvider) => {
+])("/rows (%s)", (__, dsProvider) => {
const isInternal = !dsProvider
const request = setup.getRequest()
@@ -1511,4 +1514,393 @@ describe.each([
})
})
})
+
+ describe("bb reference fields", () => {
+ let tableId: string
+ let users: User[]
+
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+
+ if (config.datasource) {
+ tableConfig.sourceId = config.datasource._id
+ if (config.datasource.plus) {
+ tableConfig.type = "external"
+ }
+ }
+ const table = await config.api.table.create({
+ ...tableConfig,
+ schema: {
+ ...tableConfig.schema,
+ user: {
+ name: "user",
+ type: FieldType.BB_REFERENCE,
+ subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
+ relationshipType: RelationshipType.ONE_TO_MANY,
+ },
+ users: {
+ name: "users",
+ type: FieldType.BB_REFERENCE,
+ subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
+ relationshipType: RelationshipType.MANY_TO_MANY,
+ },
+ },
+ })
+ tableId = table._id!
+
+ users = [
+ await config.createUser(),
+ await config.createUser(),
+ await config.createUser(),
+ await config.createUser(),
+ ]
+ })
+
+ it("can save a row when BB reference fields are empty", async () => {
+ const rowData = {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ }
+ const row = await config.api.row.save(tableId, rowData)
+
+ expect(row).toEqual({
+ name: rowData.name,
+ description: rowData.description,
+ tableId,
+ _id: expect.any(String),
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ type: isInternal ? "row" : undefined,
+ })
+ })
+
+ it("can save a row with a single BB reference field", async () => {
+ const user = _.sample(users)!
+ const rowData = {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ user: user,
+ }
+ const row = await config.api.row.save(tableId, rowData)
+
+ expect(row).toEqual({
+ name: rowData.name,
+ description: rowData.description,
+ tableId,
+ user: [
+ {
+ _id: user._id,
+ email: user.email,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ primaryDisplay: user.email,
+ },
+ ],
+ _id: expect.any(String),
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ type: isInternal ? "row" : undefined,
+ })
+ })
+
+ it("can save a row with a multiple BB reference field", async () => {
+ const selectedUsers = _.sampleSize(users, 2)
+ const rowData = {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: selectedUsers,
+ }
+ const row = await config.api.row.save(tableId, rowData)
+
+ expect(row).toEqual({
+ name: rowData.name,
+ description: rowData.description,
+ tableId,
+ users: selectedUsers.map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ _id: expect.any(String),
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ type: isInternal ? "row" : undefined,
+ })
+ })
+
+ it("can retrieve rows with no populated BB references", async () => {
+ const rowData = {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ }
+ const row = await config.api.row.save(tableId, rowData)
+
+ const { body: retrieved } = await config.api.row.get(tableId, row._id!)
+ expect(retrieved).toEqual({
+ name: rowData.name,
+ description: rowData.description,
+ tableId,
+ user: undefined,
+ users: undefined,
+ _id: row._id,
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ ...defaultRowFields,
+ })
+ })
+
+ it("can retrieve rows with populated BB references", async () => {
+ const [user1, user2] = _.sampleSize(users, 2)
+
+ const rowData = {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: [user1, user2],
+ user: [user1],
+ }
+ const row = await config.api.row.save(tableId, rowData)
+
+ const { body: retrieved } = await config.api.row.get(tableId, row._id!)
+ expect(retrieved).toEqual({
+ name: rowData.name,
+ description: rowData.description,
+ tableId,
+ user: [user1].map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ users: [user1, user2].map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ _id: row._id,
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ ...defaultRowFields,
+ })
+ })
+
+ it("can update an existing populated row", async () => {
+ const [user1, user2, user3] = _.sampleSize(users, 3)
+
+ const rowData = {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: [user1, user2],
+ }
+ const row = await config.api.row.save(tableId, rowData)
+
+ const updatedRow = await config.api.row.save(tableId, {
+ ...row,
+ user: [user3],
+ users: [user3, user2],
+ })
+ expect(updatedRow).toEqual({
+ name: rowData.name,
+ description: rowData.description,
+ tableId,
+ user: [
+ {
+ _id: user3._id,
+ email: user3.email,
+ firstName: user3.firstName,
+ lastName: user3.lastName,
+ primaryDisplay: user3.email,
+ },
+ ],
+ users: [user3, user2].map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ _id: row._id,
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ type: isInternal ? "row" : undefined,
+ })
+ })
+
+ it("can wipe an existing populated BB references in row", async () => {
+ const [user1, user2] = _.sampleSize(users, 2)
+
+ const rowData = {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: [user1, user2],
+ }
+ const row = await config.api.row.save(tableId, rowData)
+
+ const updatedRow = await config.api.row.save(tableId, {
+ ...row,
+ user: null,
+ users: null,
+ })
+ expect(updatedRow).toEqual({
+ name: rowData.name,
+ description: rowData.description,
+ tableId,
+ user: isInternal ? null : undefined,
+ users: isInternal ? null : undefined,
+ _id: row._id,
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ type: isInternal ? "row" : undefined,
+ })
+ })
+
+ it("fetch all will populate the BB references", async () => {
+ const [user1, user2, user3] = _.sampleSize(users, 3)
+
+ const rows: {
+ name: string
+ description: string
+ user?: User[]
+ users?: User[]
+ tableId: string
+ }[] = [
+ {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: [user1, user2],
+ },
+ {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ user: [user1],
+ users: [user1, user3],
+ },
+ {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: [user3],
+ },
+ ]
+
+ await config.api.row.save(tableId, rows[0])
+ await config.api.row.save(tableId, rows[1])
+ await config.api.row.save(tableId, rows[2])
+
+ const res = await config.api.row.fetch(tableId)
+
+ expect(res).toEqual(
+ expect.arrayContaining(
+ rows.map(r => ({
+ name: r.name,
+ description: r.description,
+ tableId,
+ user: r.user?.map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ users: r.users?.map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ _id: expect.any(String),
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ ...defaultRowFields,
+ }))
+ )
+ )
+ })
+
+ it("search all will populate the BB references", async () => {
+ const [user1, user2, user3] = _.sampleSize(users, 3)
+
+ const rows: {
+ name: string
+ description: string
+ user?: User[]
+ users?: User[]
+ tableId: string
+ }[] = [
+ {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: [user1, user2],
+ },
+ {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ user: [user1],
+ users: [user1, user3],
+ },
+ {
+ ...basicRow(tableId),
+ name: generator.name(),
+ description: generator.name(),
+ users: [user3],
+ },
+ ]
+
+ await config.api.row.save(tableId, rows[0])
+ await config.api.row.save(tableId, rows[1])
+ await config.api.row.save(tableId, rows[2])
+
+ const res = await config.api.row.search(tableId)
+
+ expect(res).toEqual({
+ rows: expect.arrayContaining(
+ rows.map(r => ({
+ name: r.name,
+ description: r.description,
+ tableId,
+ user: r.user?.map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ users: r.users?.map(u => ({
+ _id: u._id,
+ email: u.email,
+ firstName: u.firstName,
+ lastName: u.lastName,
+ primaryDisplay: u.email,
+ })),
+ _id: expect.any(String),
+ _rev: expect.any(String),
+ id: isInternal ? undefined : expect.any(Number),
+ ...defaultRowFields,
+ }))
+ ),
+ ...(isInternal
+ ? {}
+ : {
+ hasNextPage: false,
+ bookmark: null,
+ }),
+ })
+ })
+ })
})
diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts
index 447d1d7d16..817bfce33d 100644
--- a/packages/server/src/sdk/app/rows/search/external.ts
+++ b/packages/server/src/sdk/app/rows/search/external.ts
@@ -17,6 +17,7 @@ import { utils } from "@budibase/shared-core"
import { ExportRowsParams, ExportRowsResult } from "../search"
import { HTTPError, db } from "@budibase/backend-core"
import pick from "lodash/pick"
+import { outputProcessing } from "../../../../utilities/rowProcessor"
export async function search(options: SearchParams) {
const { tableId } = options
@@ -75,6 +76,9 @@ export async function search(options: SearchParams) {
rows = rows.map((r: any) => pick(r, fields))
}
+ const table = await sdk.tables.getTable(tableId)
+ rows = await outputProcessing(table, rows)
+
// need wrapper object for bookmarks etc when paginating
return { rows, hasNextPage, bookmark: bookmark && bookmark + 1 }
} catch (err: any) {
@@ -166,9 +170,11 @@ export async function exportRows(
}
export async function fetch(tableId: string) {
- return handleRequest(Operation.READ, tableId, {
+ const response = await handleRequest(Operation.READ, tableId, {
includeSqlRelationships: IncludeRelationship.INCLUDE,
})
+ const table = await sdk.tables.getTable(tableId)
+ return await outputProcessing(table, response)
}
export async function fetchView(viewName: string) {
diff --git a/packages/server/src/sdk/tests/rows/row.spec.ts b/packages/server/src/sdk/tests/rows/row.spec.ts
index 08c5746f2e..af3d405e15 100644
--- a/packages/server/src/sdk/tests/rows/row.spec.ts
+++ b/packages/server/src/sdk/tests/rows/row.spec.ts
@@ -7,9 +7,14 @@ import { HTTPError } from "@budibase/backend-core"
import { Operation } from "@budibase/types"
const mockDatasourcesGet = jest.fn()
+const mockTableGet = jest.fn()
sdk.datasources.get = mockDatasourcesGet
+sdk.tables.getTable = mockTableGet
jest.mock("../../../api/controllers/row/ExternalRequest")
+jest.mock("../../../utilities/rowProcessor", () => ({
+ outputProcessing: jest.fn((_, rows) => rows),
+}))
jest.mock("../../../api/controllers/view/exporters", () => ({
...jest.requireActual("../../../api/controllers/view/exporters"),
diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts
index 686c8c031b..adeb96a593 100644
--- a/packages/server/src/tests/utilities/api/row.ts
+++ b/packages/server/src/tests/utilities/api/row.ts
@@ -44,12 +44,12 @@ export class RowAPI extends TestAPI {
}
save = async (
- sourceId: string,
+ tableId: string,
row: SaveRowRequest,
{ expectStatus } = { expectStatus: 200 }
): Promise => {
const resp = await this.request
- .post(`/api/${sourceId}/rows`)
+ .post(`/api/${tableId}/rows`)
.send(row)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
@@ -122,4 +122,16 @@ export class RowAPI extends TestAPI {
.expect(expectStatus)
return request
}
+
+ search = async (
+ sourceId: string,
+ { expectStatus } = { expectStatus: 200 }
+ ): Promise => {
+ const request = this.request
+ .post(`/api/${sourceId}/search`)
+ .set(this.config.defaultHeaders())
+ .expect(expectStatus)
+
+ return (await request).body
+ }
}
diff --git a/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts b/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts
index b9b91b6789..5409ed925c 100644
--- a/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts
+++ b/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts
@@ -6,7 +6,7 @@ import { InvalidBBRefError } from "./errors"
export async function processInputBBReferences(
value: string | string[] | { _id: string } | { _id: string }[],
subtype: FieldSubtype
-): Promise {
+): Promise {
const referenceIds: string[] = []
if (Array.isArray(value)) {
@@ -39,7 +39,7 @@ export async function processInputBBReferences(
throw utils.unreachable(subtype)
}
- return referenceIds.join(",") || undefined
+ return referenceIds.join(",") || null
}
export async function processOutputBBReferences(
diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts
index 933e9bd2f5..773b54dd6a 100644
--- a/packages/server/src/utilities/rowProcessor/index.ts
+++ b/packages/server/src/utilities/rowProcessor/index.ts
@@ -200,7 +200,10 @@ export async function inputProcessing(
export async function outputProcessing(
table: Table,
rows: T,
- opts = { squash: true }
+ opts: { squash?: boolean; preserveLinks?: boolean } = {
+ squash: true,
+ preserveLinks: false,
+ }
): Promise {
let safeRows: Row[]
let wasArray = true
@@ -211,7 +214,9 @@ export async function outputProcessing(
safeRows = rows
}
// attach any linked row information
- let enriched = await linkRows.attachFullLinkedDocs(table, safeRows)
+ let enriched = !opts.preserveLinks
+ ? await linkRows.attachFullLinkedDocs(table, safeRows)
+ : safeRows
// process formulas
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
diff --git a/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts b/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts
index 67a44a86f2..d0932b399c 100644
--- a/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts
+++ b/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts
@@ -139,20 +139,20 @@ describe("bbReferenceProcessor", () => {
expect(cacheGetUsersSpy).toBeCalledWith(userIds)
})
- it("empty strings will return undefined", async () => {
+ it("empty strings will return null", async () => {
const result = await config.doInTenant(() =>
processInputBBReferences("", FieldSubtype.USER)
)
- expect(result).toEqual(undefined)
+ expect(result).toEqual(null)
})
- it("empty arrays will return undefined", async () => {
+ it("empty arrays will return null", async () => {
const result = await config.doInTenant(() =>
processInputBBReferences([], FieldSubtype.USER)
)
- expect(result).toEqual(undefined)
+ expect(result).toEqual(null)
})
})
})