@voyantjs/products 0.30.7 → 0.31.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.
@@ -1 +1 @@
1
- {"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EA0Y3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
1
+ {"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EAwgB3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
@@ -144,6 +144,34 @@ const PRODUCT_FIELD_POLICY = [
144
144
  overrideFriction: "none",
145
145
  sourceFreshness: "sync",
146
146
  },
147
+ {
148
+ path: "shortDescription",
149
+ class: "merchandisable",
150
+ merge: "replace",
151
+ drift: "low",
152
+ reindex: "entry-locale",
153
+ snapshot: "on-book",
154
+ query: "indexed-column",
155
+ localized: true,
156
+ visibility: ["staff", "customer", "partner"],
157
+ editRole: "marketing",
158
+ overrideFriction: "none",
159
+ sourceFreshness: "sync",
160
+ },
161
+ {
162
+ path: "slug",
163
+ class: "merchandisable",
164
+ merge: "replace",
165
+ drift: "medium",
166
+ reindex: "entry-locale",
167
+ snapshot: "on-book",
168
+ query: "indexed-column",
169
+ localized: true,
170
+ visibility: ["staff", "customer", "partner"],
171
+ editRole: "marketing",
172
+ overrideFriction: "none",
173
+ sourceFreshness: "sync",
174
+ },
147
175
  // TODO(catalog): split into marketing_tags + facet_tags per architecture
148
176
  // §7.1 (human-readable + machine-evaluable rule). Today's schema has one
149
177
  // `tags` column; declared here as merchandisable + additive-set so
@@ -321,6 +349,104 @@ const PRODUCT_FIELD_POLICY = [
321
349
  overrideFriction: "none",
322
350
  sourceFreshness: "sync",
323
351
  },
352
+ {
353
+ path: "startDateEpochDays",
354
+ class: "structural",
355
+ merge: "source-only",
356
+ drift: "medium",
357
+ reindex: "facet-affecting",
358
+ snapshot: "on-book",
359
+ query: "indexed-column",
360
+ localized: false,
361
+ visibility: ["staff", "customer", "partner"],
362
+ editRole: "none",
363
+ overrideFriction: "none",
364
+ sourceFreshness: "sync",
365
+ },
366
+ {
367
+ path: "endDateEpochDays",
368
+ class: "structural",
369
+ merge: "source-only",
370
+ drift: "medium",
371
+ reindex: "facet-affecting",
372
+ snapshot: "on-book",
373
+ query: "indexed-column",
374
+ localized: false,
375
+ visibility: ["staff", "customer", "partner"],
376
+ editRole: "none",
377
+ overrideFriction: "none",
378
+ sourceFreshness: "sync",
379
+ },
380
+ {
381
+ path: "durationDays",
382
+ class: "structural",
383
+ merge: "source-only",
384
+ drift: "low",
385
+ reindex: "facet-affecting",
386
+ snapshot: "on-book",
387
+ query: "indexed-column",
388
+ localized: false,
389
+ visibility: ["staff", "customer", "partner"],
390
+ editRole: "none",
391
+ overrideFriction: "none",
392
+ sourceFreshness: "sync",
393
+ },
394
+ {
395
+ path: "primaryMediaUrl",
396
+ class: "merchandisable",
397
+ merge: "source-only",
398
+ drift: "low",
399
+ reindex: "entry",
400
+ snapshot: "on-book",
401
+ query: "indexed-column",
402
+ localized: false,
403
+ visibility: ["staff", "customer", "partner"],
404
+ editRole: "none",
405
+ overrideFriction: "none",
406
+ sourceFreshness: "sync",
407
+ },
408
+ {
409
+ path: "coverMediaUrl",
410
+ class: "merchandisable",
411
+ merge: "source-only",
412
+ drift: "low",
413
+ reindex: "entry",
414
+ snapshot: "on-book",
415
+ query: "indexed-column",
416
+ localized: false,
417
+ visibility: ["staff", "customer", "partner"],
418
+ editRole: "none",
419
+ overrideFriction: "none",
420
+ sourceFreshness: "sync",
421
+ },
422
+ {
423
+ path: "latitude",
424
+ class: "structural",
425
+ merge: "source-only",
426
+ drift: "low",
427
+ reindex: "facet-affecting",
428
+ snapshot: "on-book",
429
+ query: "indexed-column",
430
+ localized: false,
431
+ visibility: ["staff", "customer", "partner"],
432
+ editRole: "none",
433
+ overrideFriction: "none",
434
+ sourceFreshness: "sync",
435
+ },
436
+ {
437
+ path: "longitude",
438
+ class: "structural",
439
+ merge: "source-only",
440
+ drift: "low",
441
+ reindex: "facet-affecting",
442
+ snapshot: "on-book",
443
+ query: "indexed-column",
444
+ localized: false,
445
+ visibility: ["staff", "customer", "partner"],
446
+ editRole: "none",
447
+ overrideFriction: "none",
448
+ sourceFreshness: "sync",
449
+ },
324
450
  {
325
451
  path: "timezone",
326
452
  class: "managed",
package/dist/routes.d.ts CHANGED
@@ -1278,13 +1278,13 @@ export declare const productRoutes: import("hono/hono-base").HonoBase<Env, {
1278
1278
  updatedAt: string;
1279
1279
  productId: string;
1280
1280
  title: string;
1281
+ latitude: number | null;
1282
+ longitude: number | null;
1281
1283
  city: string | null;
1282
1284
  address: string | null;
1283
1285
  sortOrder: number;
1284
1286
  countryCode: string | null;
1285
1287
  locationType: "start" | "other" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
1286
- latitude: number | null;
1287
- longitude: number | null;
1288
1288
  googlePlaceId: string | null;
1289
1289
  applePlaceId: string | null;
1290
1290
  tripadvisorLocationId: string | null;
@@ -2433,8 +2433,8 @@ export declare const productRoutes: import("hono/hono-base").HonoBase<Env, {
2433
2433
  slug: string | null;
2434
2434
  description: string | null;
2435
2435
  productId: string;
2436
- languageTag: string;
2437
2436
  shortDescription: string | null;
2437
+ languageTag: string;
2438
2438
  seoTitle: string | null;
2439
2439
  seoDescription: string | null;
2440
2440
  };
@@ -2592,8 +2592,8 @@ export declare const productRoutes: import("hono/hono-base").HonoBase<Env, {
2592
2592
  updatedAt: string;
2593
2593
  description: string | null;
2594
2594
  optionId: string;
2595
- languageTag: string;
2596
2595
  shortDescription: string | null;
2596
+ languageTag: string;
2597
2597
  };
2598
2598
  };
2599
2599
  outputFormat: "json";
@@ -2746,8 +2746,8 @@ export declare const productRoutes: import("hono/hono-base").HonoBase<Env, {
2746
2746
  updatedAt: string;
2747
2747
  description: string | null;
2748
2748
  unitId: string;
2749
- languageTag: string;
2750
2749
  shortDescription: string | null;
2750
+ languageTag: string;
2751
2751
  };
2752
2752
  };
2753
2753
  outputFormat: "json";
@@ -167,6 +167,12 @@ export declare function createProductDocumentBuilder(db: AnyDrizzleDb, context:
167
167
  extensions?: ReadonlyArray<ProductProjectionExtension>;
168
168
  registry?: FieldPolicyRegistry;
169
169
  }): DocumentBuilder;
170
+ /**
171
+ * Product-owned storefront-card projection. This extension keeps the
172
+ * customer catalog slice directly renderable by denormalizing localized
173
+ * routing, card media, duration, and map coordinates into the search doc.
174
+ */
175
+ export declare function createProductStorefrontCardProjectionExtension(): ProductProjectionExtension;
170
176
  /**
171
177
  * Re-exports for routes that only import from this file.
172
178
  */
@@ -1 +1 @@
1
- {"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"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"}
@@ -19,9 +19,11 @@
19
19
  * in their own service-catalog-plane.ts files).
20
20
  */
21
21
  import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyantjs/catalog";
22
- import { eq } from "drizzle-orm";
22
+ import { asc, eq } from "drizzle-orm";
23
23
  import { productCatalogPolicy } from "./catalog-policy.js";
24
24
  import { products } from "./schema-core.js";
25
+ import { productDays, productItineraries, productMedia } from "./schema-itinerary.js";
26
+ import { productLocations, productTranslations } from "./schema-settings.js";
25
27
  /**
26
28
  * Lazy-initialized registry. Built once per process; the field-policy file
27
29
  * is static so this is safe to memoize.
@@ -69,6 +71,8 @@ export function productRowToProjection(row, context) {
69
71
  ["pax", row.pax],
70
72
  ["startDate", row.startDate],
71
73
  ["endDate", row.endDate],
74
+ ["startDateEpochDays", dateToEpochDays(row.startDate)],
75
+ ["endDateEpochDays", dateToEpochDays(row.endDate)],
72
76
  ["timezone", row.timezone],
73
77
  ["reservationTimeoutMinutes", row.reservationTimeoutMinutes],
74
78
  // Pricing (configured defaults — quote-time prices come from pricing module)
@@ -206,6 +210,17 @@ export function createProductDocumentEmitter(context) {
206
210
  },
207
211
  };
208
212
  }
213
+ function isPublicStorefrontProduct(row) {
214
+ return row.status === "active" && row.activated === true && row.visibility === "public";
215
+ }
216
+ function shouldEmitForSlice(row, slice) {
217
+ if (slice.audience === "customer" ||
218
+ slice.audience === "partner" ||
219
+ slice.audience === "supplier") {
220
+ return isPublicStorefrontProduct(row);
221
+ }
222
+ return true;
223
+ }
209
224
  /**
210
225
  * Async `DocumentBuilder` for products — fetches the row by id, then emits.
211
226
  * Plug this into `IndexerService.reindexEntity` for live reindex events.
@@ -230,6 +245,8 @@ export function createProductDocumentBuilder(db, context) {
230
245
  const row = rows[0];
231
246
  if (!row)
232
247
  return null;
248
+ if (!shouldEmitForSlice(row, slice))
249
+ return null;
233
250
  const baseProjection = productRowToProjection(row, {
234
251
  sellerOperatorId: context.sellerOperatorId,
235
252
  });
@@ -246,3 +263,101 @@ export function createProductDocumentBuilder(db, context) {
246
263
  return buildIndexerDocument(registry, merged, slice, entityId);
247
264
  };
248
265
  }
266
+ /**
267
+ * Product-owned storefront-card projection. This extension keeps the
268
+ * customer catalog slice directly renderable by denormalizing localized
269
+ * routing, card media, duration, and map coordinates into the search doc.
270
+ */
271
+ export function createProductStorefrontCardProjectionExtension() {
272
+ return {
273
+ name: "products:storefront-card",
274
+ async project(db, productId, slice) {
275
+ const [translations, mediaRows, locationRows, itineraryRows] = await Promise.all([
276
+ db
277
+ .select({
278
+ languageTag: productTranslations.languageTag,
279
+ name: productTranslations.name,
280
+ slug: productTranslations.slug,
281
+ shortDescription: productTranslations.shortDescription,
282
+ })
283
+ .from(productTranslations)
284
+ .where(eq(productTranslations.productId, productId))
285
+ .orderBy(asc(productTranslations.updatedAt)),
286
+ db
287
+ .select({
288
+ url: productMedia.url,
289
+ isCover: productMedia.isCover,
290
+ isBrochure: productMedia.isBrochure,
291
+ sortOrder: productMedia.sortOrder,
292
+ createdAt: productMedia.createdAt,
293
+ })
294
+ .from(productMedia)
295
+ .where(eq(productMedia.productId, productId))
296
+ .orderBy(asc(productMedia.sortOrder), asc(productMedia.createdAt)),
297
+ db
298
+ .select({
299
+ latitude: productLocations.latitude,
300
+ longitude: productLocations.longitude,
301
+ sortOrder: productLocations.sortOrder,
302
+ createdAt: productLocations.createdAt,
303
+ })
304
+ .from(productLocations)
305
+ .where(eq(productLocations.productId, productId))
306
+ .orderBy(asc(productLocations.sortOrder), asc(productLocations.createdAt)),
307
+ db
308
+ .select({ id: productItineraries.id, isDefault: productItineraries.isDefault })
309
+ .from(productItineraries)
310
+ .where(eq(productItineraries.productId, productId))
311
+ .orderBy(asc(productItineraries.sortOrder)),
312
+ ]);
313
+ const translation = pickTranslation(translations, slice.locale);
314
+ const cover = mediaRows.filter((m) => !m.isBrochure).find((m) => m.isCover);
315
+ const primaryMedia = cover ?? mediaRows.find((m) => !m.isBrochure) ?? null;
316
+ const coordinateLocation = locationRows.find((l) => l.latitude != null && l.longitude != null) ?? null;
317
+ const defaultItinerary = itineraryRows.find((it) => it.isDefault) ?? itineraryRows[0];
318
+ const durationDays = defaultItinerary
319
+ ? await estimateItineraryDurationDays(db, defaultItinerary.id)
320
+ : null;
321
+ const out = new Map([
322
+ ["slug", translation?.slug ?? null],
323
+ ["shortDescription", translation?.shortDescription ?? null],
324
+ ["primaryMediaUrl", primaryMedia?.url ?? null],
325
+ ["coverMediaUrl", primaryMedia?.url ?? null],
326
+ ["durationDays", durationDays],
327
+ ["latitude", coordinateLocation?.latitude ?? null],
328
+ ["longitude", coordinateLocation?.longitude ?? null],
329
+ ]);
330
+ if (translation?.name) {
331
+ out.set("name", translation.name);
332
+ }
333
+ return out;
334
+ },
335
+ };
336
+ }
337
+ function pickTranslation(rows, locale) {
338
+ return (rows.find((row) => row.languageTag === locale) ??
339
+ rows.find((row) => row.languageTag.toLowerCase() === locale.toLowerCase()) ??
340
+ rows.find((row) => row.languageTag.split("-")[0] === locale.split("-")[0]) ??
341
+ rows[0] ??
342
+ null);
343
+ }
344
+ async function estimateItineraryDurationDays(db, itineraryId) {
345
+ const rows = await db
346
+ .select({ dayNumber: productDays.dayNumber })
347
+ .from(productDays)
348
+ .where(eq(productDays.itineraryId, itineraryId))
349
+ .orderBy(asc(productDays.dayNumber));
350
+ if (rows.length === 0)
351
+ return null;
352
+ const max = Math.max(...rows.map((row) => row.dayNumber));
353
+ return Number.isFinite(max) && max > 0 ? max : null;
354
+ }
355
+ function dateToEpochDays(value) {
356
+ if (!value)
357
+ return null;
358
+ const date = typeof value === "string" ? new Date(value) : value;
359
+ const time = date.getTime();
360
+ if (Number.isNaN(time))
361
+ return null;
362
+ return Math.floor(time / (24 * 60 * 60 * 1000));
363
+ }
package/dist/service.d.ts CHANGED
@@ -574,13 +574,13 @@ export declare const productsService: {
574
574
  updatedAt: Date;
575
575
  productId: string;
576
576
  title: string;
577
+ latitude: number | null;
578
+ longitude: number | null;
577
579
  city: string | null;
578
580
  address: string | null;
579
581
  sortOrder: number;
580
582
  countryCode: string | null;
581
583
  locationType: "start" | "other" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
582
- latitude: number | null;
583
- longitude: number | null;
584
584
  googlePlaceId: string | null;
585
585
  applePlaceId: string | null;
586
586
  tripadvisorLocationId: string | null;
@@ -1013,8 +1013,8 @@ export declare const productsService: {
1013
1013
  slug: string | null;
1014
1014
  description: string | null;
1015
1015
  productId: string;
1016
- languageTag: string;
1017
1016
  shortDescription: string | null;
1017
+ languageTag: string;
1018
1018
  seoTitle: string | null;
1019
1019
  seoDescription: string | null;
1020
1020
  } | null>;
@@ -1066,8 +1066,8 @@ export declare const productsService: {
1066
1066
  updatedAt: Date;
1067
1067
  description: string | null;
1068
1068
  optionId: string;
1069
- languageTag: string;
1070
1069
  shortDescription: string | null;
1070
+ languageTag: string;
1071
1071
  } | null>;
1072
1072
  updateOptionTranslation(db: PostgresJsDatabase, id: string, data: UpdateProductOptionTranslationInput): Promise<{
1073
1073
  id: string;
@@ -1114,8 +1114,8 @@ export declare const productsService: {
1114
1114
  updatedAt: Date;
1115
1115
  description: string | null;
1116
1116
  unitId: string;
1117
- languageTag: string;
1118
1117
  shortDescription: string | null;
1118
+ languageTag: string;
1119
1119
  } | null>;
1120
1120
  updateUnitTranslation(db: PostgresJsDatabase, id: string, data: UpdateOptionUnitTranslationInput): Promise<{
1121
1121
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/products",
3
- "version": "0.30.7",
3
+ "version": "0.31.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -125,12 +125,12 @@
125
125
  "hono": "^4.12.10",
126
126
  "pdf-lib": "^1.17.1",
127
127
  "zod": "^4.3.6",
128
- "@voyantjs/core": "0.30.7",
129
- "@voyantjs/db": "0.30.7",
130
- "@voyantjs/hono": "0.30.7",
131
- "@voyantjs/utils": "0.30.7",
132
- "@voyantjs/catalog": "0.30.7",
133
- "@voyantjs/storage": "0.30.7"
128
+ "@voyantjs/core": "0.31.0",
129
+ "@voyantjs/db": "0.31.0",
130
+ "@voyantjs/hono": "0.31.0",
131
+ "@voyantjs/utils": "0.31.0",
132
+ "@voyantjs/catalog": "0.31.0",
133
+ "@voyantjs/storage": "0.31.0"
134
134
  },
135
135
  "devDependencies": {
136
136
  "typescript": "^6.0.2",