diff --git a/.prettierignore b/.prettierignore index a73fed4890..64607d74ab 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,5 @@ packages/backend-core/coverage packages/server/client packages/server/src/definitions/openapi.ts packages/builder/.routify -packages/sdk/sdk \ No newline at end of file +packages/sdk/sdk +packages/pro/coverage \ No newline at end of file diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index b24b642035..2c4428599b 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -1,4 +1,5 @@ import { + AutoReason, Datasource, FieldSchema, FieldType, @@ -24,7 +25,7 @@ import { isSQL, } from "../../../integrations/utils" import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils" -import { FieldTypes } from "../../../constants" +import { AutoFieldSubTypes, FieldTypes } from "../../../constants" import { processObjectSync } from "@budibase/string-templates" import { cloneDeep } from "lodash/fp" import { processDates, processFormulas } from "../../../utilities/rowProcessor" @@ -259,6 +260,15 @@ function isOneSide(field: FieldSchema) { ) } +function isEditableColumn(column: FieldSchema) { + const isExternalAutoColumn = + column.autocolumn && + column.autoReason !== AutoReason.FOREIGN_KEY && + column.subtype !== AutoFieldSubTypes.AUTO_ID + const isFormula = column.type === FieldTypes.FORMULA + return !(isExternalAutoColumn || isFormula) +} + export class ExternalRequest { private operation: Operation private tableId: string @@ -295,11 +305,7 @@ export class ExternalRequest { manyRelationships: ManyRelationship[] = [] for (let [key, field] of Object.entries(table.schema)) { // if set already, or not set just skip it - if ( - row[key] == null || - newRow[key] || - !sdk.tables.isEditableColumn(field) - ) { + if (row[key] == null || newRow[key] || !isEditableColumn(field)) { continue } // if its an empty string then it means return the column to null (if possible) diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index 491cbfa8f9..3881d1dd08 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -18,6 +18,8 @@ import { import sdk from "../../../sdk" import * as utils from "./utils" import { dataFilters } from "@budibase/shared-core" +import { inputProcessing } from "../../../utilities/rowProcessor" +import { cloneDeep, isEqual } from "lodash" export async function handleRequest( operation: Operation, @@ -88,10 +90,24 @@ export async function save(ctx: UserCtx) { if (!validateResult.valid) { throw { validation: validateResult.errors } } + + const table = await sdk.tables.getTable(tableId) + const { table: updatedTable, row } = inputProcessing( + ctx.user, + cloneDeep(table), + inputs + ) + const response = await handleRequest(Operation.CREATE, tableId, { - row: inputs, + row, }) + const responseRow = response as { row: Row } + + if (!isEqual(table, updatedTable)) { + await sdk.tables.saveTable(updatedTable) + } + const rowId = responseRow.row._id if (rowId) { const row = await sdk.rows.external.getRow(tableId, rowId, { @@ -106,10 +122,18 @@ export async function save(ctx: UserCtx) { } } -export async function find(ctx: UserCtx) { +export async function find(ctx: UserCtx): Promise { const id = ctx.params.rowId const tableId = utils.getTableId(ctx) - return sdk.rows.external.getRow(tableId, id) + const row = await sdk.rows.external.getRow(tableId, id, { + relationships: true, + }) + + if (!row) { + ctx.throw(404) + } + + return row } export async function destroy(ctx: UserCtx) { @@ -119,7 +143,7 @@ export async function destroy(ctx: UserCtx) { id: breakRowIdField(_id), includeSqlRelationships: IncludeRelationship.EXCLUDE, })) as { row: Row } - return { response: { ok: true }, row } + return { response: { ok: true, id: _id }, row } } export async function bulkDestroy(ctx: UserCtx) { diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 88d3f50dbe..f0f2462019 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -14,6 +14,10 @@ import { SearchRowResponse, SearchRowRequest, SearchParams, + GetRowResponse, + ValidateResponse, + ExportRowsRequest, + ExportRowsResponse, } from "@budibase/types" import * as utils from "./utils" import { gridSocket } from "../../../websockets" @@ -111,7 +115,7 @@ export async function fetch(ctx: any) { }) } -export async function find(ctx: any) { +export async function find(ctx: UserCtx) { const tableId = utils.getTableId(ctx) ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx), { datasourceId: tableId, @@ -214,11 +218,11 @@ export async function search(ctx: Ctx) { }) } -export async function validate(ctx: Ctx) { +export async function validate(ctx: Ctx) { const tableId = utils.getTableId(ctx) // external tables are hard to validate currently if (isExternalTable(tableId)) { - ctx.body = { valid: true } + ctx.body = { valid: true, errors: {} } } else { ctx.body = await sdk.rows.utils.validate({ row: ctx.request.body, @@ -237,7 +241,9 @@ export async function fetchEnrichedRow(ctx: any) { ) } -export const exportRows = async (ctx: any) => { +export const exportRows = async ( + ctx: Ctx +) => { const tableId = utils.getTableId(ctx) const format = ctx.query.format diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts index 4a412c42eb..b80bd339e6 100644 --- a/packages/server/src/api/controllers/row/internal.ts +++ b/packages/server/src/api/controllers/row/internal.ts @@ -131,7 +131,7 @@ export async function save(ctx: UserCtx) { }) } -export async function find(ctx: UserCtx) { +export async function find(ctx: UserCtx): Promise { const tableId = utils.getTableId(ctx), rowId = ctx.params.rowId const table = await sdk.tables.getTable(tableId) diff --git a/packages/server/src/api/controllers/row/utils.ts b/packages/server/src/api/controllers/row/utils.ts index e85ec4553c..c1df80600c 100644 --- a/packages/server/src/api/controllers/row/utils.ts +++ b/packages/server/src/api/controllers/row/utils.ts @@ -27,7 +27,7 @@ validateJs.extend(validateJs.validators.datetime, { export async function findRow(ctx: UserCtx, tableId: string, rowId: string) { const db = context.getAppDB() - let row + let row: Row // TODO remove special user case in future if (tableId === InternalTables.USER_METADATA) { ctx.params = { diff --git a/packages/server/src/api/controllers/table/index.ts b/packages/server/src/api/controllers/table/index.ts index 759974d6a7..29c41ad985 100644 --- a/packages/server/src/api/controllers/table/index.ts +++ b/packages/server/src/api/controllers/table/index.ts @@ -78,9 +78,9 @@ export async function save(ctx: UserCtx) { ctx.status = 200 ctx.message = `Table ${table.name} saved successfully.` ctx.eventEmitter && - ctx.eventEmitter.emitTable(`table:save`, appId, savedTable) + ctx.eventEmitter.emitTable(`table:save`, appId, { ...savedTable }) ctx.body = savedTable - builderSocket?.emitTableUpdate(ctx, savedTable) + builderSocket?.emitTableUpdate(ctx, { ...savedTable }) } export async function destroy(ctx: UserCtx) { diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index f63be2318f..a74a9f7960 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1,3 +1,5 @@ +import { databaseTestProviders } from "../../../integrations/tests/utils" + import tk from "timekeeper" import { outputProcessing } from "../../../utilities/rowProcessor" import * as setup from "./utilities" @@ -8,13 +10,16 @@ import { MonthlyQuotaName, PermissionLevel, QuotaUsageType, + RelationshipType, Row, + SaveTableRequest, SortOrder, SortType, StaticQuotaName, Table, } from "@budibase/types" import { + expectAnyExternalColsAttributes, expectAnyInternalColsAttributes, generator, mocks, @@ -26,30 +31,68 @@ tk.freeze(timestamp) const { basicRow } = setup.structures -describe("/rows", () => { - let request = setup.getRequest() - let config = setup.getConfig() +describe.each([ + ["internal", undefined], + ["postgres", databaseTestProviders.postgres], +])("/rows (%s)", (_, dsProvider) => { + const isInternal = !dsProvider + + const request = setup.getRequest() + const config = setup.getConfig() let table: Table - let row: Row + let tableId: string afterAll(setup.afterAll) beforeAll(async () => { await config.init() + + if (dsProvider) { + await config.createDatasource({ + datasource: await dsProvider.getDsConfig(), + }) + } }) + const generateTableConfig: () => SaveTableRequest = () => { + return { + name: generator.word(), + type: "table", + primary: ["id"], + primaryDisplay: "name", + schema: { + id: { + type: FieldType.AUTO, + name: "id", + autocolumn: true, + constraints: { + presence: true, + }, + }, + name: { + type: FieldType.STRING, + name: "name", + constraints: { + type: "string", + }, + }, + description: { + type: FieldType.STRING, + name: "description", + constraints: { + type: "string", + }, + }, + }, + } + } + beforeEach(async () => { mocks.licenses.useCloudFree() - table = await config.createTable() - row = basicRow(table._id!) }) - const loadRow = async (id: string, tbl_Id: string, status = 200) => - await request - .get(`/api/${tbl_Id}/rows/${id}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(status) + const loadRow = (id: string, tbl_Id: string, status = 200) => + config.api.row.get(tbl_Id, id, { expectStatus: status }) const getRowUsage = async () => { const { total } = await config.doInContext(null, () => @@ -78,19 +121,33 @@ describe("/rows", () => { expect(usage).toBe(expected) } + const defaultRowFields = isInternal + ? { + type: "row", + createdAt: timestamp, + updatedAt: timestamp, + } + : undefined + + beforeAll(async () => { + const tableConfig = generateTableConfig() + const table = await config.createTable(tableConfig) + tableId = table._id! + }) + describe("save, load, update", () => { it("returns a success message when the row is created", async () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() const res = await request - .post(`/api/${row.tableId}/rows`) - .send(row) + .post(`/api/${tableId}/rows`) + .send(basicRow(tableId)) .set(config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) expect((res as any).res.statusMessage).toEqual( - `${table.name} saved successfully` + `${config.table!.name} saved successfully` ) expect(res.body.name).toEqual("Test Contact") expect(res.body._rev).toBeDefined() @@ -102,47 +159,43 @@ describe("/rows", () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const newTable = await config.createTable({ - name: "TestTableAuto", - type: "table", - schema: { - ...table.schema, - "Row ID": { - name: "Row ID", - type: FieldType.NUMBER, - subtype: "autoID", - icon: "ri-magic-line", - autocolumn: true, - constraints: { - type: "number", - presence: false, - numericality: { - greaterThanOrEqualTo: "", - lessThanOrEqualTo: "", + const tableConfig = generateTableConfig() + const newTable = await config.createTable( + { + ...tableConfig, + name: "TestTableAuto", + schema: { + ...tableConfig.schema, + "Row ID": { + name: "Row ID", + type: FieldType.NUMBER, + subtype: "autoID", + icon: "ri-magic-line", + autocolumn: true, + constraints: { + type: "number", + presence: true, + numericality: { + greaterThanOrEqualTo: "", + lessThanOrEqualTo: "", + }, }, }, }, }, - }) + { skipReassigning: true } + ) const ids = [1, 2, 3] // Performing several create row requests should increment the autoID fields accordingly const createRow = async (id: number) => { - const res = await request - .post(`/api/${newTable._id}/rows`) - .send({ - name: "row_" + id, - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect((res as any).res.statusMessage).toEqual( - `${newTable.name} saved successfully` - ) - expect(res.body.name).toEqual("row_" + id) - expect(res.body._rev).toBeDefined() - expect(res.body["Row ID"]).toEqual(id) + const res = await config.api.row.save(newTable._id!, { + name: "row_" + id, + }) + expect(res.name).toEqual("row_" + id) + expect(res._rev).toBeDefined() + expect(res["Row ID"]).toEqual(id) } for (let i = 0; i < ids.length; i++) { @@ -158,22 +211,14 @@ describe("/rows", () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .post(`/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.save(tableId, { + _id: existing._id, + _rev: existing._rev, + tableId, + name: "Updated Name", + }) - expect((res as any).res.statusMessage).toEqual( - `${table.name} updated successfully.` - ) - expect(res.body.name).toEqual("Updated Name") + expect(res.name).toEqual("Updated Name") await assertRowUsage(rowUsage) await assertQueryUsage(queryUsage + 1) }) @@ -182,42 +227,34 @@ describe("/rows", () => { const existing = await config.createRow() const queryUsage = await getQueryUsage() - const res = await request - .get(`/api/${table._id}/rows/${existing._id}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.get(tableId, existing._id!) expect(res.body).toEqual({ - ...row, - _id: existing._id, - _rev: existing._rev, - type: "row", - createdAt: timestamp, - updatedAt: timestamp, + ...existing, + ...defaultRowFields, }) await assertQueryUsage(queryUsage + 1) }) it("should list all rows for given tableId", async () => { + const table = await config.createTable(generateTableConfig(), { + skipReassigning: true, + }) + const tableId = table._id! const newRow = { - tableId: table._id, + tableId, name: "Second Contact", - status: "new", + description: "new", } - await config.createRow() + const firstRow = await config.createRow({ tableId }) await config.createRow(newRow) const queryUsage = await getQueryUsage() - const res = await request - .get(`/api/${table._id}/rows`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.fetch(tableId) - expect(res.body.length).toBe(2) - expect(res.body.find((r: Row) => r.name === newRow.name)).toBeDefined() - expect(res.body.find((r: Row) => r.name === row.name)).toBeDefined() + expect(res.length).toBe(2) + expect(res.find((r: Row) => r.name === newRow.name)).toBeDefined() + expect(res.find((r: Row) => r.name === firstRow.name)).toBeDefined() await assertQueryUsage(queryUsage + 1) }) @@ -225,55 +262,54 @@ describe("/rows", () => { await config.createRow() const queryUsage = await getQueryUsage() - await request - .get(`/api/${table._id}/rows/not-a-valid-id`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(404) + await config.api.row.get(tableId, "1234567", { + expectStatus: 404, + }) await assertQueryUsage(queryUsage) // no change }) - it("row values are coerced", async () => { - const str = { - type: FieldType.STRING, - name: "str", - constraints: { type: "string", presence: false }, - } - const attachment = { - type: FieldType.ATTACHMENT, - name: "attachment", - constraints: { type: "array", presence: false }, - } - const bool = { - type: FieldType.BOOLEAN, - name: "boolean", - constraints: { type: "boolean", presence: false }, - } - const number = { - type: FieldType.NUMBER, - name: "str", - constraints: { type: "number", presence: false }, - } - const datetime = { - type: FieldType.DATETIME, - name: "datetime", - constraints: { - type: "string", - presence: false, - datetime: { earliest: "", latest: "" }, - }, - } - const arrayField = { - type: FieldType.ARRAY, - constraints: { - type: "array", - presence: false, - inclusion: ["One", "Two", "Three"], - }, - name: "Sample Tags", - sortable: false, - } - const optsField = { + isInternal && + it("row values are coerced", async () => { + const str = { + type: FieldType.STRING, + name: "str", + constraints: { type: "string", presence: false }, + } + const attachment = { + type: FieldType.ATTACHMENT, + name: "attachment", + constraints: { type: "array", presence: false }, + } + const bool = { + type: FieldType.BOOLEAN, + name: "boolean", + constraints: { type: "boolean", presence: false }, + } + const number = { + type: FieldType.NUMBER, + name: "str", + constraints: { type: "number", presence: false }, + } + const datetime = { + type: FieldType.DATETIME, + name: "datetime", + constraints: { + type: "string", + presence: false, + datetime: { earliest: "", latest: "" }, + }, + } + const arrayField = { + type: FieldType.ARRAY, + constraints: { + type: "array", + presence: false, + inclusion: ["One", "Two", "Three"], + }, + name: "Sample Tags", + sortable: false, + } + const optsField = { fieldName: "Sample Opts", name: "Sample Opts", type: FieldType.OPTIONS, @@ -282,8 +318,8 @@ describe("/rows", () => { presence: false, inclusion: ["Alpha", "Beta", "Gamma"], }, - }, - table = await config.createTable({ + } + const table = await config.createTable({ name: "TestTable2", type: "table", schema: { @@ -321,92 +357,93 @@ describe("/rows", () => { }, }) - row = { - name: "Test Row", - stringUndefined: undefined, - stringNull: null, - stringString: "i am a string", - numberEmptyString: "", - numberNull: null, - numberUndefined: undefined, - numberString: "123", - numberNumber: 123, - datetimeEmptyString: "", - datetimeNull: null, - datetimeUndefined: undefined, - datetimeString: "1984-04-20T00:00:00.000Z", - datetimeDate: new Date("1984-04-20"), - boolNull: null, - boolEmpty: "", - boolUndefined: undefined, - boolString: "true", - boolBool: true, - tableId: table._id, - attachmentNull: null, - attachmentUndefined: undefined, - attachmentEmpty: "", - attachmentEmptyArrayStr: "[]", - arrayFieldEmptyArrayStr: "[]", - arrayFieldUndefined: undefined, - arrayFieldNull: null, - arrayFieldArrayStrKnown: "['One']", - optsFieldEmptyStr: "", - optsFieldUndefined: undefined, - optsFieldNull: null, - optsFieldStrKnown: "Alpha", - } + const row = { + name: "Test Row", + stringUndefined: undefined, + stringNull: null, + stringString: "i am a string", + numberEmptyString: "", + numberNull: null, + numberUndefined: undefined, + numberString: "123", + numberNumber: 123, + datetimeEmptyString: "", + datetimeNull: null, + datetimeUndefined: undefined, + datetimeString: "1984-04-20T00:00:00.000Z", + datetimeDate: new Date("1984-04-20"), + boolNull: null, + boolEmpty: "", + boolUndefined: undefined, + boolString: "true", + boolBool: true, + tableId: table._id, + attachmentNull: null, + attachmentUndefined: undefined, + attachmentEmpty: "", + attachmentEmptyArrayStr: "[]", + arrayFieldEmptyArrayStr: "[]", + arrayFieldUndefined: undefined, + arrayFieldNull: null, + arrayFieldArrayStrKnown: "['One']", + optsFieldEmptyStr: "", + optsFieldUndefined: undefined, + optsFieldNull: null, + optsFieldStrKnown: "Alpha", + } - const createdRow = await config.createRow(row) - const id = createdRow._id! + const createdRow = await config.createRow(row) + const id = createdRow._id! - const saved = (await loadRow(id, table._id!)).body + const saved = (await loadRow(id, table._id!)).body - expect(saved.stringUndefined).toBe(undefined) - expect(saved.stringNull).toBe("") - expect(saved.stringString).toBe("i am a string") - expect(saved.numberEmptyString).toBe(null) - expect(saved.numberNull).toBe(null) - expect(saved.numberUndefined).toBe(undefined) - expect(saved.numberString).toBe(123) - expect(saved.numberNumber).toBe(123) - expect(saved.datetimeEmptyString).toBe(null) - expect(saved.datetimeNull).toBe(null) - expect(saved.datetimeUndefined).toBe(undefined) - expect(saved.datetimeString).toBe( - new Date(row.datetimeString).toISOString() - ) - expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString()) - expect(saved.boolNull).toBe(null) - expect(saved.boolEmpty).toBe(null) - expect(saved.boolUndefined).toBe(undefined) - expect(saved.boolString).toBe(true) - expect(saved.boolBool).toBe(true) - expect(saved.attachmentNull).toEqual([]) - expect(saved.attachmentUndefined).toBe(undefined) - expect(saved.attachmentEmpty).toEqual([]) - expect(saved.attachmentEmptyArrayStr).toEqual([]) - expect(saved.arrayFieldEmptyArrayStr).toEqual([]) - expect(saved.arrayFieldNull).toEqual([]) - expect(saved.arrayFieldUndefined).toEqual(undefined) - expect(saved.optsFieldEmptyStr).toEqual(null) - expect(saved.optsFieldUndefined).toEqual(undefined) - expect(saved.optsFieldNull).toEqual(null) - expect(saved.arrayFieldArrayStrKnown).toEqual(["One"]) - expect(saved.optsFieldStrKnown).toEqual("Alpha") - }) + expect(saved.stringUndefined).toBe(undefined) + expect(saved.stringNull).toBe("") + expect(saved.stringString).toBe("i am a string") + expect(saved.numberEmptyString).toBe(null) + expect(saved.numberNull).toBe(null) + expect(saved.numberUndefined).toBe(undefined) + expect(saved.numberString).toBe(123) + expect(saved.numberNumber).toBe(123) + expect(saved.datetimeEmptyString).toBe(null) + expect(saved.datetimeNull).toBe(null) + expect(saved.datetimeUndefined).toBe(undefined) + expect(saved.datetimeString).toBe( + new Date(row.datetimeString).toISOString() + ) + expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString()) + expect(saved.boolNull).toBe(null) + expect(saved.boolEmpty).toBe(null) + expect(saved.boolUndefined).toBe(undefined) + expect(saved.boolString).toBe(true) + expect(saved.boolBool).toBe(true) + expect(saved.attachmentNull).toEqual([]) + expect(saved.attachmentUndefined).toBe(undefined) + expect(saved.attachmentEmpty).toEqual([]) + expect(saved.attachmentEmptyArrayStr).toEqual([]) + expect(saved.arrayFieldEmptyArrayStr).toEqual([]) + expect(saved.arrayFieldNull).toEqual([]) + expect(saved.arrayFieldUndefined).toEqual(undefined) + expect(saved.optsFieldEmptyStr).toEqual(null) + expect(saved.optsFieldUndefined).toEqual(undefined) + expect(saved.optsFieldNull).toEqual(null) + expect(saved.arrayFieldArrayStrKnown).toEqual(["One"]) + expect(saved.optsFieldStrKnown).toEqual("Alpha") + }) }) describe("view save", () => { - function orderTable(): Table { - return { + it("views have extra data trimmed", async () => { + const table = await config.createTable({ name: "orders", + primary: ["OrderID"], schema: { Country: { type: FieldType.STRING, name: "Country", }, OrderID: { - type: FieldType.STRING, + type: FieldType.NUMBER, name: "OrderID", }, Story: { @@ -414,32 +451,48 @@ describe("/rows", () => { name: "Story", }, }, - } - } + }) - it("views have extra data trimmed", async () => { - const table = await config.createTable(orderTable()) - - const createViewResponse = await config.api.viewV2.create({ - tableId: table._id, + const createViewResponse = await config.createView({ + name: generator.word(), schema: { - Country: {}, - OrderID: {}, + Country: { + visible: true, + }, + OrderID: { + visible: true, + }, }, }) - const response = await config.api.row.save(createViewResponse.id, { - Country: "Aussy", - OrderID: "1111", - Story: "aaaaa", - }) + const createRowResponse = await config.api.row.save( + createViewResponse.id, + { + OrderID: "1111", + Country: "Aussy", + Story: "aaaaa", + } + ) - const row = await config.api.row.get(table._id!, response._id!) + const row = await config.api.row.get(table._id!, createRowResponse._id!) expect(row.body.Story).toBeUndefined() + expect(row.body).toEqual({ + ...defaultRowFields, + OrderID: 1111, + Country: "Aussy", + _id: createRowResponse._id, + _rev: createRowResponse._rev, + tableId: table._id, + }) }) }) describe("patch", () => { + beforeAll(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) + }) + it("should update only the fields that are supplied", async () => { const existing = await config.createRow() @@ -489,19 +542,17 @@ describe("/rows", () => { }) describe("destroy", () => { + beforeAll(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) + }) + it("should be able to delete a row", async () => { - const createdRow = await config.createRow(row) + const createdRow = await config.createRow() const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .delete(`/api/${table._id}/rows`) - .send({ - rows: [createdRow], - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.delete(table._id!, [createdRow]) expect(res.body[0]._id).toEqual(createdRow._id) await assertRowUsage(rowUsage - 1) await assertQueryUsage(queryUsage + 1) @@ -509,19 +560,19 @@ describe("/rows", () => { }) describe("validate", () => { + beforeAll(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) + }) + it("should return no errors on valid row", async () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .post(`/api/${table._id}/rows/validate`) - .send({ name: "ivan" }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.validate(table._id!, { name: "ivan" }) - expect(res.body.valid).toBe(true) - expect(Object.keys(res.body.errors)).toEqual([]) + expect(res.valid).toBe(true) + expect(Object.keys(res.errors)).toEqual([]) await assertRowUsage(rowUsage) await assertQueryUsage(queryUsage) }) @@ -530,35 +581,34 @@ describe("/rows", () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .post(`/api/${table._id}/rows/validate`) - .send({ name: 1 }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.validate(table._id!, { name: 1 }) - expect(res.body.valid).toBe(false) - expect(Object.keys(res.body.errors)).toEqual(["name"]) + if (isInternal) { + expect(res.valid).toBe(false) + expect(Object.keys(res.errors)).toEqual(["name"]) + } else { + // Validation for external is not implemented, so it will always return valid + expect(res.valid).toBe(true) + expect(Object.keys(res.errors)).toEqual([]) + } await assertRowUsage(rowUsage) await assertQueryUsage(queryUsage) }) }) describe("bulkDelete", () => { + beforeAll(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) + }) + it("should be able to delete a bulk set of rows", async () => { const row1 = await config.createRow() const row2 = await config.createRow() const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .delete(`/api/${table._id}/rows`) - .send({ - rows: [row1, row2], - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.delete(table._id!, [row1, row2]) expect(res.body.length).toEqual(2) await loadRow(row1._id!, table._id!, 404) @@ -567,20 +617,19 @@ describe("/rows", () => { }) it("should be able to delete a variety of row set types", async () => { - const row1 = await config.createRow() - const row2 = await config.createRow() - const row3 = await config.createRow() + const [row1, row2, row3] = await Promise.all([ + config.createRow(), + config.createRow(), + config.createRow(), + ]) const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .delete(`/api/${table._id}/rows`) - .send({ - rows: [row1, row2._id, { _id: row3._id }], - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.delete(table._id!, [ + row1, + row2._id, + { _id: row3._id }, + ]) expect(res.body.length).toEqual(3) await loadRow(row1._id!, table._id!, 404) @@ -593,12 +642,7 @@ describe("/rows", () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .delete(`/api/${table._id}/rows`) - .send(row1) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.delete(table._id!, row1) expect(res.body.id).toEqual(row1._id) await loadRow(row1._id!, table._id!, 404) @@ -610,31 +654,23 @@ describe("/rows", () => { const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() - const res = await request - .delete(`/api/${table._id}/rows`) - .send({ not: "valid" }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(400) - + const res = await config.api.row.delete( + table._id!, + { not: "valid" }, + { expectStatus: 400 } + ) expect(res.body.message).toEqual("Invalid delete rows request") - const res2 = await request - .delete(`/api/${table._id}/rows`) - .send({ rows: 123 }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(400) - + const res2 = await config.api.row.delete( + table._id!, + { rows: 123 }, + { expectStatus: 400 } + ) expect(res2.body.message).toEqual("Invalid delete rows request") - const res3 = await request - .delete(`/api/${table._id}/rows`) - .send("invalid") - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(400) - + const res3 = await config.api.row.delete(table._id!, "invalid", { + expectStatus: 400, + }) expect(res3.body.message).toEqual("Invalid delete rows request") await assertRowUsage(rowUsage) @@ -642,61 +678,86 @@ describe("/rows", () => { }) }) - describe("fetchView", () => { - it("should be able to fetch tables contents via 'view'", async () => { - const row = await config.createRow() - const rowUsage = await getRowUsage() - const queryUsage = await getQueryUsage() + // Legacy views are not available for external + isInternal && + describe("fetchView", () => { + beforeEach(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) + }) - const res = await request - .get(`/api/views/${table._id}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.length).toEqual(1) - expect(res.body[0]._id).toEqual(row._id) - await assertRowUsage(rowUsage) - await assertQueryUsage(queryUsage + 1) + it("should be able to fetch tables contents via 'view'", async () => { + const row = await config.createRow() + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + const res = await config.api.legacyView.get(table._id!) + expect(res.body.length).toEqual(1) + expect(res.body[0]._id).toEqual(row._id) + await assertRowUsage(rowUsage) + await assertQueryUsage(queryUsage + 1) + }) + + it("should throw an error if view doesn't exist", async () => { + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + await config.api.legacyView.get("derp", { expectStatus: 404 }) + + await assertRowUsage(rowUsage) + await assertQueryUsage(queryUsage) + }) + + it("should be able to run on a view", async () => { + const view = await config.createLegacyView({ + tableId: table._id!, + name: "ViewTest", + filters: [], + schema: {}, + }) + const row = await config.createRow() + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + const res = await config.api.legacyView.get(view.name) + expect(res.body.length).toEqual(1) + expect(res.body[0]._id).toEqual(row._id) + + await assertRowUsage(rowUsage) + await assertQueryUsage(queryUsage + 1) + }) }) - it("should throw an error if view doesn't exist", async () => { - const rowUsage = await getRowUsage() - const queryUsage = await getQueryUsage() - - await request - .get(`/api/views/derp`) - .set(config.defaultHeaders()) - .expect(404) - - await assertRowUsage(rowUsage) - await assertQueryUsage(queryUsage) - }) - - it("should be able to run on a view", async () => { - const view = await config.createLegacyView() - const row = await config.createRow() - const rowUsage = await getRowUsage() - const queryUsage = await getQueryUsage() - - const res = await request - .get(`/api/views/${view.name}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.length).toEqual(1) - expect(res.body[0]._id).toEqual(row._id) - - await assertRowUsage(rowUsage) - await assertQueryUsage(queryUsage + 1) - }) - }) - describe("fetchEnrichedRows", () => { + beforeAll(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) + }) + it("should allow enriching some linked rows", async () => { - const { table, firstRow, secondRow } = await tenancy.doInTenant( + const { linkedTable, firstRow, secondRow } = await tenancy.doInTenant( config.getTenantId(), async () => { - const table = await config.createLinkedTable() + const linkedTable = await config.createLinkedTable( + RelationshipType.ONE_TO_MANY, + ["link"], + { + name: generator.word(), + type: "table", + primary: ["id"], + primaryDisplay: "id", + schema: { + id: { + type: FieldType.AUTO, + name: "id", + autocolumn: true, + constraints: { + presence: true, + }, + }, + }, + } + ) const firstRow = await config.createRow({ name: "Test Contact", description: "original description", @@ -706,29 +767,30 @@ describe("/rows", () => { name: "Test 2", description: "og desc", link: [{ _id: firstRow._id }], - tableId: table._id, + tableId: linkedTable._id, }) - return { table, firstRow, secondRow } + return { linkedTable, firstRow, secondRow } } ) const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() // test basic enrichment - const resBasic = await request - .get(`/api/${table._id}/rows/${secondRow._id}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(resBasic.body.link[0]._id).toBe(firstRow._id) - expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact") + const resBasic = await config.api.row.get( + linkedTable._id!, + secondRow._id! + ) + expect(resBasic.body.link.length).toBe(1) + expect(resBasic.body.link[0]).toEqual({ + _id: firstRow._id, + primaryDisplay: firstRow.name, + }) // test full enrichment - const resEnriched = await request - .get(`/api/${table._id}/${secondRow._id}/enrich`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + const resEnriched = await config.api.row.getEnriched( + linkedTable._id!, + secondRow._id! + ) expect(resEnriched.body.link.length).toBe(1) expect(resEnriched.body.link[0]._id).toBe(firstRow._id) expect(resEnriched.body.link[0].name).toBe("Test Contact") @@ -738,43 +800,49 @@ describe("/rows", () => { }) }) - describe("attachments", () => { - it("should allow enriching attachment rows", async () => { - const table = await config.createAttachmentTable() - const attachmentId = `${structures.uuid()}.csv` - const row = await config.createRow({ - name: "test", - description: "test", - attachment: [ - { - key: `${config.getAppId()}/attachments/${attachmentId}`, - }, - ], - tableId: table._id, + isInternal && + describe("attachments", () => { + beforeAll(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) }) - // the environment needs configured for this - await setup.switchToSelfHosted(async () => { - return context.doInAppContext(config.getAppId(), async () => { - const enriched = await outputProcessing(table, [row]) - expect((enriched as Row[])[0].attachment[0].url).toBe( - `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}` - ) + + it("should allow enriching attachment rows", async () => { + const table = await config.createAttachmentTable() + const attachmentId = `${structures.uuid()}.csv` + const row = await config.createRow({ + name: "test", + description: "test", + attachment: [ + { + key: `${config.getAppId()}/attachments/${attachmentId}`, + }, + ], + tableId: table._id, + }) + // the environment needs configured for this + await setup.switchToSelfHosted(async () => { + return context.doInAppContext(config.getAppId(), async () => { + const enriched = await outputProcessing(table, [row]) + expect((enriched as Row[])[0].attachment[0].url).toBe( + `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}` + ) + }) }) }) }) - }) describe("exportData", () => { + beforeAll(async () => { + const tableConfig = generateTableConfig() + table = await config.createTable(tableConfig) + }) + it("should allow exporting all columns", async () => { const existing = await config.createRow() - const res = await request - .post(`/api/${table._id}/rows/exportRows?format=json`) - .set(config.defaultHeaders()) - .send({ - rows: [existing._id], - }) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.exportRows(table._id!, { + rows: [existing._id!], + }) const results = JSON.parse(res.text) expect(results.length).toEqual(1) const row = results[0] @@ -790,15 +858,10 @@ describe("/rows", () => { it("should allow exporting only certain columns", async () => { const existing = await config.createRow() - const res = await request - .post(`/api/${table._id}/rows/exportRows?format=json`) - .set(config.defaultHeaders()) - .send({ - rows: [existing._id], - columns: ["_id"], - }) - .expect("Content-Type", /json/) - .expect(200) + const res = await config.api.row.exportRows(table._id!, { + rows: [existing._id!], + columns: ["_id"], + }) const results = JSON.parse(res.text) expect(results.length).toEqual(1) const row = results[0] @@ -810,18 +873,27 @@ describe("/rows", () => { }) describe("view 2.0", () => { - function userTable(): Table { + async function userTable(): Promise { return { - name: "user", - type: "user", + name: `users_${generator.word()}`, + type: "table", + primary: ["id"], schema: { + id: { + type: FieldType.AUTO, + name: "id", + autocolumn: true, + constraints: { + presence: true, + }, + }, name: { type: FieldType.STRING, name: "name", }, surname: { type: FieldType.STRING, - name: "name", + name: "surname", }, age: { type: FieldType.NUMBER, @@ -849,9 +921,8 @@ describe("/rows", () => { describe("create", () => { it("should persist a new row with only the provided view fields", async () => { - const table = await config.createTable(userTable()) - const view = await config.api.viewV2.create({ - tableId: table._id!, + const table = await config.createTable(await userTable()) + const view = await config.createView({ schema: { name: { visible: true }, surname: { visible: true }, @@ -861,7 +932,7 @@ describe("/rows", () => { const data = randomRowData() const newRow = await config.api.row.save(view.id, { - tableId: config.table!._id, + tableId: table!._id, _viewId: view.id, ...data, }) @@ -871,12 +942,11 @@ describe("/rows", () => { name: data.name, surname: data.surname, address: data.address, - tableId: config.table!._id, - type: "row", - _id: expect.any(String), - _rev: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), + tableId: table!._id, + _id: newRow._id, + _rev: newRow._rev, + id: newRow.id, + ...defaultRowFields, }) expect(row.body._viewId).toBeUndefined() expect(row.body.age).toBeUndefined() @@ -886,10 +956,9 @@ describe("/rows", () => { describe("patch", () => { it("should update only the view fields for a row", async () => { - const table = await config.createTable(userTable()) + const table = await config.createTable(await userTable()) const tableId = table._id! - const view = await config.api.viewV2.create({ - tableId, + const view = await config.createView({ schema: { name: { visible: true }, address: { visible: true }, @@ -913,13 +982,12 @@ describe("/rows", () => { const row = await config.api.row.get(tableId, newRow._id!) expect(row.body).toEqual({ ...newRow, - type: "row", name: newData.name, address: newData.address, - _id: expect.any(String), + _id: newRow._id, _rev: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), + id: newRow.id, + ...defaultRowFields, }) expect(row.body._viewId).toBeUndefined() expect(row.body.age).toBeUndefined() @@ -929,10 +997,9 @@ describe("/rows", () => { describe("destroy", () => { it("should be able to delete a row", async () => { - const table = await config.createTable(userTable()) + const table = await config.createTable(await userTable()) const tableId = table._id! - const view = await config.api.viewV2.create({ - tableId, + const view = await config.createView({ schema: { name: { visible: true }, address: { visible: true }, @@ -954,21 +1021,20 @@ describe("/rows", () => { }) it("should be able to delete multiple rows", async () => { - const table = await config.createTable(userTable()) + const table = await config.createTable(await userTable()) const tableId = table._id! - const view = await config.api.viewV2.create({ - tableId, + const view = await config.createView({ schema: { name: { visible: true }, address: { visible: true }, }, }) - const rows = [ - await config.createRow(), - await config.createRow(), - await config.createRow(), - ] + const rows = await Promise.all([ + config.createRow(), + config.createRow(), + config.createRow(), + ]) const rowUsage = await getRowUsage() const queryUsage = await getQueryUsage() @@ -989,11 +1055,20 @@ describe("/rows", () => { describe("view search", () => { const viewSchema = { age: { visible: true }, name: { visible: true } } - function userTable(): Table { + async function userTable(): Promise
{ return { - name: "user", - type: "user", + name: `users_${generator.word()}`, + type: "table", + primary: ["id"], schema: { + id: { + type: FieldType.AUTO, + name: "id", + autocolumn: true, + constraints: { + presence: true, + }, + }, name: { type: FieldType.STRING, name: "name", @@ -1008,42 +1083,61 @@ describe("/rows", () => { } } - it("returns table rows from view", async () => { - const table = await config.createTable(userTable()) - const rows = [] - for (let i = 0; i < 10; i++) { - rows.push(await config.createRow({ tableId: table._id })) - } + it("returns empty rows from view when no schema is passed", async () => { + const table = await config.createTable(await userTable()) + const rows = await Promise.all( + Array.from({ length: 10 }, () => + config.api.row.save(table._id!, { tableId: table._id }) + ) + ) - const createViewResponse = await config.api.viewV2.create() + const createViewResponse = await config.createView() const response = await config.api.viewV2.search(createViewResponse.id) expect(response.body.rows).toHaveLength(10) expect(response.body).toEqual({ - rows: expect.arrayContaining(rows.map(expect.objectContaining)), + rows: expect.arrayContaining( + rows.map(r => ({ + _viewId: createViewResponse.id, + tableId: table._id, + _id: r._id, + _rev: r._rev, + ...defaultRowFields, + })) + ), + ...(isInternal + ? {} + : { + hasNextPage: false, + bookmark: null, + }), }) }) it("searching respects the view filters", async () => { - const table = await config.createTable(userTable()) - const expectedRows = [] - for (let i = 0; i < 10; i++) - await config.createRow({ - tableId: table._id, - name: generator.name(), - age: generator.integer({ min: 10, max: 30 }), - }) + const table = await config.createTable(await userTable()) - for (let i = 0; i < 5; i++) - expectedRows.push( - await config.createRow({ + await Promise.all( + Array.from({ length: 10 }, () => + config.api.row.save(table._id!, { + tableId: table._id, + name: generator.name(), + age: generator.integer({ min: 10, max: 30 }), + }) + ) + ) + + const expectedRows = await Promise.all( + Array.from({ length: 5 }, () => + config.api.row.save(table._id!, { tableId: table._id, name: generator.name(), age: 40, }) ) + ) - const createViewResponse = await config.api.viewV2.create({ + const createViewResponse = await config.createView({ query: [{ operator: "equal", field: "age", value: 40 }], schema: viewSchema, }) @@ -1053,8 +1147,22 @@ describe("/rows", () => { expect(response.body.rows).toHaveLength(5) expect(response.body).toEqual({ rows: expect.arrayContaining( - expectedRows.map(expect.objectContaining) + expectedRows.map(r => ({ + _viewId: createViewResponse.id, + tableId: table._id, + name: r.name, + age: r.age, + _id: r._id, + _rev: r._rev, + ...defaultRowFields, + })) ), + ...(isInternal + ? {} + : { + hasNextPage: false, + bookmark: null, + }), }) }) @@ -1127,94 +1235,87 @@ describe("/rows", () => { ], ] - it.each(sortTestOptions)( - "allow sorting (%s)", - async (sortParams, expected) => { - await config.createTable(userTable()) + describe("sorting", () => { + beforeAll(async () => { + const table = await config.createTable(await userTable()) const users = [ { name: "Alice", age: 25 }, { name: "Bob", age: 30 }, { name: "Charly", age: 27 }, { name: "Danny", age: 15 }, ] - for (const user of users) { - await config.createRow({ - tableId: config.table!._id, - ...user, - }) - } - - const createViewResponse = await config.api.viewV2.create({ - sort: sortParams, - schema: viewSchema, - }) - - const response = await config.api.viewV2.search(createViewResponse.id) - - expect(response.body.rows).toHaveLength(4) - expect(response.body).toEqual({ - rows: expected.map(name => expect.objectContaining({ name })), - }) - } - ) - - it.each(sortTestOptions)( - "allow override the default view sorting (%s)", - async (sortParams, expected) => { - await config.createTable(userTable()) - const users = [ - { name: "Alice", age: 25 }, - { name: "Bob", age: 30 }, - { name: "Charly", age: 27 }, - { name: "Danny", age: 15 }, - ] - for (const user of users) { - await config.createRow({ - tableId: config.table!._id, - ...user, - }) - } - - const createViewResponse = await config.api.viewV2.create({ - sort: { - field: "name", - order: SortOrder.ASCENDING, - type: SortType.STRING, - }, - schema: viewSchema, - }) - - const response = await config.api.viewV2.search( - createViewResponse.id, - { - sort: sortParams.field, - sortOrder: sortParams.order, - sortType: sortParams.type, - query: {}, - } + await Promise.all( + users.map(u => + config.api.row.save(table._id!, { + tableId: table._id, + ...u, + }) + ) ) + }) - expect(response.body.rows).toHaveLength(4) - expect(response.body).toEqual({ - rows: expected.map(name => expect.objectContaining({ name })), - }) - } - ) + it.each(sortTestOptions)( + "allow sorting (%s)", + async (sortParams, expected) => { + const createViewResponse = await config.createView({ + sort: sortParams, + schema: viewSchema, + }) + + const response = await config.api.viewV2.search( + createViewResponse.id + ) + + expect(response.body.rows).toHaveLength(4) + expect(response.body.rows).toEqual( + expected.map(name => expect.objectContaining({ name })) + ) + } + ) + + it.each(sortTestOptions)( + "allow override the default view sorting (%s)", + async (sortParams, expected) => { + const createViewResponse = await config.createView({ + sort: { + field: "name", + order: SortOrder.ASCENDING, + type: SortType.STRING, + }, + schema: viewSchema, + }) + + const response = await config.api.viewV2.search( + createViewResponse.id, + { + sort: sortParams.field, + sortOrder: sortParams.order, + sortType: sortParams.type, + query: {}, + } + ) + + expect(response.body.rows).toHaveLength(4) + expect(response.body.rows).toEqual( + expected.map(name => expect.objectContaining({ name })) + ) + } + ) + }) it("when schema is defined, defined columns and row attributes are returned", async () => { - const table = await config.createTable(userTable()) - const rows = [] - for (let i = 0; i < 10; i++) { - rows.push( - await config.createRow({ + const table = await config.createTable(await userTable()) + const rows = await Promise.all( + Array.from({ length: 10 }, () => + config.api.row.save(table._id!, { tableId: table._id, name: generator.name(), age: generator.age(), }) ) - } + ) - const view = await config.api.viewV2.create({ + const view = await config.createView({ schema: { name: { visible: true } }, }) const response = await config.api.viewV2.search(view.id) @@ -1223,7 +1324,9 @@ describe("/rows", () => { expect(response.body.rows).toEqual( expect.arrayContaining( rows.map(r => ({ - ...expectAnyInternalColsAttributes, + ...(isInternal + ? expectAnyInternalColsAttributes + : expectAnyExternalColsAttributes), _viewId: view.id, name: r.name, })) @@ -1232,23 +1335,21 @@ describe("/rows", () => { }) it("views without data can be returned", async () => { - await config.createTable(userTable()) + const table = await config.createTable(await userTable()) - const createViewResponse = await config.api.viewV2.create() + const createViewResponse = await config.createView() const response = await config.api.viewV2.search(createViewResponse.id) expect(response.body.rows).toHaveLength(0) }) it("respects the limit parameter", async () => { - const table = await config.createTable(userTable()) - const rows = [] - for (let i = 0; i < 10; i++) { - rows.push(await config.createRow({ tableId: table._id })) - } + await config.createTable(await userTable()) + await Promise.all(Array.from({ length: 10 }, () => config.createRow())) + const limit = generator.integer({ min: 1, max: 8 }) - const createViewResponse = await config.api.viewV2.create() + const createViewResponse = await config.createView() const response = await config.api.viewV2.search(createViewResponse.id, { limit, query: {}, @@ -1258,14 +1359,10 @@ describe("/rows", () => { }) it("can handle pagination", async () => { - const table = await config.createTable(userTable()) - const rows = [] - for (let i = 0; i < 10; i++) { - rows.push(await config.createRow({ tableId: table._id })) - } - // rows.sort((a, b) => (a._id! > b._id! ? 1 : -1)) + await config.createTable(await userTable()) + await Promise.all(Array.from({ length: 10 }, () => config.createRow())) - const createViewResponse = await config.api.viewV2.create() + const createViewResponse = await config.createView() const allRows = (await config.api.viewV2.search(createViewResponse.id)) .body.rows @@ -1279,9 +1376,9 @@ describe("/rows", () => { ) expect(firstPageResponse.body).toEqual({ rows: expect.arrayContaining(allRows.slice(0, 4)), - totalRows: 10, + totalRows: isInternal ? 10 : undefined, hasNextPage: true, - bookmark: expect.any(String), + bookmark: expect.anything(), }) const secondPageResponse = await config.api.viewV2.search( @@ -1296,9 +1393,9 @@ describe("/rows", () => { ) expect(secondPageResponse.body).toEqual({ rows: expect.arrayContaining(allRows.slice(4, 8)), - totalRows: 10, + totalRows: isInternal ? 10 : undefined, hasNextPage: true, - bookmark: expect.any(String), + bookmark: expect.anything(), }) const lastPageResponse = await config.api.viewV2.search( @@ -1312,9 +1409,9 @@ describe("/rows", () => { ) expect(lastPageResponse.body).toEqual({ rows: expect.arrayContaining(allRows.slice(8)), - totalRows: 10, + totalRows: isInternal ? 10 : undefined, hasNextPage: false, - bookmark: expect.any(String), + bookmark: expect.anything(), }) }) @@ -1323,13 +1420,12 @@ describe("/rows", () => { let tableId: string beforeAll(async () => { - const table = await config.createTable(userTable()) - const rows = [] - for (let i = 0; i < 10; i++) { - rows.push(await config.createRow({ tableId: table._id })) - } + await config.createTable(await userTable()) + await Promise.all( + Array.from({ length: 10 }, () => config.createRow()) + ) - const createViewResponse = await config.api.viewV2.create() + const createViewResponse = await config.createView() tableId = table._id! viewId = createViewResponse.id diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts index 9914e6d66f..f56c6e4e44 100644 --- a/packages/server/src/api/routes/tests/table.spec.ts +++ b/packages/server/src/api/routes/tests/table.spec.ts @@ -1,6 +1,6 @@ import { generator } from "@budibase/backend-core/tests" import { events, context } from "@budibase/backend-core" -import { FieldType, Table } from "@budibase/types" +import { FieldType, Table, ViewCalculation } from "@budibase/types" import { checkBuilderEndpoint } from "./utilities/TestFunctions" import * as setup from "./utilities" const { basicTable } = setup.structures @@ -90,8 +90,10 @@ describe("/tables", () => { await config.createLegacyView({ name: "TestView", field: "Price", - calculation: "stats", - tableId: testTable._id, + calculation: ViewCalculation.STATISTICS, + tableId: testTable._id!, + schema: {}, + filters: [], }) const testRow = await request @@ -254,7 +256,7 @@ describe("/tables", () => { })) await config.api.viewV2.create({ tableId }) - await config.createLegacyView({ tableId, name: generator.guid() }) + await config.createLegacyView() const res = await config.api.table.fetch() @@ -366,7 +368,7 @@ describe("/tables", () => { .expect("Content-Type", /json/) .expect(200) expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`) - const dependentTable = await config.getTable(linkedTable._id) + const dependentTable = await config.api.table.get(linkedTable._id!) expect(dependentTable.schema.TestTable).not.toBeDefined() }) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 5e8ae09e55..6d893c1c7f 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -6,6 +6,7 @@ import { SortOrder, SortType, Table, + UIFieldMetadata, UpdateViewRequest, ViewV2, } from "@budibase/types" @@ -418,9 +419,12 @@ describe.each([ const res = await config.api.viewV2.create(newView) const view = await config.api.viewV2.get(res.id) expect(view!.schema?.Price).toBeUndefined() - const updatedTable = await config.getTable(table._id!) - const viewSchema = updatedTable.views[view!.name!].schema - expect(viewSchema.Price.visible).toEqual(false) + const updatedTable = await config.api.table.get(table._id!) + const viewSchema = updatedTable.views![view!.name!].schema as Record< + string, + UIFieldMetadata + > + expect(viewSchema.Price?.visible).toEqual(false) }) }) }) diff --git a/packages/server/src/db/tests/linkController.spec.js b/packages/server/src/db/tests/linkController.spec.js index 1c50142871..59d0f3f983 100644 --- a/packages/server/src/db/tests/linkController.spec.js +++ b/packages/server/src/db/tests/linkController.spec.js @@ -6,7 +6,7 @@ const { RelationshipType } = require("../../constants") const { cloneDeep } = require("lodash/fp") describe("test the link controller", () => { - let config = new TestConfig(false) + let config = new TestConfig() let table1, table2, appId beforeAll(async () => { diff --git a/packages/server/src/db/tests/linkTests.spec.js b/packages/server/src/db/tests/linkTests.spec.js index b7764dacb7..19a0eb88d3 100644 --- a/packages/server/src/db/tests/linkTests.spec.js +++ b/packages/server/src/db/tests/linkTests.spec.js @@ -4,7 +4,7 @@ const linkUtils = require("../linkedRows/linkUtils") const { context } = require("@budibase/backend-core") describe("test link functionality", () => { - const config = new TestConfig(false) + const config = new TestConfig() let appId describe("getLinkedTable", () => { diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index 6b0e7b4266..86a43c8c42 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -12,18 +12,15 @@ import { FieldType, RelationshipType, Row, - SourceName, Table, } from "@budibase/types" import _ from "lodash" import { generator } from "@budibase/backend-core/tests" import { utils } from "@budibase/backend-core" -import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" +import { databaseTestProviders } from "../integrations/tests/utils" const config = setup.getConfig()! -jest.setTimeout(30000) - jest.unmock("pg") jest.mock("../websockets") @@ -35,62 +32,18 @@ describe("postgres integrations", () => { manyToOneRelationshipInfo: ForeignTableInfo, manyToManyRelationshipInfo: ForeignTableInfo - let host: string - let port: number - const containers: StartedTestContainer[] = [] - beforeAll(async () => { - const containerPostgres = await new GenericContainer("postgres") - .withExposedPorts(5432) - .withEnv("POSTGRES_PASSWORD", "password") - .withWaitStrategy( - Wait.forLogMessage( - "PostgreSQL init process complete; ready for start up." - ) - ) - .start() - - host = containerPostgres.getContainerIpAddress() - port = containerPostgres.getMappedPort(5432) - await config.init() const apiKey = await config.generateApiKey() - containers.push(containerPostgres) - makeRequest = generateMakeRequest(apiKey, true) - }) - afterAll(async () => { - for (let container of containers) { - await container.stop() - } + postgresDatasource = await config.api.datasource.create( + await databaseTestProviders.postgres.getDsConfig() + ) }) - function pgDatasourceConfig() { - return { - datasource: { - type: "datasource", - source: SourceName.POSTGRES, - plus: true, - config: { - host, - port, - database: "postgres", - user: "postgres", - password: "password", - schema: "public", - ssl: false, - rejectUnauthorized: false, - ca: false, - }, - }, - } - } - beforeEach(async () => { - postgresDatasource = await config.createDatasource(pgDatasourceConfig()) - async function createAuxTable(prefix: string) { return await config.createTable({ name: `${prefix}_${generator.word({ length: 6 })}`, @@ -226,25 +179,6 @@ describe("postgres integrations", () => { let { rowData } = opts as any let foreignRows: ForeignRowsInfo[] = [] - async function createForeignRow(tableInfo: ForeignTableInfo) { - const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}` - - const foreignRow = await config.createRow({ - tableId: tableInfo.table._id, - title: generator.name(), - }) - - rowData = { - ...rowData, - [foreignKey]: foreignRow.id, - } - foreignRows.push({ - row: foreignRow, - - relationshipType: tableInfo.relationshipType, - }) - } - if (opts?.createForeignRows?.createOneToMany) { const foreignKey = `fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}` @@ -322,6 +256,14 @@ describe("postgres integrations", () => { }) } + const createRandomTableWithRows = async () => { + const tableId = (await createDefaultPgTable())._id! + return await config.api.row.save(tableId, { + tableId, + title: generator.name(), + }) + } + async function populatePrimaryRows( count: number, opts?: { @@ -357,9 +299,9 @@ describe("postgres integrations", () => { config: { ca: false, database: "postgres", - host, + host: postgresDatasource.config!.host, password: "--secret-value--", - port, + port: postgresDatasource.config!.port, rejectUnauthorized: false, schema: "public", ssl: false, @@ -401,12 +343,16 @@ describe("postgres integrations", () => { it("multiple rows can be persisted", async () => { const numberOfRows = 10 - const newRows = Array(numberOfRows).fill(generateRandomPrimaryRowData()) + const newRows: Row[] = Array(numberOfRows).fill( + generateRandomPrimaryRowData() + ) - for (const newRow of newRows) { - const res = await createRow(primaryPostgresTable._id, newRow) - expect(res.status).toBe(200) - } + await Promise.all( + newRows.map(async newRow => { + const res = await createRow(primaryPostgresTable._id, newRow) + expect(res.status).toBe(200) + }) + ) const persistedRows = await config.getRows(primaryPostgresTable._id!) expect(persistedRows).toHaveLength(numberOfRows) @@ -567,7 +513,7 @@ describe("postgres integrations", () => { foreignRows = createdRow.foreignRows }) - it("only one to many foreign keys are retrieved", async () => { + it("only one to primary keys are retrieved", async () => { const res = await getRow(primaryPostgresTable._id, row.id) expect(res.status).toBe(200) @@ -575,6 +521,12 @@ describe("postgres integrations", () => { const one2ManyForeignRows = foreignRows.filter( x => x.relationshipType === RelationshipType.ONE_TO_MANY ) + const many2OneForeignRows = foreignRows.filter( + x => x.relationshipType === RelationshipType.MANY_TO_ONE + ) + const many2ManyForeignRows = foreignRows.filter( + x => x.relationshipType === RelationshipType.MANY_TO_MANY + ) expect(one2ManyForeignRows).toHaveLength(1) expect(res.body).toEqual({ @@ -585,9 +537,25 @@ describe("postgres integrations", () => { _rev: expect.any(String), [`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]: one2ManyForeignRows[0].row.id, + [oneToManyRelationshipInfo.fieldName]: expect.arrayContaining( + one2ManyForeignRows.map(r => ({ + _id: r.row._id, + primaryDisplay: r.row.title, + })) + ), + [manyToOneRelationshipInfo.fieldName]: expect.arrayContaining( + many2OneForeignRows.map(r => ({ + _id: r.row._id, + primaryDisplay: r.row.title, + })) + ), + [manyToManyRelationshipInfo.fieldName]: expect.arrayContaining( + many2ManyForeignRows.map(r => ({ + _id: r.row._id, + primaryDisplay: r.row.title, + })) + ), }) - - expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() }) }) @@ -616,9 +584,13 @@ describe("postgres integrations", () => { _rev: expect.any(String), [`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]: foreignRows[0].row.id, + [oneToManyRelationshipInfo.fieldName]: expect.arrayContaining( + foreignRows.map(r => ({ + _id: r.row._id, + primaryDisplay: r.row.title, + })) + ), }) - - expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() }) }) @@ -645,9 +617,13 @@ describe("postgres integrations", () => { tableId: row.tableId, _id: expect.any(String), _rev: expect.any(String), + [manyToOneRelationshipInfo.fieldName]: expect.arrayContaining( + foreignRows.map(r => ({ + _id: r.row._id, + primaryDisplay: r.row.title, + })) + ), }) - - expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() }) }) @@ -674,9 +650,13 @@ describe("postgres integrations", () => { tableId: row.tableId, _id: expect.any(String), _rev: expect.any(String), + [manyToManyRelationshipInfo.fieldName]: expect.arrayContaining( + foreignRows.map(r => ({ + _id: r.row._id, + primaryDisplay: r.row.title, + })) + ), }) - - expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined() }) }) }) @@ -730,12 +710,6 @@ describe("postgres integrations", () => { describe("given than multiple tables have multiple rows", () => { const rowsCount = 6 beforeEach(async () => { - const createRandomTableWithRows = async () => - await config.createRow({ - tableId: (await createDefaultPgTable())._id, - title: generator.name(), - }) - await createRandomTableWithRows() await createRandomTableWithRows() @@ -1023,12 +997,6 @@ describe("postgres integrations", () => { const rowsCount = 6 beforeEach(async () => { - const createRandomTableWithRows = async () => - await config.createRow({ - tableId: (await createDefaultPgTable())._id, - title: generator.name(), - }) - await createRandomTableWithRows() await populatePrimaryRows(rowsCount) await createRandomTableWithRows() @@ -1046,24 +1014,25 @@ describe("postgres integrations", () => { describe("POST /api/datasources/verify", () => { it("should be able to verify the connection", async () => { - const config = pgDatasourceConfig() - const response = await makeRequest( - "post", - "/api/datasources/verify", - config - ) + const response = await config.api.datasource.verify({ + datasource: await databaseTestProviders.postgres.getDsConfig(), + }) expect(response.status).toBe(200) expect(response.body.connected).toBe(true) }) it("should state an invalid datasource cannot connect", async () => { - const config = pgDatasourceConfig() - config.datasource.config.password = "wrongpassword" - const response = await makeRequest( - "post", - "/api/datasources/verify", - config - ) + const dbConfig = await databaseTestProviders.postgres.getDsConfig() + const response = await config.api.datasource.verify({ + datasource: { + ...dbConfig, + config: { + ...dbConfig.config, + password: "wrongpassword", + }, + }, + }) + expect(response.status).toBe(200) expect(response.body.connected).toBe(false) expect(response.body.error).toBeDefined() diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts new file mode 100644 index 0000000000..a28141db08 --- /dev/null +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -0,0 +1,14 @@ +jest.unmock("pg") + +import { Datasource } from "@budibase/types" +import * as pg from "./postgres" + +jest.setTimeout(30000) + +export interface DatabasePlusTestProvider { + getDsConfig(): Promise +} + +export const databaseTestProviders = { + postgres: pg, +} diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts new file mode 100644 index 0000000000..036e81bbd8 --- /dev/null +++ b/packages/server/src/integrations/tests/utils/postgres.ts @@ -0,0 +1,38 @@ +import { Datasource, SourceName } from "@budibase/types" +import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" + +let container: StartedTestContainer | undefined + +export async function getDsConfig(): Promise { + if (!container) { + container = await new GenericContainer("postgres") + .withExposedPorts(5432) + .withEnv("POSTGRES_PASSWORD", "password") + .withWaitStrategy( + Wait.forLogMessage( + "PostgreSQL init process complete; ready for start up." + ) + ) + .start() + } + + const host = container.getContainerIpAddress() + const port = container.getMappedPort(5432) + + return { + type: "datasource_plus", + source: SourceName.POSTGRES, + plus: true, + config: { + host, + port, + database: "postgres", + user: "postgres", + password: "password", + schema: "public", + ssl: false, + rejectUnauthorized: false, + ca: false, + }, + } +} diff --git a/packages/server/src/migrations/tests/index.spec.ts b/packages/server/src/migrations/tests/index.spec.ts index b64cad26c1..d83e3cf31e 100644 --- a/packages/server/src/migrations/tests/index.spec.ts +++ b/packages/server/src/migrations/tests/index.spec.ts @@ -11,6 +11,7 @@ import { MIGRATIONS } from "../" import * as helpers from "./helpers" import tk from "timekeeper" +import { View } from "@budibase/types" const timestamp = new Date().toISOString() tk.freeze(timestamp) @@ -52,7 +53,9 @@ describe("migrations", () => { await config.createTable() await config.createLegacyView() await config.createTable() - await config.createLegacyView(structures.view(config.table!._id!)) + await config.createLegacyView( + structures.view(config.table!._id!) as View + ) await config.createScreen() await config.createScreen() diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index 4861f473ea..380521a05a 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -30,7 +30,7 @@ export interface ExportRowsParams { format: Format rowIds?: string[] columns?: string[] - query: SearchFilters + query?: SearchFilters } export interface ExportRowsResult { diff --git a/packages/server/src/sdk/app/tables/index.ts b/packages/server/src/sdk/app/tables/index.ts index 1bf9117837..64fcde4bff 100644 --- a/packages/server/src/sdk/app/tables/index.ts +++ b/packages/server/src/sdk/app/tables/index.ts @@ -12,7 +12,7 @@ import { TableViewsResponse, } from "@budibase/types" import datasources from "../datasources" -import { isEditableColumn, populateExternalTableSchemas } from "./validation" +import { populateExternalTableSchemas } from "./validation" import sdk from "../../../sdk" async function getAllInternalTables(db?: Database): Promise { @@ -73,12 +73,23 @@ function enrichViewSchemas(table: Table): TableResponse { } } +async function saveTable(table: Table) { + const db = context.getAppDB() + if (isExternalTable(table._id!)) { + const datasource = await sdk.datasources.get(table.sourceId!) + datasource.entities![table.name] = table + await db.put(datasource) + } else { + await db.put(table) + } +} + export default { getAllInternalTables, getAllExternalTables, getExternalTable, getTable, populateExternalTableSchemas, - isEditableColumn, enrichViewSchemas, + saveTable, } diff --git a/packages/server/src/sdk/app/tables/validation.ts b/packages/server/src/sdk/app/tables/validation.ts index 8dc41107d3..56f3e84c7a 100644 --- a/packages/server/src/sdk/app/tables/validation.ts +++ b/packages/server/src/sdk/app/tables/validation.ts @@ -55,13 +55,6 @@ function checkForeignKeysAreAutoColumns(datasource: Datasource) { return datasource } -export function isEditableColumn(column: FieldSchema) { - const isAutoColumn = - column.autocolumn && column.autoReason !== AutoReason.FOREIGN_KEY - const isFormula = column.type === FieldTypes.FORMULA - return !(isAutoColumn || isFormula) -} - export function populateExternalTableSchemas(datasource: Datasource) { return checkForeignKeysAreAutoColumns(datasource) } diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 4c6d0701f3..da7af8acd7 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -50,6 +50,11 @@ import { SearchFilters, UserRoles, Automation, + View, + FieldType, + RelationshipType, + ViewV2, + CreateViewRequest, } from "@budibase/types" import API from "./api" @@ -75,9 +80,8 @@ class TestConfiguration { globalUserId: any userMetadataId: any table?: Table - linkedTable: any automation: any - datasource: any + datasource?: Datasource tenantId?: string defaultUserValues: DefaultUserValues api: API @@ -527,7 +531,7 @@ class TestConfiguration { // TABLE async updateTable( - config?: any, + config?: Table, { skipReassigning } = { skipReassigning: false } ): Promise
{ config = config || basicTable() @@ -542,33 +546,50 @@ class TestConfiguration { if (config != null && config._id) { delete config._id } + config = config || basicTable() + if (this.datasource && !config.sourceId) { + config.sourceId = this.datasource._id + if (this.datasource.plus) { + config.type = "external" + } + } + return this.updateTable(config, options) } async getTable(tableId?: string) { - tableId = tableId || this.table?._id + tableId = tableId || this.table!._id! return this._req(null, { tableId }, controllers.table.find) } - async createLinkedTable(relationshipType?: string, links: any = ["link"]) { + async createLinkedTable( + relationshipType = RelationshipType.ONE_TO_MANY, + links: any = ["link"], + config?: Table + ) { if (!this.table) { throw "Must have created a table first." } - const tableConfig: any = basicTable() + const tableConfig = config || basicTable() tableConfig.primaryDisplay = "name" for (let link of links) { tableConfig.schema[link] = { - type: "link", + type: FieldType.LINK, fieldName: link, tableId: this.table._id, name: link, - } - if (relationshipType) { - tableConfig.schema[link].relationshipType = relationshipType + relationshipType, } } + + if (this.datasource && !tableConfig.sourceId) { + tableConfig.sourceId = this.datasource._id + if (this.datasource.plus) { + tableConfig.type = "external" + } + } + const linkedTable = await this.createTable(tableConfig) - this.linkedTable = linkedTable return linkedTable } @@ -621,17 +642,36 @@ class TestConfiguration { // VIEW - async createLegacyView(config?: any) { - if (!this.table) { + async createLegacyView(config?: View) { + if (!this.table && !config) { throw "Test requires table to be configured." } const view = config || { - tableId: this.table._id, - name: "ViewTest", + tableId: this.table!._id, + name: generator.guid(), } return this._req(view, null, controllers.view.v1.save) } + async createView( + config?: Omit & { + name?: string + tableId?: string + } + ) { + if (!this.table && !config?.tableId) { + throw "Test requires table to be configured." + } + + const view: CreateViewRequest = { + ...config, + tableId: config?.tableId || this.table!._id!, + name: config?.name || generator.word(), + } + + return await this.api.viewV2.create(view) + } + // AUTOMATION async createAutomation(config?: any) { @@ -677,17 +717,17 @@ class TestConfiguration { config = config || basicDatasource() const response = await this._req(config, null, controllers.datasource.save) this.datasource = response.datasource - return this.datasource + return this.datasource! } - async updateDatasource(datasource: any) { + async updateDatasource(datasource: Datasource): Promise { const response = await this._req( datasource, { datasourceId: datasource._id }, controllers.datasource.update ) this.datasource = response.datasource - return this.datasource + return this.datasource! } async restDatasource(cfg?: any) { @@ -771,7 +811,7 @@ class TestConfiguration { if (!this.datasource && !config) { throw "No datasource created for query." } - config = config || basicQuery(this.datasource._id) + config = config || basicQuery(this.datasource!._id!) return this._req(config, null, controllers.query.save) } diff --git a/packages/server/src/tests/utilities/api/datasource.ts b/packages/server/src/tests/utilities/api/datasource.ts new file mode 100644 index 0000000000..ee698334f2 --- /dev/null +++ b/packages/server/src/tests/utilities/api/datasource.ts @@ -0,0 +1,57 @@ +import { + CreateDatasourceRequest, + Datasource, + VerifyDatasourceRequest, + VerifyDatasourceResponse, +} from "@budibase/types" +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class DatasourceAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + create = async ( + config: Datasource, + { expectStatus } = { expectStatus: 200 } + ): Promise => { + const body: CreateDatasourceRequest = { + datasource: config, + tablesFilter: [], + } + const result = await this.request + .post(`/api/datasources`) + .send(body) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + return result.body.datasource as Datasource + } + + update = async ( + datasource: Datasource, + { expectStatus } = { expectStatus: 200 } + ): Promise => { + const result = await this.request + .put(`/api/datasources/${datasource._id}`) + .send(datasource) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + return result.body.datasource as Datasource + } + + verify = async ( + data: VerifyDatasourceRequest, + { expectStatus } = { expectStatus: 200 } + ) => { + const result = await this.request + .post(`/api/datasources/verify`) + .send(data) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + return result + } +} diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index 40995b62f2..31c74a0e78 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -3,17 +3,23 @@ import { PermissionAPI } from "./permission" import { RowAPI } from "./row" import { TableAPI } from "./table" import { ViewV2API } from "./viewV2" +import { DatasourceAPI } from "./datasource" +import { LegacyViewAPI } from "./legacyView" export default class API { table: TableAPI + legacyView: LegacyViewAPI viewV2: ViewV2API row: RowAPI permission: PermissionAPI + datasource: DatasourceAPI constructor(config: TestConfiguration) { this.table = new TableAPI(config) + this.legacyView = new LegacyViewAPI(config) this.viewV2 = new ViewV2API(config) this.row = new RowAPI(config) this.permission = new PermissionAPI(config) + this.datasource = new DatasourceAPI(config) } } diff --git a/packages/server/src/tests/utilities/api/legacyView.ts b/packages/server/src/tests/utilities/api/legacyView.ts new file mode 100644 index 0000000000..63981cec5e --- /dev/null +++ b/packages/server/src/tests/utilities/api/legacyView.ts @@ -0,0 +1,16 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class LegacyViewAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + get = async (id: string, { expectStatus } = { expectStatus: 200 }) => { + return await this.request + .get(`/api/views/${id}`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + } +} diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts index c6ef4606d2..686c8c031b 100644 --- a/packages/server/src/tests/utilities/api/row.ts +++ b/packages/server/src/tests/utilities/api/row.ts @@ -1,4 +1,10 @@ -import { PatchRowRequest, SaveRowRequest, Row } from "@budibase/types" +import { + PatchRowRequest, + SaveRowRequest, + Row, + ValidateResponse, + ExportRowsRequest, +} from "@budibase/types" import TestConfiguration from "../TestConfiguration" import { TestAPI } from "./base" @@ -22,6 +28,21 @@ export class RowAPI extends TestAPI { return request } + getEnriched = async ( + sourceId: string, + rowId: string, + { expectStatus } = { expectStatus: 200 } + ) => { + const request = this.request + .get(`/api/${sourceId}/${rowId}/enrich`) + .set(this.config.defaultHeaders()) + .expect(expectStatus) + if (expectStatus !== 404) { + request.expect("Content-Type", /json/) + } + return request + } + save = async ( sourceId: string, row: SaveRowRequest, @@ -36,6 +57,20 @@ export class RowAPI extends TestAPI { return resp.body as Row } + validate = async ( + sourceId: string, + row: SaveRowRequest, + { expectStatus } = { expectStatus: 200 } + ): Promise => { + const resp = await this.request + .post(`/api/${sourceId}/rows/validate`) + .send(row) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + return resp.body as ValidateResponse + } + patch = async ( sourceId: string, row: PatchRowRequest, @@ -51,14 +86,40 @@ export class RowAPI extends TestAPI { delete = async ( sourceId: string, - rows: Row[], + rows: Row | string | (Row | string)[], { expectStatus } = { expectStatus: 200 } ) => { return this.request .delete(`/api/${sourceId}/rows`) - .send({ rows }) + .send(Array.isArray(rows) ? { rows } : rows) .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) .expect(expectStatus) } + + fetch = async ( + sourceId: string, + { expectStatus } = { expectStatus: 200 } + ): Promise => { + const request = this.request + .get(`/api/${sourceId}/rows`) + .set(this.config.defaultHeaders()) + .expect(expectStatus) + + return (await request).body + } + + exportRows = async ( + tableId: string, + body: ExportRowsRequest, + { expectStatus } = { expectStatus: 200 } + ) => { + const request = this.request + .post(`/api/${tableId}/rows/exportRows?format=json`) + .set(this.config.defaultHeaders()) + .send(body) + .expect("Content-Type", /json/) + .expect(expectStatus) + return request + } } diff --git a/packages/server/src/tests/utilities/api/table.ts b/packages/server/src/tests/utilities/api/table.ts index 70f0869650..04432a788a 100644 --- a/packages/server/src/tests/utilities/api/table.ts +++ b/packages/server/src/tests/utilities/api/table.ts @@ -1,4 +1,4 @@ -import { Table } from "@budibase/types" +import { SaveTableRequest, SaveTableResponse, Table } from "@budibase/types" import TestConfiguration from "../TestConfiguration" import { TestAPI } from "./base" @@ -7,6 +7,19 @@ export class TableAPI extends TestAPI { super(config) } + create = async ( + data: SaveTableRequest, + { expectStatus } = { expectStatus: 200 } + ): Promise => { + const res = await this.request + .post(`/api/tables`) + .send(data) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + return res.body + } + fetch = async ( { expectStatus } = { expectStatus: 200 } ): Promise => { diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index 0682361e16..92a6d394bf 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -23,8 +23,8 @@ export class ViewV2API extends TestAPI { if (!tableId && !this.config.table) { throw "Test requires table to be configured." } - const table = this.config.table - tableId = table!._id! + + tableId = tableId || this.config.table!._id! const view = { tableId, name: generator.guid(), diff --git a/packages/types/src/api/web/app/row.ts b/packages/types/src/api/web/app/row.ts index f9623a3daf..4ab4090461 100644 --- a/packages/types/src/api/web/app/row.ts +++ b/packages/types/src/api/web/app/row.ts @@ -1,5 +1,7 @@ import { Row } from "../../../documents/app/row" +export interface GetRowResponse extends Row {} + export interface DeleteRows { rows: (Row | string)[] } @@ -9,3 +11,8 @@ export interface DeleteRow { } export type DeleteRowRequest = DeleteRows | DeleteRow + +export interface ValidateResponse { + valid: boolean + errors: Record +} diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts index a99ef0e837..62ea90a6a4 100644 --- a/packages/types/src/api/web/app/rows.ts +++ b/packages/types/src/api/web/app/rows.ts @@ -1,5 +1,6 @@ -import { SearchParams } from "../../../sdk" +import { SearchFilters, SearchParams } from "../../../sdk" import { Row } from "../../../documents" +import { ReadStream } from "fs" export interface SaveRowRequest extends Row {} @@ -28,3 +29,11 @@ export interface SearchViewRowRequest export interface SearchRowResponse { rows: any[] } + +export interface ExportRowsRequest { + rows: string[] + columns?: string[] + query?: SearchFilters +} + +export type ExportRowsResponse = ReadStream