@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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/lib/application/dto/list-deliveries-query.dto.d.ts +8 -0
- package/dist/lib/application/dto/list-deliveries-query.dto.d.ts.map +1 -0
- package/dist/lib/application/dto/list-deliveries-query.dto.js +64 -0
- package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts +23 -0
- package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts.map +1 -0
- package/dist/lib/application/dto/save-webhook-endpoint.dto.js +127 -0
- package/dist/lib/application/webhook-endpoints.service.d.ts +82 -0
- package/dist/lib/application/webhook-endpoints.service.d.ts.map +1 -0
- package/dist/lib/application/webhook-endpoints.service.js +213 -0
- package/dist/lib/docs/describe-webhooks-api.d.ts +16 -0
- package/dist/lib/docs/describe-webhooks-api.d.ts.map +1 -0
- package/dist/lib/docs/describe-webhooks-api.js +170 -0
- package/dist/lib/docs/webhook-schemas.d.ts +21 -0
- package/dist/lib/docs/webhook-schemas.d.ts.map +1 -0
- package/dist/lib/docs/webhook-schemas.js +360 -0
- package/dist/lib/domain/errors/index.d.ts +3 -0
- package/dist/lib/domain/errors/index.d.ts.map +1 -0
- package/dist/lib/domain/errors/index.js +7 -0
- package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts +12 -0
- package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/webhook-delivery-not-found.error.js +19 -0
- package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts +6 -0
- package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/webhook-endpoint-not-found.error.js +13 -0
- package/dist/lib/domain/webhook-secret.d.ts +29 -0
- package/dist/lib/domain/webhook-secret.d.ts.map +1 -0
- package/dist/lib/domain/webhook-secret.js +34 -0
- package/dist/lib/domain/webhook-views.d.ts +85 -0
- package/dist/lib/domain/webhook-views.d.ts.map +1 -0
- package/dist/lib/domain/webhook-views.js +2 -0
- package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts +28 -0
- package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts.map +1 -0
- package/dist/lib/http/controllers/webhook-deliveries.controller.js +110 -0
- package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts +61 -0
- package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts.map +1 -0
- package/dist/lib/http/controllers/webhook-endpoints.controller.js +213 -0
- package/dist/lib/http/controllers/webhook-events.controller.d.ts +13 -0
- package/dist/lib/http/controllers/webhook-events.controller.d.ts.map +1 -0
- package/dist/lib/http/controllers/webhook-events.controller.js +38 -0
- package/dist/lib/infrastructure/event-mapping.d.ts +13 -0
- package/dist/lib/infrastructure/event-mapping.d.ts.map +1 -0
- package/dist/lib/infrastructure/event-mapping.js +69 -0
- package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts +42 -0
- package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts.map +1 -0
- package/dist/lib/infrastructure/purge/webhook-workspaces.purger.js +66 -0
- package/dist/lib/infrastructure/schema/index.d.ts +3 -0
- package/dist/lib/infrastructure/schema/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/schema/index.js +8 -0
- package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts +332 -0
- package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts.map +1 -0
- package/dist/lib/infrastructure/schema/webhook-deliveries.js +92 -0
- package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts +330 -0
- package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts.map +1 -0
- package/dist/lib/infrastructure/schema/webhook-endpoints.js +94 -0
- package/dist/lib/infrastructure/webhook-delivery.repository.d.ts +99 -0
- package/dist/lib/infrastructure/webhook-delivery.repository.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-delivery.repository.js +255 -0
- package/dist/lib/infrastructure/webhook-delivery.worker.d.ts +72 -0
- package/dist/lib/infrastructure/webhook-delivery.worker.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-delivery.worker.js +216 -0
- package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts +88 -0
- package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-endpoint.repository.js +306 -0
- package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts +36 -0
- package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-fanout.subscriber.js +83 -0
- package/dist/lib/infrastructure/webhook-http.client.d.ts +76 -0
- package/dist/lib/infrastructure/webhook-http.client.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-http.client.js +225 -0
- package/dist/lib/types/webhooks-config.d.ts +64 -0
- package/dist/lib/types/webhooks-config.d.ts.map +1 -0
- package/dist/lib/types/webhooks-config.js +24 -0
- package/dist/lib/utils/webhooks-plugin.d.ts +35 -0
- package/dist/lib/utils/webhooks-plugin.d.ts.map +1 -0
- package/dist/lib/utils/webhooks-plugin.js +90 -0
- package/dist/lib/webhooks.module.d.ts +18 -0
- package/dist/lib/webhooks.module.d.ts.map +1 -0
- package/dist/lib/webhooks.module.js +72 -0
- package/dist/lib/webhooks.tokens.d.ts +5 -0
- package/dist/lib/webhooks.tokens.d.ts.map +1 -0
- package/dist/lib/webhooks.tokens.js +9 -0
- package/migrations/0000_init_webhooks.sql +53 -0
- package/migrations/meta/0000_snapshot.json +424 -0
- package/migrations/meta/_journal.json +13 -0
- 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 @@
|
|
|
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
|
+
]);
|