budibase/packages/server/src/api/routes/tests/row.spec.ts

1451 lines
43 KiB
TypeScript
Raw Normal View History

2023-09-06 12:51:08 +02:00
import { databaseTestProviders } from "../../../integrations/tests/utils"
2023-07-13 12:17:24 +02:00
import tk from "timekeeper"
import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities"
2024-03-20 17:06:22 +01:00
import { context, InternalTable, tenancy } from "@budibase/backend-core"
2023-07-13 12:17:24 +02:00
import { quotas } from "@budibase/pro"
import {
AutoFieldSubType,
Datasource,
2024-03-01 18:03:34 +01:00
DeleteRow,
2023-10-06 09:12:45 +02:00
FieldSchema,
2023-08-11 16:00:50 +02:00
FieldType,
2023-09-27 16:51:42 +02:00
FieldTypeSubtypes,
FormulaType,
INTERNAL_TABLE_SOURCE_ID,
2023-08-11 16:00:50 +02:00
QuotaUsageType,
2023-09-13 14:09:48 +02:00
RelationshipType,
2023-07-13 12:17:24 +02:00
Row,
SaveTableRequest,
2023-08-11 16:00:50 +02:00
StaticQuotaName,
Table,
TableSourceType,
2023-07-13 12:17:24 +02:00
} from "@budibase/types"
2024-03-20 17:06:22 +01:00
import { generator, mocks } from "@budibase/backend-core/tests"
import _, { merge } from "lodash"
import * as uuid from "uuid"
2023-08-11 16:00:50 +02:00
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
tk.freeze(timestamp)
jest.unmock("mssql")
jest.unmock("pg")
2023-09-05 12:09:11 +02:00
describe.each([
2024-03-12 15:46:52 +01:00
["internal", undefined],
["postgres", databaseTestProviders.postgres],
["mysql", databaseTestProviders.mysql],
["mssql", databaseTestProviders.mssql],
2024-03-12 16:27:34 +01:00
["mariadb", databaseTestProviders.mariadb],
2023-09-27 17:35:16 +02:00
])("/rows (%s)", (__, dsProvider) => {
const isInternal = dsProvider === undefined
2023-09-13 09:57:41 +02:00
const config = setup.getConfig()
2020-04-22 17:35:20 +02:00
let table: Table
let datasource: Datasource | undefined
2020-04-22 17:35:20 +02:00
2023-02-03 18:55:40 +01:00
beforeAll(async () => {
await config.init()
2023-09-05 12:09:11 +02:00
if (dsProvider) {
datasource = await config.createDatasource({
datasource: await dsProvider.datasource(),
2023-09-12 19:18:19 +02:00
})
2023-09-05 12:09:11 +02:00
}
2023-09-12 19:18:19 +02:00
})
2023-09-08 15:46:10 +02:00
afterAll(async () => {
if (dsProvider) {
await dsProvider.stop()
}
setup.afterAll()
})
function saveTableRequest(
...overrides: Partial<SaveTableRequest>[]
): SaveTableRequest {
const req: SaveTableRequest = {
name: uuid.v4().substring(0, 16),
2023-09-08 15:46:10 +02:00
type: "table",
sourceType: datasource
? TableSourceType.EXTERNAL
: TableSourceType.INTERNAL,
sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID,
2023-09-08 15:46:10 +02:00
primary: ["id"],
schema: {
id: {
type: FieldType.AUTO,
name: "id",
autocolumn: true,
constraints: {
presence: true,
},
},
},
}
return merge(req, ...overrides)
}
function defaultTable(
...overrides: Partial<SaveTableRequest>[]
): SaveTableRequest {
return saveTableRequest(
{
primaryDisplay: "name",
schema: {
name: {
type: FieldType.STRING,
name: "name",
constraints: {
type: "string",
},
2023-09-08 15:46:10 +02:00
},
description: {
type: FieldType.STRING,
name: "description",
constraints: {
type: "string",
},
2023-09-08 15:46:10 +02:00
},
},
},
...overrides
)
2023-09-08 15:46:10 +02:00
}
2023-09-05 12:09:11 +02:00
2023-09-08 15:46:10 +02:00
beforeEach(async () => {
mocks.licenses.useCloudFree()
2020-05-27 18:23:01 +02:00
})
2020-04-24 19:02:51 +02:00
2022-05-11 12:32:53 +02:00
const getRowUsage = async () => {
2024-02-28 12:20:42 +01:00
const { total } = await config.doInContext(undefined, () =>
2023-07-13 12:17:24 +02:00
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
)
2022-09-28 12:57:58 +02:00
return total
2022-05-11 12:32:53 +02:00
}
2023-07-13 12:17:24 +02:00
const assertRowUsage = async (expected: number) => {
2022-05-11 12:32:53 +02:00
const usage = await getRowUsage()
expect(usage).toBe(expected)
}
2023-09-08 15:29:05 +02:00
const defaultRowFields = isInternal
? {
type: "row",
createdAt: timestamp,
updatedAt: timestamp,
2023-09-08 10:04:49 +02:00
}
2023-09-08 15:29:05 +02:00
: undefined
2023-09-08 10:04:49 +02:00
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
2023-09-12 21:09:25 +02:00
})
2023-09-12 20:40:00 +02:00
describe("create", () => {
it("creates a new row successfully", async () => {
const rowUsage = await getRowUsage()
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(table._id!, {
name: "Test Contact",
})
expect(row.name).toEqual("Test Contact")
expect(row._rev).toBeDefined()
await assertRowUsage(rowUsage + 1)
2020-04-22 17:35:20 +02:00
})
it("fails to create a row for a table that does not exist", async () => {
const rowUsage = await getRowUsage()
await config.api.row.save("1234567", {}, { status: 404 })
await assertRowUsage(rowUsage)
})
it("fails to create a row if required fields are missing", async () => {
const rowUsage = await getRowUsage()
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"],
},
},
}
)
await assertRowUsage(rowUsage)
})
it("increment row autoId per create row request", async () => {
2023-05-15 20:22:22 +02:00
const rowUsage = await getRowUsage()
const newTable = await config.api.table.save(
saveTableRequest({
2023-09-12 20:40:00 +02:00
name: "TestTableAuto",
schema: {
"Row ID": {
name: "Row ID",
type: FieldType.NUMBER,
subtype: AutoFieldSubType.AUTO_ID,
2023-09-12 20:40:00 +02:00
icon: "ri-magic-line",
autocolumn: true,
constraints: {
type: "number",
presence: true,
numericality: {
greaterThanOrEqualTo: "",
lessThanOrEqualTo: "",
},
2023-05-15 20:22:22 +02:00
},
},
},
2023-09-13 09:57:41 +02:00
})
)
2023-05-15 20:22:22 +02:00
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"]
2023-05-15 20:22:22 +02:00
}
await assertRowUsage(rowUsage + 10)
2023-05-15 20:22:22 +02:00
})
2023-09-12 11:02:44 +02:00
isInternal &&
it("row values are coerced", async () => {
2023-10-06 09:12:45 +02:00
const str: FieldSchema = {
2023-09-12 11:02:44 +02:00
type: FieldType.STRING,
name: "str",
constraints: { type: "string", presence: false },
}
2023-10-06 09:12:45 +02:00
const attachment: FieldSchema = {
2023-09-12 11:02:44 +02:00
type: FieldType.ATTACHMENT,
name: "attachment",
constraints: { type: "array", presence: false },
}
2023-10-06 09:12:45 +02:00
const bool: FieldSchema = {
2023-09-12 11:02:44 +02:00
type: FieldType.BOOLEAN,
name: "boolean",
constraints: { type: "boolean", presence: false },
}
2023-10-06 09:12:45 +02:00
const number: FieldSchema = {
2023-09-12 11:02:44 +02:00
type: FieldType.NUMBER,
name: "str",
constraints: { type: "number", presence: false },
}
2023-10-06 09:12:45 +02:00
const datetime: FieldSchema = {
2023-09-12 11:02:44 +02:00
type: FieldType.DATETIME,
name: "datetime",
2023-07-13 12:17:24 +02:00
constraints: {
type: "string",
presence: false,
2023-09-12 11:02:44 +02:00
datetime: { earliest: "", latest: "" },
2023-07-13 12:17:24 +02:00
},
2023-09-12 11:02:44 +02:00
}
2023-10-06 09:12:45 +02:00
const arrayField: FieldSchema = {
2023-09-12 11:02:44 +02:00
type: FieldType.ARRAY,
constraints: {
type: "array",
presence: false,
inclusion: ["One", "Two", "Three"],
2023-07-13 12:17:24 +02:00
},
2023-09-12 11:02:44 +02:00
name: "Sample Tags",
sortable: false,
}
2023-10-06 09:12:45 +02:00
const optsField: FieldSchema = {
2023-09-12 20:40:00 +02:00
name: "Sample Opts",
type: FieldType.OPTIONS,
constraints: {
type: "string",
presence: false,
inclusion: ["Alpha", "Beta", "Gamma"],
2023-09-12 11:02:44 +02:00
},
2023-09-12 20:40:00 +02:00
}
2024-03-13 17:32:33 +01:00
const table = await config.api.table.save(
saveTableRequest({
schema: {
name: str,
stringUndefined: str,
stringNull: str,
stringString: str,
numberEmptyString: number,
numberNull: number,
numberUndefined: number,
numberString: number,
numberNumber: number,
datetimeEmptyString: datetime,
datetimeNull: datetime,
datetimeUndefined: datetime,
datetimeString: datetime,
datetimeDate: datetime,
boolNull: bool,
boolEmpty: bool,
boolUndefined: bool,
boolString: bool,
boolBool: bool,
attachmentNull: attachment,
attachmentUndefined: attachment,
attachmentEmpty: attachment,
attachmentEmptyArrayStr: attachment,
arrayFieldEmptyArrayStr: arrayField,
arrayFieldArrayStrKnown: arrayField,
arrayFieldNull: arrayField,
arrayFieldUndefined: arrayField,
optsFieldEmptyStr: optsField,
optsFieldUndefined: optsField,
optsFieldNull: optsField,
optsFieldStrKnown: optsField,
},
})
)
2024-03-13 17:32:33 +01:00
const datetimeStr = "1984-04-20T00:00:00.000Z"
const row = await config.api.row.save(table._id!, {
2023-09-12 11:02:44 +02:00
name: "Test Row",
stringUndefined: undefined,
stringNull: null,
stringString: "i am a string",
numberEmptyString: "",
numberNull: null,
numberUndefined: undefined,
numberString: "123",
numberNumber: 123,
datetimeEmptyString: "",
datetimeNull: null,
datetimeUndefined: undefined,
2024-03-13 17:32:33 +01:00
datetimeString: datetimeStr,
datetimeDate: new Date(datetimeStr),
2023-09-12 11:02:44 +02:00
boolNull: null,
boolEmpty: "",
boolUndefined: undefined,
boolString: "true",
boolBool: true,
tableId: table._id,
attachmentNull: null,
attachmentUndefined: undefined,
attachmentEmpty: "",
attachmentEmptyArrayStr: "[]",
arrayFieldEmptyArrayStr: "[]",
arrayFieldUndefined: undefined,
arrayFieldNull: null,
arrayFieldArrayStrKnown: "['One']",
optsFieldEmptyStr: "",
optsFieldUndefined: undefined,
optsFieldNull: null,
optsFieldStrKnown: "Alpha",
2024-03-13 17:32:33 +01:00
})
2023-09-12 11:02:44 +02:00
2024-03-13 17:32:33 +01:00
expect(row.stringUndefined).toBe(undefined)
expect(row.stringNull).toBe(null)
expect(row.stringString).toBe("i am a string")
expect(row.numberEmptyString).toBe(null)
expect(row.numberNull).toBe(null)
expect(row.numberUndefined).toBe(undefined)
expect(row.numberString).toBe(123)
expect(row.numberNumber).toBe(123)
expect(row.datetimeEmptyString).toBe(null)
expect(row.datetimeNull).toBe(null)
expect(row.datetimeUndefined).toBe(undefined)
expect(row.datetimeString).toBe(new Date(datetimeStr).toISOString())
expect(row.datetimeDate).toBe(new Date(datetimeStr).toISOString())
expect(row.boolNull).toBe(null)
expect(row.boolEmpty).toBe(null)
expect(row.boolUndefined).toBe(undefined)
expect(row.boolString).toBe(true)
expect(row.boolBool).toBe(true)
expect(row.attachmentNull).toEqual([])
expect(row.attachmentUndefined).toBe(undefined)
expect(row.attachmentEmpty).toEqual([])
expect(row.attachmentEmptyArrayStr).toEqual([])
expect(row.arrayFieldEmptyArrayStr).toEqual([])
expect(row.arrayFieldNull).toEqual([])
expect(row.arrayFieldUndefined).toEqual(undefined)
expect(row.optsFieldEmptyStr).toEqual(null)
expect(row.optsFieldUndefined).toEqual(undefined)
expect(row.optsFieldNull).toEqual(null)
expect(row.arrayFieldArrayStrKnown).toEqual(["One"])
expect(row.optsFieldStrKnown).toEqual("Alpha")
2023-09-12 11:02:44 +02:00
})
isInternal &&
it("doesn't allow creating in user table", async () => {
const userTableId = InternalTable.USER_METADATA
const response = await config.api.row.save(
userTableId,
{
tableId: userTableId,
firstName: "Joe",
lastName: "Joe",
email: "joe@joe.com",
roles: {},
},
{ status: 400 }
)
expect(response.message).toBe("Cannot create new user entry.")
})
2020-04-22 17:35:20 +02:00
})
2020-05-28 16:39:29 +02:00
describe("get", () => {
it("reads an existing row successfully", async () => {
const existing = await config.api.row.save(table._id!, {})
2023-08-11 16:00:50 +02:00
const res = await config.api.row.get(table._id!, existing._id!)
expect(res).toEqual({
...existing,
...defaultRowFields,
2023-08-11 16:00:50 +02:00
})
})
2023-08-11 16:00:50 +02:00
it("returns 404 when row does not exist", async () => {
const table = await config.api.table.save(defaultTable())
await config.api.row.save(table._id!, {})
await config.api.row.get(table._id!, "1234567", {
status: 404,
})
})
})
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!, {}),
config.api.row.save(table._id!, {}),
])
const res = await config.api.row.fetch(table._id!)
expect(res.map(r => r._id)).toEqual(
expect.arrayContaining(rows.map(r => r._id))
2023-09-12 09:52:46 +02:00
)
})
2023-08-11 16:00:50 +02:00
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!, {})
const rowUsage = await getRowUsage()
const res = await config.api.row.save(table._id!, {
_id: existing._id,
_rev: existing._rev,
name: "Updated Name",
2023-09-12 09:52:46 +02:00
})
expect(res.name).toEqual("Updated Name")
await assertRowUsage(rowUsage)
2023-08-11 16:00:50 +02:00
})
})
2020-09-10 10:36:14 +02:00
describe("patch", () => {
let otherTable: Table
2023-09-12 20:49:47 +02:00
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
otherTable = await config.api.table.save(
defaultTable({
name: "a",
schema: {
relationship: {
name: "relationship",
relationshipType: RelationshipType.ONE_TO_MANY,
type: FieldType.LINK,
tableId: table._id!,
fieldName: "relationship",
},
},
})
)
2023-09-12 20:47:06 +02:00
})
2020-09-10 10:36:14 +02:00
it("should update only the fields that are supplied", async () => {
const existing = await config.api.row.save(table._id!, {})
2020-09-10 10:36:14 +02:00
2022-05-11 12:32:53 +02:00
const rowUsage = await getRowUsage()
2023-11-03 13:53:37 +01:00
const row = await config.api.row.patch(table._id!, {
2023-07-26 13:53:35 +02:00
_id: existing._id!,
_rev: existing._rev!,
tableId: table._id!,
2023-07-26 12:29:19 +02:00
name: "Updated Name",
})
2022-09-28 09:56:45 +02:00
2023-11-03 13:53:37 +01:00
expect(row.name).toEqual("Updated Name")
expect(row.description).toEqual(existing.description)
2020-09-10 10:36:14 +02:00
2024-03-01 17:03:52 +01:00
const savedRow = await config.api.row.get(table._id!, row._id!)
2020-09-10 10:36:14 +02:00
2024-03-01 16:20:07 +01:00
expect(savedRow.description).toEqual(existing.description)
expect(savedRow.name).toEqual("Updated Name")
2022-05-11 12:32:53 +02:00
await assertRowUsage(rowUsage)
})
it("should throw an error when given improper types", async () => {
const existing = await config.api.row.save(table._id!, {})
2022-05-11 12:32:53 +02:00
const rowUsage = await getRowUsage()
2023-07-26 12:29:19 +02:00
await config.api.row.patch(
table._id!,
{
2023-07-26 13:53:35 +02:00
_id: existing._id!,
_rev: existing._rev!,
tableId: table._id!,
name: 1,
2023-07-26 12:29:19 +02:00
},
2024-03-01 18:03:34 +01:00
{ status: 400 }
2023-07-26 12:29:19 +02:00
)
2022-05-11 12:32:53 +02:00
await assertRowUsage(rowUsage)
})
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,
}
2024-01-26 10:56:38 +01:00
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!)
2024-03-01 16:20:07 +01:00
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!)
2024-03-01 16:20:07 +01:00
expect(getResp.user1[0]._id).toEqual(user2._id)
expect(getResp.user2[0]._id).toEqual(user2._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],
})
2024-03-01 16:20:07 +01:00
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)
})
})
describe("destroy", () => {
2023-09-12 20:49:47 +02:00
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
2023-09-12 20:47:06 +02:00
})
it("should be able to delete a row", async () => {
const createdRow = await config.api.row.save(table._id!, {})
2022-05-11 12:32:53 +02:00
const rowUsage = await getRowUsage()
2024-03-01 18:11:19 +01:00
const res = await config.api.row.bulkDelete(table._id!, {
2024-03-01 18:03:34 +01:00
rows: [createdRow],
})
expect(res[0]._id).toEqual(createdRow._id)
2022-09-28 09:56:45 +02:00
await assertRowUsage(rowUsage - 1)
2020-09-10 10:36:14 +02:00
})
it("should be able to bulk delete rows, including a row that doesn't exist", async () => {
const createdRow = await config.api.row.save(table._id!, {})
const res = await config.api.row.bulkDelete(table._id!, {
rows: [createdRow, { _id: "9999999" }],
})
expect(res[0]._id).toEqual(createdRow._id)
expect(res.length).toEqual(1)
})
2020-09-10 10:36:14 +02:00
})
2020-05-28 16:39:29 +02:00
describe("validate", () => {
2023-09-12 20:49:47 +02:00
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
2023-09-12 20:47:06 +02:00
})
it("should return no errors on valid row", async () => {
2022-05-11 12:32:53 +02:00
const rowUsage = await getRowUsage()
2023-09-13 09:57:41 +02:00
const res = await config.api.row.validate(table._id!, { name: "ivan" })
2022-09-28 09:56:45 +02:00
2023-09-13 09:57:41 +02:00
expect(res.valid).toBe(true)
expect(Object.keys(res.errors)).toEqual([])
2022-05-11 12:32:53 +02:00
await assertRowUsage(rowUsage)
2020-05-28 16:39:29 +02:00
})
it("should errors on invalid row", async () => {
2022-05-11 12:32:53 +02:00
const rowUsage = await getRowUsage()
2023-09-13 09:57:41 +02:00
const res = await config.api.row.validate(table._id!, { name: 1 })
2022-09-28 09:56:45 +02:00
2023-09-08 10:22:24 +02:00
if (isInternal) {
2023-09-13 09:57:41 +02:00
expect(res.valid).toBe(false)
expect(Object.keys(res.errors)).toEqual(["name"])
2023-09-08 10:22:24 +02:00
} else {
// Validation for external is not implemented, so it will always return valid
2023-09-13 09:57:41 +02:00
expect(res.valid).toBe(true)
expect(Object.keys(res.errors)).toEqual([])
2023-09-08 10:22:24 +02:00
}
2022-05-11 12:32:53 +02:00
await assertRowUsage(rowUsage)
2020-05-28 16:39:29 +02:00
})
})
describe("bulkDelete", () => {
2023-09-12 21:09:25 +02:00
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
2023-09-12 20:47:06 +02:00
})
it("should be able to delete a bulk set of rows", async () => {
const row1 = await config.api.row.save(table._id!, {})
const row2 = await config.api.row.save(table._id!, {})
2022-05-11 12:32:53 +02:00
const rowUsage = await getRowUsage()
2024-03-01 18:11:19 +01:00
const res = await config.api.row.bulkDelete(table._id!, {
2024-03-01 18:03:34 +01:00
rows: [row1, row2],
})
2022-05-11 12:32:53 +02:00
2024-03-01 18:03:34 +01:00
expect(res.length).toEqual(2)
2024-03-01 17:03:52 +01:00
await config.api.row.get(table._id!, row1._id!, { status: 404 })
2022-05-11 12:32:53 +02:00
await assertRowUsage(rowUsage - 2)
})
2023-07-24 10:08:10 +02:00
it("should be able to delete a variety of row set types", async () => {
2023-09-13 09:37:11 +02:00
const [row1, row2, row3] = await Promise.all([
config.api.row.save(table._id!, {}),
config.api.row.save(table._id!, {}),
config.api.row.save(table._id!, {}),
2023-09-13 09:37:11 +02:00
])
2023-07-24 10:08:10 +02:00
const rowUsage = await getRowUsage()
2024-03-01 18:11:19 +01:00
const res = await config.api.row.bulkDelete(table._id!, {
2024-03-01 18:03:34 +01:00
rows: [row1, row2._id!, { _id: row3._id }],
})
2023-07-24 10:08:10 +02:00
2024-03-01 18:03:34 +01:00
expect(res.length).toEqual(3)
2024-03-01 17:03:52 +01:00
await config.api.row.get(table._id!, row1._id!, { status: 404 })
2023-07-24 10:08:10 +02:00
await assertRowUsage(rowUsage - 3)
})
it("should accept a valid row object and delete the row", async () => {
const row1 = await config.api.row.save(table._id!, {})
2023-07-24 10:08:10 +02:00
const rowUsage = await getRowUsage()
2024-03-01 18:03:34 +01:00
const res = await config.api.row.delete(table._id!, row1 as DeleteRow)
2023-07-24 10:08:10 +02:00
2024-03-01 18:03:34 +01:00
expect(res.id).toEqual(row1._id)
2024-03-01 17:03:52 +01:00
await config.api.row.get(table._id!, row1._id!, { status: 404 })
2023-07-24 10:08:10 +02:00
await assertRowUsage(rowUsage - 1)
})
2023-07-24 16:03:13 +02:00
it("Should ignore malformed/invalid delete requests", async () => {
const rowUsage = await getRowUsage()
2024-03-01 18:03:34 +01:00
await config.api.row.delete(table._id!, { not: "valid" } as any, {
status: 400,
body: {
message: "Invalid delete rows request",
},
})
2023-07-24 16:03:13 +02:00
2024-03-01 18:03:34 +01:00
await config.api.row.delete(table._id!, { rows: 123 } as any, {
status: 400,
body: {
message: "Invalid delete rows request",
},
})
2023-07-24 16:03:13 +02:00
2024-03-01 18:03:34 +01:00
await config.api.row.delete(table._id!, "invalid" as any, {
status: 400,
body: {
message: "Invalid delete rows request",
},
2023-09-13 09:57:41 +02:00
})
2023-07-24 16:03:13 +02:00
await assertRowUsage(rowUsage)
})
})
2024-03-14 18:29:50 +01:00
describe("enrich", () => {
2023-09-12 21:09:25 +02:00
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
2023-09-12 20:47:06 +02:00
})
it("should allow enriching some linked rows", async () => {
2023-09-08 15:09:50 +02:00
const { linkedTable, firstRow, secondRow } = await tenancy.doInTenant(
2023-07-13 12:17:24 +02:00
config.getTenantId(),
2022-09-28 09:56:45 +02:00
async () => {
const linkedTable = await config.api.table.save(
defaultTable({
2023-09-13 14:09:48 +02:00
schema: {
link: {
name: "link",
fieldName: "link",
type: FieldType.LINK,
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: table._id!,
2023-09-08 15:09:50 +02:00
},
},
})
2023-09-13 14:09:48 +02:00
)
const firstRow = await config.api.row.save(table._id!, {
2022-09-28 09:56:45 +02:00
name: "Test Contact",
description: "original description",
})
const secondRow = await config.api.row.save(linkedTable._id!, {
2022-09-28 09:56:45 +02:00
name: "Test 2",
description: "og desc",
link: [{ _id: firstRow._id }],
})
2023-09-08 15:09:50 +02:00
return { linkedTable, firstRow, secondRow }
2022-09-28 09:56:45 +02:00
}
)
2022-05-11 12:32:53 +02:00
const rowUsage = await getRowUsage()
// test basic enrichment
2023-09-13 10:13:54 +02:00
const resBasic = await config.api.row.get(
linkedTable._id!,
secondRow._id!
)
2024-03-01 16:20:07 +01:00
expect(resBasic.link.length).toBe(1)
expect(resBasic.link[0]).toEqual({
2023-09-12 18:09:09 +02:00
_id: firstRow._id,
primaryDisplay: firstRow.name,
})
// test full enrichment
2023-09-13 10:13:54 +02:00
const resEnriched = await config.api.row.getEnriched(
linkedTable._id!,
secondRow._id!
)
2024-03-01 16:20:07 +01:00
expect(resEnriched.link.length).toBe(1)
expect(resEnriched.link[0]._id).toBe(firstRow._id)
expect(resEnriched.link[0].name).toBe("Test Contact")
expect(resEnriched.link[0].description).toBe("original description")
2022-05-11 12:32:53 +02:00
await assertRowUsage(rowUsage)
})
})
2023-09-12 18:07:50 +02:00
isInternal &&
describe("attachments", () => {
it("should allow enriching attachment rows", async () => {
2024-03-13 17:44:05 +01:00
const table = await config.api.table.save(
defaultTable({
schema: {
attachment: {
type: FieldType.ATTACHMENT,
name: "attachment",
constraints: { type: "array", presence: false },
},
},
})
)
2024-03-13 17:32:33 +01:00
const attachmentId = `${uuid.v4()}.csv`
2024-03-13 17:44:05 +01:00
const row = await config.api.row.save(table._id!, {
2023-09-12 18:07:50 +02:00
name: "test",
description: "test",
attachment: [
{
key: `${config.getAppId()}/attachments/${attachmentId}`,
},
],
tableId: table._id,
})
await config.withEnv({ SELF_HOSTED: "true" }, async () => {
2023-09-12 18:07:50 +02:00
return context.doInAppContext(config.getAppId(), async () => {
const enriched = await outputProcessing(table, [row])
expect((enriched as Row[])[0].attachment[0].url).toBe(
`/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
)
})
})
})
2021-03-04 14:07:33 +01:00
})
2022-09-28 09:56:45 +02:00
2024-03-14 18:29:50 +01:00
describe("exportRows", () => {
2023-09-12 21:09:25 +02:00
beforeAll(async () => {
table = await config.api.table.save(defaultTable())
2023-09-12 20:47:06 +02:00
})
2022-09-28 09:56:45 +02:00
it("should allow exporting all columns", async () => {
const existing = await config.api.row.save(table._id!, {})
2023-09-13 10:13:54 +02:00
const res = await config.api.row.exportRows(table._id!, {
rows: [existing._id!],
})
2024-03-01 18:10:49 +01:00
const results = JSON.parse(res)
2022-09-28 09:56:45 +02:00
expect(results.length).toEqual(1)
const row = results[0]
// Ensure all original columns were exported
expect(Object.keys(row).length).toBeGreaterThanOrEqual(
Object.keys(existing).length
)
Object.keys(existing).forEach(key => {
expect(row[key]).toEqual(existing[key])
})
})
it("should allow exporting only certain columns", async () => {
const existing = await config.api.row.save(table._id!, {})
2023-09-13 10:13:54 +02:00
const res = await config.api.row.exportRows(table._id!, {
rows: [existing._id!],
columns: ["_id"],
})
2024-03-01 18:10:49 +01:00
const results = JSON.parse(res)
2022-09-28 09:56:45 +02:00
expect(results.length).toEqual(1)
const row = results[0]
// Ensure only the _id column was exported
expect(Object.keys(row).length).toEqual(1)
expect(row._id).toEqual(existing._id)
})
it("should handle single quotes in row filtering", async () => {
const existing = await config.api.row.save(table._id!, {})
const res = await config.api.row.exportRows(table._id!, {
rows: [`['${existing._id!}']`],
})
const results = JSON.parse(res)
expect(results.length).toEqual(1)
const row = results[0]
expect(row._id).toEqual(existing._id)
})
it("should return an error on composite keys", async () => {
const existing = await config.api.row.save(table._id!, {})
await config.api.row.exportRows(
table._id!,
{
rows: [`['${existing._id!}']`, "['d001', '10111']"],
},
{
status: 400,
body: {
message: "Export data does not support composite keys.",
},
}
)
})
it("should return an error if no table is found", async () => {
const existing = await config.api.row.save(table._id!, {})
await config.api.row.exportRows(
"1234567",
{ rows: [existing._id!] },
{ status: 404 }
)
})
2022-09-28 09:56:45 +02:00
})
2023-07-14 10:26:22 +02:00
let o2mTable: Table
let m2mTable: Table
beforeAll(async () => {
o2mTable = await config.api.table.save(defaultTable({ name: "o2m" }))
m2mTable = await config.api.table.save(defaultTable({ name: "m2m" }))
})
describe.each([
2024-03-12 15:46:52 +01:00
[
"relationship fields",
(): Record<string, FieldSchema> => ({
user: {
name: "user",
relationshipType: RelationshipType.ONE_TO_MANY,
type: FieldType.LINK,
tableId: o2mTable._id!,
fieldName: "fk_o2m",
},
users: {
name: "users",
relationshipType: RelationshipType.MANY_TO_MANY,
type: FieldType.LINK,
tableId: m2mTable._id!,
fieldName: "fk_m2m",
},
}),
(tableId: string) =>
config.api.row.save(tableId, {
name: uuid.v4(),
description: generator.paragraph(),
tableId,
}),
(row: Row) => ({
_id: row._id,
primaryDisplay: row.name,
}),
],
[
"bb reference fields",
2023-10-06 09:12:45 +02:00
(): Record<string, FieldSchema> => ({
user: {
name: "user",
type: FieldType.BB_REFERENCE,
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
},
users: {
name: "users",
type: FieldType.BB_REFERENCE,
2023-10-04 17:53:46 +02:00
subtype: FieldTypeSubtypes.BB_REFERENCE.USERS,
},
}),
() => config.createUser(),
(row: Row) => ({
_id: row._id,
primaryDisplay: row.email,
email: row.email,
firstName: row.firstName,
lastName: row.lastName,
}),
],
])("links - %s", (__, relSchema, dataGenerator, resultMapper) => {
2023-09-27 16:51:42 +02:00
let tableId: string
let o2mData: Row[]
let m2mData: Row[]
2023-09-27 18:32:18 +02:00
2023-09-27 16:51:42 +02:00
beforeAll(async () => {
const table = await config.api.table.save(
defaultTable({ schema: relSchema() })
)
2023-09-27 16:51:42 +02:00
tableId = table._id!
2023-09-27 17:35:16 +02:00
o2mData = [
await dataGenerator(o2mTable._id!),
await dataGenerator(o2mTable._id!),
await dataGenerator(o2mTable._id!),
await dataGenerator(o2mTable._id!),
]
m2mData = [
await dataGenerator(m2mTable._id!),
await dataGenerator(m2mTable._id!),
await dataGenerator(m2mTable._id!),
await dataGenerator(m2mTable._id!),
2023-09-27 17:35:16 +02:00
]
2023-09-27 16:51:42 +02:00
})
it("can save a row when relationship fields are empty", async () => {
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(tableId, {
name: "foo",
description: "bar",
})
2023-09-27 16:51:42 +02:00
expect(row).toEqual({
_id: expect.any(String),
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
type: isInternal ? "row" : undefined,
2024-03-13 17:20:45 +01:00
name: "foo",
description: "bar",
tableId,
2023-09-27 16:51:42 +02:00
})
})
it("can save a row with a single relationship field", async () => {
const user = _.sample(o2mData)!
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(tableId, {
name: "foo",
description: "bar",
user: [user],
2024-03-13 17:20:45 +01:00
})
2023-09-27 16:51:42 +02:00
expect(row).toEqual({
2024-03-13 17:20:45 +01:00
name: "foo",
description: "bar",
2023-09-27 16:51:42 +02:00
tableId,
user: [user].map(u => resultMapper(u)),
2023-09-27 16:51:42 +02:00
_id: expect.any(String),
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
type: isInternal ? "row" : undefined,
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id,
2023-09-27 16:51:42 +02:00
})
})
it("can save a row with a multiple relationship field", async () => {
const selectedUsers = _.sampleSize(m2mData, 2)
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(tableId, {
name: "foo",
description: "bar",
2023-09-27 17:35:16 +02:00
users: selectedUsers,
2024-03-13 17:20:45 +01:00
})
2023-09-27 16:51:42 +02:00
expect(row).toEqual({
2024-03-13 17:20:45 +01:00
name: "foo",
description: "bar",
2023-09-27 16:51:42 +02:00
tableId,
users: expect.arrayContaining(selectedUsers.map(u => resultMapper(u))),
2023-09-27 16:51:42 +02:00
_id: expect.any(String),
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
type: isInternal ? "row" : undefined,
2023-09-27 16:51:42 +02:00
})
2023-09-27 17:16:37 +02:00
})
it("can retrieve rows with no populated relationships", async () => {
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(tableId, {
name: "foo",
description: "bar",
})
2023-09-27 17:16:37 +02:00
2024-03-01 16:20:07 +01:00
const retrieved = await config.api.row.get(tableId, row._id!)
2023-09-27 17:16:37 +02:00
expect(retrieved).toEqual({
2024-03-13 17:20:45 +01:00
name: "foo",
description: "bar",
2023-09-27 17:16:37 +02:00
tableId,
user: undefined,
users: undefined,
_id: row._id,
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
...defaultRowFields,
2023-09-27 17:16:37 +02:00
})
})
it("can retrieve rows with populated relationships", async () => {
const user1 = _.sample(o2mData)!
const [user2, user3] = _.sampleSize(m2mData, 2)
2023-09-27 17:16:37 +02:00
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(tableId, {
name: "foo",
description: "bar",
users: [user2, user3],
2023-09-27 17:16:37 +02:00
user: [user1],
2024-03-13 17:20:45 +01:00
})
2023-09-27 17:16:37 +02:00
2024-03-01 16:20:07 +01:00
const retrieved = await config.api.row.get(tableId, row._id!)
2023-09-27 17:16:37 +02:00
expect(retrieved).toEqual({
2024-03-13 17:20:45 +01:00
name: "foo",
description: "bar",
2023-09-27 17:16:37 +02:00
tableId,
user: expect.arrayContaining([user1].map(u => resultMapper(u))),
users: expect.arrayContaining([user2, user3].map(u => resultMapper(u))),
2023-09-27 17:16:37 +02:00
_id: row._id,
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user1.id,
2023-09-27 18:32:18 +02:00
...defaultRowFields,
2023-09-27 17:16:37 +02:00
})
2023-09-27 16:51:42 +02:00
})
2023-09-27 17:03:16 +02:00
it("can update an existing populated row", async () => {
const user = _.sample(o2mData)!
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
2023-09-27 17:03:16 +02:00
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(tableId, {
name: "foo",
description: "bar",
users: [users1, users2],
2024-03-13 17:20:45 +01:00
})
2023-09-27 17:03:16 +02:00
const updatedRow = await config.api.row.save(tableId, {
...row,
user: [user],
users: [users3, users1],
2023-09-27 17:03:16 +02:00
})
expect(updatedRow).toEqual({
2024-03-13 17:20:45 +01:00
name: "foo",
description: "bar",
2023-09-27 17:03:16 +02:00
tableId,
user: expect.arrayContaining([user].map(u => resultMapper(u))),
users: expect.arrayContaining(
[users3, users1].map(u => resultMapper(u))
),
2023-09-27 17:03:16 +02:00
_id: row._id,
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
type: isInternal ? "row" : undefined,
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id,
2023-09-27 17:03:16 +02:00
})
})
2023-09-27 17:08:29 +02:00
it("can wipe an existing populated relationships in row", async () => {
const [user1, user2] = _.sampleSize(m2mData, 2)
2024-03-13 17:20:45 +01:00
const row = await config.api.row.save(tableId, {
name: "foo",
description: "bar",
2023-09-27 17:08:29 +02:00
users: [user1, user2],
2024-03-13 17:20:45 +01:00
})
2023-09-27 17:08:29 +02:00
const updatedRow = await config.api.row.save(tableId, {
...row,
user: null,
users: null,
})
expect(updatedRow).toEqual({
2024-03-13 17:20:45 +01:00
name: "foo",
description: "bar",
2023-09-27 17:08:29 +02:00
tableId,
_id: row._id,
_rev: expect.any(String),
2023-09-29 14:58:41 +02:00
id: isInternal ? undefined : expect.any(Number),
type: isInternal ? "row" : undefined,
})
2023-09-27 17:08:29 +02:00
})
2023-09-27 17:48:30 +02:00
it("fetch all will populate the relationships", async () => {
const [user1] = _.sampleSize(o2mData, 1)
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
2023-09-27 17:48:30 +02:00
2024-03-13 17:20:45 +01:00
const rows = [
2023-09-27 17:48:30 +02:00
{
name: generator.name(),
description: generator.name(),
users: [users1, users2],
2023-09-27 17:48:30 +02:00
},
{
name: generator.name(),
description: generator.name(),
user: [user1],
users: [users1, users3],
2023-09-27 17:48:30 +02:00
},
{
name: generator.name(),
description: generator.name(),
users: [users3],
2023-09-27 17:48:30 +02:00
},
]
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 => resultMapper(u)),
2023-09-29 12:54:16 +02:00
users: r.users?.length
? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
: undefined,
2023-09-27 17:48:30 +02:00
_id: expect.any(String),
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
[`fk_${o2mTable.name}_fk_o2m`]:
isInternal || !r.user?.length ? undefined : r.user[0].id,
2023-09-27 18:32:18 +02:00
...defaultRowFields,
2023-09-27 17:48:30 +02:00
}))
)
)
})
it("search all will populate the relationships", async () => {
const [user1] = _.sampleSize(o2mData, 1)
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
2023-09-27 17:48:30 +02:00
2024-03-13 17:20:45 +01:00
const rows = [
2023-09-27 17:48:30 +02:00
{
name: generator.name(),
description: generator.name(),
users: [users1, users2],
2023-09-27 17:48:30 +02:00
},
{
name: generator.name(),
description: generator.name(),
user: [user1],
users: [users1, users3],
2023-09-27 17:48:30 +02:00
},
{
name: generator.name(),
description: generator.name(),
users: [users3],
2023-09-27 17:48:30 +02:00
},
]
await config.api.row.save(tableId, rows[0])
2024-03-12 15:46:52 +01:00
await config.api.row.save(tableId, rows[1])
await config.api.row.save(tableId, rows[2])
2023-09-27 17:48:30 +02:00
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 => resultMapper(u)),
users: r.users?.length
? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
: undefined,
2023-09-27 17:48:30 +02:00
_id: expect.any(String),
_rev: expect.any(String),
2023-09-27 18:32:18 +02:00
id: isInternal ? undefined : expect.any(Number),
[`fk_${o2mTable.name}_fk_o2m`]:
isInternal || !r.user?.length ? undefined : r.user[0].id,
2023-09-27 18:32:18 +02:00
...defaultRowFields,
2023-09-27 17:48:30 +02:00
}))
),
2023-09-27 18:32:18 +02:00
...(isInternal
? {}
: {
hasNextPage: false,
bookmark: null,
}),
2023-09-27 17:48:30 +02:00
})
})
2023-09-27 16:51:42 +02:00
})
describe("Formula fields", () => {
let table: Table
let otherTable: Table
let relatedRow: Row
beforeAll(async () => {
otherTable = await config.api.table.save(defaultTable())
table = await config.api.table.save(
saveTableRequest({
2023-11-08 15:57:30 +01:00
name: "b",
schema: {
links: {
name: "links",
fieldName: "links",
type: FieldType.LINK,
tableId: otherTable._id!,
relationshipType: RelationshipType.ONE_TO_MANY,
},
formula: {
name: "formula",
type: FieldType.FORMULA,
formula: "{{ links.0.name }}",
formulaType: FormulaType.DYNAMIC,
},
},
})
)
relatedRow = await config.api.row.save(otherTable._id!, {
name: generator.word(),
description: generator.paragraph(),
})
await config.api.row.save(table._id!, {
name: generator.word(),
description: generator.paragraph(),
tableId: table._id!,
links: [relatedRow._id],
})
})
it("should be able to search for rows containing formulas", async () => {
const { rows } = await config.api.row.search(table._id!)
expect(rows.length).toBe(1)
expect(rows[0].links.length).toBe(1)
const row = rows[0]
expect(row.formula).toBe(relatedRow.name)
})
})
describe("Formula JS protection", () => {
it("should time out JS execution if a single cell takes too long", async () => {
await config.withEnv({ JS_PER_INVOCATION_TIMEOUT_MS: 20 }, async () => {
2023-12-18 18:05:50 +01:00
const js = Buffer.from(
`
let i = 0;
while (true) {
i++;
}
return i;
`
2023-12-18 18:05:50 +01:00
).toString("base64")
const table = await config.api.table.save(
saveTableRequest({
schema: {
text: {
name: "text",
type: FieldType.STRING,
},
formula: {
name: "formula",
type: FieldType.FORMULA,
formula: `{{ js "${js}"}}`,
formulaType: FormulaType.DYNAMIC,
},
2023-12-18 18:05:50 +01:00
},
})
)
2023-12-18 18:05:50 +01:00
await config.api.row.save(table._id!, { text: "foo" })
const { rows } = await config.api.row.search(table._id!)
expect(rows).toHaveLength(1)
const row = rows[0]
expect(row.text).toBe("foo")
expect(row.formula).toBe("Timed out while executing JS")
})
})
it("should time out JS execution if a multiple cells take too long", async () => {
await config.withEnv(
{
JS_PER_INVOCATION_TIMEOUT_MS: 20,
2024-02-08 16:23:27 +01:00
JS_PER_REQUEST_TIMEOUT_MS: 40,
},
async () => {
const js = Buffer.from(
`
let i = 0;
while (true) {
i++;
}
return i;
`
).toString("base64")
2024-03-13 17:32:33 +01:00
const table = await config.api.table.save(
saveTableRequest({
name: "table",
schema: {
text: {
name: "text",
type: FieldType.STRING,
},
formula: {
name: "formula",
type: FieldType.FORMULA,
formula: `{{ js "${js}"}}`,
formulaType: FormulaType.DYNAMIC,
},
},
2024-03-13 17:32:33 +01:00
})
)
for (let i = 0; i < 10; i++) {
await config.api.row.save(table._id!, { text: "foo" })
}
2023-12-18 18:05:50 +01:00
// Run this test 3 times to make sure that there's no cross-request
// pollution of the execution time tracking.
2023-12-18 18:05:50 +01:00
for (let reqs = 0; reqs < 3; reqs++) {
const { rows } = await config.api.row.search(table._id!)
expect(rows).toHaveLength(10)
let i = 0
for (; i < 10; i++) {
const row = rows[i]
if (row.formula !== "Timed out while executing JS") {
break
}
}
2023-12-18 18:05:50 +01:00
// Given the execution times are not deterministic, we can't be sure
// of the exact number of rows that were executed before the timeout
// but it should absolutely be at least 1.
expect(i).toBeGreaterThan(0)
expect(i).toBeLessThan(5)
2023-12-18 18:05:50 +01:00
for (; i < 10; i++) {
const row = rows[i]
expect(row.text).toBe("foo")
expect(row.formula).toBe("Request JS execution limit hit")
}
}
}
)
})
2024-02-20 11:59:04 +01:00
it("should not carry over context between formulas", async () => {
const js = Buffer.from(`return $("[text]");`).toString("base64")
2024-03-13 17:32:33 +01:00
const table = await config.api.table.save(
saveTableRequest({
schema: {
text: {
name: "text",
type: FieldType.STRING,
},
formula: {
name: "formula",
type: FieldType.FORMULA,
formula: `{{ js "${js}"}}`,
formulaType: FormulaType.DYNAMIC,
},
2024-02-20 11:59:04 +01:00
},
2024-03-13 17:32:33 +01:00
})
)
2024-02-20 11:59:04 +01:00
for (let i = 0; i < 10; i++) {
await config.api.row.save(table._id!, { text: `foo${i}` })
}
const { rows } = await config.api.row.search(table._id!)
expect(rows).toHaveLength(10)
const formulaValues = rows.map(r => r.formula)
expect(formulaValues).toEqual(
expect.arrayContaining([
"foo0",
"foo1",
"foo2",
"foo3",
"foo4",
"foo5",
"foo6",
"foo7",
"foo8",
"foo9",
])
)
})
})
2021-09-30 13:55:21 +02:00
})