From 1b0c551602cb1841489e354a1ca0bf211e3476d6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 19 Dec 2023 16:59:23 +0100 Subject: [PATCH 01/43] Create create-many script --- .../scripts/load/create-many-relationships.js | 47 +++++++++++++++++++ packages/server/scripts/load/utils.js | 43 ++++++++++------- 2 files changed, 73 insertions(+), 17 deletions(-) create mode 100755 packages/server/scripts/load/create-many-relationships.js 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..78353c029c --- /dev/null +++ b/packages/server/scripts/load/create-many-relationships.js @@ -0,0 +1,47 @@ +#!/bin/node +const { createApp, getTable, createRow, createTable } = require("./utils") + +const ROW_COUNT = 10 + +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 app = await createApp(apiKey) + console.log(`App created: ${app._id}`) + const table = await getTable(apiKey, app._id) + console.log(`Table found: ${table.name}`) + + const subjectTable = await createTable(apiKey, app._id, { + schema: { + title: { + name: "Name", + type: "string", + }, + }, + name: "Subjects", + }) + for (let i = 0; i < 10; i++) { + await createRow(apiKey, app._id, subjectTable) + } + + const promises = [] + for (let i = 0; i < ROW_COUNT; i++) { + promises.push(await createRow(apiKey, app._id, table)) + console.log(`Row ${i + 1} of ${ROW_COUNT} created`) + } + await Promise.all(promises) + + console.log(`App created: http://localhost:10000/builder/app/${app._id}`) +} + +run() + .then(() => { + console.log(`Finished creating ${ROW_COUNT} rows.`) + }) + .catch(err => { + console.error(err) + }) diff --git a/packages/server/scripts/load/utils.js b/packages/server/scripts/load/utils.js index 97ff8f1e13..7af867ab1d 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 = { @@ -38,29 +39,37 @@ exports.createApp = async apiKey => { } exports.getTable = async (apiKey, appId) => { - const res = await request(apiKey, URL_TABLE, "POST", {}, appId) + const res = await request(apiKey, URL_SEARCH_TABLE, "POST", {}, appId) const json = await res.json() return json.data[0] } -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.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.createTable = async (apiKey, appId, config) => { + const res = await request(apiKey, URL_TABLE, "POST", config, appId) + const json = await res.json() + return json.data +} From 1b6c7f27297f72b28f67966b5d10fde117c27506 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 11:51:35 +0100 Subject: [PATCH 02/43] Add content to subjects --- .../scripts/load/create-many-relationships.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 78353c029c..1589c1111e 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -1,6 +1,9 @@ #!/bin/node const { createApp, getTable, createRow, createTable } = require("./utils") +const Chance = require("chance") +const generator = new Chance() + const ROW_COUNT = 10 if (!process.argv[2]) { @@ -17,23 +20,26 @@ async function run() { const subjectTable = await createTable(apiKey, app._id, { schema: { - title: { + Name: { name: "Name", type: "string", }, }, name: "Subjects", }) - for (let i = 0; i < 10; i++) { - await createRow(apiKey, app._id, subjectTable) + + const SUBJECT_COUNT = 10 + for (let i = 0; i < SUBJECT_COUNT; i++) { + await createRow(apiKey, app._id, subjectTable, { + Name: generator.profession(), + }) + console.log(`Subject ${i + 1} of ${SUBJECT_COUNT} created`) } - const promises = [] for (let i = 0; i < ROW_COUNT; i++) { - promises.push(await createRow(apiKey, app._id, table)) + await createRow(apiKey, app._id, table) console.log(`Row ${i + 1} of ${ROW_COUNT} created`) } - await Promise.all(promises) console.log(`App created: http://localhost:10000/builder/app/${app._id}`) } From e986374c23618703e6fc3dce7b00490ac65808b6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 12:18:29 +0100 Subject: [PATCH 03/43] Check fetched table --- .../scripts/load/create-many-relationships.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 1589c1111e..926d0ac342 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -4,19 +4,27 @@ const { createApp, getTable, createRow, createTable } = require("./utils") const Chance = require("chance") const generator = new Chance() -const ROW_COUNT = 10 +const STUDENT_COUNT = 10 +const SUBJECT_COUNT = 10 if (!process.argv[2]) { console.error("Please specify an API key as script argument.") process.exit(-1) } +async function sleep(ms) { + return new Promise(r => setTimeout(() => r(), ms)) +} + async function run() { const apiKey = process.argv[2] const app = await createApp(apiKey) console.log(`App created: ${app._id}`) - const table = await getTable(apiKey, app._id) - console.log(`Table found: ${table.name}`) + const studentsTable = await getTable(apiKey, app._id) + if (studentsTable.name !== "Students") { + throw 'Fetched table should be "Students"' + } + console.log(`Table found: ${studentsTable.name}`) const subjectTable = await createTable(apiKey, app._id, { schema: { @@ -28,17 +36,17 @@ async function run() { name: "Subjects", }) - const SUBJECT_COUNT = 10 for (let i = 0; i < SUBJECT_COUNT; i++) { await createRow(apiKey, app._id, subjectTable, { Name: generator.profession(), }) console.log(`Subject ${i + 1} of ${SUBJECT_COUNT} created`) + await sleep(50) } - for (let i = 0; i < ROW_COUNT; i++) { - await createRow(apiKey, app._id, table) - console.log(`Row ${i + 1} of ${ROW_COUNT} created`) + for (let i = 0; i < STUDENT_COUNT; i++) { + await createRow(apiKey, app._id, studentsTable) + console.log(`Row ${i + 1} of ${STUDENT_COUNT} created`) } console.log(`App created: http://localhost:10000/builder/app/${app._id}`) From e4acd91237a0cb56b849d85cdeab162b2701df4c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 12:30:35 +0100 Subject: [PATCH 04/43] Populate rows --- .../scripts/load/create-many-relationships.js | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 926d0ac342..374dda58c2 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -4,7 +4,7 @@ const { createApp, getTable, createRow, createTable } = require("./utils") const Chance = require("chance") const generator = new Chance() -const STUDENT_COUNT = 10 +const STUDENT_COUNT = 100 const SUBJECT_COUNT = 10 if (!process.argv[2]) { @@ -19,7 +19,8 @@ async function sleep(ms) { async function run() { const apiKey = process.argv[2] const app = await createApp(apiKey) - console.log(`App created: ${app._id}`) + console.log(`App created: http://localhost:10000/builder/app/${app._id}`) + const studentsTable = await getTable(apiKey, app._id) if (studentsTable.name !== "Students") { throw 'Fetched table should be "Students"' @@ -44,18 +45,22 @@ async function run() { await sleep(50) } + let studentNumber = studentsTable.schema["Auto ID"].lastID for (let i = 0; i < STUDENT_COUNT; i++) { - await createRow(apiKey, app._id, studentsTable) + await createRow(apiKey, app._id, studentsTable, { + "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 }), + }) console.log(`Row ${i + 1} of ${STUDENT_COUNT} created`) } - - console.log(`App created: http://localhost:10000/builder/app/${app._id}`) } -run() - .then(() => { - console.log(`Finished creating ${ROW_COUNT} rows.`) - }) - .catch(err => { - console.error(err) - }) +run().catch(err => { + console.error(err) +}) From 6d6100eaf40f552b46f1bf94f4229dda2526024c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 12:52:25 +0100 Subject: [PATCH 05/43] Add grades --- .../scripts/load/create-many-relationships.js | 79 ++++++++++++++++--- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 374dda58c2..5d0cf3046a 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -27,6 +27,26 @@ async function run() { } console.log(`Table found: ${studentsTable.name}`) + const students = [], + subjects = [] + + let studentNumber = studentsTable.schema["Auto ID"].lastID + for (let i = 0; i < STUDENT_COUNT; i++) { + students.push( + await createRow(apiKey, app._id, studentsTable, { + "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 }), + }) + ) + console.log(`Row ${i + 1} of ${STUDENT_COUNT} created`) + } + const subjectTable = await createTable(apiKey, app._id, { schema: { Name: { @@ -35,29 +55,64 @@ async function run() { }, }, name: "Subjects", + primaryDisplay: "Name", }) for (let i = 0; i < SUBJECT_COUNT; i++) { + subjects.push( await createRow(apiKey, app._id, subjectTable, { Name: generator.profession(), }) + ) console.log(`Subject ${i + 1} of ${SUBJECT_COUNT} created`) await sleep(50) } - let studentNumber = studentsTable.schema["Auto ID"].lastID - for (let i = 0; i < STUDENT_COUNT; i++) { - await createRow(apiKey, app._id, studentsTable, { - "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 gradesTable = await createTable(apiKey, app._id, { + 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", + }, + }, + name: "Grades", + }) + + let i = 0 + for (const student of students) { + for (const subject of subjects) { + await createRow(apiKey, app._id, gradesTable, { + Score: generator.integer({ min: 0, max: 100 }), + Student: [student], + Subject: [subject], }) - console.log(`Row ${i + 1} of ${STUDENT_COUNT} created`) + console.log( + `Grade ${++i} of ${students.length * subjects.length} created` + ) + await sleep(20) + } } } From 192d980e53d7e6e9c2c215de6e8857436661976a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 13:04:16 +0100 Subject: [PATCH 06/43] Log timings --- .../scripts/load/create-many-relationships.js | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 5d0cf3046a..1c146ccaa0 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -4,7 +4,7 @@ const { createApp, getTable, createRow, createTable } = require("./utils") const Chance = require("chance") const generator = new Chance() -const STUDENT_COUNT = 100 +const STUDENT_COUNT = 500 const SUBJECT_COUNT = 10 if (!process.argv[2]) { @@ -16,6 +16,8 @@ async function sleep(ms) { return new Promise(r => setTimeout(() => r(), ms)) } +const start = Date.now() + async function run() { const apiKey = process.argv[2] const app = await createApp(apiKey) @@ -44,7 +46,11 @@ async function run() { "Attendance_(%)": generator.integer({ min: 0, max: 100 }), }) ) - console.log(`Row ${i + 1} of ${STUDENT_COUNT} created`) + console.log( + `Student ${i + 1} of ${STUDENT_COUNT} created (${ + (Date.now() - start) / 1000 + }s)` + ) } const subjectTable = await createTable(apiKey, app._id, { @@ -60,11 +66,15 @@ async function run() { for (let i = 0; i < SUBJECT_COUNT; i++) { subjects.push( - await createRow(apiKey, app._id, subjectTable, { - Name: generator.profession(), - }) + await createRow(apiKey, app._id, subjectTable, { + Name: generator.profession(), + }) + ) + console.log( + `Subject ${i + 1} of ${SUBJECT_COUNT} created (${ + (Date.now() - start) / 1000 + }s)` ) - console.log(`Subject ${i + 1} of ${SUBJECT_COUNT} created`) await sleep(50) } @@ -107,15 +117,21 @@ async function run() { Score: generator.integer({ min: 0, max: 100 }), Student: [student], Subject: [subject], - }) + }) console.log( - `Grade ${++i} of ${students.length * subjects.length} created` + `Grade ${++i} of ${students.length * subjects.length} created (${ + (Date.now() - start) / 1000 + }s)` ) await sleep(20) } } } -run().catch(err => { - console.error(err) -}) +run() + .then(() => { + console.log(`Done in(${(Date.now() - start) / 1000} seconds`) + }) + .catch(err => { + console.error(err) + }) From 5240b04a3a73997842d904c13dd9add656722da4 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 13:09:02 +0100 Subject: [PATCH 07/43] Remove 429 in dev --- packages/server/scripts/load/create-many-relationships.js | 6 ------ packages/server/src/api/routes/public/index.ts | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 1c146ccaa0..ee05cc8fba 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -12,10 +12,6 @@ if (!process.argv[2]) { process.exit(-1) } -async function sleep(ms) { - return new Promise(r => setTimeout(() => r(), ms)) -} - const start = Date.now() async function run() { @@ -75,7 +71,6 @@ async function run() { (Date.now() - start) / 1000 }s)` ) - await sleep(50) } const gradesTable = await createTable(apiKey, app._id, { @@ -123,7 +118,6 @@ async function run() { (Date.now() - start) / 1000 }s)` ) - await sleep(20) } } } 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) } From 57a7be7a266d7d49379166766805ab8a05523051 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 15:02:21 +0100 Subject: [PATCH 08/43] Run creation in parallel --- .../scripts/load/create-many-relationships.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index ee05cc8fba..49d4efb28d 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -25,12 +25,11 @@ async function run() { } console.log(`Table found: ${studentsTable.name}`) - const students = [], - subjects = [] - + const studentsPromise = [] let studentNumber = studentsTable.schema["Auto ID"].lastID for (let i = 0; i < STUDENT_COUNT; i++) { - students.push( + studentsPromise.push( + (async () => { await createRow(apiKey, app._id, studentsTable, { "Student Number": (++studentNumber).toString(), "First Name": generator.first(), @@ -41,14 +40,18 @@ async function run() { "Home Number": generator.phone(), "Attendance_(%)": generator.integer({ min: 0, max: 100 }), }) - ) + console.log( `Student ${i + 1} of ${STUDENT_COUNT} created (${ (Date.now() - start) / 1000 }s)` + ) + })() ) } + const students = await Promise.all(studentsPromise) + const subjectTable = await createTable(apiKey, app._id, { schema: { Name: { @@ -60,19 +63,24 @@ async function run() { primaryDisplay: "Name", }) + const subjectPromises = [] for (let i = 0; i < SUBJECT_COUNT; i++) { - subjects.push( + subjectPromises.push( + (async () => { await createRow(apiKey, app._id, subjectTable, { Name: generator.profession(), }) - ) console.log( `Subject ${i + 1} of ${SUBJECT_COUNT} created (${ (Date.now() - start) / 1000 }s)` + ) + })() ) } + const subjects = await Promise.all(subjectPromises) + const gradesTable = await createTable(apiKey, app._id, { schema: { Score: { @@ -124,7 +132,7 @@ async function run() { run() .then(() => { - console.log(`Done in(${(Date.now() - start) / 1000} seconds`) + console.log(`Done in ${(Date.now() - start) / 1000} seconds`) }) .catch(err => { console.error(err) From ed32b701ecd82bd0d1971839a830ec0b46b3c4df Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 15:41:25 +0100 Subject: [PATCH 09/43] Move row save to sdk --- .../src/api/controllers/row/external.ts | 46 +--------------- .../server/src/api/controllers/row/index.ts | 11 ++-- .../src/api/controllers/row/internal.ts | 41 +------------- packages/server/src/sdk/app/rows/external.ts | 54 ++++++++++++++++++- packages/server/src/sdk/app/rows/internal.ts | 49 +++++++++++++++++ packages/server/src/sdk/app/rows/rows.ts | 18 +++++++ 6 files changed, 129 insertions(+), 90 deletions(-) create mode 100644 packages/server/src/sdk/app/rows/internal.ts 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/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..c735d3e430 --- /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 "src/api/controllers/row/staticFormula" +import { inputProcessing } from "src/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) +} From 0452b1a307967184f02e8074ae1321d4fd3725a8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 16:25:08 +0100 Subject: [PATCH 10/43] Fix imports --- packages/server/src/sdk/app/rows/internal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/sdk/app/rows/internal.ts b/packages/server/src/sdk/app/rows/internal.ts index c735d3e430..14e771b36e 100644 --- a/packages/server/src/sdk/app/rows/internal.ts +++ b/packages/server/src/sdk/app/rows/internal.ts @@ -2,8 +2,8 @@ import { db } from "@budibase/backend-core" import { Row } from "@budibase/types" import sdk from "../../../sdk" import cloneDeep from "lodash/fp/cloneDeep" -import { finaliseRow } from "src/api/controllers/row/staticFormula" -import { inputProcessing } from "src/utilities/rowProcessor" +import { finaliseRow } from "../../../api/controllers/row/staticFormula" +import { inputProcessing } from "../../../utilities/rowProcessor" import * as linkRows from "../../../db/linkedRows" export async function save( From becb7bd46d3b6c5f829359da3ee0d72d5d31fab7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 17:26:51 +0100 Subject: [PATCH 11/43] Add tests --- .../src/sdk/app/rows/tests/internal.spec.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 packages/server/src/sdk/app/rows/tests/internal.spec.ts 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..0d19543889 --- /dev/null +++ b/packages/server/src/sdk/app/rows/tests/internal.spec.ts @@ -0,0 +1,127 @@ +import * as internalSdk from "../internal" + +import { generator } from "@budibase/backend-core/tests" +import { + INTERNAL_TABLE_SOURCE_ID, + TableSourceType, + FieldType, + Table, +} from "@budibase/types" + +import TestConfiguration from "../../../../tests/utilities/TestConfiguration" + +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: { + id: { + name: "id", + type: FieldType.AUTO, + autocolumn: true, + lastID: 0, + }, + 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.only("can persist in parallel", async () => { + const table = await config.createTable(tableData) + const rows = Array.from({ length: 100 }, () => makeRow()) + + await config.doInContext(config.appId, async () => { + await Promise.all( + rows.map(row => internalSdk.save(table._id!, row, config.user._id)) + ) + + const persistedRows = await config.getRows(table._id!) + expect(persistedRows).toHaveLength(100) + }) + }) + }) +}) From 3824156ca5b90908d5a078ffb5d375194dd31ba8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 20 Dec 2023 17:36:23 +0100 Subject: [PATCH 12/43] Add delete all apps script --- .../server/scripts/load/delete-all-apps.js | 29 +++++++++++++++++++ packages/server/scripts/load/utils.js | 11 +++++++ 2 files changed, 40 insertions(+) create mode 100755 packages/server/scripts/load/delete-all-apps.js 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 7af867ab1d..04ebb6f3d6 100644 --- a/packages/server/scripts/load/utils.js +++ b/packages/server/scripts/load/utils.js @@ -38,6 +38,17 @@ exports.createApp = async apiKey => { 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.deleteApp = async (apiKey, appId) => { + const res = await request(apiKey, `${URL_APP}/${appId}`, "DELETE") + return res +} + exports.getTable = async (apiKey, appId) => { const res = await request(apiKey, URL_SEARCH_TABLE, "POST", {}, appId) const json = await res.json() From f722ae970cdd79f7cbdd7bf7c4d1c6182932b91b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 10:30:27 +0100 Subject: [PATCH 13/43] Add autoid tests (failing) --- .../src/sdk/app/rows/tests/internal.spec.ts | 112 ++++++++++++++++-- 1 file changed, 99 insertions(+), 13 deletions(-) diff --git a/packages/server/src/sdk/app/rows/tests/internal.spec.ts b/packages/server/src/sdk/app/rows/tests/internal.spec.ts index 0d19543889..62a056bef3 100644 --- a/packages/server/src/sdk/app/rows/tests/internal.spec.ts +++ b/packages/server/src/sdk/app/rows/tests/internal.spec.ts @@ -1,3 +1,4 @@ +import tk from "timekeeper" import * as internalSdk from "../internal" import { generator } from "@budibase/backend-core/tests" @@ -6,9 +7,13 @@ import { 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() @@ -33,12 +38,6 @@ describe("sdk >> rows >> internal", () => { sourceId: INTERNAL_TABLE_SOURCE_ID, sourceType: TableSourceType.INTERNAL, schema: { - id: { - name: "id", - type: FieldType.AUTO, - autocolumn: true, - lastID: 0, - }, name: { name: "name", type: FieldType.STRING, @@ -110,18 +109,105 @@ describe("sdk >> rows >> internal", () => { }) }) - it.only("can persist in parallel", async () => { - const table = await config.createTable(tableData) - const rows = Array.from({ length: 100 }, () => makeRow()) + 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 () => { - await Promise.all( - rows.map(row => internalSdk.save(table._id!, row, config.user._id)) + const response = await internalSdk.save( + table._id!, + row, + config.user._id ) - const persistedRows = await config.getRows(table._id!) - expect(persistedRows).toHaveLength(100) + 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(30)) { + await internalSdk.save(table._id!, row, config.user._id) + } + await Promise.all( + makeRows(200).map(row => + internalSdk.save(table._id!, row, config.user._id) + ) + ) + for (const row of makeRows(20)) { + await internalSdk.save(table._id!, row, config.user._id) + } + }) + + const persistedRows = await config.getRows(table._id!) + expect(persistedRows).toHaveLength(250) + + const persistedTable = await config.getTable(table._id) + expect((table as any).schema.id.lastID).toBe(0) + expect(persistedTable.schema.id.lastID).toBe(250) + }) }) }) From c0a19c2a7ef5a7319997ec019caa3be7c5895dda Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 15:45:30 +0100 Subject: [PATCH 14/43] Type cache arguments --- packages/backend-core/src/cache/generic.ts | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) 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) From 0c77cf2b40d6fcec40de872934e7a90e79278ea8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 17:14:34 +0100 Subject: [PATCH 15/43] Fix concurrent saves --- .../src/api/controllers/row/staticFormula.ts | 26 +++++++++++++------ .../src/sdk/app/rows/tests/internal.spec.ts | 17 ++++++++---- packages/types/src/sdk/locks.ts | 1 + 3 files changed, 31 insertions(+), 13 deletions(-) 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/sdk/app/rows/tests/internal.spec.ts b/packages/server/src/sdk/app/rows/tests/internal.spec.ts index 62a056bef3..b60d31b226 100644 --- a/packages/server/src/sdk/app/rows/tests/internal.spec.ts +++ b/packages/server/src/sdk/app/rows/tests/internal.spec.ts @@ -189,25 +189,32 @@ describe("sdk >> rows >> internal", () => { }) await config.doInContext(config.appId, async () => { - for (const row of makeRows(30)) { + for (const row of makeRows(5)) { await internalSdk.save(table._id!, row, config.user._id) } await Promise.all( - makeRows(200).map(row => + makeRows(10).map(row => internalSdk.save(table._id!, row, config.user._id) ) ) - for (const row of makeRows(20)) { + 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(250) + 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(250) + 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 = { From 69527cd4b964ab6fffdf7009161ecf9ec3c493be Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 17:22:41 +0100 Subject: [PATCH 16/43] Parallel creations --- .../scripts/load/create-many-relationships.js | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 49d4efb28d..dba953e4c7 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -25,11 +25,10 @@ async function run() { } console.log(`Table found: ${studentsTable.name}`) - const studentsPromise = [] let studentNumber = studentsTable.schema["Auto ID"].lastID - for (let i = 0; i < STUDENT_COUNT; i++) { - studentsPromise.push( - (async () => { + let i = 0 + const students = await Promise.all( + Array.from({ length: STUDENT_COUNT }).map(async () => { await createRow(apiKey, app._id, studentsTable, { "Student Number": (++studentNumber).toString(), "First Name": generator.first(), @@ -42,15 +41,12 @@ async function run() { }) console.log( - `Student ${i + 1} of ${STUDENT_COUNT} created (${ + `Student ${++i} of ${STUDENT_COUNT} created (${ (Date.now() - start) / 1000 }s)` ) - })() + }) ) - } - - const students = await Promise.all(studentsPromise) const subjectTable = await createTable(apiKey, app._id, { schema: { @@ -63,23 +59,19 @@ async function run() { primaryDisplay: "Name", }) - const subjectPromises = [] - for (let i = 0; i < SUBJECT_COUNT; i++) { - subjectPromises.push( - (async () => { + i = 0 + const subjects = await Promise.all( + Array.from({ length: SUBJECT_COUNT }).map(async () => { await createRow(apiKey, app._id, subjectTable, { Name: generator.profession(), }) console.log( - `Subject ${i + 1} of ${SUBJECT_COUNT} created (${ + `Subject ${++i} of ${SUBJECT_COUNT} created (${ (Date.now() - start) / 1000 }s)` ) - })() + }) ) - } - - const subjects = await Promise.all(subjectPromises) const gradesTable = await createTable(apiKey, app._id, { schema: { @@ -113,7 +105,7 @@ async function run() { name: "Grades", }) - let i = 0 + i = 0 for (const student of students) { for (const subject of subjects) { await createRow(apiKey, app._id, gradesTable, { From b034542536b9a2b922bed014697447ed50c9b85c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 17:24:48 +0100 Subject: [PATCH 17/43] Fix --- .../scripts/load/create-many-relationships.js | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index dba953e4c7..c1e59bd586 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -29,7 +29,7 @@ async function run() { let i = 0 const students = await Promise.all( Array.from({ length: STUDENT_COUNT }).map(async () => { - await createRow(apiKey, app._id, studentsTable, { + const row = await createRow(apiKey, app._id, studentsTable, { "Student Number": (++studentNumber).toString(), "First Name": generator.first(), "Last Name": generator.last(), @@ -40,13 +40,14 @@ async function run() { "Attendance_(%)": generator.integer({ min: 0, max: 100 }), }) - console.log( + console.log( `Student ${++i} of ${STUDENT_COUNT} created (${ - (Date.now() - start) / 1000 - }s)` - ) + (Date.now() - start) / 1000 + }s)` + ) + return row }) - ) + ) const subjectTable = await createTable(apiKey, app._id, { schema: { @@ -62,16 +63,17 @@ async function run() { i = 0 const subjects = await Promise.all( Array.from({ length: SUBJECT_COUNT }).map(async () => { - await createRow(apiKey, app._id, subjectTable, { + const row = await createRow(apiKey, app._id, subjectTable, { Name: generator.profession(), }) - console.log( + console.log( `Subject ${++i} of ${SUBJECT_COUNT} created (${ - (Date.now() - start) / 1000 - }s)` - ) + (Date.now() - start) / 1000 + }s)` + ) + return row }) - ) + ) const gradesTable = await createTable(apiKey, app._id, { schema: { From c46509d6cb2673cb8738a63c67ea8c65e85eeeeb Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 17:39:14 +0100 Subject: [PATCH 18/43] Use batches --- .../scripts/load/create-many-relationships.js | 80 +++++++++++-------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index c1e59bd586..f034cb0cc2 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -7,6 +7,8 @@ const generator = new Chance() const STUDENT_COUNT = 500 const SUBJECT_COUNT = 10 +const batchSize = 100 + if (!process.argv[2]) { console.error("Please specify an API key as script argument.") process.exit(-1) @@ -14,6 +16,31 @@ if (!process.argv[2]) { const start = Date.now() +async function batchCreate(apiKey, appId, count, table, generator) { + let i = 0 + + async function createSingleRow() { + const row = await createRow(apiKey, appId, table, generator()) + console.log( + `${table.name} - ${++i} of ${count} created (${ + (Date.now() - start) / 1000 + }s)` + ) + return row + } + + const rows = [] + for (let j = 0; j < count; j += batchSize) { + const batchPromises = Array.from( + { length: Math.min(batchSize, count - j) }, + createSingleRow + ) + const batchRows = await Promise.all(batchPromises) + rows.push(...batchRows) + } + return rows +} + async function run() { const apiKey = process.argv[2] const app = await createApp(apiKey) @@ -26,26 +53,20 @@ async function run() { console.log(`Table found: ${studentsTable.name}`) let studentNumber = studentsTable.schema["Auto ID"].lastID - let i = 0 - const students = await Promise.all( - Array.from({ length: STUDENT_COUNT }).map(async () => { - const row = await createRow(apiKey, app._id, studentsTable, { - "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 }), - }) - - console.log( - `Student ${++i} of ${STUDENT_COUNT} created (${ - (Date.now() - start) / 1000 - }s)` - ) - return row + const students = await batchCreate( + apiKey, + app._id, + STUDENT_COUNT, + studentsTable, + () => ({ + "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 }), }) ) @@ -60,18 +81,13 @@ async function run() { primaryDisplay: "Name", }) - i = 0 - const subjects = await Promise.all( - Array.from({ length: SUBJECT_COUNT }).map(async () => { - const row = await createRow(apiKey, app._id, subjectTable, { - Name: generator.profession(), - }) - console.log( - `Subject ${++i} of ${SUBJECT_COUNT} created (${ - (Date.now() - start) / 1000 - }s)` - ) - return row + const subjects = await batchCreate( + apiKey, + app._id, + SUBJECT_COUNT, + subjectTable, + () => ({ + Name: generator.profession(), }) ) From dbf4d6dbdd5d76fb52b43ae4434f2e719ae9dddf Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 17:48:39 +0100 Subject: [PATCH 19/43] Create grades in batch --- .../scripts/load/create-many-relationships.js | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index f034cb0cc2..a90c21d250 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -7,8 +7,6 @@ const generator = new Chance() const STUDENT_COUNT = 500 const SUBJECT_COUNT = 10 -const batchSize = 100 - if (!process.argv[2]) { console.error("Please specify an API key as script argument.") process.exit(-1) @@ -16,13 +14,13 @@ if (!process.argv[2]) { const start = Date.now() -async function batchCreate(apiKey, appId, count, table, generator) { +async function batchCreate(apiKey, appId, table, items, batchSize = 100) { let i = 0 - async function createSingleRow() { - const row = await createRow(apiKey, appId, table, generator()) + async function createSingleRow(item) { + const row = await createRow(apiKey, appId, table, item) console.log( - `${table.name} - ${++i} of ${count} created (${ + `${table.name} - ${++i} of ${items.length} created (${ (Date.now() - start) / 1000 }s)` ) @@ -30,11 +28,8 @@ async function batchCreate(apiKey, appId, count, table, generator) { } const rows = [] - for (let j = 0; j < count; j += batchSize) { - const batchPromises = Array.from( - { length: Math.min(batchSize, count - j) }, - createSingleRow - ) + for (let j = 0; j < items.length; j += batchSize) { + const batchPromises = items.slice(j, j + batchSize).map(createSingleRow) const batchRows = await Promise.all(batchPromises) rows.push(...batchRows) } @@ -56,9 +51,8 @@ async function run() { const students = await batchCreate( apiKey, app._id, - STUDENT_COUNT, studentsTable, - () => ({ + Array.from({ length: SUBJECT_COUNT }).map(() => ({ "Student Number": (++studentNumber).toString(), "First Name": generator.first(), "Last Name": generator.last(), @@ -67,7 +61,7 @@ async function run() { "Tardiness (Days)": generator.integer({ min: 1, max: 100 }), "Home Number": generator.phone(), "Attendance_(%)": generator.integer({ min: 0, max: 100 }), - }) + })) ) const subjectTable = await createTable(apiKey, app._id, { @@ -84,11 +78,10 @@ async function run() { const subjects = await batchCreate( apiKey, app._id, - SUBJECT_COUNT, subjectTable, - () => ({ + Array.from({ length: SUBJECT_COUNT }).map(() => ({ Name: generator.profession(), - }) + })) ) const gradesTable = await createTable(apiKey, app._id, { @@ -123,21 +116,18 @@ async function run() { name: "Grades", }) - i = 0 - for (const student of students) { - for (const subject of subjects) { - await createRow(apiKey, app._id, gradesTable, { + await batchCreate( + apiKey, + app._id, + gradesTable, + students.flatMap(student => + subjects.map(subject => ({ Score: generator.integer({ min: 0, max: 100 }), Student: [student], Subject: [subject], - }) - console.log( - `Grade ${++i} of ${students.length * subjects.length} created (${ - (Date.now() - start) / 1000 - }s)` - ) - } - } + })) + ) + ) } run() From 3eee77c5498328547c5b7c5ff9368eda5757718d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 17:52:04 +0100 Subject: [PATCH 20/43] Create 500 students --- packages/server/scripts/load/create-many-relationships.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index a90c21d250..4626b8fe24 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -52,7 +52,7 @@ async function run() { apiKey, app._id, studentsTable, - Array.from({ length: SUBJECT_COUNT }).map(() => ({ + Array.from({ length: STUDENT_COUNT }).map(() => ({ "Student Number": (++studentNumber).toString(), "First Name": generator.first(), "Last Name": generator.last(), From c518d8a7fad730d9c7798080840dcf0abfb3bbf1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 18:02:44 +0100 Subject: [PATCH 21/43] Add logs --- packages/server/scripts/load/create-many-relationships.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 4626b8fe24..92db2f83a1 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -128,6 +128,10 @@ async function run() { })) ) ) + + console.log( + `Access the app here: http://localhost:10000/builder/app/${app._id}` + ) } run() From 92f1107e6caf686bc24820f0a11527299de3fbb1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 18:28:10 +0100 Subject: [PATCH 22/43] Prevent one single request to stop the process --- .../scripts/load/create-many-relationships.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 92db2f83a1..d7526a70ff 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -18,13 +18,17 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 100) { let i = 0 async function createSingleRow(item) { - const row = await createRow(apiKey, appId, table, item) - console.log( - `${table.name} - ${++i} of ${items.length} created (${ - (Date.now() - start) / 1000 - }s)` - ) - return row + 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 { + console.error("Error creating row", item) + } } const rows = [] From 494a2ff91f5b87e85658f1ffcd06c1da9887de89 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 18:29:57 +0100 Subject: [PATCH 23/43] Errors --- packages/server/scripts/load/create-many-relationships.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index d7526a70ff..15ef9bcb1e 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -17,6 +17,7 @@ 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) @@ -27,7 +28,7 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 100) { ) return row } catch { - console.error("Error creating row", item) + errors++ } } @@ -37,6 +38,9 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 100) { const batchRows = await Promise.all(batchPromises) rows.push(...batchRows) } + if (errors) { + console.error(`Error creating ${errors} row`) + } return rows } From abf025b3f7e5fbe2458b53995f362268323cb376 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 21 Dec 2023 19:06:26 +0100 Subject: [PATCH 24/43] Use inflight max instead of batch wait --- .../scripts/load/create-many-relationships.js | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 15ef9bcb1e..0f4fe3470c 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -13,11 +13,10 @@ if (!process.argv[2]) { } const start = Date.now() - -async function batchCreate(apiKey, appId, table, items, batchSize = 100) { +async function batchCreate(apiKey, appId, table, items, batchSize = 10) { let i = 0 - let errors = 0 + async function createSingleRow(item) { try { const row = await createRow(apiKey, appId, table, item) @@ -33,14 +32,25 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 100) { } const rows = [] - for (let j = 0; j < items.length; j += batchSize) { - const batchPromises = items.slice(j, j + batchSize).map(createSingleRow) - const batchRows = await Promise.all(batchPromises) - rows.push(...batchRows) - } - if (errors) { - console.error(`Error creating ${errors} row`) + 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) + delete inFlight[itemIndex] + }) + + inFlight[itemIndex] = promise + + if (Object.keys(inFlight).length >= maxConcurrency) { + await Promise.race(Object.values(inFlight)) + } } + + await Promise.all(Object.values(inFlight)) + return rows } From 31c01e0a6b8338aa6c58772ff572e08c16bf6620 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Dec 2023 10:05:49 +0100 Subject: [PATCH 25/43] Display errors --- .../server/scripts/load/create-many-relationships.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 0f4fe3470c..14eaccd99c 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -2,6 +2,7 @@ const { createApp, getTable, createRow, createTable } = require("./utils") const Chance = require("chance") + const generator = new Chance() const STUDENT_COUNT = 500 @@ -13,7 +14,7 @@ if (!process.argv[2]) { } const start = Date.now() -async function batchCreate(apiKey, appId, table, items, batchSize = 10) { +async function batchCreate(apiKey, appId, table, items, batchSize = 1000) { let i = 0 let errors = 0 @@ -51,6 +52,14 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 10) { await Promise.all(Object.values(inFlight)) + if (errors) { + console.error( + `${table.name} - ${errors} creation errored (${ + (Date.now() - start) / 1000 + }s)` + ) + } + return rows } From 3e991cc2f13ba80aca8fc85e63b1bf087a1da412 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Dec 2023 11:36:29 +0100 Subject: [PATCH 26/43] Use yargs --- .../server/scripts/load/create-many-relationships.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 14eaccd99c..84d1f94dda 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -1,5 +1,5 @@ #!/bin/node -const { createApp, getTable, createRow, createTable } = require("./utils") +const yargs = require("yargs") const Chance = require("chance") @@ -8,10 +8,9 @@ const generator = new Chance() const STUDENT_COUNT = 500 const SUBJECT_COUNT = 10 -if (!process.argv[2]) { - console.error("Please specify an API key as script argument.") - process.exit(-1) -} +const { apiKey, appId } = require("yargs") + .demandOption(["apiKey"]) + .option("appId").argv const start = Date.now() async function batchCreate(apiKey, appId, table, items, batchSize = 1000) { @@ -64,7 +63,6 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 1000) { } async function run() { - const apiKey = process.argv[2] const app = await createApp(apiKey) console.log(`App created: http://localhost:10000/builder/app/${app._id}`) From c5a50a911f64b7094c8941b12b5eb6b34e70b9a0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Dec 2023 11:46:49 +0100 Subject: [PATCH 27/43] Allow reusing app --- .../scripts/load/create-many-relationships.js | 54 ++++++++++++------- packages/server/scripts/load/utils.js | 13 ++++- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 84d1f94dda..f683d3aed4 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -1,5 +1,11 @@ #!/bin/node -const yargs = require("yargs") +const { + createApp, + getTable, + createRow, + createTable, + getApp, +} = require("./utils") const Chance = require("chance") @@ -8,12 +14,12 @@ const generator = new Chance() const STUDENT_COUNT = 500 const SUBJECT_COUNT = 10 -const { apiKey, appId } = require("yargs") +let { apiKey, appId } = require("yargs") .demandOption(["apiKey"]) .option("appId").argv const start = Date.now() -async function batchCreate(apiKey, appId, table, items, batchSize = 1000) { +async function batchCreate(apiKey, appId, table, items, batchSize = 100) { let i = 0 let errors = 0 @@ -62,20 +68,34 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 1000) { return rows } -async function run() { - const app = await createApp(apiKey) - console.log(`App created: http://localhost:10000/builder/app/${app._id}`) +const useExistingApp = !!appId - const studentsTable = await getTable(apiKey, app._id) - if (studentsTable.name !== "Students") { - throw 'Fetched table should be "Students"' +async function upsertTable(appId, tableName, tableData) { + if (useExistingApp) { + return await getTable(apiKey, appId, tableName) } - console.log(`Table found: ${studentsTable.name}`) + + 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: 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, - app._id, + appId, studentsTable, Array.from({ length: STUDENT_COUNT }).map(() => ({ "Student Number": (++studentNumber).toString(), @@ -89,27 +109,26 @@ async function run() { })) ) - const subjectTable = await createTable(apiKey, app._id, { + const subjectTable = await upsertTable(appId, "Subjects", { schema: { Name: { name: "Name", type: "string", }, }, - name: "Subjects", primaryDisplay: "Name", }) const subjects = await batchCreate( apiKey, - app._id, + appId, subjectTable, Array.from({ length: SUBJECT_COUNT }).map(() => ({ Name: generator.profession(), })) ) - const gradesTable = await createTable(apiKey, app._id, { + const gradesTable = await upsertTable(appId, "Grades", { schema: { Score: { name: "Score", @@ -138,12 +157,11 @@ async function run() { type: "link", }, }, - name: "Grades", }) await batchCreate( apiKey, - app._id, + appId, gradesTable, students.flatMap(student => subjects.map(subject => ({ @@ -155,7 +173,7 @@ async function run() { ) console.log( - `Access the app here: http://localhost:10000/builder/app/${app._id}` + `Access the app here: http://localhost:10000/builder/app/${appId}` ) } diff --git a/packages/server/scripts/load/utils.js b/packages/server/scripts/load/utils.js index 04ebb6f3d6..2b789cde52 100644 --- a/packages/server/scripts/load/utils.js +++ b/packages/server/scripts/load/utils.js @@ -38,6 +38,11 @@ exports.createApp = async apiKey => { return json.data } +exports.getApp = async (apiKey, appId) => { + const res = await request(apiKey, `${URL_APP}/${appId}`, "GET") + const json = await res.json() + return json.data +} exports.searchApps = async apiKey => { const res = await request(apiKey, `${URL_APP}/search`, "POST", {}) const json = await res.json() @@ -49,10 +54,14 @@ exports.deleteApp = async (apiKey, appId) => { return res } -exports.getTable = async (apiKey, appId) => { +exports.getTable = async (apiKey, appId, tableName) => { const res = await request(apiKey, URL_SEARCH_TABLE, "POST", {}, appId) const json = await res.json() - return json.data[0] + 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) => { From bd221f27270a56bb6b9d363866dfc3c07a5dfa9a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Dec 2023 11:52:48 +0100 Subject: [PATCH 28/43] Prevent locks --- .../server/scripts/load/create-many-relationships.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index f683d3aed4..07861cc632 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -43,10 +43,13 @@ async function batchCreate(apiKey, appId, table, items, batchSize = 100) { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const item = items[itemIndex] - const promise = createSingleRow(item).then(result => { - rows.push(result) - delete inFlight[itemIndex] - }) + const promise = createSingleRow(item) + .then(result => { + rows.push(result) + }) + .finally(() => { + delete inFlight[itemIndex] + }) inFlight[itemIndex] = promise From 5a45249ad4f802d5b6a0d724d99207218ae7a025 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 22 Dec 2023 12:18:24 +0100 Subject: [PATCH 29/43] Reuse subjects --- .../scripts/load/create-many-relationships.js | 19 +++++++++++-------- packages/server/scripts/load/utils.js | 6 ++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 07861cc632..13f861ec0b 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -5,6 +5,7 @@ const { createRow, createTable, getApp, + getRows, } = require("./utils") const Chance = require("chance") @@ -122,14 +123,16 @@ async function run() { primaryDisplay: "Name", }) - const subjects = await batchCreate( - apiKey, - appId, - subjectTable, - Array.from({ length: SUBJECT_COUNT }).map(() => ({ - Name: generator.profession(), - })) - ) + 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: { diff --git a/packages/server/scripts/load/utils.js b/packages/server/scripts/load/utils.js index 2b789cde52..1dabdcec9a 100644 --- a/packages/server/scripts/load/utils.js +++ b/packages/server/scripts/load/utils.js @@ -88,6 +88,12 @@ exports.createRow = async (apiKey, appId, table, body) => { 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() From 3cfe641486af117b1db5468e7054909c720b3427 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 22 Dec 2023 18:28:07 +0000 Subject: [PATCH 30/43] Fixing issue with Redis disconnection - this should correctly reconnect the service when Redis service becomes available again. --- packages/backend-core/src/queue/queue.ts | 2 +- packages/backend-core/src/redis/redis.ts | 21 +++++++-------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index 0657437a3b..b95dace5b2 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -47,7 +47,7 @@ export function createQueue( cleanupInterval = timers.set(cleanup, CLEANUP_PERIOD_MS) // fire off an initial cleanup cleanup().catch(err => { - console.error(`Unable to cleanup automation queue initially - ${err}`) + console.error(`Unable to cleanup ${jobQueue} initially - ${err}`) }) } return queue diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 701e262091..d15453ba62 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -18,6 +18,7 @@ import { SelectableDatabase, getRedisConnectionDetails, } from "./utils" +import { logAlert } from "../logging" import * as timers from "../timers" const RETRY_PERIOD_MS = 2000 @@ -39,21 +40,16 @@ function pickClient(selectDb: number): any { return CLIENTS[selectDb] } -function connectionError( - selectDb: number, - timeout: NodeJS.Timeout, - err: Error | string -) { +function connectionError(timeout: NodeJS.Timeout, err: Error | string) { // manually shut down, ignore errors if (CLOSED) { return } - pickClient(selectDb).disconnect() CLOSED = true // always clear this on error clearTimeout(timeout) CONNECTED = false - console.error("Redis connection failed - " + err) + logAlert("Redis connection failed", err) setTimeout(() => { init() }, RETRY_PERIOD_MS) @@ -79,11 +75,7 @@ function init(selectDb = DEFAULT_SELECT_DB) { // start the timer - only allowed 5 seconds to connect timeout = setTimeout(() => { if (!CONNECTED) { - connectionError( - selectDb, - timeout, - "Did not successfully connect in timeout" - ) + connectionError(timeout, "Did not successfully connect in timeout") } }, STARTUP_TIMEOUT_MS) @@ -106,12 +98,13 @@ function init(selectDb = DEFAULT_SELECT_DB) { // allow the process to exit return } - connectionError(selectDb, timeout, err) + connectionError(timeout, err) }) client.on("error", (err: Error) => { - connectionError(selectDb, timeout, err) + connectionError(timeout, err) }) client.on("connect", () => { + console.log(`Connected to Redis DB: ${selectDb}`) clearTimeout(timeout) CONNECTED = true }) From a6537e66c3085e70e89ac76aeaea4ee33b22e9d6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 29 Dec 2023 10:26:03 +0100 Subject: [PATCH 31/43] Set scim config on beforeall --- packages/worker/src/api/routes/global/tests/scim.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim.spec.ts b/packages/worker/src/api/routes/global/tests/scim.spec.ts index 884625805c..3614328d91 100644 --- a/packages/worker/src/api/routes/global/tests/scim.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim.spec.ts @@ -14,9 +14,14 @@ import { events } from "@budibase/backend-core" jest.retryTimes(2, { logErrorsBeforeRetry: true }) jest.setTimeout(30000) -mocks.licenses.useScimIntegration() - describe("scim", () => { + beforeAll(async () => { + tk.freeze(mocks.date.MOCK_DATE) + mocks.licenses.useScimIntegration() + + await config.setSCIMConfig(true) + }) + beforeEach(async () => { jest.resetAllMocks() tk.freeze(mocks.date.MOCK_DATE) From fede6dc3e4136925946d97c4a45eceef7a988d5f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 29 Dec 2023 11:12:31 +0100 Subject: [PATCH 32/43] Fix flaky scim test --- .../worker/src/api/routes/global/tests/scim.spec.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim.spec.ts b/packages/worker/src/api/routes/global/tests/scim.spec.ts index 3614328d91..56b7ca9f40 100644 --- a/packages/worker/src/api/routes/global/tests/scim.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim.spec.ts @@ -1,6 +1,6 @@ import tk from "timekeeper" import _ from "lodash" -import { mocks, structures } from "@budibase/backend-core/tests" +import { generator, mocks, structures } from "@budibase/backend-core/tests" import { ScimCreateUserRequest, ScimGroupResponse, @@ -575,8 +575,15 @@ describe("scim", () => { beforeAll(async () => { groups = [] - for (let i = 0; i < groupCount; i++) { - const body = structures.scim.createGroupRequest() + const groupNames = generator.unique( + () => generator.word(), + groupCount + ) + + for (const groupName of groupNames) { + const body = structures.scim.createGroupRequest({ + displayName: groupName, + }) groups.push(await config.api.scimGroupsAPI.post({ body })) } From f722f9e2d63997f1b2da6a89b23614d8bc19b7c2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 29 Dec 2023 15:06:04 +0100 Subject: [PATCH 33/43] Invalidate reset code once used --- packages/backend-core/src/cache/passwordReset.ts | 11 ++++++++++- packages/worker/src/sdk/auth/auth.ts | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/cache/passwordReset.ts b/packages/backend-core/src/cache/passwordReset.ts index 7f5a93f149..a19e99745a 100644 --- a/packages/backend-core/src/cache/passwordReset.ts +++ b/packages/backend-core/src/cache/passwordReset.ts @@ -1,6 +1,6 @@ import * as redis from "../redis/init" import * as utils from "../utils" -import { Duration, DurationType } from "../utils" +import { Duration } from "../utils" const TTL_SECONDS = Duration.fromHours(1).toSeconds() @@ -36,3 +36,12 @@ export async function getCode(code: string): Promise { } return value } + +/** + * Given a reset code this will invalidate it. + * @param code The code provided via the email link. + */ +export async function invalidateCode(code: string): Promise { + const client = await redis.getPasswordResetClient() + await client.delete(code) +} diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 1f9da8a260..bdc5fc2366 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -79,6 +79,8 @@ export const resetUpdate = async (resetCode: string, password: string) => { user.password = password user = await userSdk.db.save(user) + await cache.passwordReset.invalidateCode(resetCode) + // remove password from the user before sending events delete user.password await events.user.passwordReset(user) From dcacd6bf17a2491f64708b6c4b88f428c5fdc6d3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 29 Dec 2023 16:07:26 +0100 Subject: [PATCH 34/43] Add basic test --- packages/backend-core/src/users/db.ts | 2 +- .../worker/src/sdk/auth/tests/auth.spec.ts | 28 +++++++++++++++++++ .../worker/src/sdk/users/tests/users.spec.ts | 1 - 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 packages/worker/src/sdk/auth/tests/auth.spec.ts diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 326bed3cc5..01fa4899d1 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -2,7 +2,7 @@ import env from "../environment" import * as eventHelpers from "./events" import * as accountSdk from "../accounts" import * as cache from "../cache" -import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context" +import { getGlobalDB, getIdentity, getTenantId } from "../context" import * as dbUtils from "../db" import { EmailUnavailableError, HTTPError } from "../errors" import * as platform from "../platform" diff --git a/packages/worker/src/sdk/auth/tests/auth.spec.ts b/packages/worker/src/sdk/auth/tests/auth.spec.ts new file mode 100644 index 0000000000..b1758e79c6 --- /dev/null +++ b/packages/worker/src/sdk/auth/tests/auth.spec.ts @@ -0,0 +1,28 @@ +import { cache, context, utils } from "@budibase/backend-core" +import { resetUpdate } from "../auth" +import { generator, structures } from "@budibase/backend-core/tests" +import { TestConfiguration } from "../../../tests" + +describe("auth", () => { + const config = new TestConfiguration() + + describe("resetUpdate", () => { + it("providing a valid code will update the password", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const user = await config.createUser() + const previousPassword = user.password + + const code = await cache.passwordReset.createCode(user._id!, {}) + const newPassword = generator.hash() + + await resetUpdate(code, newPassword) + + const persistedUser = await config.getUser(user.email) + expect(persistedUser.password).not.toBe(previousPassword) + expect( + await utils.compare(newPassword, persistedUser.password!) + ).toBeTruthy() + }) + }) + }) +}) diff --git a/packages/worker/src/sdk/users/tests/users.spec.ts b/packages/worker/src/sdk/users/tests/users.spec.ts index df1aa74200..a02ca42c2f 100644 --- a/packages/worker/src/sdk/users/tests/users.spec.ts +++ b/packages/worker/src/sdk/users/tests/users.spec.ts @@ -1,6 +1,5 @@ import { structures, mocks } from "../../../tests" import { env, context } from "@budibase/backend-core" -import * as users from "../users" import { db as userDb } from "../" import { CloudAccount } from "@budibase/types" From f74264c1c82dce62fdbbe45c3e94369d9e857a74 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 29 Dec 2023 16:37:34 +0100 Subject: [PATCH 35/43] Add tests --- .../backend-core/src/cache/passwordReset.ts | 4 ++- .../worker/src/sdk/auth/tests/auth.spec.ts | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/cache/passwordReset.ts b/packages/backend-core/src/cache/passwordReset.ts index a19e99745a..db32b520f7 100644 --- a/packages/backend-core/src/cache/passwordReset.ts +++ b/packages/backend-core/src/cache/passwordReset.ts @@ -32,7 +32,9 @@ export async function getCode(code: string): Promise { const client = await redis.getPasswordResetClient() const value = (await client.get(code)) as PasswordReset | undefined if (!value) { - throw "Provided information is not valid, cannot reset password - please try again." + throw new Error( + "Provided information is not valid, cannot reset password - please try again." + ) } return value } diff --git a/packages/worker/src/sdk/auth/tests/auth.spec.ts b/packages/worker/src/sdk/auth/tests/auth.spec.ts index b1758e79c6..0d05a3fbb3 100644 --- a/packages/worker/src/sdk/auth/tests/auth.spec.ts +++ b/packages/worker/src/sdk/auth/tests/auth.spec.ts @@ -24,5 +24,30 @@ describe("auth", () => { ).toBeTruthy() }) }) + + it("wrong code will not allow to reset the password", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const code = generator.hash() + const newPassword = generator.hash() + + await expect(resetUpdate(code, newPassword)).rejects.toThrow( + "Provided information is not valid, cannot reset password - please try again." + ) + }) + }) + + it("the same code cannot be used twice", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const user = await config.createUser() + + const code = await cache.passwordReset.createCode(user._id!, {}) + const newPassword = generator.hash() + + await resetUpdate(code, newPassword) + await expect(resetUpdate(code, newPassword)).rejects.toThrow( + "Provided information is not valid, cannot reset password - please try again." + ) + }) + }) }) }) From d1ffe242692570e4f04d147178710c319a2bfd8c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 29 Dec 2023 16:54:47 +0100 Subject: [PATCH 36/43] Invalidate session on password update --- packages/worker/src/sdk/auth/auth.ts | 1 + .../worker/src/sdk/auth/tests/auth.spec.ts | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index bdc5fc2366..3f24de440a 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -80,6 +80,7 @@ export const resetUpdate = async (resetCode: string, password: string) => { user = await userSdk.db.save(user) await cache.passwordReset.invalidateCode(resetCode) + await sessions.invalidateSessions(userId) // remove password from the user before sending events delete user.password diff --git a/packages/worker/src/sdk/auth/tests/auth.spec.ts b/packages/worker/src/sdk/auth/tests/auth.spec.ts index 0d05a3fbb3..e9f348f7c7 100644 --- a/packages/worker/src/sdk/auth/tests/auth.spec.ts +++ b/packages/worker/src/sdk/auth/tests/auth.spec.ts @@ -1,5 +1,5 @@ -import { cache, context, utils } from "@budibase/backend-core" -import { resetUpdate } from "../auth" +import { cache, context, sessions, utils } from "@budibase/backend-core" +import { loginUser, resetUpdate } from "../auth" import { generator, structures } from "@budibase/backend-core/tests" import { TestConfiguration } from "../../../tests" @@ -49,5 +49,22 @@ describe("auth", () => { ) }) }) + + it("updating the password will invalidate all the sessions", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const user = await config.createUser() + + await loginUser(user) + + expect(await sessions.getSessionsForUser(user._id!)).toHaveLength(1) + + const code = await cache.passwordReset.createCode(user._id!, {}) + const newPassword = generator.hash() + + await resetUpdate(code, newPassword) + + expect(await sessions.getSessionsForUser(user._id!)).toHaveLength(0) + }) + }) }) }) From 865038d02f42bc6b23567359fdb3dc152a28a213 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 29 Dec 2023 17:24:51 +0000 Subject: [PATCH 37/43] Bump version to 2.13.51 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 671935c34b..bbe4da4264 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.50", + "version": "2.13.51", "npmClient": "yarn", "packages": [ "packages/*", From d277832f47b9c4bcb769e6f5c90dfbc7d6592256 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Tue, 2 Jan 2024 12:01:17 +0100 Subject: [PATCH 38/43] Fix enterprise license color --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-portal b/packages/account-portal index e46a352a63..ebdd4aa056 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit e46a352a6326a838faa00f912de069aee95d7300 +Subproject commit ebdd4aa0561dfcc3de3a906eead3979b680d039a From e38624161ecc486fb49c862a8808c6c4701a842e Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 2 Jan 2024 11:02:25 +0000 Subject: [PATCH 39/43] Update account-portal submodule ref --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-portal b/packages/account-portal index e46a352a63..af1ae16221 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit e46a352a6326a838faa00f912de069aee95d7300 +Subproject commit af1ae16221a4e6ababe8cc1f14803cbc849b2e85 From debf2b3d1e127f236bb09be0e3e910f9a7668263 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Tue, 2 Jan 2024 12:06:18 +0100 Subject: [PATCH 40/43] Update account portal submodule --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-portal b/packages/account-portal index ebdd4aa056..d6a1f89aa5 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit ebdd4aa0561dfcc3de3a906eead3979b680d039a +Subproject commit d6a1f89aa543bdce7acde5fbe4ce650a1344e2fe From e26c39677dde005f5133d0401322d8474393f541 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 2 Jan 2024 12:31:08 +0100 Subject: [PATCH 41/43] Update messages --- packages/server/scripts/load/create-many-relationships.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/scripts/load/create-many-relationships.js b/packages/server/scripts/load/create-many-relationships.js index 13f861ec0b..b81aed3d5d 100755 --- a/packages/server/scripts/load/create-many-relationships.js +++ b/packages/server/scripts/load/create-many-relationships.js @@ -90,9 +90,13 @@ async function run() { if (!appId) { const app = appId ? await getApp(apiKey, appId) : await createApp(apiKey) appId = app._id - } - console.log(`App created: http://localhost:10000/builder/app/${appId}`) + 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") From 642b75e0aeae5e50e9a38d847bd9efee091129fd Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 2 Jan 2024 11:36:32 +0000 Subject: [PATCH 42/43] Revert "Remove all custom tracing to see if it's the cause of the memory leak." --- .../backend-core/src/db/couch/DatabaseImpl.ts | 4 +- packages/backend-core/src/db/db.ts | 3 +- .../backend-core/src/db/instrumentation.ts | 156 +++++ .../backend-core/src/logging/pino/logger.ts | 6 + packages/server/src/automations/utils.ts | 70 ++- packages/server/src/jsRunner.ts | 53 +- packages/server/src/middleware/currentapp.ts | 14 + packages/server/src/threads/automation.ts | 583 ++++++++++-------- .../src/utilities/rowProcessor/utils.ts | 56 +- 9 files changed, 622 insertions(+), 323 deletions(-) create mode 100644 packages/backend-core/src/db/instrumentation.ts diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index c2c0b6b21d..3fec573bb9 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -17,6 +17,7 @@ import { directCouchUrlCall } from "./utils" import { getPouchDB } from "./pouchDB" import { WriteStream, ReadStream } from "fs" import { newid } from "../../docIds/newid" +import { DDInstrumentedDatabase } from "../instrumentation" function buildNano(couchInfo: { url: string; cookie: string }) { return Nano({ @@ -35,7 +36,8 @@ export function DatabaseWithConnection( connection: string, opts?: DatabaseOpts ) { - return new DatabaseImpl(dbName, opts, connection) + const db = new DatabaseImpl(dbName, opts, connection) + return new DDInstrumentedDatabase(db) } export class DatabaseImpl implements Database { diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts index 3e69d49f0e..197770298e 100644 --- a/packages/backend-core/src/db/db.ts +++ b/packages/backend-core/src/db/db.ts @@ -1,8 +1,9 @@ import { directCouchQuery, DatabaseImpl } from "./couch" import { CouchFindOptions, Database, DatabaseOpts } from "@budibase/types" +import { DDInstrumentedDatabase } from "./instrumentation" export function getDB(dbName: string, opts?: DatabaseOpts): Database { - return new DatabaseImpl(dbName, opts) + return new DDInstrumentedDatabase(new DatabaseImpl(dbName, opts)) } // we have to use a callback for this so that we can close diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts new file mode 100644 index 0000000000..ba5febcba6 --- /dev/null +++ b/packages/backend-core/src/db/instrumentation.ts @@ -0,0 +1,156 @@ +import { + DocumentScope, + DocumentDestroyResponse, + DocumentInsertResponse, + DocumentBulkResponse, + OkResponse, +} from "@budibase/nano" +import { + AllDocsResponse, + AnyDocument, + Database, + DatabaseDumpOpts, + DatabasePutOpts, + DatabaseQueryOpts, + Document, +} from "@budibase/types" +import tracer from "dd-trace" +import { Writable } from "stream" + +export class DDInstrumentedDatabase implements Database { + constructor(private readonly db: Database) {} + + get name(): string { + return this.db.name + } + + exists(): Promise { + return tracer.trace("db.exists", span => { + span?.addTags({ db_name: this.name }) + return this.db.exists() + }) + } + + checkSetup(): Promise> { + return tracer.trace("db.checkSetup", span => { + span?.addTags({ db_name: this.name }) + return this.db.checkSetup() + }) + } + + get(id?: string | undefined): Promise { + return tracer.trace("db.get", span => { + span?.addTags({ db_name: this.name, doc_id: id }) + return this.db.get(id) + }) + } + + getMultiple( + ids: string[], + opts?: { allowMissing?: boolean | undefined } | undefined + ): Promise { + return tracer.trace("db.getMultiple", span => { + span?.addTags({ + db_name: this.name, + num_docs: ids.length, + allow_missing: opts?.allowMissing, + }) + return this.db.getMultiple(ids, opts) + }) + } + + remove( + id: string | Document, + rev?: string | undefined + ): Promise { + return tracer.trace("db.remove", span => { + span?.addTags({ db_name: this.name, doc_id: id }) + return this.db.remove(id, rev) + }) + } + + put( + document: AnyDocument, + opts?: DatabasePutOpts | undefined + ): Promise { + return tracer.trace("db.put", span => { + span?.addTags({ db_name: this.name, doc_id: document._id }) + return this.db.put(document, opts) + }) + } + + bulkDocs(documents: AnyDocument[]): Promise { + return tracer.trace("db.bulkDocs", span => { + span?.addTags({ db_name: this.name, num_docs: documents.length }) + return this.db.bulkDocs(documents) + }) + } + + allDocs( + params: DatabaseQueryOpts + ): Promise> { + return tracer.trace("db.allDocs", span => { + span?.addTags({ db_name: this.name }) + return this.db.allDocs(params) + }) + } + + query( + viewName: string, + params: DatabaseQueryOpts + ): Promise> { + return tracer.trace("db.query", span => { + span?.addTags({ db_name: this.name, view_name: viewName }) + return this.db.query(viewName, params) + }) + } + + destroy(): Promise { + return tracer.trace("db.destroy", span => { + span?.addTags({ db_name: this.name }) + return this.db.destroy() + }) + } + + compact(): Promise { + return tracer.trace("db.compact", span => { + span?.addTags({ db_name: this.name }) + return this.db.compact() + }) + } + + dump(stream: Writable, opts?: DatabaseDumpOpts | undefined): Promise { + return tracer.trace("db.dump", span => { + span?.addTags({ db_name: this.name }) + return this.db.dump(stream, opts) + }) + } + + load(...args: any[]): Promise { + return tracer.trace("db.load", span => { + span?.addTags({ db_name: this.name }) + return this.db.load(...args) + }) + } + + createIndex(...args: any[]): Promise { + return tracer.trace("db.createIndex", span => { + span?.addTags({ db_name: this.name }) + return this.db.createIndex(...args) + }) + } + + deleteIndex(...args: any[]): Promise { + return tracer.trace("db.deleteIndex", span => { + span?.addTags({ db_name: this.name }) + return this.db.deleteIndex(...args) + }) + } + + getIndexes(...args: any[]): Promise { + return tracer.trace("db.getIndexes", span => { + span?.addTags({ db_name: this.name }) + return this.db.getIndexes(...args) + }) + } +} diff --git a/packages/backend-core/src/logging/pino/logger.ts b/packages/backend-core/src/logging/pino/logger.ts index ad68bd300d..7a051e7f12 100644 --- a/packages/backend-core/src/logging/pino/logger.ts +++ b/packages/backend-core/src/logging/pino/logger.ts @@ -5,6 +5,7 @@ import { IdentityType } from "@budibase/types" import env from "../../environment" import * as context from "../../context" import * as correlation from "../correlation" +import tracer from "dd-trace" import { formats } from "dd-trace/ext" import { localFileDestination } from "../system" @@ -116,6 +117,11 @@ if (!env.DISABLE_PINO_LOGGER) { correlationId: correlation.getId(), } + const span = tracer.scope().active() + if (span) { + tracer.inject(span.context(), formats.LOG, contextObject) + } + const mergingObject: any = { err: error, pid: process.pid, diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 04fd36e3d1..0c28787f67 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -16,6 +16,7 @@ import { } from "@budibase/types" import sdk from "../sdk" import { automationsEnabled } from "../features" +import tracer from "dd-trace" const REBOOT_CRON = "@reboot" const WH_STEP_ID = definitions.WEBHOOK.stepId @@ -39,27 +40,62 @@ function loggingArgs(job: AutomationJob) { } export async function processEvent(job: AutomationJob) { - const appId = job.data.event.appId! - const automationId = job.data.automation._id! + return tracer.trace( + "processEvent", + { resource: "automation" }, + async span => { + const appId = job.data.event.appId! + const automationId = job.data.automation._id! - const task = async () => { - try { - // need to actually await these so that an error can be captured properly - console.log("automation running", ...loggingArgs(job)) - - const runFn = () => Runner.run(job) - const result = await quotas.addAutomation(runFn, { + span?.addTags({ + appId, automationId, + job: { + id: job.id, + name: job.name, + attemptsMade: job.attemptsMade, + opts: { + attempts: job.opts.attempts, + priority: job.opts.priority, + delay: job.opts.delay, + repeat: job.opts.repeat, + backoff: job.opts.backoff, + lifo: job.opts.lifo, + timeout: job.opts.timeout, + jobId: job.opts.jobId, + removeOnComplete: job.opts.removeOnComplete, + removeOnFail: job.opts.removeOnFail, + stackTraceLimit: job.opts.stackTraceLimit, + preventParsingData: job.opts.preventParsingData, + }, + }, }) - console.log("automation completed", ...loggingArgs(job)) - return result - } catch (err) { - console.error(`automation was unable to run`, err, ...loggingArgs(job)) - return { err } - } - } - return await context.doInAutomationContext({ appId, automationId, task }) + const task = async () => { + try { + // need to actually await these so that an error can be captured properly + console.log("automation running", ...loggingArgs(job)) + + const runFn = () => Runner.run(job) + const result = await quotas.addAutomation(runFn, { + automationId, + }) + console.log("automation completed", ...loggingArgs(job)) + return result + } catch (err) { + span?.addTags({ error: true }) + console.error( + `automation was unable to run`, + err, + ...loggingArgs(job) + ) + return { err } + } + } + + return await context.doInAutomationContext({ appId, automationId, task }) + } + ) } export async function updateTestHistory( diff --git a/packages/server/src/jsRunner.ts b/packages/server/src/jsRunner.ts index a9301feb60..ab0381a399 100644 --- a/packages/server/src/jsRunner.ts +++ b/packages/server/src/jsRunner.ts @@ -2,35 +2,44 @@ import vm from "vm" import env from "./environment" import { setJSRunner } from "@budibase/string-templates" import { context, timers } from "@budibase/backend-core" +import tracer from "dd-trace" type TrackerFn = (f: () => T) => T export function init() { setJSRunner((js: string, ctx: vm.Context) => { - const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS - let track: TrackerFn = f => f() - if (perRequestLimit) { - const bbCtx = context.getCurrentContext() - if (bbCtx) { - if (!bbCtx.jsExecutionTracker) { - bbCtx.jsExecutionTracker = - timers.ExecutionTimeTracker.withLimit(perRequestLimit) + return tracer.trace("runJS", {}, span => { + const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS + let track: TrackerFn = f => f() + if (perRequestLimit) { + const bbCtx = context.getCurrentContext() + if (bbCtx) { + if (!bbCtx.jsExecutionTracker) { + bbCtx.jsExecutionTracker = + timers.ExecutionTimeTracker.withLimit(perRequestLimit) + } + track = bbCtx.jsExecutionTracker.track.bind(bbCtx.jsExecutionTracker) + span?.addTags({ + js: { + limitMS: bbCtx.jsExecutionTracker.limitMs, + elapsedMS: bbCtx.jsExecutionTracker.elapsedMS, + }, + }) } - track = bbCtx.jsExecutionTracker.track.bind(bbCtx.jsExecutionTracker) } - } - ctx = { - ...ctx, - alert: undefined, - setInterval: undefined, - setTimeout: undefined, - } - vm.createContext(ctx) - return track(() => - vm.runInNewContext(js, ctx, { - timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, - }) - ) + ctx = { + ...ctx, + alert: undefined, + setInterval: undefined, + setTimeout: undefined, + } + vm.createContext(ctx) + return track(() => + vm.runInNewContext(js, ctx, { + timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, + }) + ) + }) }) } diff --git a/packages/server/src/middleware/currentapp.ts b/packages/server/src/middleware/currentapp.ts index 984dd8e5e9..ad6f2afa18 100644 --- a/packages/server/src/middleware/currentapp.ts +++ b/packages/server/src/middleware/currentapp.ts @@ -12,6 +12,7 @@ import { getCachedSelf } from "../utilities/global" import env from "../environment" import { isWebhookEndpoint } from "./utils" import { UserCtx, ContextUser } from "@budibase/types" +import tracer from "dd-trace" export default async (ctx: UserCtx, next: any) => { // try to get the appID from the request @@ -20,6 +21,11 @@ export default async (ctx: UserCtx, next: any) => { return next() } + if (requestAppId) { + const span = tracer.scope().active() + span?.setTag("appId", requestAppId) + } + // deny access to application preview if (!env.isTest()) { if ( @@ -70,6 +76,14 @@ export default async (ctx: UserCtx, next: any) => { return next() } + if (ctx.user) { + const span = tracer.scope().active() + if (ctx.user._id) { + span?.setTag("userId", ctx.user._id) + } + span?.setTag("tenantId", ctx.user.tenantId) + } + const userId = ctx.user ? generateUserMetadataID(ctx.user._id!) : undefined // if the user is not in the right tenant then make sure to wipe their cookie diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 9bb1717f3c..4447899f96 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -34,6 +34,7 @@ import { cloneDeep } from "lodash/fp" import { performance } from "perf_hooks" import * as sdkUtils from "../sdk/utils" import env from "../environment" +import tracer from "dd-trace" threadUtils.threadSetup() const FILTER_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.FILTER.stepId @@ -242,281 +243,347 @@ class Orchestrator { } async execute(): Promise { - // this will retrieve from context created at start of thread - this._context.env = await sdkUtils.getEnvironmentVariables() - let automation = this._automation - let stopped = false - let loopStep: AutomationStep | undefined = undefined + return tracer.trace( + "Orchestrator.execute", + { resource: "automation" }, + async span => { + span?.addTags({ + appId: this._appId, + automationId: this._automation._id, + }) - let stepCount = 0 - let loopStepNumber: any = undefined - let loopSteps: LoopStep[] | undefined = [] - let metadata - let timeoutFlag = false - let wasLoopStep = false - let timeout = this._job.data.event.timeout - // check if this is a recurring automation, - if (isProdAppID(this._appId) && isRecurring(automation)) { - metadata = await this.getMetadata() - const shouldStop = await this.checkIfShouldStop(metadata) - if (shouldStop) { - return - } - } - const start = performance.now() - for (let step of automation.definition.steps) { - let input: any, - iterations = 1, - iterationCount = 0 + // this will retrieve from context created at start of thread + this._context.env = await sdkUtils.getEnvironmentVariables() + let automation = this._automation + let stopped = false + let loopStep: AutomationStep | undefined = undefined - if (timeoutFlag) { - break - } - - if (timeout) { - setTimeout(() => { - timeoutFlag = true - }, timeout || 12000) - } - - stepCount++ - if (step.stepId === LOOP_STEP_ID) { - loopStep = step - loopStepNumber = stepCount - continue - } - - if (loopStep) { - input = await processObject(loopStep.inputs, this._context) - iterations = getLoopIterations(loopStep as LoopStep) - } - for (let index = 0; index < iterations; index++) { - let originalStepInput = cloneDeep(step.inputs) - // Handle if the user has set a max iteration count or if it reaches the max limit set by us - if (loopStep && input.binding) { - let tempOutput = { - items: loopSteps, - iterations: iterationCount, - } - try { - loopStep.inputs.binding = automationUtils.typecastForLooping( - loopStep as LoopStep, - loopStep.inputs as LoopInput - ) - } catch (err) { - this.updateContextAndOutput(loopStepNumber, step, tempOutput, { - status: AutomationErrors.INCORRECT_TYPE, - success: false, - }) - loopSteps = undefined - loopStep = undefined - break - } - let item = [] - if ( - typeof loopStep.inputs.binding === "string" && - loopStep.inputs.option === "String" - ) { - item = automationUtils.stringSplit(loopStep.inputs.binding) - } else if (Array.isArray(loopStep.inputs.binding)) { - item = loopStep.inputs.binding - } - this._context.steps[loopStepNumber] = { - currentItem: item[index], - } - - // The "Loop" binding in the front end is "fake", so replace it here so the context can understand it - // Pretty hacky because we need to account for the row object - for (let [key, value] of Object.entries(originalStepInput)) { - if (typeof value === "object") { - for (let [innerKey, innerValue] of Object.entries( - originalStepInput[key] - )) { - if (typeof innerValue === "string") { - originalStepInput[key][innerKey] = - automationUtils.substituteLoopStep( - innerValue, - `steps.${loopStepNumber}` - ) - } else if (typeof value === "object") { - for (let [innerObject, innerValue] of Object.entries( - originalStepInput[key][innerKey] - )) { - originalStepInput[key][innerKey][innerObject] = - automationUtils.substituteLoopStep( - innerValue as string, - `steps.${loopStepNumber}` - ) - } - } - } - } else { - if (typeof value === "string") { - originalStepInput[key] = automationUtils.substituteLoopStep( - value, - `steps.${loopStepNumber}` - ) - } - } - } - - if ( - index === env.AUTOMATION_MAX_ITERATIONS || - index === parseInt(loopStep.inputs.iterations) - ) { - this.updateContextAndOutput(loopStepNumber, step, tempOutput, { - status: AutomationErrors.MAX_ITERATIONS, - success: true, - }) - loopSteps = undefined - loopStep = undefined - break - } - - let isFailure = false - const currentItem = this._context.steps[loopStepNumber]?.currentItem - if (currentItem && typeof currentItem === "object") { - isFailure = Object.keys(currentItem).some(value => { - return currentItem[value] === loopStep?.inputs.failure - }) - } else { - isFailure = currentItem && currentItem === loopStep.inputs.failure - } - - if (isFailure) { - this.updateContextAndOutput(loopStepNumber, step, tempOutput, { - status: AutomationErrors.FAILURE_CONDITION, - success: false, - }) - loopSteps = undefined - loopStep = undefined - break + let stepCount = 0 + let loopStepNumber: any = undefined + let loopSteps: LoopStep[] | undefined = [] + let metadata + let timeoutFlag = false + let wasLoopStep = false + let timeout = this._job.data.event.timeout + // check if this is a recurring automation, + if (isProdAppID(this._appId) && isRecurring(automation)) { + span?.addTags({ recurring: true }) + metadata = await this.getMetadata() + const shouldStop = await this.checkIfShouldStop(metadata) + if (shouldStop) { + span?.addTags({ shouldStop: true }) + return } } - - // execution stopped, record state for that - if (stopped) { - this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS) - continue - } - - // If it's a loop step, we need to manually add the bindings to the context - let stepFn = await this.getStepFunctionality(step.stepId) - let inputs = await processObject(originalStepInput, this._context) - inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs) - - try { - // appId is always passed - const outputs = await stepFn({ - inputs: inputs, - appId: this._appId, - emitter: this._emitter, - context: this._context, + const start = performance.now() + for (let step of automation.definition.steps) { + const stepSpan = tracer.startSpan("Orchestrator.execute.step", { + childOf: span, + }) + stepSpan.addTags({ + resource: "automation", + step: { + stepId: step.stepId, + id: step.id, + name: step.name, + type: step.type, + title: step.stepTitle, + internal: step.internal, + deprecated: step.deprecated, + }, }) - this._context.steps[stepCount] = outputs - // if filter causes us to stop execution don't break the loop, set a var - // so that we can finish iterating through the steps and record that it stopped - if (step.stepId === FILTER_STEP_ID && !outputs.result) { - stopped = true - this.updateExecutionOutput(step.id, step.stepId, step.inputs, { - ...outputs, - ...STOPPED_STATUS, - }) - continue - } - if (loopStep && loopSteps) { - loopSteps.push(outputs) - } else { - this.updateExecutionOutput( - step.id, - step.stepId, - step.inputs, - outputs - ) - } - } catch (err) { - console.error(`Automation error - ${step.stepId} - ${err}`) - return err - } + let input: any, + iterations = 1, + iterationCount = 0 - if (loopStep) { - iterationCount++ - if (index === iterations - 1) { + try { + if (timeoutFlag) { + span?.addTags({ timedOut: true }) + break + } + + if (timeout) { + setTimeout(() => { + timeoutFlag = true + }, timeout || 12000) + } + + stepCount++ + if (step.stepId === LOOP_STEP_ID) { + loopStep = step + loopStepNumber = stepCount + continue + } + + if (loopStep) { + input = await processObject(loopStep.inputs, this._context) + iterations = getLoopIterations(loopStep as LoopStep) + stepSpan?.addTags({ step: { iterations } }) + } + for (let index = 0; index < iterations; index++) { + let originalStepInput = cloneDeep(step.inputs) + // Handle if the user has set a max iteration count or if it reaches the max limit set by us + if (loopStep && input.binding) { + let tempOutput = { + items: loopSteps, + iterations: iterationCount, + } + try { + loopStep.inputs.binding = automationUtils.typecastForLooping( + loopStep as LoopStep, + loopStep.inputs as LoopInput + ) + } catch (err) { + this.updateContextAndOutput( + loopStepNumber, + step, + tempOutput, + { + status: AutomationErrors.INCORRECT_TYPE, + success: false, + } + ) + loopSteps = undefined + loopStep = undefined + break + } + let item = [] + if ( + typeof loopStep.inputs.binding === "string" && + loopStep.inputs.option === "String" + ) { + item = automationUtils.stringSplit(loopStep.inputs.binding) + } else if (Array.isArray(loopStep.inputs.binding)) { + item = loopStep.inputs.binding + } + this._context.steps[loopStepNumber] = { + currentItem: item[index], + } + + // The "Loop" binding in the front end is "fake", so replace it here so the context can understand it + // Pretty hacky because we need to account for the row object + for (let [key, value] of Object.entries(originalStepInput)) { + if (typeof value === "object") { + for (let [innerKey, innerValue] of Object.entries( + originalStepInput[key] + )) { + if (typeof innerValue === "string") { + originalStepInput[key][innerKey] = + automationUtils.substituteLoopStep( + innerValue, + `steps.${loopStepNumber}` + ) + } else if (typeof value === "object") { + for (let [innerObject, innerValue] of Object.entries( + originalStepInput[key][innerKey] + )) { + originalStepInput[key][innerKey][innerObject] = + automationUtils.substituteLoopStep( + innerValue as string, + `steps.${loopStepNumber}` + ) + } + } + } + } else { + if (typeof value === "string") { + originalStepInput[key] = + automationUtils.substituteLoopStep( + value, + `steps.${loopStepNumber}` + ) + } + } + } + + if ( + index === env.AUTOMATION_MAX_ITERATIONS || + index === parseInt(loopStep.inputs.iterations) + ) { + this.updateContextAndOutput( + loopStepNumber, + step, + tempOutput, + { + status: AutomationErrors.MAX_ITERATIONS, + success: true, + } + ) + loopSteps = undefined + loopStep = undefined + break + } + + let isFailure = false + const currentItem = + this._context.steps[loopStepNumber]?.currentItem + if (currentItem && typeof currentItem === "object") { + isFailure = Object.keys(currentItem).some(value => { + return currentItem[value] === loopStep?.inputs.failure + }) + } else { + isFailure = + currentItem && currentItem === loopStep.inputs.failure + } + + if (isFailure) { + this.updateContextAndOutput( + loopStepNumber, + step, + tempOutput, + { + status: AutomationErrors.FAILURE_CONDITION, + success: false, + } + ) + loopSteps = undefined + loopStep = undefined + break + } + } + + // execution stopped, record state for that + if (stopped) { + this.updateExecutionOutput( + step.id, + step.stepId, + {}, + STOPPED_STATUS + ) + continue + } + + // If it's a loop step, we need to manually add the bindings to the context + let stepFn = await this.getStepFunctionality(step.stepId) + let inputs = await processObject(originalStepInput, this._context) + inputs = automationUtils.cleanInputValues( + inputs, + step.schema.inputs + ) + + try { + // appId is always passed + const outputs = await stepFn({ + inputs: inputs, + appId: this._appId, + emitter: this._emitter, + context: this._context, + }) + + this._context.steps[stepCount] = outputs + // if filter causes us to stop execution don't break the loop, set a var + // so that we can finish iterating through the steps and record that it stopped + if (step.stepId === FILTER_STEP_ID && !outputs.result) { + stopped = true + this.updateExecutionOutput( + step.id, + step.stepId, + step.inputs, + { + ...outputs, + ...STOPPED_STATUS, + } + ) + continue + } + if (loopStep && loopSteps) { + loopSteps.push(outputs) + } else { + this.updateExecutionOutput( + step.id, + step.stepId, + step.inputs, + outputs + ) + } + } catch (err) { + console.error(`Automation error - ${step.stepId} - ${err}`) + return err + } + + if (loopStep) { + iterationCount++ + if (index === iterations - 1) { + loopStep = undefined + this._context.steps.splice(loopStepNumber, 1) + break + } + } + } + } finally { + stepSpan?.finish() + } + + if (loopStep && iterations === 0) { loopStep = undefined + this.executionOutput.steps.splice(loopStepNumber + 1, 0, { + id: step.id, + stepId: step.stepId, + outputs: { + status: AutomationStepStatus.NO_ITERATIONS, + success: true, + }, + inputs: {}, + }) + this._context.steps.splice(loopStepNumber, 1) - break + iterations = 1 + } + + // Delete the step after the loop step as it's irrelevant, since information is included + // in the loop step + if (wasLoopStep && !loopStep) { + this._context.steps.splice(loopStepNumber + 1, 1) + wasLoopStep = false + } + if (loopSteps && loopSteps.length) { + let tempOutput = { + success: true, + items: loopSteps, + iterations: iterationCount, + } + this.executionOutput.steps.splice(loopStepNumber + 1, 0, { + id: step.id, + stepId: step.stepId, + outputs: tempOutput, + inputs: step.inputs, + }) + this._context.steps[loopStepNumber] = tempOutput + + wasLoopStep = true + loopSteps = [] } } - } - if (loopStep && iterations === 0) { - loopStep = undefined - this.executionOutput.steps.splice(loopStepNumber + 1, 0, { - id: step.id, - stepId: step.stepId, - outputs: { - status: AutomationStepStatus.NO_ITERATIONS, - success: true, - }, - inputs: {}, - }) + const end = performance.now() + const executionTime = end - start - this._context.steps.splice(loopStepNumber, 1) - iterations = 1 - } + console.info( + `Automation ID: ${automation._id} Execution time: ${executionTime} milliseconds`, + { + _logKey: "automation", + executionTime, + } + ) - // Delete the step after the loop step as it's irrelevant, since information is included - // in the loop step - if (wasLoopStep && !loopStep) { - this._context.steps.splice(loopStepNumber + 1, 1) - wasLoopStep = false - } - if (loopSteps && loopSteps.length) { - let tempOutput = { - success: true, - items: loopSteps, - iterations: iterationCount, + // store the logs for the automation run + try { + await storeLog(this._automation, this.executionOutput) + } catch (e: any) { + if (e.status === 413 && e.request?.data) { + // if content is too large we shouldn't log it + delete e.request.data + e.request.data = { message: "removed due to large size" } + } + logging.logAlert("Error writing automation log", e) } - this.executionOutput.steps.splice(loopStepNumber + 1, 0, { - id: step.id, - stepId: step.stepId, - outputs: tempOutput, - inputs: step.inputs, - }) - this._context.steps[loopStepNumber] = tempOutput - - wasLoopStep = true - loopSteps = [] - } - } - - const end = performance.now() - const executionTime = end - start - - console.info( - `Automation ID: ${automation._id} Execution time: ${executionTime} milliseconds`, - { - _logKey: "automation", - executionTime, + if (isProdAppID(this._appId) && isRecurring(automation) && metadata) { + await this.updateMetadata(metadata) + } + return this.executionOutput } ) - - // store the logs for the automation run - try { - await storeLog(this._automation, this.executionOutput) - } catch (e: any) { - if (e.status === 413 && e.request?.data) { - // if content is too large we shouldn't log it - delete e.request.data - e.request.data = { message: "removed due to large size" } - } - logging.logAlert("Error writing automation log", e) - } - if (isProdAppID(this._appId) && isRecurring(automation) && metadata) { - await this.updateMetadata(metadata) - } - return this.executionOutput } } diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts index 22c099c58c..cafd366cae 100644 --- a/packages/server/src/utilities/rowProcessor/utils.ts +++ b/packages/server/src/utilities/rowProcessor/utils.ts @@ -11,6 +11,7 @@ import { Row, Table, } from "@budibase/types" +import tracer from "dd-trace" interface FormulaOpts { dynamic?: boolean @@ -50,35 +51,42 @@ export function processFormulas( inputRows: T, { dynamic, contextRows }: FormulaOpts = { dynamic: true } ): T { - const rows = Array.isArray(inputRows) ? inputRows : [inputRows] - if (rows) { - for (let [column, schema] of Object.entries(table.schema)) { - if (schema.type !== FieldTypes.FORMULA) { - continue - } + return tracer.trace("processFormulas", {}, span => { + const numRows = Array.isArray(inputRows) ? inputRows.length : 1 + span?.addTags({ table_id: table._id, dynamic, numRows }) + const rows = Array.isArray(inputRows) ? inputRows : [inputRows] + if (rows) { + for (let [column, schema] of Object.entries(table.schema)) { + if (schema.type !== FieldTypes.FORMULA) { + continue + } - const isStatic = schema.formulaType === FormulaTypes.STATIC + const isStatic = schema.formulaType === FormulaTypes.STATIC - if ( - schema.formula == null || - (dynamic && isStatic) || - (!dynamic && !isStatic) - ) { - continue - } - // iterate through rows and process formula - for (let i = 0; i < rows.length; i++) { - let row = rows[i] - let context = contextRows ? contextRows[i] : row - let formula = schema.formula - rows[i] = { - ...row, - [column]: processStringSync(formula, context), + if ( + schema.formula == null || + (dynamic && isStatic) || + (!dynamic && !isStatic) + ) { + continue + } + // iterate through rows and process formula + for (let i = 0; i < rows.length; i++) { + let row = rows[i] + let context = contextRows ? contextRows[i] : row + let formula = schema.formula + rows[i] = { + ...row, + [column]: tracer.trace("processStringSync", {}, span => { + span?.addTags({ table_id: table._id, column, static: isStatic }) + return processStringSync(formula, context) + }), + } } } } - } - return Array.isArray(inputRows) ? rows : rows[0] + return Array.isArray(inputRows) ? rows : rows[0] + }) } /** From 5392d89fde6e1cc920d79c251e443d8ea61dde82 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 2 Jan 2024 12:38:50 +0100 Subject: [PATCH 43/43] Update account-portal ref --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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