From 2c6725404fe292f455286a17c8112e8f827aa62e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 25 Jul 2023 17:35:05 +0200 Subject: [PATCH 01/14] Types --- packages/server/src/api/controllers/row/index.ts | 6 ++++-- .../server/src/sdk/app/rows/search/internal.ts | 8 ++++---- .../server/src/utilities/rowProcessor/index.ts | 15 +++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 79cd5fbfe0..4f0f8df975 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -12,6 +12,7 @@ import { SearchResponse, SortOrder, SortType, + UserCtx, ViewV2, } from "@budibase/types" import * as utils from "./utils" @@ -29,7 +30,7 @@ function pickApi(tableId: any) { return internal } -export async function patch(ctx: any): Promise { +export async function patch(ctx: UserCtx): Promise { const appId = ctx.appId const tableId = utils.getTableId(ctx) const body = ctx.request.body @@ -53,7 +54,7 @@ export async function patch(ctx: any): Promise { ctx.message = `${table.name} updated successfully.` ctx.body = row gridSocket?.emitRowUpdate(ctx, row) - } catch (err) { + } catch (err: any) { ctx.throw(400, err) } } @@ -78,6 +79,7 @@ export const save = async (ctx: any) => { ctx.body = row || squashed gridSocket?.emitRowUpdate(ctx, row || squashed) } + export async function fetchView(ctx: any) { const tableId = utils.getTableId(ctx) const viewName = decodeURIComponent(ctx.params.viewName) diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts index 5a29541705..e7f0aadfd6 100644 --- a/packages/server/src/sdk/app/rows/search/internal.ts +++ b/packages/server/src/sdk/app/rows/search/internal.ts @@ -147,8 +147,8 @@ export async function exportRows( export async function fetch(tableId: string) { const db = context.getAppDB() - let table = await sdk.tables.getTable(tableId) - let rows = await getRawTableData(db, tableId) + const table = await sdk.tables.getTable(tableId) + const rows = await getRawTableData(db, tableId) const result = await outputProcessing(table, rows) return result } @@ -171,7 +171,7 @@ async function getRawTableData(db: Database, tableId: string) { export async function fetchView( viewName: string, options: { calculation: string; group: string; field: string } -) { +): Promise { // if this is a table view being looked for just transfer to that if (viewName.startsWith(DocumentType.TABLE)) { return fetch(viewName) @@ -197,7 +197,7 @@ export async function fetchView( ) } - let rows + let rows: Row[] = [] if (!calculation) { response.rows = response.rows.map(row => row.doc) let table: Table diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 4b6e0f6e87..8e95a15dca 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -186,18 +186,21 @@ export function inputProcessing( * @param {object} opts used to set some options for the output, such as disabling relationship squashing. * @returns {object[]|object} the enriched rows will be returned. */ -export async function outputProcessing( +export async function outputProcessing( table: Table, - rows: Row[] | Row, + rows: T, opts = { squash: true } -) { +): Promise { + let safeRows: Row[] let wasArray = true if (!(rows instanceof Array)) { - rows = [rows] + safeRows = [rows] wasArray = false + } else { + safeRows = rows } // attach any linked row information - let enriched = await linkRows.attachFullLinkedDocs(table, rows as Row[]) + let enriched = await linkRows.attachFullLinkedDocs(table, safeRows) // process formulas enriched = processFormulas(table, enriched, { dynamic: true }) as Row[] @@ -221,7 +224,7 @@ export async function outputProcessing( enriched )) as Row[] } - return wasArray ? enriched : enriched[0] + return (wasArray ? enriched : enriched[0]) as T } /** From b69e18ab703b62ca11bdc46ac21819b14bce57a8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 25 Jul 2023 17:43:31 +0200 Subject: [PATCH 02/14] Return _viewId when searching --- packages/server/src/api/controllers/row/index.ts | 5 ++++- packages/server/src/api/routes/tests/row.spec.ts | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 4f0f8df975..7eeab0e9fa 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -269,7 +269,7 @@ export async function searchView(ctx: Ctx) { undefined ctx.status = 200 - ctx.body = await quotas.addQuery( + const result = await quotas.addQuery( () => sdk.rows.search({ tableId: view.tableId, @@ -281,6 +281,9 @@ export async function searchView(ctx: Ctx) { datasourceId: view.tableId, } ) + + result.rows.forEach(r => (r._viewId = view.id)) + ctx.body = result } export async function validate(ctx: Ctx) { diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 6a5cfa77a2..cde661b892 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -986,16 +986,17 @@ describe("/rows", () => { ) } - const createViewResponse = await config.api.viewV2.create({ + const view = await config.api.viewV2.create({ columns: { name: { visible: true } }, }) - const response = await config.api.viewV2.search(createViewResponse.id) + const response = await config.api.viewV2.search(view.id) expect(response.body.rows).toHaveLength(10) expect(response.body.rows).toEqual( expect.arrayContaining( rows.map(r => ({ ...expectAnyInternalColsAttributes, + _viewId: view.id, name: r.name, })) ) From f6e6243c81f47cd4d558e917a7319fa92f1d9ba9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 12:20:06 +0200 Subject: [PATCH 03/14] Type patch --- packages/server/src/api/controllers/row/index.ts | 9 ++++++--- packages/types/src/api/web/app/rows.ts | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 7eeab0e9fa..92771411b5 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -9,10 +9,11 @@ import { DeleteRow, DeleteRows, Row, + PatchRowRequest, + PatchRowResponse, SearchResponse, SortOrder, SortType, - UserCtx, ViewV2, } from "@budibase/types" import * as utils from "./utils" @@ -30,7 +31,9 @@ function pickApi(tableId: any) { return internal } -export async function patch(ctx: UserCtx): Promise { +export async function patch( + ctx: UserCtx +): Promise { const appId = ctx.appId const tableId = utils.getTableId(ctx) const body = ctx.request.body @@ -39,7 +42,7 @@ export async function patch(ctx: UserCtx): Promise { return save(ctx) } try { - const { row, table } = await quotas.addQuery( + const { row, table } = await quotas.addQuery( () => pickApi(tableId).patch(ctx), { datasourceId: tableId, diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts index d40d2ee15d..42622f091f 100644 --- a/packages/types/src/api/web/app/rows.ts +++ b/packages/types/src/api/web/app/rows.ts @@ -1,3 +1,9 @@ +import { Row } from "../../../documents" + +export interface PatchRowRequest extends Row {} + +export interface PatchRowResponse extends Row {} + export interface SearchResponse { rows: any[] } From 9bb1cfcc7abf574a588b8b44e8e2d75fd582f1f8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 12:20:23 +0200 Subject: [PATCH 04/14] Add view id to row --- packages/types/src/documents/app/row.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index a2295c4a42..f659caf20e 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -30,5 +30,6 @@ export interface RowAttachment { export interface Row extends Document { type?: string tableId?: string + viewId?: string [key: string]: any } From 77c2ce590c69f6cb56564cae93f4571d6cc86ac1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 12:29:19 +0200 Subject: [PATCH 05/14] Use test api for patch --- .../server/src/api/routes/tests/row.spec.ts | 29 ++++++++----------- .../server/src/tests/utilities/api/index.ts | 3 ++ .../server/src/tests/utilities/api/row.ts | 22 ++++++++++++++ 3 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/tests/utilities/api/row.ts diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index cde661b892..eb86fd9d3f 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -399,17 +399,12 @@ describe("/rows", () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .patch(`/api/${table._id}/rows`) - .send({ - _id: existing._id, - _rev: existing._rev, - tableId: table._id, - name: "Updated Name", - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.patch(table._id!, { + _id: existing._id, + _rev: existing._rev, + tableId: table._id, + name: "Updated Name", + }) expect((res as any).res.statusMessage).toEqual( `${table.name} updated successfully.` @@ -430,16 +425,16 @@ describe("/rows", () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - await request - .patch(`/api/${table._id}/rows`) - .send({ + await config.api.row.patch( + table._id!, + { _id: existing._id, _rev: existing._rev, tableId: table._id, name: 1, - }) - .set(config.defaultHeaders()) - .expect(400) + }, + { expectStatus: 400 } + ) await assertRowUsage(rowUsage) await assertQueryUsage(queryUsage) diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index cd9f42b82c..a6002a72d8 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -1,13 +1,16 @@ import TestConfiguration from "../TestConfiguration" +import { RowAPI } from "./row" import { TableAPI } from "./table" import { ViewV2API } from "./viewV2" export default class API { table: TableAPI viewV2: ViewV2API + row: RowAPI constructor(config: TestConfiguration) { this.table = new TableAPI(config) this.viewV2 = new ViewV2API(config) + this.row = new RowAPI(config) } } diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts new file mode 100644 index 0000000000..9c7e33278d --- /dev/null +++ b/packages/server/src/tests/utilities/api/row.ts @@ -0,0 +1,22 @@ +import { PatchRowRequest } from "@budibase/types" +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class RowAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + patch = async ( + tableId: string, + row: PatchRowRequest, + { expectStatus } = { expectStatus: 200 } + ) => { + return this.request + .patch(`/api/${tableId}/rows`) + .send(row) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + } +} From 97dcd3fb0a3d632a9454cf1b065b6b2d6ab7657c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 13:06:22 +0200 Subject: [PATCH 06/14] Add patch view test --- .../server/src/api/routes/tests/row.spec.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index eb86fd9d3f..0e8f574742 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -5,7 +5,7 @@ tk.freeze(timestamp) import { outputProcessing } from "../../../utilities/rowProcessor" import * as setup from "./utilities" const { basicRow } = setup.structures -import { context, db, tenancy } from "@budibase/backend-core" +import { context, tenancy } from "@budibase/backend-core" import { quotas } from "@budibase/pro" import { QuotaUsageType, @@ -439,6 +439,35 @@ describe("/rows", () => { await assertRowUsage(rowUsage) await assertQueryUsage(queryUsage) }) + + describe("view row", () => { + it("should not edit fields that don't belong to the view", async () => { + const existing = await config.createRow() + const view = await config.api.viewV2.create({ + columns: { name: { visible: true } }, + }) + const searchResponse = await config.api.viewV2.search(view.id) + + const [row] = searchResponse.body.rows as Row[] + + const res = await config.api.row.patch(table._id!, { + ...row, + name: "Updated Name", + description: "Updated Description", + }) + + const savedRow = await loadRow(res.body._id, table._id!) + + expect(savedRow.body).toEqual({ + ...existing, + name: "Updated Name", + _rev: expect.anything(), + createdAt: expect.anything(), + updatedAt: expect.anything(), + }) + expect(savedRow.body.description).not.toEqual("Updated Description") + }) + }) }) describe("destroy", () => { From f0f68f10d83f38f1a7158f068ab4066f17eeea15 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 13:07:10 +0200 Subject: [PATCH 07/14] Add tests --- .../server/src/api/routes/tests/row.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 0e8f574742..eed15c0355 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -441,6 +441,33 @@ describe("/rows", () => { }) describe("view row", () => { + it("should update only the fields that are supplied", async () => { + const existing = await config.createRow() + const view = await config.api.viewV2.create({ + columns: { name: { visible: true } }, + }) + const searchResponse = await config.api.viewV2.search(view.id) + + const [row] = searchResponse.body.rows as Row[] + + const res = await config.api.row.patch(table._id!, { + ...row, + name: "Updated Name", + description: "Updated Description", + }) + + const savedRow = await loadRow(res.body._id, table._id!) + + expect(savedRow.body).toEqual({ + ...existing, + name: "Updated Name", + description: "Updated Description", + _rev: expect.anything(), + createdAt: expect.anything(), + updatedAt: expect.anything(), + }) + }) + it("should not edit fields that don't belong to the view", async () => { const existing = await config.createRow() const view = await config.api.viewV2.create({ From af933bd1582c196af1947339e4e260a2be3b722f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 13:53:35 +0200 Subject: [PATCH 08/14] More types --- .../server/src/api/controllers/row/external.ts | 14 +++++++------- .../server/src/api/controllers/row/internal.ts | 15 +++++++++++---- packages/server/src/api/controllers/user.ts | 3 +-- .../server/src/api/routes/tests/row.spec.ts | 17 +++++++++-------- packages/types/src/api/web/app/rows.ts | 6 +++++- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index 6bae6afd48..0b6471eefd 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -8,6 +8,8 @@ import { Datasource, IncludeRelationship, Operation, + PatchRowRequest, + PatchRowResponse, Row, Table, UserCtx, @@ -55,14 +57,12 @@ export async function handleRequest( ) } -export async function patch(ctx: UserCtx) { - const inputs = ctx.request.body +export async function patch(ctx: UserCtx) { const tableId = ctx.params.tableId - const id = inputs._id - // don't save the ID to db - delete inputs._id + const { id, ...rowData } = ctx.request.body + const validateResult = await utils.validate({ - row: inputs, + row: rowData, tableId, }) if (!validateResult.valid) { @@ -70,7 +70,7 @@ export async function patch(ctx: UserCtx) { } const response = await handleRequest(Operation.UPDATE, tableId, { id: breakRowIdField(id), - row: inputs, + row: rowData, }) const row = await getRow(tableId, id, { relationships: true }) const table = await sdk.tables.getTable(tableId) diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts index d56ba3f14a..cdf0a396dc 100644 --- a/packages/server/src/api/controllers/row/internal.ts +++ b/packages/server/src/api/controllers/row/internal.ts @@ -15,10 +15,17 @@ import * as utils from "./utils" import { cloneDeep } from "lodash/fp" import { context, db as dbCore } from "@budibase/backend-core" import { finaliseRow, updateRelatedFormula } from "./staticFormula" -import { UserCtx, LinkDocumentValue, Row, Table } from "@budibase/types" +import { + UserCtx, + LinkDocumentValue, + Row, + Table, + PatchRowRequest, + PatchRowResponse, +} from "@budibase/types" import sdk from "../../../sdk" -export async function patch(ctx: UserCtx) { +export async function patch(ctx: UserCtx) { const inputs = ctx.request.body const tableId = inputs.tableId const isUserTable = tableId === InternalTables.USER_METADATA @@ -27,7 +34,7 @@ export async function patch(ctx: UserCtx) { let dbTable = await sdk.tables.getTable(tableId) oldRow = await outputProcessing( dbTable, - await utils.findRow(ctx, tableId, inputs._id) + await utils.findRow(ctx, tableId, inputs._id!) ) } catch (err) { if (isUserTable) { @@ -74,7 +81,7 @@ export async function patch(ctx: UserCtx) { if (isUserTable) { // the row has been updated, need to put it into the ctx - ctx.request.body = row + ctx.request.body = row as any await userController.updateMetadata(ctx) return { row: ctx.body as Row, table } } diff --git a/packages/server/src/api/controllers/user.ts b/packages/server/src/api/controllers/user.ts index 1a2a3850ce..dbbfc5c586 100644 --- a/packages/server/src/api/controllers/user.ts +++ b/packages/server/src/api/controllers/user.ts @@ -1,6 +1,5 @@ -import { generateUserMetadataID, generateUserFlagID } from "../../db/utils" +import { generateUserFlagID } from "../../db/utils" import { InternalTables } from "../../db/utils" -import { getGlobalUsers } from "../../utilities/global" import { getFullUser } from "../../utilities/users" import { context } from "@budibase/backend-core" import { Ctx, UserCtx } from "@budibase/types" diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index eed15c0355..b153627992 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -16,6 +16,7 @@ import { FieldType, SortType, SortOrder, + PatchRowRequest, } from "@budibase/types" import { expectAnyInternalColsAttributes, @@ -400,9 +401,9 @@ describe("/rows", () => { const queryUsage = await getQueryUsage() const res = await config.api.row.patch(table._id!, { - _id: existing._id, - _rev: existing._rev, - tableId: table._id, + _id: existing._id!, + _rev: existing._rev!, + tableId: table._id!, name: "Updated Name", }) @@ -428,9 +429,9 @@ describe("/rows", () => { await config.api.row.patch( table._id!, { - _id: existing._id, - _rev: existing._rev, - tableId: table._id, + _id: existing._id!, + _rev: existing._rev!, + tableId: table._id!, name: 1, }, { expectStatus: 400 } @@ -451,7 +452,7 @@ describe("/rows", () => { const [row] = searchResponse.body.rows as Row[] const res = await config.api.row.patch(table._id!, { - ...row, + ...(row as PatchRowRequest), name: "Updated Name", description: "Updated Description", }) @@ -478,7 +479,7 @@ describe("/rows", () => { const [row] = searchResponse.body.rows as Row[] const res = await config.api.row.patch(table._id!, { - ...row, + ...(row as PatchRowRequest), name: "Updated Name", description: "Updated Description", }) diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts index 42622f091f..fedb8ec146 100644 --- a/packages/types/src/api/web/app/rows.ts +++ b/packages/types/src/api/web/app/rows.ts @@ -1,6 +1,10 @@ import { Row } from "../../../documents" -export interface PatchRowRequest extends Row {} +export interface PatchRowRequest extends Row { + _id: string + _rev: string + tableId: string +} export interface PatchRowResponse extends Row {} From 11f05694469dda348364cb0d560e31c47d8563c8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 14:12:58 +0200 Subject: [PATCH 09/14] Move row.validate to the sdk --- .../src/api/controllers/row/external.ts | 5 +- .../src/api/controllers/row/internal.ts | 8 +- packages/server/src/sdk/app/rows/utils.ts | 91 ++++++++++++++++++- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index 0b6471eefd..aac94707e6 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -15,7 +15,6 @@ import { UserCtx, } from "@budibase/types" import sdk from "../../../sdk" -import * as utils from "./utils" async function getRow( tableId: string, @@ -61,7 +60,7 @@ export async function patch(ctx: UserCtx) { const tableId = ctx.params.tableId const { id, ...rowData } = ctx.request.body - const validateResult = await utils.validate({ + const validateResult = await sdk.rows.utils.validate({ row: rowData, tableId, }) @@ -84,7 +83,7 @@ export async function patch(ctx: UserCtx) { export async function save(ctx: UserCtx) { const inputs = ctx.request.body const tableId = ctx.params.tableId - const validateResult = await utils.validate({ + const validateResult = await sdk.rows.utils.validate({ row: inputs, tableId, }) diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts index cdf0a396dc..1153461b89 100644 --- a/packages/server/src/api/controllers/row/internal.ts +++ b/packages/server/src/api/controllers/row/internal.ts @@ -30,8 +30,8 @@ export async function patch(ctx: UserCtx) { const tableId = inputs.tableId const isUserTable = tableId === InternalTables.USER_METADATA let oldRow + const dbTable = await sdk.tables.getTable(tableId) try { - let dbTable = await sdk.tables.getTable(tableId) oldRow = await outputProcessing( dbTable, await utils.findRow(ctx, tableId, inputs._id!) @@ -47,7 +47,7 @@ export async function patch(ctx: UserCtx) { throw "Row does not exist" } } - let dbTable = await sdk.tables.getTable(tableId) + // need to build up full patch fields before coerce let combinedRow: any = cloneDeep(oldRow) for (let key of Object.keys(inputs)) { @@ -60,7 +60,7 @@ export async function patch(ctx: UserCtx) { // this returns the table and row incase they have been updated let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow) - const validateResult = await utils.validate({ + const validateResult = await sdk.rows.utils.validate({ row, table, }) @@ -109,7 +109,7 @@ export async function save(ctx: UserCtx) { let { table, row } = inputProcessing(ctx.user, tableClone, inputs) - const validateResult = await utils.validate({ + const validateResult = await sdk.rows.utils.validate({ row, table, }) diff --git a/packages/server/src/sdk/app/rows/utils.ts b/packages/server/src/sdk/app/rows/utils.ts index 6a037a4ade..51e418c324 100644 --- a/packages/server/src/sdk/app/rows/utils.ts +++ b/packages/server/src/sdk/app/rows/utils.ts @@ -1,4 +1,6 @@ -import { TableSchema } from "@budibase/types" +import cloneDeep from "lodash/cloneDeep" +import validateJs from "validate.js" +import { FieldType, Row, Table, TableSchema } from "@budibase/types" import { FieldTypes } from "../../../constants" import { makeExternalQuery } from "../../../integrations/base/query" import { Format } from "../../../api/controllers/view/exporters" @@ -46,3 +48,90 @@ export function cleanExportRows( return cleanRows } + +function isForeignKey(key: string, table: Table) { + const relationships = Object.values(table.schema).filter( + column => column.type === FieldType.LINK + ) + return relationships.some(relationship => relationship.foreignKey === key) +} + +export async function validate({ + tableId, + row, + table, +}: { + tableId?: string + row: Row + table?: Table +}): Promise<{ + valid: boolean + errors: Record +}> { + let fetchedTable: Table + if (!table) { + fetchedTable = await sdk.tables.getTable(tableId) + } else { + fetchedTable = table + } + const errors: Record = {} + for (let fieldName of Object.keys(fetchedTable.schema)) { + const column = fetchedTable.schema[fieldName] + const constraints = cloneDeep(column.constraints) + const type = column.type + // foreign keys are likely to be enriched + if (isForeignKey(fieldName, fetchedTable)) { + continue + } + // formulas shouldn't validated, data will be deleted anyway + if (type === FieldTypes.FORMULA || column.autocolumn) { + continue + } + // special case for options, need to always allow unselected (empty) + if (type === FieldTypes.OPTIONS && constraints?.inclusion) { + constraints.inclusion.push(null as any, "") + } + let res + + // Validate.js doesn't seem to handle array + if (type === FieldTypes.ARRAY && row[fieldName]) { + if (row[fieldName].length) { + if (!Array.isArray(row[fieldName])) { + row[fieldName] = row[fieldName].split(",") + } + row[fieldName].map((val: any) => { + if ( + !constraints?.inclusion?.includes(val) && + constraints?.inclusion?.length !== 0 + ) { + errors[fieldName] = "Field not in list" + } + }) + } else if (constraints?.presence && row[fieldName].length === 0) { + // non required MultiSelect creates an empty array, which should not throw errors + errors[fieldName] = [`${fieldName} is required`] + } + } else if ( + (type === FieldTypes.ATTACHMENT || type === FieldTypes.JSON) && + typeof row[fieldName] === "string" + ) { + // this should only happen if there is an error + try { + const json = JSON.parse(row[fieldName]) + if (type === FieldTypes.ATTACHMENT) { + if (Array.isArray(json)) { + row[fieldName] = json + } else { + errors[fieldName] = [`Must be an array`] + } + } + } catch (err) { + errors[fieldName] = [`Contains invalid JSON`] + } + } else { + res = validateJs.single(row[fieldName], constraints) + } + if (res) errors[fieldName] = res + } + return { valid: Object.keys(errors).length === 0, errors } +} From b0af0a287e897753148166c2d2fda8ed491cb325 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 14:53:51 +0200 Subject: [PATCH 10/14] Remove view tests --- .../server/src/api/routes/tests/row.spec.ts | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index b153627992..dbc417a5b5 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -440,62 +440,6 @@ describe("/rows", () => { await assertRowUsage(rowUsage) await assertQueryUsage(queryUsage) }) - - describe("view row", () => { - it("should update only the fields that are supplied", async () => { - const existing = await config.createRow() - const view = await config.api.viewV2.create({ - columns: { name: { visible: true } }, - }) - const searchResponse = await config.api.viewV2.search(view.id) - - const [row] = searchResponse.body.rows as Row[] - - const res = await config.api.row.patch(table._id!, { - ...(row as PatchRowRequest), - name: "Updated Name", - description: "Updated Description", - }) - - const savedRow = await loadRow(res.body._id, table._id!) - - expect(savedRow.body).toEqual({ - ...existing, - name: "Updated Name", - description: "Updated Description", - _rev: expect.anything(), - createdAt: expect.anything(), - updatedAt: expect.anything(), - }) - }) - - it("should not edit fields that don't belong to the view", async () => { - const existing = await config.createRow() - const view = await config.api.viewV2.create({ - columns: { name: { visible: true } }, - }) - const searchResponse = await config.api.viewV2.search(view.id) - - const [row] = searchResponse.body.rows as Row[] - - const res = await config.api.row.patch(table._id!, { - ...(row as PatchRowRequest), - name: "Updated Name", - description: "Updated Description", - }) - - const savedRow = await loadRow(res.body._id, table._id!) - - expect(savedRow.body).toEqual({ - ...existing, - name: "Updated Name", - _rev: expect.anything(), - createdAt: expect.anything(), - updatedAt: expect.anything(), - }) - expect(savedRow.body.description).not.toEqual("Updated Description") - }) - }) }) describe("destroy", () => { From 508e30edae0810bdb8ae1dccf12a599ec8c73dd7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 14:54:59 +0200 Subject: [PATCH 11/14] Fix sdk refs --- packages/server/src/api/controllers/row/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 92771411b5..4cbf17d844 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -295,7 +295,7 @@ export async function validate(ctx: Ctx) { if (isExternalTable(tableId)) { ctx.body = { valid: true } } else { - ctx.body = await utils.validate({ + ctx.body = await sdk.rows.utils.validate({ row: ctx.request.body, tableId, }) From 292bb2ad6218d5651abd1fdb0113f22919217480 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 15:31:44 +0200 Subject: [PATCH 12/14] _viewId instead of viewId --- packages/types/src/documents/app/row.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index f659caf20e..c09dc79b95 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -30,6 +30,6 @@ export interface RowAttachment { export interface Row extends Document { type?: string tableId?: string - viewId?: string + _viewId?: string [key: string]: any } From 2457bf1b37efd036786cdb0f996ec2e610999e41 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 26 Jul 2023 16:03:14 +0200 Subject: [PATCH 13/14] Extract external getrow to sdk --- .../src/api/controllers/row/external.ts | 24 ++++++------------- packages/server/src/sdk/app/rows/external.ts | 17 +++++++++++++ packages/server/src/sdk/app/rows/index.ts | 4 +++- 3 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 packages/server/src/sdk/app/rows/external.ts diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index aac94707e6..802c70b6cb 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -16,20 +16,6 @@ import { } from "@budibase/types" import sdk from "../../../sdk" -async function getRow( - tableId: string, - rowId: string, - opts?: { relationships?: boolean } -) { - const response = (await handleRequest(Operation.READ, tableId, { - id: breakRowIdField(rowId), - includeSqlRelationships: opts?.relationships - ? IncludeRelationship.INCLUDE - : IncludeRelationship.EXCLUDE, - })) as Row[] - return response ? response[0] : response -} - export async function handleRequest( operation: Operation, tableId: string, @@ -71,7 +57,9 @@ export async function patch(ctx: UserCtx) { id: breakRowIdField(id), row: rowData, }) - const row = await getRow(tableId, id, { relationships: true }) + const row = await sdk.rows.external.getRow(tableId, id, { + relationships: true, + }) const table = await sdk.tables.getTable(tableId) return { ...response, @@ -96,7 +84,9 @@ export async function save(ctx: UserCtx) { const responseRow = response as { row: Row } const rowId = responseRow.row._id if (rowId) { - const row = await getRow(tableId, rowId, { relationships: true }) + const row = await sdk.rows.external.getRow(tableId, rowId, { + relationships: true, + }) return { ...response, row, @@ -109,7 +99,7 @@ export async function save(ctx: UserCtx) { export async function find(ctx: UserCtx) { const id = ctx.params.rowId const tableId = ctx.params.tableId - return getRow(tableId, id) + return sdk.rows.external.getRow(tableId, id) } export async function destroy(ctx: UserCtx) { diff --git a/packages/server/src/sdk/app/rows/external.ts b/packages/server/src/sdk/app/rows/external.ts new file mode 100644 index 0000000000..568bd07e9d --- /dev/null +++ b/packages/server/src/sdk/app/rows/external.ts @@ -0,0 +1,17 @@ +import { IncludeRelationship, Operation, Row } from "@budibase/types" +import { handleRequest } from "../../../api/controllers/row/external" +import { breakRowIdField } from "../../../integrations/utils" + +export async function getRow( + tableId: string, + rowId: string, + opts?: { relationships?: boolean } +) { + const response = (await handleRequest(Operation.READ, tableId, { + id: breakRowIdField(rowId), + includeSqlRelationships: opts?.relationships + ? IncludeRelationship.INCLUDE + : IncludeRelationship.EXCLUDE, + })) as Row[] + return response ? response[0] : response +} diff --git a/packages/server/src/sdk/app/rows/index.ts b/packages/server/src/sdk/app/rows/index.ts index 1ba91b134f..ea501e93d9 100644 --- a/packages/server/src/sdk/app/rows/index.ts +++ b/packages/server/src/sdk/app/rows/index.ts @@ -2,10 +2,12 @@ import * as attachments from "./attachments" import * as rows from "./rows" import * as search from "./search" import * as utils from "./utils" +import * as external from "./external" export default { ...attachments, ...rows, ...search, - utils: utils, + utils, + external, } From 6795cb15e8b4abd9c11589cbe810d53e3bdb31f0 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 27 Jul 2023 15:43:30 +0000 Subject: [PATCH 14/14] Bump version to 2.8.29-alpha.1 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 54ba89443c..252d319ab9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.8.29-alpha.0", + "version": "2.8.29-alpha.1", "npmClient": "yarn", "packages": [ "packages/*"