diff --git a/packages/account-portal b/packages/account-portal index d6a1f89aa5..c1a53bb2f4 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit d6a1f89aa543bdce7acde5fbe4ce650a1344e2fe +Subproject commit c1a53bb2f4cafcb4c55ad7181146617b449907f2 diff --git a/packages/backend-core/src/cache/generic.ts b/packages/backend-core/src/cache/generic.ts index 7a2be5a0f0..3ac323a8d4 100644 --- a/packages/backend-core/src/cache/generic.ts +++ b/packages/backend-core/src/cache/generic.ts @@ -18,14 +18,15 @@ export enum TTL { ONE_DAY = 86400, } -function performExport(funcName: string) { - // @ts-ignore - return (...args: any) => GENERIC[funcName](...args) -} - -export const keys = performExport("keys") -export const get = performExport("get") -export const store = performExport("store") -export const destroy = performExport("delete") -export const withCache = performExport("withCache") -export const bustCache = performExport("bustCache") +export const keys = (...args: Parameters) => + GENERIC.keys(...args) +export const get = (...args: Parameters) => + GENERIC.get(...args) +export const store = (...args: Parameters) => + GENERIC.store(...args) +export const destroy = (...args: Parameters) => + GENERIC.delete(...args) +export const withCache = (...args: Parameters) => + GENERIC.withCache(...args) +export const bustCache = (...args: Parameters) => + GENERIC.bustCache(...args) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js new file mode 100755 index 0000000000..b81aed3d5d --- /dev/null +++ b/packages/server/scripts/load/create-many-relationships.js @@ -0,0 +1,196 @@ +#!/bin/node +const { + createApp, + getTable, + createRow, + createTable, + getApp, + getRows, +} = require("./utils") + +const Chance = require("chance") + +const generator = new Chance() + +const STUDENT_COUNT = 500 +const SUBJECT_COUNT = 10 + +let { apiKey, appId } = require("yargs") + .demandOption(["apiKey"]) + .option("appId").argv + +const start = Date.now() +async function batchCreate(apiKey, appId, table, items, batchSize = 100) { + let i = 0 + let errors = 0 + + async function createSingleRow(item) { + try { + const row = await createRow(apiKey, appId, table, item) + console.log( + `${table.name} - ${++i} of ${items.length} created (${ + (Date.now() - start) / 1000 + }s)` + ) + return row + } catch { + errors++ + } + } + + const rows = [] + const maxConcurrency = Math.min(batchSize, items.length) + const inFlight = {} + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const item = items[itemIndex] + const promise = createSingleRow(item) + .then(result => { + rows.push(result) + }) + .finally(() => { + delete inFlight[itemIndex] + }) + + inFlight[itemIndex] = promise + + if (Object.keys(inFlight).length >= maxConcurrency) { + await Promise.race(Object.values(inFlight)) + } + } + + await Promise.all(Object.values(inFlight)) + + if (errors) { + console.error( + `${table.name} - ${errors} creation errored (${ + (Date.now() - start) / 1000 + }s)` + ) + } + + return rows +} + +const useExistingApp = !!appId + +async function upsertTable(appId, tableName, tableData) { + if (useExistingApp) { + return await getTable(apiKey, appId, tableName) + } + + const table = await createTable(apiKey, appId, { + ...tableData, + name: tableName, + }) + return table +} + +async function run() { + if (!appId) { + const app = appId ? await getApp(apiKey, appId) : await createApp(apiKey) + appId = app._id + + console.log(`App created. Url: http://localhost:10000/builder/app/${appId}`) + } else { + console.log( + `App retrieved. Url: http://localhost:10000/builder/app/${appId}` + ) + } + + const studentsTable = await getTable(apiKey, appId, "Students") + + let studentNumber = studentsTable.schema["Auto ID"].lastID + const students = await batchCreate( + apiKey, + appId, + studentsTable, + Array.from({ length: STUDENT_COUNT }).map(() => ({ + "Student Number": (++studentNumber).toString(), + "First Name": generator.first(), + "Last Name": generator.last(), + Gender: generator.pickone(["M", "F"]), + Grade: generator.pickone(["8", "9", "10", "11"]), + "Tardiness (Days)": generator.integer({ min: 1, max: 100 }), + "Home Number": generator.phone(), + "Attendance_(%)": generator.integer({ min: 0, max: 100 }), + })) + ) + + const subjectTable = await upsertTable(appId, "Subjects", { + schema: { + Name: { + name: "Name", + type: "string", + }, + }, + primaryDisplay: "Name", + }) + + const subjects = useExistingApp + ? await getRows(apiKey, appId, subjectTable._id) + : await batchCreate( + apiKey, + appId, + subjectTable, + Array.from({ length: SUBJECT_COUNT }).map(() => ({ + Name: generator.profession(), + })) + ) + + const gradesTable = await upsertTable(appId, "Grades", { + schema: { + Score: { + name: "Score", + type: "number", + }, + Student: { + name: "Student", + tableId: studentsTable._id, + constraints: { + presence: true, + type: "array", + }, + fieldName: "Grades", + relationshipType: "one-to-many", + type: "link", + }, + Subject: { + name: "Subject", + tableId: subjectTable._id, + constraints: { + presence: true, + type: "array", + }, + fieldName: "Grades", + relationshipType: "one-to-many", + type: "link", + }, + }, + }) + + await batchCreate( + apiKey, + appId, + gradesTable, + students.flatMap(student => + subjects.map(subject => ({ + Score: generator.integer({ min: 0, max: 100 }), + Student: [student], + Subject: [subject], + })) + ) + ) + + console.log( + `Access the app here: http://localhost:10000/builder/app/${appId}` + ) +} + +run() + .then(() => { + console.log(`Done in ${(Date.now() - start) / 1000} seconds`) + }) + .catch(err => { + console.error(err) + }) diff --git a/packages/server/scripts/load/delete-all-apps.js b/packages/server/scripts/load/delete-all-apps.js new file mode 100755 index 0000000000..5c9e974c7d --- /dev/null +++ b/packages/server/scripts/load/delete-all-apps.js @@ -0,0 +1,29 @@ +#!/bin/node +const { searchApps, deleteApp } = require("./utils") + +if (!process.argv[2]) { + console.error("Please specify an API key as script argument.") + process.exit(-1) +} + +async function run() { + const apiKey = process.argv[2] + const apps = await searchApps(apiKey) + console.log(`Deleting ${apps.length} apps`) + + let deletedApps = 0 + await Promise.all( + apps.map(async app => { + await deleteApp(apiKey, app._id) + console.log(`App ${++deletedApps} of ${apps.length} deleted`) + }) + ) +} + +run() + .then(() => { + console.log("Done!") + }) + .catch(err => { + console.error(err) + }) diff --git a/packages/server/scripts/load/utils.js b/packages/server/scripts/load/utils.js index 97ff8f1e13..1dabdcec9a 100644 --- a/packages/server/scripts/load/utils.js +++ b/packages/server/scripts/load/utils.js @@ -2,7 +2,8 @@ const fetch = require("node-fetch") const uuid = require("uuid/v4") const URL_APP = "http://localhost:10000/api/public/v1/applications" -const URL_TABLE = "http://localhost:10000/api/public/v1/tables/search" +const URL_TABLE = "http://localhost:10000/api/public/v1/tables" +const URL_SEARCH_TABLE = "http://localhost:10000/api/public/v1/tables/search" async function request(apiKey, url, method, body, appId = undefined) { const headers = { @@ -37,30 +38,64 @@ exports.createApp = async apiKey => { return json.data } -exports.getTable = async (apiKey, appId) => { - const res = await request(apiKey, URL_TABLE, "POST", {}, appId) +exports.getApp = async (apiKey, appId) => { + const res = await request(apiKey, `${URL_APP}/${appId}`, "GET") const json = await res.json() - return json.data[0] + return json.data +} +exports.searchApps = async apiKey => { + const res = await request(apiKey, `${URL_APP}/search`, "POST", {}) + const json = await res.json() + return json.data } -exports.createRow = async (apiKey, appId, table) => { - const body = {} - for (let [key, schema] of Object.entries(table.schema)) { - let fake - switch (schema.type) { - default: - case "string": - fake = schema.constraints.inclusion - ? schema.constraints.inclusion[0] - : "a" - break - case "number": - fake = 1 - break +exports.deleteApp = async (apiKey, appId) => { + const res = await request(apiKey, `${URL_APP}/${appId}`, "DELETE") + return res +} + +exports.getTable = async (apiKey, appId, tableName) => { + const res = await request(apiKey, URL_SEARCH_TABLE, "POST", {}, appId) + const json = await res.json() + const table = json.data.find(t => t.name === tableName) + if (!table) { + throw `Table '${tableName} not found` + } + return table +} + +exports.createRow = async (apiKey, appId, table, body) => { + if (!body) { + body = {} + for (let [key, schema] of Object.entries(table.schema)) { + let fake + switch (schema.type) { + default: + case "string": + fake = schema.constraints?.inclusion + ? schema.constraints.inclusion[0] + : "a" + break + case "number": + fake = 1 + break + } + body[key] = fake } - body[key] = fake } const url = `http://localhost:10000/api/public/v1/tables/${table._id}/rows` const res = await request(apiKey, url, "POST", body, appId) return (await res.json()).data } + +exports.getRows = async (apiKey, appId, tableId) => { + const url = `${URL_TABLE}/${tableId}/rows/search` + const res = await request(apiKey, url, "POST", {}, appId) + return (await res.json()).data +} + +exports.createTable = async (apiKey, appId, config) => { + const res = await request(apiKey, URL_TABLE, "POST", config, appId) + const json = await res.json() + return json.data +} diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index 287b2ae6aa..d741247687 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -26,7 +26,7 @@ import { inputProcessing, outputProcessing, } from "../../../utilities/rowProcessor" -import { cloneDeep, isEqual } from "lodash" +import { cloneDeep } from "lodash" export async function handleRequest( operation: T, @@ -86,50 +86,6 @@ export async function patch(ctx: UserCtx) { } } -export async function save(ctx: UserCtx) { - const inputs = ctx.request.body - const tableId = utils.getTableId(ctx) - - const table = await sdk.tables.getTable(tableId) - const { table: updatedTable, row } = await inputProcessing( - ctx.user?._id, - cloneDeep(table), - inputs - ) - - const validateResult = await sdk.rows.utils.validate({ - row, - tableId, - }) - if (!validateResult.valid) { - throw { validation: validateResult.errors } - } - - const response = await handleRequest(Operation.CREATE, tableId, { - row, - }) - - if (!isEqual(table, updatedTable)) { - await sdk.tables.saveTable(updatedTable) - } - - const rowId = response.row._id - if (rowId) { - const row = await sdk.rows.external.getRow(tableId, rowId, { - relationships: true, - }) - return { - ...response, - row: await outputProcessing(table, row, { - preserveLinks: true, - squash: true, - }), - } - } else { - return response - } -} - export async function find(ctx: UserCtx): Promise { const id = ctx.params.rowId const tableId = utils.getTableId(ctx) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 38731a87e1..7ff8d83e71 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -30,7 +30,7 @@ import { Format } from "../view/exporters" export * as views from "./views" -function pickApi(tableId: any) { +function pickApi(tableId: string) { if (isExternalTableID(tableId)) { return external } @@ -84,9 +84,12 @@ export const save = async (ctx: UserCtx) => { return patch(ctx as UserCtx) } const { row, table, squashed } = await quotas.addRow(() => - quotas.addQuery(() => pickApi(tableId).save(ctx), { - datasourceId: tableId, - }) + quotas.addQuery( + () => sdk.rows.save(tableId, ctx.request.body, ctx.user?._id), + { + datasourceId: tableId, + } + ) ) ctx.status = 200 ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table) diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts index a251724b0a..5c714c098a 100644 --- a/packages/server/src/api/controllers/row/internal.ts +++ b/packages/server/src/api/controllers/row/internal.ts @@ -1,5 +1,5 @@ import * as linkRows from "../../../db/linkedRows" -import { generateRowID, InternalTables } from "../../../db/utils" +import { InternalTables } from "../../../db/utils" import * as userController from "../user" import { AttachmentCleanup, @@ -94,45 +94,6 @@ export async function patch(ctx: UserCtx) { }) } -export async function save(ctx: UserCtx) { - let inputs = ctx.request.body - inputs.tableId = utils.getTableId(ctx) - - if (!inputs._rev && !inputs._id) { - inputs._id = generateRowID(inputs.tableId) - } - - // this returns the table and row incase they have been updated - const dbTable = await sdk.tables.getTable(inputs.tableId) - - // need to copy the table so it can be differenced on way out - const tableClone = cloneDeep(dbTable) - - let { table, row } = await inputProcessing(ctx.user?._id, tableClone, inputs) - - const validateResult = await sdk.rows.utils.validate({ - row, - table, - }) - - if (!validateResult.valid) { - throw { validation: validateResult.errors } - } - - // make sure link rows are up-to-date - row = (await linkRows.updateLinks({ - eventType: linkRows.EventType.ROW_SAVE, - row, - tableId: row.tableId, - table, - })) as Row - - return finaliseRow(table, row, { - oldTable: dbTable, - updateFormula: true, - }) -} - export async function find(ctx: UserCtx): Promise { const tableId = utils.getTableId(ctx), rowId = ctx.params.rowId diff --git a/packages/server/src/api/controllers/row/staticFormula.ts b/packages/server/src/api/controllers/row/staticFormula.ts index 8d52b6a05c..3cb7348ee3 100644 --- a/packages/server/src/api/controllers/row/staticFormula.ts +++ b/packages/server/src/api/controllers/row/staticFormula.ts @@ -5,8 +5,8 @@ import { processFormulas, } from "../../../utilities/rowProcessor" import { FieldTypes, FormulaTypes } from "../../../constants" -import { context } from "@budibase/backend-core" -import { Table, Row } from "@budibase/types" +import { context, locks } from "@budibase/backend-core" +import { Table, Row, LockType, LockName } from "@budibase/types" import * as linkRows from "../../../db/linkedRows" import sdk from "../../../sdk" import isEqual from "lodash/isEqual" @@ -149,12 +149,22 @@ export async function finaliseRow( await db.put(table) } catch (err: any) { if (err.status === 409) { - const updatedTable = await sdk.tables.getTable(table._id!) - let response = processAutoColumn(null, updatedTable, row, { - reprocessing: true, - }) - await db.put(response.table) - row = response.row + // Some conflicts with the autocolumns occurred, we need to refetch the table and recalculate + await locks.doWithLock( + { + type: LockType.AUTO_EXTEND, + name: LockName.PROCESS_AUTO_COLUMNS, + resource: table._id, + }, + async () => { + const latestTable = await sdk.tables.getTable(table._id!) + let response = processAutoColumn(null, latestTable, row, { + reprocessing: true, + }) + await db.put(response.table) + row = response.row + } + ) } else { throw err } diff --git a/packages/server/src/api/routes/public/index.ts b/packages/server/src/api/routes/public/index.ts index f27f3f8857..36e0f74bee 100644 --- a/packages/server/src/api/routes/public/index.ts +++ b/packages/server/src/api/routes/public/index.ts @@ -77,7 +77,7 @@ const publicRouter = new Router({ prefix: PREFIX, }) -if (limiter) { +if (limiter && !env.isDev()) { publicRouter.use(limiter) } diff --git a/packages/server/src/sdk/app/rows/external.ts b/packages/server/src/sdk/app/rows/external.ts index beae02e134..7ad5ea37ff 100644 --- a/packages/server/src/sdk/app/rows/external.ts +++ b/packages/server/src/sdk/app/rows/external.ts @@ -1,6 +1,13 @@ -import { IncludeRelationship, Operation } from "@budibase/types" +import { IncludeRelationship, Operation, Row } from "@budibase/types" import { handleRequest } from "../../../api/controllers/row/external" import { breakRowIdField } from "../../../integrations/utils" +import sdk from "../../../sdk" +import { + inputProcessing, + outputProcessing, +} from "../../../utilities/rowProcessor" +import cloneDeep from "lodash/fp/cloneDeep" +import isEqual from "lodash/fp/isEqual" export async function getRow( tableId: string, @@ -15,3 +22,48 @@ export async function getRow( }) return response ? response[0] : response } + +export async function save( + tableId: string, + inputs: Row, + userId: string | undefined +) { + const table = await sdk.tables.getTable(tableId) + const { table: updatedTable, row } = await inputProcessing( + userId, + cloneDeep(table), + inputs + ) + + const validateResult = await sdk.rows.utils.validate({ + row, + tableId, + }) + if (!validateResult.valid) { + throw { validation: validateResult.errors } + } + + const response = await handleRequest(Operation.CREATE, tableId, { + row, + }) + + if (!isEqual(table, updatedTable)) { + await sdk.tables.saveTable(updatedTable) + } + + const rowId = response.row._id + if (rowId) { + const row = await sdk.rows.external.getRow(tableId, rowId, { + relationships: true, + }) + return { + ...response, + row: await outputProcessing(table, row, { + preserveLinks: true, + squash: true, + }), + } + } else { + return response + } +} diff --git a/packages/server/src/sdk/app/rows/internal.ts b/packages/server/src/sdk/app/rows/internal.ts new file mode 100644 index 0000000000..14e771b36e --- /dev/null +++ b/packages/server/src/sdk/app/rows/internal.ts @@ -0,0 +1,49 @@ +import { db } from "@budibase/backend-core" +import { Row } from "@budibase/types" +import sdk from "../../../sdk" +import cloneDeep from "lodash/fp/cloneDeep" +import { finaliseRow } from "../../../api/controllers/row/staticFormula" +import { inputProcessing } from "../../../utilities/rowProcessor" +import * as linkRows from "../../../db/linkedRows" + +export async function save( + tableId: string, + inputs: Row, + userId: string | undefined +) { + inputs.tableId = tableId + + if (!inputs._rev && !inputs._id) { + inputs._id = db.generateRowID(inputs.tableId) + } + + // this returns the table and row incase they have been updated + const dbTable = await sdk.tables.getTable(inputs.tableId) + + // need to copy the table so it can be differenced on way out + const tableClone = cloneDeep(dbTable) + + let { table, row } = await inputProcessing(userId, tableClone, inputs) + + const validateResult = await sdk.rows.utils.validate({ + row, + table, + }) + + if (!validateResult.valid) { + throw { validation: validateResult.errors } + } + + // make sure link rows are up-to-date + row = (await linkRows.updateLinks({ + eventType: linkRows.EventType.ROW_SAVE, + row, + tableId: row.tableId, + table, + })) as Row + + return finaliseRow(table, row, { + oldTable: dbTable, + updateFormula: true, + }) +} diff --git a/packages/server/src/sdk/app/rows/rows.ts b/packages/server/src/sdk/app/rows/rows.ts index 8709180f0b..bfd84a715c 100644 --- a/packages/server/src/sdk/app/rows/rows.ts +++ b/packages/server/src/sdk/app/rows/rows.ts @@ -1,6 +1,9 @@ import { db as dbCore, context } from "@budibase/backend-core" import { Database, Row } from "@budibase/types" import { getRowParams } from "../../../db/utils" +import { isExternalTableID } from "../../../integrations/utils" +import * as internal from "./internal" +import * as external from "./external" export async function getAllInternalRows(appId?: string) { let db: Database @@ -16,3 +19,18 @@ export async function getAllInternalRows(appId?: string) { ) return response.rows.map(row => row.doc) as Row[] } + +function pickApi(tableId: any) { + if (isExternalTableID(tableId)) { + return external + } + return internal +} + +export async function save( + tableId: string, + row: Row, + userId: string | undefined +) { + return pickApi(tableId).save(tableId, row, userId) +} diff --git a/packages/server/src/sdk/app/rows/tests/internal.spec.ts b/packages/server/src/sdk/app/rows/tests/internal.spec.ts new file mode 100644 index 0000000000..b60d31b226 --- /dev/null +++ b/packages/server/src/sdk/app/rows/tests/internal.spec.ts @@ -0,0 +1,220 @@ +import tk from "timekeeper" +import * as internalSdk from "../internal" + +import { generator } from "@budibase/backend-core/tests" +import { + INTERNAL_TABLE_SOURCE_ID, + TableSourceType, + FieldType, + Table, + AutoFieldSubTypes, +} from "@budibase/types" + +import TestConfiguration from "../../../../tests/utilities/TestConfiguration" +import { cache } from "@budibase/backend-core" + +tk.freeze(Date.now()) + +describe("sdk >> rows >> internal", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.init() + }) + + function makeRow() { + return { + name: generator.first(), + surname: generator.last(), + age: generator.age(), + address: generator.address(), + } + } + + describe("save", () => { + const tableData: Table = { + name: generator.word(), + type: "table", + sourceId: INTERNAL_TABLE_SOURCE_ID, + sourceType: TableSourceType.INTERNAL, + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { + type: FieldType.STRING, + }, + }, + surname: { + name: "surname", + type: FieldType.STRING, + constraints: { + type: FieldType.STRING, + }, + }, + age: { + name: "age", + type: FieldType.NUMBER, + constraints: { + type: FieldType.NUMBER, + }, + }, + address: { + name: "address", + type: FieldType.STRING, + constraints: { + type: FieldType.STRING, + }, + }, + }, + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("save will persist the row properly", async () => { + const table = await config.createTable(tableData) + const row = makeRow() + + await config.doInContext(config.appId, async () => { + const response = await internalSdk.save( + table._id!, + row, + config.user._id + ) + + expect(response).toEqual({ + table, + row: { + ...row, + type: "row", + _rev: expect.stringMatching("1-.*"), + }, + squashed: { + ...row, + type: "row", + _rev: expect.stringMatching("1-.*"), + }, + }) + + const persistedRow = await config.getRow(table._id!, response.row._id!) + expect(persistedRow).toEqual({ + ...row, + type: "row", + _rev: expect.stringMatching("1-.*"), + createdAt: expect.any(String), + updatedAt: expect.any(String), + }) + }) + }) + + it("auto ids will update when creating new rows", async () => { + const table = await config.createTable({ + ...tableData, + schema: { + ...tableData.schema, + id: { + name: "id", + type: FieldType.AUTO, + subtype: AutoFieldSubTypes.AUTO_ID, + autocolumn: true, + lastID: 0, + }, + }, + }) + const row = makeRow() + + await config.doInContext(config.appId, async () => { + const response = await internalSdk.save( + table._id!, + row, + config.user._id + ) + + expect(response).toEqual({ + table: { + ...table, + schema: { + ...table.schema, + id: { + ...table.schema.id, + lastID: 1, + }, + }, + }, + row: { + ...row, + id: 1, + type: "row", + _rev: expect.stringMatching("1-.*"), + }, + squashed: { + ...row, + id: 1, + type: "row", + _rev: expect.stringMatching("1-.*"), + }, + }) + + const persistedRow = await config.getRow(table._id!, response.row._id!) + expect(persistedRow).toEqual({ + ...row, + type: "row", + id: 1, + _rev: expect.stringMatching("1-.*"), + createdAt: expect.any(String), + updatedAt: expect.any(String), + }) + }) + }) + + it("auto ids will update when creating new rows in parallel", async () => { + function makeRows(count: number) { + return Array.from({ length: count }, () => makeRow()) + } + + const table = await config.createTable({ + ...tableData, + schema: { + ...tableData.schema, + id: { + name: "id", + type: FieldType.AUTO, + subtype: AutoFieldSubTypes.AUTO_ID, + autocolumn: true, + lastID: 0, + }, + }, + }) + + await config.doInContext(config.appId, async () => { + for (const row of makeRows(5)) { + await internalSdk.save(table._id!, row, config.user._id) + } + await Promise.all( + makeRows(10).map(row => + internalSdk.save(table._id!, row, config.user._id) + ) + ) + for (const row of makeRows(5)) { + await internalSdk.save(table._id!, row, config.user._id) + } + }) + + const persistedRows = await config.getRows(table._id!) + expect(persistedRows).toHaveLength(20) + expect(persistedRows).toEqual( + expect.arrayContaining( + Array.from({ length: 20 }).map((_, i) => + expect.objectContaining({ id: i + 1 }) + ) + ) + ) + + const persistedTable = await config.getTable(table._id) + expect((table as any).schema.id.lastID).toBe(0) + expect(persistedTable.schema.id.lastID).toBe(20) + }) + }) +}) diff --git a/packages/types/src/sdk/locks.ts b/packages/types/src/sdk/locks.ts index 82a7089b3f..0e6053a4db 100644 --- a/packages/types/src/sdk/locks.ts +++ b/packages/types/src/sdk/locks.ts @@ -21,6 +21,7 @@ export enum LockName { PERSIST_WRITETHROUGH = "persist_writethrough", QUOTA_USAGE_EVENT = "quota_usage_event", APP_MIGRATION = "app_migrations", + PROCESS_AUTO_COLUMNS = "process_auto_columns", } export type LockOptions = {