diff --git a/README.md b/README.md index 218df0a..a1e75eb 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,20 @@ 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, plus the required **"Make an API Call"** universal module for authorized requests to the OpnForm API. +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 1. User searches "OpnForm" in Make's module picker 2. Creates a connection using their OpnForm API key (Sanctum token) 3. Selects a workspace and form from dynamic dropdowns -4. Make auto-registers a webhook via OpnForm's standard integration API -5. On each form submission, OpnForm POSTs data to Make's webhook +4. Make auto-registers the dedicated `make` integration via OpnForm's standard integration API +5. On each form submission, OpnForm POSTs the Make-specific payload to Make's webhook 6. When the webhook is deleted, Make auto-unregisters it from OpnForm ## Architecture -This integration follows the same lightweight pattern as OpnForm's Activepieces integration. It uses OpnForm's **existing `/open/*` API** -- no dedicated Make endpoints are needed. +This integration follows the same lightweight pattern as OpnForm's Activepieces integration. It uses OpnForm's **existing `/open/*` API** -- no dedicated Make endpoints are needed. The existing API accepts `integration_id: "make"` through OpnForm's integration registry and dispatches submissions through `MakeIntegration`, which removes the deprecated generic `submission` field and exposes `form_id`, `form_title`, `form_slug`, `submission_id`, optional `edit_link`, and `data`. ## Structure @@ -46,7 +46,7 @@ All endpoints are part of OpnForm's existing API, authenticated via Bearer token | Endpoint | Method | Purpose | |---|---|---| | `/open/workspaces` | GET | Validate API key + list workspaces | -| `/open/workspaces/{id}/forms` | GET | List forms (paginated, up to 100/page) | +| `/open/workspaces/{id}/forms` | GET | List forms (paginated, up to 100/page and 300 results per picker load) | | `/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 | @@ -62,7 +62,7 @@ Docs: [developers.make.com/custom-apps-documentation](https://developers.make.co 3. Click **+ Create app** and name it `opnform` (label: "OpnForm") 4. Configure the **Connection** using `connection/parameters.json` and `connection/communication.json` 5. Create a **Webhook** (dedicated + attached) using `webhook/` configs -6. Create an **Instant Trigger** module "Watch New Submissions" using `modules/watchNewSubmissions/` configs +6. Create an **Instant Trigger** module "Watch new submissions" using `modules/watchNewSubmissions/` configs 7. Create **RPCs** `listWorkspaces` and `listForms` using `rpcs/` configs 8. Test end-to-end with a real OpnForm API key 9. Submit for marketplace review (review takes ~4-6 weeks) diff --git a/connection/communication.json b/connection/communication.json index 6266757..5866a69 100644 --- a/connection/communication.json +++ b/connection/communication.json @@ -5,10 +5,11 @@ }, "response": { "metadata": { + "type": "text", "value": "{{body.length}} workspace(s)" }, "error": { - "message": "{{body.message}}" + "message": "[{{statusCode}}] {{ifempty(body.message, 'Request failed')}}" } }, "log": { diff --git a/connection/parameters.json b/connection/parameters.json index 7b331e7..b455fe4 100644 --- a/connection/parameters.json +++ b/connection/parameters.json @@ -2,15 +2,16 @@ { "name": "apiKey", "type": "password", - "label": "OpnForm API Key", - "help": "Generate an API key from your [OpnForm account settings](https://opnform.com/home?user-settings=access-tokens).", + "label": "OpnForm API key", + "help": "Generate an API key from your [OpnForm account settings](https://opnform.com/home?user-settings=access-tokens) with the `workspaces-read`, `forms-read`, and `manage-integrations` abilities.", "required": true }, { "name": "baseUrl", "type": "url", - "label": "API Base URL", - "help": "Leave empty for OpnForm Cloud. For self-hosted instances, enter your API URL (e.g. `https://api.your-domain.com`).", - "default": "https://api.opnform.com" + "label": "API base URL", + "help": "Leave the default for OpnForm Cloud. For self-hosted instances, enter your API URL (e.g. `https://api.your-domain.com`). This URL cannot be changed after the connection is created.", + "default": "https://api.opnform.com", + "editable": false } ] diff --git a/makecomapp.json b/makecomapp.json index 8328f53..0f4ea61 100644 --- a/makecomapp.json +++ b/makecomapp.json @@ -9,7 +9,7 @@ "components": { "connection": { "apiKeyAuth": { - "label": "OpnForm API Key", + "label": "OpnForm API key", "connectionType": "basic", "codeFiles": { "communication": "connection/communication.json", @@ -20,7 +20,7 @@ }, "webhook": { "newSubmission": { - "label": "New Submission", + "label": "New submission", "webhookType": "web", "connection": "apiKeyAuth", "codeFiles": { @@ -32,7 +32,7 @@ }, "module": { "watchNewSubmissions": { - "label": "Watch New Submissions", + "label": "Watch new submissions", "description": "Triggers when a new form submission is received.", "moduleType": "instant_trigger", "connection": "apiKeyAuth", @@ -45,7 +45,7 @@ } }, "makeAnApiCall": { - "label": "Make an API Call", + "label": "Make an API call", "description": "Performs an arbitrary authorized API call.", "moduleType": "universal", "connection": "apiKeyAuth", @@ -60,14 +60,14 @@ }, "rpc": { "listWorkspaces": { - "label": "List Workspaces", + "label": "List workspaces", "connection": "apiKeyAuth", "codeFiles": { "communication": "rpcs/listWorkspaces.json" } }, "listForms": { - "label": "List Forms", + "label": "List forms", "connection": "apiKeyAuth", "codeFiles": { "communication": "rpcs/listForms.json" diff --git a/modules/makeAnApiCall/expect.json b/modules/makeAnApiCall/expect.json index bcd1081..8cbdab3 100644 --- a/modules/makeAnApiCall/expect.json +++ b/modules/makeAnApiCall/expect.json @@ -74,7 +74,7 @@ } ], "type": "array", - "label": "Query String" + "label": "Query string" }, { "name": "body", diff --git a/modules/watchNewSubmissions/interface.json b/modules/watchNewSubmissions/interface.json index 83205a5..94e8ab5 100644 --- a/modules/watchNewSubmissions/interface.json +++ b/modules/watchNewSubmissions/interface.json @@ -7,12 +7,12 @@ { "name": "form_title", "type": "text", - "label": "Form Title" + "label": "Form title" }, { "name": "form_slug", "type": "text", - "label": "Form Slug" + "label": "Form slug" }, { "name": "submission_id", @@ -22,12 +22,12 @@ { "name": "edit_link", "type": "url", - "label": "Edit Submission URL" + "label": "Edit submission URL" }, { "name": "data", "type": "collection", - "label": "Form Fields", + "label": "Form fields", "spec": [] } ] diff --git a/rpcs/listForms.json b/rpcs/listForms.json index a10140c..1eb7c48 100644 --- a/rpcs/listForms.json +++ b/rpcs/listForms.json @@ -5,6 +5,7 @@ "per_page": 100 }, "response": { + "limit": 300, "output": { "label": "{{item.title}}", "value": "{{item.id}}" diff --git a/tests/contracts.test.mjs b/tests/contracts.test.mjs index e1f0f0e..c40a58c 100644 --- a/tests/contracts.test.mjs +++ b/tests/contracts.test.mjs @@ -27,7 +27,9 @@ test('attached webhook persists and reuses the remote integration id', async () const detach = await readJson('webhook/detach.json') assert.equal(communication.output, '{{body}}') + assert.equal(communication.respond.type, 'json') assert.equal(communication.respond.status, 200) + assert.equal(attach.body.integration_id, 'make') assert.equal(attach.response.data.externalHookId, '{{body.form_integration.id}}') assert.equal(attach.response.data.formId, '{{parameters.formId}}') assert.match(detach.url, /{{webhook\.formId}}/) @@ -53,6 +55,7 @@ test('form RPC follows the Laravel resource pagination envelope', async () => { } assert.equal(rpc.response.iterate, '{{body.data}}') + assert.equal(rpc.response.limit, 300) assert.equal(rpc.pagination.url, '{{body.links.next}}') assert.equal(rpc.pagination.condition, '{{body.links.next}}') assert.equal(response.links.next.endsWith('page=2'), true) @@ -60,6 +63,41 @@ test('form RPC follows the Laravel resource pagination envelope', async () => { assert.equal(rpc.qs.per_page, 100) }) +test('connection protects its domain and explains the required token abilities', async () => { + const parameters = await readJson('connection/parameters.json') + const communication = await readJson('connection/communication.json') + const apiKey = parameters.find(({ name }) => name === 'apiKey') + const baseUrl = parameters.find(({ name }) => name === 'baseUrl') + + assert.match(apiKey.help, /workspaces-read/) + assert.match(apiKey.help, /forms-read/) + assert.match(apiKey.help, /manage-integrations/) + assert.equal(baseUrl.editable, false) + assert.match(baseUrl.help, /cannot be changed/) + assert.equal(communication.response.metadata.type, 'text') + assert.equal( + communication.response.error.message, + "[{{statusCode}}] {{ifempty(body.message, 'Request failed')}}" + ) +}) + +test('user-facing labels follow Make sentence case', async () => { + const manifest = await readJson('makecomapp.json') + const parameters = await readJson('connection/parameters.json') + const triggerInterface = await readJson('modules/watchNewSubmissions/interface.json') + const universalExpect = await readJson('modules/makeAnApiCall/expect.json') + + assert.equal(manifest.components.connection.apiKeyAuth.label, 'OpnForm API key') + assert.equal(manifest.components.module.watchNewSubmissions.label, 'Watch new submissions') + assert.equal(manifest.components.module.makeAnApiCall.label, 'Make an API call') + assert.equal(parameters.find(({ name }) => name === 'baseUrl').label, 'API base URL') + assert.equal(triggerInterface.find(({ name }) => name === 'form_title').label, 'Form title') + assert.equal(triggerInterface.find(({ name }) => name === 'form_slug').label, 'Form slug') + assert.equal(triggerInterface.find(({ name }) => name === 'edit_link').label, 'Edit submission URL') + assert.equal(triggerInterface.find(({ name }) => name === 'data').label, 'Form fields') + assert.equal(universalExpect.find(({ name }) => name === 'qs').label, 'Query string') +}) + test('trigger interface exposes dynamic fields and optional edit link', async () => { const output = await readJson('modules/watchNewSubmissions/interface.json') const data = output.find(({ name }) => name === 'data') diff --git a/webhook/communication.json b/webhook/communication.json index e314cca..3f4a12e 100644 --- a/webhook/communication.json +++ b/webhook/communication.json @@ -1,6 +1,7 @@ { "output": "{{body}}", "respond": { + "type": "json", "status": 200, "body": { "ok": true