Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This app provides a **"Watch New Submissions"** instant trigger so Make users ca
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
6. When the scenario is deleted, Make auto-unregisters the webhook
6. When the webhook is deleted, Make auto-unregisters it from OpnForm

## Architecture

Expand All @@ -26,15 +26,16 @@ This integration follows the same lightweight pattern as OpnForm's Activepieces
│ └── communication.json # Auth validation (GET /open/workspaces)
├── webhook/
│ ├── parameters.json # Webhook parameters (form selection)
│ ├── communication.json # Convert the incoming request body to a Make bundle
│ ├── attach.json # Register webhook (POST /open/forms/{id}/integrations)
│ └── detach.json # Unregister webhook (DELETE /open/forms/{id}/integrations/{id})
├── modules/
│ └── watchNewSubmissions/
│ ├── communication.json # Incoming webhook data processing
│ ├── communication.json # Empty: the paired webhook already produces the bundle
│ └── expect.json # Module parameters (form/workspace selectors)
└── rpcs/
├── listWorkspaces.json # Dynamic dropdown (GET /open/workspaces)
└── listForms.json # Dynamic dropdown (GET /open/workspaces/{id}/forms) with pagination
└── listForms.json # Dynamic dropdown (GET /open/workspaces/{id}/forms) with bounded pagination
```

## API Endpoints Used
Expand All @@ -44,7 +45,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, 10/page) |
| `/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) |

Expand Down
10 changes: 1 addition & 9 deletions modules/watchNewSubmissions/communication.json
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
{
"output": "{{body}}",
"respond": {
"status": 200,
"body": {
"ok": true
}
}
}
{}
3 changes: 3 additions & 0 deletions rpcs/listForms.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"url": "/open/workspaces/{{parameters.workspaceId}}/forms",
"method": "GET",
"qs": {
"per_page": 100
},
"response": {
"output": {
"label": "{{item.title}}",
Expand Down
14 changes: 13 additions & 1 deletion tests/contracts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,26 @@ test('production origin targets the real Make app', async () => {
})

test('attached webhook persists and reuses the remote integration id', async () => {
const communication = await readJson('webhook/communication.json')
const attach = await readJson('webhook/attach.json')
const detach = await readJson('webhook/detach.json')

assert.equal(communication.output, '{{body}}')
assert.equal(communication.respond.status, 200)
assert.equal(attach.response.data.externalHookId, '{{body.form_integration.id}}')
assert.equal(attach.response.data.formId, '{{parameters.formId}}')
assert.match(detach.url, /{{webhook\.formId}}/)
assert.match(detach.url, /{{webhook\.externalHookId}}/)
assert.doesNotMatch(detach.url, /parameters\.formId/)
assert.doesNotMatch(detach.url, /webhook\.data/)
})

test('instant trigger preserves the bundle produced by its webhook', async () => {
const communication = await readJson('modules/watchNewSubmissions/communication.json')

assert.deepEqual(communication, {})
})

test('form RPC follows the Laravel resource pagination envelope', async () => {
const rpc = await readJson('rpcs/listForms.json')
const response = {
Expand All @@ -43,7 +55,7 @@ test('form RPC follows the Laravel resource pagination envelope', async () => {
assert.equal(rpc.pagination.condition, '{{body.links.next}}')
assert.equal(response.links.next.endsWith('page=2'), true)
assert.equal(response.next_page_url, undefined)
assert.equal(rpc.qs, undefined)
assert.equal(rpc.qs.per_page, 100)
})

test('trigger interface exposes dynamic fields and optional edit link', async () => {
Expand Down
3 changes: 2 additions & 1 deletion webhook/attach.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
},
"response": {
"data": {
"externalHookId": "{{body.form_integration.id}}"
"externalHookId": "{{body.form_integration.id}}",
"formId": "{{parameters.formId}}"
}
}
}
9 changes: 9 additions & 0 deletions webhook/communication.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"output": "{{body}}",
"respond": {
"status": 200,
"body": {
"ok": true
}
}
}
2 changes: 1 addition & 1 deletion webhook/detach.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"url": "/open/forms/{{parameters.formId}}/integrations/{{webhook.externalHookId}}",
"url": "/open/forms/{{webhook.formId}}/integrations/{{webhook.externalHookId}}",
"method": "DELETE"
}
Loading