@objectstack/plugin-webhooks 14.5.0 → 14.7.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 +39 -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 +28 -6
- package/dist/schema.d.ts +28 -6
- package/dist/schema.js +1 -1
- package/package.json +5 -5
- 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;
|
|
@@ -749,6 +750,15 @@ declare const SysWebhook: Omit<{
|
|
|
749
750
|
helpText?: string | undefined;
|
|
750
751
|
defaultValue?: unknown;
|
|
751
752
|
defaultFromRow?: boolean | undefined;
|
|
753
|
+
visible?: {
|
|
754
|
+
dialect: "cel" | "js" | "cron" | "template";
|
|
755
|
+
source?: string | undefined;
|
|
756
|
+
ast?: unknown;
|
|
757
|
+
meta?: {
|
|
758
|
+
rationale?: string | undefined;
|
|
759
|
+
generatedBy?: string | undefined;
|
|
760
|
+
} | undefined;
|
|
761
|
+
} | undefined;
|
|
752
762
|
}[] | undefined;
|
|
753
763
|
variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
|
|
754
764
|
order?: number | undefined;
|
|
@@ -955,6 +965,7 @@ declare const SysWebhook: Omit<{
|
|
|
955
965
|
readonly precision?: number | undefined;
|
|
956
966
|
readonly required?: boolean | undefined;
|
|
957
967
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
968
|
+
readonly widget?: string | undefined;
|
|
958
969
|
readonly multiple?: boolean | undefined;
|
|
959
970
|
readonly dependencies?: string[] | undefined;
|
|
960
971
|
readonly externalId?: boolean | undefined;
|
|
@@ -1138,6 +1149,7 @@ declare const SysWebhook: Omit<{
|
|
|
1138
1149
|
readonly precision?: number | undefined;
|
|
1139
1150
|
readonly required?: boolean | undefined;
|
|
1140
1151
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1152
|
+
readonly widget?: string | undefined;
|
|
1141
1153
|
readonly multiple?: boolean | undefined;
|
|
1142
1154
|
readonly dependencies?: string[] | undefined;
|
|
1143
1155
|
readonly externalId?: boolean | undefined;
|
|
@@ -1321,6 +1333,7 @@ declare const SysWebhook: Omit<{
|
|
|
1321
1333
|
readonly precision?: number | undefined;
|
|
1322
1334
|
readonly required?: boolean | undefined;
|
|
1323
1335
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1336
|
+
readonly widget?: string | undefined;
|
|
1324
1337
|
readonly multiple?: boolean | undefined;
|
|
1325
1338
|
readonly dependencies?: string[] | undefined;
|
|
1326
1339
|
readonly externalId?: boolean | undefined;
|
|
@@ -1504,6 +1517,7 @@ declare const SysWebhook: Omit<{
|
|
|
1504
1517
|
readonly precision?: number | undefined;
|
|
1505
1518
|
readonly required?: boolean | undefined;
|
|
1506
1519
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1520
|
+
readonly widget?: string | undefined;
|
|
1507
1521
|
readonly multiple?: boolean | undefined;
|
|
1508
1522
|
readonly dependencies?: string[] | undefined;
|
|
1509
1523
|
readonly externalId?: boolean | undefined;
|
|
@@ -1666,7 +1680,7 @@ declare const SysWebhook: Omit<{
|
|
|
1666
1680
|
readonly triggers: {
|
|
1667
1681
|
readonly readonly?: boolean | undefined;
|
|
1668
1682
|
readonly format?: string | undefined;
|
|
1669
|
-
|
|
1683
|
+
options: {
|
|
1670
1684
|
label: string;
|
|
1671
1685
|
value: string;
|
|
1672
1686
|
color?: string | undefined;
|
|
@@ -1680,13 +1694,14 @@ declare const SysWebhook: Omit<{
|
|
|
1680
1694
|
generatedBy?: string | undefined;
|
|
1681
1695
|
} | undefined;
|
|
1682
1696
|
} | undefined;
|
|
1683
|
-
}[]
|
|
1697
|
+
}[];
|
|
1684
1698
|
readonly description?: string | undefined;
|
|
1685
1699
|
readonly label?: string | undefined;
|
|
1686
1700
|
readonly name?: string | undefined;
|
|
1687
1701
|
readonly precision?: number | undefined;
|
|
1688
1702
|
readonly required?: boolean | undefined;
|
|
1689
1703
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1704
|
+
readonly widget?: string | undefined;
|
|
1690
1705
|
readonly multiple?: boolean | undefined;
|
|
1691
1706
|
readonly dependencies?: string[] | undefined;
|
|
1692
1707
|
readonly externalId?: boolean | undefined;
|
|
@@ -1844,7 +1859,7 @@ declare const SysWebhook: Omit<{
|
|
|
1844
1859
|
readonly inlineHelpText?: string | undefined;
|
|
1845
1860
|
readonly autonumberFormat?: string | undefined;
|
|
1846
1861
|
readonly index?: boolean | undefined;
|
|
1847
|
-
readonly type: "
|
|
1862
|
+
readonly type: "select";
|
|
1848
1863
|
};
|
|
1849
1864
|
readonly url: {
|
|
1850
1865
|
readonly readonly?: boolean | undefined;
|
|
@@ -1870,6 +1885,7 @@ declare const SysWebhook: Omit<{
|
|
|
1870
1885
|
readonly precision?: number | undefined;
|
|
1871
1886
|
readonly required?: boolean | undefined;
|
|
1872
1887
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1888
|
+
readonly widget?: string | undefined;
|
|
1873
1889
|
readonly multiple?: boolean | undefined;
|
|
1874
1890
|
readonly dependencies?: string[] | undefined;
|
|
1875
1891
|
readonly externalId?: boolean | undefined;
|
|
@@ -2032,7 +2048,7 @@ declare const SysWebhook: Omit<{
|
|
|
2032
2048
|
readonly method: {
|
|
2033
2049
|
readonly readonly?: boolean | undefined;
|
|
2034
2050
|
readonly format?: string | undefined;
|
|
2035
|
-
|
|
2051
|
+
options: {
|
|
2036
2052
|
label: string;
|
|
2037
2053
|
value: string;
|
|
2038
2054
|
color?: string | undefined;
|
|
@@ -2046,13 +2062,14 @@ declare const SysWebhook: Omit<{
|
|
|
2046
2062
|
generatedBy?: string | undefined;
|
|
2047
2063
|
} | undefined;
|
|
2048
2064
|
} | undefined;
|
|
2049
|
-
}[]
|
|
2065
|
+
}[];
|
|
2050
2066
|
readonly description?: string | undefined;
|
|
2051
2067
|
readonly label?: string | undefined;
|
|
2052
2068
|
readonly name?: string | undefined;
|
|
2053
2069
|
readonly precision?: number | undefined;
|
|
2054
2070
|
readonly required?: boolean | undefined;
|
|
2055
2071
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2072
|
+
readonly widget?: string | undefined;
|
|
2056
2073
|
readonly multiple?: boolean | undefined;
|
|
2057
2074
|
readonly dependencies?: string[] | undefined;
|
|
2058
2075
|
readonly externalId?: boolean | undefined;
|
|
@@ -2210,7 +2227,7 @@ declare const SysWebhook: Omit<{
|
|
|
2210
2227
|
readonly inlineHelpText?: string | undefined;
|
|
2211
2228
|
readonly autonumberFormat?: string | undefined;
|
|
2212
2229
|
readonly index?: boolean | undefined;
|
|
2213
|
-
readonly type: "
|
|
2230
|
+
readonly type: "select";
|
|
2214
2231
|
};
|
|
2215
2232
|
readonly description: {
|
|
2216
2233
|
readonly readonly?: boolean | undefined;
|
|
@@ -2236,6 +2253,7 @@ declare const SysWebhook: Omit<{
|
|
|
2236
2253
|
readonly precision?: number | undefined;
|
|
2237
2254
|
readonly required?: boolean | undefined;
|
|
2238
2255
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2256
|
+
readonly widget?: string | undefined;
|
|
2239
2257
|
readonly multiple?: boolean | undefined;
|
|
2240
2258
|
readonly dependencies?: string[] | undefined;
|
|
2241
2259
|
readonly externalId?: boolean | undefined;
|
|
@@ -2419,6 +2437,7 @@ declare const SysWebhook: Omit<{
|
|
|
2419
2437
|
readonly precision?: number | undefined;
|
|
2420
2438
|
readonly required?: boolean | undefined;
|
|
2421
2439
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2440
|
+
readonly widget?: string | undefined;
|
|
2422
2441
|
readonly multiple?: boolean | undefined;
|
|
2423
2442
|
readonly dependencies?: string[] | undefined;
|
|
2424
2443
|
readonly externalId?: boolean | undefined;
|
|
@@ -2602,6 +2621,7 @@ declare const SysWebhook: Omit<{
|
|
|
2602
2621
|
readonly precision?: number | undefined;
|
|
2603
2622
|
readonly required?: boolean | undefined;
|
|
2604
2623
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2624
|
+
readonly widget?: string | undefined;
|
|
2605
2625
|
readonly multiple?: boolean | undefined;
|
|
2606
2626
|
readonly dependencies?: string[] | undefined;
|
|
2607
2627
|
readonly externalId?: boolean | undefined;
|
|
@@ -2785,6 +2805,7 @@ declare const SysWebhook: Omit<{
|
|
|
2785
2805
|
readonly precision?: number | undefined;
|
|
2786
2806
|
readonly required?: boolean | undefined;
|
|
2787
2807
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2808
|
+
readonly widget?: string | undefined;
|
|
2788
2809
|
readonly multiple?: boolean | undefined;
|
|
2789
2810
|
readonly dependencies?: string[] | undefined;
|
|
2790
2811
|
readonly externalId?: boolean | undefined;
|
|
@@ -2968,6 +2989,7 @@ declare const SysWebhook: Omit<{
|
|
|
2968
2989
|
readonly precision?: number | undefined;
|
|
2969
2990
|
readonly required?: boolean | undefined;
|
|
2970
2991
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2992
|
+
readonly widget?: string | undefined;
|
|
2971
2993
|
readonly multiple?: boolean | undefined;
|
|
2972
2994
|
readonly dependencies?: string[] | undefined;
|
|
2973
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;
|
|
@@ -749,6 +750,15 @@ declare const SysWebhook: Omit<{
|
|
|
749
750
|
helpText?: string | undefined;
|
|
750
751
|
defaultValue?: unknown;
|
|
751
752
|
defaultFromRow?: boolean | undefined;
|
|
753
|
+
visible?: {
|
|
754
|
+
dialect: "cel" | "js" | "cron" | "template";
|
|
755
|
+
source?: string | undefined;
|
|
756
|
+
ast?: unknown;
|
|
757
|
+
meta?: {
|
|
758
|
+
rationale?: string | undefined;
|
|
759
|
+
generatedBy?: string | undefined;
|
|
760
|
+
} | undefined;
|
|
761
|
+
} | undefined;
|
|
752
762
|
}[] | undefined;
|
|
753
763
|
variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
|
|
754
764
|
order?: number | undefined;
|
|
@@ -955,6 +965,7 @@ declare const SysWebhook: Omit<{
|
|
|
955
965
|
readonly precision?: number | undefined;
|
|
956
966
|
readonly required?: boolean | undefined;
|
|
957
967
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
968
|
+
readonly widget?: string | undefined;
|
|
958
969
|
readonly multiple?: boolean | undefined;
|
|
959
970
|
readonly dependencies?: string[] | undefined;
|
|
960
971
|
readonly externalId?: boolean | undefined;
|
|
@@ -1138,6 +1149,7 @@ declare const SysWebhook: Omit<{
|
|
|
1138
1149
|
readonly precision?: number | undefined;
|
|
1139
1150
|
readonly required?: boolean | undefined;
|
|
1140
1151
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1152
|
+
readonly widget?: string | undefined;
|
|
1141
1153
|
readonly multiple?: boolean | undefined;
|
|
1142
1154
|
readonly dependencies?: string[] | undefined;
|
|
1143
1155
|
readonly externalId?: boolean | undefined;
|
|
@@ -1321,6 +1333,7 @@ declare const SysWebhook: Omit<{
|
|
|
1321
1333
|
readonly precision?: number | undefined;
|
|
1322
1334
|
readonly required?: boolean | undefined;
|
|
1323
1335
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1336
|
+
readonly widget?: string | undefined;
|
|
1324
1337
|
readonly multiple?: boolean | undefined;
|
|
1325
1338
|
readonly dependencies?: string[] | undefined;
|
|
1326
1339
|
readonly externalId?: boolean | undefined;
|
|
@@ -1504,6 +1517,7 @@ declare const SysWebhook: Omit<{
|
|
|
1504
1517
|
readonly precision?: number | undefined;
|
|
1505
1518
|
readonly required?: boolean | undefined;
|
|
1506
1519
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1520
|
+
readonly widget?: string | undefined;
|
|
1507
1521
|
readonly multiple?: boolean | undefined;
|
|
1508
1522
|
readonly dependencies?: string[] | undefined;
|
|
1509
1523
|
readonly externalId?: boolean | undefined;
|
|
@@ -1666,7 +1680,7 @@ declare const SysWebhook: Omit<{
|
|
|
1666
1680
|
readonly triggers: {
|
|
1667
1681
|
readonly readonly?: boolean | undefined;
|
|
1668
1682
|
readonly format?: string | undefined;
|
|
1669
|
-
|
|
1683
|
+
options: {
|
|
1670
1684
|
label: string;
|
|
1671
1685
|
value: string;
|
|
1672
1686
|
color?: string | undefined;
|
|
@@ -1680,13 +1694,14 @@ declare const SysWebhook: Omit<{
|
|
|
1680
1694
|
generatedBy?: string | undefined;
|
|
1681
1695
|
} | undefined;
|
|
1682
1696
|
} | undefined;
|
|
1683
|
-
}[]
|
|
1697
|
+
}[];
|
|
1684
1698
|
readonly description?: string | undefined;
|
|
1685
1699
|
readonly label?: string | undefined;
|
|
1686
1700
|
readonly name?: string | undefined;
|
|
1687
1701
|
readonly precision?: number | undefined;
|
|
1688
1702
|
readonly required?: boolean | undefined;
|
|
1689
1703
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1704
|
+
readonly widget?: string | undefined;
|
|
1690
1705
|
readonly multiple?: boolean | undefined;
|
|
1691
1706
|
readonly dependencies?: string[] | undefined;
|
|
1692
1707
|
readonly externalId?: boolean | undefined;
|
|
@@ -1844,7 +1859,7 @@ declare const SysWebhook: Omit<{
|
|
|
1844
1859
|
readonly inlineHelpText?: string | undefined;
|
|
1845
1860
|
readonly autonumberFormat?: string | undefined;
|
|
1846
1861
|
readonly index?: boolean | undefined;
|
|
1847
|
-
readonly type: "
|
|
1862
|
+
readonly type: "select";
|
|
1848
1863
|
};
|
|
1849
1864
|
readonly url: {
|
|
1850
1865
|
readonly readonly?: boolean | undefined;
|
|
@@ -1870,6 +1885,7 @@ declare const SysWebhook: Omit<{
|
|
|
1870
1885
|
readonly precision?: number | undefined;
|
|
1871
1886
|
readonly required?: boolean | undefined;
|
|
1872
1887
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
1888
|
+
readonly widget?: string | undefined;
|
|
1873
1889
|
readonly multiple?: boolean | undefined;
|
|
1874
1890
|
readonly dependencies?: string[] | undefined;
|
|
1875
1891
|
readonly externalId?: boolean | undefined;
|
|
@@ -2032,7 +2048,7 @@ declare const SysWebhook: Omit<{
|
|
|
2032
2048
|
readonly method: {
|
|
2033
2049
|
readonly readonly?: boolean | undefined;
|
|
2034
2050
|
readonly format?: string | undefined;
|
|
2035
|
-
|
|
2051
|
+
options: {
|
|
2036
2052
|
label: string;
|
|
2037
2053
|
value: string;
|
|
2038
2054
|
color?: string | undefined;
|
|
@@ -2046,13 +2062,14 @@ declare const SysWebhook: Omit<{
|
|
|
2046
2062
|
generatedBy?: string | undefined;
|
|
2047
2063
|
} | undefined;
|
|
2048
2064
|
} | undefined;
|
|
2049
|
-
}[]
|
|
2065
|
+
}[];
|
|
2050
2066
|
readonly description?: string | undefined;
|
|
2051
2067
|
readonly label?: string | undefined;
|
|
2052
2068
|
readonly name?: string | undefined;
|
|
2053
2069
|
readonly precision?: number | undefined;
|
|
2054
2070
|
readonly required?: boolean | undefined;
|
|
2055
2071
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2072
|
+
readonly widget?: string | undefined;
|
|
2056
2073
|
readonly multiple?: boolean | undefined;
|
|
2057
2074
|
readonly dependencies?: string[] | undefined;
|
|
2058
2075
|
readonly externalId?: boolean | undefined;
|
|
@@ -2210,7 +2227,7 @@ declare const SysWebhook: Omit<{
|
|
|
2210
2227
|
readonly inlineHelpText?: string | undefined;
|
|
2211
2228
|
readonly autonumberFormat?: string | undefined;
|
|
2212
2229
|
readonly index?: boolean | undefined;
|
|
2213
|
-
readonly type: "
|
|
2230
|
+
readonly type: "select";
|
|
2214
2231
|
};
|
|
2215
2232
|
readonly description: {
|
|
2216
2233
|
readonly readonly?: boolean | undefined;
|
|
@@ -2236,6 +2253,7 @@ declare const SysWebhook: Omit<{
|
|
|
2236
2253
|
readonly precision?: number | undefined;
|
|
2237
2254
|
readonly required?: boolean | undefined;
|
|
2238
2255
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2256
|
+
readonly widget?: string | undefined;
|
|
2239
2257
|
readonly multiple?: boolean | undefined;
|
|
2240
2258
|
readonly dependencies?: string[] | undefined;
|
|
2241
2259
|
readonly externalId?: boolean | undefined;
|
|
@@ -2419,6 +2437,7 @@ declare const SysWebhook: Omit<{
|
|
|
2419
2437
|
readonly precision?: number | undefined;
|
|
2420
2438
|
readonly required?: boolean | undefined;
|
|
2421
2439
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2440
|
+
readonly widget?: string | undefined;
|
|
2422
2441
|
readonly multiple?: boolean | undefined;
|
|
2423
2442
|
readonly dependencies?: string[] | undefined;
|
|
2424
2443
|
readonly externalId?: boolean | undefined;
|
|
@@ -2602,6 +2621,7 @@ declare const SysWebhook: Omit<{
|
|
|
2602
2621
|
readonly precision?: number | undefined;
|
|
2603
2622
|
readonly required?: boolean | undefined;
|
|
2604
2623
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2624
|
+
readonly widget?: string | undefined;
|
|
2605
2625
|
readonly multiple?: boolean | undefined;
|
|
2606
2626
|
readonly dependencies?: string[] | undefined;
|
|
2607
2627
|
readonly externalId?: boolean | undefined;
|
|
@@ -2785,6 +2805,7 @@ declare const SysWebhook: Omit<{
|
|
|
2785
2805
|
readonly precision?: number | undefined;
|
|
2786
2806
|
readonly required?: boolean | undefined;
|
|
2787
2807
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2808
|
+
readonly widget?: string | undefined;
|
|
2788
2809
|
readonly multiple?: boolean | undefined;
|
|
2789
2810
|
readonly dependencies?: string[] | undefined;
|
|
2790
2811
|
readonly externalId?: boolean | undefined;
|
|
@@ -2968,6 +2989,7 @@ declare const SysWebhook: Omit<{
|
|
|
2968
2989
|
readonly precision?: number | undefined;
|
|
2969
2990
|
readonly required?: boolean | undefined;
|
|
2970
2991
|
readonly returnType?: "number" | "boolean" | "date" | "text" | undefined;
|
|
2992
|
+
readonly widget?: string | undefined;
|
|
2971
2993
|
readonly multiple?: boolean | undefined;
|
|
2972
2994
|
readonly dependencies?: string[] | undefined;
|
|
2973
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.7.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,12 +19,12 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@objectstack/core": "14.
|
|
23
|
-
"@objectstack/service-messaging": "14.
|
|
24
|
-
"@objectstack/spec": "14.
|
|
22
|
+
"@objectstack/core": "14.7.0",
|
|
23
|
+
"@objectstack/service-messaging": "14.7.0",
|
|
24
|
+
"@objectstack/spec": "14.7.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@types/node": "^26.1.
|
|
27
|
+
"@types/node": "^26.1.1",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
30
30
|
},
|
|
@@ -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: [
|
package/src/auto-enqueuer.ts
CHANGED
|
@@ -205,11 +205,30 @@ export class AutoEnqueuer {
|
|
|
205
205
|
|
|
206
206
|
private parseRow(row: any): CachedSubscription | null {
|
|
207
207
|
if (!row?.id || !row?.url) return null;
|
|
208
|
-
|
|
208
|
+
// `triggers` is now authored as a multi-select (stored as an array), but
|
|
209
|
+
// legacy rows stored a comma-separated string (and some drivers hand a
|
|
210
|
+
// JSON-encoded array back as a string). Accept all three shapes so a
|
|
211
|
+
// schema change never silently drops a subscription's events.
|
|
212
|
+
const rawTriggers = row.triggers;
|
|
213
|
+
let triggerList: string[];
|
|
214
|
+
if (Array.isArray(rawTriggers)) {
|
|
215
|
+
triggerList = rawTriggers.map((t) => String(t));
|
|
216
|
+
} else {
|
|
217
|
+
const s = String(rawTriggers ?? '').trim();
|
|
218
|
+
if (s.startsWith('[')) {
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(s);
|
|
221
|
+
triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];
|
|
222
|
+
} catch {
|
|
223
|
+
triggerList = s.split(',');
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
triggerList = s.split(',');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
209
229
|
const triggers = new Set(
|
|
210
|
-
|
|
211
|
-
.
|
|
212
|
-
.map((s: string) => s.trim().toLowerCase())
|
|
230
|
+
triggerList
|
|
231
|
+
.map((t) => t.trim().toLowerCase())
|
|
213
232
|
.filter(Boolean) as Array<'create' | 'update' | 'delete' | 'undelete'>,
|
|
214
233
|
);
|
|
215
234
|
if (triggers.size === 0) {
|
|
@@ -235,7 +254,11 @@ export class AutoEnqueuer {
|
|
|
235
254
|
objectName: row.object_name ? String(row.object_name) : undefined,
|
|
236
255
|
triggers,
|
|
237
256
|
url: String(row.url),
|
|
238
|
-
|
|
257
|
+
// Method is authored via a select whose option values are lowercased
|
|
258
|
+
// (get/post/…); upper-case here so delivery uses a canonical HTTP
|
|
259
|
+
// method regardless of whether the row was authored before or after
|
|
260
|
+
// the select change (legacy rows stored 'POST').
|
|
261
|
+
method: String(row.method ?? defn.method ?? 'POST').toUpperCase(),
|
|
239
262
|
headers: defn.headers,
|
|
240
263
|
secret: defn.secret,
|
|
241
264
|
timeoutMs: defn.timeoutMs,
|