diff --git a/README.md b/README.md index af64cec..218df0a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Configuration files for the [OpnForm](https://opnform.com) custom app on [Make.com](https://www.make.com) (formerly Integromat). -This app provides a **"Watch New Submissions"** instant trigger so Make users can automate workflows whenever a form receives a submission. +This app provides a **"Watch New Submissions"** instant trigger so Make users can automate workflows whenever a form receives a submission, plus the required **"Make an API Call"** universal module for authorized requests to the OpnForm API. ## How It Works @@ -30,9 +30,10 @@ This integration follows the same lightweight pattern as OpnForm's Activepieces │ ├── attach.json # Register webhook (POST /open/forms/{id}/integrations) │ └── detach.json # Unregister webhook (DELETE /open/forms/{id}/integrations/{id}) ├── modules/ -│ └── watchNewSubmissions/ -│ ├── communication.json # Empty: the paired webhook already produces the bundle -│ └── expect.json # Module parameters (form/workspace selectors) +│ ├── watchNewSubmissions/ +│ │ ├── communication.json # Empty: the paired webhook already produces the bundle +│ │ └── expect.json # Empty: form/workspace selectors belong to the webhook +│ └── makeAnApiCall/ # Authorized REST universal module using relative API paths └── rpcs/ ├── listWorkspaces.json # Dynamic dropdown (GET /open/workspaces) └── listForms.json # Dynamic dropdown (GET /open/workspaces/{id}/forms) with bounded pagination @@ -48,6 +49,7 @@ All endpoints are part of OpnForm's existing API, authenticated via Bearer token | `/open/workspaces/{id}/forms` | GET | List forms (paginated, up to 100/page) | | `/open/forms/{id}/integrations` | POST | Create Make integration (attach webhook) | | `/open/forms/{id}/integrations/{integrationId}` | DELETE | Remove integration (detach webhook) | +| Any relative path selected by the user | Any supported REST method | Universal authorized API call | ## Setup in Make diff --git a/makecomapp.json b/makecomapp.json index fc45966..8328f53 100644 --- a/makecomapp.json +++ b/makecomapp.json @@ -43,6 +43,19 @@ "interface": "modules/watchNewSubmissions/interface.json", "samples": "modules/watchNewSubmissions/samples.json" } + }, + "makeAnApiCall": { + "label": "Make an API Call", + "description": "Performs an arbitrary authorized API call.", + "moduleType": "universal", + "connection": "apiKeyAuth", + "codeFiles": { + "communication": "modules/makeAnApiCall/communication.json", + "parameters": "modules/makeAnApiCall/parameters.json", + "mappableParams": "modules/makeAnApiCall/expect.json", + "interface": "modules/makeAnApiCall/interface.json", + "samples": "modules/makeAnApiCall/samples.json" + } } }, "rpc": { diff --git a/modules/makeAnApiCall/communication.json b/modules/makeAnApiCall/communication.json new file mode 100644 index 0000000..84d1d1b --- /dev/null +++ b/modules/makeAnApiCall/communication.json @@ -0,0 +1,19 @@ +{ + "qs": { + "{{...}}": "{{toCollection(parameters.qs, 'key', 'value')}}" + }, + "url": "{{parameters.url}}", + "body": "{{parameters.body}}", + "type": "text", + "method": "{{parameters.method}}", + "headers": { + "{{...}}": "{{toCollection(parameters.headers, 'key', 'value')}}" + }, + "response": { + "output": { + "body": "{{body}}", + "headers": "{{headers}}", + "statusCode": "{{statusCode}}" + } + } +} diff --git a/modules/makeAnApiCall/expect.json b/modules/makeAnApiCall/expect.json new file mode 100644 index 0000000..bcd1081 --- /dev/null +++ b/modules/makeAnApiCall/expect.json @@ -0,0 +1,84 @@ +[ + { + "help": "Enter a path relative to the configured OpnForm API base URL. For example: /open/workspaces", + "name": "url", + "type": "text", + "label": "URL", + "required": true + }, + { + "name": "method", + "type": "select", + "label": "Method", + "default": "GET", + "options": [ + { + "label": "GET", + "value": "GET" + }, + { + "label": "POST", + "value": "POST" + }, + { + "label": "PUT", + "value": "PUT" + }, + { + "label": "PATCH", + "value": "PATCH" + }, + { + "label": "DELETE", + "value": "DELETE" + } + ], + "required": true + }, + { + "help": "You don't have to add authorization headers; we already did that for you.", + "name": "headers", + "spec": [ + { + "name": "key", + "type": "text", + "label": "Key" + }, + { + "name": "value", + "type": "text", + "label": "Value" + } + ], + "type": "array", + "label": "Headers", + "default": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + }, + { + "name": "qs", + "spec": [ + { + "name": "key", + "type": "text", + "label": "Key" + }, + { + "name": "value", + "type": "text", + "label": "Value" + } + ], + "type": "array", + "label": "Query String" + }, + { + "name": "body", + "type": "any", + "label": "Body" + } +] diff --git a/modules/makeAnApiCall/interface.json b/modules/makeAnApiCall/interface.json new file mode 100644 index 0000000..7e2b517 --- /dev/null +++ b/modules/makeAnApiCall/interface.json @@ -0,0 +1,17 @@ +[ + { + "name": "body", + "type": "any", + "label": "Body" + }, + { + "name": "headers", + "type": "collection", + "label": "Headers" + }, + { + "name": "statusCode", + "type": "number", + "label": "Status code" + } +] diff --git a/modules/makeAnApiCall/parameters.json b/modules/makeAnApiCall/parameters.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/modules/makeAnApiCall/parameters.json @@ -0,0 +1 @@ +[] diff --git a/modules/makeAnApiCall/samples.json b/modules/makeAnApiCall/samples.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/modules/makeAnApiCall/samples.json @@ -0,0 +1 @@ +{} diff --git a/modules/watchNewSubmissions/expect.json b/modules/watchNewSubmissions/expect.json index 72164f9..fe51488 100644 --- a/modules/watchNewSubmissions/expect.json +++ b/modules/watchNewSubmissions/expect.json @@ -1,20 +1 @@ -[ - { - "name": "workspaceId", - "type": "select", - "label": "Workspace", - "required": true, - "options": { - "store": "rpc://listWorkspaces", - "nested": [ - { - "name": "formId", - "type": "select", - "label": "Form", - "required": true, - "options": "rpc://listForms" - } - ] - } - } -] +[] diff --git a/tests/contracts.test.mjs b/tests/contracts.test.mjs index 4ec211e..e1f0f0e 100644 --- a/tests/contracts.test.mjs +++ b/tests/contracts.test.mjs @@ -38,8 +38,10 @@ test('attached webhook persists and reuses the remote integration id', async () test('instant trigger preserves the bundle produced by its webhook', async () => { const communication = await readJson('modules/watchNewSubmissions/communication.json') + const parameters = await readJson('modules/watchNewSubmissions/expect.json') assert.deepEqual(communication, {}) + assert.deepEqual(parameters, []) }) test('form RPC follows the Laravel resource pagination envelope', async () => { @@ -67,6 +69,19 @@ test('trigger interface exposes dynamic fields and optional edit link', async () assert.equal(editLink.type, 'url') }) +test('universal module only accepts relative API paths and uses the app connection', async () => { + const manifest = await readJson('makecomapp.json') + const universal = manifest.components.module.makeAnApiCall + const communication = await readJson('modules/makeAnApiCall/communication.json') + const parameters = await readJson('modules/makeAnApiCall/expect.json') + + assert.equal(universal.moduleType, 'universal') + assert.equal(universal.connection, 'apiKeyAuth') + assert.equal(communication.url, '{{parameters.url}}') + assert.doesNotMatch(communication.url, /^https?:\/\//) + assert.match(parameters.find(({ name }) => name === 'url').help, /\/open\/workspaces/) +}) + test('Make requests use relative open API paths inherited from Base', async () => { const paths = [ 'rpcs/listWorkspaces.json',