@voyant-travel/catalog 0.117.2

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 (243) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +190 -0
  3. package/dist/adapter/booking-forwarding.d.ts +2 -0
  4. package/dist/adapter/booking-forwarding.d.ts.map +1 -0
  5. package/dist/adapter/booking-forwarding.js +1 -0
  6. package/dist/adapter/channel-push-contracts.d.ts +2 -0
  7. package/dist/adapter/channel-push-contracts.d.ts.map +1 -0
  8. package/dist/adapter/channel-push-contracts.js +1 -0
  9. package/dist/adapter/contract.d.ts +2 -0
  10. package/dist/adapter/contract.d.ts.map +1 -0
  11. package/dist/adapter/contract.js +1 -0
  12. package/dist/adapter/contract.test.d.ts +2 -0
  13. package/dist/adapter/contract.test.d.ts.map +1 -0
  14. package/dist/adapter/contract.test.js +390 -0
  15. package/dist/adapter/provider-contracts.d.ts +2 -0
  16. package/dist/adapter/provider-contracts.d.ts.map +1 -0
  17. package/dist/adapter/provider-contracts.js +1 -0
  18. package/dist/adapter/provider-contracts.test.d.ts +2 -0
  19. package/dist/adapter/provider-contracts.test.d.ts.map +1 -0
  20. package/dist/adapter/provider-contracts.test.js +206 -0
  21. package/dist/adapter/schemas.d.ts +2 -0
  22. package/dist/adapter/schemas.d.ts.map +1 -0
  23. package/dist/adapter/schemas.js +1 -0
  24. package/dist/adapter/schemas.test.d.ts +2 -0
  25. package/dist/adapter/schemas.test.d.ts.map +1 -0
  26. package/dist/adapter/schemas.test.js +344 -0
  27. package/dist/booking-engine/book.d.ts +124 -0
  28. package/dist/booking-engine/book.d.ts.map +1 -0
  29. package/dist/booking-engine/book.js +311 -0
  30. package/dist/booking-engine/cancel.d.ts +40 -0
  31. package/dist/booking-engine/cancel.d.ts.map +1 -0
  32. package/dist/booking-engine/cancel.js +56 -0
  33. package/dist/booking-engine/checkout-finalize.d.ts +146 -0
  34. package/dist/booking-engine/checkout-finalize.d.ts.map +1 -0
  35. package/dist/booking-engine/checkout-finalize.js +132 -0
  36. package/dist/booking-engine/contracts.d.ts +9 -0
  37. package/dist/booking-engine/contracts.d.ts.map +1 -0
  38. package/dist/booking-engine/contracts.js +8 -0
  39. package/dist/booking-engine/contracts.test.d.ts +2 -0
  40. package/dist/booking-engine/contracts.test.d.ts.map +1 -0
  41. package/dist/booking-engine/contracts.test.js +116 -0
  42. package/dist/booking-engine/draft-shape.d.ts +10 -0
  43. package/dist/booking-engine/draft-shape.d.ts.map +1 -0
  44. package/dist/booking-engine/draft-shape.js +9 -0
  45. package/dist/booking-engine/draft-shape.test.d.ts +2 -0
  46. package/dist/booking-engine/draft-shape.test.d.ts.map +1 -0
  47. package/dist/booking-engine/draft-shape.test.js +74 -0
  48. package/dist/booking-engine/drafts-schema.d.ts +302 -0
  49. package/dist/booking-engine/drafts-schema.d.ts.map +1 -0
  50. package/dist/booking-engine/drafts-schema.js +53 -0
  51. package/dist/booking-engine/drafts-service.d.ts +41 -0
  52. package/dist/booking-engine/drafts-service.d.ts.map +1 -0
  53. package/dist/booking-engine/drafts-service.js +108 -0
  54. package/dist/booking-engine/errors.d.ts +81 -0
  55. package/dist/booking-engine/errors.d.ts.map +1 -0
  56. package/dist/booking-engine/errors.js +113 -0
  57. package/dist/booking-engine/index.d.ts +36 -0
  58. package/dist/booking-engine/index.d.ts.map +1 -0
  59. package/dist/booking-engine/index.js +34 -0
  60. package/dist/booking-engine/orders.d.ts +41 -0
  61. package/dist/booking-engine/orders.d.ts.map +1 -0
  62. package/dist/booking-engine/orders.js +49 -0
  63. package/dist/booking-engine/owned-handler.d.ts +166 -0
  64. package/dist/booking-engine/owned-handler.d.ts.map +1 -0
  65. package/dist/booking-engine/owned-handler.js +50 -0
  66. package/dist/booking-engine/owned-handler.test.d.ts +2 -0
  67. package/dist/booking-engine/owned-handler.test.d.ts.map +1 -0
  68. package/dist/booking-engine/owned-handler.test.js +63 -0
  69. package/dist/booking-engine/promotions-contract.d.ts +8 -0
  70. package/dist/booking-engine/promotions-contract.d.ts.map +1 -0
  71. package/dist/booking-engine/promotions-contract.js +7 -0
  72. package/dist/booking-engine/quote-enricher.test.d.ts +12 -0
  73. package/dist/booking-engine/quote-enricher.test.d.ts.map +1 -0
  74. package/dist/booking-engine/quote-enricher.test.js +138 -0
  75. package/dist/booking-engine/quote.d.ts +163 -0
  76. package/dist/booking-engine/quote.d.ts.map +1 -0
  77. package/dist/booking-engine/quote.js +259 -0
  78. package/dist/booking-engine/registry.d.ts +85 -0
  79. package/dist/booking-engine/registry.d.ts.map +1 -0
  80. package/dist/booking-engine/registry.js +118 -0
  81. package/dist/booking-engine/registry.test.d.ts +2 -0
  82. package/dist/booking-engine/registry.test.d.ts.map +1 -0
  83. package/dist/booking-engine/registry.test.js +132 -0
  84. package/dist/booking-engine/routes-contracts.d.ts +169 -0
  85. package/dist/booking-engine/routes-contracts.d.ts.map +1 -0
  86. package/dist/booking-engine/routes-contracts.js +63 -0
  87. package/dist/booking-engine/routes.d.ts +7 -0
  88. package/dist/booking-engine/routes.d.ts.map +1 -0
  89. package/dist/booking-engine/routes.js +443 -0
  90. package/dist/booking-engine/routes.test.d.ts +2 -0
  91. package/dist/booking-engine/routes.test.d.ts.map +1 -0
  92. package/dist/booking-engine/routes.test.js +304 -0
  93. package/dist/booking-engine/schema.d.ts +455 -0
  94. package/dist/booking-engine/schema.d.ts.map +1 -0
  95. package/dist/booking-engine/schema.js +75 -0
  96. package/dist/booking-engine/snapshot-content.d.ts +120 -0
  97. package/dist/booking-engine/snapshot-content.d.ts.map +1 -0
  98. package/dist/booking-engine/snapshot-content.js +110 -0
  99. package/dist/booking-engine/snapshot-content.test.d.ts +2 -0
  100. package/dist/booking-engine/snapshot-content.test.d.ts.map +1 -0
  101. package/dist/booking-engine/snapshot-content.test.js +213 -0
  102. package/dist/booking-engine/sync.d.ts +136 -0
  103. package/dist/booking-engine/sync.d.ts.map +1 -0
  104. package/dist/booking-engine/sync.js +177 -0
  105. package/dist/booking-engine/sync.test.d.ts +2 -0
  106. package/dist/booking-engine/sync.test.d.ts.map +1 -0
  107. package/dist/booking-engine/sync.test.js +377 -0
  108. package/dist/contract.d.ts +2 -0
  109. package/dist/contract.d.ts.map +1 -0
  110. package/dist/contract.js +1 -0
  111. package/dist/contract.test.d.ts +2 -0
  112. package/dist/contract.test.d.ts.map +1 -0
  113. package/dist/contract.test.js +107 -0
  114. package/dist/drift/events.d.ts +2 -0
  115. package/dist/drift/events.d.ts.map +1 -0
  116. package/dist/drift/events.js +1 -0
  117. package/dist/drift/events.test.d.ts +2 -0
  118. package/dist/drift/events.test.d.ts.map +1 -0
  119. package/dist/drift/events.test.js +100 -0
  120. package/dist/embeddings/contract.d.ts +85 -0
  121. package/dist/embeddings/contract.d.ts.map +1 -0
  122. package/dist/embeddings/contract.js +42 -0
  123. package/dist/embeddings/contract.test.d.ts +2 -0
  124. package/dist/embeddings/contract.test.d.ts.map +1 -0
  125. package/dist/embeddings/contract.test.js +30 -0
  126. package/dist/embeddings/gemini.d.ts +110 -0
  127. package/dist/embeddings/gemini.d.ts.map +1 -0
  128. package/dist/embeddings/gemini.js +118 -0
  129. package/dist/embeddings/gemini.test.d.ts +2 -0
  130. package/dist/embeddings/gemini.test.d.ts.map +1 -0
  131. package/dist/embeddings/gemini.test.js +132 -0
  132. package/dist/embeddings/model-registry.d.ts +62 -0
  133. package/dist/embeddings/model-registry.d.ts.map +1 -0
  134. package/dist/embeddings/model-registry.js +78 -0
  135. package/dist/embeddings/model-registry.test.d.ts +2 -0
  136. package/dist/embeddings/model-registry.test.d.ts.map +1 -0
  137. package/dist/embeddings/model-registry.test.js +81 -0
  138. package/dist/embeddings/openai.d.ts +81 -0
  139. package/dist/embeddings/openai.d.ts.map +1 -0
  140. package/dist/embeddings/openai.js +123 -0
  141. package/dist/embeddings/openai.test.d.ts +2 -0
  142. package/dist/embeddings/openai.test.d.ts.map +1 -0
  143. package/dist/embeddings/openai.test.js +164 -0
  144. package/dist/events/taxonomy.d.ts +158 -0
  145. package/dist/events/taxonomy.d.ts.map +1 -0
  146. package/dist/events/taxonomy.js +99 -0
  147. package/dist/events/taxonomy.test.d.ts +2 -0
  148. package/dist/events/taxonomy.test.d.ts.map +1 -0
  149. package/dist/events/taxonomy.test.js +48 -0
  150. package/dist/index.d.ts +27 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +39 -0
  153. package/dist/indexer/contract.d.ts +203 -0
  154. package/dist/indexer/contract.d.ts.map +1 -0
  155. package/dist/indexer/contract.js +16 -0
  156. package/dist/indexer/typesense-search-query.d.ts +31 -0
  157. package/dist/indexer/typesense-search-query.d.ts.map +1 -0
  158. package/dist/indexer/typesense-search-query.js +185 -0
  159. package/dist/indexer/typesense.d.ts +105 -0
  160. package/dist/indexer/typesense.d.ts.map +1 -0
  161. package/dist/indexer/typesense.js +394 -0
  162. package/dist/indexer/typesense.test.d.ts +2 -0
  163. package/dist/indexer/typesense.test.d.ts.map +1 -0
  164. package/dist/indexer/typesense.test.js +253 -0
  165. package/dist/overlay/resolver.d.ts +101 -0
  166. package/dist/overlay/resolver.d.ts.map +1 -0
  167. package/dist/overlay/resolver.js +167 -0
  168. package/dist/overlay/resolver.test.d.ts +2 -0
  169. package/dist/overlay/resolver.test.d.ts.map +1 -0
  170. package/dist/overlay/resolver.test.js +179 -0
  171. package/dist/overlay/schema.d.ts +266 -0
  172. package/dist/overlay/schema.d.ts.map +1 -0
  173. package/dist/overlay/schema.js +71 -0
  174. package/dist/provenance.d.ts +2 -0
  175. package/dist/provenance.d.ts.map +1 -0
  176. package/dist/provenance.js +1 -0
  177. package/dist/schema-sourced-entries.d.ts +344 -0
  178. package/dist/schema-sourced-entries.d.ts.map +1 -0
  179. package/dist/schema-sourced-entries.js +75 -0
  180. package/dist/schema.d.ts +21 -0
  181. package/dist/schema.d.ts.map +1 -0
  182. package/dist/schema.js +20 -0
  183. package/dist/search/federate.d.ts +58 -0
  184. package/dist/search/federate.d.ts.map +1 -0
  185. package/dist/search/federate.js +103 -0
  186. package/dist/search/federate.test.d.ts +2 -0
  187. package/dist/search/federate.test.d.ts.map +1 -0
  188. package/dist/search/federate.test.js +146 -0
  189. package/dist/search/rerank.d.ts +77 -0
  190. package/dist/search/rerank.d.ts.map +1 -0
  191. package/dist/search/rerank.js +68 -0
  192. package/dist/search/rerank.test.d.ts +2 -0
  193. package/dist/search/rerank.test.d.ts.map +1 -0
  194. package/dist/search/rerank.test.js +60 -0
  195. package/dist/search/routes.d.ts +144 -0
  196. package/dist/search/routes.d.ts.map +1 -0
  197. package/dist/search/routes.js +288 -0
  198. package/dist/search/routes.test.d.ts +2 -0
  199. package/dist/search/routes.test.d.ts.map +1 -0
  200. package/dist/search/routes.test.js +322 -0
  201. package/dist/search/semantic.d.ts +63 -0
  202. package/dist/search/semantic.d.ts.map +1 -0
  203. package/dist/search/semantic.js +75 -0
  204. package/dist/search/semantic.test.d.ts +2 -0
  205. package/dist/search/semantic.test.d.ts.map +1 -0
  206. package/dist/search/semantic.test.js +143 -0
  207. package/dist/services/build-indexer-document.test.d.ts +2 -0
  208. package/dist/services/build-indexer-document.test.d.ts.map +1 -0
  209. package/dist/services/build-indexer-document.test.js +102 -0
  210. package/dist/services/content-service.d.ts +125 -0
  211. package/dist/services/content-service.d.ts.map +1 -0
  212. package/dist/services/content-service.js +139 -0
  213. package/dist/services/content-service.test.d.ts +2 -0
  214. package/dist/services/content-service.test.d.ts.map +1 -0
  215. package/dist/services/content-service.test.js +322 -0
  216. package/dist/services/indexer-service.d.ts +109 -0
  217. package/dist/services/indexer-service.d.ts.map +1 -0
  218. package/dist/services/indexer-service.js +123 -0
  219. package/dist/services/indexer-service.test.d.ts +2 -0
  220. package/dist/services/indexer-service.test.d.ts.map +1 -0
  221. package/dist/services/indexer-service.test.js +176 -0
  222. package/dist/services/overlay-service.d.ts +108 -0
  223. package/dist/services/overlay-service.d.ts.map +1 -0
  224. package/dist/services/overlay-service.js +211 -0
  225. package/dist/services/overlay-service.test.d.ts +2 -0
  226. package/dist/services/overlay-service.test.d.ts.map +1 -0
  227. package/dist/services/overlay-service.test.js +79 -0
  228. package/dist/services/snapshot-builder.test.d.ts +2 -0
  229. package/dist/services/snapshot-builder.test.d.ts.map +1 -0
  230. package/dist/services/snapshot-builder.test.js +93 -0
  231. package/dist/services/snapshot-service.d.ts +78 -0
  232. package/dist/services/snapshot-service.d.ts.map +1 -0
  233. package/dist/services/snapshot-service.js +165 -0
  234. package/dist/services/sourced-entry-service.d.ts +142 -0
  235. package/dist/services/sourced-entry-service.d.ts.map +1 -0
  236. package/dist/services/sourced-entry-service.js +203 -0
  237. package/dist/services/sourced-entry-service.test.d.ts +10 -0
  238. package/dist/services/sourced-entry-service.test.d.ts.map +1 -0
  239. package/dist/services/sourced-entry-service.test.js +66 -0
  240. package/dist/snapshot/schema.d.ts +362 -0
  241. package/dist/snapshot/schema.d.ts.map +1 -0
  242. package/dist/snapshot/schema.js +102 -0
  243. package/package.json +210 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * SourcedEntryService — drizzle-bound entry point for the
3
+ * `catalog_sourced_entries` store.
4
+ *
5
+ * One row per sourced entity, written at `discover()` time. The row carries
6
+ * provenance, lifecycle markers, and the canonical local copy of the
7
+ * indexed projection (so the read service and the thin-content synthesizer
8
+ * can dispatch without round-tripping the search index).
9
+ *
10
+ * Three primitives:
11
+ *
12
+ * - `upsertSourcedEntry` — called from `sync.ts` for every projection;
13
+ * idempotent on (entity_module, entity_id) and on
14
+ * (source_kind, source_connection_id, source_ref).
15
+ * - `readSourcedEntry` — point-read by Voyant-side identity. Returns
16
+ * null for owned entities (which have no row here).
17
+ * - `createReadProvenance` — factory that composes `readSourcedEntry`
18
+ * with vertical-specific owned-checkers to produce a unified
19
+ * `readProvenance(db, entity_module, entity_id)` that returns one of:
20
+ * `{ kind: "owned" }`, `{ kind: "sourced", ... }`, or `null`.
21
+ *
22
+ * The factory pattern keeps this package neutral — it doesn't know how to
23
+ * read the products / cruises / hotels owned tables. Each vertical
24
+ * registers its owned-checker once when wiring its content service (Phase
25
+ * D and beyond).
26
+ *
27
+ * See `docs/architecture/catalog-sourced-content.md` §2.5.
28
+ */
29
+ import { and, eq, inArray, isNull, notInArray, sql } from "drizzle-orm";
30
+ import { catalogSourcedEntriesTable, } from "../schema-sourced-entries.js";
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Reads
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ /**
35
+ * Read one sourced-entry row by Voyant-side identity. Returns `null` for
36
+ * entities that aren't in the sourced-entry store — owned entities, or
37
+ * sourced entities the deployment hasn't yet discovered.
38
+ */
39
+ export async function readSourcedEntry(db, entityModule, entityId) {
40
+ const rows = await db
41
+ .select()
42
+ .from(catalogSourcedEntriesTable)
43
+ .where(and(eq(catalogSourcedEntriesTable.entity_module, entityModule), eq(catalogSourcedEntriesTable.entity_id, entityId)))
44
+ .limit(1);
45
+ return rows[0] ?? null;
46
+ }
47
+ /**
48
+ * Build a unified `readProvenance(db, entity_module, entity_id)` against a
49
+ * registry of vertical-specific owned-checkers. The returned function:
50
+ *
51
+ * 1. Calls the vertical's owned-checker. If it returns `true`, the entity
52
+ * is owned — return `{ kind: "owned", provenance: ... }` without
53
+ * touching the sourced-entry table.
54
+ * 2. Otherwise, look up the sourced-entry row. If found, return
55
+ * `{ kind: "sourced", ... }`.
56
+ * 3. If neither, return `null`.
57
+ *
58
+ * Verticals not in `ownedCheckers` skip the owned check (treated as
59
+ * sourced-only). This is intentional: not every vertical has an owned
60
+ * counterpart for every sourced entity.
61
+ */
62
+ export function createReadProvenance(options) {
63
+ const ownedCheckers = options.ownedCheckers ?? new Map();
64
+ return async function readProvenance(db, entityModule, entityId) {
65
+ const ownedChecker = ownedCheckers.get(entityModule);
66
+ if (ownedChecker) {
67
+ const isOwned = await ownedChecker(db, entityId);
68
+ if (isOwned) {
69
+ return {
70
+ kind: "owned",
71
+ provenance: { source_kind: "owned", source_freshness: "static" },
72
+ };
73
+ }
74
+ }
75
+ const row = await readSourcedEntry(db, entityModule, entityId);
76
+ if (!row)
77
+ return null;
78
+ return {
79
+ kind: "sourced",
80
+ provenance: {
81
+ source_kind: row.source_kind,
82
+ source_provider: row.source_provider ?? undefined,
83
+ source_connection_id: row.source_connection_id ?? undefined,
84
+ source_ref: row.source_ref ?? undefined,
85
+ source_freshness: row.source_freshness,
86
+ last_sourced_at: row.last_sourced_at ?? undefined,
87
+ },
88
+ entry_id: row.id,
89
+ status: row.status,
90
+ projection: row.projection,
91
+ projection_etag: row.projection_etag,
92
+ projection_seen_at: row.projection_seen_at,
93
+ first_seen_at: row.first_seen_at,
94
+ last_seen_at: row.last_seen_at,
95
+ };
96
+ };
97
+ }
98
+ /**
99
+ * Upsert a sourced-entry row. Idempotent on `(entity_module, entity_id)`
100
+ * — repeated calls update `projection`, `projection_etag`,
101
+ * `projection_seen_at`, `last_seen_at`, `last_sourced_at`,
102
+ * `source_freshness`, and `updated_at`. The first-seen timestamp is
103
+ * preserved.
104
+ *
105
+ * Owned projections are rejected — `provenance.source_kind === "owned"`
106
+ * has no place in the sourced-entry store. Callers in `sync.ts` should
107
+ * already filter these out, but this guard makes the invariant explicit.
108
+ */
109
+ export async function upsertSourcedEntry(db, input) {
110
+ const { projection, projectionEtag, lastSourcedAt, status } = input;
111
+ const provenance = projection.provenance;
112
+ if (provenance.source_kind === "owned") {
113
+ throw new Error(`upsertSourcedEntry called with owned provenance for ${projection.entity_module}/${projection.entity_id}; owned entities live in the vertical's owned schema, not catalog_sourced_entries`);
114
+ }
115
+ const now = new Date();
116
+ const stampedAt = lastSourcedAt ?? provenance.last_sourced_at ?? now;
117
+ const rows = await db
118
+ .insert(catalogSourcedEntriesTable)
119
+ .values({
120
+ entity_module: projection.entity_module,
121
+ entity_id: projection.entity_id,
122
+ source_kind: provenance.source_kind,
123
+ source_provider: provenance.source_provider ?? null,
124
+ source_connection_id: provenance.source_connection_id ?? null,
125
+ source_ref: provenance.source_ref ?? null,
126
+ source_freshness: (provenance.source_freshness ?? "static"),
127
+ last_sourced_at: stampedAt,
128
+ status: status ?? "active",
129
+ first_seen_at: now,
130
+ last_seen_at: now,
131
+ projection: projection.fields,
132
+ projection_etag: projectionEtag ?? null,
133
+ projection_seen_at: now,
134
+ created_at: now,
135
+ updated_at: now,
136
+ })
137
+ .onConflictDoUpdate({
138
+ target: [catalogSourcedEntriesTable.entity_module, catalogSourcedEntriesTable.entity_id],
139
+ set: {
140
+ source_kind: sql `excluded.source_kind`,
141
+ source_provider: sql `excluded.source_provider`,
142
+ source_connection_id: sql `excluded.source_connection_id`,
143
+ source_ref: sql `excluded.source_ref`,
144
+ source_freshness: sql `excluded.source_freshness`,
145
+ last_sourced_at: sql `excluded.last_sourced_at`,
146
+ status: sql `excluded.status`,
147
+ last_seen_at: sql `excluded.last_seen_at`,
148
+ projection: sql `excluded.projection`,
149
+ projection_etag: sql `excluded.projection_etag`,
150
+ projection_seen_at: sql `excluded.projection_seen_at`,
151
+ updated_at: sql `excluded.updated_at`,
152
+ },
153
+ })
154
+ .returning();
155
+ const row = rows[0];
156
+ if (!row) {
157
+ throw new Error(`upsertSourcedEntry returned no row for ${projection.entity_module}/${projection.entity_id}`);
158
+ }
159
+ return row;
160
+ }
161
+ /**
162
+ * Mark a sourced-entry row as withdrawn (the upstream stopped emitting
163
+ * it). Used by the periodic withdrawal sweeper or by drift events of kind
164
+ * `entity_archived`. Does not delete the row — withdrawals are auditable.
165
+ */
166
+ export async function markSourcedEntryWithdrawn(db, entityModule, entityId) {
167
+ const now = new Date();
168
+ await db
169
+ .update(catalogSourcedEntriesTable)
170
+ .set({ status: "withdrawn", updated_at: now })
171
+ .where(and(eq(catalogSourcedEntriesTable.entity_module, entityModule), eq(catalogSourcedEntriesTable.entity_id, entityId)));
172
+ }
173
+ /**
174
+ * Mark active sourced rows missing from a successful full-source discovery pass
175
+ * as withdrawn. Callers should invoke this only after an adapter completed its
176
+ * projection stream; failed refreshes must leave existing rows untouched.
177
+ */
178
+ export async function markMissingSourcedEntriesWithdrawn(db, input) {
179
+ const conditions = [
180
+ eq(catalogSourcedEntriesTable.entity_module, input.entityModule),
181
+ eq(catalogSourcedEntriesTable.source_kind, input.sourceKind),
182
+ eq(catalogSourcedEntriesTable.status, "active"),
183
+ input.sourceConnectionId == null
184
+ ? isNull(catalogSourcedEntriesTable.source_connection_id)
185
+ : eq(catalogSourcedEntriesTable.source_connection_id, input.sourceConnectionId),
186
+ ];
187
+ const seen = [...input.seenEntityIds];
188
+ if (seen.length > 0) {
189
+ conditions.push(notInArray(catalogSourcedEntriesTable.entity_id, seen));
190
+ }
191
+ const rows = await db
192
+ .select()
193
+ .from(catalogSourcedEntriesTable)
194
+ .where(and(...conditions));
195
+ if (rows.length === 0)
196
+ return [];
197
+ const now = new Date();
198
+ await db
199
+ .update(catalogSourcedEntriesTable)
200
+ .set({ status: "withdrawn", updated_at: now })
201
+ .where(inArray(catalogSourcedEntriesTable.id, rows.map((row) => row.id)));
202
+ return rows;
203
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Pure-function tests for the sourced-entry service.
3
+ *
4
+ * Integration tests (real Postgres) live in
5
+ * `tests/integration/sourced-entry-service.test.ts`. These cover the
6
+ * `createReadProvenance` factory's branching against stub owned-checkers
7
+ * and a stub DB — no Postgres needed.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=sourced-entry-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sourced-entry-service.test.d.ts","sourceRoot":"","sources":["../../src/services/sourced-entry-service.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pure-function tests for the sourced-entry service.
3
+ *
4
+ * Integration tests (real Postgres) live in
5
+ * `tests/integration/sourced-entry-service.test.ts`. These cover the
6
+ * `createReadProvenance` factory's branching against stub owned-checkers
7
+ * and a stub DB — no Postgres needed.
8
+ */
9
+ import { describe, expect, it, vi } from "vitest";
10
+ import { createReadProvenance } from "./sourced-entry-service.js";
11
+ // Minimal stub mirroring the columns `readSourcedEntry` selects. We don't
12
+ // exercise any drizzle methods here — we replace the `readSourcedEntry`
13
+ // shape entirely via vi.mock for the dispatch tests.
14
+ function fakeDb() {
15
+ return {};
16
+ }
17
+ describe("createReadProvenance — owned check fast path", () => {
18
+ it("returns kind: 'owned' when the owned-checker returns true", async () => {
19
+ const ownedChecker = vi.fn(async () => true);
20
+ const readProvenance = createReadProvenance({
21
+ ownedCheckers: new Map([["products", ownedChecker]]),
22
+ });
23
+ const result = await readProvenance(fakeDb(), "products", "prod_abc");
24
+ expect(result?.kind).toBe("owned");
25
+ if (result?.kind === "owned") {
26
+ expect(result.provenance.source_kind).toBe("owned");
27
+ expect(result.provenance.source_freshness).toBe("static");
28
+ }
29
+ expect(ownedChecker).toHaveBeenCalledWith(expect.anything(), "prod_abc");
30
+ });
31
+ it("skips the owned-checker for verticals not in the registry", async () => {
32
+ const productsOwnedChecker = vi.fn(async () => true);
33
+ const readProvenance = createReadProvenance({
34
+ ownedCheckers: new Map([["products", productsOwnedChecker]]),
35
+ });
36
+ // No checker registered for "cruises" — falls through to sourced-entry
37
+ // lookup directly. We can't hit a real DB here, so we expect this to
38
+ // attempt a query and fail cleanly. The point is the products checker
39
+ // is NOT called for a different vertical.
40
+ await expect(readProvenance(fakeDb(), "cruises", "crus_xyz")).rejects.toBeTruthy();
41
+ expect(productsOwnedChecker).not.toHaveBeenCalled();
42
+ });
43
+ });
44
+ describe("createReadProvenance — empty checker map", () => {
45
+ it("treats every vertical as sourced-only when no checkers are registered", async () => {
46
+ const readProvenance = createReadProvenance({});
47
+ // With no checkers, the function falls straight through to the
48
+ // sourced-entry table read — which fails against the stub DB. We
49
+ // verify only that it didn't throw before reaching the DB read.
50
+ await expect(readProvenance(fakeDb(), "products", "prod_abc")).rejects.toBeTruthy();
51
+ });
52
+ });
53
+ describe("CatalogProjection input shape", () => {
54
+ it("requires non-owned provenance for the upsert path", () => {
55
+ // Pure-shape assertion — the upsert function throws if you pass an
56
+ // owned projection. This is a structural property of the input
57
+ // contract; the DB is irrelevant.
58
+ const projection = {
59
+ entity_module: "products",
60
+ entity_id: "prod_abc",
61
+ provenance: { source_kind: "direct:tui", source_freshness: "sync" },
62
+ fields: { title: "Sample" },
63
+ };
64
+ expect(projection.provenance.source_kind).toBe("direct:tui");
65
+ });
66
+ });
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Booking snapshot graph — immutable frozen views of CatalogEntries captured
3
+ * at booking commit time.
4
+ *
5
+ * One booking can produce multiple snapshot rows (one per participating
6
+ * CatalogEntry — a TUI package booking captures the package, the referenced
7
+ * hotel, each selected excursion, the chosen departure, the selected flight).
8
+ * Snapshots are never mutated and are preserved through source disconnection.
9
+ *
10
+ * See `docs/architecture/catalog-architecture.md` §5.3 for the full design.
11
+ */
12
+ import type { PricingBasis } from "@voyant-travel/catalog-contracts/snapshot";
13
+ import type { AppliedOffer } from "../booking-engine/promotions-contract.js";
14
+ export type { PricingBasis };
15
+ /**
16
+ * `booking_catalog_snapshot` — frozen view of a CatalogEntry captured at
17
+ * booking commit. One booking may produce many snapshot rows (one per
18
+ * participating CatalogEntry).
19
+ */
20
+ export declare const bookingCatalogSnapshotTable: import("drizzle-orm/pg-core").PgTableWithColumns<{
21
+ name: "booking_catalog_snapshot";
22
+ schema: undefined;
23
+ columns: {
24
+ id: import("drizzle-orm/pg-core").PgColumn<{
25
+ name: string;
26
+ tableName: "booking_catalog_snapshot";
27
+ dataType: "string";
28
+ columnType: "PgText";
29
+ data: string;
30
+ driverParam: string;
31
+ notNull: true;
32
+ hasDefault: true;
33
+ isPrimaryKey: true;
34
+ isAutoincrement: false;
35
+ hasRuntimeDefault: true;
36
+ enumValues: [string, ...string[]];
37
+ baseColumn: never;
38
+ identity: undefined;
39
+ generated: undefined;
40
+ }, {}, {}>;
41
+ booking_id: import("drizzle-orm/pg-core").PgColumn<{
42
+ name: "booking_id";
43
+ tableName: "booking_catalog_snapshot";
44
+ dataType: "string";
45
+ columnType: "PgText";
46
+ data: string;
47
+ driverParam: string;
48
+ notNull: true;
49
+ hasDefault: false;
50
+ isPrimaryKey: false;
51
+ isAutoincrement: false;
52
+ hasRuntimeDefault: false;
53
+ enumValues: [string, ...string[]];
54
+ baseColumn: never;
55
+ identity: undefined;
56
+ generated: undefined;
57
+ }, {}, {}>;
58
+ entity_module: import("drizzle-orm/pg-core").PgColumn<{
59
+ name: "entity_module";
60
+ tableName: "booking_catalog_snapshot";
61
+ dataType: "string";
62
+ columnType: "PgText";
63
+ data: string;
64
+ driverParam: string;
65
+ notNull: true;
66
+ hasDefault: false;
67
+ isPrimaryKey: false;
68
+ isAutoincrement: false;
69
+ hasRuntimeDefault: false;
70
+ enumValues: [string, ...string[]];
71
+ baseColumn: never;
72
+ identity: undefined;
73
+ generated: undefined;
74
+ }, {}, {}>;
75
+ entity_id: import("drizzle-orm/pg-core").PgColumn<{
76
+ name: "entity_id";
77
+ tableName: "booking_catalog_snapshot";
78
+ dataType: "string";
79
+ columnType: "PgText";
80
+ data: string;
81
+ driverParam: string;
82
+ notNull: true;
83
+ hasDefault: false;
84
+ isPrimaryKey: false;
85
+ isAutoincrement: false;
86
+ hasRuntimeDefault: false;
87
+ enumValues: [string, ...string[]];
88
+ baseColumn: never;
89
+ identity: undefined;
90
+ generated: undefined;
91
+ }, {}, {}>;
92
+ source_kind: import("drizzle-orm/pg-core").PgColumn<{
93
+ name: "source_kind";
94
+ tableName: "booking_catalog_snapshot";
95
+ dataType: "string";
96
+ columnType: "PgText";
97
+ data: string;
98
+ driverParam: string;
99
+ notNull: true;
100
+ hasDefault: false;
101
+ isPrimaryKey: false;
102
+ isAutoincrement: false;
103
+ hasRuntimeDefault: false;
104
+ enumValues: [string, ...string[]];
105
+ baseColumn: never;
106
+ identity: undefined;
107
+ generated: undefined;
108
+ }, {}, {}>;
109
+ source_provider: import("drizzle-orm/pg-core").PgColumn<{
110
+ name: "source_provider";
111
+ tableName: "booking_catalog_snapshot";
112
+ dataType: "string";
113
+ columnType: "PgText";
114
+ data: string;
115
+ driverParam: string;
116
+ notNull: false;
117
+ hasDefault: false;
118
+ isPrimaryKey: false;
119
+ isAutoincrement: false;
120
+ hasRuntimeDefault: false;
121
+ enumValues: [string, ...string[]];
122
+ baseColumn: never;
123
+ identity: undefined;
124
+ generated: undefined;
125
+ }, {}, {}>;
126
+ source_connection_id: import("drizzle-orm/pg-core").PgColumn<{
127
+ name: "source_connection_id";
128
+ tableName: "booking_catalog_snapshot";
129
+ dataType: "string";
130
+ columnType: "PgText";
131
+ data: string;
132
+ driverParam: string;
133
+ notNull: false;
134
+ hasDefault: false;
135
+ isPrimaryKey: false;
136
+ isAutoincrement: false;
137
+ hasRuntimeDefault: false;
138
+ enumValues: [string, ...string[]];
139
+ baseColumn: never;
140
+ identity: undefined;
141
+ generated: undefined;
142
+ }, {}, {}>;
143
+ source_ref: import("drizzle-orm/pg-core").PgColumn<{
144
+ name: "source_ref";
145
+ tableName: "booking_catalog_snapshot";
146
+ dataType: "string";
147
+ columnType: "PgText";
148
+ data: string;
149
+ driverParam: string;
150
+ notNull: false;
151
+ hasDefault: false;
152
+ isPrimaryKey: false;
153
+ isAutoincrement: false;
154
+ hasRuntimeDefault: false;
155
+ enumValues: [string, ...string[]];
156
+ baseColumn: never;
157
+ identity: undefined;
158
+ generated: undefined;
159
+ }, {}, {}>;
160
+ frozen_payload: import("drizzle-orm/pg-core").PgColumn<{
161
+ name: "frozen_payload";
162
+ tableName: "booking_catalog_snapshot";
163
+ dataType: "json";
164
+ columnType: "PgJsonb";
165
+ data: unknown;
166
+ driverParam: unknown;
167
+ notNull: true;
168
+ hasDefault: false;
169
+ isPrimaryKey: false;
170
+ isAutoincrement: false;
171
+ hasRuntimeDefault: false;
172
+ enumValues: undefined;
173
+ baseColumn: never;
174
+ identity: undefined;
175
+ generated: undefined;
176
+ }, {}, {}>;
177
+ overlay_state_at_capture: import("drizzle-orm/pg-core").PgColumn<{
178
+ name: "overlay_state_at_capture";
179
+ tableName: "booking_catalog_snapshot";
180
+ dataType: "json";
181
+ columnType: "PgJsonb";
182
+ data: unknown;
183
+ driverParam: unknown;
184
+ notNull: false;
185
+ hasDefault: false;
186
+ isPrimaryKey: false;
187
+ isAutoincrement: false;
188
+ hasRuntimeDefault: false;
189
+ enumValues: undefined;
190
+ baseColumn: never;
191
+ identity: undefined;
192
+ generated: undefined;
193
+ }, {}, {}>;
194
+ pricing_base_amount: import("drizzle-orm/pg-core").PgColumn<{
195
+ name: "pricing_base_amount";
196
+ tableName: "booking_catalog_snapshot";
197
+ dataType: "string";
198
+ columnType: "PgNumeric";
199
+ data: string;
200
+ driverParam: string;
201
+ notNull: false;
202
+ hasDefault: false;
203
+ isPrimaryKey: false;
204
+ isAutoincrement: false;
205
+ hasRuntimeDefault: false;
206
+ enumValues: undefined;
207
+ baseColumn: never;
208
+ identity: undefined;
209
+ generated: undefined;
210
+ }, {}, {}>;
211
+ pricing_taxes: import("drizzle-orm/pg-core").PgColumn<{
212
+ name: "pricing_taxes";
213
+ tableName: "booking_catalog_snapshot";
214
+ dataType: "string";
215
+ columnType: "PgNumeric";
216
+ data: string;
217
+ driverParam: string;
218
+ notNull: false;
219
+ hasDefault: false;
220
+ isPrimaryKey: false;
221
+ isAutoincrement: false;
222
+ hasRuntimeDefault: false;
223
+ enumValues: undefined;
224
+ baseColumn: never;
225
+ identity: undefined;
226
+ generated: undefined;
227
+ }, {}, {}>;
228
+ pricing_fees: import("drizzle-orm/pg-core").PgColumn<{
229
+ name: "pricing_fees";
230
+ tableName: "booking_catalog_snapshot";
231
+ dataType: "string";
232
+ columnType: "PgNumeric";
233
+ data: string;
234
+ driverParam: string;
235
+ notNull: false;
236
+ hasDefault: false;
237
+ isPrimaryKey: false;
238
+ isAutoincrement: false;
239
+ hasRuntimeDefault: false;
240
+ enumValues: undefined;
241
+ baseColumn: never;
242
+ identity: undefined;
243
+ generated: undefined;
244
+ }, {}, {}>;
245
+ pricing_surcharges: import("drizzle-orm/pg-core").PgColumn<{
246
+ name: "pricing_surcharges";
247
+ tableName: "booking_catalog_snapshot";
248
+ dataType: "string";
249
+ columnType: "PgNumeric";
250
+ data: string;
251
+ driverParam: string;
252
+ notNull: false;
253
+ hasDefault: false;
254
+ isPrimaryKey: false;
255
+ isAutoincrement: false;
256
+ hasRuntimeDefault: false;
257
+ enumValues: undefined;
258
+ baseColumn: never;
259
+ identity: undefined;
260
+ generated: undefined;
261
+ }, {}, {}>;
262
+ pricing_currency: import("drizzle-orm/pg-core").PgColumn<{
263
+ name: "pricing_currency";
264
+ tableName: "booking_catalog_snapshot";
265
+ dataType: "string";
266
+ columnType: "PgText";
267
+ data: string;
268
+ driverParam: string;
269
+ notNull: false;
270
+ hasDefault: false;
271
+ isPrimaryKey: false;
272
+ isAutoincrement: false;
273
+ hasRuntimeDefault: false;
274
+ enumValues: [string, ...string[]];
275
+ baseColumn: never;
276
+ identity: undefined;
277
+ generated: undefined;
278
+ }, {}, {}>;
279
+ pricing_breakdown: import("drizzle-orm/pg-core").PgColumn<{
280
+ name: "pricing_breakdown";
281
+ tableName: "booking_catalog_snapshot";
282
+ dataType: "json";
283
+ columnType: "PgJsonb";
284
+ data: Record<string, unknown>;
285
+ driverParam: unknown;
286
+ notNull: false;
287
+ hasDefault: false;
288
+ isPrimaryKey: false;
289
+ isAutoincrement: false;
290
+ hasRuntimeDefault: false;
291
+ enumValues: undefined;
292
+ baseColumn: never;
293
+ identity: undefined;
294
+ generated: undefined;
295
+ }, {}, {
296
+ $type: Record<string, unknown>;
297
+ }>;
298
+ pricing_applied_offers: import("drizzle-orm/pg-core").PgColumn<{
299
+ name: "pricing_applied_offers";
300
+ tableName: "booking_catalog_snapshot";
301
+ dataType: "json";
302
+ columnType: "PgJsonb";
303
+ data: AppliedOffer[];
304
+ driverParam: unknown;
305
+ notNull: false;
306
+ hasDefault: false;
307
+ isPrimaryKey: false;
308
+ isAutoincrement: false;
309
+ hasRuntimeDefault: false;
310
+ enumValues: undefined;
311
+ baseColumn: never;
312
+ identity: undefined;
313
+ generated: undefined;
314
+ }, {}, {
315
+ $type: AppliedOffer[];
316
+ }>;
317
+ idempotency_key: import("drizzle-orm/pg-core").PgColumn<{
318
+ name: "idempotency_key";
319
+ tableName: "booking_catalog_snapshot";
320
+ dataType: "string";
321
+ columnType: "PgText";
322
+ data: string;
323
+ driverParam: string;
324
+ notNull: false;
325
+ hasDefault: false;
326
+ isPrimaryKey: false;
327
+ isAutoincrement: false;
328
+ hasRuntimeDefault: false;
329
+ enumValues: [string, ...string[]];
330
+ baseColumn: never;
331
+ identity: undefined;
332
+ generated: undefined;
333
+ }, {}, {}>;
334
+ captured_at: import("drizzle-orm/pg-core").PgColumn<{
335
+ name: "captured_at";
336
+ tableName: "booking_catalog_snapshot";
337
+ dataType: "date";
338
+ columnType: "PgTimestamp";
339
+ data: Date;
340
+ driverParam: string;
341
+ notNull: true;
342
+ hasDefault: true;
343
+ isPrimaryKey: false;
344
+ isAutoincrement: false;
345
+ hasRuntimeDefault: false;
346
+ enumValues: undefined;
347
+ baseColumn: never;
348
+ identity: undefined;
349
+ generated: undefined;
350
+ }, {}, {}>;
351
+ };
352
+ dialect: "pg";
353
+ }>;
354
+ export type InsertBookingCatalogSnapshot = typeof bookingCatalogSnapshotTable.$inferInsert;
355
+ export type SelectBookingCatalogSnapshot = typeof bookingCatalogSnapshotTable.$inferSelect;
356
+ /**
357
+ * Helper to compose a `PricingBasis` from the structured columns of a
358
+ * snapshot row. Returns `null` if the snapshot has no pricing columns set
359
+ * (volatile-live fields without `on-quote` / `on-book` snapshot mode).
360
+ */
361
+ export declare function readPricingBasis(row: SelectBookingCatalogSnapshot): PricingBasis | null;
362
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/snapshot/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAA;AAK7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAA;AAI5E,YAAY,EAAE,YAAY,EAAE,CAAA;AAE5B;;;;GAIG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+EvC,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG,OAAO,2BAA2B,CAAC,YAAY,CAAA;AAC1F,MAAM,MAAM,4BAA4B,GAAG,OAAO,2BAA2B,CAAC,YAAY,CAAA;AAE1F;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,4BAA4B,GAAG,YAAY,GAAG,IAAI,CAavF"}