-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathsys-webhook.object.ts
More file actions
231 lines (213 loc) · 8.74 KB
/
Copy pathsys-webhook.object.ts
File metadata and controls
231 lines (213 loc) · 8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { ObjectSchema, Field } from '@objectstack/spec/data';
/**
* sys_webhook — Outbound HTTP integration configuration (runtime).
*
* Persists a single {@link Webhook} envelope per row so administrators
* can author, enable/disable, and edit webhook subscriptions from the
* Studio UI without code changes. The canonical Zod schema for the
* `definition_json` envelope lives at `@objectstack/spec/automation/webhook`.
*
* ## Two authoring doors, one row
* Rows land here two ways, distinguished by the `managed_by` provenance column:
* - **admin** — created/edited directly through this object's CRUD UI.
* - **package** — declared in code (`defineStack({ webhooks })` /
* `defineWebhook()`) and materialized on boot by
* `bootstrapDeclaredWebhooks` (#3461). Re-seeded every boot, but an admin
* edit stamps `customized: true` and freezes the row (seed-not-clobber,
* mirrors `sys_sharing_rule` #2909).
*
* One row per `name`. This plugin's {@link AutoEnqueuer} loads active rows on
* boot + on `sys_webhook:changed` events, and turns matching `data.record.*`
* events into deliveries on the shared `service-messaging` HTTP outbox
* (ADR-0018 M3 — `sys_http_delivery`, drained by the messaging dispatcher).
*
* Ownership (ADR-0029 K2.a): this object is **owned by
* `@objectstack/plugin-webhooks`** — the plugin that consumes these rows. It
* used to live in the `@objectstack/platform-objects` monolith and be imported
* here; the definition now lives with its owner so the plugin ships both data
* and behavior as one unit.
*
* Platform-wide on purpose: every project (standalone, single-tenant,
* cloud) can integrate with external systems (Slack, Stripe, internal
* services) the same way.
*
* @namespace sys
*/
export const SysWebhook = ObjectSchema.create({
name: 'sys_webhook',
label: 'Webhook',
pluralLabel: 'Webhooks',
icon: 'webhook',
isSystem: true,
managedBy: 'config',
// Authoring a webhook from the UI requires a structured form for the
// headers / auth / retry / payload blocks — the generic JSON textarea
// is acceptable as a v1 until a dedicated builder lands. Re-enable
// create/edit/delete so admins can at least toggle `active` and edit
// simple URL/method fields without round-tripping through code.
userActions: { create: true, edit: true, delete: true, import: false },
description: 'Outbound HTTP webhook subscription. Declared in code via defineStack({ webhooks }) / defineWebhook() (materialized into rows on boot) or authored directly in the Studio editor; dispatched by the webhook auto-enqueuer onto the shared HTTP outbox.',
displayNameField: 'name',
nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
titleFormat: '{label}',
highlightFields: ['name', 'object_name', 'url', 'active', 'updated_at'],
listViews: {
active: {
type: 'grid',
name: 'active',
label: 'Active',
data: { provider: 'object', object: 'sys_webhook' },
columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],
filter: [{ field: 'active', operator: 'equals', value: true }],
sort: [{ field: 'label', order: 'asc' }],
pagination: { pageSize: 50 },
},
inactive: {
type: 'grid',
name: 'inactive',
label: 'Inactive',
data: { provider: 'object', object: 'sys_webhook' },
columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],
filter: [{ field: 'active', operator: 'equals', value: false }],
sort: [{ field: 'label', order: 'asc' }],
pagination: { pageSize: 50 },
},
by_object: {
type: 'grid',
name: 'by_object',
label: 'By Object',
data: { provider: 'object', object: 'sys_webhook' },
columns: ['object_name', 'label', 'url', 'active', 'updated_at'],
sort: [{ field: 'object_name', order: 'asc' }, { field: 'label', order: 'asc' }],
grouping: { fields: [{ field: 'object_name', order: 'asc', collapsed: false }] },
pagination: { pageSize: 100 },
},
all_webhooks: {
type: 'grid',
name: 'all_webhooks',
label: 'All',
data: { provider: 'object', object: 'sys_webhook' },
columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],
sort: [{ field: 'label', order: 'asc' }],
pagination: { pageSize: 50 },
},
},
fields: {
id: Field.text({ label: 'Webhook ID', required: true, readonly: true, group: 'System' }),
name: Field.text({
label: 'Name',
required: true,
maxLength: 100,
description: 'Unique snake_case name — referenced in logs and audit',
group: 'Definition',
}),
label: Field.text({
label: 'Display Label',
required: false,
maxLength: 200,
group: 'Definition',
}),
object_name: Field.text({
label: 'Object',
required: false,
maxLength: 100,
// Object picker (same widget as sys_sharing_rule) instead of a free-text
// machine name. Falls back to a text input when the widget is unavailable.
widget: 'object-ref',
description: 'Short object name whose record events (create/update/delete) fire this webhook',
group: 'Definition',
}),
triggers: Field.select(
['create', 'update', 'delete'],
{
label: 'Triggers',
required: false,
// Multi-select instead of a hand-typed comma-separated string. Stored as
// an array; the auto-enqueuer parser also tolerates the legacy
// comma-separated / JSON-string forms so existing rows keep working.
multiple: true,
description: 'Record events that fire this webhook',
group: 'Definition',
},
),
url: Field.text({
label: 'Target URL',
required: true,
maxLength: 2048,
description: 'External endpoint that receives the POST',
group: 'Definition',
}),
method: Field.select(
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
{
label: 'HTTP Method',
required: true,
// Select instead of free text. Option values are lowercased by the
// Field.select helper (get/post/…); the auto-enqueuer upper-cases the
// resolved method before delivery, so existing 'POST' rows and the
// lowercase option values both normalise correctly.
defaultValue: 'post',
description: 'HTTP method used for the callback request',
group: 'Definition',
},
),
description: Field.textarea({ label: 'Description', required: false, group: 'Definition' }),
active: Field.boolean({
label: 'Active',
required: true,
defaultValue: true,
description: 'Inactive webhooks are skipped by the dispatcher',
group: 'Definition',
}),
definition_json: Field.textarea({
label: 'Definition',
required: true,
description: 'Serialised Webhook JSON (see @objectstack/spec/automation/webhook) — full headers/auth/retry/payload config',
group: 'Definition',
}),
// ── Provenance (#3461 — record-authoritative seed-not-clobber) ──
// Mirrors sys_sharing_rule (#2909). Both columns are `readonly`: the
// engine strips them from non-system payloads (forge/clear-proof), while
// bootstrapDeclaredWebhooks and the provenance stamp hook write with
// isSystem. Deliberately NOT a write gate: webhooks are a first-class admin
// authoring/tuning surface — admins may edit or deactivate a package row;
// the seeder simply stops overwriting it once `customized` is stamped.
managed_by: Field.select(
['platform', 'package', 'admin'],
{
label: 'Managed By',
required: false,
readonly: true,
defaultValue: 'admin',
description:
'Record provenance: platform = framework built-in / package = app/package-declared ' +
'(boot-seeded from defineStack webhooks) / admin = created in Setup.',
group: 'System',
},
),
customized: Field.boolean({
label: 'Customized',
required: false,
readonly: true,
defaultValue: false,
description:
'Set when an admin edits a package-declared webhook; boot seeding will no longer ' +
'overwrite the row (a deactivated noisy webhook survives redeploys). Meaningless on admin rows.',
group: 'System',
}),
created_at: Field.datetime({
label: 'Created At',
required: true,
defaultValue: 'NOW()',
readonly: true,
group: 'System',
}),
updated_at: Field.datetime({ label: 'Updated At', required: false, group: 'System' }),
},
indexes: [
{ fields: ['name'], unique: true },
{ fields: ['object_name'] },
{ fields: ['active', 'object_name'] },
],
});