@voyant-travel/inventory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/LICENSE +201 -0
  2. package/dist/action-ledger-drift.d.ts +29 -0
  3. package/dist/action-ledger-drift.d.ts.map +1 -0
  4. package/dist/action-ledger-drift.js +338 -0
  5. package/dist/action-ledger.d.ts +104 -0
  6. package/dist/action-ledger.d.ts.map +1 -0
  7. package/dist/action-ledger.js +100 -0
  8. package/dist/authoring/builder.d.ts +37 -0
  9. package/dist/authoring/builder.d.ts.map +1 -0
  10. package/dist/authoring/builder.js +248 -0
  11. package/dist/authoring/clone-content.d.ts +38 -0
  12. package/dist/authoring/clone-content.d.ts.map +1 -0
  13. package/dist/authoring/clone-content.js +367 -0
  14. package/dist/authoring/clone-pricing.d.ts +9 -0
  15. package/dist/authoring/clone-pricing.d.ts.map +1 -0
  16. package/dist/authoring/clone-pricing.js +242 -0
  17. package/dist/authoring/clone.d.ts +45 -0
  18. package/dist/authoring/clone.d.ts.map +1 -0
  19. package/dist/authoring/clone.js +142 -0
  20. package/dist/authoring/errors.d.ts +21 -0
  21. package/dist/authoring/errors.d.ts.map +1 -0
  22. package/dist/authoring/errors.js +13 -0
  23. package/dist/authoring/extension.d.ts +248 -0
  24. package/dist/authoring/extension.d.ts.map +1 -0
  25. package/dist/authoring/extension.js +116 -0
  26. package/dist/authoring/index.d.ts +12 -0
  27. package/dist/authoring/index.d.ts.map +1 -0
  28. package/dist/authoring/index.js +11 -0
  29. package/dist/authoring/schema.d.ts +85 -0
  30. package/dist/authoring/schema.d.ts.map +1 -0
  31. package/dist/authoring/schema.js +16 -0
  32. package/dist/authoring/service.d.ts +28 -0
  33. package/dist/authoring/service.d.ts.map +1 -0
  34. package/dist/authoring/service.js +66 -0
  35. package/dist/authoring/spec.d.ts +524 -0
  36. package/dist/authoring/spec.d.ts.map +1 -0
  37. package/dist/authoring/spec.js +167 -0
  38. package/dist/authoring/validate.d.ts +17 -0
  39. package/dist/authoring/validate.d.ts.map +1 -0
  40. package/dist/authoring/validate.js +83 -0
  41. package/dist/authoring.d.ts +2 -0
  42. package/dist/authoring.d.ts.map +1 -0
  43. package/dist/authoring.js +1 -0
  44. package/dist/booking-engine/handler-support.d.ts +91 -0
  45. package/dist/booking-engine/handler-support.d.ts.map +1 -0
  46. package/dist/booking-engine/handler-support.js +355 -0
  47. package/dist/booking-engine/handler.d.ts +404 -0
  48. package/dist/booking-engine/handler.d.ts.map +1 -0
  49. package/dist/booking-engine/handler.js +398 -0
  50. package/dist/booking-engine/index.d.ts +8 -0
  51. package/dist/booking-engine/index.d.ts.map +1 -0
  52. package/dist/booking-engine/index.js +7 -0
  53. package/dist/booking-engine.d.ts +2 -0
  54. package/dist/booking-engine.d.ts.map +1 -0
  55. package/dist/booking-engine.js +1 -0
  56. package/dist/booking-extension.d.ts +278 -0
  57. package/dist/booking-extension.d.ts.map +1 -0
  58. package/dist/booking-extension.js +161 -0
  59. package/dist/catalog-policy-departures.d.ts +52 -0
  60. package/dist/catalog-policy-departures.d.ts.map +1 -0
  61. package/dist/catalog-policy-departures.js +169 -0
  62. package/dist/catalog-policy-destinations.d.ts +43 -0
  63. package/dist/catalog-policy-destinations.d.ts.map +1 -0
  64. package/dist/catalog-policy-destinations.js +165 -0
  65. package/dist/catalog-policy-pricing.d.ts +55 -0
  66. package/dist/catalog-policy-pricing.d.ts.map +1 -0
  67. package/dist/catalog-policy-pricing.js +109 -0
  68. package/dist/catalog-policy-promotions.d.ts +52 -0
  69. package/dist/catalog-policy-promotions.d.ts.map +1 -0
  70. package/dist/catalog-policy-promotions.js +270 -0
  71. package/dist/catalog-policy-taxonomy.d.ts +51 -0
  72. package/dist/catalog-policy-taxonomy.d.ts.map +1 -0
  73. package/dist/catalog-policy-taxonomy.js +191 -0
  74. package/dist/catalog-policy.d.ts +33 -0
  75. package/dist/catalog-policy.d.ts.map +1 -0
  76. package/dist/catalog-policy.js +733 -0
  77. package/dist/content-shape.d.ts +15 -0
  78. package/dist/content-shape.d.ts.map +1 -0
  79. package/dist/content-shape.js +28 -0
  80. package/dist/draft-shape.d.ts +43 -0
  81. package/dist/draft-shape.d.ts.map +1 -0
  82. package/dist/draft-shape.js +48 -0
  83. package/dist/events.d.ts +37 -0
  84. package/dist/events.d.ts.map +1 -0
  85. package/dist/events.js +32 -0
  86. package/dist/extras/catalog-policy.d.ts +30 -0
  87. package/dist/extras/catalog-policy.d.ts.map +1 -0
  88. package/dist/extras/catalog-policy.js +319 -0
  89. package/dist/extras/content-shape.d.ts +5 -0
  90. package/dist/extras/content-shape.d.ts.map +1 -0
  91. package/dist/extras/content-shape.js +13 -0
  92. package/dist/extras/draft-shape.d.ts +34 -0
  93. package/dist/extras/draft-shape.d.ts.map +1 -0
  94. package/dist/extras/draft-shape.js +69 -0
  95. package/dist/extras/routes.d.ts +380 -0
  96. package/dist/extras/routes.d.ts.map +1 -0
  97. package/dist/extras/routes.js +59 -0
  98. package/dist/extras/schema-sourced-content.d.ts +254 -0
  99. package/dist/extras/schema-sourced-content.d.ts.map +1 -0
  100. package/dist/extras/schema-sourced-content.js +45 -0
  101. package/dist/extras/schema.d.ts +628 -0
  102. package/dist/extras/schema.d.ts.map +1 -0
  103. package/dist/extras/schema.js +87 -0
  104. package/dist/extras/service-catalog-plane.d.ts +77 -0
  105. package/dist/extras/service-catalog-plane.d.ts.map +1 -0
  106. package/dist/extras/service-catalog-plane.js +219 -0
  107. package/dist/extras/service-content-synthesizer.d.ts +41 -0
  108. package/dist/extras/service-content-synthesizer.d.ts.map +1 -0
  109. package/dist/extras/service-content-synthesizer.js +138 -0
  110. package/dist/extras/service-content.d.ts +48 -0
  111. package/dist/extras/service-content.d.ts.map +1 -0
  112. package/dist/extras/service-content.js +253 -0
  113. package/dist/extras/service.d.ts +185 -0
  114. package/dist/extras/service.d.ts.map +1 -0
  115. package/dist/extras/service.js +96 -0
  116. package/dist/extras/validation.d.ts +437 -0
  117. package/dist/extras/validation.d.ts.map +1 -0
  118. package/dist/extras/validation.js +149 -0
  119. package/dist/extras.d.ts +267 -0
  120. package/dist/extras.d.ts.map +1 -0
  121. package/dist/extras.js +19 -0
  122. package/dist/index.d.ts +22 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +32 -0
  125. package/dist/interface.d.ts +5869 -0
  126. package/dist/interface.d.ts.map +1 -0
  127. package/dist/interface.js +54 -0
  128. package/dist/public-routes.d.ts +2 -0
  129. package/dist/public-routes.d.ts.map +1 -0
  130. package/dist/public-routes.js +1 -0
  131. package/dist/public-validation.d.ts +2 -0
  132. package/dist/public-validation.d.ts.map +1 -0
  133. package/dist/public-validation.js +1 -0
  134. package/dist/read-model.d.ts +25 -0
  135. package/dist/read-model.d.ts.map +1 -0
  136. package/dist/read-model.js +99 -0
  137. package/dist/route-env.d.ts +22 -0
  138. package/dist/route-env.d.ts.map +1 -0
  139. package/dist/route-env.js +1 -0
  140. package/dist/routes-associations.d.ts +164 -0
  141. package/dist/routes-associations.d.ts.map +1 -0
  142. package/dist/routes-associations.js +100 -0
  143. package/dist/routes-catalog.d.ts +436 -0
  144. package/dist/routes-catalog.d.ts.map +1 -0
  145. package/dist/routes-catalog.js +104 -0
  146. package/dist/routes-configuration.d.ts +773 -0
  147. package/dist/routes-configuration.d.ts.map +1 -0
  148. package/dist/routes-configuration.js +364 -0
  149. package/dist/routes-content.d.ts +74 -0
  150. package/dist/routes-content.d.ts.map +1 -0
  151. package/dist/routes-content.js +117 -0
  152. package/dist/routes-core.d.ts +331 -0
  153. package/dist/routes-core.d.ts.map +1 -0
  154. package/dist/routes-core.js +95 -0
  155. package/dist/routes-itinerary.d.ts +759 -0
  156. package/dist/routes-itinerary.d.ts.map +1 -0
  157. package/dist/routes-itinerary.js +387 -0
  158. package/dist/routes-maintenance.d.ts +32 -0
  159. package/dist/routes-maintenance.d.ts.map +1 -0
  160. package/dist/routes-maintenance.js +14 -0
  161. package/dist/routes-media.d.ts +634 -0
  162. package/dist/routes-media.d.ts.map +1 -0
  163. package/dist/routes-media.js +245 -0
  164. package/dist/routes-merchandising.d.ts +1120 -0
  165. package/dist/routes-merchandising.d.ts.map +1 -0
  166. package/dist/routes-merchandising.js +377 -0
  167. package/dist/routes-options.d.ts +363 -0
  168. package/dist/routes-options.d.ts.map +1 -0
  169. package/dist/routes-options.js +173 -0
  170. package/dist/routes-public.d.ts +776 -0
  171. package/dist/routes-public.d.ts.map +1 -0
  172. package/dist/routes-public.js +119 -0
  173. package/dist/routes-translations.d.ts +489 -0
  174. package/dist/routes-translations.d.ts.map +1 -0
  175. package/dist/routes-translations.js +258 -0
  176. package/dist/routes.d.ts +5097 -0
  177. package/dist/routes.d.ts.map +1 -0
  178. package/dist/routes.js +64 -0
  179. package/dist/schema-core.d.ts +1238 -0
  180. package/dist/schema-core.d.ts.map +1 -0
  181. package/dist/schema-core.js +157 -0
  182. package/dist/schema-itinerary.d.ts +1169 -0
  183. package/dist/schema-itinerary.d.ts.map +1 -0
  184. package/dist/schema-itinerary.js +130 -0
  185. package/dist/schema-relations.d.ts +117 -0
  186. package/dist/schema-relations.d.ts.map +1 -0
  187. package/dist/schema-relations.js +192 -0
  188. package/dist/schema-settings.d.ts +1800 -0
  189. package/dist/schema-settings.d.ts.map +1 -0
  190. package/dist/schema-settings.js +220 -0
  191. package/dist/schema-shared.d.ts +15 -0
  192. package/dist/schema-shared.d.ts.map +1 -0
  193. package/dist/schema-shared.js +91 -0
  194. package/dist/schema-sourced-content.d.ts +262 -0
  195. package/dist/schema-sourced-content.d.ts.map +1 -0
  196. package/dist/schema-sourced-content.js +69 -0
  197. package/dist/schema-taxonomy.d.ts +1363 -0
  198. package/dist/schema-taxonomy.d.ts.map +1 -0
  199. package/dist/schema-taxonomy.js +203 -0
  200. package/dist/schema.d.ts +10 -0
  201. package/dist/schema.d.ts.map +1 -0
  202. package/dist/schema.js +9 -0
  203. package/dist/service-aggregates.d.ts +29 -0
  204. package/dist/service-aggregates.d.ts.map +1 -0
  205. package/dist/service-aggregates.js +56 -0
  206. package/dist/service-catalog-plane-destinations.d.ts +30 -0
  207. package/dist/service-catalog-plane-destinations.d.ts.map +1 -0
  208. package/dist/service-catalog-plane-destinations.js +143 -0
  209. package/dist/service-catalog-plane-taxonomy.d.ts +73 -0
  210. package/dist/service-catalog-plane-taxonomy.d.ts.map +1 -0
  211. package/dist/service-catalog-plane-taxonomy.js +242 -0
  212. package/dist/service-catalog-plane.d.ts +179 -0
  213. package/dist/service-catalog-plane.d.ts.map +1 -0
  214. package/dist/service-catalog-plane.js +431 -0
  215. package/dist/service-catalog.d.ts +251 -0
  216. package/dist/service-catalog.d.ts.map +1 -0
  217. package/dist/service-catalog.js +517 -0
  218. package/dist/service-configuration.d.ts +261 -0
  219. package/dist/service-configuration.d.ts.map +1 -0
  220. package/dist/service-configuration.js +343 -0
  221. package/dist/service-content-owned.d.ts +68 -0
  222. package/dist/service-content-owned.d.ts.map +1 -0
  223. package/dist/service-content-owned.js +329 -0
  224. package/dist/service-content-synthesizer.d.ts +90 -0
  225. package/dist/service-content-synthesizer.d.ts.map +1 -0
  226. package/dist/service-content-synthesizer.js +178 -0
  227. package/dist/service-content.d.ts +106 -0
  228. package/dist/service-content.d.ts.map +1 -0
  229. package/dist/service-content.js +388 -0
  230. package/dist/service-core.d.ts +194 -0
  231. package/dist/service-core.d.ts.map +1 -0
  232. package/dist/service-core.js +213 -0
  233. package/dist/service-delivery-formats.d.ts +58 -0
  234. package/dist/service-delivery-formats.d.ts.map +1 -0
  235. package/dist/service-delivery-formats.js +107 -0
  236. package/dist/service-destinations.d.ts +223 -0
  237. package/dist/service-destinations.d.ts.map +1 -0
  238. package/dist/service-destinations.js +310 -0
  239. package/dist/service-itinerary-history.d.ts +457 -0
  240. package/dist/service-itinerary-history.d.ts.map +1 -0
  241. package/dist/service-itinerary-history.js +135 -0
  242. package/dist/service-itinerary.d.ts +1149 -0
  243. package/dist/service-itinerary.d.ts.map +1 -0
  244. package/dist/service-itinerary.js +419 -0
  245. package/dist/service-media.d.ts +272 -0
  246. package/dist/service-media.d.ts.map +1 -0
  247. package/dist/service-media.js +320 -0
  248. package/dist/service-merchandising.d.ts +184 -0
  249. package/dist/service-merchandising.d.ts.map +1 -0
  250. package/dist/service-merchandising.js +181 -0
  251. package/dist/service-option-translations.d.ts +268 -0
  252. package/dist/service-option-translations.d.ts.map +1 -0
  253. package/dist/service-option-translations.js +300 -0
  254. package/dist/service-options.d.ts +181 -0
  255. package/dist/service-options.d.ts.map +1 -0
  256. package/dist/service-options.js +179 -0
  257. package/dist/service-product-destinations.d.ts +37 -0
  258. package/dist/service-product-destinations.d.ts.map +1 -0
  259. package/dist/service-product-destinations.js +94 -0
  260. package/dist/service-public.d.ts +664 -0
  261. package/dist/service-public.d.ts.map +1 -0
  262. package/dist/service-public.js +374 -0
  263. package/dist/service-taxonomy.d.ts +197 -0
  264. package/dist/service-taxonomy.d.ts.map +1 -0
  265. package/dist/service-taxonomy.js +221 -0
  266. package/dist/service.d.ts +3929 -0
  267. package/dist/service.d.ts.map +1 -0
  268. package/dist/service.js +28 -0
  269. package/dist/tasks/brochure-printers.d.ts +31 -0
  270. package/dist/tasks/brochure-printers.d.ts.map +1 -0
  271. package/dist/tasks/brochure-printers.js +149 -0
  272. package/dist/tasks/brochure-templates.d.ts +36 -0
  273. package/dist/tasks/brochure-templates.d.ts.map +1 -0
  274. package/dist/tasks/brochure-templates.js +110 -0
  275. package/dist/tasks/brochures.d.ts +43 -0
  276. package/dist/tasks/brochures.d.ts.map +1 -0
  277. package/dist/tasks/brochures.js +72 -0
  278. package/dist/tasks/generate-pdf.d.ts +8 -0
  279. package/dist/tasks/generate-pdf.d.ts.map +1 -0
  280. package/dist/tasks/generate-pdf.js +106 -0
  281. package/dist/tasks/index.d.ts +5 -0
  282. package/dist/tasks/index.d.ts.map +1 -0
  283. package/dist/tasks/index.js +4 -0
  284. package/dist/tasks/pdf-text.d.ts +2 -0
  285. package/dist/tasks/pdf-text.d.ts.map +1 -0
  286. package/dist/tasks/pdf-text.js +40 -0
  287. package/dist/tasks.d.ts +2 -0
  288. package/dist/tasks.d.ts.map +1 -0
  289. package/dist/tasks.js +1 -0
  290. package/dist/validation-catalog.d.ts +2 -0
  291. package/dist/validation-catalog.d.ts.map +1 -0
  292. package/dist/validation-catalog.js +3 -0
  293. package/dist/validation-config.d.ts +2 -0
  294. package/dist/validation-config.d.ts.map +1 -0
  295. package/dist/validation-config.js +3 -0
  296. package/dist/validation-content.d.ts +2 -0
  297. package/dist/validation-content.d.ts.map +1 -0
  298. package/dist/validation-content.js +3 -0
  299. package/dist/validation-core.d.ts +2 -0
  300. package/dist/validation-core.d.ts.map +1 -0
  301. package/dist/validation-core.js +3 -0
  302. package/dist/validation-public.d.ts +2 -0
  303. package/dist/validation-public.d.ts.map +1 -0
  304. package/dist/validation-public.js +3 -0
  305. package/dist/validation-shared.d.ts +2 -0
  306. package/dist/validation-shared.d.ts.map +1 -0
  307. package/dist/validation-shared.js +3 -0
  308. package/dist/validation.d.ts +2 -0
  309. package/dist/validation.d.ts.map +1 -0
  310. package/dist/validation.js +3 -0
  311. package/package.json +204 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Projection extension that joins product → categories (with ancestor walk)
3
+ * and product → tags, contributing taxonomy fields onto the product search
4
+ * document.
5
+ *
6
+ * Wire via `createProductDocumentBuilder({ extensions: [taxonomyExtension] })`.
7
+ * Requires the registry to include `productTaxonomyCatalogPolicy` —
8
+ * otherwise the contributed fields are silently dropped by the indexer's
9
+ * field-policy filter.
10
+ *
11
+ * Hierarchy denormalization: Typesense can't recurse, so a product linked
12
+ * to "Hiking" (parent: "Adventure") needs both labels in `categories[]` for
13
+ * the "Adventure" filter to match. The projection walks the parent chain
14
+ * iteratively, filtering inactive ancestors (so an operator-paused parent
15
+ * stops surfacing its still-active children under the parent filter).
16
+ *
17
+ * Localization (#502): when a `product_category_translations` /
18
+ * `product_tag_translations` row exists for the slice's locale, its `name`
19
+ * wins. Otherwise the projection falls back to the canonical
20
+ * `productCategories.name` / `productTags.name` (the legacy single-locale
21
+ * column). This makes the upgrade non-breaking — operators that haven't
22
+ * created translations keep seeing the English label on every locale slice
23
+ * exactly as before.
24
+ *
25
+ * Slugs stay single-locale on `productCategories.slug` (per #502 non-goals
26
+ * — operators want stable URLs that don't shift when translations are
27
+ * edited).
28
+ */
29
+ import { and, eq, inArray } from "drizzle-orm";
30
+ import { productCategories, productCategoryProducts, productCategoryTranslations, productTagProducts, productTags, productTagTranslations, } from "./schema-taxonomy.js";
31
+ /**
32
+ * Resolve linked category ids for a product, ordered by the operator-set
33
+ * `sortOrder` on `product_category_products` (lowest first). Direct links
34
+ * only — ancestors are walked separately.
35
+ */
36
+ async function fetchDirectCategoryLinks(db, productId) {
37
+ return db
38
+ .select({
39
+ categoryId: productCategoryProducts.categoryId,
40
+ sortOrder: productCategoryProducts.sortOrder,
41
+ })
42
+ .from(productCategoryProducts)
43
+ .where(eq(productCategoryProducts.productId, productId));
44
+ }
45
+ /**
46
+ * Walk up the parent chain from a set of seed category ids, filtering
47
+ * inactive rows. Returns every active row reachable from the seeds.
48
+ *
49
+ * Iterative breadth-first to bound depth and avoid Drizzle recursive-CTE
50
+ * complexity. Real-world category trees are O(depth ≤ 5), so this issues
51
+ * at most a handful of `inArray` lookups regardless of breadth.
52
+ *
53
+ * Cycle-protected via a visited set — a misconfigured parent loop won't
54
+ * spin the indexer.
55
+ */
56
+ async function walkActiveCategoryChain(db, seedIds) {
57
+ const resolved = new Map();
58
+ const visited = new Set();
59
+ let frontier = Array.from(new Set(seedIds));
60
+ while (frontier.length > 0) {
61
+ for (const id of frontier)
62
+ visited.add(id);
63
+ const rows = await db
64
+ .select({
65
+ id: productCategories.id,
66
+ parentId: productCategories.parentId,
67
+ name: productCategories.name,
68
+ slug: productCategories.slug,
69
+ active: productCategories.active,
70
+ })
71
+ .from(productCategories)
72
+ .where(and(inArray(productCategories.id, frontier), eq(productCategories.active, true)));
73
+ const nextFrontier = [];
74
+ for (const row of rows) {
75
+ resolved.set(row.id, row);
76
+ if (row.parentId && !visited.has(row.parentId) && !resolved.has(row.parentId)) {
77
+ nextFrontier.push(row.parentId);
78
+ }
79
+ }
80
+ frontier = Array.from(new Set(nextFrontier));
81
+ }
82
+ return resolved;
83
+ }
84
+ async function fetchProductTags(db, productId) {
85
+ return db
86
+ .select({ id: productTags.id, name: productTags.name })
87
+ .from(productTagProducts)
88
+ .innerJoin(productTags, eq(productTagProducts.tagId, productTags.id))
89
+ .where(eq(productTagProducts.productId, productId));
90
+ }
91
+ /**
92
+ * Look up locale-specific category names for the given category ids. Returns
93
+ * a Map keyed by category id; entries only exist when a translation row was
94
+ * found for the slice locale. Callers fall back to the canonical
95
+ * `productCategories.name` for any missing entry.
96
+ */
97
+ async function fetchCategoryTranslations(db, categoryIds, languageTag) {
98
+ if (categoryIds.length === 0)
99
+ return new Map();
100
+ const rows = await db
101
+ .select({
102
+ categoryId: productCategoryTranslations.categoryId,
103
+ name: productCategoryTranslations.name,
104
+ })
105
+ .from(productCategoryTranslations)
106
+ .where(and(inArray(productCategoryTranslations.categoryId, categoryIds), eq(productCategoryTranslations.languageTag, languageTag)));
107
+ const out = new Map();
108
+ for (const row of rows)
109
+ out.set(row.categoryId, row.name);
110
+ return out;
111
+ }
112
+ async function fetchTagTranslations(db, tagIds, languageTag) {
113
+ if (tagIds.length === 0)
114
+ return new Map();
115
+ const rows = await db
116
+ .select({
117
+ tagId: productTagTranslations.tagId,
118
+ name: productTagTranslations.name,
119
+ })
120
+ .from(productTagTranslations)
121
+ .where(and(inArray(productTagTranslations.tagId, tagIds), eq(productTagTranslations.languageTag, languageTag)));
122
+ const out = new Map();
123
+ for (const row of rows)
124
+ out.set(row.tagId, row.name);
125
+ return out;
126
+ }
127
+ /**
128
+ * Pure aggregation kernel. `categoryNameByLocale` and `tagNameByLocale`
129
+ * carry slice-locale translations; entries override the canonical name on
130
+ * the row. Missing entries fall back to the canonical name. Slug stays
131
+ * canonical regardless — there's no per-locale slug.
132
+ */
133
+ function buildTaxonomyProjection(directLinks, resolvedCategories, tags, categoryNameByLocale = new Map(), tagNameByLocale = new Map()) {
134
+ // Primary = first direct link (by sortOrder asc, then category name asc)
135
+ // that resolved as active. Tie-break uses the canonical row name so the
136
+ // ordering is stable across slice locales — a translation that sorts
137
+ // differently shouldn't shuffle which category is "primary".
138
+ const sortedDirect = [...directLinks].sort((a, b) => {
139
+ if (a.sortOrder !== b.sortOrder)
140
+ return a.sortOrder - b.sortOrder;
141
+ const nameA = resolvedCategories.get(a.categoryId)?.name ?? "";
142
+ const nameB = resolvedCategories.get(b.categoryId)?.name ?? "";
143
+ return nameA.localeCompare(nameB);
144
+ });
145
+ let primary = null;
146
+ for (const link of sortedDirect) {
147
+ const row = resolvedCategories.get(link.categoryId);
148
+ if (row) {
149
+ primary = row;
150
+ break;
151
+ }
152
+ }
153
+ // Categories[] = direct links + ancestors, deduped, ordered by direct-link
154
+ // sortOrder first then ancestor walk order. Stable enough that storefronts
155
+ // can rely on `categories[0]` being a representative label.
156
+ const seenIds = new Set();
157
+ const categoryIds = [];
158
+ const categoryNames = [];
159
+ const categorySlugs = [];
160
+ function emit(row) {
161
+ if (seenIds.has(row.id))
162
+ return;
163
+ seenIds.add(row.id);
164
+ categoryIds.push(row.id);
165
+ // Locale override wins; otherwise use the canonical row.name.
166
+ categoryNames.push(categoryNameByLocale.get(row.id) ?? row.name);
167
+ categorySlugs.push(row.slug);
168
+ }
169
+ // Pass 1: direct links first, in operator-controlled order.
170
+ for (const link of sortedDirect) {
171
+ const row = resolvedCategories.get(link.categoryId);
172
+ if (row)
173
+ emit(row);
174
+ }
175
+ // Pass 2: ancestors of direct links, walking up via parentId.
176
+ for (const link of sortedDirect) {
177
+ let cursor = resolvedCategories.get(link.categoryId)?.parentId ?? null;
178
+ const guard = new Set(); // local guard against malformed loops
179
+ while (cursor && !guard.has(cursor)) {
180
+ guard.add(cursor);
181
+ const parentRow = resolvedCategories.get(cursor);
182
+ if (!parentRow)
183
+ break;
184
+ emit(parentRow);
185
+ cursor = parentRow.parentId;
186
+ }
187
+ }
188
+ return {
189
+ categoryIds,
190
+ categoryNames,
191
+ categorySlugs,
192
+ primaryCategoryId: primary?.id ?? null,
193
+ primaryCategoryName: primary ? (categoryNameByLocale.get(primary.id) ?? primary.name) : null,
194
+ primaryCategorySlug: primary?.slug ?? null,
195
+ tagIds: tags.map((t) => t.id),
196
+ tagLabels: tags.map((t) => tagNameByLocale.get(t.id) ?? t.name),
197
+ };
198
+ }
199
+ /**
200
+ * Construct the taxonomy projection extension.
201
+ *
202
+ * Returns a `ProductProjectionExtension` ready to pass to
203
+ * `createProductDocumentBuilder`.
204
+ */
205
+ export function createProductTaxonomyProjectionExtension() {
206
+ return {
207
+ name: "products:taxonomy",
208
+ async project(db, productId, slice) {
209
+ const [directLinks, tags] = await Promise.all([
210
+ fetchDirectCategoryLinks(db, productId),
211
+ fetchProductTags(db, productId),
212
+ ]);
213
+ const seedIds = directLinks.map((l) => l.categoryId);
214
+ const resolvedCategories = seedIds.length > 0
215
+ ? await walkActiveCategoryChain(db, seedIds)
216
+ : new Map();
217
+ // Translations cover the FULL chain (direct + ancestors), so look up
218
+ // by every resolved-category id, not just the seed set.
219
+ const allCategoryIds = Array.from(resolvedCategories.keys());
220
+ const tagIds = tags.map((t) => t.id);
221
+ const [categoryNameByLocale, tagNameByLocale] = await Promise.all([
222
+ fetchCategoryTranslations(db, allCategoryIds, slice.locale),
223
+ fetchTagTranslations(db, tagIds, slice.locale),
224
+ ]);
225
+ const projection = buildTaxonomyProjection(directLinks, resolvedCategories, tags, categoryNameByLocale, tagNameByLocale);
226
+ return new Map([
227
+ ["categories[]", projection.categoryNames],
228
+ ["categoryIds[]", projection.categoryIds],
229
+ ["categorySlugs[]", projection.categorySlugs],
230
+ ["primaryCategoryId", projection.primaryCategoryId],
231
+ ["primaryCategoryName", projection.primaryCategoryName],
232
+ ["primaryCategorySlug", projection.primaryCategorySlug],
233
+ ["tagLabels[]", projection.tagLabels],
234
+ ["tagIds[]", projection.tagIds],
235
+ ]);
236
+ },
237
+ };
238
+ }
239
+ // Internal exports for unit tests — kept separate from the public surface.
240
+ export const __test__ = {
241
+ buildTaxonomyProjection,
242
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Catalog-plane integration for the products service.
3
+ *
4
+ * Adds catalog-aware service methods alongside the existing `productsService`
5
+ * surface in `service.ts`. Routes opt in: the original `getProductById` /
6
+ * `listProducts` continue to return raw DB rows; the methods here return
7
+ * resolved CatalogEntry views with overlays + visibility filtering applied.
8
+ *
9
+ * Existing service code is untouched. Migration is per-route, gradual.
10
+ *
11
+ * Naming note: this file is `service-catalog-plane.ts` (not `service-catalog.ts`)
12
+ * because the existing `service-catalog.ts` handles the products module's own
13
+ * catalog management (categories, tags, types). The "catalog plane" is the
14
+ * cross-vertical projection / overlay / snapshot infrastructure from
15
+ * `@voyant-travel/catalog`.
16
+ *
17
+ * See `docs/architecture/catalog-architecture.md` §9.1 for the integration
18
+ * pattern this file establishes (replicated for cruises, accommodations, etc.
19
+ * in their own service-catalog-plane.ts files).
20
+ */
21
+ import { type CaptureSnapshotInput, type DocumentBuilder, type DocumentEmitter, type FieldPolicy, type FieldPolicyRegistry, type IndexerDocument, type IndexerSlice, type PricingBasis, type Provenance, type ResolvedView, type ResolverScope, type Visibility } from "@voyant-travel/catalog";
22
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
23
+ import { products } from "./schema-core.js";
24
+ /**
25
+ * Maps a product row to a field-keyed projection consumable by the catalog
26
+ * resolver. Field paths match the policy registry declarations in
27
+ * `catalog-policy.ts`.
28
+ *
29
+ * Provenance fields (`source.kind`, `source.ref`, `seller.operator_id`) are
30
+ * synthesized: today's products module models operator-owned inventory
31
+ * exclusively, so `source.kind = "owned"` and `source.ref = undefined`.
32
+ * When sourced products land (e.g. via Voyant Connect), this helper picks
33
+ * up the provenance from a parallel provenance row instead.
34
+ */
35
+ export declare function productRowToProjection(row: typeof products.$inferSelect, context: {
36
+ sellerOperatorId: string;
37
+ }): ReadonlyMap<string, unknown>;
38
+ /**
39
+ * Returns the Provenance tuple for a product row. Owned products synthesize
40
+ * a `source.kind: "owned"` provenance with `static` freshness; sourced
41
+ * products (Voyant Connect / GDS / direct API) carry their actual source
42
+ * connection identity. Phase 1 ships only the owned form.
43
+ */
44
+ export declare function productProvenance(_row: typeof products.$inferSelect, _context: {
45
+ sellerOperatorId: string;
46
+ }): Provenance;
47
+ /** Service-context the catalog-aware methods need. Templates wire this in. */
48
+ export interface ProductCatalogContext {
49
+ /** The deployment's operator/tenant identifier — synthesized into provenance. */
50
+ sellerOperatorId: string;
51
+ /** Variant scope for the request. */
52
+ scope: ResolverScope;
53
+ }
54
+ /**
55
+ * Catalog-aware product fetch. Returns the resolved view (source projection
56
+ * + active overlays + visibility filtering) instead of the raw DB row.
57
+ *
58
+ * The original `productsService.getProductById` continues to return raw
59
+ * rows — routes that haven't migrated to the catalog plane keep working.
60
+ *
61
+ * Returns `null` if no product with `id` exists.
62
+ */
63
+ export declare function getResolvedProductById(db: AnyDrizzleDb, id: string, context: ProductCatalogContext): Promise<ResolvedView | null>;
64
+ /**
65
+ * Catalog-aware product list. Returns resolved views per row.
66
+ *
67
+ * Caller fetches the rows (typically via the existing `productsService.listProducts`
68
+ * with whatever filtering / pagination / sort the route applies) and passes
69
+ * them in. This keeps query construction in the existing service layer and
70
+ * adds the catalog overlay step on top.
71
+ *
72
+ * Overlays for the whole page are fetched in ONE query via
73
+ * `fetchOverlaysForEntities` and applied in-memory per product — the
74
+ * per-product output is byte-identical to calling `resolveEntityView`
75
+ * once per row, minus the N-1 sequential round trips.
76
+ *
77
+ * Real high-volume list paths (storefront browse, admin search) should
78
+ * still go through the search index instead — `IndexerService.search` is
79
+ * already wired for that purpose. Use this method for small admin-facing
80
+ * lists or detail-page composition where the index isn't on the read path.
81
+ */
82
+ export declare function listResolvedProducts(db: AnyDrizzleDb, rows: ReadonlyArray<typeof products.$inferSelect>, context: ProductCatalogContext): Promise<ResolvedView[]>;
83
+ /**
84
+ * Build a `CaptureSnapshotInput` for a product to feed into the catalog
85
+ * plane's `captureSnapshot` / `captureSnapshotGraph` helpers at booking
86
+ * commit time. Fetches the product, resolves its view (overlays applied,
87
+ * visibility filter for the supplied scope), and returns the snapshot
88
+ * input shape.
89
+ *
90
+ * Returns `null` if the product doesn't exist.
91
+ *
92
+ * Composition: a single-product booking calls this once and passes the
93
+ * result to `captureSnapshot`. A composite booking (e.g. a tour-package
94
+ * booking with referenced accommodations + excursions) calls this and the
95
+ * other verticals' equivalents, collects the inputs, and passes them all
96
+ * to `captureSnapshotGraph` in one transaction.
97
+ */
98
+ export declare function buildProductSnapshotInput(db: AnyDrizzleDb, productId: string, context: ProductCatalogContext & {
99
+ pricingBasis?: PricingBasis;
100
+ }): Promise<Omit<CaptureSnapshotInput, "bookingId"> | null>;
101
+ /**
102
+ * A projection extension contributes additional field-keyed entries to the
103
+ * product search document. The builder runs all extensions in parallel after
104
+ * fetching the product row, then merges their entries into the base
105
+ * projection before emitting.
106
+ *
107
+ * Used by child-entity registries (destinations, taxonomy, departures, etc.)
108
+ * to denormalize fields onto the product doc. See architecture §5.4 — the
109
+ * search index is the canonical place for cross-entity denormalization.
110
+ *
111
+ * `buildIndexerDocument` silently drops entries whose paths aren't registered
112
+ * in the field-policy registry — so an extension's contributed registry must
113
+ * be composed into the registry passed to `createProductDocumentBuilder` for
114
+ * its fields to actually land in the document.
115
+ */
116
+ export interface ProductProjectionExtension {
117
+ /** Identifier — used for diagnostics and logging only. */
118
+ readonly name: string;
119
+ /**
120
+ * Contribute additional projection entries for one product. The slice
121
+ * carries locale and audience for translation lookups and audience
122
+ * filtering.
123
+ */
124
+ project(db: AnyDrizzleDb, productId: string, slice: IndexerSlice): Promise<ReadonlyMap<string, unknown>>;
125
+ }
126
+ /**
127
+ * Compose the registry from the base product policy plus any contributing
128
+ * extensions' policies. Templates wire this when they enable child-entity
129
+ * registries.
130
+ */
131
+ export declare function createProductsRegistry(...extensionPolicies: ReadonlyArray<ReadonlyArray<FieldPolicy>>): FieldPolicyRegistry;
132
+ /**
133
+ * Construct a sync `DocumentEmitter` for products. The emitter takes a
134
+ * pre-fetched product row + a slice and returns the indexer document
135
+ * (filtered by visibility, with blob-only fields skipped).
136
+ *
137
+ * Bulk-reindex pipelines that already have rows in hand call this directly.
138
+ * Live reindex paths use `createProductDocumentBuilder` below, which fetches
139
+ * the row before emitting.
140
+ *
141
+ * Pass a custom `registry` when the deployment composes additional
142
+ * child-entity policies; otherwise the default products registry is used.
143
+ */
144
+ export declare function createProductDocumentEmitter(context: {
145
+ sellerOperatorId: string;
146
+ registry?: FieldPolicyRegistry;
147
+ }): DocumentEmitter<typeof products.$inferSelect>;
148
+ /**
149
+ * Async `DocumentBuilder` for products — fetches the row by id, then emits.
150
+ * Plug this into `IndexerService.reindexEntity` for live reindex events.
151
+ *
152
+ * Returns `null` if the product no longer exists (e.g. it was deleted
153
+ * between the reindex enqueue and the worker picking it up). Callers can
154
+ * treat `null` as a delete signal.
155
+ *
156
+ * `extensions` denormalize child-entity fields onto the product doc. They
157
+ * run in parallel after the base row is fetched. An extension that throws
158
+ * fails the whole build — failures here would otherwise produce silently
159
+ * incomplete documents.
160
+ *
161
+ * Pass a custom `registry` (composed via `createProductsRegistry`) when
162
+ * extensions contribute fields beyond the base products policy.
163
+ */
164
+ export declare function createProductDocumentBuilder(db: AnyDrizzleDb, context: {
165
+ sellerOperatorId: string;
166
+ extensions?: ReadonlyArray<ProductProjectionExtension>;
167
+ registry?: FieldPolicyRegistry;
168
+ }): DocumentBuilder;
169
+ /**
170
+ * Product-owned storefront-card projection. This extension keeps the
171
+ * customer catalog slice directly renderable by denormalizing localized
172
+ * routing, card media, duration, and map coordinates into the search doc.
173
+ */
174
+ export declare function createProductStorefrontCardProjectionExtension(): ProductProjectionExtension;
175
+ /**
176
+ * Re-exports for routes that only import from this file.
177
+ */
178
+ export type { CaptureSnapshotInput, DocumentBuilder, DocumentEmitter, IndexerDocument, IndexerSlice, PricingBasis, Provenance, ResolvedView, ResolverScope, Visibility, };
179
+ //# sourceMappingURL=service-catalog-plane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EAExB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAGlB,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAgB3C;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,QAAQ,CAAC,YAAY,EACjC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACpC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CA8C9B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,QAAQ,CAAC,YAAY,EAClC,QAAQ,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACrC,UAAU,CAKZ;AAED,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,gBAAgB,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,KAAK,EAAE,aAAa,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAS9B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,EACjD,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,EAAE,CAAC,CAkBzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC/D,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CASzD;AAMD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,0BAA0B;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,OAAO,CACL,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACzC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,iBAAiB,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,GAC9D,mBAAmB,CAOrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAC/B,GAAG,eAAe,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,CAWhD;AAsBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IACP,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,aAAa,CAAC,0BAA0B,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAC/B,GACA,eAAe,CA2BjB;AAED;;;;GAIG;AACH,wBAAgB,8CAA8C,IAAI,0BAA0B,CA0F3F;AA4ED;;GAEG;AACH,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,GACX,CAAA"}