@voyantjs/products 0.28.3 → 0.29.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 (43) hide show
  1. package/dist/booking-engine/handler.d.ts +8 -0
  2. package/dist/booking-engine/handler.d.ts.map +1 -1
  3. package/dist/booking-engine/handler.js +38 -0
  4. package/dist/catalog-policy-departures.d.ts +52 -0
  5. package/dist/catalog-policy-departures.d.ts.map +1 -0
  6. package/dist/catalog-policy-departures.js +169 -0
  7. package/dist/catalog-policy-destinations.d.ts +41 -0
  8. package/dist/catalog-policy-destinations.d.ts.map +1 -0
  9. package/dist/catalog-policy-destinations.js +121 -0
  10. package/dist/catalog-policy-pricing.d.ts +55 -0
  11. package/dist/catalog-policy-pricing.d.ts.map +1 -0
  12. package/dist/catalog-policy-pricing.js +109 -0
  13. package/dist/catalog-policy-promotions.d.ts +52 -0
  14. package/dist/catalog-policy-promotions.d.ts.map +1 -0
  15. package/dist/catalog-policy-promotions.js +270 -0
  16. package/dist/catalog-policy-taxonomy.d.ts +51 -0
  17. package/dist/catalog-policy-taxonomy.d.ts.map +1 -0
  18. package/dist/catalog-policy-taxonomy.js +191 -0
  19. package/dist/events.d.ts +1 -1
  20. package/dist/events.d.ts.map +1 -1
  21. package/dist/routes.d.ts +254 -19
  22. package/dist/routes.d.ts.map +1 -1
  23. package/dist/routes.js +73 -5
  24. package/dist/schema-taxonomy.d.ts +287 -0
  25. package/dist/schema-taxonomy.d.ts.map +1 -1
  26. package/dist/schema-taxonomy.js +47 -0
  27. package/dist/service-catalog-plane-destinations.d.ts +30 -0
  28. package/dist/service-catalog-plane-destinations.d.ts.map +1 -0
  29. package/dist/service-catalog-plane-destinations.js +119 -0
  30. package/dist/service-catalog-plane-taxonomy.d.ts +73 -0
  31. package/dist/service-catalog-plane-taxonomy.d.ts.map +1 -0
  32. package/dist/service-catalog-plane-taxonomy.js +242 -0
  33. package/dist/service-catalog-plane.d.ts +46 -1
  34. package/dist/service-catalog-plane.d.ts.map +1 -1
  35. package/dist/service-catalog-plane.js +42 -6
  36. package/dist/service.d.ts +98 -19
  37. package/dist/service.d.ts.map +1 -1
  38. package/dist/service.js +142 -1
  39. package/dist/tasks/brochures.d.ts +1 -1
  40. package/dist/validation-content.d.ts +38 -0
  41. package/dist/validation-content.d.ts.map +1 -1
  42. package/dist/validation-content.js +29 -0
  43. package/package.json +77 -7
@@ -360,6 +360,178 @@ export declare const productCategories: import("drizzle-orm/pg-core").PgTableWit
360
360
  }>;
361
361
  export type ProductCategory = typeof productCategories.$inferSelect;
362
362
  export type NewProductCategory = typeof productCategories.$inferInsert;
363
+ /**
364
+ * Locale-aware category labels. Mirrors `destinationTranslations`.
365
+ *
366
+ * The catalog plane's taxonomy projection (`catalog-policy-taxonomy.ts` +
367
+ * `service-catalog-plane-taxonomy.ts`) reads this table per-slice locale
368
+ * and falls back to `productCategories.name` when no row exists for a
369
+ * given `(categoryId, languageTag)`. Slug stays single-locale on
370
+ * `productCategories.slug` per #502 non-goals — operators want stable
371
+ * URLs that don't shift when translations are edited.
372
+ */
373
+ export declare const productCategoryTranslations: import("drizzle-orm/pg-core").PgTableWithColumns<{
374
+ name: "product_category_translations";
375
+ schema: undefined;
376
+ columns: {
377
+ id: import("drizzle-orm/pg-core").PgColumn<{
378
+ name: string;
379
+ tableName: "product_category_translations";
380
+ dataType: "string";
381
+ columnType: "PgText";
382
+ data: string;
383
+ driverParam: string;
384
+ notNull: true;
385
+ hasDefault: true;
386
+ isPrimaryKey: true;
387
+ isAutoincrement: false;
388
+ hasRuntimeDefault: true;
389
+ enumValues: [string, ...string[]];
390
+ baseColumn: never;
391
+ identity: undefined;
392
+ generated: undefined;
393
+ }, {}, {}>;
394
+ categoryId: import("drizzle-orm/pg-core").PgColumn<{
395
+ name: string;
396
+ tableName: "product_category_translations";
397
+ dataType: "string";
398
+ columnType: "PgText";
399
+ data: string;
400
+ driverParam: string;
401
+ notNull: true;
402
+ hasDefault: false;
403
+ isPrimaryKey: false;
404
+ isAutoincrement: false;
405
+ hasRuntimeDefault: false;
406
+ enumValues: [string, ...string[]];
407
+ baseColumn: never;
408
+ identity: undefined;
409
+ generated: undefined;
410
+ }, {}, {}>;
411
+ languageTag: import("drizzle-orm/pg-core").PgColumn<{
412
+ name: "language_tag";
413
+ tableName: "product_category_translations";
414
+ dataType: "string";
415
+ columnType: "PgText";
416
+ data: string;
417
+ driverParam: string;
418
+ notNull: true;
419
+ hasDefault: false;
420
+ isPrimaryKey: false;
421
+ isAutoincrement: false;
422
+ hasRuntimeDefault: false;
423
+ enumValues: [string, ...string[]];
424
+ baseColumn: never;
425
+ identity: undefined;
426
+ generated: undefined;
427
+ }, {}, {}>;
428
+ name: import("drizzle-orm/pg-core").PgColumn<{
429
+ name: "name";
430
+ tableName: "product_category_translations";
431
+ dataType: "string";
432
+ columnType: "PgText";
433
+ data: string;
434
+ driverParam: string;
435
+ notNull: true;
436
+ hasDefault: false;
437
+ isPrimaryKey: false;
438
+ isAutoincrement: false;
439
+ hasRuntimeDefault: false;
440
+ enumValues: [string, ...string[]];
441
+ baseColumn: never;
442
+ identity: undefined;
443
+ generated: undefined;
444
+ }, {}, {}>;
445
+ description: import("drizzle-orm/pg-core").PgColumn<{
446
+ name: "description";
447
+ tableName: "product_category_translations";
448
+ dataType: "string";
449
+ columnType: "PgText";
450
+ data: string;
451
+ driverParam: string;
452
+ notNull: false;
453
+ hasDefault: false;
454
+ isPrimaryKey: false;
455
+ isAutoincrement: false;
456
+ hasRuntimeDefault: false;
457
+ enumValues: [string, ...string[]];
458
+ baseColumn: never;
459
+ identity: undefined;
460
+ generated: undefined;
461
+ }, {}, {}>;
462
+ seoTitle: import("drizzle-orm/pg-core").PgColumn<{
463
+ name: "seo_title";
464
+ tableName: "product_category_translations";
465
+ dataType: "string";
466
+ columnType: "PgText";
467
+ data: string;
468
+ driverParam: string;
469
+ notNull: false;
470
+ hasDefault: false;
471
+ isPrimaryKey: false;
472
+ isAutoincrement: false;
473
+ hasRuntimeDefault: false;
474
+ enumValues: [string, ...string[]];
475
+ baseColumn: never;
476
+ identity: undefined;
477
+ generated: undefined;
478
+ }, {}, {}>;
479
+ seoDescription: import("drizzle-orm/pg-core").PgColumn<{
480
+ name: "seo_description";
481
+ tableName: "product_category_translations";
482
+ dataType: "string";
483
+ columnType: "PgText";
484
+ data: string;
485
+ driverParam: string;
486
+ notNull: false;
487
+ hasDefault: false;
488
+ isPrimaryKey: false;
489
+ isAutoincrement: false;
490
+ hasRuntimeDefault: false;
491
+ enumValues: [string, ...string[]];
492
+ baseColumn: never;
493
+ identity: undefined;
494
+ generated: undefined;
495
+ }, {}, {}>;
496
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
497
+ name: "created_at";
498
+ tableName: "product_category_translations";
499
+ dataType: "date";
500
+ columnType: "PgTimestamp";
501
+ data: Date;
502
+ driverParam: string;
503
+ notNull: true;
504
+ hasDefault: true;
505
+ isPrimaryKey: false;
506
+ isAutoincrement: false;
507
+ hasRuntimeDefault: false;
508
+ enumValues: undefined;
509
+ baseColumn: never;
510
+ identity: undefined;
511
+ generated: undefined;
512
+ }, {}, {}>;
513
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
514
+ name: "updated_at";
515
+ tableName: "product_category_translations";
516
+ dataType: "date";
517
+ columnType: "PgTimestamp";
518
+ data: Date;
519
+ driverParam: string;
520
+ notNull: true;
521
+ hasDefault: true;
522
+ isPrimaryKey: false;
523
+ isAutoincrement: false;
524
+ hasRuntimeDefault: false;
525
+ enumValues: undefined;
526
+ baseColumn: never;
527
+ identity: undefined;
528
+ generated: undefined;
529
+ }, {}, {}>;
530
+ };
531
+ dialect: "pg";
532
+ }>;
533
+ export type ProductCategoryTranslation = typeof productCategoryTranslations.$inferSelect;
534
+ export type NewProductCategoryTranslation = typeof productCategoryTranslations.$inferInsert;
363
535
  export declare const productTags: import("drizzle-orm/pg-core").PgTableWithColumns<{
364
536
  name: "product_tags";
365
537
  schema: undefined;
@@ -437,6 +609,121 @@ export declare const productTags: import("drizzle-orm/pg-core").PgTableWithColum
437
609
  }>;
438
610
  export type ProductTag = typeof productTags.$inferSelect;
439
611
  export type NewProductTag = typeof productTags.$inferInsert;
612
+ /**
613
+ * Locale-aware tag labels. Slimmer than category translations — tags are
614
+ * short labels with no description / SEO blurbs (per #502 non-goals).
615
+ */
616
+ export declare const productTagTranslations: import("drizzle-orm/pg-core").PgTableWithColumns<{
617
+ name: "product_tag_translations";
618
+ schema: undefined;
619
+ columns: {
620
+ id: import("drizzle-orm/pg-core").PgColumn<{
621
+ name: string;
622
+ tableName: "product_tag_translations";
623
+ dataType: "string";
624
+ columnType: "PgText";
625
+ data: string;
626
+ driverParam: string;
627
+ notNull: true;
628
+ hasDefault: true;
629
+ isPrimaryKey: true;
630
+ isAutoincrement: false;
631
+ hasRuntimeDefault: true;
632
+ enumValues: [string, ...string[]];
633
+ baseColumn: never;
634
+ identity: undefined;
635
+ generated: undefined;
636
+ }, {}, {}>;
637
+ tagId: import("drizzle-orm/pg-core").PgColumn<{
638
+ name: string;
639
+ tableName: "product_tag_translations";
640
+ dataType: "string";
641
+ columnType: "PgText";
642
+ data: string;
643
+ driverParam: string;
644
+ notNull: true;
645
+ hasDefault: false;
646
+ isPrimaryKey: false;
647
+ isAutoincrement: false;
648
+ hasRuntimeDefault: false;
649
+ enumValues: [string, ...string[]];
650
+ baseColumn: never;
651
+ identity: undefined;
652
+ generated: undefined;
653
+ }, {}, {}>;
654
+ languageTag: import("drizzle-orm/pg-core").PgColumn<{
655
+ name: "language_tag";
656
+ tableName: "product_tag_translations";
657
+ dataType: "string";
658
+ columnType: "PgText";
659
+ data: string;
660
+ driverParam: string;
661
+ notNull: true;
662
+ hasDefault: false;
663
+ isPrimaryKey: false;
664
+ isAutoincrement: false;
665
+ hasRuntimeDefault: false;
666
+ enumValues: [string, ...string[]];
667
+ baseColumn: never;
668
+ identity: undefined;
669
+ generated: undefined;
670
+ }, {}, {}>;
671
+ name: import("drizzle-orm/pg-core").PgColumn<{
672
+ name: "name";
673
+ tableName: "product_tag_translations";
674
+ dataType: "string";
675
+ columnType: "PgText";
676
+ data: string;
677
+ driverParam: string;
678
+ notNull: true;
679
+ hasDefault: false;
680
+ isPrimaryKey: false;
681
+ isAutoincrement: false;
682
+ hasRuntimeDefault: false;
683
+ enumValues: [string, ...string[]];
684
+ baseColumn: never;
685
+ identity: undefined;
686
+ generated: undefined;
687
+ }, {}, {}>;
688
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
689
+ name: "created_at";
690
+ tableName: "product_tag_translations";
691
+ dataType: "date";
692
+ columnType: "PgTimestamp";
693
+ data: Date;
694
+ driverParam: string;
695
+ notNull: true;
696
+ hasDefault: true;
697
+ isPrimaryKey: false;
698
+ isAutoincrement: false;
699
+ hasRuntimeDefault: false;
700
+ enumValues: undefined;
701
+ baseColumn: never;
702
+ identity: undefined;
703
+ generated: undefined;
704
+ }, {}, {}>;
705
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
706
+ name: "updated_at";
707
+ tableName: "product_tag_translations";
708
+ dataType: "date";
709
+ columnType: "PgTimestamp";
710
+ data: Date;
711
+ driverParam: string;
712
+ notNull: true;
713
+ hasDefault: true;
714
+ isPrimaryKey: false;
715
+ isAutoincrement: false;
716
+ hasRuntimeDefault: false;
717
+ enumValues: undefined;
718
+ baseColumn: never;
719
+ identity: undefined;
720
+ generated: undefined;
721
+ }, {}, {}>;
722
+ };
723
+ dialect: "pg";
724
+ }>;
725
+ export type ProductTagTranslation = typeof productTagTranslations.$inferSelect;
726
+ export type NewProductTagTranslation = typeof productTagTranslations.$inferInsert;
440
727
  export declare const destinations: import("drizzle-orm/pg-core").PgTableWithColumns<{
441
728
  name: "destinations";
442
729
  schema: undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"schema-taxonomy.d.ts","sourceRoot":"","sources":["../src/schema-taxonomy.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBxB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,YAAY,CAAC,YAAY,CAAA;AAC1D,MAAM,MAAM,cAAc,GAAG,OAAO,YAAY,CAAC,YAAY,CAAA;AAE7D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuC7B,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,iBAAiB,CAAC,YAAY,CAAA;AACnE,MAAM,MAAM,kBAAkB,GAAG,OAAO,iBAAiB,CAAC,YAAY,CAAA;AAEtE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASvB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,OAAO,WAAW,CAAC,YAAY,CAAA;AACxD,MAAM,MAAM,aAAa,GAAG,OAAO,WAAW,CAAC,YAAY,CAAA;AAE3D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBxB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,YAAY,CAAC,YAAY,CAAA;AAC1D,MAAM,MAAM,cAAc,GAAG,OAAO,YAAY,CAAC,YAAY,CAAA;AAE7D,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBnC,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG,OAAO,uBAAuB,CAAC,YAAY,CAAA;AAChF,MAAM,MAAM,yBAAyB,GAAG,OAAO,uBAAuB,CAAC,YAAY,CAAA;AAEnF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkBnC,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe9B,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB/B,CAAA"}
1
+ {"version":3,"file":"schema-taxonomy.d.ts","sourceRoot":"","sources":["../src/schema-taxonomy.ts"],"names":[],"mappings":"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"}
@@ -49,12 +49,59 @@ export const productCategories = pgTable("product_categories", {
49
49
  index("idx_product_categories_active_sort_name").on(table.active, table.sortOrder, table.name),
50
50
  index("idx_product_categories_parent_sort_name").on(table.parentId, table.sortOrder, table.name),
51
51
  ]);
52
+ /**
53
+ * Locale-aware category labels. Mirrors `destinationTranslations`.
54
+ *
55
+ * The catalog plane's taxonomy projection (`catalog-policy-taxonomy.ts` +
56
+ * `service-catalog-plane-taxonomy.ts`) reads this table per-slice locale
57
+ * and falls back to `productCategories.name` when no row exists for a
58
+ * given `(categoryId, languageTag)`. Slug stays single-locale on
59
+ * `productCategories.slug` per #502 non-goals — operators want stable
60
+ * URLs that don't shift when translations are edited.
61
+ */
62
+ export const productCategoryTranslations = pgTable("product_category_translations", {
63
+ id: typeId("product_category_translations"),
64
+ categoryId: typeIdRef("category_id")
65
+ .notNull()
66
+ .references(() => productCategories.id, { onDelete: "cascade" }),
67
+ languageTag: text("language_tag").notNull(),
68
+ name: text("name").notNull(),
69
+ description: text("description"),
70
+ seoTitle: text("seo_title"),
71
+ seoDescription: text("seo_description"),
72
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
73
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
74
+ }, (table) => [
75
+ uniqueIndex("uidx_product_category_translations_locale").on(table.categoryId, table.languageTag),
76
+ index("idx_product_category_translations_language").on(table.languageTag),
77
+ index("idx_product_category_translations_category_language_created").on(table.categoryId, table.languageTag, table.createdAt),
78
+ index("idx_product_category_translations_language_created").on(table.languageTag, table.createdAt),
79
+ ]);
52
80
  export const productTags = pgTable("product_tags", {
53
81
  id: typeId("product_tags"),
54
82
  name: text("name").notNull(),
55
83
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
56
84
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
57
85
  }, (table) => [uniqueIndex("uidx_product_tags_name").on(table.name)]);
86
+ /**
87
+ * Locale-aware tag labels. Slimmer than category translations — tags are
88
+ * short labels with no description / SEO blurbs (per #502 non-goals).
89
+ */
90
+ export const productTagTranslations = pgTable("product_tag_translations", {
91
+ id: typeId("product_tag_translations"),
92
+ tagId: typeIdRef("tag_id")
93
+ .notNull()
94
+ .references(() => productTags.id, { onDelete: "cascade" }),
95
+ languageTag: text("language_tag").notNull(),
96
+ name: text("name").notNull(),
97
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
98
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
99
+ }, (table) => [
100
+ uniqueIndex("uidx_product_tag_translations_locale").on(table.tagId, table.languageTag),
101
+ index("idx_product_tag_translations_language").on(table.languageTag),
102
+ index("idx_product_tag_translations_tag_language_created").on(table.tagId, table.languageTag, table.createdAt),
103
+ index("idx_product_tag_translations_language_created").on(table.languageTag, table.createdAt),
104
+ ]);
58
105
  export const destinations = pgTable("destinations", {
59
106
  id: typeId("destinations"),
60
107
  parentId: typeIdRef("parent_id"),
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Projection extension that joins product → destinations and contributes
3
+ * locale-aware destination fields (regions, countries, cities, slugs, ids)
4
+ * onto the product search document.
5
+ *
6
+ * Wire via `createProductDocumentBuilder({ extensions: [destinationsExtension] })`.
7
+ * Requires the registry to include `productDestinationsCatalogPolicy` —
8
+ * otherwise the contributed fields are silently dropped by the indexer's
9
+ * field-policy filter.
10
+ *
11
+ * Locale handling: `destination_translations` is keyed by `(destination_id,
12
+ * language_tag)`. The projection looks up the slice's locale; missing
13
+ * translations fall back to the destination's canonical `slug`. Operators
14
+ * who want the locale lookup to fall back to a different language (e.g.
15
+ * `fr-CA` → `fr` → `en`) should pre-populate the translation row;
16
+ * multi-tier fallback isn't built into the joins.
17
+ *
18
+ * Today's destinations table has no coordinate columns — geopoints come
19
+ * from `product_locations` and will ship as a separate projection
20
+ * extension once that policy lands.
21
+ */
22
+ import type { ProductProjectionExtension } from "./service-catalog-plane.js";
23
+ /**
24
+ * Construct the destinations projection extension.
25
+ *
26
+ * Returns a `ProductProjectionExtension` ready to pass to
27
+ * `createProductDocumentBuilder`.
28
+ */
29
+ export declare function createProductDestinationsProjectionExtension(): ProductProjectionExtension;
30
+ //# sourceMappingURL=service-catalog-plane-destinations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-catalog-plane-destinations.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane-destinations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAgG5E;;;;;GAKG;AACH,wBAAgB,4CAA4C,IAAI,0BAA0B,CAyBzF"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Projection extension that joins product → destinations and contributes
3
+ * locale-aware destination fields (regions, countries, cities, slugs, ids)
4
+ * onto the product search document.
5
+ *
6
+ * Wire via `createProductDocumentBuilder({ extensions: [destinationsExtension] })`.
7
+ * Requires the registry to include `productDestinationsCatalogPolicy` —
8
+ * otherwise the contributed fields are silently dropped by the indexer's
9
+ * field-policy filter.
10
+ *
11
+ * Locale handling: `destination_translations` is keyed by `(destination_id,
12
+ * language_tag)`. The projection looks up the slice's locale; missing
13
+ * translations fall back to the destination's canonical `slug`. Operators
14
+ * who want the locale lookup to fall back to a different language (e.g.
15
+ * `fr-CA` → `fr` → `en`) should pre-populate the translation row;
16
+ * multi-tier fallback isn't built into the joins.
17
+ *
18
+ * Today's destinations table has no coordinate columns — geopoints come
19
+ * from `product_locations` and will ship as a separate projection
20
+ * extension once that policy lands.
21
+ */
22
+ import { and, eq, inArray } from "drizzle-orm";
23
+ import { destinations, destinationTranslations, productDestinations } from "./schema-taxonomy.js";
24
+ async function joinProductDestinations(db, productId, languageTag) {
25
+ // Sub-query the linked destination ids first so the locale lookup is
26
+ // single-shot. Drizzle's join builder doesn't compose left joins on
27
+ // composite predicates cleanly across all dialects, so two queries +
28
+ // an in-memory merge keeps the contract obvious.
29
+ const links = await db
30
+ .select({
31
+ destinationId: productDestinations.destinationId,
32
+ destinationType: destinations.destinationType,
33
+ slug: destinations.slug,
34
+ active: destinations.active,
35
+ })
36
+ .from(productDestinations)
37
+ .innerJoin(destinations, eq(productDestinations.destinationId, destinations.id))
38
+ .where(eq(productDestinations.productId, productId));
39
+ const activeLinks = links.filter((row) => row.active);
40
+ if (activeLinks.length === 0)
41
+ return [];
42
+ const destinationIds = activeLinks.map((row) => row.destinationId);
43
+ const translations = await db
44
+ .select({
45
+ destinationId: destinationTranslations.destinationId,
46
+ name: destinationTranslations.name,
47
+ })
48
+ .from(destinationTranslations)
49
+ .where(and(inArray(destinationTranslations.destinationId, destinationIds), eq(destinationTranslations.languageTag, languageTag)));
50
+ const nameByDestinationId = new Map();
51
+ for (const row of translations) {
52
+ nameByDestinationId.set(row.destinationId, row.name);
53
+ }
54
+ return activeLinks.map((row) => ({
55
+ destinationId: row.destinationId,
56
+ destinationType: row.destinationType,
57
+ slug: row.slug,
58
+ translatedName: nameByDestinationId.get(row.destinationId) ?? null,
59
+ }));
60
+ }
61
+ /** Bucket destinations by `destinationType` and return locale-aware names. */
62
+ function bucketByType(rows) {
63
+ const regions = [];
64
+ const countries = [];
65
+ const cities = [];
66
+ const slugs = [];
67
+ const ids = [];
68
+ for (const row of rows) {
69
+ const label = row.translatedName ?? row.slug;
70
+ switch (row.destinationType) {
71
+ case "region":
72
+ regions.push(label);
73
+ break;
74
+ case "country":
75
+ countries.push(label);
76
+ break;
77
+ case "city":
78
+ cities.push(label);
79
+ break;
80
+ // "destination" (the catch-all default) doesn't bucket into any of
81
+ // the typed lists. Its slug + id still land in the doc so storefronts
82
+ // can filter generically.
83
+ }
84
+ slugs.push(row.slug);
85
+ ids.push(row.destinationId);
86
+ }
87
+ return { regions, countries, cities, slugs, ids };
88
+ }
89
+ /**
90
+ * Construct the destinations projection extension.
91
+ *
92
+ * Returns a `ProductProjectionExtension` ready to pass to
93
+ * `createProductDocumentBuilder`.
94
+ */
95
+ export function createProductDestinationsProjectionExtension() {
96
+ return {
97
+ name: "products:destinations",
98
+ async project(db, productId, slice) {
99
+ const rows = await joinProductDestinations(db, productId, slice.locale);
100
+ if (rows.length === 0) {
101
+ return new Map([
102
+ ["regions[]", []],
103
+ ["countries[]", []],
104
+ ["cities[]", []],
105
+ ["destinationSlugs[]", []],
106
+ ["destinationIds[]", []],
107
+ ]);
108
+ }
109
+ const { regions, countries, cities, slugs, ids } = bucketByType(rows);
110
+ return new Map([
111
+ ["regions[]", regions],
112
+ ["countries[]", countries],
113
+ ["cities[]", cities],
114
+ ["destinationSlugs[]", slugs],
115
+ ["destinationIds[]", ids],
116
+ ]);
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,73 @@
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 type { ProductProjectionExtension } from "./service-catalog-plane.js";
30
+ interface CategoryRow {
31
+ id: string;
32
+ parentId: string | null;
33
+ name: string;
34
+ slug: string;
35
+ active: boolean;
36
+ }
37
+ interface DirectCategoryLink {
38
+ categoryId: string;
39
+ sortOrder: number;
40
+ }
41
+ interface TagRow {
42
+ id: string;
43
+ name: string;
44
+ }
45
+ interface TaxonomyProjection {
46
+ categoryIds: string[];
47
+ categoryNames: string[];
48
+ categorySlugs: string[];
49
+ primaryCategoryId: string | null;
50
+ primaryCategoryName: string | null;
51
+ primaryCategorySlug: string | null;
52
+ tagIds: string[];
53
+ tagLabels: string[];
54
+ }
55
+ /**
56
+ * Pure aggregation kernel. `categoryNameByLocale` and `tagNameByLocale`
57
+ * carry slice-locale translations; entries override the canonical name on
58
+ * the row. Missing entries fall back to the canonical name. Slug stays
59
+ * canonical regardless — there's no per-locale slug.
60
+ */
61
+ declare function buildTaxonomyProjection(directLinks: ReadonlyArray<DirectCategoryLink>, resolvedCategories: ReadonlyMap<string, CategoryRow>, tags: ReadonlyArray<TagRow>, categoryNameByLocale?: ReadonlyMap<string, string>, tagNameByLocale?: ReadonlyMap<string, string>): TaxonomyProjection;
62
+ /**
63
+ * Construct the taxonomy projection extension.
64
+ *
65
+ * Returns a `ProductProjectionExtension` ready to pass to
66
+ * `createProductDocumentBuilder`.
67
+ */
68
+ export declare function createProductTaxonomyProjectionExtension(): ProductProjectionExtension;
69
+ export declare const __test__: {
70
+ buildTaxonomyProjection: typeof buildTaxonomyProjection;
71
+ };
72
+ export {};
73
+ //# sourceMappingURL=service-catalog-plane-taxonomy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-catalog-plane-taxonomy.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane-taxonomy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAaH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAE5E,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAkED,UAAU,MAAM;IACd,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;CACb;AA8DD,UAAU,kBAAkB;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;GAKG;AACH,iBAAS,uBAAuB,CAC9B,WAAW,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAC9C,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,EACpD,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,oBAAoB,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,EAC7D,eAAe,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,GACvD,kBAAkB,CAmEpB;AAED;;;;;GAKG;AACH,wBAAgB,wCAAwC,IAAI,0BAA0B,CA6CrF;AAGD,eAAO,MAAM,QAAQ;;CAEpB,CAAA"}