diff --git a/.github/workflows/readme-openapi.yml b/.github/workflows/readme-openapi.yml new file mode 100644 index 0000000000..14e9887dd6 --- /dev/null +++ b/.github/workflows/readme-openapi.yml @@ -0,0 +1,28 @@ +name: ReadMe GitHub Action 🦉 + +on: + push: + branches: + - master + +jobs: + rdme-openapi: + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v3 + + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: yarn + - run: yarn --frozen-lockfile + + - name: update specs + run: cd packages/server && yarn specs + + - name: Run `openapi` command + uses: readmeio/rdme@v8 + with: + rdme: openapi specs/openapi.yaml --key=${{ secrets.README_API_KEY }} --id=6728a74f5918b50036c61841 diff --git a/lerna.json b/lerna.json index dc238bb392..fdd10e4bf1 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.12", + "version": "3.2.13", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index c47a14cf21..9d356b9931 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -2166,7 +2166,6 @@ "query": { "description": "Search parameters for view", "type": "object", - "required": [], "properties": { "logicalOperator": { "description": "When using groups this defines whether all of the filters must match, or only one of them.", @@ -2449,7 +2448,6 @@ "query": { "description": "Search parameters for view", "type": "object", - "required": [], "properties": { "logicalOperator": { "description": "When using groups this defines whether all of the filters must match, or only one of them.", @@ -2743,7 +2741,6 @@ "query": { "description": "Search parameters for view", "type": "object", - "required": [], "properties": { "logicalOperator": { "description": "When using groups this defines whether all of the filters must match, or only one of them.", diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index edfb29f432..9ad242f24c 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -1802,7 +1802,6 @@ components: query: description: Search parameters for view type: object - required: [] properties: logicalOperator: description: When using groups this defines whether all of the filters must @@ -2012,7 +2011,6 @@ components: query: description: Search parameters for view type: object - required: [] properties: logicalOperator: description: When using groups this defines whether all of the filters must @@ -2229,7 +2227,6 @@ components: query: description: Search parameters for view type: object - required: [] properties: logicalOperator: description: When using groups this defines whether all of the filters must diff --git a/packages/server/specs/resources/view.ts b/packages/server/specs/resources/view.ts index aeb2b97aa9..5becd67a37 100644 --- a/packages/server/specs/resources/view.ts +++ b/packages/server/specs/resources/view.ts @@ -142,7 +142,6 @@ layeredFilterGroup.items.properties.groups = filterGroup const viewQuerySchema = { description: "Search parameters for view", type: "object", - required: [], properties: { logicalOperator, onEmptyFilter: { diff --git a/packages/server/src/automations/tests/createRow.spec.ts b/packages/server/src/automations/tests/createRow.spec.ts index bcf9845669..bd78de2217 100644 --- a/packages/server/src/automations/tests/createRow.spec.ts +++ b/packages/server/src/automations/tests/createRow.spec.ts @@ -1,6 +1,7 @@ import * as setup from "./utilities" import { basicTableWithAttachmentField } from "../../tests/utilities/structures" import { objectStore } from "@budibase/backend-core" +import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" async function uploadTestFile(filename: string) { let bucket = "testbucket" @@ -13,6 +14,7 @@ async function uploadTestFile(filename: string) { return presignedUrl } + describe("test the create row action", () => { let table: any let row: any @@ -31,30 +33,78 @@ describe("test the create row action", () => { afterAll(setup.afterAll) it("should be able to run the action", async () => { - const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { - row, + const result = await createAutomationBuilder({ + name: "Test Create Row Flow", + appId: config.getAppId(), + config, }) - expect(res.id).toBeDefined() - expect(res.revision).toBeDefined() - expect(res.success).toEqual(true) - const gottenRow = await config.api.row.get(table._id, res.id) + .appAction({ fields: { status: "new" } }) + .serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" }) + .createRow({ row }, { stepName: "CreateRow" }) + .serverLog( + { text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" }, + { stepName: "CreationLog" } + ) + .run() + + expect(result.steps[1].outputs.success).toBeDefined() + expect(result.steps[1].outputs.id).toBeDefined() + expect(result.steps[1].outputs.revision).toBeDefined() + const gottenRow = await config.api.row.get( + table._id, + result.steps[1].outputs.id + ) expect(gottenRow.name).toEqual("test") expect(gottenRow.description).toEqual("test") + expect(result.steps[2].outputs.message).toContain( + "Row created with ID: " + result.steps[1].outputs.id + ) }) it("should return an error (not throw) when bad info provided", async () => { - const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { - row: { - tableId: "invalid", - invalid: "invalid", - }, + const result = await createAutomationBuilder({ + name: "Test Create Row Error Flow", + appId: config.getAppId(), + config, }) - expect(res.success).toEqual(false) + .appAction({ fields: { status: "error" } }) + .serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" }) + .createRow( + { + row: { + tableId: "invalid", + invalid: "invalid", + }, + }, + { stepName: "CreateRow" } + ) + .run() + + expect(result.steps[1].outputs.success).toEqual(false) }) it("should check invalid inputs return an error", async () => { - const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {}) - expect(res.success).toEqual(false) + const result = await createAutomationBuilder({ + name: "Test Create Row Invalid Flow", + appId: config.getAppId(), + config, + }) + .appAction({ fields: { status: "invalid" } }) + .serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" }) + .createRow({ row: {} }, { stepName: "CreateRow" }) + .filter({ + field: "{{ stepsByName.CreateRow.success }}", + condition: "equal", + value: true, + }) + .serverLog( + { text: "This log should not appear" }, + { stepName: "SkippedLog" } + ) + .run() + + expect(result.steps[1].outputs.success).toEqual(false) + expect(result.steps.length).toBeLessThan(4) }) it("should check that an attachment field is sent to storage and parsed", async () => { @@ -76,13 +126,33 @@ describe("test the create row action", () => { ] attachmentRow.file_attachment = attachmentObject - const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { - row: attachmentRow, + const result = await createAutomationBuilder({ + name: "Test Create Row Attachment Flow", + appId: config.getAppId(), + config, }) + .appAction({ fields: { type: "attachment" } }) + .serverLog( + { text: "Processing attachment upload" }, + { stepName: "StartLog" } + ) + .createRow({ row: attachmentRow }, { stepName: "CreateRow" }) + .filter({ + field: "{{ stepsByName.CreateRow.success }}", + condition: "equal", + value: true, + }) + .serverLog( + { + text: "Attachment uploaded with key: {{ stepsByName.CreateRow.row.file_attachment.0.key }}", + }, + { stepName: "UploadLog" } + ) + .run() - expect(res.success).toEqual(true) - expect(res.row.file_attachment[0]).toHaveProperty("key") - let s3Key = res.row.file_attachment[0].key + expect(result.steps[1].outputs.success).toEqual(true) + expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key") + let s3Key = result.steps[1].outputs.row.file_attachment[0].key const client = objectStore.ObjectStore(objectStore.ObjectStoreBuckets.APPS) @@ -111,13 +181,53 @@ describe("test the create row action", () => { } attachmentRow.single_file_attachment = attachmentObject - const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { - row: attachmentRow, + const result = await createAutomationBuilder({ + name: "Test Create Row Single Attachment Flow", + appId: config.getAppId(), + config, }) + .appAction({ fields: { type: "single-attachment" } }) + .serverLog( + { text: "Processing single attachment" }, + { stepName: "StartLog" } + ) + .createRow({ row: attachmentRow }, { stepName: "CreateRow" }) + .branch({ + success: { + steps: stepBuilder => + stepBuilder + .serverLog( + { text: "Single attachment processed" }, + { stepName: "ProcessLog" } + ) + .serverLog( + { + text: "File key: {{ stepsByName.CreateRow.row.single_file_attachment.key }}", + }, + { stepName: "KeyLog" } + ), + condition: { + equal: { "{{ stepsByName.CreateRow.success }}": true }, + }, + }, + error: { + steps: stepBuilder => + stepBuilder.serverLog( + { text: "Failed to process attachment" }, + { stepName: "ErrorLog" } + ), + condition: { + equal: { "{{ stepsByName.CreateRow.success }}": false }, + }, + }, + }) + .run() - expect(res.success).toEqual(true) - expect(res.row.single_file_attachment).toHaveProperty("key") - let s3Key = res.row.single_file_attachment.key + expect(result.steps[1].outputs.success).toEqual(true) + expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty( + "key" + ) + let s3Key = result.steps[1].outputs.row.single_file_attachment.key const client = objectStore.ObjectStore(objectStore.ObjectStoreBuckets.APPS) @@ -146,13 +256,50 @@ describe("test the create row action", () => { } attachmentRow.single_file_attachment = attachmentObject - const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { - row: attachmentRow, + const result = await createAutomationBuilder({ + name: "Test Create Row Invalid Attachment Flow", + appId: config.getAppId(), + config, }) + .appAction({ fields: { type: "invalid-attachment" } }) + .serverLog( + { text: "Testing invalid attachment keys" }, + { stepName: "StartLog" } + ) + .createRow({ row: attachmentRow }, { stepName: "CreateRow" }) + .branch({ + success: { + steps: stepBuilder => + stepBuilder.serverLog( + { text: "Unexpected success" }, + { stepName: "UnexpectedLog" } + ), + condition: { + equal: { "{{ stepsByName.CreateRow.success }}": true }, + }, + }, + error: { + steps: stepBuilder => + stepBuilder + .serverLog( + { text: "Expected error occurred" }, + { stepName: "ErrorLog" } + ) + .serverLog( + { text: "Error: {{ stepsByName.CreateRow.response }}" }, + { stepName: "ErrorDetailsLog" } + ), + condition: { + equal: { "{{ stepsByName.CreateRow.success }}": false }, + }, + }, + }) + .run() - expect(res.success).toEqual(false) - expect(res.response).toEqual( + expect(result.steps[1].outputs.success).toEqual(false) + expect(result.steps[1].outputs.response).toEqual( 'Error: Attachments must have both "url" and "filename" keys. You have provided: wrongKey, anotherWrongKey' ) + expect(result.steps[2].outputs.status).toEqual("No branch condition met") }) }) diff --git a/packages/server/src/automations/tests/deleteRow.spec.ts b/packages/server/src/automations/tests/deleteRow.spec.ts index dd13aa49c1..cabf590421 100644 --- a/packages/server/src/automations/tests/deleteRow.spec.ts +++ b/packages/server/src/automations/tests/deleteRow.spec.ts @@ -1,52 +1,65 @@ +import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import * as setup from "./utilities" describe("test the delete row action", () => { - let table: any - let row: any - let inputs: any - let config = setup.getConfig() + let table: any, + row: any, + config = setup.getConfig() - beforeEach(async () => { + beforeAll(async () => { await config.init() table = await config.createTable() row = await config.createRow() - inputs = { - tableId: table._id, - id: row._id, - revision: row._rev, - } }) afterAll(setup.afterAll) - it("should be able to run the action", async () => { - const res = await setup.runStep( - config, - setup.actions.DELETE_ROW.stepId, - inputs - ) - expect(res.success).toEqual(true) - expect(res.response).toBeDefined() - expect(res.row._id).toEqual(row._id) - }) + it("should be able to run the delete row action", async () => { + const builder = createAutomationBuilder({ + name: "Delete Row Automation", + }) - it("check usage quota attempts", async () => { - await setup.runInProd(async () => { - await setup.runStep(config, setup.actions.DELETE_ROW.stepId, inputs) + await builder + .appAction({ fields: {} }) + .deleteRow({ + tableId: table._id, + id: row._id, + revision: row._rev, + }) + .run() + + await config.api.row.get(table._id, row._id, { + status: 404, }) }) it("should check invalid inputs return an error", async () => { - const res = await setup.runStep(config, setup.actions.DELETE_ROW.stepId, {}) - expect(res.success).toEqual(false) + const builder = createAutomationBuilder({ + name: "Invalid Inputs Automation", + }) + + const results = await builder + .appAction({ fields: {} }) + .deleteRow({ tableId: "", id: "", revision: "" }) + .run() + + expect(results.steps[0].outputs.success).toEqual(false) }) it("should return an error when table doesn't exist", async () => { - const res = await setup.runStep(config, setup.actions.DELETE_ROW.stepId, { - tableId: "invalid", - id: "invalid", - revision: "invalid", + const builder = createAutomationBuilder({ + name: "Nonexistent Table Automation", }) - expect(res.success).toEqual(false) + + const results = await builder + .appAction({ fields: {} }) + .deleteRow({ + tableId: "invalid", + id: "invalid", + revision: "invalid", + }) + .run() + + expect(results.steps[0].outputs.success).toEqual(false) }) }) diff --git a/packages/server/src/automations/tests/updateRow.spec.ts b/packages/server/src/automations/tests/updateRow.spec.ts index 457bf60533..45f78826f6 100644 --- a/packages/server/src/automations/tests/updateRow.spec.ts +++ b/packages/server/src/automations/tests/updateRow.spec.ts @@ -8,58 +8,83 @@ import { Table, TableSourceType, } from "@budibase/types" +import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import * as setup from "./utilities" import * as uuid from "uuid" describe("test the update row action", () => { - let table: Table, row: Row, inputs: any - let config = setup.getConfig() + let table: Table, + row: Row, + config = setup.getConfig() beforeAll(async () => { await config.init() table = await config.createTable() row = await config.createRow() - inputs = { - rowId: row._id, - row: { - ...row, - name: "Updated name", - // put a falsy option in to be removed - description: "", - }, - } }) afterAll(setup.afterAll) - it("should be able to run the action", async () => { - const res = await setup.runStep( - config, - setup.actions.UPDATE_ROW.stepId, - inputs + it("should be able to run the update row action", async () => { + const builder = createAutomationBuilder({ + name: "Update Row Automation", + }) + + const results = await builder + .appAction({ fields: {} }) + .updateRow({ + rowId: row._id!, + row: { + ...row, + name: "Updated name", + description: "", + }, + meta: {}, + }) + .run() + + expect(results.steps[0].outputs.success).toEqual(true) + const updatedRow = await config.api.row.get( + table._id!, + results.steps[0].outputs.id ) - expect(res.success).toEqual(true) - const updatedRow = await config.api.row.get(table._id!, res.id) expect(updatedRow.name).toEqual("Updated name") expect(updatedRow.description).not.toEqual("") }) it("should check invalid inputs return an error", async () => { - const res = await setup.runStep(config, setup.actions.UPDATE_ROW.stepId, {}) - expect(res.success).toEqual(false) + const builder = createAutomationBuilder({ + name: "Invalid Inputs Automation", + }) + + const results = await builder + .appAction({ fields: {} }) + .updateRow({ meta: {}, row: {}, rowId: "" }) + .run() + + expect(results.steps[0].outputs.success).toEqual(false) }) it("should return an error when table doesn't exist", async () => { - const res = await setup.runStep(config, setup.actions.UPDATE_ROW.stepId, { - row: { _id: "invalid" }, - rowId: "invalid", + const builder = createAutomationBuilder({ + name: "Nonexistent Table Automation", }) - expect(res.success).toEqual(false) + + const results = await builder + .appAction({ fields: {} }) + .updateRow({ + row: { _id: "invalid" }, + rowId: "invalid", + meta: {}, + }) + .run() + + expect(results.steps[0].outputs.success).toEqual(false) }) it("should not overwrite links if those links are not set", async () => { - let linkField: FieldSchema = { + const linkField: FieldSchema = { type: FieldType.LINK, name: "", fieldName: "", @@ -71,7 +96,7 @@ describe("test the update row action", () => { tableId: InternalTable.USER_METADATA, } - let table = await config.api.table.save({ + const table = await config.api.table.save({ name: uuid.v4(), type: "table", sourceType: TableSourceType.INTERNAL, @@ -82,23 +107,22 @@ describe("test the update row action", () => { }, }) - let user1 = await config.createUser() - let user2 = await config.createUser() + const user1 = await config.createUser() + const user2 = await config.createUser() - let row = await config.api.row.save(table._id!, { + const row = await config.api.row.save(table._id!, { user1: [{ _id: user1._id }], user2: [{ _id: user2._id }], }) - let getResp = await config.api.row.get(table._id!, row._id!) - expect(getResp.user1[0]._id).toEqual(user1._id) - expect(getResp.user2[0]._id).toEqual(user2._id) + const builder = createAutomationBuilder({ + name: "Link Preservation Automation", + }) - let stepResp = await setup.runStep( - config, - setup.actions.UPDATE_ROW.stepId, - { - rowId: row._id, + const results = await builder + .appAction({ fields: {} }) + .updateRow({ + rowId: row._id!, row: { _id: row._id, _rev: row._rev, @@ -106,17 +130,19 @@ describe("test the update row action", () => { user1: [user2._id], user2: "", }, - } - ) - expect(stepResp.success).toEqual(true) + meta: {}, + }) + .run() - getResp = await config.api.row.get(table._id!, row._id!) + expect(results.steps[0].outputs.success).toEqual(true) + + const getResp = await config.api.row.get(table._id!, row._id!) expect(getResp.user1[0]._id).toEqual(user2._id) expect(getResp.user2[0]._id).toEqual(user2._id) }) - it("should overwrite links if those links are not set and we ask it do", async () => { - let linkField: FieldSchema = { + it("should overwrite links if those links are not set and we ask it to", async () => { + const linkField: FieldSchema = { type: FieldType.LINK, name: "", fieldName: "", @@ -128,7 +154,7 @@ describe("test the update row action", () => { tableId: InternalTable.USER_METADATA, } - let table = await config.api.table.save({ + const table = await config.api.table.save({ name: uuid.v4(), type: "table", sourceType: TableSourceType.INTERNAL, @@ -139,23 +165,22 @@ describe("test the update row action", () => { }, }) - let user1 = await config.createUser() - let user2 = await config.createUser() + const user1 = await config.createUser() + const user2 = await config.createUser() - let row = await config.api.row.save(table._id!, { + const row = await config.api.row.save(table._id!, { user1: [{ _id: user1._id }], user2: [{ _id: user2._id }], }) - let getResp = await config.api.row.get(table._id!, row._id!) - expect(getResp.user1[0]._id).toEqual(user1._id) - expect(getResp.user2[0]._id).toEqual(user2._id) + const builder = createAutomationBuilder({ + name: "Link Overwrite Automation", + }) - let stepResp = await setup.runStep( - config, - setup.actions.UPDATE_ROW.stepId, - { - rowId: row._id, + const results = await builder + .appAction({ fields: {} }) + .updateRow({ + rowId: row._id!, row: { _id: row._id, _rev: row._rev, @@ -170,11 +195,12 @@ describe("test the update row action", () => { }, }, }, - } - ) - expect(stepResp.success).toEqual(true) + }) + .run() - getResp = await config.api.row.get(table._id!, row._id!) + expect(results.steps[0].outputs.success).toEqual(true) + + const getResp = await config.api.row.get(table._id!, row._id!) expect(getResp.user1[0]._id).toEqual(user2._id) expect(getResp.user2).toBeUndefined() })