@voyant-travel/inventory 0.3.7 → 0.3.8
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/dist/read-model.d.ts +6 -2
- package/dist/read-model.d.ts.map +1 -1
- package/dist/read-model.js +11 -10
- package/dist/routes-public.d.ts.map +1 -1
- package/dist/routes-public.js +16 -4
- package/dist/service-catalog.d.ts +1 -0
- package/dist/service-catalog.d.ts.map +1 -1
- package/dist/service-catalog.js +3 -1
- package/dist/service-public.d.ts.map +1 -1
- package/dist/service-public.js +21 -5
- package/package.json +5 -5
package/dist/read-model.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { KVStore } from "@voyant-travel/utils/cache";
|
|
2
2
|
export declare function productDocKey(productId: string, variant: string): string;
|
|
3
|
-
export declare function productSlugMapKey(slug: string): string;
|
|
3
|
+
export declare function productSlugMapKey(slug: string, variant: string): string;
|
|
4
4
|
/** Stable variant id from the detail query (currently just the locale). */
|
|
5
5
|
export declare function productDocVariant(query: {
|
|
6
6
|
languageTag?: string | null;
|
|
7
7
|
}): string;
|
|
8
|
+
export interface ProductSlugResolution {
|
|
9
|
+
productId: string;
|
|
10
|
+
languageTag: string | null;
|
|
11
|
+
}
|
|
8
12
|
/**
|
|
9
13
|
* Read-through document fetch. `null` compute results (missing/inactive
|
|
10
14
|
* product) are never cached — a 404 must not mask a product that
|
|
@@ -15,7 +19,7 @@ export declare function readThroughProductDoc<T>(kv: KVStore | undefined, key: s
|
|
|
15
19
|
fromReadModel: boolean;
|
|
16
20
|
}>;
|
|
17
21
|
/** Resolve a slug to a product id through the KV mapping. */
|
|
18
|
-
export declare function readThroughSlugMapping(kv: KVStore | undefined, slug: string, resolve: () => Promise<
|
|
22
|
+
export declare function readThroughSlugMapping(kv: KVStore | undefined, slug: string, variant: string, resolve: () => Promise<ProductSlugResolution | null>): Promise<ProductSlugResolution | null>;
|
|
19
23
|
/**
|
|
20
24
|
* Drop every cached document variant for a product. Uses KV `list` by
|
|
21
25
|
* prefix (optional on the KVStore contract — silently a no-op without
|
package/dist/read-model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-model.d.ts","sourceRoot":"","sources":["../src/read-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AA2BzD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"read-model.d.ts","sourceRoot":"","sources":["../src/read-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AA2BzD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,MAAM,CAEhF;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,EAAE,EAAE,OAAO,GAAG,SAAS,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,GAC/B,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,CAkBrD;AAED,6DAA6D;AAC7D,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,OAAO,GAAG,SAAS,EACvB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,GACnD,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAmBvC;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F"}
|
package/dist/read-model.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* `productsReadModelInvalidation` in routes.ts), and a generous TTL
|
|
11
11
|
* bounds staleness for anything invalidation misses.
|
|
12
12
|
*
|
|
13
|
-
* Slug lookups resolve through a short-lived slug→
|
|
14
|
-
* id-keyed document is shared between `/:id` and `/slug/:slug`. The
|
|
13
|
+
* Slug lookups resolve through a short-lived slug→product+locale mapping
|
|
14
|
+
* so the id-keyed document is shared between `/:id` and `/slug/:slug`. The
|
|
15
15
|
* mapping is deliberately NOT invalidated on mutation: it's cheap to
|
|
16
16
|
* refill, and its short TTL bounds the only affected case (a renamed
|
|
17
17
|
* slug serving the old document) to a few minutes.
|
|
@@ -24,8 +24,8 @@ const SLUG_MAP_TTL_SECONDS = 5 * 60;
|
|
|
24
24
|
export function productDocKey(productId, variant) {
|
|
25
25
|
return `${RM_PREFIX}:${productId}:${variant}`;
|
|
26
26
|
}
|
|
27
|
-
export function productSlugMapKey(slug) {
|
|
28
|
-
return `rm:v1:product-slug:${slug}`;
|
|
27
|
+
export function productSlugMapKey(slug, variant) {
|
|
28
|
+
return `rm:v1:product-slug:${variant}:${slug}`;
|
|
29
29
|
}
|
|
30
30
|
/** Stable variant id from the detail query (currently just the locale). */
|
|
31
31
|
export function productDocVariant(query) {
|
|
@@ -59,10 +59,11 @@ export async function readThroughProductDoc(kv, key, compute) {
|
|
|
59
59
|
return { data, fromReadModel: false };
|
|
60
60
|
}
|
|
61
61
|
/** Resolve a slug to a product id through the KV mapping. */
|
|
62
|
-
export async function readThroughSlugMapping(kv, slug, resolve) {
|
|
62
|
+
export async function readThroughSlugMapping(kv, slug, variant, resolve) {
|
|
63
|
+
const key = productSlugMapKey(slug, variant);
|
|
63
64
|
if (kv) {
|
|
64
65
|
try {
|
|
65
|
-
const hit = await kv.get(
|
|
66
|
+
const hit = await kv.get(key, { type: "json" });
|
|
66
67
|
if (hit)
|
|
67
68
|
return hit;
|
|
68
69
|
}
|
|
@@ -70,16 +71,16 @@ export async function readThroughSlugMapping(kv, slug, resolve) {
|
|
|
70
71
|
// fall through
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
74
|
+
const resolution = await resolve();
|
|
75
|
+
if (resolution && kv) {
|
|
75
76
|
try {
|
|
76
|
-
await kv.put(
|
|
77
|
+
await kv.put(key, JSON.stringify(resolution), { expirationTtl: SLUG_MAP_TTL_SECONDS });
|
|
77
78
|
}
|
|
78
79
|
catch {
|
|
79
80
|
// best-effort
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
return
|
|
83
|
+
return resolution;
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
86
|
* Drop every cached document variant for a product. Uses KV `list` by
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAqBjE,KAAK,GAAG,GAAG;IACT,QAAQ,EAAE;QACR,mEAAmE;QACnE,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB,CAAA;IACD,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AA2DD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAqBjE,KAAK,GAAG,GAAG;IACT,QAAQ,EAAE;QACR,mEAAmE;QACnE,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB,CAAA;IACD,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AA2DD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgG5B,CAAA;AAEJ,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
|
package/dist/routes-public.js
CHANGED
|
@@ -66,14 +66,26 @@ export const publicProductRoutes = new Hono()
|
|
|
66
66
|
const slug = c.req.param("slug");
|
|
67
67
|
// Resolve slug → id through the short-lived KV mapping so both detail
|
|
68
68
|
// routes share one id-keyed document per variant.
|
|
69
|
-
const
|
|
69
|
+
const requestedVariant = productDocVariant(query);
|
|
70
|
+
const resolution = await readThroughSlugMapping(kv, slug, requestedVariant, async () => {
|
|
70
71
|
const row = await publicProductsService.getCatalogProductBySlug(c.get("db"), slug, query);
|
|
71
|
-
|
|
72
|
+
const productId = row ? (row.id ?? null) : null;
|
|
73
|
+
if (!productId)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
productId,
|
|
77
|
+
languageTag: row.contentLanguageTag ??
|
|
78
|
+
query.languageTag ??
|
|
79
|
+
null,
|
|
80
|
+
};
|
|
72
81
|
});
|
|
73
|
-
if (!
|
|
82
|
+
if (!resolution) {
|
|
74
83
|
return c.json({ error: "Catalog product not found" }, 404);
|
|
75
84
|
}
|
|
76
|
-
const
|
|
85
|
+
const detailQuery = resolution.languageTag
|
|
86
|
+
? { ...query, languageTag: resolution.languageTag }
|
|
87
|
+
: query;
|
|
88
|
+
const { data } = await readThroughProductDoc(kv, productDocKey(resolution.productId, productDocVariant(detailQuery)), () => publicProductsService.getCatalogProductById(c.get("db"), resolution.productId, detailQuery));
|
|
77
89
|
if (!data) {
|
|
78
90
|
return c.json({ error: "Catalog product not found" }, 404);
|
|
79
91
|
}
|
|
@@ -7,6 +7,7 @@ type HydrateCatalogProductOptions = {
|
|
|
7
7
|
languageTag?: string | null;
|
|
8
8
|
fallbackLanguageTags?: string[];
|
|
9
9
|
};
|
|
10
|
+
export declare const DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS: readonly ["en", "ro"];
|
|
10
11
|
export declare const catalogProductsService: {
|
|
11
12
|
hydrateProducts(db: PostgresJsDatabase, productRows: CatalogProductRow[], options?: HydrateCatalogProductOptions): Promise<({
|
|
12
13
|
description: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-catalog.d.ts","sourceRoot":"","sources":["../src/service-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAWL,QAAQ,EAMT,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EACV,qBAAqB,EACrB,8BAA8B,EAE/B,MAAM,yBAAyB,CAAA;AAEhC,KAAK,iBAAiB,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAA;AAErD,KAAK,4BAA4B,GAAG;IAClC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC,CAAA;
|
|
1
|
+
{"version":3,"file":"service-catalog.d.ts","sourceRoot":"","sources":["../src/service-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAWL,QAAQ,EAMT,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EACV,qBAAqB,EACrB,8BAA8B,EAE/B,MAAM,yBAAyB,CAAA;AAEhC,KAAK,iBAAiB,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAA;AAErD,KAAK,4BAA4B,GAAG;IAClC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC,CAAA;AAED,eAAO,MAAM,6CAA6C,uBAAwB,CAAA;AA2alF,eAAO,MAAM,sBAAsB;wBAE3B,kBAAkB,eACT,iBAAiB,EAAE,YACvB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAkIjC,kBAAkB,SACf,8BAA8B,GACpC,OAAO,CAAC;QACT,IAAI,EAAE,qBAAqB,EAAE,CAAA;QAC7B,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAC;qCAoFI,kBAAkB,aACX,MAAM,UACV,OAAO,CAAC,IAAI,CAAC,8BAA8B,EAAE,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAa1F,CAAA"}
|
package/dist/service-catalog.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// agent-quality: file-size exception -- owner: inventory; existing service module stays co-located until a dedicated split preserves behavior and tests.
|
|
2
2
|
import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
|
|
3
3
|
import { destinations, destinationTranslations, productCapabilities, productCategories, productCategoryProducts, productDestinations, productFaqs, productFeatures, productLocations, productMedia, products, productTagProducts, productTags, productTranslations, productTypes, productVisibilitySettings, } from "./schema.js";
|
|
4
|
+
export const DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS = ["en", "ro"];
|
|
4
5
|
function normalizeDate(value) {
|
|
5
6
|
if (!value) {
|
|
6
7
|
return null;
|
|
@@ -456,7 +457,8 @@ export const catalogProductsService = {
|
|
|
456
457
|
const localizedProducts = (await this.hydrateProducts(db, rows, {
|
|
457
458
|
includeContent: true,
|
|
458
459
|
languageTag: query.languageTag,
|
|
459
|
-
fallbackLanguageTags: query.fallbackLanguageTags ??
|
|
460
|
+
fallbackLanguageTags: query.fallbackLanguageTags ??
|
|
461
|
+
(query.languageTag ? [...DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS] : []),
|
|
460
462
|
}));
|
|
461
463
|
const rowById = new Map(rows.map((row) => [row.id, row]));
|
|
462
464
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAmBjE,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,6BAA6B,EAC7B,qCAAqC,EACrC,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AA0I/B,eAAO,MAAM,qBAAqB;4BAE1B,kBAAkB,SACf,6BAA6B,GAAG;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA6H/D,kBAAkB,MAClB,MAAM,UACH;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA2BlC,kBAAkB,QAChB,MAAM,UACL,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAoDxC,kBAAkB,aACX,MAAM,UACV;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;8BAMR,kBAAkB,SAAS,8BAA8B;;;;;;;;;;;;;wBAkD/D,kBAAkB,SAAS,yBAAyB;;;;;;;;;gCA+B5C,kBAAkB,SAAS,iCAAiC;;;;;;;;;;;;;;;;;;;CAqG/F,CAAA"}
|
package/dist/service-public.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// agent-quality: file-size exception -- owner: inventory; existing service module stays co-located until a dedicated split preserves behavior and tests.
|
|
2
2
|
import { and, asc, desc, eq, ilike, inArray, notInArray, or, sql } from "drizzle-orm";
|
|
3
3
|
import { destinations, destinationTranslations, productCategories, productCategoryProducts, productDestinations, productLocations, products, productTagProducts, productTags, productTranslations, productVisibilitySettings, } from "./schema.js";
|
|
4
|
-
import { catalogProductsService } from "./service-catalog.js";
|
|
4
|
+
import { catalogProductsService, DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS, } from "./service-catalog.js";
|
|
5
5
|
function impossibleCondition() {
|
|
6
6
|
return sql `1 = 0`;
|
|
7
7
|
}
|
|
@@ -9,6 +9,11 @@ function normalizeLanguageTag(value) {
|
|
|
9
9
|
const normalized = value?.trim().toLowerCase();
|
|
10
10
|
return normalized || null;
|
|
11
11
|
}
|
|
12
|
+
function normalizeLanguageTagList(values) {
|
|
13
|
+
return Array.from(new Set(values
|
|
14
|
+
.map((value) => normalizeLanguageTag(value))
|
|
15
|
+
.filter((value) => Boolean(value))));
|
|
16
|
+
}
|
|
12
17
|
async function listProductIdsForCategory(db, categoryId) {
|
|
13
18
|
const rows = await db
|
|
14
19
|
.select({ productId: productCategoryProducts.productId })
|
|
@@ -198,6 +203,12 @@ export const publicProductsService = {
|
|
|
198
203
|
async getCatalogProductBySlug(db, slug, query = {}) {
|
|
199
204
|
const normalizedSlug = slug.trim().toLowerCase();
|
|
200
205
|
const normalizedLanguageTag = normalizeLanguageTag(query.languageTag);
|
|
206
|
+
const candidateLanguageTags = normalizedLanguageTag
|
|
207
|
+
? normalizeLanguageTagList([
|
|
208
|
+
normalizedLanguageTag,
|
|
209
|
+
...DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS,
|
|
210
|
+
])
|
|
211
|
+
: [];
|
|
201
212
|
const conditions = [
|
|
202
213
|
// agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
203
214
|
sql `lower(${productTranslations.slug}) = ${normalizedSlug}`,
|
|
@@ -206,9 +217,9 @@ export const publicProductsService = {
|
|
|
206
217
|
eq(products.visibility, "public"),
|
|
207
218
|
];
|
|
208
219
|
if (normalizedLanguageTag) {
|
|
209
|
-
conditions.push(
|
|
220
|
+
conditions.push(inArray(productTranslations.languageTag, candidateLanguageTags));
|
|
210
221
|
}
|
|
211
|
-
const
|
|
222
|
+
const rows = await db
|
|
212
223
|
.select({
|
|
213
224
|
productId: products.id,
|
|
214
225
|
languageTag: productTranslations.languageTag,
|
|
@@ -217,12 +228,17 @@ export const publicProductsService = {
|
|
|
217
228
|
.innerJoin(products, eq(products.id, productTranslations.productId))
|
|
218
229
|
.where(and(...conditions))
|
|
219
230
|
.orderBy(desc(productTranslations.updatedAt))
|
|
220
|
-
.limit(1);
|
|
231
|
+
.limit(candidateLanguageTags.length || 1);
|
|
232
|
+
const row = candidateLanguageTags.length > 0
|
|
233
|
+
? (candidateLanguageTags
|
|
234
|
+
.map((languageTag) => rows.find((item) => normalizeLanguageTag(item.languageTag) === languageTag))
|
|
235
|
+
.find(Boolean) ?? null)
|
|
236
|
+
: (rows[0] ?? null);
|
|
221
237
|
if (!row) {
|
|
222
238
|
return null;
|
|
223
239
|
}
|
|
224
240
|
return this.getCatalogProductById(db, row.productId, {
|
|
225
|
-
languageTag:
|
|
241
|
+
languageTag: row.languageTag,
|
|
226
242
|
});
|
|
227
243
|
},
|
|
228
244
|
async getCatalogProductBrochure(db, productId, query = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/inventory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -164,15 +164,15 @@
|
|
|
164
164
|
"zod": "^4.3.6",
|
|
165
165
|
"@voyant-travel/action-ledger": "^0.105.3",
|
|
166
166
|
"@voyant-travel/catalog": "^0.124.1",
|
|
167
|
+
"@voyant-travel/extras-contracts": "^0.104.2",
|
|
167
168
|
"@voyant-travel/core": "^0.110.0",
|
|
168
169
|
"@voyant-travel/db": "^0.108.4",
|
|
169
|
-
"@voyant-travel/extras-contracts": "^0.104.2",
|
|
170
170
|
"@voyant-travel/hono": "^0.112.2",
|
|
171
|
-
"@voyant-travel/
|
|
171
|
+
"@voyant-travel/commerce": "^0.8.1",
|
|
172
172
|
"@voyant-travel/storage": "^0.105.0",
|
|
173
173
|
"@voyant-travel/utils": "^0.105.2",
|
|
174
|
-
"@voyant-travel/
|
|
175
|
-
"@voyant-travel/
|
|
174
|
+
"@voyant-travel/products-contracts": "^0.105.7",
|
|
175
|
+
"@voyant-travel/operations": "^0.1.7"
|
|
176
176
|
},
|
|
177
177
|
"devDependencies": {
|
|
178
178
|
"@types/sanitize-html": "^2.16.1",
|