@orthacms/webhooks-server 0.5.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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +19 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +33 -0
  6. package/dist/lib/application/dto/list-deliveries-query.dto.d.ts +8 -0
  7. package/dist/lib/application/dto/list-deliveries-query.dto.d.ts.map +1 -0
  8. package/dist/lib/application/dto/list-deliveries-query.dto.js +64 -0
  9. package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts +23 -0
  10. package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts.map +1 -0
  11. package/dist/lib/application/dto/save-webhook-endpoint.dto.js +127 -0
  12. package/dist/lib/application/webhook-endpoints.service.d.ts +82 -0
  13. package/dist/lib/application/webhook-endpoints.service.d.ts.map +1 -0
  14. package/dist/lib/application/webhook-endpoints.service.js +213 -0
  15. package/dist/lib/docs/describe-webhooks-api.d.ts +16 -0
  16. package/dist/lib/docs/describe-webhooks-api.d.ts.map +1 -0
  17. package/dist/lib/docs/describe-webhooks-api.js +170 -0
  18. package/dist/lib/docs/webhook-schemas.d.ts +21 -0
  19. package/dist/lib/docs/webhook-schemas.d.ts.map +1 -0
  20. package/dist/lib/docs/webhook-schemas.js +360 -0
  21. package/dist/lib/domain/errors/index.d.ts +3 -0
  22. package/dist/lib/domain/errors/index.d.ts.map +1 -0
  23. package/dist/lib/domain/errors/index.js +7 -0
  24. package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts +12 -0
  25. package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts.map +1 -0
  26. package/dist/lib/domain/errors/webhook-delivery-not-found.error.js +19 -0
  27. package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts +6 -0
  28. package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts.map +1 -0
  29. package/dist/lib/domain/errors/webhook-endpoint-not-found.error.js +13 -0
  30. package/dist/lib/domain/webhook-secret.d.ts +29 -0
  31. package/dist/lib/domain/webhook-secret.d.ts.map +1 -0
  32. package/dist/lib/domain/webhook-secret.js +34 -0
  33. package/dist/lib/domain/webhook-views.d.ts +85 -0
  34. package/dist/lib/domain/webhook-views.d.ts.map +1 -0
  35. package/dist/lib/domain/webhook-views.js +2 -0
  36. package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts +28 -0
  37. package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts.map +1 -0
  38. package/dist/lib/http/controllers/webhook-deliveries.controller.js +110 -0
  39. package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts +61 -0
  40. package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts.map +1 -0
  41. package/dist/lib/http/controllers/webhook-endpoints.controller.js +213 -0
  42. package/dist/lib/http/controllers/webhook-events.controller.d.ts +13 -0
  43. package/dist/lib/http/controllers/webhook-events.controller.d.ts.map +1 -0
  44. package/dist/lib/http/controllers/webhook-events.controller.js +38 -0
  45. package/dist/lib/infrastructure/event-mapping.d.ts +13 -0
  46. package/dist/lib/infrastructure/event-mapping.d.ts.map +1 -0
  47. package/dist/lib/infrastructure/event-mapping.js +69 -0
  48. package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts +42 -0
  49. package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts.map +1 -0
  50. package/dist/lib/infrastructure/purge/webhook-workspaces.purger.js +66 -0
  51. package/dist/lib/infrastructure/schema/index.d.ts +3 -0
  52. package/dist/lib/infrastructure/schema/index.d.ts.map +1 -0
  53. package/dist/lib/infrastructure/schema/index.js +8 -0
  54. package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts +332 -0
  55. package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts.map +1 -0
  56. package/dist/lib/infrastructure/schema/webhook-deliveries.js +92 -0
  57. package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts +330 -0
  58. package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts.map +1 -0
  59. package/dist/lib/infrastructure/schema/webhook-endpoints.js +94 -0
  60. package/dist/lib/infrastructure/webhook-delivery.repository.d.ts +99 -0
  61. package/dist/lib/infrastructure/webhook-delivery.repository.d.ts.map +1 -0
  62. package/dist/lib/infrastructure/webhook-delivery.repository.js +255 -0
  63. package/dist/lib/infrastructure/webhook-delivery.worker.d.ts +72 -0
  64. package/dist/lib/infrastructure/webhook-delivery.worker.d.ts.map +1 -0
  65. package/dist/lib/infrastructure/webhook-delivery.worker.js +216 -0
  66. package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts +88 -0
  67. package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts.map +1 -0
  68. package/dist/lib/infrastructure/webhook-endpoint.repository.js +306 -0
  69. package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts +36 -0
  70. package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts.map +1 -0
  71. package/dist/lib/infrastructure/webhook-fanout.subscriber.js +83 -0
  72. package/dist/lib/infrastructure/webhook-http.client.d.ts +76 -0
  73. package/dist/lib/infrastructure/webhook-http.client.d.ts.map +1 -0
  74. package/dist/lib/infrastructure/webhook-http.client.js +225 -0
  75. package/dist/lib/types/webhooks-config.d.ts +64 -0
  76. package/dist/lib/types/webhooks-config.d.ts.map +1 -0
  77. package/dist/lib/types/webhooks-config.js +24 -0
  78. package/dist/lib/utils/webhooks-plugin.d.ts +35 -0
  79. package/dist/lib/utils/webhooks-plugin.d.ts.map +1 -0
  80. package/dist/lib/utils/webhooks-plugin.js +90 -0
  81. package/dist/lib/webhooks.module.d.ts +18 -0
  82. package/dist/lib/webhooks.module.d.ts.map +1 -0
  83. package/dist/lib/webhooks.module.js +72 -0
  84. package/dist/lib/webhooks.tokens.d.ts +5 -0
  85. package/dist/lib/webhooks.tokens.d.ts.map +1 -0
  86. package/dist/lib/webhooks.tokens.js +9 -0
  87. package/migrations/0000_init_webhooks.sql +53 -0
  88. package/migrations/meta/0000_snapshot.json +424 -0
  89. package/migrations/meta/_journal.json +13 -0
  90. package/package.json +45 -0
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toRoutableEvent = toRoutableEvent;
4
+ exports.toSourceEvent = toSourceEvent;
5
+ /**
6
+ * Turns an outbox `DomainEvent` into the shapes the webhooks domain works in.
7
+ *
8
+ * This is the only file that knows both vocabularies. Keeping it at the edge is
9
+ * what lets `@orthacms/webhooks-domain` — and therefore the contract external
10
+ * receivers depend on — stay ignorant of how the outbox happens to store an
11
+ * event today.
12
+ */
13
+ /** The payload keys that are envelope metadata rather than event data. */
14
+ const ENVELOPE_KEYS = new Set(['actor', 'workspaceId']);
15
+ /** The routing facts, for the subscription filters. */
16
+ function toRoutableEvent(event) {
17
+ return {
18
+ kind: event.kind,
19
+ workspaceId: readString(event.payload, 'workspaceId'),
20
+ contentType: readString(event.payload, 'contentType')
21
+ };
22
+ }
23
+ /**
24
+ * The full source event, for the envelope.
25
+ *
26
+ * `actor` and `workspaceId` are lifted out of the payload into the envelope's
27
+ * own fields, so `data` carries what the event kind is *about* and nothing that
28
+ * every event has.
29
+ */
30
+ function toSourceEvent(event) {
31
+ const data = {};
32
+ for (const [key, value] of Object.entries(event.payload)) {
33
+ if (!ENVELOPE_KEYS.has(key))
34
+ data[key] = value;
35
+ }
36
+ return {
37
+ eventId: event.eventId,
38
+ kind: event.kind,
39
+ occurredAt: event.occurredAt,
40
+ workspaceId: readString(event.payload, 'workspaceId'),
41
+ aggregateType: event.aggregateType,
42
+ aggregateId: event.aggregateId,
43
+ data,
44
+ actor: readActor(event.payload)
45
+ };
46
+ }
47
+ /** A string payload key, or `null` when it is absent or the wrong shape. */
48
+ function readString(payload, key) {
49
+ const value = payload[key];
50
+ return typeof value === 'string' && value.length > 0 ? value : null;
51
+ }
52
+ /**
53
+ * The acting user off the payload, as `attachActor` wrote it.
54
+ *
55
+ * Absent for a write made with an API token or by the system, and `null` is the
56
+ * honest answer there — naming a user who did not do it would be worse than
57
+ * saying nothing.
58
+ */
59
+ function readActor(payload) {
60
+ const actor = payload['actor'];
61
+ if (typeof actor !== 'object' || actor === null)
62
+ return null;
63
+ const record = actor;
64
+ const id = record['id'];
65
+ if (typeof id !== 'string')
66
+ return null;
67
+ const email = record['email'];
68
+ return { id, email: typeof email === 'string' ? email : null };
69
+ }
@@ -0,0 +1,42 @@
1
+ import { OnModuleInit } from '@nestjs/common';
2
+ import { UnitOfWork } from '@orthacms/database';
3
+ import { WorkspacePurgeRegistry, type WorkspacePurger, type WorkspacePurgeOutcome } from '@orthacms/workspaces-server';
4
+ /**
5
+ * Removes a deleted workspace from every endpoint that subscribed to it.
6
+ *
7
+ * `webhook_endpoint_workspaces.workspace_id` is a plain uuid with no foreign
8
+ * key — the `workspaces` table belongs to another plugin — and the only other
9
+ * delete is the cascade from `webhook_endpoints`, which fires when the
10
+ * *endpoint* goes, never when the workspace does. So a deleted workspace left
11
+ * its subscription rows behind, and fan-out kept reading them on every content
12
+ * write, matching an id that could never appear in an event again.
13
+ *
14
+ * ## Why pruning is safe here and not in `segments`
15
+ *
16
+ * The two look identical — a workspace id with no FK, in a set that decides who
17
+ * receives something — and they get opposite treatment, so the difference is
18
+ * worth stating. An audience's empty `workspace_ids` **means "every
19
+ * workspace"**, so pruning the last id would widen who may read. An endpoint's
20
+ * empty set means nothing at all: `all_workspaces` is a separate boolean, held
21
+ * explicitly for this exact reason ("fan-out must never have to disambiguate
22
+ * 'no rows' between 'all' and 'none'"). Pruning here can only narrow.
23
+ *
24
+ * ## The endpoint itself stays
25
+ *
26
+ * An endpoint that named only the deleted workspace is left enabled with an
27
+ * empty set, which `matches` reads as "no workspace-carrying event" — it still
28
+ * receives the kinds whose descriptor carries no workspace. Deleting the
29
+ * endpoint instead would destroy a signing secret and a URL somebody
30
+ * configured, on the strength of a workspace delete that says nothing about
31
+ * either; and disabling it would be a state the admin cannot distinguish from
32
+ * a human having switched it off.
33
+ */
34
+ export declare class WebhookWorkspacesPurger implements WorkspacePurger, OnModuleInit {
35
+ private readonly uow;
36
+ private readonly registry?;
37
+ readonly purgeName = "webhooks:endpoint-workspaces";
38
+ constructor(uow: UnitOfWork, registry?: WorkspacePurgeRegistry | undefined);
39
+ onModuleInit(): void;
40
+ purge(workspaceId: string): Promise<WorkspacePurgeOutcome>;
41
+ }
42
+ //# sourceMappingURL=webhook-workspaces.purger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-workspaces.purger.d.ts","sourceRoot":"","sources":["../../../../src/lib/infrastructure/purge/webhook-workspaces.purger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,YAAY,EAAY,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EACH,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC7B,MAAM,6BAA6B,CAAC;AAGrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBACa,uBAAwB,YAAW,eAAe,EAAE,YAAY;IAIrE,OAAO,CAAC,QAAQ,CAAC,GAAG;IACR,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAJ1C,QAAQ,CAAC,SAAS,kCAAkC;gBAG/B,GAAG,EAAE,UAAU,EACH,QAAQ,CAAC,EAAE,sBAAsB,YAAA;IAGlE,YAAY,IAAI,IAAI;IAId,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CASnE"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookWorkspacesPurger = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const drizzle_orm_1 = require("drizzle-orm");
7
+ const database_1 = require("@orthacms/database");
8
+ const workspaces_server_1 = require("@orthacms/workspaces-server");
9
+ const webhook_endpoints_1 = require("../schema/webhook-endpoints");
10
+ /**
11
+ * Removes a deleted workspace from every endpoint that subscribed to it.
12
+ *
13
+ * `webhook_endpoint_workspaces.workspace_id` is a plain uuid with no foreign
14
+ * key — the `workspaces` table belongs to another plugin — and the only other
15
+ * delete is the cascade from `webhook_endpoints`, which fires when the
16
+ * *endpoint* goes, never when the workspace does. So a deleted workspace left
17
+ * its subscription rows behind, and fan-out kept reading them on every content
18
+ * write, matching an id that could never appear in an event again.
19
+ *
20
+ * ## Why pruning is safe here and not in `segments`
21
+ *
22
+ * The two look identical — a workspace id with no FK, in a set that decides who
23
+ * receives something — and they get opposite treatment, so the difference is
24
+ * worth stating. An audience's empty `workspace_ids` **means "every
25
+ * workspace"**, so pruning the last id would widen who may read. An endpoint's
26
+ * empty set means nothing at all: `all_workspaces` is a separate boolean, held
27
+ * explicitly for this exact reason ("fan-out must never have to disambiguate
28
+ * 'no rows' between 'all' and 'none'"). Pruning here can only narrow.
29
+ *
30
+ * ## The endpoint itself stays
31
+ *
32
+ * An endpoint that named only the deleted workspace is left enabled with an
33
+ * empty set, which `matches` reads as "no workspace-carrying event" — it still
34
+ * receives the kinds whose descriptor carries no workspace. Deleting the
35
+ * endpoint instead would destroy a signing secret and a URL somebody
36
+ * configured, on the strength of a workspace delete that says nothing about
37
+ * either; and disabling it would be a state the admin cannot distinguish from
38
+ * a human having switched it off.
39
+ */
40
+ let WebhookWorkspacesPurger = class WebhookWorkspacesPurger {
41
+ uow;
42
+ registry;
43
+ purgeName = 'webhooks:endpoint-workspaces';
44
+ constructor(uow, registry) {
45
+ this.uow = uow;
46
+ this.registry = registry;
47
+ }
48
+ onModuleInit() {
49
+ this.registry?.register(this);
50
+ }
51
+ async purge(workspaceId) {
52
+ const removed = await this.uow
53
+ .current()
54
+ .delete(webhook_endpoints_1.webhookEndpointWorkspaces)
55
+ .where((0, drizzle_orm_1.eq)(webhook_endpoints_1.webhookEndpointWorkspaces.workspaceId, workspaceId))
56
+ .returning({ endpointId: webhook_endpoints_1.webhookEndpointWorkspaces.endpointId });
57
+ return { rows: removed.length };
58
+ }
59
+ };
60
+ exports.WebhookWorkspacesPurger = WebhookWorkspacesPurger;
61
+ exports.WebhookWorkspacesPurger = WebhookWorkspacesPurger = tslib_1.__decorate([
62
+ (0, common_1.Injectable)(),
63
+ tslib_1.__param(1, (0, common_1.Optional)()),
64
+ tslib_1.__metadata("design:paramtypes", [database_1.UnitOfWork,
65
+ workspaces_server_1.WorkspacePurgeRegistry])
66
+ ], WebhookWorkspacesPurger);
@@ -0,0 +1,3 @@
1
+ export { webhookEndpoints, webhookEndpointWorkspaces } from './webhook-endpoints';
2
+ export { webhookDeliveries } from './webhook-deliveries';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/infrastructure/schema/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,gBAAgB,EAChB,yBAAyB,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webhookDeliveries = exports.webhookEndpointWorkspaces = exports.webhookEndpoints = void 0;
4
+ var webhook_endpoints_1 = require("./webhook-endpoints");
5
+ Object.defineProperty(exports, "webhookEndpoints", { enumerable: true, get: function () { return webhook_endpoints_1.webhookEndpoints; } });
6
+ Object.defineProperty(exports, "webhookEndpointWorkspaces", { enumerable: true, get: function () { return webhook_endpoints_1.webhookEndpointWorkspaces; } });
7
+ var webhook_deliveries_1 = require("./webhook-deliveries");
8
+ Object.defineProperty(exports, "webhookDeliveries", { enumerable: true, get: function () { return webhook_deliveries_1.webhookDeliveries; } });
@@ -0,0 +1,332 @@
1
+ /**
2
+ * One event's journey to one endpoint — the queue and the log at once.
3
+ *
4
+ * **Why this table exists at all.** `OutboxDispatcher` delivers to subscribers
5
+ * *inside* the transaction that claims the batch. A subscriber that made an
6
+ * outgoing HTTP request would hold that transaction and its pool client open
7
+ * for the whole round trip, stalling every other subscriber behind a stranger's
8
+ * server; worse, a thrown error would count against the **outbox row's**
9
+ * attempt budget and eventually dead-letter an event the activity log and the
10
+ * alarms evaluator also needed. So the subscriber only writes rows here, and a
11
+ * worker does the network with no transaction open.
12
+ *
13
+ * The partial unique index on `(endpoint_id, event_id)` is the idempotency:
14
+ * outbox delivery is at-least-once, and a re-delivered event must not queue the
15
+ * same webhook twice. It is *partial* so that a deliberate redelivery — which
16
+ * carries the same `event_id` on purpose, because that is what a receiver
17
+ * deduplicates on — is exempt from it.
18
+ */
19
+ export declare const webhookDeliveries: import("drizzle-orm/pg-core").PgTableWithColumns<{
20
+ name: "webhook_deliveries";
21
+ schema: undefined;
22
+ columns: {
23
+ id: import("drizzle-orm/pg-core").PgColumn<{
24
+ name: "id";
25
+ tableName: "webhook_deliveries";
26
+ dataType: "string";
27
+ columnType: "PgUUID";
28
+ data: string;
29
+ driverParam: string;
30
+ notNull: true;
31
+ hasDefault: true;
32
+ isPrimaryKey: true;
33
+ isAutoincrement: false;
34
+ hasRuntimeDefault: false;
35
+ enumValues: undefined;
36
+ baseColumn: never;
37
+ identity: undefined;
38
+ generated: undefined;
39
+ }, {}, {}>;
40
+ endpointId: import("drizzle-orm/pg-core").PgColumn<{
41
+ name: "endpoint_id";
42
+ tableName: "webhook_deliveries";
43
+ dataType: "string";
44
+ columnType: "PgUUID";
45
+ data: string;
46
+ driverParam: string;
47
+ notNull: true;
48
+ hasDefault: false;
49
+ isPrimaryKey: false;
50
+ isAutoincrement: false;
51
+ hasRuntimeDefault: false;
52
+ enumValues: undefined;
53
+ baseColumn: never;
54
+ identity: undefined;
55
+ generated: undefined;
56
+ }, {}, {}>;
57
+ eventId: import("drizzle-orm/pg-core").PgColumn<{
58
+ name: "event_id";
59
+ tableName: "webhook_deliveries";
60
+ dataType: "string";
61
+ columnType: "PgUUID";
62
+ data: string;
63
+ driverParam: string;
64
+ notNull: true;
65
+ hasDefault: false;
66
+ isPrimaryKey: false;
67
+ isAutoincrement: false;
68
+ hasRuntimeDefault: false;
69
+ enumValues: undefined;
70
+ baseColumn: never;
71
+ identity: undefined;
72
+ generated: undefined;
73
+ }, {}, {}>;
74
+ eventKind: import("drizzle-orm/pg-core").PgColumn<{
75
+ name: "event_kind";
76
+ tableName: "webhook_deliveries";
77
+ dataType: "string";
78
+ columnType: "PgText";
79
+ data: string;
80
+ driverParam: string;
81
+ notNull: true;
82
+ hasDefault: false;
83
+ isPrimaryKey: false;
84
+ isAutoincrement: false;
85
+ hasRuntimeDefault: false;
86
+ enumValues: [string, ...string[]];
87
+ baseColumn: never;
88
+ identity: undefined;
89
+ generated: undefined;
90
+ }, {}, {}>;
91
+ workspaceId: import("drizzle-orm/pg-core").PgColumn<{
92
+ name: "workspace_id";
93
+ tableName: "webhook_deliveries";
94
+ dataType: "string";
95
+ columnType: "PgUUID";
96
+ data: string;
97
+ driverParam: string;
98
+ notNull: false;
99
+ hasDefault: false;
100
+ isPrimaryKey: false;
101
+ isAutoincrement: false;
102
+ hasRuntimeDefault: false;
103
+ enumValues: undefined;
104
+ baseColumn: never;
105
+ identity: undefined;
106
+ generated: undefined;
107
+ }, {}, {}>;
108
+ contentType: import("drizzle-orm/pg-core").PgColumn<{
109
+ name: "content_type";
110
+ tableName: "webhook_deliveries";
111
+ dataType: "string";
112
+ columnType: "PgText";
113
+ data: string;
114
+ driverParam: string;
115
+ notNull: false;
116
+ hasDefault: false;
117
+ isPrimaryKey: false;
118
+ isAutoincrement: false;
119
+ hasRuntimeDefault: false;
120
+ enumValues: [string, ...string[]];
121
+ baseColumn: never;
122
+ identity: undefined;
123
+ generated: undefined;
124
+ }, {}, {}>;
125
+ payload: import("drizzle-orm/pg-core").PgColumn<{
126
+ name: "payload";
127
+ tableName: "webhook_deliveries";
128
+ dataType: "json";
129
+ columnType: "PgJsonb";
130
+ data: unknown;
131
+ driverParam: unknown;
132
+ notNull: true;
133
+ hasDefault: false;
134
+ isPrimaryKey: false;
135
+ isAutoincrement: false;
136
+ hasRuntimeDefault: false;
137
+ enumValues: undefined;
138
+ baseColumn: never;
139
+ identity: undefined;
140
+ generated: undefined;
141
+ }, {}, {}>;
142
+ status: import("drizzle-orm/pg-core").PgColumn<{
143
+ name: "status";
144
+ tableName: "webhook_deliveries";
145
+ dataType: "string";
146
+ columnType: "PgText";
147
+ data: string;
148
+ driverParam: string;
149
+ notNull: true;
150
+ hasDefault: true;
151
+ isPrimaryKey: false;
152
+ isAutoincrement: false;
153
+ hasRuntimeDefault: false;
154
+ enumValues: [string, ...string[]];
155
+ baseColumn: never;
156
+ identity: undefined;
157
+ generated: undefined;
158
+ }, {}, {}>;
159
+ attempts: import("drizzle-orm/pg-core").PgColumn<{
160
+ name: "attempts";
161
+ tableName: "webhook_deliveries";
162
+ dataType: "number";
163
+ columnType: "PgInteger";
164
+ data: number;
165
+ driverParam: string | number;
166
+ notNull: true;
167
+ hasDefault: true;
168
+ isPrimaryKey: false;
169
+ isAutoincrement: false;
170
+ hasRuntimeDefault: false;
171
+ enumValues: undefined;
172
+ baseColumn: never;
173
+ identity: undefined;
174
+ generated: undefined;
175
+ }, {}, {}>;
176
+ nextAttemptAt: import("drizzle-orm/pg-core").PgColumn<{
177
+ name: "next_attempt_at";
178
+ tableName: "webhook_deliveries";
179
+ dataType: "date";
180
+ columnType: "PgTimestamp";
181
+ data: Date;
182
+ driverParam: string;
183
+ notNull: false;
184
+ hasDefault: false;
185
+ isPrimaryKey: false;
186
+ isAutoincrement: false;
187
+ hasRuntimeDefault: false;
188
+ enumValues: undefined;
189
+ baseColumn: never;
190
+ identity: undefined;
191
+ generated: undefined;
192
+ }, {}, {}>;
193
+ claimedAt: import("drizzle-orm/pg-core").PgColumn<{
194
+ name: "claimed_at";
195
+ tableName: "webhook_deliveries";
196
+ dataType: "date";
197
+ columnType: "PgTimestamp";
198
+ data: Date;
199
+ driverParam: string;
200
+ notNull: false;
201
+ hasDefault: false;
202
+ isPrimaryKey: false;
203
+ isAutoincrement: false;
204
+ hasRuntimeDefault: false;
205
+ enumValues: undefined;
206
+ baseColumn: never;
207
+ identity: undefined;
208
+ generated: undefined;
209
+ }, {}, {}>;
210
+ lastStatusCode: import("drizzle-orm/pg-core").PgColumn<{
211
+ name: "last_status_code";
212
+ tableName: "webhook_deliveries";
213
+ dataType: "number";
214
+ columnType: "PgInteger";
215
+ data: number;
216
+ driverParam: string | number;
217
+ notNull: false;
218
+ hasDefault: false;
219
+ isPrimaryKey: false;
220
+ isAutoincrement: false;
221
+ hasRuntimeDefault: false;
222
+ enumValues: undefined;
223
+ baseColumn: never;
224
+ identity: undefined;
225
+ generated: undefined;
226
+ }, {}, {}>;
227
+ lastError: import("drizzle-orm/pg-core").PgColumn<{
228
+ name: "last_error";
229
+ tableName: "webhook_deliveries";
230
+ dataType: "string";
231
+ columnType: "PgText";
232
+ data: string;
233
+ driverParam: string;
234
+ notNull: false;
235
+ hasDefault: false;
236
+ isPrimaryKey: false;
237
+ isAutoincrement: false;
238
+ hasRuntimeDefault: false;
239
+ enumValues: [string, ...string[]];
240
+ baseColumn: never;
241
+ identity: undefined;
242
+ generated: undefined;
243
+ }, {}, {}>;
244
+ responseSnippet: import("drizzle-orm/pg-core").PgColumn<{
245
+ name: "response_snippet";
246
+ tableName: "webhook_deliveries";
247
+ dataType: "string";
248
+ columnType: "PgText";
249
+ data: string;
250
+ driverParam: string;
251
+ notNull: false;
252
+ hasDefault: false;
253
+ isPrimaryKey: false;
254
+ isAutoincrement: false;
255
+ hasRuntimeDefault: false;
256
+ enumValues: [string, ...string[]];
257
+ baseColumn: never;
258
+ identity: undefined;
259
+ generated: undefined;
260
+ }, {}, {}>;
261
+ durationMs: import("drizzle-orm/pg-core").PgColumn<{
262
+ name: "duration_ms";
263
+ tableName: "webhook_deliveries";
264
+ dataType: "number";
265
+ columnType: "PgInteger";
266
+ data: number;
267
+ driverParam: string | number;
268
+ notNull: false;
269
+ hasDefault: false;
270
+ isPrimaryKey: false;
271
+ isAutoincrement: false;
272
+ hasRuntimeDefault: false;
273
+ enumValues: undefined;
274
+ baseColumn: never;
275
+ identity: undefined;
276
+ generated: undefined;
277
+ }, {}, {}>;
278
+ redeliveryOf: import("drizzle-orm/pg-core").PgColumn<{
279
+ name: "redelivery_of";
280
+ tableName: "webhook_deliveries";
281
+ dataType: "string";
282
+ columnType: "PgUUID";
283
+ data: string;
284
+ driverParam: string;
285
+ notNull: false;
286
+ hasDefault: false;
287
+ isPrimaryKey: false;
288
+ isAutoincrement: false;
289
+ hasRuntimeDefault: false;
290
+ enumValues: undefined;
291
+ baseColumn: never;
292
+ identity: undefined;
293
+ generated: undefined;
294
+ }, {}, {}>;
295
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
296
+ name: "created_at";
297
+ tableName: "webhook_deliveries";
298
+ dataType: "date";
299
+ columnType: "PgTimestamp";
300
+ data: Date;
301
+ driverParam: string;
302
+ notNull: true;
303
+ hasDefault: true;
304
+ isPrimaryKey: false;
305
+ isAutoincrement: false;
306
+ hasRuntimeDefault: false;
307
+ enumValues: undefined;
308
+ baseColumn: never;
309
+ identity: undefined;
310
+ generated: undefined;
311
+ }, {}, {}>;
312
+ completedAt: import("drizzle-orm/pg-core").PgColumn<{
313
+ name: "completed_at";
314
+ tableName: "webhook_deliveries";
315
+ dataType: "date";
316
+ columnType: "PgTimestamp";
317
+ data: Date;
318
+ driverParam: string;
319
+ notNull: false;
320
+ hasDefault: false;
321
+ isPrimaryKey: false;
322
+ isAutoincrement: false;
323
+ hasRuntimeDefault: false;
324
+ enumValues: undefined;
325
+ baseColumn: never;
326
+ identity: undefined;
327
+ generated: undefined;
328
+ }, {}, {}>;
329
+ };
330
+ dialect: "pg";
331
+ }>;
332
+ //# sourceMappingURL=webhook-deliveries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-deliveries.d.ts","sourceRoot":"","sources":["../../../../src/lib/infrastructure/schema/webhook-deliveries.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0E7B,CAAC"}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webhookDeliveries = void 0;
4
+ const drizzle_orm_1 = require("drizzle-orm");
5
+ const pg_core_1 = require("drizzle-orm/pg-core");
6
+ const webhook_endpoints_1 = require("./webhook-endpoints");
7
+ /**
8
+ * One event's journey to one endpoint — the queue and the log at once.
9
+ *
10
+ * **Why this table exists at all.** `OutboxDispatcher` delivers to subscribers
11
+ * *inside* the transaction that claims the batch. A subscriber that made an
12
+ * outgoing HTTP request would hold that transaction and its pool client open
13
+ * for the whole round trip, stalling every other subscriber behind a stranger's
14
+ * server; worse, a thrown error would count against the **outbox row's**
15
+ * attempt budget and eventually dead-letter an event the activity log and the
16
+ * alarms evaluator also needed. So the subscriber only writes rows here, and a
17
+ * worker does the network with no transaction open.
18
+ *
19
+ * The partial unique index on `(endpoint_id, event_id)` is the idempotency:
20
+ * outbox delivery is at-least-once, and a re-delivered event must not queue the
21
+ * same webhook twice. It is *partial* so that a deliberate redelivery — which
22
+ * carries the same `event_id` on purpose, because that is what a receiver
23
+ * deduplicates on — is exempt from it.
24
+ */
25
+ exports.webhookDeliveries = (0, pg_core_1.pgTable)('webhook_deliveries', {
26
+ /** Primary key, and the value sent as `X-Ortha-Delivery`. */
27
+ id: (0, pg_core_1.uuid)('id').primaryKey().defaultRandom(),
28
+ endpointId: (0, pg_core_1.uuid)('endpoint_id')
29
+ .notNull()
30
+ .references(() => webhook_endpoints_1.webhookEndpoints.id, { onDelete: 'cascade' }),
31
+ /**
32
+ * The originating outbox event. Sent as `X-Ortha-Event-Id` and stable
33
+ * across redeliveries, so it is the value a receiver deduplicates on.
34
+ * For a `ping` there is no outbox row, so this is a fresh uuid.
35
+ */
36
+ eventId: (0, pg_core_1.uuid)('event_id').notNull(),
37
+ eventKind: (0, pg_core_1.text)('event_kind').notNull(),
38
+ /** Denormalised for the log's filters; null when the event has none. */
39
+ workspaceId: (0, pg_core_1.uuid)('workspace_id'),
40
+ /** Denormalised for the log's filters; null for a non-content event. */
41
+ contentType: (0, pg_core_1.text)('content_type'),
42
+ /**
43
+ * The body, frozen when the row was queued. A redelivery sends exactly
44
+ * what the first attempt sent — reassembling it later would quietly
45
+ * send a *different* payload under the same event id.
46
+ */
47
+ payload: (0, pg_core_1.jsonb)('payload').notNull(),
48
+ /** One of `pending | delivering | succeeded | failed | dead`. */
49
+ status: (0, pg_core_1.text)('status').notNull().default('pending'),
50
+ attempts: (0, pg_core_1.integer)('attempts').notNull().default(0),
51
+ /** When the next attempt may be claimed; null means "now". */
52
+ nextAttemptAt: (0, pg_core_1.timestamp)('next_attempt_at', { withTimezone: true }),
53
+ /**
54
+ * When a worker claimed this row. The reaper uses it to recover rows
55
+ * left in `delivering` by a process that died mid-request — without it
56
+ * such a row is stuck forever with no error and no retry.
57
+ */
58
+ claimedAt: (0, pg_core_1.timestamp)('claimed_at', { withTimezone: true }),
59
+ lastStatusCode: (0, pg_core_1.integer)('last_status_code'),
60
+ lastError: (0, pg_core_1.text)('last_error'),
61
+ /** The first couple of kilobytes of the response, for the log. */
62
+ responseSnippet: (0, pg_core_1.text)('response_snippet'),
63
+ durationMs: (0, pg_core_1.integer)('duration_ms'),
64
+ /**
65
+ * The delivery this one repeats, when an operator pressed "Send again".
66
+ *
67
+ * It is also what exempts the row from the unique index below: a
68
+ * redelivery keeps the original `event_id` (so the receiver still
69
+ * recognises the repeat) and would otherwise collide with the row it is
70
+ * repeating.
71
+ */
72
+ redeliveryOf: (0, pg_core_1.uuid)('redelivery_of'),
73
+ createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true })
74
+ .notNull()
75
+ .defaultNow(),
76
+ completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true })
77
+ }, (table) => [
78
+ (0, pg_core_1.uniqueIndex)('webhook_deliveries_endpoint_event_unique')
79
+ .on(table.endpointId, table.eventId)
80
+ .where((0, drizzle_orm_1.sql) `${table.redeliveryOf} is null`),
81
+ // The worker's claim: `WHERE status IN ('pending','failed') ORDER BY
82
+ // next_attempt_at LIMIT n`. Partial and keyed on the sort column, like
83
+ // `outbox_events_pending_idx` — it serves the filter, the order and the
84
+ // limit together, and stays small because completed rows drop out of it.
85
+ (0, pg_core_1.index)('webhook_deliveries_claimable_idx')
86
+ .on(table.nextAttemptAt)
87
+ .where((0, drizzle_orm_1.sql) `${table.status} in ('pending', 'failed')`),
88
+ // The delivery log for one endpoint, newest first.
89
+ (0, pg_core_1.index)('webhook_deliveries_endpoint_idx').on(table.endpointId, table.createdAt),
90
+ // The retention sweep: completed rows older than the cutoff.
91
+ (0, pg_core_1.index)('webhook_deliveries_completed_idx').on(table.completedAt)
92
+ ]);