@open-mercato/webhooks 0.5.1-develop.2672.g2128128275 → 0.5.1-develop.2681.c559bb2bc3
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/dist/modules/webhooks/api/events/route.js +1 -1
- package/dist/modules/webhooks/api/events/route.js.map +2 -2
- package/dist/modules/webhooks/subscribers/outbound-dispatch.js +7 -0
- package/dist/modules/webhooks/subscribers/outbound-dispatch.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/webhooks/api/events/__tests__/route.test.ts +44 -0
- package/src/modules/webhooks/api/events/route.ts +1 -1
- package/src/modules/webhooks/subscribers/__tests__/outbound-dispatch.test.ts +52 -0
- package/src/modules/webhooks/subscribers/outbound-dispatch.ts +10 -0
|
@@ -18,7 +18,7 @@ const eventsResponseSchema = z.object({
|
|
|
18
18
|
total: z.number().int().nonnegative()
|
|
19
19
|
});
|
|
20
20
|
async function GET() {
|
|
21
|
-
const events = getDeclaredEvents().filter((event) => !event.id.startsWith("webhooks.")).sort((left, right) => left.id.localeCompare(right.id));
|
|
21
|
+
const events = getDeclaredEvents().filter((event) => !event.id.startsWith("webhooks.") && !event.excludeFromTriggers).sort((left, right) => left.id.localeCompare(right.id));
|
|
22
22
|
return json({
|
|
23
23
|
data: events,
|
|
24
24
|
total: events.length
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/webhooks/api/events/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getDeclaredEvents } from '@open-mercato/shared/modules/events'\nimport { json } from '../helpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n}\n\nconst eventDefinitionSchema = z.object({\n id: z.string(),\n label: z.string(),\n description: z.string().optional(),\n category: z.enum(['crud', 'lifecycle', 'system', 'custom']).optional(),\n module: z.string().optional(),\n entity: z.string().optional(),\n excludeFromTriggers: z.boolean().optional(),\n})\n\nconst eventsResponseSchema = z.object({\n data: z.array(eventDefinitionSchema),\n total: z.number().int().nonnegative(),\n})\n\nexport async function GET(): Promise<Response> {\n const events = getDeclaredEvents()\n .filter((event) => !event.id.startsWith('webhooks.'))\n .sort((left, right) => left.id.localeCompare(right.id))\n\n return json({\n data: events,\n total: events.length,\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'List available webhook events',\n description: 'Returns all declared platform events that can be subscribed to by webhook endpoints.',\n methods: {\n GET: {\n summary: 'List webhook events',\n description: 'Returns all declared non-webhook events, sorted by event id.',\n responses: [{ status: 200, description: 'Available events', schema: eventsResponseSchema }],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,yBAAyB;AAClC,SAAS,YAAY;AAEd,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAC/D;AAEA,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,IAAI,EAAE,OAAO;AAAA,EACb,OAAO,EAAE,OAAO;AAAA,EAChB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,KAAK,CAAC,QAAQ,aAAa,UAAU,QAAQ,CAAC,EAAE,SAAS;AAAA,EACrE,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,qBAAqB,EAAE,QAAQ,EAAE,SAAS;AAC5C,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,MAAM,qBAAqB;AAAA,EACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AACtC,CAAC;AAED,eAAsB,MAAyB;AAC7C,QAAM,SAAS,kBAAkB,EAC9B,OAAO,CAAC,UAAU,CAAC,MAAM,GAAG,WAAW,WAAW,CAAC,
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getDeclaredEvents } from '@open-mercato/shared/modules/events'\nimport { json } from '../helpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['webhooks.view'] },\n}\n\nconst eventDefinitionSchema = z.object({\n id: z.string(),\n label: z.string(),\n description: z.string().optional(),\n category: z.enum(['crud', 'lifecycle', 'system', 'custom']).optional(),\n module: z.string().optional(),\n entity: z.string().optional(),\n excludeFromTriggers: z.boolean().optional(),\n})\n\nconst eventsResponseSchema = z.object({\n data: z.array(eventDefinitionSchema),\n total: z.number().int().nonnegative(),\n})\n\nexport async function GET(): Promise<Response> {\n const events = getDeclaredEvents()\n .filter((event) => !event.id.startsWith('webhooks.') && !event.excludeFromTriggers)\n .sort((left, right) => left.id.localeCompare(right.id))\n\n return json({\n data: events,\n total: events.length,\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'List available webhook events',\n description: 'Returns all declared platform events that can be subscribed to by webhook endpoints.',\n methods: {\n GET: {\n summary: 'List webhook events',\n description: 'Returns all declared non-webhook events, sorted by event id.',\n responses: [{ status: 200, description: 'Available events', schema: eventsResponseSchema }],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,yBAAyB;AAClC,SAAS,YAAY;AAEd,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAC/D;AAEA,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,IAAI,EAAE,OAAO;AAAA,EACb,OAAO,EAAE,OAAO;AAAA,EAChB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,KAAK,CAAC,QAAQ,aAAa,UAAU,QAAQ,CAAC,EAAE,SAAS;AAAA,EACrE,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,qBAAqB,EAAE,QAAQ,EAAE,SAAS;AAC5C,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,MAAM,qBAAqB;AAAA,EACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AACtC,CAAC;AAED,eAAsB,MAAyB;AAC7C,QAAM,SAAS,kBAAkB,EAC9B,OAAO,CAAC,UAAU,CAAC,MAAM,GAAG,WAAW,WAAW,KAAK,CAAC,MAAM,mBAAmB,EACjF,KAAK,CAAC,MAAM,UAAU,KAAK,GAAG,cAAc,MAAM,EAAE,CAAC;AAExD,SAAO,KAAK;AAAA,IACV,MAAM;AAAA,IACN,OAAO,OAAO;AAAA,EAChB,CAAC;AACH;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,qBAAqB,CAAC;AAAA,IAC5F;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WebhookDeliveryEntity, WebhookEntity } from "../data/entities.js";
|
|
2
2
|
import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
3
|
import { matchAnyWebhookEventPattern } from "@open-mercato/shared/lib/events/patterns";
|
|
4
|
+
import { getDeclaredEvents } from "@open-mercato/shared/modules/events";
|
|
4
5
|
import { createWebhookDelivery } from "../lib/delivery.js";
|
|
5
6
|
import { enqueueWebhookDelivery } from "../lib/queue.js";
|
|
6
7
|
import { isWebhookIntegrationEnabled } from "../lib/integration-state.js";
|
|
@@ -9,9 +10,15 @@ const metadata = {
|
|
|
9
10
|
persistent: true,
|
|
10
11
|
id: "webhooks:outbound-dispatch"
|
|
11
12
|
};
|
|
13
|
+
function shouldSkipOutboundDispatch(eventId) {
|
|
14
|
+
if (eventId.startsWith("webhooks.") || eventId.startsWith("application.")) return true;
|
|
15
|
+
const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId);
|
|
16
|
+
return declaredEvent?.excludeFromTriggers === true;
|
|
17
|
+
}
|
|
12
18
|
async function handler(payload, ctx) {
|
|
13
19
|
const eventId = ctx.eventId ?? ctx.eventName ?? payload.eventId ?? payload.type;
|
|
14
20
|
if (!eventId) return;
|
|
21
|
+
if (shouldSkipOutboundDispatch(eventId)) return;
|
|
15
22
|
const tenantId = payload.tenantId;
|
|
16
23
|
const organizationId = payload.organizationId;
|
|
17
24
|
if (!tenantId) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/webhooks/subscribers/outbound-dispatch.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SubscriberContext } from '@open-mercato/events/types'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { matchAnyWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { createWebhookDelivery } from '../lib/delivery'\nimport { enqueueWebhookDelivery } from '../lib/queue'\nimport { isWebhookIntegrationEnabled } from '../lib/integration-state'\n\nexport const metadata = {\n event: '*',\n persistent: true,\n id: 'webhooks:outbound-dispatch',\n}\n\nexport default async function handler(\n payload: Record<string, unknown>,\n ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },\n) {\n const eventId = ctx.eventId ?? ctx.eventName ?? (payload.eventId as string) ?? (payload.type as string)\n if (!eventId) return\n\n const tenantId = payload.tenantId as string | undefined\n const organizationId = payload.organizationId as string | undefined\n if (!tenantId) return\n\n if (eventId.startsWith('webhooks.')) return\n if (eventId.startsWith('query_index.')) return\n\n const resolve = ('resolve' in ctx && typeof ctx.resolve === 'function')\n ? ctx.resolve\n : ('container' in ctx && ctx.container && typeof ctx.container.resolve === 'function')\n ? ctx.container.resolve.bind(ctx.container)\n : null\n\n if (!resolve) return\n\n const em = (resolve('em') as EntityManager).fork()\n\n const webhooks = await findWithDecryption(\n em,\n WebhookEntity,\n {\n isActive: true,\n deletedAt: null,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n },\n {},\n { tenantId, organizationId: organizationId ?? '' },\n )\n\n if (!webhooks.length) return\n\n const matchingWebhooks = webhooks.filter((webhook) =>\n matchAnyWebhookEventPattern(eventId, webhook.subscribedEvents),\n )\n\n if (!matchingWebhooks.length) return\n\n for (const webhook of matchingWebhooks) {\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n\n if (!integrationEnabled) continue\n\n let createdDeliveryId: string | null = null\n try {\n const delivery = await createWebhookDelivery({\n em,\n webhook,\n eventId,\n payload,\n })\n createdDeliveryId = delivery.id\n\n await enqueueWebhookDelivery({\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n })\n } catch (error) {\n if (createdDeliveryId) {\n const failedDelivery = await findOneWithDecryption(em, WebhookDeliveryEntity, { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId }, undefined, { tenantId: webhook.tenantId, organizationId: webhook.organizationId })\n if (failedDelivery) {\n failedDelivery.status = 'failed'\n failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : 'Queue enqueue failed'\n failedDelivery.nextRetryAt = null\n await em.flush()\n }\n }\n console.error('[webhooks] Failed to enqueue outbound delivery', {\n webhookId: webhook.id,\n eventId,\n tenantId,\n organizationId: organizationId ?? webhook.organizationId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,uBAAuB,qBAAqB;AACrD,SAAS,oBAAoB,6BAA6B;AAC1D,SAAS,mCAAmC;AAC5C,SAAS,6BAA6B;AACtC,SAAS,8BAA8B;AACvC,SAAS,mCAAmC;AAErC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,QACL,SACA,KACA;AACA,QAAM,UAAU,IAAI,WAAW,IAAI,aAAc,QAAQ,WAAuB,QAAQ;AACxF,MAAI,CAAC,QAAS;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SubscriberContext } from '@open-mercato/events/types'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { matchAnyWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { getDeclaredEvents } from '@open-mercato/shared/modules/events'\nimport { createWebhookDelivery } from '../lib/delivery'\nimport { enqueueWebhookDelivery } from '../lib/queue'\nimport { isWebhookIntegrationEnabled } from '../lib/integration-state'\n\nexport const metadata = {\n event: '*',\n persistent: true,\n id: 'webhooks:outbound-dispatch',\n}\n\nfunction shouldSkipOutboundDispatch(eventId: string): boolean {\n if (eventId.startsWith('webhooks.') || eventId.startsWith('application.')) return true\n\n const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId)\n return declaredEvent?.excludeFromTriggers === true\n}\n\nexport default async function handler(\n payload: Record<string, unknown>,\n ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },\n) {\n const eventId = ctx.eventId ?? ctx.eventName ?? (payload.eventId as string) ?? (payload.type as string)\n if (!eventId) return\n if (shouldSkipOutboundDispatch(eventId)) return\n\n const tenantId = payload.tenantId as string | undefined\n const organizationId = payload.organizationId as string | undefined\n if (!tenantId) return\n\n if (eventId.startsWith('webhooks.')) return\n if (eventId.startsWith('query_index.')) return\n\n\n const resolve = ('resolve' in ctx && typeof ctx.resolve === 'function')\n ? ctx.resolve\n : ('container' in ctx && ctx.container && typeof ctx.container.resolve === 'function')\n ? ctx.container.resolve.bind(ctx.container)\n : null\n\n if (!resolve) return\n\n const em = (resolve('em') as EntityManager).fork()\n\n const webhooks = await findWithDecryption(\n em,\n WebhookEntity,\n {\n isActive: true,\n deletedAt: null,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n },\n {},\n { tenantId, organizationId: organizationId ?? '' },\n )\n\n if (!webhooks.length) return\n\n const matchingWebhooks = webhooks.filter((webhook) =>\n matchAnyWebhookEventPattern(eventId, webhook.subscribedEvents),\n )\n\n if (!matchingWebhooks.length) return\n\n for (const webhook of matchingWebhooks) {\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n\n if (!integrationEnabled) continue\n\n let createdDeliveryId: string | null = null\n try {\n const delivery = await createWebhookDelivery({\n em,\n webhook,\n eventId,\n payload,\n })\n createdDeliveryId = delivery.id\n\n await enqueueWebhookDelivery({\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n })\n } catch (error) {\n if (createdDeliveryId) {\n const failedDelivery = await findOneWithDecryption(em, WebhookDeliveryEntity, { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId }, undefined, { tenantId: webhook.tenantId, organizationId: webhook.organizationId })\n if (failedDelivery) {\n failedDelivery.status = 'failed'\n failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : 'Queue enqueue failed'\n failedDelivery.nextRetryAt = null\n await em.flush()\n }\n }\n console.error('[webhooks] Failed to enqueue outbound delivery', {\n webhookId: webhook.id,\n eventId,\n tenantId,\n organizationId: organizationId ?? webhook.organizationId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,uBAAuB,qBAAqB;AACrD,SAAS,oBAAoB,6BAA6B;AAC1D,SAAS,mCAAmC;AAC5C,SAAS,yBAAyB;AAClC,SAAS,6BAA6B;AACtC,SAAS,8BAA8B;AACvC,SAAS,mCAAmC;AAErC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,SAAS,2BAA2B,SAA0B;AAC5D,MAAI,QAAQ,WAAW,WAAW,KAAK,QAAQ,WAAW,cAAc,EAAG,QAAO;AAElF,QAAM,gBAAgB,kBAAkB,EAAE,KAAK,CAAC,UAAU,MAAM,OAAO,OAAO;AAC9E,SAAO,eAAe,wBAAwB;AAChD;AAEA,eAAO,QACL,SACA,KACA;AACA,QAAM,UAAU,IAAI,WAAW,IAAI,aAAc,QAAQ,WAAuB,QAAQ;AACxF,MAAI,CAAC,QAAS;AACd,MAAI,2BAA2B,OAAO,EAAG;AAEzC,QAAM,WAAW,QAAQ;AACzB,QAAM,iBAAiB,QAAQ;AAC/B,MAAI,CAAC,SAAU;AAEf,MAAI,QAAQ,WAAW,WAAW,EAAG;AACrC,MAAI,QAAQ,WAAW,cAAc,EAAG;AAGxC,QAAM,UAAW,aAAa,OAAO,OAAO,IAAI,YAAY,aACxD,IAAI,UACH,eAAe,OAAO,IAAI,aAAa,OAAO,IAAI,UAAU,YAAY,aACvE,IAAI,UAAU,QAAQ,KAAK,IAAI,SAAS,IACxC;AAEN,MAAI,CAAC,QAAS;AAEd,QAAM,KAAM,QAAQ,IAAI,EAAoB,KAAK;AAEjD,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,WAAW;AAAA,MACX;AAAA,MACA,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;AAAA,IAC7C;AAAA,IACA,CAAC;AAAA,IACD,EAAE,UAAU,gBAAgB,kBAAkB,GAAG;AAAA,EACnD;AAEA,MAAI,CAAC,SAAS,OAAQ;AAEtB,QAAM,mBAAmB,SAAS;AAAA,IAAO,CAAC,YACxC,4BAA4B,SAAS,QAAQ,gBAAgB;AAAA,EAC/D;AAEA,MAAI,CAAC,iBAAiB,OAAQ;AAE9B,aAAW,WAAW,kBAAkB;AACtC,UAAM,qBAAqB,MAAM,4BAA4B,IAAI;AAAA,MAC/D,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,IAC1B,CAAC;AAED,QAAI,CAAC,mBAAoB;AAEzB,QAAI,oBAAmC;AACvC,QAAI;AACF,YAAM,WAAW,MAAM,sBAAsB;AAAA,QAC3C;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,0BAAoB,SAAS;AAE7B,YAAM,uBAAuB;AAAA,QAC3B,YAAY,SAAS;AAAA,QACrB,UAAU,SAAS;AAAA,QACnB,gBAAgB,SAAS;AAAA,MAC3B,CAAC;AAAA,IACH,SAAS,OAAO;AACd,UAAI,mBAAmB;AACrB,cAAM,iBAAiB,MAAM,sBAAsB,IAAI,uBAAuB,EAAE,IAAI,mBAAmB,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,eAAe,GAAG,QAAW,EAAE,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,eAAe,CAAC;AAC9P,YAAI,gBAAgB;AAClB,yBAAe,SAAS;AACxB,yBAAe,eAAe,iBAAiB,QAAQ,yBAAyB,MAAM,OAAO,KAAK;AAClG,yBAAe,cAAc;AAC7B,gBAAM,GAAG,MAAM;AAAA,QACjB;AAAA,MACF;AACA,cAAQ,MAAM,kDAAkD;AAAA,QAC9D,WAAW,QAAQ;AAAA,QACnB;AAAA,QACA;AAAA,QACA,gBAAgB,kBAAkB,QAAQ;AAAA,QAC1C,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/webhooks",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.2681.c559bb2bc3",
|
|
4
4
|
"description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -69,18 +69,18 @@
|
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@open-mercato/core": "0.5.1-develop.
|
|
73
|
-
"@open-mercato/queue": "0.5.1-develop.
|
|
74
|
-
"@open-mercato/ui": "0.5.1-develop.
|
|
72
|
+
"@open-mercato/core": "0.5.1-develop.2681.c559bb2bc3",
|
|
73
|
+
"@open-mercato/queue": "0.5.1-develop.2681.c559bb2bc3",
|
|
74
|
+
"@open-mercato/ui": "0.5.1-develop.2681.c559bb2bc3"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"@mikro-orm/postgresql": "^6.6.10",
|
|
78
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
78
|
+
"@open-mercato/shared": "0.5.1-develop.2681.c559bb2bc3",
|
|
79
79
|
"react": "^19.0.0",
|
|
80
80
|
"react-dom": "^19.0.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
83
|
+
"@open-mercato/shared": "0.5.1-develop.2681.c559bb2bc3",
|
|
84
84
|
"@types/jest": "^30.0.0",
|
|
85
85
|
"esbuild": "^0.28.0",
|
|
86
86
|
"glob": "^13.0.6",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { GET } from '../route'
|
|
2
|
+
|
|
3
|
+
const getDeclaredEventsMock = jest.fn()
|
|
4
|
+
|
|
5
|
+
jest.mock('@open-mercato/shared/modules/events', () => ({
|
|
6
|
+
getDeclaredEvents: () => getDeclaredEventsMock(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
describe('webhooks events route', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('filters webhook and trigger-excluded events from the response', async () => {
|
|
15
|
+
getDeclaredEventsMock.mockReturnValue([
|
|
16
|
+
{
|
|
17
|
+
id: 'catalog.product.created',
|
|
18
|
+
label: 'Product created',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'sales.document.calculate.before',
|
|
22
|
+
label: 'Before document calculate',
|
|
23
|
+
excludeFromTriggers: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'webhooks.delivery.succeeded',
|
|
27
|
+
label: 'Webhook delivery succeeded',
|
|
28
|
+
},
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const response = await GET()
|
|
32
|
+
|
|
33
|
+
expect(response.status).toBe(200)
|
|
34
|
+
await expect(response.json()).resolves.toEqual({
|
|
35
|
+
data: [
|
|
36
|
+
{
|
|
37
|
+
id: 'catalog.product.created',
|
|
38
|
+
label: 'Product created',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
total: 1,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -24,7 +24,7 @@ const eventsResponseSchema = z.object({
|
|
|
24
24
|
|
|
25
25
|
export async function GET(): Promise<Response> {
|
|
26
26
|
const events = getDeclaredEvents()
|
|
27
|
-
.filter((event) => !event.id.startsWith('webhooks.'))
|
|
27
|
+
.filter((event) => !event.id.startsWith('webhooks.') && !event.excludeFromTriggers)
|
|
28
28
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
29
29
|
|
|
30
30
|
return json({
|
|
@@ -5,6 +5,9 @@ jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
|
|
|
5
5
|
findWithDecryption: jest.fn(),
|
|
6
6
|
findOneWithDecryption: jest.fn(),
|
|
7
7
|
}))
|
|
8
|
+
jest.mock('@open-mercato/shared/modules/events', () => ({
|
|
9
|
+
getDeclaredEvents: jest.fn(),
|
|
10
|
+
}))
|
|
8
11
|
|
|
9
12
|
jest.mock('../../lib/delivery', () => ({
|
|
10
13
|
createWebhookDelivery: jest.fn(),
|
|
@@ -18,6 +21,7 @@ jest.mock('../../lib/integration-state', () => ({
|
|
|
18
21
|
}))
|
|
19
22
|
|
|
20
23
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
24
|
+
import { getDeclaredEvents } from '@open-mercato/shared/modules/events'
|
|
21
25
|
import { createWebhookDelivery } from '../../lib/delivery'
|
|
22
26
|
import { enqueueWebhookDelivery } from '../../lib/queue'
|
|
23
27
|
import { isWebhookIntegrationEnabled } from '../../lib/integration-state'
|
|
@@ -27,6 +31,10 @@ describe('webhooks outbound dispatch subscriber', () => {
|
|
|
27
31
|
jest.clearAllMocks()
|
|
28
32
|
})
|
|
29
33
|
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
;(getDeclaredEvents as jest.Mock).mockReturnValue([])
|
|
36
|
+
})
|
|
37
|
+
|
|
30
38
|
it('uses the event bus eventName for wildcard subscribers and schedules matching webhook deliveries', async () => {
|
|
31
39
|
const em = {
|
|
32
40
|
fork: jest.fn(function fork() {
|
|
@@ -160,6 +168,50 @@ describe('webhooks outbound dispatch subscriber', () => {
|
|
|
160
168
|
expect(findWithDecryption).toHaveBeenCalled()
|
|
161
169
|
})
|
|
162
170
|
|
|
171
|
+
it('skips application lifecycle events before resolving the entity manager', async () => {
|
|
172
|
+
const resolve = jest.fn()
|
|
173
|
+
|
|
174
|
+
await expect(handler(
|
|
175
|
+
{
|
|
176
|
+
tenantId: 'tenant-1',
|
|
177
|
+
organizationId: 'org-1',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
eventName: 'application.request.auth_resolved',
|
|
181
|
+
resolve,
|
|
182
|
+
},
|
|
183
|
+
)).resolves.toBeUndefined()
|
|
184
|
+
|
|
185
|
+
expect(resolve).not.toHaveBeenCalled()
|
|
186
|
+
expect(findWithDecryption).not.toHaveBeenCalled()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('skips declared events that are excluded from triggers before hitting the database', async () => {
|
|
190
|
+
;(getDeclaredEvents as jest.Mock).mockReturnValue([
|
|
191
|
+
{
|
|
192
|
+
id: 'sales.document.calculate.before',
|
|
193
|
+
label: 'Before document calculate',
|
|
194
|
+
excludeFromTriggers: true,
|
|
195
|
+
},
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
const resolve = jest.fn()
|
|
199
|
+
|
|
200
|
+
await expect(handler(
|
|
201
|
+
{
|
|
202
|
+
tenantId: 'tenant-1',
|
|
203
|
+
organizationId: 'org-1',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
eventName: 'sales.document.calculate.before',
|
|
207
|
+
resolve,
|
|
208
|
+
},
|
|
209
|
+
)).resolves.toBeUndefined()
|
|
210
|
+
|
|
211
|
+
expect(resolve).not.toHaveBeenCalled()
|
|
212
|
+
expect(findWithDecryption).not.toHaveBeenCalled()
|
|
213
|
+
})
|
|
214
|
+
|
|
163
215
|
it('skips processing for internal query_index events', async () => {
|
|
164
216
|
;(findWithDecryption as jest.Mock).mockResolvedValue([])
|
|
165
217
|
|
|
@@ -3,6 +3,7 @@ import type { SubscriberContext } from '@open-mercato/events/types'
|
|
|
3
3
|
import { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'
|
|
4
4
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
5
|
import { matchAnyWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'
|
|
6
|
+
import { getDeclaredEvents } from '@open-mercato/shared/modules/events'
|
|
6
7
|
import { createWebhookDelivery } from '../lib/delivery'
|
|
7
8
|
import { enqueueWebhookDelivery } from '../lib/queue'
|
|
8
9
|
import { isWebhookIntegrationEnabled } from '../lib/integration-state'
|
|
@@ -13,12 +14,20 @@ export const metadata = {
|
|
|
13
14
|
id: 'webhooks:outbound-dispatch',
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function shouldSkipOutboundDispatch(eventId: string): boolean {
|
|
18
|
+
if (eventId.startsWith('webhooks.') || eventId.startsWith('application.')) return true
|
|
19
|
+
|
|
20
|
+
const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId)
|
|
21
|
+
return declaredEvent?.excludeFromTriggers === true
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
export default async function handler(
|
|
17
25
|
payload: Record<string, unknown>,
|
|
18
26
|
ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },
|
|
19
27
|
) {
|
|
20
28
|
const eventId = ctx.eventId ?? ctx.eventName ?? (payload.eventId as string) ?? (payload.type as string)
|
|
21
29
|
if (!eventId) return
|
|
30
|
+
if (shouldSkipOutboundDispatch(eventId)) return
|
|
22
31
|
|
|
23
32
|
const tenantId = payload.tenantId as string | undefined
|
|
24
33
|
const organizationId = payload.organizationId as string | undefined
|
|
@@ -27,6 +36,7 @@ export default async function handler(
|
|
|
27
36
|
if (eventId.startsWith('webhooks.')) return
|
|
28
37
|
if (eventId.startsWith('query_index.')) return
|
|
29
38
|
|
|
39
|
+
|
|
30
40
|
const resolve = ('resolve' in ctx && typeof ctx.resolve === 'function')
|
|
31
41
|
? ctx.resolve
|
|
32
42
|
: ('container' in ctx && ctx.container && typeof ctx.container.resolve === 'function')
|