@budibase/server
Version:
Budibase Web Server
1,485 lines (1,336 loc) • 142 kB
text/typescript
import * as setup from "./utilities"
import { datasourceDescribe } from "../../../integrations/tests/utils"
import tk from "timekeeper"
import emitter from "../../../../src/events"
import { outputProcessing } from "../../../utilities/rowProcessor"
import {
context,
setEnv,
InternalTable,
tenancy,
utils,
} from "@budibase/backend-core"
import { quotas } from "@budibase/pro"
import {
AIOperationEnum,
AutoFieldSubType,
Datasource,
DeleteRow,
FieldSchema,
FieldType,
BBReferenceFieldSubType,
FormulaType,
INTERNAL_TABLE_SOURCE_ID,
QuotaUsageType,
RelationshipType,
Row,
SaveTableRequest,
StaticQuotaName,
Table,
TableSourceType,
UpdatedRowEventEmitter,
TableSchema,
JsonFieldSubType,
RowExportFormat,
RelationSchemaField,
FormulaResponseType,
} from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests"
import _, { merge } from "lodash"
import * as uuid from "uuid"
import { Knex } from "knex"
import { InternalTables } from "../../../db/utils"
import { withEnv } from "../../../environment"
import { JsTimeoutError } from "@budibase/string-templates"
import { isDate } from "../../../utilities"
import nock from "nock"
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/ai/openai"
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
tk.freeze(timestamp)
interface WaitOptions {
name: string
matchFn?: (event: any) => boolean
}
async function waitForEvent(
opts: WaitOptions,
callback: () => Promise<void>
): Promise<any> {
const p = new Promise((resolve: any) => {
const listener = (event: any) => {
if (opts.matchFn && !opts.matchFn(event)) {
return
}
resolve(event)
emitter.off(opts.name, listener)
}
emitter.on(opts.name, listener)
})
await callback()
return await p
}
function encodeJS(binding: string) {
return `{{ js "${Buffer.from(binding).toString("base64")}"}}`
}
const descriptions = datasourceDescribe({ plus: true })
if (descriptions.length) {
describe.each(descriptions)(
"/rows ($dbName)",
({
config,
dsProvider,
isInternal,
isMySQL,
isMariaDB,
isMSSQL,
isOracle,
isSql,
}) => {
let table: Table
let datasource: Datasource | undefined
let client: Knex | undefined
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource
client = ds.client
mocks.licenses.useCloudFree()
})
afterAll(async () => {
setup.afterAll()
})
function saveTableRequest(
// We omit the name field here because it's generated in the function with a
// high likelihood to be unique. Tests should not have any reason to control
// the table name they're writing to.
...overrides: Partial<Omit<SaveTableRequest, "name">>[]
): SaveTableRequest {
const defaultSchema: TableSchema = {
id: {
type: FieldType.NUMBER,
name: "id",
autocolumn: true,
constraints: {
presence: true,
},
},
}
for (const override of overrides) {
if (override.primary) {
delete defaultSchema.id
}
}
const req: SaveTableRequest = {
name: uuid.v4().substring(0, 10),
type: "table",
sourceType: datasource
? TableSourceType.EXTERNAL
: TableSourceType.INTERNAL,
sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID,
primary: ["id"],
schema: defaultSchema,
}
const merged = merge(req, ...overrides)
return merged
}
function defaultTable(
// We omit the name field here because it's generated in the function with a
// high likelihood to be unique. Tests should not have any reason to control
// the table name they're writing to.
...overrides: Partial<Omit<SaveTableRequest, "name">>[]
): SaveTableRequest {
return saveTableRequest(
{
primaryDisplay: "name",
schema: {
name: {
type: FieldType.STRING,
name: "name",
constraints: {
type: "string",
},
},
description: {
type: FieldType.STRING,
name: "description",
constraints: {
type: "string",
},
},
},
},
...overrides
)
}
const getRowUsage = async () => {
const { total } = await config.doInContext(undefined, () =>
quotas.getCurrentUsageValues(
QuotaUsageType.STATIC,
StaticQuotaName.ROWS
)
)
return total
}
async function expectRowUsage(expected: number, f: () => Promise<void>) {
const before = await getRowUsage()
await f()
const after = await getRowUsage()
const usage = after - before
// Because our quota tracking is not perfect, we allow a 10% margin of
// error. This is to account for the fact that parallel writes can
// result in some quota updates getting lost. We don't have any need
// to solve this right now, so we just allow for some error.
if (expected === 0) {
expect(usage).toEqual(0)
return
}
if (usage < 0) {
expect(usage).toBeGreaterThan(expected * 1.1)
expect(usage).toBeLessThan(expected * 0.9)
} else {
expect(usage).toBeGreaterThan(expected * 0.9)
expect(usage).toBeLessThan(expected * 1.1)
}
}
const defaultRowFields = isInternal
? {
type: "row",
createdAt: timestamp,
updatedAt: timestamp,
}
: undefined
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
})
describe("create", () => {
it("creates a new row successfully", async () => {
await expectRowUsage(isInternal ? 1 : 0, async () => {
const row = await config.api.row.save(table._id!, {
name: "Test Contact",
})
expect(row.name).toEqual("Test Contact")
expect(row._rev).toBeDefined()
})
})
it("fails to create a row for a table that does not exist", async () => {
await expectRowUsage(0, async () => {
await config.api.row.save("1234567", {}, { status: 404 })
})
})
it("fails to create a row if required fields are missing", async () => {
await expectRowUsage(0, async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
required: {
type: FieldType.STRING,
name: "required",
constraints: {
type: "string",
presence: true,
},
},
},
})
)
await config.api.row.save(
table._id!,
{},
{
status: 500,
body: {
validationErrors: {
required: ["can't be blank"],
},
},
}
)
})
})
isInternal &&
it("increment row autoId per create row request", async () => {
await expectRowUsage(isInternal ? 10 : 0, async () => {
const newTable = await config.api.table.save(
saveTableRequest({
schema: {
"Row ID": {
name: "Row ID",
type: FieldType.NUMBER,
subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line",
autocolumn: true,
constraints: {
type: "number",
presence: true,
numericality: {
greaterThanOrEqualTo: "",
lessThanOrEqualTo: "",
},
},
},
},
})
)
let previousId = 0
for (let i = 0; i < 10; i++) {
const row = await config.api.row.save(newTable._id!, {})
expect(row["Row ID"]).toBeGreaterThan(previousId)
previousId = row["Row ID"]
}
})
})
isInternal &&
it("should increment auto ID correctly when creating rows in parallel", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
"Row ID": {
name: "Row ID",
type: FieldType.NUMBER,
subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line",
autocolumn: true,
constraints: {
type: "number",
presence: true,
numericality: {
greaterThanOrEqualTo: "",
lessThanOrEqualTo: "",
},
},
},
},
})
)
const sequence = Array(50)
.fill(0)
.map((_, i) => i + 1)
// This block of code is simulating users creating auto ID rows at the
// same time. It's expected that this operation will sometimes return
// a document conflict error (409), but the idea is to retry in those
// situations. The code below does this a large number of times with
// small, random delays between them to try and get through the list
// as quickly as possible.
await Promise.all(
sequence.map(async () => {
const attempts = 30
for (let attempt = 0; attempt < attempts; attempt++) {
try {
await config.api.row.save(table._id!, {})
return
} catch (e) {
await new Promise(r => setTimeout(r, Math.random() * 50))
}
}
throw new Error(
`Failed to create row after ${attempts} attempts`
)
})
)
const rows = await config.api.row.fetch(table._id!)
expect(rows).toHaveLength(50)
// The main purpose of this test is to ensure that even under pressure,
// we maintain data integrity. An auto ID column should hand out
// monotonically increasing unique integers no matter what.
const ids = rows.map(r => r["Row ID"])
expect(ids).toEqual(expect.arrayContaining(sequence))
})
isInternal &&
it("doesn't allow creating in user table", async () => {
const response = await config.api.row.save(
InternalTable.USER_METADATA,
{
firstName: "Joe",
lastName: "Joe",
email: "joe@joe.com",
roles: {},
},
{ status: 400 }
)
expect(response.message).toBe("Cannot create new user entry.")
})
it("should not mis-parse date string out of JSON", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
name: {
type: FieldType.STRING,
name: "name",
},
},
})
)
const row = await config.api.row.save(table._id!, {
name: `{ "foo": "2023-01-26T11:48:57.000Z" }`,
})
expect(row.name).toEqual(`{ "foo": "2023-01-26T11:48:57.000Z" }`)
})
describe("default values", () => {
let table: Table
describe("string column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
description: {
name: "description",
type: FieldType.STRING,
default: "default description",
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.description).toEqual("default description")
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
description: "specified description",
})
expect(row.description).toEqual("specified description")
})
it("uses the default value if value is null", async () => {
const row = await config.api.row.save(table._id!, {
description: null,
})
expect(row.description).toEqual("default description")
})
it("uses the default value if value is undefined", async () => {
const row = await config.api.row.save(table._id!, {
description: undefined,
})
expect(row.description).toEqual("default description")
})
})
describe("number column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
age: {
name: "age",
type: FieldType.NUMBER,
default: "25",
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.age).toEqual(25)
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
age: 30,
})
expect(row.age).toEqual(30)
})
})
describe("date column", () => {
it("creates a row with a default value successfully", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
date: {
name: "date",
type: FieldType.DATETIME,
default: "2023-01-26T11:48:57.000Z",
},
},
})
)
const row = await config.api.row.save(table._id!, {})
expect(row.date).toEqual("2023-01-26T11:48:57.000Z")
})
it("gives an error if the default value is invalid", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
date: {
name: "date",
type: FieldType.DATETIME,
default: "invalid",
},
},
})
)
await config.api.row.save(
table._id!,
{},
{
status: 400,
body: {
message: `Invalid default value for field 'date' - Invalid date value: "invalid"`,
},
}
)
})
})
describe("options column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
status: {
name: "status",
type: FieldType.OPTIONS,
default: "requested",
constraints: {
inclusion: ["requested", "approved"],
},
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.status).toEqual("requested")
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
status: "approved",
})
expect(row.status).toEqual("approved")
})
})
describe("array column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
food: {
name: "food",
type: FieldType.ARRAY,
default: ["apple", "orange"],
constraints: {
type: JsonFieldSubType.ARRAY,
inclusion: ["apple", "orange", "banana"],
},
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.food).toEqual(["apple", "orange"])
})
it("creates a new row with a default value when given an empty list", async () => {
const row = await config.api.row.save(table._id!, { food: [] })
expect(row.food).toEqual(["apple", "orange"])
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
food: ["orange"],
})
expect(row.food).toEqual(["orange"])
})
it("resets back to its default value when empty", async () => {
let row = await config.api.row.save(table._id!, {
food: ["orange"],
})
row = await config.api.row.save(table._id!, { ...row, food: [] })
expect(row.food).toEqual(["apple", "orange"])
})
})
describe("user column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
user: {
name: "user",
type: FieldType.BB_REFERENCE_SINGLE,
subtype: BBReferenceFieldSubType.USER,
default: "{{ [Current User]._id }}",
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.user._id).toEqual(config.getUser()._id)
})
it("does not use default value if value specified", async () => {
const id = `us_${utils.newid()}`
await config.createUser({ _id: id })
const row = await config.api.row.save(table._id!, {
user: id,
})
expect(row.user._id).toEqual(id)
})
})
describe("multi-user column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
users: {
name: "users",
type: FieldType.BB_REFERENCE,
subtype: BBReferenceFieldSubType.USER,
default: ["{{ [Current User]._id }}"],
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.users).toHaveLength(1)
expect(row.users[0]._id).toEqual(config.getUser()._id)
})
it("does not use default value if value specified", async () => {
const id = `us_${utils.newid()}`
await config.createUser({ _id: id })
const row = await config.api.row.save(table._id!, {
users: [id],
})
expect(row.users).toHaveLength(1)
expect(row.users[0]._id).toEqual(id)
})
})
describe("boolean column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
active: {
name: "active",
type: FieldType.BOOLEAN,
default: "true",
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.active).toEqual(true)
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
active: false,
})
expect(row.active).toEqual(false)
})
})
describe("bigint column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
bigNumber: {
name: "bigNumber",
type: FieldType.BIGINT,
default: "1234567890",
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.bigNumber).toEqual("1234567890")
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
bigNumber: "9876543210",
})
expect(row.bigNumber).toEqual("9876543210")
})
})
describe("bindings", () => {
describe("string column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
description: {
name: "description",
type: FieldType.STRING,
default: `{{ date now "YYYY-MM-DDTHH:mm:ss" }}`,
},
},
})
)
})
it("can use bindings in default values", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.description).toMatch(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
)
})
it("does not use default value if value specified", async () => {
const row = await config.api.row.save(table._id!, {
description: "specified description",
})
expect(row.description).toEqual("specified description")
})
it("can bind the current user", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
user: {
name: "user",
type: FieldType.STRING,
default: `{{ [Current User]._id }}`,
},
},
})
)
const row = await config.api.row.save(table._id!, {})
expect(row.user).toEqual(config.getUser()._id)
})
it("cannot access current user password", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
user: {
name: "user",
type: FieldType.STRING,
default: `{{ user.password }}`,
},
},
})
)
const row = await config.api.row.save(table._id!, {})
// For some reason it's null for internal tables, and undefined for
// external.
expect(row.user == null).toBe(true)
})
})
describe("number column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
age: {
name: "age",
type: FieldType.NUMBER,
default: `{{ sum 10 10 5 }}`,
},
},
})
)
})
it("can use bindings in default values", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.age).toEqual(25)
})
describe("invalid default value", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
age: {
name: "age",
type: FieldType.NUMBER,
default: `{{ capitalize "invalid" }}`,
},
},
})
)
})
it("throws an error when invalid default value", async () => {
await config.api.row.save(
table._id!,
{},
{
status: 400,
body: {
message:
"Invalid default value for field 'age' - Invalid number value \"Invalid\"",
},
}
)
})
})
})
})
})
describe("relations to same table", () => {
let relatedRows: Row[]
beforeAll(async () => {
const relatedTable = await config.api.table.save(
defaultTable({
schema: {
name: { name: "name", type: FieldType.STRING },
},
})
)
const relatedTableId = relatedTable._id!
table = await config.api.table.save(
defaultTable({
schema: {
name: { name: "name", type: FieldType.STRING },
related1: {
type: FieldType.LINK,
name: "related1",
fieldName: "main1",
tableId: relatedTableId,
relationshipType: RelationshipType.MANY_TO_MANY,
},
related2: {
type: FieldType.LINK,
name: "related2",
fieldName: "main2",
tableId: relatedTableId,
relationshipType: RelationshipType.MANY_TO_MANY,
},
},
})
)
relatedRows = await Promise.all([
config.api.row.save(relatedTableId, { name: "foo" }),
config.api.row.save(relatedTableId, { name: "bar" }),
config.api.row.save(relatedTableId, { name: "baz" }),
config.api.row.save(relatedTableId, { name: "boo" }),
])
})
it("can create rows with both relationships", async () => {
const row = await config.api.row.save(table._id!, {
name: "test",
related1: [relatedRows[0]._id!],
related2: [relatedRows[1]._id!],
})
expect(row).toEqual(
expect.objectContaining({
name: "test",
related1: [
{
_id: relatedRows[0]._id,
primaryDisplay: relatedRows[0].name,
},
],
related2: [
{
_id: relatedRows[1]._id,
primaryDisplay: relatedRows[1].name,
},
],
})
)
})
it("can create rows with no relationships", async () => {
const row = await config.api.row.save(table._id!, {
name: "test",
})
expect(row.related1).toBeUndefined()
expect(row.related2).toBeUndefined()
})
it("can create rows with only one relationships field", async () => {
const row = await config.api.row.save(table._id!, {
name: "test",
related1: [],
related2: [relatedRows[1]._id!],
})
expect(row).toEqual(
expect.objectContaining({
name: "test",
related2: [
{
_id: relatedRows[1]._id,
primaryDisplay: relatedRows[1].name,
},
],
})
)
expect(row.related1).toBeUndefined()
})
})
})
describe("get", () => {
it("reads an existing row successfully", async () => {
const existing = await config.api.row.save(table._id!, {
name: "foo",
})
const res = await config.api.row.get(table._id!, existing._id!)
expect(res).toEqual({
...existing,
...defaultRowFields,
})
})
it("returns 404 when row does not exist", async () => {
const table = await config.api.table.save(defaultTable())
await config.api.row.save(table._id!, { name: "foo" })
await config.api.row.get(table._id!, "1234567", {
status: 404,
})
})
isInternal &&
it("can search row from user table", async () => {
const res = await config.api.row.get(
InternalTables.USER_METADATA,
config.userMetadataId!
)
expect(res).toEqual({
...config.getUser(),
_id: config.userMetadataId!,
_rev: expect.any(String),
roles: undefined,
roleId: "ADMIN",
tableId: InternalTables.USER_METADATA,
})
})
})
describe("fetch", () => {
it("fetches all rows for given tableId", async () => {
const table = await config.api.table.save(defaultTable())
const rows = await Promise.all([
config.api.row.save(table._id!, { name: "foo" }),
config.api.row.save(table._id!, { name: "bar" }),
])
const res = await config.api.row.fetch(table._id!)
expect(res.map(r => r._id)).toEqual(
expect.arrayContaining(rows.map(r => r._id))
)
})
it("returns 404 when table does not exist", async () => {
await config.api.row.fetch("1234567", { status: 404 })
})
})
describe("update", () => {
it("updates an existing row successfully", async () => {
const existing = await config.api.row.save(table._id!, {
name: "foo",
})
await expectRowUsage(0, async () => {
const res = await config.api.row.save(table._id!, {
_id: existing._id,
_rev: existing._rev,
name: "Updated Name",
})
expect(res.name).toEqual("Updated Name")
})
})
!isInternal &&
it("can update a row on an external table with a primary key", async () => {
const tableName = uuid.v4().substring(0, 10)
await client!.schema.createTable(tableName, table => {
table.increments("id").primary()
table.string("name")
})
const res = await config.api.datasource.fetchSchema({
datasourceId: datasource!._id!,
})
const table = res.datasource.entities![tableName]
const row = await config.api.row.save(table._id!, {
id: 1,
name: "Row 1",
})
const updatedRow = await config.api.row.save(table._id!, {
_id: row._id!,
name: "Row 1 Updated",
})
expect(updatedRow.name).toEqual("Row 1 Updated")
const rows = await config.api.row.fetch(table._id!)
expect(rows).toHaveLength(1)
})
describe("relations to same table", () => {
let relatedRows: Row[]
beforeAll(async () => {
const relatedTable = await config.api.table.save(
defaultTable({
schema: {
name: { name: "name", type: FieldType.STRING },
},
})
)
const relatedTableId = relatedTable._id!
table = await config.api.table.save(
defaultTable({
schema: {
name: { name: "name", type: FieldType.STRING },
related1: {
type: FieldType.LINK,
name: "related1",
fieldName: "main1",
tableId: relatedTableId,
relationshipType: RelationshipType.MANY_TO_MANY,
},
related2: {
type: FieldType.LINK,
name: "related2",
fieldName: "main2",
tableId: relatedTableId,
relationshipType: RelationshipType.MANY_TO_MANY,
},
},
})
)
relatedRows = await Promise.all([
config.api.row.save(relatedTableId, { name: "foo" }),
config.api.row.save(relatedTableId, { name: "bar" }),
config.api.row.save(relatedTableId, { name: "baz" }),
config.api.row.save(relatedTableId, { name: "boo" }),
])
})
it("can edit rows with both relationships", async () => {
let row = await config.api.row.save(table._id!, {
name: "test",
related1: [relatedRows[0]._id!],
related2: [relatedRows[1]._id!],
})
row = await config.api.row.save(table._id!, {
...row,
related1: [relatedRows[0]._id!, relatedRows[1]._id!],
related2: [relatedRows[2]._id!],
})
expect(row).toEqual(
expect.objectContaining({
name: "test",
related1: expect.arrayContaining([
{
_id: relatedRows[0]._id,
primaryDisplay: relatedRows[0].name,
},
{
_id: relatedRows[1]._id,
primaryDisplay: relatedRows[1].name,
},
]),
related2: [
{
_id: relatedRows[2]._id,
primaryDisplay: relatedRows[2].name,
},
],
})
)
})
it("can drop existing relationship", async () => {
let row = await config.api.row.save(table._id!, {
name: "test",
related1: [relatedRows[0]._id!],
related2: [relatedRows[1]._id!],
})
row = await config.api.row.save(table._id!, {
...row,
related1: [],
related2: [relatedRows[2]._id!],
})
expect(row).toEqual(
expect.objectContaining({
name: "test",
related2: [
{
_id: relatedRows[2]._id,
primaryDisplay: relatedRows[2].name,
},
],
})
)
expect(row.related1).toBeUndefined()
})
it("can drop both relationships", async () => {
let row = await config.api.row.save(table._id!, {
name: "test",
related1: [relatedRows[0]._id!],
related2: [relatedRows[1]._id!],
})
row = await config.api.row.save(table._id!, {
...row,
related1: [],
related2: [],
})
expect(row).toEqual(
expect.objectContaining({
name: "test",
})
)
expect(row.related1).toBeUndefined()
expect(row.related2).toBeUndefined()
})
})
isSql &&
describe("date", () => {
it("should be able to write back a date fetched directly from the DB", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
date: {
name: "date",
type: FieldType.DATETIME,
dateOnly: true,
},
},
})
)
const row = await config.api.row.save(table._id!, {
date: "2023-01-26",
})
const rawRows = await client!(table.name)
.select("*")
.where({ id: row.id })
await config.api.row.save(table._id!, {
...row,
date: rawRows[0].date,
})
const fetchedRow = await config.api.row.get(table._id!, row._id!)
expect(fetchedRow.date).toEqual("2023-01-26")
})
})
})
describe("patch", () => {
let otherTable: Table
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
otherTable = await config.api.table.save(
defaultTable({
schema: {
relationship: {
name: "relationship",
relationshipType: RelationshipType.ONE_TO_MANY,
type: FieldType.LINK,
tableId: table._id!,
fieldName: "relationship",
},
},
})
)
})
it("should update only the fields that are supplied", async () => {
const existing = await config.api.row.save(table._id!, {
name: "foo",
})
await expectRowUsage(0, async () => {
const row = await config.api.row.patch(table._id!, {
_id: existing._id!,
_rev: existing._rev!,
tableId: table._id!,
name: "Updated Name",
})
expect(row.name).toEqual("Updated Name")
expect(row.description).toEqual(existing.description)
const savedRow = await config.api.row.get(table._id!, row._id!)
expect(savedRow.description).toEqual(existing.description)
expect(savedRow.name).toEqual("Updated Name")
})
})
it("should not require the primary display", async () => {
const existing = await config.api.row.save(table._id!, {
name: "foo",
description: "bar",
})
await expectRowUsage(0, async () => {
const row = await config.api.row.patch(table._id!, {
_id: existing._id!,
_rev: existing._rev!,
tableId: table._id!,
description: "baz",
})
expect(row.description).toEqual("baz")
})
})
it("should update only the fields that are supplied and emit the correct oldRow", async () => {
let beforeRow = await config.api.row.save(table._id!, {
name: "test",
description: "test",
})
const opts = {
name: "row:update",
matchFn: (event: UpdatedRowEventEmitter) =>
event.row._id === beforeRow._id,
}
const event = await waitForEvent(opts, async () => {
await config.api.row.patch(table._id!, {
_id: beforeRow._id!,
_rev: beforeRow._rev!,
tableId: table._id!,
name: "Updated Name",
})
})
expect(event.oldRow).toBeDefined()
expect(event.oldRow.name).toEqual("test")
expect(event.row.name).toEqual("Updated Name")
expect(event.oldRow.description).toEqual(beforeRow.description)
expect(event.row.description).toEqual(beforeRow.description)
})
it("should throw an error when given improper types", async () => {
const existing = await config.api.row.save(table._id!, {
name: "foo",
})
await expectRowUsage(0, async () => {
await config.api.row.patch(
table._id!,
{
_id: existing._id!,
_rev: existing._rev!,
tableId: table._id!,
name: 1,
},
{ status: 400 }
)
})
})
it("should not overwrite links if those links are not set", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.save({
name: "TestTable",
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: "user1" },
user2: { ...linkField, name: "user2", fieldName: "user2" },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.user1[0]._id).toEqual(user1._id)
expect(getResp.user2[0]._id).toEqual(user2._id)
let patchResp = await config.api.row.patch(table._id!, {
_id: row._id!,
_rev: row._rev!,
tableId: table._id!,
user1: [{ _id: user2._id }],
})
expect(patchResp.user1[0]._id).toEqual(user2._id)
expect(patchResp.user2[0]._id).toEqual(user2._id)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.user1[0]._id).toEqual(user2._id)
expect(getResp.user2[0]._id).toEqual(user2._id)
})
it("should be able to remove a relationship from many side", async () => {
const row = await config.api.row.save(otherTable._id!, {
name: "test",
description: "test",
})
const row2 = await config.api.row.save(otherTable._id!, {
name: "test",
description: "test",
})
const { _id } = await config.api.row.save(table._id!, {
name: "test",
relationship: [{ _id: row._id }, { _id: row2._id }],
})
const relatedRow = await config.api.row.get(table._id!, _id!, {
status: 200,
})
expect(relatedRow.relationship.length).toEqual(2)
await config.api.row.save(table._id!, {
...relatedRow,
relationship: [{ _id: row._id }],
})
const afterRelatedRow = await config.api.row.get(table._id!, _id!, {
status: 200,
})
expect(afterRelatedRow.relationship.length).toEqual(1)
expect(afterRelatedRow.relationship[0]._id).toEqual(row._id)
})
it("should be able to update relationships when both columns are same name", async () => {
let row = await config.api.row.save(table._id!, {
name: "test",
description: "test",
})
let row2 = await config.api.row.save(otherTable._id!, {
name: "test",
description: "test",
relationship: [row._id],
})
row = await config.api.row.get(table._id!, row._id!)
expect(row.relationship.length).toBe(1)
const resp = await config.api.row.patch(table._id!, {
_id: row._id!,
_rev: row._rev!,
tableId: row.tableId!,
name: "test2",
relationship: [row2._id],
})
expect(resp.relationship.length).toBe(1)
})
it("should be able to keep linked data when updating from views that trims links from the main table", async () => {
let row = await config.api.row.save(table._id!, {
name: "main",
description: "main description",
})
const row2 = await config.api.row.save(otherTable._id!, {
name: "link",
description: "link description",
relationship: [row._id],
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: "view",
schema: {
name: { visible: true },
},
})
const resp = await config.api.row.patch(view.id, {
_id: row._id!,
_rev: row._rev!,
tableId: row.tableId!,
name: "test2",
relationship: [row2._id],
})
expect(resp.relationship).toBeUndefined()
const updatedRow = await config.api.row.get(table._id!, row._id!)
expect(updatedRow.relationship.length).toBe(1)
})
it("should be able to keep linked data when updating from views that trims links from the foreign table", async () => {
let row = await config.api.row.save(table._id!, {
name: "main",
description: "main description",
})
const row2 = await config.api.row.save(otherTable._id!, {
name: "link",
description: "link description",
relationship: [row._id],
})
const view = await config.api.viewV2.create({
tableId: otherTable._id!,
name: "view",
})
await config.api.row.patch(view.id, {
_id: row2._id!,
_rev: row2._rev!,
tableId: row2.tableId!,
})
const updatedRow = await config.api.row.get(table._id!, row._id!)
expect(updatedRow.relationship.length).toBe(1)
})
!isInternal &&
// MSSQL needs a setting called IDENTITY_INSERT to be set to ON to allow writing
// to identity columns. This is not something Budibase does currently.
!isMSSQL &&
it("should support updating fields that are part of a composite key", async () => {
const tableRequest = saveTableRequest({
primary: ["number", "string"],
schema: {
string: {
type: FieldType.STRING,
name: "string",
},
number: {
type: FieldType.NUMBER,
name: "number",
},
},
})
delete tableRequest.schema.id
const table = await config.api.table.save(tableRequest)
const stringValue = generator.word()
// MySQL and MariaDB auto-increment fields have a minimum value of 1. If
// you try to save a row with a value of 0 it will use 1 instead.
const naturalValue = generator.integer({ min: 1, max: 1000 })
const existing = await config.api.row.save(table._id!, {
stri