@objectstack/plugin-webhooks 14.6.0 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +14 -14
- package/CHANGELOG.md +42 -0
- package/dist/{chunk-RA3REIOA.cjs → chunk-I63ISOGO.cjs} +31 -16
- package/dist/chunk-I63ISOGO.cjs.map +1 -0
- package/dist/{chunk-HSSV22TO.js → chunk-YQG6SUP3.js} +31 -16
- package/dist/chunk-YQG6SUP3.js.map +1 -0
- package/dist/index.cjs +31 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -4
- package/dist/index.js.map +1 -1
- package/dist/schema.cjs +2 -2
- package/dist/schema.d.cts +24 -11
- package/dist/schema.d.ts +24 -11
- package/dist/schema.js +1 -1
- package/package.json +4 -4
- package/src/auto-enqueuer.test.ts +36 -0
- package/src/auto-enqueuer.ts +28 -5
- package/src/sys-webhook.object.ts +30 -15
- package/dist/chunk-HSSV22TO.js.map +0 -1
- package/dist/chunk-RA3REIOA.cjs.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auto-enqueuer.ts","../src/webhook-outbox-plugin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IDataEngine, IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\n\n/**\n * Enqueue callback into the shared `service-messaging` HTTP outbox (ADR-0018 M3).\n * The plugin supplies one bound to `messaging.enqueueHttp(...)`; webhooks no\n * longer own a delivery outbox/dispatcher — they share the generic substrate.\n */\nexport type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;\n\n/**\n * Optional logger interface (subset of console / kernel logger).\n */\ninterface OptionalLogger {\n info?(msg: string, meta?: unknown): void;\n warn?(msg: string, meta?: unknown): void;\n debug?(msg: string, meta?: unknown): void;\n error?(msg: string, err?: unknown, meta?: unknown): void;\n}\n\n/**\n * Per-row subscription cached in memory. Mirrors a subset of the\n * `sys_webhook` object — only what the auto-enqueuer needs to match an\n * event and build an `EnqueueInput`.\n */\ninterface CachedSubscription {\n id: string;\n name: string;\n objectName: string | undefined; // empty = matches all objects (manual-only is filtered out earlier)\n triggers: Set<'create' | 'update' | 'delete' | 'undelete'>;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n secret?: string;\n timeoutMs?: number;\n}\n\nexport interface AutoEnqueuerOptions {\n /**\n * Object name holding webhook subscriptions. Defaults to `sys_webhook`,\n * the platform-objects schema authored in apps.\n */\n subscriptionsObject?: string;\n\n /**\n * Periodic full-cache refresh interval (ms). Belt-and-braces in case\n * the subscription-change event is missed. Default 60s.\n */\n refreshIntervalMs?: number;\n\n logger?: OptionalLogger;\n}\n\n/**\n * Bridge between `IRealtimeService` (`data.record.*` events emitted by\n * the engine) and `IWebhookOutbox` (durable delivery rows the dispatcher\n * picks up).\n *\n * ## Why a separate class\n * Keeps `WebhookOutboxPlugin` lean: the plugin wires services, this\n * class owns the runtime fan-out logic + subscription cache.\n *\n * ## Hot path\n * Every `engine.insert/update/delete` fires a `data.record.*` event.\n * The handler:\n * 1. Looks up matching subscriptions in an in-memory `Map<object, sub[]>`\n * — O(1) per event, no DB hit on the write path.\n * 2. Calls `outbox.enqueue()` fire-and-forget for each match. The\n * enqueue itself is a single INSERT, which runs *after* the user's\n * request has already returned.\n *\n * Net cost on the write path: one synchronous Map lookup (~microseconds).\n *\n * ## Cache freshness\n * The cache is rebuilt:\n * 1. Once on `start()`.\n * 2. On every `data.record.{created,updated,deleted}` event whose\n * object is `sys_webhook` (self-healing — when a user toggles a\n * webhook, the handler refreshes the cache before returning).\n * 3. Periodically (default 60s) as belt-and-braces.\n *\n * For multi-node clusters this is *eventually consistent* — node B may\n * not see node A's edit for up to one cycle. That's acceptable for\n * webhook configuration changes (humans don't expect millisecond\n * propagation) and matches Hasura's behaviour.\n *\n * ## Determinism\n * `eventId` is computed from `${object}:${recordId}:${type}:${timestamp}`\n * so the outbox dedup index catches duplicates that could arise from\n * upstream replay or buggy producers — and is stable across nodes.\n */\nexport class AutoEnqueuer {\n private readonly subscriptions = new Map<string, CachedSubscription[]>();\n private readonly subscriptionsObject: string;\n private readonly refreshIntervalMs: number;\n private readonly logger: OptionalLogger;\n private subId: string | undefined;\n private subIdSelfHeal: string | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n private running = false;\n private refreshing: Promise<void> | undefined;\n\n constructor(\n private readonly engine: IDataEngine,\n private readonly realtime: IRealtimeService,\n private readonly enqueue: HttpEnqueueFn,\n opts: AutoEnqueuerOptions = {},\n ) {\n this.subscriptionsObject = opts.subscriptionsObject ?? 'sys_webhook';\n this.refreshIntervalMs = opts.refreshIntervalMs ?? 60_000;\n this.logger = opts.logger ?? {};\n }\n\n /**\n * Load the subscription cache and start listening for events.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.refresh();\n\n // Main subscription: every data event → match → enqueue.\n this.subId = await this.realtime.subscribe(\n 'webhook-auto-enqueuer',\n (event) => this.handleEvent(event),\n );\n\n // Self-healing: any change to sys_webhook refreshes the cache.\n this.subIdSelfHeal = await this.realtime.subscribe(\n 'webhook-auto-enqueuer-self-heal',\n (event) => this.handleSelfHealEvent(event),\n { object: this.subscriptionsObject },\n );\n\n if (this.refreshIntervalMs > 0) {\n this.refreshTimer = setInterval(() => {\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] periodic refresh failed', err),\n );\n }, this.refreshIntervalMs);\n // Don't keep the process alive solely for this timer.\n this.refreshTimer.unref?.();\n }\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.subId) await this.realtime.unsubscribe(this.subId);\n if (this.subIdSelfHeal) await this.realtime.unsubscribe(this.subIdSelfHeal);\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n this.subId = undefined;\n this.subIdSelfHeal = undefined;\n this.refreshTimer = undefined;\n }\n\n /**\n * Force-refresh the subscription cache from storage. Concurrent\n * callers share a single in-flight refresh.\n */\n async refresh(): Promise<void> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = this.doRefresh().finally(() => {\n this.refreshing = undefined;\n });\n return this.refreshing;\n }\n\n private async doRefresh(): Promise<void> {\n let rows: any[];\n try {\n rows = await this.engine.find(this.subscriptionsObject, {\n where: { active: true },\n });\n } catch (err) {\n this.logger.warn?.(\n `[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,\n err,\n );\n return;\n }\n\n const next = new Map<string, CachedSubscription[]>();\n for (const row of rows) {\n const sub = this.parseRow(row);\n if (!sub) continue;\n // Empty objectName == \"any object\" → indexed under '*'.\n const key = sub.objectName ?? '*';\n const arr = next.get(key) ?? [];\n arr.push(sub);\n next.set(key, arr);\n }\n\n this.subscriptions.clear();\n for (const [k, v] of next) this.subscriptions.set(k, v);\n\n this.logger.debug?.('[webhook-auto-enqueuer] cache refreshed', {\n objects: this.subscriptions.size,\n rows: rows.length,\n });\n }\n\n private parseRow(row: any): CachedSubscription | null {\n if (!row?.id || !row?.url) return null;\n const triggersField = (row.triggers ?? '') as string;\n const triggers = new Set(\n triggersField\n .split(',')\n .map((s: string) => s.trim().toLowerCase())\n .filter(Boolean) as Array<'create' | 'update' | 'delete' | 'undelete'>,\n );\n if (triggers.size === 0) {\n // Manual-only webhook (no triggers) — skip auto-enqueue.\n return null;\n }\n\n // The \"definition_json\" field carries advanced config (headers,\n // secret, timeout); attempt a best-effort parse. Fall back to\n // top-level fields where present.\n let defn: Record<string, any> = {};\n if (typeof row.definition_json === 'string' && row.definition_json.length > 0) {\n try {\n defn = JSON.parse(row.definition_json) ?? {};\n } catch {\n defn = {};\n }\n }\n\n return {\n id: row.id as string,\n name: (row.name as string) ?? row.id,\n objectName: row.object_name ? String(row.object_name) : undefined,\n triggers,\n url: String(row.url),\n method: row.method ?? defn.method ?? 'POST',\n headers: defn.headers,\n secret: defn.secret,\n timeoutMs: defn.timeoutMs,\n };\n }\n\n /**\n * Handler for the firehose subscription.\n *\n * NOTE: we intentionally `void` the inner enqueue() so the realtime\n * publisher (and therefore the user's request) is never blocked on\n * webhook persistence.\n */\n private handleEvent(event: RealtimeEventPayload): void {\n if (!event.type?.startsWith('data.record.')) return;\n if (!event.object) return;\n if (event.object === this.subscriptionsObject) return; // self-heal handles its own\n\n const action = event.type.slice('data.record.'.length) as\n | 'created' | 'updated' | 'deleted' | 'undeleted' | string;\n const trigger = mapActionToTrigger(action);\n if (!trigger) return;\n\n const subs = [\n ...(this.subscriptions.get(event.object) ?? []),\n ...(this.subscriptions.get('*') ?? []),\n ];\n if (subs.length === 0) return;\n\n const payload = event.payload ?? {};\n const recordId =\n (payload as any).recordId ??\n (payload as any).id ??\n (payload as any).after?.id ??\n (payload as any).before?.id ??\n 'unknown';\n\n // Deterministic eventId — same input on any node → same id.\n // Includes timestamp so two distinct updates to the same record\n // don't accidentally dedup.\n const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;\n\n for (const sub of subs) {\n if (!sub.triggers.has(trigger)) continue;\n\n // Fire-and-forget — never await on the hot path. Map the webhook\n // delivery onto the generic HTTP-outbox shape (ADR-0018 M3):\n // - source 'webhook' + dedupKey '<webhookId>:<eventId>' preserves\n // the old (event_id, webhook_id) at-most-once enqueue;\n // - refId = webhookId keeps per-webhook partition affinity / ordering;\n // - label = event type → X-Objectstack-Event header.\n void this.enqueue({\n source: 'webhook',\n refId: sub.id,\n dedupKey: `${sub.id}:${eventId}`,\n label: event.type,\n url: sub.url,\n method: sub.method,\n headers: sub.headers,\n signingSecret: sub.secret,\n timeoutMs: sub.timeoutMs,\n payload: {\n object: event.object,\n recordId,\n action,\n timestamp: event.timestamp,\n ...payload,\n },\n }).catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] enqueue failed', {\n webhook: sub.name,\n eventId,\n err: (err as Error)?.message ?? err,\n }),\n );\n }\n }\n\n private handleSelfHealEvent(event: RealtimeEventPayload): void {\n if (event.object !== this.subscriptionsObject) return;\n if (!event.type?.startsWith('data.record.')) return;\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] self-heal refresh failed', err),\n );\n }\n\n /** Test / admin accessor. */\n snapshot(): ReadonlyMap<string, ReadonlyArray<CachedSubscription>> {\n return this.subscriptions;\n }\n}\n\nfunction mapActionToTrigger(\n action: string,\n): 'create' | 'update' | 'delete' | 'undelete' | null {\n switch (action) {\n case 'created':\n return 'create';\n case 'updated':\n return 'update';\n case 'deleted':\n return 'delete';\n case 'undeleted':\n return 'undelete';\n default:\n return null;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\nimport { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';\nimport { SysWebhook } from './sys-webhook.object.js';\n\n/**\n * Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface\n * (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime\n * import on the service. Webhook deliveries are enqueued onto the shared\n * `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.\n */\ninterface MessagingHttpSurface {\n isHttpDeliveryReady(): boolean;\n enqueueHttp(input: EnqueueHttpInput): Promise<string>;\n redeliverHttp(id: string): Promise<{ id: string; status: string }>;\n}\n\nexport interface WebhookOutboxPluginOptions {\n /**\n * Auto-enqueue config. When enabled (default `true` if the realtime + data\n * engine services are available), the plugin subscribes to `data.record.*`\n * events and enqueues a delivery onto the shared messaging HTTP outbox for\n * every matching `sys_webhook` row.\n *\n * Set `false` to disable and enqueue webhooks imperatively elsewhere.\n */\n autoEnqueue?: boolean | AutoEnqueuerOptions;\n}\n\n/**\n * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate\n * (ADR-0018 M3).\n *\n * Webhooks are no longer their own delivery engine: the durable outbox, the\n * cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and\n * the retention sweep all live in `@objectstack/service-messaging`\n * (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the\n * webhook-specific concerns:\n * - the `sys_webhook` configuration object,\n * - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox\n * rows (`source: 'webhook'`), and\n * - the redeliver admin endpoint.\n *\n * End-to-end flow:\n *\n * engine.insert('contact', {...})\n * → engine publishes data.record.created via IRealtimeService\n * → AutoEnqueuer matches active sys_webhook rows in O(1)\n * → messaging.enqueueHttp() runs fire-and-forget (off the write path)\n * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)\n *\n * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),\n * which is a foundational, always-on capability.\n */\nexport class WebhookOutboxPlugin implements Plugin {\n name = 'com.objectstack.plugin-webhook-outbox';\n version = '2.0.0';\n type = 'standard' as const;\n dependencies = ['com.objectstack.service.messaging'];\n\n private autoEnqueuer: AutoEnqueuer | undefined;\n\n constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}\n\n async init(ctx: PluginContext): Promise<void> {\n // Register the webhook config object (ADR-0029 K2.a). The delivery\n // telemetry now lives in messaging's `sys_http_delivery`, so the nav's\n // \"Deliveries\" entry points there (filtered to source=webhook in views).\n const manifest = ctx.getService<{ register(m: any): void }>('manifest');\n if (manifest && typeof manifest.register === 'function') {\n manifest.register({\n id: 'com.objectstack.plugin-webhook-outbox.schema',\n namespace: 'sys',\n version: this.version,\n type: 'plugin',\n scope: 'system',\n name: 'Webhook Schemas',\n description: 'Registers sys_webhook (configuration). Deliveries use messaging\\'s sys_http_delivery outbox.',\n objects: [SysWebhook],\n navigationContributions: [\n {\n app: 'setup',\n group: 'group_integrations',\n priority: 100,\n items: [\n { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },\n { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },\n ],\n },\n ],\n });\n } else {\n ctx.logger.warn?.(\n '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',\n );\n }\n\n // ADR-0029 D8 — contribute object translations once i18n is up.\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n try {\n const i18n = ctx.getService<any>('i18n');\n if (i18n && typeof i18n.loadTranslations === 'function') {\n const { WebhooksTranslations } = await import('./translations/index.js');\n for (const [locale, data] of Object.entries(WebhooksTranslations)) {\n i18n.loadTranslations(locale, data as Record<string, unknown>);\n }\n }\n } catch { /* i18n optional */ }\n });\n }\n\n const autoEnqueueOpt = this.options.autoEnqueue ?? true;\n\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n await this.bootAutoEnqueue(ctx, autoEnqueueOpt);\n this.registerAdminRoutes(ctx);\n });\n }\n\n ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {\n autoEnqueue: autoEnqueueOpt !== false,\n });\n }\n\n async dispose(): Promise<void> {\n await this.autoEnqueuer?.stop();\n }\n\n private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {\n const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);\n return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;\n }\n\n private async bootAutoEnqueue(\n ctx: PluginContext,\n opt: boolean | AutoEnqueuerOptions,\n ): Promise<void> {\n if (opt === false) return;\n const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);\n const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);\n const messaging = this.getMessaging(ctx);\n if (!engine || !realtime || !messaging) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',\n { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },\n );\n return;\n }\n if (!messaging.isHttpDeliveryReady()) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',\n );\n }\n\n const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;\n this.autoEnqueuer = new AutoEnqueuer(\n engine,\n realtime,\n (input) => messaging.enqueueHttp(input),\n { ...enqOpts, logger: ctx.logger },\n );\n await this.autoEnqueuer.start();\n ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);\n ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');\n }\n\n private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {\n for (const n of names) {\n try {\n const svc = ctx.getService<T>(n);\n if (svc) return svc;\n } catch {\n // fall through\n }\n }\n return undefined;\n }\n\n /**\n * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is\n * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the\n * better-auth session cookie — every authenticated user counts.\n */\n private registerAdminRoutes(ctx: PluginContext): void {\n const http = this.tryGetService<any>(ctx, ['http-server']);\n if (!http || typeof http.getRawApp !== 'function') {\n ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');\n return;\n }\n const rawApp = http.getRawApp();\n const messaging = this.getMessaging(ctx);\n if (!rawApp || !messaging) return;\n\n rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {\n const userId = await this.resolveSessionUserId(ctx, c);\n if (!userId) {\n return c.json(\n { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },\n 401,\n );\n }\n let body: any;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);\n }\n const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';\n if (!deliveryId) {\n return c.json(\n { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },\n 400,\n );\n }\n try {\n const row = await messaging.redeliverHttp(deliveryId);\n ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });\n return c.json({ success: true, data: { id: row.id, status: row.status } });\n } catch (err: any) {\n const code = err?.code;\n if (code === 'not_found') {\n return c.json({ success: false, error: 'not_found', message: err.message }, 404);\n }\n if (code === 'not_eligible') {\n return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);\n }\n ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);\n return c.json(\n { success: false, error: 'internal_error', message: err?.message ?? String(err) },\n 500,\n );\n }\n });\n\n ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');\n }\n\n private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {\n try {\n const authService: any = this.tryGetService<any>(ctx, ['auth']);\n if (!authService) return undefined;\n let api: any = authService.api;\n if (!api && typeof authService.getApi === 'function') {\n api = await authService.getApi();\n }\n if (!api?.getSession) return undefined;\n const session = await api.getSession({ headers: c.req.raw.headers });\n const uid = session?.user?.id;\n return typeof uid === 'string' && uid.length > 0 ? uid : undefined;\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/auto-enqueuer.ts","../src/webhook-outbox-plugin.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IDataEngine, IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\n\n/**\n * Enqueue callback into the shared `service-messaging` HTTP outbox (ADR-0018 M3).\n * The plugin supplies one bound to `messaging.enqueueHttp(...)`; webhooks no\n * longer own a delivery outbox/dispatcher — they share the generic substrate.\n */\nexport type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;\n\n/**\n * Optional logger interface (subset of console / kernel logger).\n */\ninterface OptionalLogger {\n info?(msg: string, meta?: unknown): void;\n warn?(msg: string, meta?: unknown): void;\n debug?(msg: string, meta?: unknown): void;\n error?(msg: string, err?: unknown, meta?: unknown): void;\n}\n\n/**\n * Per-row subscription cached in memory. Mirrors a subset of the\n * `sys_webhook` object — only what the auto-enqueuer needs to match an\n * event and build an `EnqueueInput`.\n */\ninterface CachedSubscription {\n id: string;\n name: string;\n objectName: string | undefined; // empty = matches all objects (manual-only is filtered out earlier)\n triggers: Set<'create' | 'update' | 'delete' | 'undelete'>;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n secret?: string;\n timeoutMs?: number;\n}\n\nexport interface AutoEnqueuerOptions {\n /**\n * Object name holding webhook subscriptions. Defaults to `sys_webhook`,\n * the platform-objects schema authored in apps.\n */\n subscriptionsObject?: string;\n\n /**\n * Periodic full-cache refresh interval (ms). Belt-and-braces in case\n * the subscription-change event is missed. Default 60s.\n */\n refreshIntervalMs?: number;\n\n logger?: OptionalLogger;\n}\n\n/**\n * Bridge between `IRealtimeService` (`data.record.*` events emitted by\n * the engine) and `IWebhookOutbox` (durable delivery rows the dispatcher\n * picks up).\n *\n * ## Why a separate class\n * Keeps `WebhookOutboxPlugin` lean: the plugin wires services, this\n * class owns the runtime fan-out logic + subscription cache.\n *\n * ## Hot path\n * Every `engine.insert/update/delete` fires a `data.record.*` event.\n * The handler:\n * 1. Looks up matching subscriptions in an in-memory `Map<object, sub[]>`\n * — O(1) per event, no DB hit on the write path.\n * 2. Calls `outbox.enqueue()` fire-and-forget for each match. The\n * enqueue itself is a single INSERT, which runs *after* the user's\n * request has already returned.\n *\n * Net cost on the write path: one synchronous Map lookup (~microseconds).\n *\n * ## Cache freshness\n * The cache is rebuilt:\n * 1. Once on `start()`.\n * 2. On every `data.record.{created,updated,deleted}` event whose\n * object is `sys_webhook` (self-healing — when a user toggles a\n * webhook, the handler refreshes the cache before returning).\n * 3. Periodically (default 60s) as belt-and-braces.\n *\n * For multi-node clusters this is *eventually consistent* — node B may\n * not see node A's edit for up to one cycle. That's acceptable for\n * webhook configuration changes (humans don't expect millisecond\n * propagation) and matches Hasura's behaviour.\n *\n * ## Determinism\n * `eventId` is computed from `${object}:${recordId}:${type}:${timestamp}`\n * so the outbox dedup index catches duplicates that could arise from\n * upstream replay or buggy producers — and is stable across nodes.\n */\nexport class AutoEnqueuer {\n private readonly subscriptions = new Map<string, CachedSubscription[]>();\n private readonly subscriptionsObject: string;\n private readonly refreshIntervalMs: number;\n private readonly logger: OptionalLogger;\n private subId: string | undefined;\n private subIdSelfHeal: string | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n private running = false;\n private refreshing: Promise<void> | undefined;\n\n constructor(\n private readonly engine: IDataEngine,\n private readonly realtime: IRealtimeService,\n private readonly enqueue: HttpEnqueueFn,\n opts: AutoEnqueuerOptions = {},\n ) {\n this.subscriptionsObject = opts.subscriptionsObject ?? 'sys_webhook';\n this.refreshIntervalMs = opts.refreshIntervalMs ?? 60_000;\n this.logger = opts.logger ?? {};\n }\n\n /**\n * Load the subscription cache and start listening for events.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.refresh();\n\n // Main subscription: every data event → match → enqueue.\n this.subId = await this.realtime.subscribe(\n 'webhook-auto-enqueuer',\n (event) => this.handleEvent(event),\n );\n\n // Self-healing: any change to sys_webhook refreshes the cache.\n this.subIdSelfHeal = await this.realtime.subscribe(\n 'webhook-auto-enqueuer-self-heal',\n (event) => this.handleSelfHealEvent(event),\n { object: this.subscriptionsObject },\n );\n\n if (this.refreshIntervalMs > 0) {\n this.refreshTimer = setInterval(() => {\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] periodic refresh failed', err),\n );\n }, this.refreshIntervalMs);\n // Don't keep the process alive solely for this timer.\n this.refreshTimer.unref?.();\n }\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.subId) await this.realtime.unsubscribe(this.subId);\n if (this.subIdSelfHeal) await this.realtime.unsubscribe(this.subIdSelfHeal);\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n this.subId = undefined;\n this.subIdSelfHeal = undefined;\n this.refreshTimer = undefined;\n }\n\n /**\n * Force-refresh the subscription cache from storage. Concurrent\n * callers share a single in-flight refresh.\n */\n async refresh(): Promise<void> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = this.doRefresh().finally(() => {\n this.refreshing = undefined;\n });\n return this.refreshing;\n }\n\n private async doRefresh(): Promise<void> {\n let rows: any[];\n try {\n rows = await this.engine.find(this.subscriptionsObject, {\n where: { active: true },\n });\n } catch (err) {\n this.logger.warn?.(\n `[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,\n err,\n );\n return;\n }\n\n const next = new Map<string, CachedSubscription[]>();\n for (const row of rows) {\n const sub = this.parseRow(row);\n if (!sub) continue;\n // Empty objectName == \"any object\" → indexed under '*'.\n const key = sub.objectName ?? '*';\n const arr = next.get(key) ?? [];\n arr.push(sub);\n next.set(key, arr);\n }\n\n this.subscriptions.clear();\n for (const [k, v] of next) this.subscriptions.set(k, v);\n\n this.logger.debug?.('[webhook-auto-enqueuer] cache refreshed', {\n objects: this.subscriptions.size,\n rows: rows.length,\n });\n }\n\n private parseRow(row: any): CachedSubscription | null {\n if (!row?.id || !row?.url) return null;\n // `triggers` is now authored as a multi-select (stored as an array), but\n // legacy rows stored a comma-separated string (and some drivers hand a\n // JSON-encoded array back as a string). Accept all three shapes so a\n // schema change never silently drops a subscription's events.\n const rawTriggers = row.triggers;\n let triggerList: string[];\n if (Array.isArray(rawTriggers)) {\n triggerList = rawTriggers.map((t) => String(t));\n } else {\n const s = String(rawTriggers ?? '').trim();\n if (s.startsWith('[')) {\n try {\n const parsed = JSON.parse(s);\n triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];\n } catch {\n triggerList = s.split(',');\n }\n } else {\n triggerList = s.split(',');\n }\n }\n const triggers = new Set(\n triggerList\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean) as Array<'create' | 'update' | 'delete' | 'undelete'>,\n );\n if (triggers.size === 0) {\n // Manual-only webhook (no triggers) — skip auto-enqueue.\n return null;\n }\n\n // The \"definition_json\" field carries advanced config (headers,\n // secret, timeout); attempt a best-effort parse. Fall back to\n // top-level fields where present.\n let defn: Record<string, any> = {};\n if (typeof row.definition_json === 'string' && row.definition_json.length > 0) {\n try {\n defn = JSON.parse(row.definition_json) ?? {};\n } catch {\n defn = {};\n }\n }\n\n return {\n id: row.id as string,\n name: (row.name as string) ?? row.id,\n objectName: row.object_name ? String(row.object_name) : undefined,\n triggers,\n url: String(row.url),\n // Method is authored via a select whose option values are lowercased\n // (get/post/…); upper-case here so delivery uses a canonical HTTP\n // method regardless of whether the row was authored before or after\n // the select change (legacy rows stored 'POST').\n method: String(row.method ?? defn.method ?? 'POST').toUpperCase(),\n headers: defn.headers,\n secret: defn.secret,\n timeoutMs: defn.timeoutMs,\n };\n }\n\n /**\n * Handler for the firehose subscription.\n *\n * NOTE: we intentionally `void` the inner enqueue() so the realtime\n * publisher (and therefore the user's request) is never blocked on\n * webhook persistence.\n */\n private handleEvent(event: RealtimeEventPayload): void {\n if (!event.type?.startsWith('data.record.')) return;\n if (!event.object) return;\n if (event.object === this.subscriptionsObject) return; // self-heal handles its own\n\n const action = event.type.slice('data.record.'.length) as\n | 'created' | 'updated' | 'deleted' | 'undeleted' | string;\n const trigger = mapActionToTrigger(action);\n if (!trigger) return;\n\n const subs = [\n ...(this.subscriptions.get(event.object) ?? []),\n ...(this.subscriptions.get('*') ?? []),\n ];\n if (subs.length === 0) return;\n\n const payload = event.payload ?? {};\n const recordId =\n (payload as any).recordId ??\n (payload as any).id ??\n (payload as any).after?.id ??\n (payload as any).before?.id ??\n 'unknown';\n\n // Deterministic eventId — same input on any node → same id.\n // Includes timestamp so two distinct updates to the same record\n // don't accidentally dedup.\n const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;\n\n for (const sub of subs) {\n if (!sub.triggers.has(trigger)) continue;\n\n // Fire-and-forget — never await on the hot path. Map the webhook\n // delivery onto the generic HTTP-outbox shape (ADR-0018 M3):\n // - source 'webhook' + dedupKey '<webhookId>:<eventId>' preserves\n // the old (event_id, webhook_id) at-most-once enqueue;\n // - refId = webhookId keeps per-webhook partition affinity / ordering;\n // - label = event type → X-Objectstack-Event header.\n void this.enqueue({\n source: 'webhook',\n refId: sub.id,\n dedupKey: `${sub.id}:${eventId}`,\n label: event.type,\n url: sub.url,\n method: sub.method,\n headers: sub.headers,\n signingSecret: sub.secret,\n timeoutMs: sub.timeoutMs,\n payload: {\n object: event.object,\n recordId,\n action,\n timestamp: event.timestamp,\n ...payload,\n },\n }).catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] enqueue failed', {\n webhook: sub.name,\n eventId,\n err: (err as Error)?.message ?? err,\n }),\n );\n }\n }\n\n private handleSelfHealEvent(event: RealtimeEventPayload): void {\n if (event.object !== this.subscriptionsObject) return;\n if (!event.type?.startsWith('data.record.')) return;\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] self-heal refresh failed', err),\n );\n }\n\n /** Test / admin accessor. */\n snapshot(): ReadonlyMap<string, ReadonlyArray<CachedSubscription>> {\n return this.subscriptions;\n }\n}\n\nfunction mapActionToTrigger(\n action: string,\n): 'create' | 'update' | 'delete' | 'undelete' | null {\n switch (action) {\n case 'created':\n return 'create';\n case 'updated':\n return 'update';\n case 'deleted':\n return 'delete';\n case 'undeleted':\n return 'undelete';\n default:\n return null;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\nimport { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';\nimport { SysWebhook } from './sys-webhook.object.js';\n\n/**\n * Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface\n * (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime\n * import on the service. Webhook deliveries are enqueued onto the shared\n * `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.\n */\ninterface MessagingHttpSurface {\n isHttpDeliveryReady(): boolean;\n enqueueHttp(input: EnqueueHttpInput): Promise<string>;\n redeliverHttp(id: string): Promise<{ id: string; status: string }>;\n}\n\nexport interface WebhookOutboxPluginOptions {\n /**\n * Auto-enqueue config. When enabled (default `true` if the realtime + data\n * engine services are available), the plugin subscribes to `data.record.*`\n * events and enqueues a delivery onto the shared messaging HTTP outbox for\n * every matching `sys_webhook` row.\n *\n * Set `false` to disable and enqueue webhooks imperatively elsewhere.\n */\n autoEnqueue?: boolean | AutoEnqueuerOptions;\n}\n\n/**\n * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate\n * (ADR-0018 M3).\n *\n * Webhooks are no longer their own delivery engine: the durable outbox, the\n * cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and\n * the retention sweep all live in `@objectstack/service-messaging`\n * (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the\n * webhook-specific concerns:\n * - the `sys_webhook` configuration object,\n * - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox\n * rows (`source: 'webhook'`), and\n * - the redeliver admin endpoint.\n *\n * End-to-end flow:\n *\n * engine.insert('contact', {...})\n * → engine publishes data.record.created via IRealtimeService\n * → AutoEnqueuer matches active sys_webhook rows in O(1)\n * → messaging.enqueueHttp() runs fire-and-forget (off the write path)\n * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)\n *\n * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),\n * which is a foundational, always-on capability.\n */\nexport class WebhookOutboxPlugin implements Plugin {\n name = 'com.objectstack.plugin-webhook-outbox';\n version = '2.0.0';\n type = 'standard' as const;\n dependencies = ['com.objectstack.service.messaging'];\n\n private autoEnqueuer: AutoEnqueuer | undefined;\n\n constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}\n\n async init(ctx: PluginContext): Promise<void> {\n // Register the webhook config object (ADR-0029 K2.a). The delivery\n // telemetry now lives in messaging's `sys_http_delivery`, so the nav's\n // \"Deliveries\" entry points there (filtered to source=webhook in views).\n const manifest = ctx.getService<{ register(m: any): void }>('manifest');\n if (manifest && typeof manifest.register === 'function') {\n manifest.register({\n id: 'com.objectstack.plugin-webhook-outbox.schema',\n namespace: 'sys',\n version: this.version,\n type: 'plugin',\n scope: 'system',\n name: 'Webhook Schemas',\n description: 'Registers sys_webhook (configuration). Deliveries use messaging\\'s sys_http_delivery outbox.',\n objects: [SysWebhook],\n navigationContributions: [\n {\n app: 'setup',\n group: 'group_integrations',\n priority: 100,\n items: [\n { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },\n { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },\n ],\n },\n ],\n });\n } else {\n ctx.logger.warn?.(\n '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',\n );\n }\n\n // ADR-0029 D8 — contribute object translations once i18n is up.\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n try {\n const i18n = ctx.getService<any>('i18n');\n if (i18n && typeof i18n.loadTranslations === 'function') {\n const { WebhooksTranslations } = await import('./translations/index.js');\n for (const [locale, data] of Object.entries(WebhooksTranslations)) {\n i18n.loadTranslations(locale, data as Record<string, unknown>);\n }\n }\n } catch { /* i18n optional */ }\n });\n }\n\n const autoEnqueueOpt = this.options.autoEnqueue ?? true;\n\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n await this.bootAutoEnqueue(ctx, autoEnqueueOpt);\n this.registerAdminRoutes(ctx);\n });\n }\n\n ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {\n autoEnqueue: autoEnqueueOpt !== false,\n });\n }\n\n async dispose(): Promise<void> {\n await this.autoEnqueuer?.stop();\n }\n\n private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {\n const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);\n return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;\n }\n\n private async bootAutoEnqueue(\n ctx: PluginContext,\n opt: boolean | AutoEnqueuerOptions,\n ): Promise<void> {\n if (opt === false) return;\n const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);\n const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);\n const messaging = this.getMessaging(ctx);\n if (!engine || !realtime || !messaging) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',\n { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },\n );\n return;\n }\n if (!messaging.isHttpDeliveryReady()) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',\n );\n }\n\n const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;\n this.autoEnqueuer = new AutoEnqueuer(\n engine,\n realtime,\n (input) => messaging.enqueueHttp(input),\n { ...enqOpts, logger: ctx.logger },\n );\n await this.autoEnqueuer.start();\n ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);\n ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');\n }\n\n private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {\n for (const n of names) {\n try {\n const svc = ctx.getService<T>(n);\n if (svc) return svc;\n } catch {\n // fall through\n }\n }\n return undefined;\n }\n\n /**\n * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is\n * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the\n * better-auth session cookie — every authenticated user counts.\n */\n private registerAdminRoutes(ctx: PluginContext): void {\n const http = this.tryGetService<any>(ctx, ['http-server']);\n if (!http || typeof http.getRawApp !== 'function') {\n ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');\n return;\n }\n const rawApp = http.getRawApp();\n const messaging = this.getMessaging(ctx);\n if (!rawApp || !messaging) return;\n\n rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {\n const userId = await this.resolveSessionUserId(ctx, c);\n if (!userId) {\n return c.json(\n { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },\n 401,\n );\n }\n let body: any;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);\n }\n const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';\n if (!deliveryId) {\n return c.json(\n { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },\n 400,\n );\n }\n try {\n const row = await messaging.redeliverHttp(deliveryId);\n ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });\n return c.json({ success: true, data: { id: row.id, status: row.status } });\n } catch (err: any) {\n const code = err?.code;\n if (code === 'not_found') {\n return c.json({ success: false, error: 'not_found', message: err.message }, 404);\n }\n if (code === 'not_eligible') {\n return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);\n }\n ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);\n return c.json(\n { success: false, error: 'internal_error', message: err?.message ?? String(err) },\n 500,\n );\n }\n });\n\n ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');\n }\n\n private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {\n try {\n const authService: any = this.tryGetService<any>(ctx, ['auth']);\n if (!authService) return undefined;\n let api: any = authService.api;\n if (!api && typeof authService.getApi === 'function') {\n api = await authService.getApi();\n }\n if (!api?.getSession) return undefined;\n const session = await api.getSession({ headers: c.req.raw.headers });\n const uid = session?.user?.id;\n return typeof uid === 'string' && uid.length > 0 ? uid : undefined;\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/schema.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkI63ISOGOcjs = require('./chunk-I63ISOGO.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
exports.SysWebhook =
|
|
6
|
+
exports.SysWebhook = _chunkI63ISOGOcjs.SysWebhook;
|
|
7
7
|
//# sourceMappingURL=schema.cjs.map
|
package/dist/schema.d.cts
CHANGED
|
@@ -260,6 +260,7 @@ declare const SysWebhook: Omit<{
|
|
|
260
260
|
generatedBy?: string | undefined;
|
|
261
261
|
} | undefined;
|
|
262
262
|
} | undefined;
|
|
263
|
+
widget?: string | undefined;
|
|
263
264
|
requiredPermissions?: string[] | undefined;
|
|
264
265
|
system?: boolean | undefined;
|
|
265
266
|
inlineHelpText?: string | undefined;
|
|
@@ -491,6 +492,11 @@ declare const SysWebhook: Omit<{
|
|
|
491
492
|
schemaId: string;
|
|
492
493
|
schema?: Record<string, unknown> | undefined;
|
|
493
494
|
} | undefined;
|
|
495
|
+
emptyState?: {
|
|
496
|
+
title?: string | undefined;
|
|
497
|
+
message?: string | undefined;
|
|
498
|
+
icon?: string | undefined;
|
|
499
|
+
} | undefined;
|
|
494
500
|
tree?: {
|
|
495
501
|
[x: string]: unknown;
|
|
496
502
|
parentField?: string | undefined;
|
|
@@ -650,11 +656,6 @@ declare const SysWebhook: Omit<{
|
|
|
650
656
|
} | undefined;
|
|
651
657
|
showRecordCount?: boolean | undefined;
|
|
652
658
|
allowPrinting?: boolean | undefined;
|
|
653
|
-
emptyState?: {
|
|
654
|
-
title?: string | undefined;
|
|
655
|
-
message?: string | undefined;
|
|
656
|
-
icon?: string | undefined;
|
|
657
|
-
} | undefined;
|
|
658
659
|
performance?: {
|
|
659
660
|
lazyLoad?: boolean | undefined;
|
|
660
661
|
virtualScroll?: {
|
|
@@ -964,6 +965,7 @@ declare const SysWebhook: Omit<{
|
|
|
964
965
|
readonly precision?: number | undefined;
|
|
965
966
|
readonly required?: boolean | undefined;
|
|
966
967
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
968
|
+
readonly widget?: string | undefined;
|
|
967
969
|
readonly multiple?: boolean | undefined;
|
|
968
970
|
readonly dependencies?: string[] | undefined;
|
|
969
971
|
readonly externalId?: boolean | undefined;
|
|
@@ -1147,6 +1149,7 @@ declare const SysWebhook: Omit<{
|
|
|
1147
1149
|
readonly precision?: number | undefined;
|
|
1148
1150
|
readonly required?: boolean | undefined;
|
|
1149
1151
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1152
|
+
readonly widget?: string | undefined;
|
|
1150
1153
|
readonly multiple?: boolean | undefined;
|
|
1151
1154
|
readonly dependencies?: string[] | undefined;
|
|
1152
1155
|
readonly externalId?: boolean | undefined;
|
|
@@ -1330,6 +1333,7 @@ declare const SysWebhook: Omit<{
|
|
|
1330
1333
|
readonly precision?: number | undefined;
|
|
1331
1334
|
readonly required?: boolean | undefined;
|
|
1332
1335
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1336
|
+
readonly widget?: string | undefined;
|
|
1333
1337
|
readonly multiple?: boolean | undefined;
|
|
1334
1338
|
readonly dependencies?: string[] | undefined;
|
|
1335
1339
|
readonly externalId?: boolean | undefined;
|
|
@@ -1513,6 +1517,7 @@ declare const SysWebhook: Omit<{
|
|
|
1513
1517
|
readonly precision?: number | undefined;
|
|
1514
1518
|
readonly required?: boolean | undefined;
|
|
1515
1519
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1520
|
+
readonly widget?: string | undefined;
|
|
1516
1521
|
readonly multiple?: boolean | undefined;
|
|
1517
1522
|
readonly dependencies?: string[] | undefined;
|
|
1518
1523
|
readonly externalId?: boolean | undefined;
|
|
@@ -1675,7 +1680,7 @@ declare const SysWebhook: Omit<{
|
|
|
1675
1680
|
readonly triggers: {
|
|
1676
1681
|
readonly readonly?: boolean | undefined;
|
|
1677
1682
|
readonly format?: string | undefined;
|
|
1678
|
-
|
|
1683
|
+
options: {
|
|
1679
1684
|
label: string;
|
|
1680
1685
|
value: string;
|
|
1681
1686
|
color?: string | undefined;
|
|
@@ -1689,13 +1694,14 @@ declare const SysWebhook: Omit<{
|
|
|
1689
1694
|
generatedBy?: string | undefined;
|
|
1690
1695
|
} | undefined;
|
|
1691
1696
|
} | undefined;
|
|
1692
|
-
}[]
|
|
1697
|
+
}[];
|
|
1693
1698
|
readonly description?: string | undefined;
|
|
1694
1699
|
readonly label?: string | undefined;
|
|
1695
1700
|
readonly name?: string | undefined;
|
|
1696
1701
|
readonly precision?: number | undefined;
|
|
1697
1702
|
readonly required?: boolean | undefined;
|
|
1698
1703
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1704
|
+
readonly widget?: string | undefined;
|
|
1699
1705
|
readonly multiple?: boolean | undefined;
|
|
1700
1706
|
readonly dependencies?: string[] | undefined;
|
|
1701
1707
|
readonly externalId?: boolean | undefined;
|
|
@@ -1853,7 +1859,7 @@ declare const SysWebhook: Omit<{
|
|
|
1853
1859
|
readonly inlineHelpText?: string | undefined;
|
|
1854
1860
|
readonly autonumberFormat?: string | undefined;
|
|
1855
1861
|
readonly index?: boolean | undefined;
|
|
1856
|
-
readonly type: "
|
|
1862
|
+
readonly type: "select";
|
|
1857
1863
|
};
|
|
1858
1864
|
readonly url: {
|
|
1859
1865
|
readonly readonly?: boolean | undefined;
|
|
@@ -1879,6 +1885,7 @@ declare const SysWebhook: Omit<{
|
|
|
1879
1885
|
readonly precision?: number | undefined;
|
|
1880
1886
|
readonly required?: boolean | undefined;
|
|
1881
1887
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1888
|
+
readonly widget?: string | undefined;
|
|
1882
1889
|
readonly multiple?: boolean | undefined;
|
|
1883
1890
|
readonly dependencies?: string[] | undefined;
|
|
1884
1891
|
readonly externalId?: boolean | undefined;
|
|
@@ -2041,7 +2048,7 @@ declare const SysWebhook: Omit<{
|
|
|
2041
2048
|
readonly method: {
|
|
2042
2049
|
readonly readonly?: boolean | undefined;
|
|
2043
2050
|
readonly format?: string | undefined;
|
|
2044
|
-
|
|
2051
|
+
options: {
|
|
2045
2052
|
label: string;
|
|
2046
2053
|
value: string;
|
|
2047
2054
|
color?: string | undefined;
|
|
@@ -2055,13 +2062,14 @@ declare const SysWebhook: Omit<{
|
|
|
2055
2062
|
generatedBy?: string | undefined;
|
|
2056
2063
|
} | undefined;
|
|
2057
2064
|
} | undefined;
|
|
2058
|
-
}[]
|
|
2065
|
+
}[];
|
|
2059
2066
|
readonly description?: string | undefined;
|
|
2060
2067
|
readonly label?: string | undefined;
|
|
2061
2068
|
readonly name?: string | undefined;
|
|
2062
2069
|
readonly precision?: number | undefined;
|
|
2063
2070
|
readonly required?: boolean | undefined;
|
|
2064
2071
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2072
|
+
readonly widget?: string | undefined;
|
|
2065
2073
|
readonly multiple?: boolean | undefined;
|
|
2066
2074
|
readonly dependencies?: string[] | undefined;
|
|
2067
2075
|
readonly externalId?: boolean | undefined;
|
|
@@ -2219,7 +2227,7 @@ declare const SysWebhook: Omit<{
|
|
|
2219
2227
|
readonly inlineHelpText?: string | undefined;
|
|
2220
2228
|
readonly autonumberFormat?: string | undefined;
|
|
2221
2229
|
readonly index?: boolean | undefined;
|
|
2222
|
-
readonly type: "
|
|
2230
|
+
readonly type: "select";
|
|
2223
2231
|
};
|
|
2224
2232
|
readonly description: {
|
|
2225
2233
|
readonly readonly?: boolean | undefined;
|
|
@@ -2245,6 +2253,7 @@ declare const SysWebhook: Omit<{
|
|
|
2245
2253
|
readonly precision?: number | undefined;
|
|
2246
2254
|
readonly required?: boolean | undefined;
|
|
2247
2255
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2256
|
+
readonly widget?: string | undefined;
|
|
2248
2257
|
readonly multiple?: boolean | undefined;
|
|
2249
2258
|
readonly dependencies?: string[] | undefined;
|
|
2250
2259
|
readonly externalId?: boolean | undefined;
|
|
@@ -2428,6 +2437,7 @@ declare const SysWebhook: Omit<{
|
|
|
2428
2437
|
readonly precision?: number | undefined;
|
|
2429
2438
|
readonly required?: boolean | undefined;
|
|
2430
2439
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2440
|
+
readonly widget?: string | undefined;
|
|
2431
2441
|
readonly multiple?: boolean | undefined;
|
|
2432
2442
|
readonly dependencies?: string[] | undefined;
|
|
2433
2443
|
readonly externalId?: boolean | undefined;
|
|
@@ -2611,6 +2621,7 @@ declare const SysWebhook: Omit<{
|
|
|
2611
2621
|
readonly precision?: number | undefined;
|
|
2612
2622
|
readonly required?: boolean | undefined;
|
|
2613
2623
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2624
|
+
readonly widget?: string | undefined;
|
|
2614
2625
|
readonly multiple?: boolean | undefined;
|
|
2615
2626
|
readonly dependencies?: string[] | undefined;
|
|
2616
2627
|
readonly externalId?: boolean | undefined;
|
|
@@ -2794,6 +2805,7 @@ declare const SysWebhook: Omit<{
|
|
|
2794
2805
|
readonly precision?: number | undefined;
|
|
2795
2806
|
readonly required?: boolean | undefined;
|
|
2796
2807
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2808
|
+
readonly widget?: string | undefined;
|
|
2797
2809
|
readonly multiple?: boolean | undefined;
|
|
2798
2810
|
readonly dependencies?: string[] | undefined;
|
|
2799
2811
|
readonly externalId?: boolean | undefined;
|
|
@@ -2977,6 +2989,7 @@ declare const SysWebhook: Omit<{
|
|
|
2977
2989
|
readonly precision?: number | undefined;
|
|
2978
2990
|
readonly required?: boolean | undefined;
|
|
2979
2991
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2992
|
+
readonly widget?: string | undefined;
|
|
2980
2993
|
readonly multiple?: boolean | undefined;
|
|
2981
2994
|
readonly dependencies?: string[] | undefined;
|
|
2982
2995
|
readonly externalId?: boolean | undefined;
|
package/dist/schema.d.ts
CHANGED
|
@@ -260,6 +260,7 @@ declare const SysWebhook: Omit<{
|
|
|
260
260
|
generatedBy?: string | undefined;
|
|
261
261
|
} | undefined;
|
|
262
262
|
} | undefined;
|
|
263
|
+
widget?: string | undefined;
|
|
263
264
|
requiredPermissions?: string[] | undefined;
|
|
264
265
|
system?: boolean | undefined;
|
|
265
266
|
inlineHelpText?: string | undefined;
|
|
@@ -491,6 +492,11 @@ declare const SysWebhook: Omit<{
|
|
|
491
492
|
schemaId: string;
|
|
492
493
|
schema?: Record<string, unknown> | undefined;
|
|
493
494
|
} | undefined;
|
|
495
|
+
emptyState?: {
|
|
496
|
+
title?: string | undefined;
|
|
497
|
+
message?: string | undefined;
|
|
498
|
+
icon?: string | undefined;
|
|
499
|
+
} | undefined;
|
|
494
500
|
tree?: {
|
|
495
501
|
[x: string]: unknown;
|
|
496
502
|
parentField?: string | undefined;
|
|
@@ -650,11 +656,6 @@ declare const SysWebhook: Omit<{
|
|
|
650
656
|
} | undefined;
|
|
651
657
|
showRecordCount?: boolean | undefined;
|
|
652
658
|
allowPrinting?: boolean | undefined;
|
|
653
|
-
emptyState?: {
|
|
654
|
-
title?: string | undefined;
|
|
655
|
-
message?: string | undefined;
|
|
656
|
-
icon?: string | undefined;
|
|
657
|
-
} | undefined;
|
|
658
659
|
performance?: {
|
|
659
660
|
lazyLoad?: boolean | undefined;
|
|
660
661
|
virtualScroll?: {
|
|
@@ -964,6 +965,7 @@ declare const SysWebhook: Omit<{
|
|
|
964
965
|
readonly precision?: number | undefined;
|
|
965
966
|
readonly required?: boolean | undefined;
|
|
966
967
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
968
|
+
readonly widget?: string | undefined;
|
|
967
969
|
readonly multiple?: boolean | undefined;
|
|
968
970
|
readonly dependencies?: string[] | undefined;
|
|
969
971
|
readonly externalId?: boolean | undefined;
|
|
@@ -1147,6 +1149,7 @@ declare const SysWebhook: Omit<{
|
|
|
1147
1149
|
readonly precision?: number | undefined;
|
|
1148
1150
|
readonly required?: boolean | undefined;
|
|
1149
1151
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1152
|
+
readonly widget?: string | undefined;
|
|
1150
1153
|
readonly multiple?: boolean | undefined;
|
|
1151
1154
|
readonly dependencies?: string[] | undefined;
|
|
1152
1155
|
readonly externalId?: boolean | undefined;
|
|
@@ -1330,6 +1333,7 @@ declare const SysWebhook: Omit<{
|
|
|
1330
1333
|
readonly precision?: number | undefined;
|
|
1331
1334
|
readonly required?: boolean | undefined;
|
|
1332
1335
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1336
|
+
readonly widget?: string | undefined;
|
|
1333
1337
|
readonly multiple?: boolean | undefined;
|
|
1334
1338
|
readonly dependencies?: string[] | undefined;
|
|
1335
1339
|
readonly externalId?: boolean | undefined;
|
|
@@ -1513,6 +1517,7 @@ declare const SysWebhook: Omit<{
|
|
|
1513
1517
|
readonly precision?: number | undefined;
|
|
1514
1518
|
readonly required?: boolean | undefined;
|
|
1515
1519
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1520
|
+
readonly widget?: string | undefined;
|
|
1516
1521
|
readonly multiple?: boolean | undefined;
|
|
1517
1522
|
readonly dependencies?: string[] | undefined;
|
|
1518
1523
|
readonly externalId?: boolean | undefined;
|
|
@@ -1675,7 +1680,7 @@ declare const SysWebhook: Omit<{
|
|
|
1675
1680
|
readonly triggers: {
|
|
1676
1681
|
readonly readonly?: boolean | undefined;
|
|
1677
1682
|
readonly format?: string | undefined;
|
|
1678
|
-
|
|
1683
|
+
options: {
|
|
1679
1684
|
label: string;
|
|
1680
1685
|
value: string;
|
|
1681
1686
|
color?: string | undefined;
|
|
@@ -1689,13 +1694,14 @@ declare const SysWebhook: Omit<{
|
|
|
1689
1694
|
generatedBy?: string | undefined;
|
|
1690
1695
|
} | undefined;
|
|
1691
1696
|
} | undefined;
|
|
1692
|
-
}[]
|
|
1697
|
+
}[];
|
|
1693
1698
|
readonly description?: string | undefined;
|
|
1694
1699
|
readonly label?: string | undefined;
|
|
1695
1700
|
readonly name?: string | undefined;
|
|
1696
1701
|
readonly precision?: number | undefined;
|
|
1697
1702
|
readonly required?: boolean | undefined;
|
|
1698
1703
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1704
|
+
readonly widget?: string | undefined;
|
|
1699
1705
|
readonly multiple?: boolean | undefined;
|
|
1700
1706
|
readonly dependencies?: string[] | undefined;
|
|
1701
1707
|
readonly externalId?: boolean | undefined;
|
|
@@ -1853,7 +1859,7 @@ declare const SysWebhook: Omit<{
|
|
|
1853
1859
|
readonly inlineHelpText?: string | undefined;
|
|
1854
1860
|
readonly autonumberFormat?: string | undefined;
|
|
1855
1861
|
readonly index?: boolean | undefined;
|
|
1856
|
-
readonly type: "
|
|
1862
|
+
readonly type: "select";
|
|
1857
1863
|
};
|
|
1858
1864
|
readonly url: {
|
|
1859
1865
|
readonly readonly?: boolean | undefined;
|
|
@@ -1879,6 +1885,7 @@ declare const SysWebhook: Omit<{
|
|
|
1879
1885
|
readonly precision?: number | undefined;
|
|
1880
1886
|
readonly required?: boolean | undefined;
|
|
1881
1887
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1888
|
+
readonly widget?: string | undefined;
|
|
1882
1889
|
readonly multiple?: boolean | undefined;
|
|
1883
1890
|
readonly dependencies?: string[] | undefined;
|
|
1884
1891
|
readonly externalId?: boolean | undefined;
|
|
@@ -2041,7 +2048,7 @@ declare const SysWebhook: Omit<{
|
|
|
2041
2048
|
readonly method: {
|
|
2042
2049
|
readonly readonly?: boolean | undefined;
|
|
2043
2050
|
readonly format?: string | undefined;
|
|
2044
|
-
|
|
2051
|
+
options: {
|
|
2045
2052
|
label: string;
|
|
2046
2053
|
value: string;
|
|
2047
2054
|
color?: string | undefined;
|
|
@@ -2055,13 +2062,14 @@ declare const SysWebhook: Omit<{
|
|
|
2055
2062
|
generatedBy?: string | undefined;
|
|
2056
2063
|
} | undefined;
|
|
2057
2064
|
} | undefined;
|
|
2058
|
-
}[]
|
|
2065
|
+
}[];
|
|
2059
2066
|
readonly description?: string | undefined;
|
|
2060
2067
|
readonly label?: string | undefined;
|
|
2061
2068
|
readonly name?: string | undefined;
|
|
2062
2069
|
readonly precision?: number | undefined;
|
|
2063
2070
|
readonly required?: boolean | undefined;
|
|
2064
2071
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2072
|
+
readonly widget?: string | undefined;
|
|
2065
2073
|
readonly multiple?: boolean | undefined;
|
|
2066
2074
|
readonly dependencies?: string[] | undefined;
|
|
2067
2075
|
readonly externalId?: boolean | undefined;
|
|
@@ -2219,7 +2227,7 @@ declare const SysWebhook: Omit<{
|
|
|
2219
2227
|
readonly inlineHelpText?: string | undefined;
|
|
2220
2228
|
readonly autonumberFormat?: string | undefined;
|
|
2221
2229
|
readonly index?: boolean | undefined;
|
|
2222
|
-
readonly type: "
|
|
2230
|
+
readonly type: "select";
|
|
2223
2231
|
};
|
|
2224
2232
|
readonly description: {
|
|
2225
2233
|
readonly readonly?: boolean | undefined;
|
|
@@ -2245,6 +2253,7 @@ declare const SysWebhook: Omit<{
|
|
|
2245
2253
|
readonly precision?: number | undefined;
|
|
2246
2254
|
readonly required?: boolean | undefined;
|
|
2247
2255
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2256
|
+
readonly widget?: string | undefined;
|
|
2248
2257
|
readonly multiple?: boolean | undefined;
|
|
2249
2258
|
readonly dependencies?: string[] | undefined;
|
|
2250
2259
|
readonly externalId?: boolean | undefined;
|
|
@@ -2428,6 +2437,7 @@ declare const SysWebhook: Omit<{
|
|
|
2428
2437
|
readonly precision?: number | undefined;
|
|
2429
2438
|
readonly required?: boolean | undefined;
|
|
2430
2439
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2440
|
+
readonly widget?: string | undefined;
|
|
2431
2441
|
readonly multiple?: boolean | undefined;
|
|
2432
2442
|
readonly dependencies?: string[] | undefined;
|
|
2433
2443
|
readonly externalId?: boolean | undefined;
|
|
@@ -2611,6 +2621,7 @@ declare const SysWebhook: Omit<{
|
|
|
2611
2621
|
readonly precision?: number | undefined;
|
|
2612
2622
|
readonly required?: boolean | undefined;
|
|
2613
2623
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2624
|
+
readonly widget?: string | undefined;
|
|
2614
2625
|
readonly multiple?: boolean | undefined;
|
|
2615
2626
|
readonly dependencies?: string[] | undefined;
|
|
2616
2627
|
readonly externalId?: boolean | undefined;
|
|
@@ -2794,6 +2805,7 @@ declare const SysWebhook: Omit<{
|
|
|
2794
2805
|
readonly precision?: number | undefined;
|
|
2795
2806
|
readonly required?: boolean | undefined;
|
|
2796
2807
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2808
|
+
readonly widget?: string | undefined;
|
|
2797
2809
|
readonly multiple?: boolean | undefined;
|
|
2798
2810
|
readonly dependencies?: string[] | undefined;
|
|
2799
2811
|
readonly externalId?: boolean | undefined;
|
|
@@ -2977,6 +2989,7 @@ declare const SysWebhook: Omit<{
|
|
|
2977
2989
|
readonly precision?: number | undefined;
|
|
2978
2990
|
readonly required?: boolean | undefined;
|
|
2979
2991
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2992
|
+
readonly widget?: string | undefined;
|
|
2980
2993
|
readonly multiple?: boolean | undefined;
|
|
2981
2994
|
readonly dependencies?: string[] | undefined;
|
|
2982
2995
|
readonly externalId?: boolean | undefined;
|
package/dist/schema.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/plugin-webhooks",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.8.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.",
|
|
6
6
|
"type": "module",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@objectstack/core": "14.
|
|
23
|
-
"@objectstack/service-messaging": "14.
|
|
24
|
-
"@objectstack/spec": "14.
|
|
22
|
+
"@objectstack/core": "14.8.0",
|
|
23
|
+
"@objectstack/service-messaging": "14.8.0",
|
|
24
|
+
"@objectstack/spec": "14.8.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.1",
|
|
@@ -205,6 +205,42 @@ describe('AutoEnqueuer', () => {
|
|
|
205
205
|
await ae.stop();
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
+
it('parses triggers authored as a multi-select array', async () => {
|
|
209
|
+
// The `triggers` field is now a multi-select stored as an array; the
|
|
210
|
+
// parser must treat it identically to the legacy CSV form.
|
|
211
|
+
const engine = new FakeEngine({ sys_webhook: [webhook({ triggers: ['create'] })] });
|
|
212
|
+
const realtime = new FakeRealtime();
|
|
213
|
+
const { enqueue, calls } = makeRecorder();
|
|
214
|
+
const ae = new AutoEnqueuer(engine, realtime, enqueue, { refreshIntervalMs: 0 });
|
|
215
|
+
await ae.start();
|
|
216
|
+
|
|
217
|
+
await realtime.publish(event('created', 'contact', { id: 'c-1' }));
|
|
218
|
+
await realtime.publish(event('updated', 'contact', { id: 'c-1' }, '2026-05-24T00:00:01.000Z'));
|
|
219
|
+
await flush();
|
|
220
|
+
|
|
221
|
+
expect(calls).toHaveLength(1);
|
|
222
|
+
expect(calls[0].label).toBe('data.record.created');
|
|
223
|
+
await ae.stop();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('parses triggers stored as a JSON-encoded array string', async () => {
|
|
227
|
+
// Some drivers hand a JSON array column back as a string — accept it.
|
|
228
|
+
const engine = new FakeEngine({ sys_webhook: [webhook({ triggers: '["create","update"]' })] });
|
|
229
|
+
const realtime = new FakeRealtime();
|
|
230
|
+
const { enqueue, calls } = makeRecorder();
|
|
231
|
+
const ae = new AutoEnqueuer(engine, realtime, enqueue, { refreshIntervalMs: 0 });
|
|
232
|
+
await ae.start();
|
|
233
|
+
|
|
234
|
+
await realtime.publish(event('created', 'contact', { id: 'c-1' }));
|
|
235
|
+
await realtime.publish(event('deleted', 'contact', { id: 'c-1' }, '2026-05-24T00:00:02.000Z'));
|
|
236
|
+
await flush();
|
|
237
|
+
|
|
238
|
+
// create + update are subscribed; delete is not.
|
|
239
|
+
expect(calls).toHaveLength(1);
|
|
240
|
+
expect(calls[0].label).toBe('data.record.created');
|
|
241
|
+
await ae.stop();
|
|
242
|
+
});
|
|
243
|
+
|
|
208
244
|
it('fans out to multiple matching webhooks', async () => {
|
|
209
245
|
const engine = new FakeEngine({
|
|
210
246
|
sys_webhook: [
|