@voyant-travel/db 0.108.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 +201 -0
- package/README.md +60 -0
- package/dist/aggregate-snapshots.d.ts +66 -0
- package/dist/aggregate-snapshots.d.ts.map +1 -0
- package/dist/aggregate-snapshots.js +110 -0
- package/dist/aggregate-snapshots.js.map +1 -0
- package/dist/columns/collection.d.ts +37 -0
- package/dist/columns/collection.d.ts.map +1 -0
- package/dist/columns/collection.js +44 -0
- package/dist/columns/collection.js.map +1 -0
- package/dist/columns/cruise.d.ts +26 -0
- package/dist/columns/cruise.d.ts.map +1 -0
- package/dist/columns/cruise.js +31 -0
- package/dist/columns/cruise.js.map +1 -0
- package/dist/columns/departure-sub-tables.d.ts +179 -0
- package/dist/columns/departure-sub-tables.d.ts.map +1 -0
- package/dist/columns/departure-sub-tables.js +214 -0
- package/dist/columns/departure-sub-tables.js.map +1 -0
- package/dist/columns/departure.d.ts +20 -0
- package/dist/columns/departure.d.ts.map +1 -0
- package/dist/columns/departure.js +23 -0
- package/dist/columns/departure.js.map +1 -0
- package/dist/columns/destinations.d.ts +21 -0
- package/dist/columns/destinations.d.ts.map +1 -0
- package/dist/columns/destinations.js +24 -0
- package/dist/columns/destinations.js.map +1 -0
- package/dist/columns/index.d.ts +31 -0
- package/dist/columns/index.d.ts.map +1 -0
- package/dist/columns/index.js +57 -0
- package/dist/columns/index.js.map +1 -0
- package/dist/columns/itinerary-sub-tables.d.ts +177 -0
- package/dist/columns/itinerary-sub-tables.d.ts.map +1 -0
- package/dist/columns/itinerary-sub-tables.js +122 -0
- package/dist/columns/itinerary-sub-tables.js.map +1 -0
- package/dist/columns/itinerary.d.ts +17 -0
- package/dist/columns/itinerary.d.ts.map +1 -0
- package/dist/columns/itinerary.js +20 -0
- package/dist/columns/itinerary.js.map +1 -0
- package/dist/columns/lodging.d.ts +143 -0
- package/dist/columns/lodging.d.ts.map +1 -0
- package/dist/columns/lodging.js +164 -0
- package/dist/columns/lodging.js.map +1 -0
- package/dist/columns/offers.d.ts +31 -0
- package/dist/columns/offers.d.ts.map +1 -0
- package/dist/columns/offers.js +43 -0
- package/dist/columns/offers.js.map +1 -0
- package/dist/columns/pricing.d.ts +60 -0
- package/dist/columns/pricing.d.ts.map +1 -0
- package/dist/columns/pricing.js +71 -0
- package/dist/columns/pricing.js.map +1 -0
- package/dist/columns/product-accommodation.d.ts +99 -0
- package/dist/columns/product-accommodation.d.ts.map +1 -0
- package/dist/columns/product-accommodation.js +65 -0
- package/dist/columns/product-accommodation.js.map +1 -0
- package/dist/columns/product-addons.d.ts +18 -0
- package/dist/columns/product-addons.d.ts.map +1 -0
- package/dist/columns/product-addons.js +21 -0
- package/dist/columns/product-addons.js.map +1 -0
- package/dist/columns/product-availability-states.d.ts +65 -0
- package/dist/columns/product-availability-states.d.ts.map +1 -0
- package/dist/columns/product-availability-states.js +81 -0
- package/dist/columns/product-availability-states.js.map +1 -0
- package/dist/columns/product-availability.d.ts +21 -0
- package/dist/columns/product-availability.d.ts.map +1 -0
- package/dist/columns/product-availability.js +24 -0
- package/dist/columns/product-availability.js.map +1 -0
- package/dist/columns/product-booking-rules.d.ts +45 -0
- package/dist/columns/product-booking-rules.d.ts.map +1 -0
- package/dist/columns/product-booking-rules.js +55 -0
- package/dist/columns/product-booking-rules.js.map +1 -0
- package/dist/columns/product-category-assignments.d.ts +17 -0
- package/dist/columns/product-category-assignments.d.ts.map +1 -0
- package/dist/columns/product-category-assignments.js +20 -0
- package/dist/columns/product-category-assignments.js.map +1 -0
- package/dist/columns/product-extensions.d.ts +24 -0
- package/dist/columns/product-extensions.d.ts.map +1 -0
- package/dist/columns/product-extensions.js +27 -0
- package/dist/columns/product-extensions.js.map +1 -0
- package/dist/columns/product-media.d.ts +19 -0
- package/dist/columns/product-media.d.ts.map +1 -0
- package/dist/columns/product-media.js +22 -0
- package/dist/columns/product-media.js.map +1 -0
- package/dist/columns/product-overrides.d.ts +19 -0
- package/dist/columns/product-overrides.d.ts.map +1 -0
- package/dist/columns/product-overrides.js +22 -0
- package/dist/columns/product-overrides.js.map +1 -0
- package/dist/columns/product-preferences.d.ts +51 -0
- package/dist/columns/product-preferences.d.ts.map +1 -0
- package/dist/columns/product-preferences.js +59 -0
- package/dist/columns/product-preferences.js.map +1 -0
- package/dist/columns/product-publish-settings.d.ts +18 -0
- package/dist/columns/product-publish-settings.d.ts.map +1 -0
- package/dist/columns/product-publish-settings.js +21 -0
- package/dist/columns/product-publish-settings.js.map +1 -0
- package/dist/columns/product-rate-plans.d.ts +28 -0
- package/dist/columns/product-rate-plans.d.ts.map +1 -0
- package/dist/columns/product-rate-plans.js +33 -0
- package/dist/columns/product-rate-plans.js.map +1 -0
- package/dist/columns/product-translations.d.ts +24 -0
- package/dist/columns/product-translations.d.ts.map +1 -0
- package/dist/columns/product-translations.js +27 -0
- package/dist/columns/product-translations.js.map +1 -0
- package/dist/columns/product-versions.d.ts +19 -0
- package/dist/columns/product-versions.d.ts.map +1 -0
- package/dist/columns/product-versions.js +22 -0
- package/dist/columns/product-versions.js.map +1 -0
- package/dist/columns/product-visibility.d.ts +16 -0
- package/dist/columns/product-visibility.d.ts.map +1 -0
- package/dist/columns/product-visibility.js +19 -0
- package/dist/columns/product-visibility.js.map +1 -0
- package/dist/columns/product.d.ts +56 -0
- package/dist/columns/product.d.ts.map +1 -0
- package/dist/columns/product.js +44 -0
- package/dist/columns/product.js.map +1 -0
- package/dist/columns/room.d.ts +117 -0
- package/dist/columns/room.d.ts.map +1 -0
- package/dist/columns/room.js +86 -0
- package/dist/columns/room.js.map +1 -0
- package/dist/columns/ship.d.ts +22 -0
- package/dist/columns/ship.d.ts.map +1 -0
- package/dist/columns/ship.js +25 -0
- package/dist/columns/ship.js.map +1 -0
- package/dist/columns/tags.d.ts +33 -0
- package/dist/columns/tags.d.ts.map +1 -0
- package/dist/columns/tags.js +38 -0
- package/dist/columns/tags.js.map +1 -0
- package/dist/columns/transport.d.ts +53 -0
- package/dist/columns/transport.d.ts.map +1 -0
- package/dist/columns/transport.js +62 -0
- package/dist/columns/transport.js.map +1 -0
- package/dist/connection-config.d.ts +101 -0
- package/dist/connection-config.d.ts.map +1 -0
- package/dist/connection-config.js +106 -0
- package/dist/connection-config.js.map +1 -0
- package/dist/crud.d.ts +87 -0
- package/dist/crud.d.ts.map +1 -0
- package/dist/crud.js +190 -0
- package/dist/crud.js.map +1 -0
- package/dist/helpers.d.ts +33 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +49 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +155 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +5 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/typeid-column.d.ts +75 -0
- package/dist/lib/typeid-column.d.ts.map +1 -0
- package/dist/lib/typeid-column.js +92 -0
- package/dist/lib/typeid-column.js.map +1 -0
- package/dist/lib/typeid-core.d.ts +36 -0
- package/dist/lib/typeid-core.d.ts.map +1 -0
- package/dist/lib/typeid-core.js +67 -0
- package/dist/lib/typeid-core.js.map +1 -0
- package/dist/lib/typeid-prefixes.d.ts +342 -0
- package/dist/lib/typeid-prefixes.d.ts.map +1 -0
- package/dist/lib/typeid-prefixes.js +379 -0
- package/dist/lib/typeid-prefixes.js.map +1 -0
- package/dist/lib/typeid-schemas.d.ts +206 -0
- package/dist/lib/typeid-schemas.d.ts.map +1 -0
- package/dist/lib/typeid-schemas.js +207 -0
- package/dist/lib/typeid-schemas.js.map +1 -0
- package/dist/lib/typeid-zod.d.ts +16 -0
- package/dist/lib/typeid-zod.d.ts.map +1 -0
- package/dist/lib/typeid-zod.js +29 -0
- package/dist/lib/typeid-zod.js.map +1 -0
- package/dist/lib/typeid.d.ts +2 -0
- package/dist/lib/typeid.d.ts.map +1 -0
- package/dist/lib/typeid.js +6 -0
- package/dist/lib/typeid.js.map +1 -0
- package/dist/lifecycle.d.ts +24 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +30 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/links.d.ts +22 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +281 -0
- package/dist/links.js.map +1 -0
- package/dist/operators.d.ts +9 -0
- package/dist/operators.d.ts.map +1 -0
- package/dist/operators.js +9 -0
- package/dist/operators.js.map +1 -0
- package/dist/outbox.d.ts +87 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +245 -0
- package/dist/outbox.js.map +1 -0
- package/dist/primitives/catalog-schemas.d.ts +101 -0
- package/dist/primitives/catalog-schemas.d.ts.map +1 -0
- package/dist/primitives/catalog-schemas.js +69 -0
- package/dist/primitives/catalog-schemas.js.map +1 -0
- package/dist/primitives/catalog.d.ts +47 -0
- package/dist/primitives/catalog.d.ts.map +1 -0
- package/dist/primitives/catalog.js +94 -0
- package/dist/primitives/catalog.js.map +1 -0
- package/dist/primitives/index.d.ts +4 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +4 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/primitives/offers.d.ts +224 -0
- package/dist/primitives/offers.d.ts.map +1 -0
- package/dist/primitives/offers.js +132 -0
- package/dist/primitives/offers.js.map +1 -0
- package/dist/queries/index.d.ts +18 -0
- package/dist/queries/index.d.ts.map +1 -0
- package/dist/queries/index.js +30 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/locks.d.ts +5 -0
- package/dist/runtime/locks.d.ts.map +1 -0
- package/dist/runtime/locks.js +36 -0
- package/dist/runtime/locks.js.map +1 -0
- package/dist/schema/00_ensure_schemas.d.ts +5 -0
- package/dist/schema/00_ensure_schemas.d.ts.map +1 -0
- package/dist/schema/00_ensure_schemas.js +6 -0
- package/dist/schema/00_ensure_schemas.js.map +1 -0
- package/dist/schema/aggregate-snapshots.d.ts +97 -0
- package/dist/schema/aggregate-snapshots.d.ts.map +1 -0
- package/dist/schema/aggregate-snapshots.js +27 -0
- package/dist/schema/aggregate-snapshots.js.map +1 -0
- package/dist/schema/iam/apikey.d.ts +396 -0
- package/dist/schema/iam/apikey.d.ts.map +1 -0
- package/dist/schema/iam/apikey.js +41 -0
- package/dist/schema/iam/apikey.js.map +1 -0
- package/dist/schema/iam/auth.d.ts +1026 -0
- package/dist/schema/iam/auth.d.ts.map +1 -0
- package/dist/schema/iam/auth.js +138 -0
- package/dist/schema/iam/auth.js.map +1 -0
- package/dist/schema/iam/cloud_auth.d.ts +446 -0
- package/dist/schema/iam/cloud_auth.d.ts.map +1 -0
- package/dist/schema/iam/cloud_auth.js +46 -0
- package/dist/schema/iam/cloud_auth.js.map +1 -0
- package/dist/schema/iam/index.d.ts +8 -0
- package/dist/schema/iam/index.d.ts.map +1 -0
- package/dist/schema/iam/index.js +8 -0
- package/dist/schema/iam/index.js.map +1 -0
- package/dist/schema/iam/invitations.d.ts +173 -0
- package/dist/schema/iam/invitations.d.ts.map +1 -0
- package/dist/schema/iam/invitations.js +27 -0
- package/dist/schema/iam/invitations.js.map +1 -0
- package/dist/schema/iam/kms.d.ts +53 -0
- package/dist/schema/iam/kms.d.ts.map +1 -0
- package/dist/schema/iam/kms.js +40 -0
- package/dist/schema/iam/kms.js.map +1 -0
- package/dist/schema/iam/roles.d.ts +12 -0
- package/dist/schema/iam/roles.d.ts.map +1 -0
- package/dist/schema/iam/roles.js +12 -0
- package/dist/schema/iam/roles.js.map +1 -0
- package/dist/schema/iam/user_profiles.d.ts +442 -0
- package/dist/schema/iam/user_profiles.d.ts.map +1 -0
- package/dist/schema/iam/user_profiles.js +125 -0
- package/dist/schema/iam/user_profiles.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/infra/domains.d.ts +609 -0
- package/dist/schema/infra/domains.d.ts.map +1 -0
- package/dist/schema/infra/domains.js +108 -0
- package/dist/schema/infra/domains.js.map +1 -0
- package/dist/schema/infra/email_domain_records.d.ts +255 -0
- package/dist/schema/infra/email_domain_records.d.ts.map +1 -0
- package/dist/schema/infra/email_domain_records.js +65 -0
- package/dist/schema/infra/email_domain_records.js.map +1 -0
- package/dist/schema/infra/event_outbox.d.ts +232 -0
- package/dist/schema/infra/event_outbox.d.ts.map +1 -0
- package/dist/schema/infra/event_outbox.js +59 -0
- package/dist/schema/infra/event_outbox.js.map +1 -0
- package/dist/schema/infra/idempotency_keys.d.ts +186 -0
- package/dist/schema/infra/idempotency_keys.d.ts.map +1 -0
- package/dist/schema/infra/idempotency_keys.js +40 -0
- package/dist/schema/infra/idempotency_keys.js.map +1 -0
- package/dist/schema/infra/index.d.ts +9 -0
- package/dist/schema/infra/index.d.ts.map +1 -0
- package/dist/schema/infra/index.js +10 -0
- package/dist/schema/infra/index.js.map +1 -0
- package/dist/schema/infra/public_document_delivery_grants.d.ts +356 -0
- package/dist/schema/infra/public_document_delivery_grants.d.ts.map +1 -0
- package/dist/schema/infra/public_document_delivery_grants.js +36 -0
- package/dist/schema/infra/public_document_delivery_grants.js.map +1 -0
- package/dist/schema/infra/rate_limit_buckets.d.ts +138 -0
- package/dist/schema/infra/rate_limit_buckets.d.ts.map +1 -0
- package/dist/schema/infra/rate_limit_buckets.js +52 -0
- package/dist/schema/infra/rate_limit_buckets.js.map +1 -0
- package/dist/schema/infra/webhook_deliveries.d.ts +572 -0
- package/dist/schema/infra/webhook_deliveries.d.ts.map +1 -0
- package/dist/schema/infra/webhook_deliveries.js +136 -0
- package/dist/schema/infra/webhook_deliveries.js.map +1 -0
- package/dist/schema/infra/webhook_subscriptions.d.ts +284 -0
- package/dist/schema/infra/webhook_subscriptions.d.ts.map +1 -0
- package/dist/schema/infra/webhook_subscriptions.js +64 -0
- package/dist/schema/infra/webhook_subscriptions.js.map +1 -0
- package/dist/schema/infra/write_intents.d.ts +185 -0
- package/dist/schema/infra/write_intents.d.ts.map +1 -0
- package/dist/schema/infra/write_intents.js +50 -0
- package/dist/schema/infra/write_intents.js.map +1 -0
- package/dist/schema/voyant/bookings.d.ts +2 -0
- package/dist/schema/voyant/bookings.d.ts.map +1 -0
- package/dist/schema/voyant/bookings.js +2 -0
- package/dist/schema/voyant/bookings.js.map +1 -0
- package/dist/schema/voyant/finance.d.ts +2 -0
- package/dist/schema/voyant/finance.d.ts.map +1 -0
- package/dist/schema/voyant/finance.js +2 -0
- package/dist/schema/voyant/finance.js.map +1 -0
- package/dist/test-utils.d.ts +17 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +56 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/transaction-capability.d.ts +11 -0
- package/dist/transaction-capability.d.ts.map +1 -0
- package/dist/transaction-capability.js +17 -0
- package/dist/transaction-capability.js.map +1 -0
- package/dist/transaction.d.ts +31 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +72 -0
- package/dist/transaction.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +19 -0
- package/dist/utils.js.map +1 -0
- package/dist/write-intents.d.ts +51 -0
- package/dist/write-intents.d.ts.map +1 -0
- package/dist/write-intents.js +95 -0
- package/dist/write-intents.js.map +1 -0
- package/package.json +306 -0
package/dist/links.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime link service backed by Drizzle — executes raw SQL against the
|
|
3
|
+
* dynamically-named pivot tables materialised from {@link LinkDefinition}s.
|
|
4
|
+
*/
|
|
5
|
+
import { generateLinkTableSql, resolveLinkFromSpec } from "@voyant-travel/core";
|
|
6
|
+
import { sql } from "drizzle-orm";
|
|
7
|
+
import { newIdFromPrefix } from "./lib/typeid.js";
|
|
8
|
+
/**
|
|
9
|
+
* TypeID prefix used for link row IDs.
|
|
10
|
+
* Short, fixed, and outside the module-owned prefix list in lib/typeid.ts.
|
|
11
|
+
*/
|
|
12
|
+
const LINK_ID_PREFIX = "lnk";
|
|
13
|
+
function toDate(v) {
|
|
14
|
+
return v instanceof Date ? v : new Date(v);
|
|
15
|
+
}
|
|
16
|
+
function toNullableDate(v) {
|
|
17
|
+
if (v === null)
|
|
18
|
+
return null;
|
|
19
|
+
return v instanceof Date ? v : new Date(v);
|
|
20
|
+
}
|
|
21
|
+
function unique(ids) {
|
|
22
|
+
return Array.from(new Set(ids));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Collapse the singular + plural ID filters for one side into a single
|
|
26
|
+
* constraint:
|
|
27
|
+
*
|
|
28
|
+
* - `undefined` — the side is unconstrained.
|
|
29
|
+
* - `null` — the filter can never match (empty array, or a singular ID that
|
|
30
|
+
* isn't in the plural set); callers short-circuit to `[]` without querying.
|
|
31
|
+
* - `string[]` — match any of these (deduped) IDs.
|
|
32
|
+
*
|
|
33
|
+
* Falsy singular IDs are ignored, matching the historical truthy check.
|
|
34
|
+
*/
|
|
35
|
+
function normalizeIdFilter(single, many) {
|
|
36
|
+
if (single && many) {
|
|
37
|
+
return many.includes(single) ? [single] : null;
|
|
38
|
+
}
|
|
39
|
+
if (single)
|
|
40
|
+
return [single];
|
|
41
|
+
if (many) {
|
|
42
|
+
const ids = unique(many);
|
|
43
|
+
return ids.length === 0 ? null : ids;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a (possibly batched) list filter against a read-only link.
|
|
49
|
+
*
|
|
50
|
+
* Read-only resolvers only understand singular `leftId`/`rightId` filters,
|
|
51
|
+
* so a batched filter fans out one resolver call per ID on one side and
|
|
52
|
+
* applies any remaining ID-set constraint locally.
|
|
53
|
+
*/
|
|
54
|
+
async function listReadOnly(ro, leftIds, rightIds) {
|
|
55
|
+
const singleLeft = leftIds && leftIds.length === 1 ? leftIds[0] : undefined;
|
|
56
|
+
const singleRight = rightIds && rightIds.length === 1 ? rightIds[0] : undefined;
|
|
57
|
+
// At most one ID per side — the resolver's native filter shape.
|
|
58
|
+
if ((!leftIds || singleLeft !== undefined) && (!rightIds || singleRight !== undefined)) {
|
|
59
|
+
const filter = {};
|
|
60
|
+
if (singleLeft !== undefined)
|
|
61
|
+
filter.leftId = singleLeft;
|
|
62
|
+
if (singleRight !== undefined)
|
|
63
|
+
filter.rightId = singleRight;
|
|
64
|
+
return ro.list(filter);
|
|
65
|
+
}
|
|
66
|
+
const fanLeft = leftIds !== undefined && singleLeft === undefined;
|
|
67
|
+
const fanIds = (fanLeft ? leftIds : rightIds);
|
|
68
|
+
const batches = await Promise.all(fanIds.map((id) => {
|
|
69
|
+
const filter = fanLeft
|
|
70
|
+
? { leftId: id }
|
|
71
|
+
: { rightId: id };
|
|
72
|
+
if (fanLeft && singleRight !== undefined)
|
|
73
|
+
filter.rightId = singleRight;
|
|
74
|
+
if (!fanLeft && singleLeft !== undefined)
|
|
75
|
+
filter.leftId = singleLeft;
|
|
76
|
+
return ro.list(filter);
|
|
77
|
+
}));
|
|
78
|
+
let rows = batches.flat();
|
|
79
|
+
// Both sides batched — the right-side set couldn't be pushed into the
|
|
80
|
+
// resolver calls, so apply it here.
|
|
81
|
+
if (fanLeft && rightIds && singleRight === undefined) {
|
|
82
|
+
const allowed = new Set(rightIds);
|
|
83
|
+
rows = rows.filter((row) => allowed.has(row.rightId));
|
|
84
|
+
}
|
|
85
|
+
return rows;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Create a runtime {@link LinkService} for the given set of link definitions.
|
|
89
|
+
*
|
|
90
|
+
* The service supports both a positional API (`create(linkKey, leftId, rightId)`)
|
|
91
|
+
* and a Medusa-style spec API (`create({ moduleA: { a_id }, moduleB: { b_id } })`)
|
|
92
|
+
* resolved against the provided definitions.
|
|
93
|
+
*/
|
|
94
|
+
export function createLinkService(getDb, definitions) {
|
|
95
|
+
const byKey = new Map();
|
|
96
|
+
for (const def of definitions) {
|
|
97
|
+
if (byKey.has(def.tableName)) {
|
|
98
|
+
throw new Error(`createLinkService: duplicate link definition for table "${def.tableName}"`);
|
|
99
|
+
}
|
|
100
|
+
byKey.set(def.tableName, def);
|
|
101
|
+
}
|
|
102
|
+
function lookupByKey(linkKey) {
|
|
103
|
+
const def = byKey.get(linkKey);
|
|
104
|
+
if (!def) {
|
|
105
|
+
throw new Error(`createLinkService: unknown link key "${linkKey}"`);
|
|
106
|
+
}
|
|
107
|
+
return def;
|
|
108
|
+
}
|
|
109
|
+
function resolveArgs(keyOrSpec, leftId, rightId) {
|
|
110
|
+
if (typeof keyOrSpec === "string") {
|
|
111
|
+
if (leftId === undefined || rightId === undefined) {
|
|
112
|
+
throw new Error("createLinkService: positional API requires linkKey, leftId, and rightId");
|
|
113
|
+
}
|
|
114
|
+
return { definition: lookupByKey(keyOrSpec), leftId, rightId };
|
|
115
|
+
}
|
|
116
|
+
return resolveLinkFromSpec(keyOrSpec, definitions);
|
|
117
|
+
}
|
|
118
|
+
function rowFromRaw(def, raw) {
|
|
119
|
+
const leftId = raw[def.leftColumn];
|
|
120
|
+
const rightId = raw[def.rightColumn];
|
|
121
|
+
if (typeof leftId !== "string" || typeof rightId !== "string") {
|
|
122
|
+
throw new Error(`createLinkService: malformed row returned from "${def.tableName}"`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
id: raw.id,
|
|
126
|
+
leftId,
|
|
127
|
+
rightId,
|
|
128
|
+
createdAt: toDate(raw.created_at),
|
|
129
|
+
updatedAt: toDate(raw.updated_at),
|
|
130
|
+
deletedAt: toNullableDate(raw.deleted_at),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function executeRows(query) {
|
|
134
|
+
// biome-ignore lint/suspicious/noExplicitAny: drizzle's execute() return type varies by adapter -- owner: db; existing suppression is intentional pending typed cleanup.
|
|
135
|
+
const result = await getDb().execute(query);
|
|
136
|
+
if (Array.isArray(result))
|
|
137
|
+
return result;
|
|
138
|
+
if (result && Array.isArray(result.rows))
|
|
139
|
+
return result.rows;
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
async function createImpl(spec) {
|
|
143
|
+
const { definition: def, leftId, rightId } = spec;
|
|
144
|
+
const table = sql.identifier(def.tableName);
|
|
145
|
+
const leftCol = sql.identifier(def.leftColumn);
|
|
146
|
+
const rightCol = sql.identifier(def.rightColumn);
|
|
147
|
+
const id = newIdFromPrefix(LINK_ID_PREFIX);
|
|
148
|
+
// Resurrect any soft-deleted pair first — otherwise the partial unique
|
|
149
|
+
// index would prevent the INSERT, and we'd fail the "idempotent" contract.
|
|
150
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
151
|
+
const restoreQuery = sql `UPDATE ${table}
|
|
152
|
+
SET "deleted_at" = NULL, "updated_at" = now()
|
|
153
|
+
WHERE ${leftCol} = ${leftId} AND ${rightCol} = ${rightId} AND "deleted_at" IS NOT NULL
|
|
154
|
+
RETURNING *`;
|
|
155
|
+
const restored = await executeRows(restoreQuery);
|
|
156
|
+
if (restored.length > 0 && restored[0]) {
|
|
157
|
+
return rowFromRaw(def, restored[0]);
|
|
158
|
+
}
|
|
159
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
160
|
+
const insertQuery = sql `INSERT INTO ${table}
|
|
161
|
+
("id", ${leftCol}, ${rightCol}, "created_at", "updated_at", "deleted_at")
|
|
162
|
+
VALUES (${id}, ${leftId}, ${rightId}, now(), now(), NULL)
|
|
163
|
+
ON CONFLICT DO NOTHING
|
|
164
|
+
RETURNING *`;
|
|
165
|
+
const inserted = await executeRows(insertQuery);
|
|
166
|
+
if (inserted.length > 0 && inserted[0]) {
|
|
167
|
+
return rowFromRaw(def, inserted[0]);
|
|
168
|
+
}
|
|
169
|
+
// Conflict — a row (or matching pair) already exists. Fetch the active one.
|
|
170
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
171
|
+
const fetchQuery = sql `SELECT * FROM ${table}
|
|
172
|
+
WHERE ${leftCol} = ${leftId} AND ${rightCol} = ${rightId} AND "deleted_at" IS NULL
|
|
173
|
+
LIMIT 1`;
|
|
174
|
+
const existing = await executeRows(fetchQuery);
|
|
175
|
+
if (existing.length > 0 && existing[0]) {
|
|
176
|
+
return rowFromRaw(def, existing[0]);
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`createLinkService: could not create or find link row in "${def.tableName}"`);
|
|
179
|
+
}
|
|
180
|
+
async function dismissImpl(spec) {
|
|
181
|
+
const { definition: def, leftId, rightId } = spec;
|
|
182
|
+
if (def.readOnly) {
|
|
183
|
+
throw new Error(`createLinkService: read-only link "${def.tableName}" cannot be dismissed`);
|
|
184
|
+
}
|
|
185
|
+
const table = sql.identifier(def.tableName);
|
|
186
|
+
const leftCol = sql.identifier(def.leftColumn);
|
|
187
|
+
const rightCol = sql.identifier(def.rightColumn);
|
|
188
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
189
|
+
const query = sql `UPDATE ${table}
|
|
190
|
+
SET "deleted_at" = now(), "updated_at" = now()
|
|
191
|
+
WHERE ${leftCol} = ${leftId} AND ${rightCol} = ${rightId} AND "deleted_at" IS NULL`;
|
|
192
|
+
await executeRows(query);
|
|
193
|
+
}
|
|
194
|
+
async function deleteImpl(spec) {
|
|
195
|
+
const { definition: def, leftId, rightId } = spec;
|
|
196
|
+
if (def.readOnly) {
|
|
197
|
+
throw new Error(`createLinkService: read-only link "${def.tableName}" cannot be deleted`);
|
|
198
|
+
}
|
|
199
|
+
const table = sql.identifier(def.tableName);
|
|
200
|
+
const leftCol = sql.identifier(def.leftColumn);
|
|
201
|
+
const rightCol = sql.identifier(def.rightColumn);
|
|
202
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
203
|
+
const query = sql `DELETE FROM ${table}
|
|
204
|
+
WHERE ${leftCol} = ${leftId} AND ${rightCol} = ${rightId}`;
|
|
205
|
+
await executeRows(query);
|
|
206
|
+
}
|
|
207
|
+
async function list(linkKey, filter = {}) {
|
|
208
|
+
const def = lookupByKey(linkKey);
|
|
209
|
+
const leftIds = normalizeIdFilter(filter.leftId, filter.leftIds);
|
|
210
|
+
const rightIds = normalizeIdFilter(filter.rightId, filter.rightIds);
|
|
211
|
+
// A provably-empty filter (e.g. `leftIds: []`) can never match — skip the
|
|
212
|
+
// query (and on Workers, the subrequest) entirely.
|
|
213
|
+
if (leftIds === null || rightIds === null)
|
|
214
|
+
return [];
|
|
215
|
+
if (def.readOnly) {
|
|
216
|
+
return listReadOnly(def.readOnly, leftIds, rightIds);
|
|
217
|
+
}
|
|
218
|
+
const table = sql.identifier(def.tableName);
|
|
219
|
+
const leftCol = sql.identifier(def.leftColumn);
|
|
220
|
+
const rightCol = sql.identifier(def.rightColumn);
|
|
221
|
+
// NOTE: a plain array embedded in a drizzle sql template is flattened
|
|
222
|
+
// into per-element chunks; sql.param() binds it as ONE array parameter,
|
|
223
|
+
// so a batched filter stays a single `col = ANY($1)` query.
|
|
224
|
+
const idClause = (col, ids) =>
|
|
225
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
226
|
+
ids.length === 1 ? sql `${col} = ${ids[0]}` : sql `${col} = ANY(${sql.param(ids)})`;
|
|
227
|
+
const whereClauses = [sql `"deleted_at" IS NULL`];
|
|
228
|
+
if (leftIds)
|
|
229
|
+
whereClauses.push(idClause(leftCol, leftIds));
|
|
230
|
+
if (rightIds)
|
|
231
|
+
whereClauses.push(idClause(rightCol, rightIds));
|
|
232
|
+
// Manually join clauses with AND.
|
|
233
|
+
let whereSql = whereClauses[0];
|
|
234
|
+
for (let i = 1; i < whereClauses.length; i++) {
|
|
235
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
236
|
+
whereSql = sql `${whereSql} AND ${whereClauses[i]}`;
|
|
237
|
+
}
|
|
238
|
+
// agent-quality: raw-sql reviewed -- owner: db; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
239
|
+
const query = sql `SELECT * FROM ${table}
|
|
240
|
+
WHERE ${whereSql}
|
|
241
|
+
ORDER BY "created_at" ASC`;
|
|
242
|
+
const rows = await executeRows(query);
|
|
243
|
+
return rows.map((r) => rowFromRaw(def, r));
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
async create(keyOrSpec, leftId, rightId) {
|
|
247
|
+
const spec = resolveArgs(keyOrSpec, leftId, rightId);
|
|
248
|
+
if (spec.definition.readOnly) {
|
|
249
|
+
throw new Error(`createLinkService: read-only link "${spec.definition.tableName}" cannot be created`);
|
|
250
|
+
}
|
|
251
|
+
return createImpl(spec);
|
|
252
|
+
},
|
|
253
|
+
async dismiss(keyOrSpec, leftId, rightId) {
|
|
254
|
+
return dismissImpl(resolveArgs(keyOrSpec, leftId, rightId));
|
|
255
|
+
},
|
|
256
|
+
async delete(keyOrSpec, leftId, rightId) {
|
|
257
|
+
return deleteImpl(resolveArgs(keyOrSpec, leftId, rightId));
|
|
258
|
+
},
|
|
259
|
+
list,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Materialise every link definition's pivot table (CREATE TABLE + indexes).
|
|
264
|
+
* Intended for use by a `db:sync-links` CLI command in templates.
|
|
265
|
+
*
|
|
266
|
+
* Runs each DDL statement individually against the provided Drizzle client.
|
|
267
|
+
*/
|
|
268
|
+
export async function syncLinks(db, definitions) {
|
|
269
|
+
for (const def of definitions) {
|
|
270
|
+
if (def.readOnly)
|
|
271
|
+
continue;
|
|
272
|
+
const { createTable, indexes } = generateLinkTableSql(def);
|
|
273
|
+
// biome-ignore lint/suspicious/noExplicitAny: drizzle adapter execute typing varies -- owner: db; existing suppression is intentional pending typed cleanup.
|
|
274
|
+
await db.execute(sql.raw(createTable));
|
|
275
|
+
for (const idx of indexes) {
|
|
276
|
+
// biome-ignore lint/suspicious/noExplicitAny: drizzle adapter execute typing varies -- owner: db; existing suppression is intentional pending typed cleanup.
|
|
277
|
+
await db.execute(sql.raw(idx));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export drizzle-orm operators to ensure type compatibility
|
|
3
|
+
* when used with tables from this package.
|
|
4
|
+
*
|
|
5
|
+
* Always import operators from this file (or @voyant-travel/db/operators)
|
|
6
|
+
* when using them with @voyant-travel/db tables to avoid type conflicts.
|
|
7
|
+
*/
|
|
8
|
+
export { and, arrayContained, arrayContains, arrayOverlaps, asc, avg, avgDistinct, between, count, countDistinct, desc, eq, exists, gt, gte, ilike, inArray, isNotNull, isNull, like, lt, lte, max, min, ne, not, notBetween, notExists, notIlike, notInArray, notLike, or, sql, sum, sumDistinct, } from "drizzle-orm";
|
|
9
|
+
//# sourceMappingURL=operators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operators.d.ts","sourceRoot":"","sources":["../src/operators.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,GAAG,EACH,cAAc,EACd,aAAa,EACb,aAAa,EACb,GAAG,EACH,GAAG,EACH,WAAW,EACX,OAAO,EACP,KAAK,EACL,aAAa,EACb,IAAI,EACJ,EAAE,EACF,MAAM,EACN,EAAE,EACF,GAAG,EACH,KAAK,EACL,OAAO,EACP,SAAS,EACT,MAAM,EACN,IAAI,EACJ,EAAE,EACF,GAAG,EACH,GAAG,EACH,GAAG,EACH,EAAE,EACF,GAAG,EACH,UAAU,EACV,SAAS,EACT,QAAQ,EACR,UAAU,EACV,OAAO,EACP,EAAE,EACF,GAAG,EACH,GAAG,EACH,WAAW,GACZ,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export drizzle-orm operators to ensure type compatibility
|
|
3
|
+
* when used with tables from this package.
|
|
4
|
+
*
|
|
5
|
+
* Always import operators from this file (or @voyant-travel/db/operators)
|
|
6
|
+
* when using them with @voyant-travel/db tables to avoid type conflicts.
|
|
7
|
+
*/
|
|
8
|
+
export { and, arrayContained, arrayContains, arrayOverlaps, asc, avg, avgDistinct, between, count, countDistinct, desc, eq, exists, gt, gte, ilike, inArray, isNotNull, isNull, like, lt, lte, max, min, ne, not, notBetween, notExists, notIlike, notInArray, notLike, or, sql, sum, sumDistinct, } from "drizzle-orm";
|
|
9
|
+
//# sourceMappingURL=operators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operators.js","sourceRoot":"","sources":["../src/operators.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,GAAG,EACH,cAAc,EACd,aAAa,EACb,aAAa,EACb,GAAG,EACH,GAAG,EACH,WAAW,EACX,OAAO,EACP,KAAK,EACL,aAAa,EACb,IAAI,EACJ,EAAE,EACF,MAAM,EACN,EAAE,EACF,GAAG,EACH,KAAK,EACL,OAAO,EACP,SAAS,EACT,MAAM,EACN,IAAI,EACJ,EAAE,EACF,GAAG,EACH,GAAG,EACH,GAAG,EACH,EAAE,EACF,GAAG,EACH,UAAU,EACV,SAAS,EACT,QAAQ,EACR,UAAU,EACV,OAAO,EACP,EAAE,EACF,GAAG,EACH,GAAG,EACH,WAAW,GACZ,MAAM,aAAa,CAAA"}
|
package/dist/outbox.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { EventBus, EventEnvelope, EventMetadata, OutboxEventStore } from "@voyant-travel/core";
|
|
2
|
+
import { type EventOutboxRow } from "./schema/infra/event_outbox.js";
|
|
3
|
+
import type { DrizzleClient } from "./types.js";
|
|
4
|
+
export interface DrainOutboxOptions {
|
|
5
|
+
/** Max rows claimed per drain pass. Default 25. */
|
|
6
|
+
limit?: number;
|
|
7
|
+
/**
|
|
8
|
+
* How long a claimed row stays invisible to other drains. Must exceed
|
|
9
|
+
* the worst-case delivery time of one event (all subscribers, each
|
|
10
|
+
* bounded by the bus's per-handler timeout). A crashed claimer's rows
|
|
11
|
+
* simply become due again after this window. Default 120s.
|
|
12
|
+
*/
|
|
13
|
+
visibilityTimeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DrainOutboxResult {
|
|
16
|
+
claimed: number;
|
|
17
|
+
delivered: number;
|
|
18
|
+
retried: number;
|
|
19
|
+
deadLettered: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Postgres-backed {@link OutboxEventStore} for `createEventBus`'s durable
|
|
23
|
+
* emit path. `getDb` is called per operation so the store can be bound
|
|
24
|
+
* to a per-request client (Workers) or a long-lived one (Node).
|
|
25
|
+
*/
|
|
26
|
+
export declare function createOutboxEventStore(getDb: () => DrizzleClient): OutboxEventStore;
|
|
27
|
+
/**
|
|
28
|
+
* Persist envelopes as pending outbox rows. Pass a transaction handle to
|
|
29
|
+
* capture events atomically with the domain write ("transactional
|
|
30
|
+
* outbox" proper):
|
|
31
|
+
*
|
|
32
|
+
* await db.transaction(async (tx) => {
|
|
33
|
+
* await tx.insert(bookings).values(...)
|
|
34
|
+
* await insertOutboxEvents(tx, [{ name: "booking.created", data, metadata }])
|
|
35
|
+
* })
|
|
36
|
+
* // post-commit: the drain (or a waitUntil kick) delivers.
|
|
37
|
+
*
|
|
38
|
+
* Duplicate `metadata.eventId`s are skipped (ON CONFLICT DO NOTHING) —
|
|
39
|
+
* re-emits and webhook redeliveries capture once. Envelopes without an
|
|
40
|
+
* eventId get one stamped here.
|
|
41
|
+
*/
|
|
42
|
+
export declare function insertOutboxEvents(db: DrizzleClient, envelopes: ReadonlyArray<Pick<EventEnvelope, "name" | "data"> & {
|
|
43
|
+
metadata?: EventMetadata;
|
|
44
|
+
emittedAt?: string;
|
|
45
|
+
}>): Promise<EventOutboxRow[]>;
|
|
46
|
+
/** Mark a row delivered. */
|
|
47
|
+
export declare function completeOutboxEvent(db: DrizzleClient, id: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Record a failed delivery: reschedules with exponential backoff
|
|
50
|
+
* (5s · 2^attempts, capped at 15min, ±20% jitter) or dead-letters as
|
|
51
|
+
* `failed` once `max_attempts` is exhausted. Single statement — safe on
|
|
52
|
+
* neon-http. Returns the resulting status when the row exists.
|
|
53
|
+
*/
|
|
54
|
+
export declare function failOutboxEvent(db: DrizzleClient, id: string, error: string): Promise<"pending" | "failed" | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Atomically claim due pending rows: bumps `attempts` and pushes
|
|
57
|
+
* `next_attempt_at` past the visibility timeout so concurrent drains
|
|
58
|
+
* (and a crashed claimer's successor) never double-claim. One statement
|
|
59
|
+
* (`FOR UPDATE SKIP LOCKED` inside the subquery) — safe on neon-http.
|
|
60
|
+
*/
|
|
61
|
+
export declare function claimDueOutboxEvents(db: DrizzleClient, options?: DrainOutboxOptions): Promise<EventOutboxRow[]>;
|
|
62
|
+
/** Rebuild the bus envelope from a stored row. */
|
|
63
|
+
export declare function outboxRowToEnvelope(row: EventOutboxRow): EventEnvelope;
|
|
64
|
+
/**
|
|
65
|
+
* One drain pass: claim due rows, redeliver each through the bus, mark
|
|
66
|
+
* delivered / reschedule / dead-letter. Call from a scheduled handler
|
|
67
|
+
* (cron), a `waitUntil` kick after emit, or a long-running Node loop.
|
|
68
|
+
* Safe to run concurrently from multiple isolates (SKIP LOCKED claim).
|
|
69
|
+
*/
|
|
70
|
+
export declare function drainOutbox(db: DrizzleClient, bus: EventBus, options?: DrainOutboxOptions): Promise<DrainOutboxResult>;
|
|
71
|
+
/**
|
|
72
|
+
* Delete delivered rows past the retention window (delivered rows are
|
|
73
|
+
* receipts, not a log sink — long-term archival belongs elsewhere).
|
|
74
|
+
* Dead-lettered (`failed`) rows are NOT pruned: they represent lost
|
|
75
|
+
* deliveries until a human resolves them. Returns the deleted count.
|
|
76
|
+
*/
|
|
77
|
+
export declare function pruneDeliveredOutboxEvents(db: DrizzleClient, options?: {
|
|
78
|
+
olderThanDays?: number;
|
|
79
|
+
}): Promise<number>;
|
|
80
|
+
/** Row counts by status — observability for dashboards/health checks. */
|
|
81
|
+
export declare function getOutboxStats(db: DrizzleClient): Promise<{
|
|
82
|
+
pending: number;
|
|
83
|
+
delivered: number;
|
|
84
|
+
failed: number;
|
|
85
|
+
dueNow: number;
|
|
86
|
+
}>;
|
|
87
|
+
//# sourceMappingURL=outbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.d.ts","sourceRoot":"","sources":["../src/outbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAInG,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,gCAAgC,CAAA;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAU/C,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,aAAa,GAAG,gBAAgB,CAcnF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,aAAa,EACjB,SAAS,EAAE,aAAa,CACtB,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,aAAa,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CACxF,GACA,OAAO,CAAC,cAAc,EAAE,CAAC,CAoB3B;AAED,4BAA4B;AAC5B,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKtF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,aAAa,EACjB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC,CAoBtC;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,aAAa,EACjB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,cAAc,EAAE,CAAC,CAmB3B;AA+BD,kDAAkD;AAClD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,aAAa,CAOtE;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,aAAa,EACjB,GAAG,EAAE,QAAQ,EACb,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CA0C5B;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,aAAa,EACjB,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACvC,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED,yEAAyE;AACzE,wBAAsB,cAAc,CAClC,EAAE,EAAE,aAAa,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjF"}
|
package/dist/outbox.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { generateEventId } from "@voyant-travel/core";
|
|
2
|
+
import { eq, sql } from "drizzle-orm";
|
|
3
|
+
import { eventOutboxTable } from "./schema/infra/event_outbox.js";
|
|
4
|
+
/** Base delay before the first retry; doubles per attempt. */
|
|
5
|
+
const BACKOFF_BASE_MS = 5_000;
|
|
6
|
+
/** Retry delay ceiling. */
|
|
7
|
+
const BACKOFF_CAP_MS = 15 * 60 * 1000;
|
|
8
|
+
/**
|
|
9
|
+
* Postgres-backed {@link OutboxEventStore} for `createEventBus`'s durable
|
|
10
|
+
* emit path. `getDb` is called per operation so the store can be bound
|
|
11
|
+
* to a per-request client (Workers) or a long-lived one (Node).
|
|
12
|
+
*/
|
|
13
|
+
export function createOutboxEventStore(getDb) {
|
|
14
|
+
return {
|
|
15
|
+
async insert(envelope) {
|
|
16
|
+
const rows = (await insertOutboxEvents(getDb(), [envelope]));
|
|
17
|
+
const row = rows[0];
|
|
18
|
+
return row ? { id: row.id } : null;
|
|
19
|
+
},
|
|
20
|
+
async complete(id) {
|
|
21
|
+
await completeOutboxEvent(getDb(), id);
|
|
22
|
+
},
|
|
23
|
+
async fail(id, error) {
|
|
24
|
+
await failOutboxEvent(getDb(), id, error);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Persist envelopes as pending outbox rows. Pass a transaction handle to
|
|
30
|
+
* capture events atomically with the domain write ("transactional
|
|
31
|
+
* outbox" proper):
|
|
32
|
+
*
|
|
33
|
+
* await db.transaction(async (tx) => {
|
|
34
|
+
* await tx.insert(bookings).values(...)
|
|
35
|
+
* await insertOutboxEvents(tx, [{ name: "booking.created", data, metadata }])
|
|
36
|
+
* })
|
|
37
|
+
* // post-commit: the drain (or a waitUntil kick) delivers.
|
|
38
|
+
*
|
|
39
|
+
* Duplicate `metadata.eventId`s are skipped (ON CONFLICT DO NOTHING) —
|
|
40
|
+
* re-emits and webhook redeliveries capture once. Envelopes without an
|
|
41
|
+
* eventId get one stamped here.
|
|
42
|
+
*/
|
|
43
|
+
export async function insertOutboxEvents(db, envelopes) {
|
|
44
|
+
if (envelopes.length === 0)
|
|
45
|
+
return [];
|
|
46
|
+
const values = envelopes.map((envelope) => {
|
|
47
|
+
const metadata = { ...(envelope.metadata ?? {}) };
|
|
48
|
+
if (typeof metadata.eventId !== "string" || metadata.eventId.length === 0) {
|
|
49
|
+
metadata.eventId = generateEventId();
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
eventId: metadata.eventId,
|
|
53
|
+
name: envelope.name,
|
|
54
|
+
payload: envelope.data,
|
|
55
|
+
metadata,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
const rows = await db
|
|
59
|
+
.insert(eventOutboxTable)
|
|
60
|
+
.values(values)
|
|
61
|
+
.onConflictDoNothing({ target: eventOutboxTable.eventId })
|
|
62
|
+
.returning();
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
/** Mark a row delivered. */
|
|
66
|
+
export async function completeOutboxEvent(db, id) {
|
|
67
|
+
await db
|
|
68
|
+
.update(eventOutboxTable)
|
|
69
|
+
.set({ status: "delivered", deliveredAt: new Date(), lastError: null })
|
|
70
|
+
.where(eq(eventOutboxTable.id, id));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Record a failed delivery: reschedules with exponential backoff
|
|
74
|
+
* (5s · 2^attempts, capped at 15min, ±20% jitter) or dead-letters as
|
|
75
|
+
* `failed` once `max_attempts` is exhausted. Single statement — safe on
|
|
76
|
+
* neon-http. Returns the resulting status when the row exists.
|
|
77
|
+
*/
|
|
78
|
+
export async function failOutboxEvent(db, id, error) {
|
|
79
|
+
// Truncate pathological error strings — this is a diagnostic, not a log sink.
|
|
80
|
+
const lastError = error.length > 2000 ? `${error.slice(0, 2000)}…` : error;
|
|
81
|
+
const rows = (await db.execute(sql `
|
|
82
|
+
UPDATE ${eventOutboxTable}
|
|
83
|
+
SET
|
|
84
|
+
"last_error" = ${lastError},
|
|
85
|
+
"status" = CASE WHEN "attempts" >= "max_attempts" THEN 'failed' ELSE "status" END,
|
|
86
|
+
"next_attempt_at" = CASE
|
|
87
|
+
WHEN "attempts" >= "max_attempts" THEN "next_attempt_at"
|
|
88
|
+
ELSE now() + (
|
|
89
|
+
least(${BACKOFF_BASE_MS} * power(2, "attempts"), ${BACKOFF_CAP_MS})
|
|
90
|
+
* (0.8 + random() * 0.4)
|
|
91
|
+
) * interval '1 millisecond'
|
|
92
|
+
END
|
|
93
|
+
WHERE ${eventOutboxTable.id} = ${id}
|
|
94
|
+
RETURNING "status"
|
|
95
|
+
`));
|
|
96
|
+
const list = Array.isArray(rows) ? rows : rows.rows;
|
|
97
|
+
return list[0]?.status ?? null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Atomically claim due pending rows: bumps `attempts` and pushes
|
|
101
|
+
* `next_attempt_at` past the visibility timeout so concurrent drains
|
|
102
|
+
* (and a crashed claimer's successor) never double-claim. One statement
|
|
103
|
+
* (`FOR UPDATE SKIP LOCKED` inside the subquery) — safe on neon-http.
|
|
104
|
+
*/
|
|
105
|
+
export async function claimDueOutboxEvents(db, options = {}) {
|
|
106
|
+
const limit = options.limit ?? 25;
|
|
107
|
+
const visibilityMs = options.visibilityTimeoutMs ?? 120_000;
|
|
108
|
+
const result = (await db.execute(sql `
|
|
109
|
+
UPDATE ${eventOutboxTable}
|
|
110
|
+
SET
|
|
111
|
+
"attempts" = "attempts" + 1,
|
|
112
|
+
"next_attempt_at" = now() + (${visibilityMs} * interval '1 millisecond')
|
|
113
|
+
WHERE "id" IN (
|
|
114
|
+
SELECT "id" FROM ${eventOutboxTable}
|
|
115
|
+
WHERE "status" = 'pending' AND "next_attempt_at" <= now()
|
|
116
|
+
ORDER BY "next_attempt_at" ASC
|
|
117
|
+
LIMIT ${limit}
|
|
118
|
+
FOR UPDATE SKIP LOCKED
|
|
119
|
+
)
|
|
120
|
+
RETURNING *
|
|
121
|
+
`));
|
|
122
|
+
const rows = Array.isArray(result) ? result : (result.rows ?? []);
|
|
123
|
+
return rows.map(normalizeClaimedRow);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `db.execute` returns snake_case column names (raw SQL bypasses
|
|
127
|
+
* drizzle's column mapping) — normalize to the drizzle row shape.
|
|
128
|
+
*/
|
|
129
|
+
function normalizeClaimedRow(raw) {
|
|
130
|
+
const row = raw;
|
|
131
|
+
return {
|
|
132
|
+
id: row.id,
|
|
133
|
+
eventId: row.event_id ?? row.eventId,
|
|
134
|
+
name: row.name,
|
|
135
|
+
payload: row.payload,
|
|
136
|
+
metadata: row.metadata,
|
|
137
|
+
status: row.status,
|
|
138
|
+
attempts: row.attempts,
|
|
139
|
+
maxAttempts: row.max_attempts ?? row.maxAttempts,
|
|
140
|
+
nextAttemptAt: coerceDate(row.next_attempt_at ?? row.nextAttemptAt),
|
|
141
|
+
lastError: (row.last_error ?? row.lastError ?? null),
|
|
142
|
+
createdAt: coerceDate(row.created_at ?? row.createdAt),
|
|
143
|
+
deliveredAt: (row.delivered_at ?? row.deliveredAt) == null
|
|
144
|
+
? null
|
|
145
|
+
: coerceDate(row.delivered_at ?? row.deliveredAt),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function coerceDate(value) {
|
|
149
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
150
|
+
}
|
|
151
|
+
/** Rebuild the bus envelope from a stored row. */
|
|
152
|
+
export function outboxRowToEnvelope(row) {
|
|
153
|
+
return {
|
|
154
|
+
name: row.name,
|
|
155
|
+
data: row.payload,
|
|
156
|
+
metadata: { ...(row.metadata ?? {}), eventId: row.eventId },
|
|
157
|
+
emittedAt: row.createdAt.toISOString(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* One drain pass: claim due rows, redeliver each through the bus, mark
|
|
162
|
+
* delivered / reschedule / dead-letter. Call from a scheduled handler
|
|
163
|
+
* (cron), a `waitUntil` kick after emit, or a long-running Node loop.
|
|
164
|
+
* Safe to run concurrently from multiple isolates (SKIP LOCKED claim).
|
|
165
|
+
*/
|
|
166
|
+
export async function drainOutbox(db, bus, options = {}) {
|
|
167
|
+
const claimed = await claimDueOutboxEvents(db, options);
|
|
168
|
+
const result = {
|
|
169
|
+
claimed: claimed.length,
|
|
170
|
+
delivered: 0,
|
|
171
|
+
retried: 0,
|
|
172
|
+
deadLettered: 0,
|
|
173
|
+
};
|
|
174
|
+
if (claimed.length === 0)
|
|
175
|
+
return result;
|
|
176
|
+
await Promise.all(claimed.map(async (row) => {
|
|
177
|
+
const envelope = outboxRowToEnvelope(row);
|
|
178
|
+
let failedCount = 0;
|
|
179
|
+
let errors = [];
|
|
180
|
+
try {
|
|
181
|
+
if (bus.deliver) {
|
|
182
|
+
const delivery = await bus.deliver(envelope);
|
|
183
|
+
failedCount = delivery.failed;
|
|
184
|
+
errors = delivery.errors;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Third-party bus without failure reporting: emit is
|
|
188
|
+
// fire-and-forget; count as success.
|
|
189
|
+
await bus.emit(envelope.name, envelope.data, envelope.metadata);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
failedCount = 1;
|
|
194
|
+
errors = [err instanceof Error ? err.message : String(err)];
|
|
195
|
+
}
|
|
196
|
+
if (failedCount === 0) {
|
|
197
|
+
await completeOutboxEvent(db, row.id);
|
|
198
|
+
result.delivered += 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const status = await failOutboxEvent(db, row.id, errors.join("; "));
|
|
202
|
+
if (status === "failed")
|
|
203
|
+
result.deadLettered += 1;
|
|
204
|
+
else
|
|
205
|
+
result.retried += 1;
|
|
206
|
+
}));
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Delete delivered rows past the retention window (delivered rows are
|
|
211
|
+
* receipts, not a log sink — long-term archival belongs elsewhere).
|
|
212
|
+
* Dead-lettered (`failed`) rows are NOT pruned: they represent lost
|
|
213
|
+
* deliveries until a human resolves them. Returns the deleted count.
|
|
214
|
+
*/
|
|
215
|
+
export async function pruneDeliveredOutboxEvents(db, options = {}) {
|
|
216
|
+
const days = options.olderThanDays ?? 14;
|
|
217
|
+
const result = (await db.execute(sql `
|
|
218
|
+
DELETE FROM ${eventOutboxTable}
|
|
219
|
+
WHERE "status" = 'delivered'
|
|
220
|
+
AND "delivered_at" < now() - (${days} * interval '1 day')
|
|
221
|
+
RETURNING "id"
|
|
222
|
+
`));
|
|
223
|
+
const rows = Array.isArray(result) ? result : (result.rows ?? []);
|
|
224
|
+
return rows.length;
|
|
225
|
+
}
|
|
226
|
+
/** Row counts by status — observability for dashboards/health checks. */
|
|
227
|
+
export async function getOutboxStats(db) {
|
|
228
|
+
const result = (await db.execute(sql `
|
|
229
|
+
SELECT
|
|
230
|
+
count(*) FILTER (WHERE "status" = 'pending')::int AS pending,
|
|
231
|
+
count(*) FILTER (WHERE "status" = 'delivered')::int AS delivered,
|
|
232
|
+
count(*) FILTER (WHERE "status" = 'failed')::int AS failed,
|
|
233
|
+
count(*) FILTER (WHERE "status" = 'pending' AND "next_attempt_at" <= now())::int AS due_now
|
|
234
|
+
FROM ${eventOutboxTable}
|
|
235
|
+
`));
|
|
236
|
+
const rows = Array.isArray(result) ? result : (result.rows ?? []);
|
|
237
|
+
const row = (rows[0] ?? {});
|
|
238
|
+
return {
|
|
239
|
+
pending: row.pending ?? 0,
|
|
240
|
+
delivered: row.delivered ?? 0,
|
|
241
|
+
failed: row.failed ?? 0,
|
|
242
|
+
dueNow: row.due_now ?? 0,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=outbox.js.map
|