@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.
- package/LICENSE +201 -0
- package/dist/action-ledger-drift.d.ts +29 -0
- package/dist/action-ledger-drift.d.ts.map +1 -0
- package/dist/action-ledger-drift.js +338 -0
- package/dist/action-ledger.d.ts +104 -0
- package/dist/action-ledger.d.ts.map +1 -0
- package/dist/action-ledger.js +100 -0
- package/dist/authoring/builder.d.ts +37 -0
- package/dist/authoring/builder.d.ts.map +1 -0
- package/dist/authoring/builder.js +248 -0
- package/dist/authoring/clone-content.d.ts +38 -0
- package/dist/authoring/clone-content.d.ts.map +1 -0
- package/dist/authoring/clone-content.js +367 -0
- package/dist/authoring/clone-pricing.d.ts +9 -0
- package/dist/authoring/clone-pricing.d.ts.map +1 -0
- package/dist/authoring/clone-pricing.js +242 -0
- package/dist/authoring/clone.d.ts +45 -0
- package/dist/authoring/clone.d.ts.map +1 -0
- package/dist/authoring/clone.js +142 -0
- package/dist/authoring/errors.d.ts +21 -0
- package/dist/authoring/errors.d.ts.map +1 -0
- package/dist/authoring/errors.js +13 -0
- package/dist/authoring/extension.d.ts +248 -0
- package/dist/authoring/extension.d.ts.map +1 -0
- package/dist/authoring/extension.js +116 -0
- package/dist/authoring/index.d.ts +12 -0
- package/dist/authoring/index.d.ts.map +1 -0
- package/dist/authoring/index.js +11 -0
- package/dist/authoring/schema.d.ts +85 -0
- package/dist/authoring/schema.d.ts.map +1 -0
- package/dist/authoring/schema.js +16 -0
- package/dist/authoring/service.d.ts +28 -0
- package/dist/authoring/service.d.ts.map +1 -0
- package/dist/authoring/service.js +66 -0
- package/dist/authoring/spec.d.ts +524 -0
- package/dist/authoring/spec.d.ts.map +1 -0
- package/dist/authoring/spec.js +167 -0
- package/dist/authoring/validate.d.ts +17 -0
- package/dist/authoring/validate.d.ts.map +1 -0
- package/dist/authoring/validate.js +83 -0
- package/dist/authoring.d.ts +2 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +1 -0
- package/dist/booking-engine/handler-support.d.ts +91 -0
- package/dist/booking-engine/handler-support.d.ts.map +1 -0
- package/dist/booking-engine/handler-support.js +355 -0
- package/dist/booking-engine/handler.d.ts +404 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +398 -0
- package/dist/booking-engine/index.d.ts +8 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +7 -0
- package/dist/booking-engine.d.ts +2 -0
- package/dist/booking-engine.d.ts.map +1 -0
- package/dist/booking-engine.js +1 -0
- package/dist/booking-extension.d.ts +278 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +161 -0
- package/dist/catalog-policy-departures.d.ts +52 -0
- package/dist/catalog-policy-departures.d.ts.map +1 -0
- package/dist/catalog-policy-departures.js +169 -0
- package/dist/catalog-policy-destinations.d.ts +43 -0
- package/dist/catalog-policy-destinations.d.ts.map +1 -0
- package/dist/catalog-policy-destinations.js +165 -0
- package/dist/catalog-policy-pricing.d.ts +55 -0
- package/dist/catalog-policy-pricing.d.ts.map +1 -0
- package/dist/catalog-policy-pricing.js +109 -0
- package/dist/catalog-policy-promotions.d.ts +52 -0
- package/dist/catalog-policy-promotions.d.ts.map +1 -0
- package/dist/catalog-policy-promotions.js +270 -0
- package/dist/catalog-policy-taxonomy.d.ts +51 -0
- package/dist/catalog-policy-taxonomy.d.ts.map +1 -0
- package/dist/catalog-policy-taxonomy.js +191 -0
- package/dist/catalog-policy.d.ts +33 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +733 -0
- package/dist/content-shape.d.ts +15 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +28 -0
- package/dist/draft-shape.d.ts +43 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +48 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +32 -0
- package/dist/extras/catalog-policy.d.ts +30 -0
- package/dist/extras/catalog-policy.d.ts.map +1 -0
- package/dist/extras/catalog-policy.js +319 -0
- package/dist/extras/content-shape.d.ts +5 -0
- package/dist/extras/content-shape.d.ts.map +1 -0
- package/dist/extras/content-shape.js +13 -0
- package/dist/extras/draft-shape.d.ts +34 -0
- package/dist/extras/draft-shape.d.ts.map +1 -0
- package/dist/extras/draft-shape.js +69 -0
- package/dist/extras/routes.d.ts +380 -0
- package/dist/extras/routes.d.ts.map +1 -0
- package/dist/extras/routes.js +59 -0
- package/dist/extras/schema-sourced-content.d.ts +254 -0
- package/dist/extras/schema-sourced-content.d.ts.map +1 -0
- package/dist/extras/schema-sourced-content.js +45 -0
- package/dist/extras/schema.d.ts +628 -0
- package/dist/extras/schema.d.ts.map +1 -0
- package/dist/extras/schema.js +87 -0
- package/dist/extras/service-catalog-plane.d.ts +77 -0
- package/dist/extras/service-catalog-plane.d.ts.map +1 -0
- package/dist/extras/service-catalog-plane.js +219 -0
- package/dist/extras/service-content-synthesizer.d.ts +41 -0
- package/dist/extras/service-content-synthesizer.d.ts.map +1 -0
- package/dist/extras/service-content-synthesizer.js +138 -0
- package/dist/extras/service-content.d.ts +48 -0
- package/dist/extras/service-content.d.ts.map +1 -0
- package/dist/extras/service-content.js +253 -0
- package/dist/extras/service.d.ts +185 -0
- package/dist/extras/service.d.ts.map +1 -0
- package/dist/extras/service.js +96 -0
- package/dist/extras/validation.d.ts +437 -0
- package/dist/extras/validation.d.ts.map +1 -0
- package/dist/extras/validation.js +149 -0
- package/dist/extras.d.ts +267 -0
- package/dist/extras.d.ts.map +1 -0
- package/dist/extras.js +19 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/interface.d.ts +5869 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +54 -0
- package/dist/public-routes.d.ts +2 -0
- package/dist/public-routes.d.ts.map +1 -0
- package/dist/public-routes.js +1 -0
- package/dist/public-validation.d.ts +2 -0
- package/dist/public-validation.d.ts.map +1 -0
- package/dist/public-validation.js +1 -0
- package/dist/read-model.d.ts +25 -0
- package/dist/read-model.d.ts.map +1 -0
- package/dist/read-model.js +99 -0
- package/dist/route-env.d.ts +22 -0
- package/dist/route-env.d.ts.map +1 -0
- package/dist/route-env.js +1 -0
- package/dist/routes-associations.d.ts +164 -0
- package/dist/routes-associations.d.ts.map +1 -0
- package/dist/routes-associations.js +100 -0
- package/dist/routes-catalog.d.ts +436 -0
- package/dist/routes-catalog.d.ts.map +1 -0
- package/dist/routes-catalog.js +104 -0
- package/dist/routes-configuration.d.ts +773 -0
- package/dist/routes-configuration.d.ts.map +1 -0
- package/dist/routes-configuration.js +364 -0
- package/dist/routes-content.d.ts +74 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +117 -0
- package/dist/routes-core.d.ts +331 -0
- package/dist/routes-core.d.ts.map +1 -0
- package/dist/routes-core.js +95 -0
- package/dist/routes-itinerary.d.ts +759 -0
- package/dist/routes-itinerary.d.ts.map +1 -0
- package/dist/routes-itinerary.js +387 -0
- package/dist/routes-maintenance.d.ts +32 -0
- package/dist/routes-maintenance.d.ts.map +1 -0
- package/dist/routes-maintenance.js +14 -0
- package/dist/routes-media.d.ts +634 -0
- package/dist/routes-media.d.ts.map +1 -0
- package/dist/routes-media.js +245 -0
- package/dist/routes-merchandising.d.ts +1120 -0
- package/dist/routes-merchandising.d.ts.map +1 -0
- package/dist/routes-merchandising.js +377 -0
- package/dist/routes-options.d.ts +363 -0
- package/dist/routes-options.d.ts.map +1 -0
- package/dist/routes-options.js +173 -0
- package/dist/routes-public.d.ts +776 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +119 -0
- package/dist/routes-translations.d.ts +489 -0
- package/dist/routes-translations.d.ts.map +1 -0
- package/dist/routes-translations.js +258 -0
- package/dist/routes.d.ts +5097 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +64 -0
- package/dist/schema-core.d.ts +1238 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +157 -0
- package/dist/schema-itinerary.d.ts +1169 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +130 -0
- package/dist/schema-relations.d.ts +117 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +192 -0
- package/dist/schema-settings.d.ts +1800 -0
- package/dist/schema-settings.d.ts.map +1 -0
- package/dist/schema-settings.js +220 -0
- package/dist/schema-shared.d.ts +15 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +91 -0
- package/dist/schema-sourced-content.d.ts +262 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +69 -0
- package/dist/schema-taxonomy.d.ts +1363 -0
- package/dist/schema-taxonomy.d.ts.map +1 -0
- package/dist/schema-taxonomy.js +203 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +9 -0
- package/dist/service-aggregates.d.ts +29 -0
- package/dist/service-aggregates.d.ts.map +1 -0
- package/dist/service-aggregates.js +56 -0
- package/dist/service-catalog-plane-destinations.d.ts +30 -0
- package/dist/service-catalog-plane-destinations.d.ts.map +1 -0
- package/dist/service-catalog-plane-destinations.js +143 -0
- package/dist/service-catalog-plane-taxonomy.d.ts +73 -0
- package/dist/service-catalog-plane-taxonomy.d.ts.map +1 -0
- package/dist/service-catalog-plane-taxonomy.js +242 -0
- package/dist/service-catalog-plane.d.ts +179 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +431 -0
- package/dist/service-catalog.d.ts +251 -0
- package/dist/service-catalog.d.ts.map +1 -0
- package/dist/service-catalog.js +517 -0
- package/dist/service-configuration.d.ts +261 -0
- package/dist/service-configuration.d.ts.map +1 -0
- package/dist/service-configuration.js +343 -0
- package/dist/service-content-owned.d.ts +68 -0
- package/dist/service-content-owned.d.ts.map +1 -0
- package/dist/service-content-owned.js +329 -0
- package/dist/service-content-synthesizer.d.ts +90 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +178 -0
- package/dist/service-content.d.ts +106 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +388 -0
- package/dist/service-core.d.ts +194 -0
- package/dist/service-core.d.ts.map +1 -0
- package/dist/service-core.js +213 -0
- package/dist/service-delivery-formats.d.ts +58 -0
- package/dist/service-delivery-formats.d.ts.map +1 -0
- package/dist/service-delivery-formats.js +107 -0
- package/dist/service-destinations.d.ts +223 -0
- package/dist/service-destinations.d.ts.map +1 -0
- package/dist/service-destinations.js +310 -0
- package/dist/service-itinerary-history.d.ts +457 -0
- package/dist/service-itinerary-history.d.ts.map +1 -0
- package/dist/service-itinerary-history.js +135 -0
- package/dist/service-itinerary.d.ts +1149 -0
- package/dist/service-itinerary.d.ts.map +1 -0
- package/dist/service-itinerary.js +419 -0
- package/dist/service-media.d.ts +272 -0
- package/dist/service-media.d.ts.map +1 -0
- package/dist/service-media.js +320 -0
- package/dist/service-merchandising.d.ts +184 -0
- package/dist/service-merchandising.d.ts.map +1 -0
- package/dist/service-merchandising.js +181 -0
- package/dist/service-option-translations.d.ts +268 -0
- package/dist/service-option-translations.d.ts.map +1 -0
- package/dist/service-option-translations.js +300 -0
- package/dist/service-options.d.ts +181 -0
- package/dist/service-options.d.ts.map +1 -0
- package/dist/service-options.js +179 -0
- package/dist/service-product-destinations.d.ts +37 -0
- package/dist/service-product-destinations.d.ts.map +1 -0
- package/dist/service-product-destinations.js +94 -0
- package/dist/service-public.d.ts +664 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +374 -0
- package/dist/service-taxonomy.d.ts +197 -0
- package/dist/service-taxonomy.d.ts.map +1 -0
- package/dist/service-taxonomy.js +221 -0
- package/dist/service.d.ts +3929 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +28 -0
- package/dist/tasks/brochure-printers.d.ts +31 -0
- package/dist/tasks/brochure-printers.d.ts.map +1 -0
- package/dist/tasks/brochure-printers.js +149 -0
- package/dist/tasks/brochure-templates.d.ts +36 -0
- package/dist/tasks/brochure-templates.d.ts.map +1 -0
- package/dist/tasks/brochure-templates.js +110 -0
- package/dist/tasks/brochures.d.ts +43 -0
- package/dist/tasks/brochures.d.ts.map +1 -0
- package/dist/tasks/brochures.js +72 -0
- package/dist/tasks/generate-pdf.d.ts +8 -0
- package/dist/tasks/generate-pdf.d.ts.map +1 -0
- package/dist/tasks/generate-pdf.js +106 -0
- package/dist/tasks/index.d.ts +5 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/pdf-text.d.ts +2 -0
- package/dist/tasks/pdf-text.d.ts.map +1 -0
- package/dist/tasks/pdf-text.js +40 -0
- package/dist/tasks.d.ts +2 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +1 -0
- package/dist/validation-catalog.d.ts +2 -0
- package/dist/validation-catalog.d.ts.map +1 -0
- package/dist/validation-catalog.js +3 -0
- package/dist/validation-config.d.ts +2 -0
- package/dist/validation-config.d.ts.map +1 -0
- package/dist/validation-config.js +3 -0
- package/dist/validation-content.d.ts +2 -0
- package/dist/validation-content.d.ts.map +1 -0
- package/dist/validation-content.js +3 -0
- package/dist/validation-core.d.ts +2 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +3 -0
- package/dist/validation-public.d.ts +2 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +3 -0
- package/dist/validation-shared.d.ts +2 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +3 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +3 -0
- 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":"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"}
|