UNPKG

@budibase/server

Version:
1,485 lines (1,336 loc) • 142 kB
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