Skip to content

Commit 4887742

Browse files
committed
fix(spec): trim dead webhook triggers undelete + api (#3196)
WebhookTriggerType declared 5 triggers but only create/update/delete ever fired. The other two had no event source: - undelete: the engine has no soft-delete/restore capability (delete is a hard delete; no deleted_at convention, no restore operation, no data.record.undeleted emit). The undeleted case in the auto-enqueuer's action mapper was dead code awaiting a producer that doesn't exist. - api ("manually triggered"): no manual/programmatic fire path exists — the only webhook HTTP surface re-queues already-failed deliveries. Same declared≠enforced pattern as #3184 (validation delete-event) and #3195 (hook events). Trim, contract-first: authoring a webhook on a removed trigger now fails loudly at os validate / registration instead of registering a webhook that silently never fires. No shipped webhook metadata used either. The auto-enqueuer now also warns when a persisted sys_webhook row carries a trigger it can't map to an emitted record event (drift-guard, mirroring #3195's registerHook warning) and drops it rather than caching a subscription that matches nothing. Updated the sys_webhook trigger options + field help (all locales), docs, and regenerated reference; added rejection + drift-guard tests. Reintroduce undelete only with a real restore subsystem, and api only with a real manual-fire endpoint. Closes #3196 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCUSMJBsX14C3RQdWFw7N7
1 parent 5754a23 commit 4887742

14 files changed

Lines changed: 160 additions & 47 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/plugin-webhooks": patch
4+
---
5+
6+
Trim the dead `undelete` and `api` webhook triggers (#3196). `WebhookTriggerType` declared five triggers but only three ever fired:
7+
8+
- `undelete` had no event source — the engine has no soft-delete/restore capability (`delete` is a hard delete; no `deleted_at` convention, no restore operation, and `data.record.undeleted` is never emitted). The `undeleted` case in the auto-enqueuer's action mapper was dead code awaiting a producer that doesn't exist.
9+
- `api` ("manually triggered") had no fire path — the only webhook HTTP surface re-queues already-failed deliveries; nothing originates a manual fire.
10+
11+
Both are removed from the enum (contract-first, matching #3184/#3195): authoring a webhook on a removed trigger now fails loudly at `os validate` / registration instead of registering a webhook that silently never fires. No shipped webhook metadata used either. The auto-enqueuer now also warns when a persisted `sys_webhook` row carries a trigger it can't map to an emitted record event (a drift-guard, so a dead trigger can't silently no-op again). Reintroduce `undelete` only alongside a real restore subsystem, and `api` only alongside a real manual-fire endpoint. Updated the `sys_webhook` trigger options, field help (all locales), docs, and reference; added rejection tests.

content/docs/automation/webhooks.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ in `definition_json`, a serialised `Webhook` JSON (canonical schema:
9393
| `id` | text | Primary key. |
9494
| `name` | text | Unique snake_case name — referenced in logs and audit. |
9595
| `label` | text | Optional display label. |
96-
| `object_name` | text | Short object name whose events fire this webhook (blank = manual / API-triggered). |
97-
| `triggers` | text | Comma-separated event list: `create,update,delete,undelete,api`. |
96+
| `object_name` | text | Short object name whose record events fire this webhook. |
97+
| `triggers` | text | Comma-separated event list: `create,update,delete`. |
9898
| `url` | text | External endpoint that receives the POST. |
9999
| `method` | text | HTTP method. Default `POST`. |
100100
| `description` | textarea | Free-text description. |
@@ -321,7 +321,7 @@ Notes:
321321

322322
- **`object`** — short object name the event came from.
323323
- **`recordId`** — id of the affected record.
324-
- **`action`** — `created` / `updated` / `deleted` / `undeleted`.
324+
- **`action`** — `created` / `updated` / `deleted`.
325325
- **`timestamp`** — event timestamp, an ISO 8601 string (passed through
326326
unchanged from the originating realtime event's `timestamp` field — not
327327
an epoch-ms number).

content/docs/references/automation/webhook.mdx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,36 @@ Webhook Trigger Event
99

1010
When should this webhook fire?
1111

12+
These mirror the record events the engine actually emits
13+
14+
(`data.record.created` / `updated` / `deleted`), which the webhook
15+
16+
auto-enqueuer maps to `create` / `update` / `delete`. Only events with a real
17+
18+
producer are declared here — an author can't subscribe to something that
19+
20+
never fires.
21+
22+
Deliberately NOT triggers (#3196):
23+
24+
- `undelete` — there is no soft-delete / restore capability in the engine
25+
26+
(`delete` is a hard delete; no `deleted_at` convention, no restore
27+
28+
operation, no `data.record.undeleted` emit), so it had no event source.
29+
30+
Reintroduce it only alongside a real restore subsystem that emits an
31+
32+
undelete event.
33+
34+
- `api` (manual/programmatic fire) — no manual fire path exists (the only
35+
36+
webhook HTTP surface re-queues already-failed deliveries). Reintroduce it
37+
38+
with a real "fire this webhook now" endpoint/service, not as a bare enum
39+
40+
value that silently never fires.
41+
1242
<Callout type="info">
1343
**Source:** `packages/spec/src/automation/webhook.zod.ts`
1444
</Callout>
@@ -33,8 +63,8 @@ const result = Webhook.parse(data);
3363
| :--- | :--- | :--- | :--- |
3464
| **name** | `string` || Webhook unique name (lowercase snake_case) |
3565
| **label** | `string` | optional | Human-readable webhook label |
36-
| **object** | `string` | optional | Object to listen to (optional for manual webhooks) |
37-
| **triggers** | `Enum<'create' \| 'update' \| 'delete' \| 'undelete' \| 'api'>[]` | optional | Events that trigger execution |
66+
| **object** | `string` | optional | Object whose record events (create/update/delete) trigger this webhook |
67+
| **triggers** | `Enum<'create' \| 'update' \| 'delete'>[]` | optional | Events that trigger execution |
3868
| **url** | `string` || External webhook endpoint URL |
3969
| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` || HTTP method |
4070
| **headers** | `Record<string, string>` | optional | Custom HTTP headers |
@@ -75,8 +105,6 @@ const result = Webhook.parse(data);
75105
* `create`
76106
* `update`
77107
* `delete`
78-
* `undelete`
79-
* `api`
80108

81109

82110
---

content/docs/references/integration/connector.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ Circuit breaker configuration
184184
| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | |
185185
| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
186186
| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record<string, any> }; defaultValue?: any; … }[]` | optional | Field mapping rules |
187-
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'undelete' \| 'api'>[]; … }[]` | optional | Webhook configurations |
187+
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations |
188188
| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
189189
| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
190190
| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
@@ -310,7 +310,7 @@ Connector type
310310
| **triggers** | `{ key: string; label: string; description?: string; type: Enum<'polling' \| 'webhook'>; … }[]` | optional | |
311311
| **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration |
312312
| **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record<string, any> }; defaultValue?: any; … }[]` | optional | Field mapping rules |
313-
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'undelete' \| 'api'>[]; … }[]` | optional | Webhook configurations |
313+
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete'>[]; … }[]` | optional | Webhook configurations |
314314
| **rateLimitConfig** | `{ strategy?: Enum<'fixed_window' \| 'sliding_window' \| 'token_bucket' \| 'leaky_bucket'>; maxRequests: number; windowSeconds: number; burstCapacity?: number; … }` | optional | Rate limiting configuration |
315315
| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
316316
| **connectionTimeoutMs** | `number` | optional | Connection timeout in ms |
@@ -465,8 +465,8 @@ Synchronization strategy
465465
| :--- | :--- | :--- | :--- |
466466
| **name** | `string` || Webhook unique name (lowercase snake_case) |
467467
| **label** | `string` | optional | Human-readable webhook label |
468-
| **object** | `string` | optional | Object to listen to (optional for manual webhooks) |
469-
| **triggers** | `Enum<'create' \| 'update' \| 'delete' \| 'undelete' \| 'api'>[]` | optional | Events that trigger execution |
468+
| **object** | `string` | optional | Object whose record events (create/update/delete) trigger this webhook |
469+
| **triggers** | `Enum<'create' \| 'update' \| 'delete'>[]` | optional | Events that trigger execution |
470470
| **url** | `string` || External webhook endpoint URL |
471471
| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` || HTTP method |
472472
| **headers** | `Record<string, string>` | optional | Custom HTTP headers |

0 commit comments

Comments
 (0)