Merge pull request #11908 from Budibase/BUDI-7403/ds+_user_column_edition
Fixing DS+ save rows
This commit is contained in:
commit
216e22c898
|
@ -33,7 +33,7 @@
|
||||||
import { getBindings } from "components/backend/DataTable/formula"
|
import { getBindings } from "components/backend/DataTable/formula"
|
||||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||||
import { FieldSubtype, FieldType } from "@budibase/types"
|
import { FieldType } from "@budibase/types"
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
|
|
||||||
const AUTO_TYPE = "auto"
|
const AUTO_TYPE = "auto"
|
||||||
|
@ -43,11 +43,7 @@
|
||||||
const NUMBER_TYPE = FIELDS.NUMBER.type
|
const NUMBER_TYPE = FIELDS.NUMBER.type
|
||||||
const JSON_TYPE = FIELDS.JSON.type
|
const JSON_TYPE = FIELDS.JSON.type
|
||||||
const DATE_TYPE = FIELDS.DATETIME.type
|
const DATE_TYPE = FIELDS.DATETIME.type
|
||||||
const BB_REFERENCE_TYPE = FieldType.BB_REFERENCE
|
const USER_REFRENCE_TYPE = FIELDS.BB_REFERENCE_USER.compositeType
|
||||||
const BB_USER_REFERENCE_TYPE = composeType(
|
|
||||||
BB_REFERENCE_TYPE,
|
|
||||||
FieldSubtype.USER
|
|
||||||
)
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||||
|
@ -80,33 +76,6 @@
|
||||||
fieldName: $tables.selected.name,
|
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) {
|
$: if (primaryDisplay) {
|
||||||
editableColumn.constraints.presence = { allowEmpty: false }
|
editableColumn.constraints.presence = { allowEmpty: false }
|
||||||
}
|
}
|
||||||
|
@ -149,12 +118,8 @@
|
||||||
$tables.selected.primaryDisplay == null ||
|
$tables.selected.primaryDisplay == null ||
|
||||||
$tables.selected.primaryDisplay === editableColumn.name
|
$tables.selected.primaryDisplay === editableColumn.name
|
||||||
|
|
||||||
const mapped = Object.entries(bbRefTypeMapping).find(
|
if (editableColumn.type === FieldType.BB_REFERENCE) {
|
||||||
([_, v]) => v.type === field.type && v.subtype === field.subtype
|
editableColumn.type = `${editableColumn.type}_${editableColumn.subtype}`
|
||||||
)
|
|
||||||
if (mapped) {
|
|
||||||
editableColumn.type = mapped[0]
|
|
||||||
delete editableColumn.subtype
|
|
||||||
}
|
}
|
||||||
} else if (!savingColumn) {
|
} else if (!savingColumn) {
|
||||||
let highestNumber = 0
|
let highestNumber = 0
|
||||||
|
@ -188,8 +153,6 @@
|
||||||
|
|
||||||
$: initialiseField(field, savingColumn)
|
$: initialiseField(field, savingColumn)
|
||||||
|
|
||||||
$: isBBReference = !!bbRefTypeMapping[editableColumn.type]
|
|
||||||
|
|
||||||
$: checkConstraints(editableColumn)
|
$: checkConstraints(editableColumn)
|
||||||
$: required = !!editableColumn?.constraints?.presence || primaryDisplay
|
$: required = !!editableColumn?.constraints?.presence || primaryDisplay
|
||||||
$: uneditable =
|
$: uneditable =
|
||||||
|
@ -265,11 +228,12 @@
|
||||||
|
|
||||||
let saveColumn = cloneDeep(editableColumn)
|
let saveColumn = cloneDeep(editableColumn)
|
||||||
|
|
||||||
if (bbRefTypeMapping[saveColumn.type]) {
|
// Handle types on composite types
|
||||||
saveColumn = {
|
const definition = fieldDefinitions[saveColumn.type.toUpperCase()]
|
||||||
...saveColumn,
|
if (definition && saveColumn.type === definition.compositeType) {
|
||||||
...bbRefTypeMapping[saveColumn.type],
|
saveColumn.type = definition.type
|
||||||
}
|
saveColumn.subtype = definition.subtype
|
||||||
|
delete saveColumn.compositeType
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveColumn.type === AUTO_TYPE) {
|
if (saveColumn.type === AUTO_TYPE) {
|
||||||
|
@ -352,7 +316,7 @@
|
||||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||||
} else if (editableColumn.type === FORMULA_TYPE) {
|
} else if (editableColumn.type === FORMULA_TYPE) {
|
||||||
editableColumn.formulaType = "dynamic"
|
editableColumn.formulaType = "dynamic"
|
||||||
} else if (editableColumn.type === BB_USER_REFERENCE_TYPE) {
|
} else if (editableColumn.type === USER_REFRENCE_TYPE) {
|
||||||
editableColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
editableColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -410,14 +374,12 @@
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
FIELDS.BIGINT,
|
FIELDS.BIGINT,
|
||||||
|
FIELDS.BB_REFERENCE_USER,
|
||||||
]
|
]
|
||||||
// no-sql or a spreadsheet
|
// no-sql or a spreadsheet
|
||||||
if (!external || table.sql) {
|
if (!external || table.sql) {
|
||||||
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
||||||
}
|
}
|
||||||
if (fieldDefinitions.USER) {
|
|
||||||
fields.push(fieldDefinitions.USER)
|
|
||||||
}
|
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -426,8 +388,9 @@
|
||||||
if (!fieldToCheck) {
|
if (!fieldToCheck) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// most types need this, just make sure its always present
|
// most types need this, just make sure its always present
|
||||||
if (fieldToCheck && !fieldToCheck.constraints) {
|
if (!fieldToCheck.constraints) {
|
||||||
fieldToCheck.constraints = {}
|
fieldToCheck.constraints = {}
|
||||||
}
|
}
|
||||||
// some string types may have been built by server, may not always have constraints
|
// some string types may have been built by server, may not always have constraints
|
||||||
|
@ -507,7 +470,7 @@
|
||||||
on:change={handleTypeChange}
|
on:change={handleTypeChange}
|
||||||
options={allowedTypes}
|
options={allowedTypes}
|
||||||
getOptionLabel={field => field.name}
|
getOptionLabel={field => field.name}
|
||||||
getOptionValue={field => field.type}
|
getOptionValue={field => field.compositeType || field.type}
|
||||||
getOptionIcon={field => field.icon}
|
getOptionIcon={field => field.icon}
|
||||||
isOptionEnabled={option => {
|
isOptionEnabled={option => {
|
||||||
if (option.type == AUTO_TYPE) {
|
if (option.type == AUTO_TYPE) {
|
||||||
|
@ -671,7 +634,7 @@
|
||||||
<Button primary text on:click={openJsonSchemaEditor}
|
<Button primary text on:click={openJsonSchemaEditor}
|
||||||
>Open schema editor</Button
|
>Open schema editor</Button
|
||||||
>
|
>
|
||||||
{:else if isBBReference}
|
{:else if editableColumn.type === USER_REFRENCE_TYPE}
|
||||||
<Toggle
|
<Toggle
|
||||||
value={editableColumn.relationshipType === RelationshipType.MANY_TO_MANY}
|
value={editableColumn.relationshipType === RelationshipType.MANY_TO_MANY}
|
||||||
on:change={e =>
|
on:change={e =>
|
||||||
|
|
|
@ -120,10 +120,11 @@ export const FIELDS = {
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
USER: {
|
BB_REFERENCE_USER: {
|
||||||
name: "User",
|
name: "User",
|
||||||
type: "bb_reference",
|
type: "bb_reference",
|
||||||
subtype: "user",
|
subtype: "user",
|
||||||
|
compositeType: "bb_reference_user", // Used for working with the subtype on CreateEditColumn as is it was a primary type
|
||||||
icon: "User",
|
icon: "User",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,10 @@ import {
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
import { inputProcessing } from "../../../utilities/rowProcessor"
|
import {
|
||||||
|
inputProcessing,
|
||||||
|
outputProcessing,
|
||||||
|
} from "../../../utilities/rowProcessor"
|
||||||
import { cloneDeep, isEqual } from "lodash"
|
import { cloneDeep, isEqual } from "lodash"
|
||||||
|
|
||||||
export async function handleRequest(
|
export async function handleRequest(
|
||||||
|
@ -46,24 +49,31 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
const { _id, ...rowData } = ctx.request.body
|
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({
|
const validateResult = await sdk.rows.utils.validate({
|
||||||
row: rowData,
|
row: dataToUpdate,
|
||||||
tableId,
|
tableId,
|
||||||
})
|
})
|
||||||
if (!validateResult.valid) {
|
if (!validateResult.valid) {
|
||||||
throw { validation: validateResult.errors }
|
throw { validation: validateResult.errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await handleRequest(Operation.UPDATE, tableId, {
|
const response = await handleRequest(Operation.UPDATE, tableId, {
|
||||||
id: breakRowIdField(_id),
|
id: breakRowIdField(_id),
|
||||||
row: rowData,
|
row: dataToUpdate,
|
||||||
})
|
})
|
||||||
const row = await sdk.rows.external.getRow(tableId, _id, {
|
const row = await sdk.rows.external.getRow(tableId, _id, {
|
||||||
relationships: true,
|
relationships: true,
|
||||||
})
|
})
|
||||||
const table = await sdk.tables.getTable(tableId)
|
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
row,
|
row: await outputProcessing(table, row),
|
||||||
table,
|
table,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,13 +81,6 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
export async function save(ctx: UserCtx) {
|
export async function save(ctx: UserCtx) {
|
||||||
const inputs = ctx.request.body
|
const inputs = ctx.request.body
|
||||||
const tableId = utils.getTableId(ctx)
|
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 = await sdk.tables.getTable(tableId)
|
||||||
const { table: updatedTable, row } = await inputProcessing(
|
const { table: updatedTable, row } = await inputProcessing(
|
||||||
|
@ -86,6 +89,14 @@ export async function save(ctx: UserCtx) {
|
||||||
inputs
|
inputs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const validateResult = await sdk.rows.utils.validate({
|
||||||
|
row,
|
||||||
|
tableId,
|
||||||
|
})
|
||||||
|
if (!validateResult.valid) {
|
||||||
|
throw { validation: validateResult.errors }
|
||||||
|
}
|
||||||
|
|
||||||
const response = await handleRequest(Operation.CREATE, tableId, {
|
const response = await handleRequest(Operation.CREATE, tableId, {
|
||||||
row,
|
row,
|
||||||
})
|
})
|
||||||
|
@ -103,7 +114,7 @@ export async function save(ctx: UserCtx) {
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
row,
|
row: await outputProcessing(table, row),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return response
|
return response
|
||||||
|
@ -121,7 +132,12 @@ export async function find(ctx: UserCtx): Promise<Row> {
|
||||||
ctx.throw(404)
|
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) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { context, InternalTable, roles, tenancy } from "@budibase/backend-core"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
|
FieldTypeSubtypes,
|
||||||
MonthlyQuotaName,
|
MonthlyQuotaName,
|
||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
QuotaUsageType,
|
QuotaUsageType,
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
SortType,
|
SortType,
|
||||||
StaticQuotaName,
|
StaticQuotaName,
|
||||||
Table,
|
Table,
|
||||||
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
expectAnyExternalColsAttributes,
|
expectAnyExternalColsAttributes,
|
||||||
|
@ -25,6 +27,7 @@ import {
|
||||||
mocks,
|
mocks,
|
||||||
structures,
|
structures,
|
||||||
} from "@budibase/backend-core/tests"
|
} from "@budibase/backend-core/tests"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
||||||
tk.freeze(timestamp)
|
tk.freeze(timestamp)
|
||||||
|
@ -34,7 +37,7 @@ const { basicRow } = setup.structures
|
||||||
describe.each([
|
describe.each([
|
||||||
["internal", undefined],
|
["internal", undefined],
|
||||||
["postgres", databaseTestProviders.postgres],
|
["postgres", databaseTestProviders.postgres],
|
||||||
])("/rows (%s)", (_, dsProvider) => {
|
])("/rows (%s)", (__, dsProvider) => {
|
||||||
const isInternal = !dsProvider
|
const isInternal = !dsProvider
|
||||||
|
|
||||||
const request = setup.getRequest()
|
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,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { utils } from "@budibase/shared-core"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "../search"
|
import { ExportRowsParams, ExportRowsResult } from "../search"
|
||||||
import { HTTPError, db } from "@budibase/backend-core"
|
import { HTTPError, db } from "@budibase/backend-core"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
|
|
||||||
export async function search(options: SearchParams) {
|
export async function search(options: SearchParams) {
|
||||||
const { tableId } = options
|
const { tableId } = options
|
||||||
|
@ -75,6 +76,9 @@ export async function search(options: SearchParams) {
|
||||||
rows = rows.map((r: any) => pick(r, fields))
|
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
|
// need wrapper object for bookmarks etc when paginating
|
||||||
return { rows, hasNextPage, bookmark: bookmark && bookmark + 1 }
|
return { rows, hasNextPage, bookmark: bookmark && bookmark + 1 }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -166,9 +170,11 @@ export async function exportRows(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(tableId: string) {
|
export async function fetch(tableId: string) {
|
||||||
return handleRequest(Operation.READ, tableId, {
|
const response = await handleRequest(Operation.READ, tableId, {
|
||||||
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
||||||
})
|
})
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
return await outputProcessing(table, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchView(viewName: string) {
|
export async function fetchView(viewName: string) {
|
||||||
|
|
|
@ -7,9 +7,14 @@ import { HTTPError } from "@budibase/backend-core"
|
||||||
import { Operation } from "@budibase/types"
|
import { Operation } from "@budibase/types"
|
||||||
|
|
||||||
const mockDatasourcesGet = jest.fn()
|
const mockDatasourcesGet = jest.fn()
|
||||||
|
const mockTableGet = jest.fn()
|
||||||
sdk.datasources.get = mockDatasourcesGet
|
sdk.datasources.get = mockDatasourcesGet
|
||||||
|
sdk.tables.getTable = mockTableGet
|
||||||
|
|
||||||
jest.mock("../../../api/controllers/row/ExternalRequest")
|
jest.mock("../../../api/controllers/row/ExternalRequest")
|
||||||
|
jest.mock("../../../utilities/rowProcessor", () => ({
|
||||||
|
outputProcessing: jest.fn((_, rows) => rows),
|
||||||
|
}))
|
||||||
|
|
||||||
jest.mock("../../../api/controllers/view/exporters", () => ({
|
jest.mock("../../../api/controllers/view/exporters", () => ({
|
||||||
...jest.requireActual("../../../api/controllers/view/exporters"),
|
...jest.requireActual("../../../api/controllers/view/exporters"),
|
||||||
|
|
|
@ -44,12 +44,12 @@ export class RowAPI extends TestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
save = async (
|
save = async (
|
||||||
sourceId: string,
|
tableId: string,
|
||||||
row: SaveRowRequest,
|
row: SaveRowRequest,
|
||||||
{ expectStatus } = { expectStatus: 200 }
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
): Promise<Row> => {
|
): Promise<Row> => {
|
||||||
const resp = await this.request
|
const resp = await this.request
|
||||||
.post(`/api/${sourceId}/rows`)
|
.post(`/api/${tableId}/rows`)
|
||||||
.send(row)
|
.send(row)
|
||||||
.set(this.config.defaultHeaders())
|
.set(this.config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
|
@ -122,4 +122,16 @@ export class RowAPI extends TestAPI {
|
||||||
.expect(expectStatus)
|
.expect(expectStatus)
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search = async (
|
||||||
|
sourceId: string,
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
): Promise<Row[]> => {
|
||||||
|
const request = this.request
|
||||||
|
.post(`/api/${sourceId}/search`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect(expectStatus)
|
||||||
|
|
||||||
|
return (await request).body
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { InvalidBBRefError } from "./errors"
|
||||||
export async function processInputBBReferences(
|
export async function processInputBBReferences(
|
||||||
value: string | string[] | { _id: string } | { _id: string }[],
|
value: string | string[] | { _id: string } | { _id: string }[],
|
||||||
subtype: FieldSubtype
|
subtype: FieldSubtype
|
||||||
): Promise<string | undefined> {
|
): Promise<string | null> {
|
||||||
const referenceIds: string[] = []
|
const referenceIds: string[] = []
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
@ -39,7 +39,7 @@ export async function processInputBBReferences(
|
||||||
throw utils.unreachable(subtype)
|
throw utils.unreachable(subtype)
|
||||||
}
|
}
|
||||||
|
|
||||||
return referenceIds.join(",") || undefined
|
return referenceIds.join(",") || null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processOutputBBReferences(
|
export async function processOutputBBReferences(
|
||||||
|
|
|
@ -200,7 +200,10 @@ export async function inputProcessing(
|
||||||
export async function outputProcessing<T extends Row[] | Row>(
|
export async function outputProcessing<T extends Row[] | Row>(
|
||||||
table: Table,
|
table: Table,
|
||||||
rows: T,
|
rows: T,
|
||||||
opts = { squash: true }
|
opts: { squash?: boolean; preserveLinks?: boolean } = {
|
||||||
|
squash: true,
|
||||||
|
preserveLinks: false,
|
||||||
|
}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
let safeRows: Row[]
|
let safeRows: Row[]
|
||||||
let wasArray = true
|
let wasArray = true
|
||||||
|
@ -211,7 +214,9 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
safeRows = rows
|
safeRows = rows
|
||||||
}
|
}
|
||||||
// attach any linked row information
|
// attach any linked row information
|
||||||
let enriched = await linkRows.attachFullLinkedDocs(table, safeRows)
|
let enriched = !opts.preserveLinks
|
||||||
|
? await linkRows.attachFullLinkedDocs(table, safeRows)
|
||||||
|
: safeRows
|
||||||
|
|
||||||
// process formulas
|
// process formulas
|
||||||
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
|
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
|
||||||
|
|
|
@ -139,20 +139,20 @@ describe("bbReferenceProcessor", () => {
|
||||||
expect(cacheGetUsersSpy).toBeCalledWith(userIds)
|
expect(cacheGetUsersSpy).toBeCalledWith(userIds)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("empty strings will return undefined", async () => {
|
it("empty strings will return null", async () => {
|
||||||
const result = await config.doInTenant(() =>
|
const result = await config.doInTenant(() =>
|
||||||
processInputBBReferences("", FieldSubtype.USER)
|
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(() =>
|
const result = await config.doInTenant(() =>
|
||||||
processInputBBReferences([], FieldSubtype.USER)
|
processInputBBReferences([], FieldSubtype.USER)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual(undefined)
|
expect(result).toEqual(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue