@voyant-travel/catalog 0.117.2

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 (243) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +190 -0
  3. package/dist/adapter/booking-forwarding.d.ts +2 -0
  4. package/dist/adapter/booking-forwarding.d.ts.map +1 -0
  5. package/dist/adapter/booking-forwarding.js +1 -0
  6. package/dist/adapter/channel-push-contracts.d.ts +2 -0
  7. package/dist/adapter/channel-push-contracts.d.ts.map +1 -0
  8. package/dist/adapter/channel-push-contracts.js +1 -0
  9. package/dist/adapter/contract.d.ts +2 -0
  10. package/dist/adapter/contract.d.ts.map +1 -0
  11. package/dist/adapter/contract.js +1 -0
  12. package/dist/adapter/contract.test.d.ts +2 -0
  13. package/dist/adapter/contract.test.d.ts.map +1 -0
  14. package/dist/adapter/contract.test.js +390 -0
  15. package/dist/adapter/provider-contracts.d.ts +2 -0
  16. package/dist/adapter/provider-contracts.d.ts.map +1 -0
  17. package/dist/adapter/provider-contracts.js +1 -0
  18. package/dist/adapter/provider-contracts.test.d.ts +2 -0
  19. package/dist/adapter/provider-contracts.test.d.ts.map +1 -0
  20. package/dist/adapter/provider-contracts.test.js +206 -0
  21. package/dist/adapter/schemas.d.ts +2 -0
  22. package/dist/adapter/schemas.d.ts.map +1 -0
  23. package/dist/adapter/schemas.js +1 -0
  24. package/dist/adapter/schemas.test.d.ts +2 -0
  25. package/dist/adapter/schemas.test.d.ts.map +1 -0
  26. package/dist/adapter/schemas.test.js +344 -0
  27. package/dist/booking-engine/book.d.ts +124 -0
  28. package/dist/booking-engine/book.d.ts.map +1 -0
  29. package/dist/booking-engine/book.js +311 -0
  30. package/dist/booking-engine/cancel.d.ts +40 -0
  31. package/dist/booking-engine/cancel.d.ts.map +1 -0
  32. package/dist/booking-engine/cancel.js +56 -0
  33. package/dist/booking-engine/checkout-finalize.d.ts +146 -0
  34. package/dist/booking-engine/checkout-finalize.d.ts.map +1 -0
  35. package/dist/booking-engine/checkout-finalize.js +132 -0
  36. package/dist/booking-engine/contracts.d.ts +9 -0
  37. package/dist/booking-engine/contracts.d.ts.map +1 -0
  38. package/dist/booking-engine/contracts.js +8 -0
  39. package/dist/booking-engine/contracts.test.d.ts +2 -0
  40. package/dist/booking-engine/contracts.test.d.ts.map +1 -0
  41. package/dist/booking-engine/contracts.test.js +116 -0
  42. package/dist/booking-engine/draft-shape.d.ts +10 -0
  43. package/dist/booking-engine/draft-shape.d.ts.map +1 -0
  44. package/dist/booking-engine/draft-shape.js +9 -0
  45. package/dist/booking-engine/draft-shape.test.d.ts +2 -0
  46. package/dist/booking-engine/draft-shape.test.d.ts.map +1 -0
  47. package/dist/booking-engine/draft-shape.test.js +74 -0
  48. package/dist/booking-engine/drafts-schema.d.ts +302 -0
  49. package/dist/booking-engine/drafts-schema.d.ts.map +1 -0
  50. package/dist/booking-engine/drafts-schema.js +53 -0
  51. package/dist/booking-engine/drafts-service.d.ts +41 -0
  52. package/dist/booking-engine/drafts-service.d.ts.map +1 -0
  53. package/dist/booking-engine/drafts-service.js +108 -0
  54. package/dist/booking-engine/errors.d.ts +81 -0
  55. package/dist/booking-engine/errors.d.ts.map +1 -0
  56. package/dist/booking-engine/errors.js +113 -0
  57. package/dist/booking-engine/index.d.ts +36 -0
  58. package/dist/booking-engine/index.d.ts.map +1 -0
  59. package/dist/booking-engine/index.js +34 -0
  60. package/dist/booking-engine/orders.d.ts +41 -0
  61. package/dist/booking-engine/orders.d.ts.map +1 -0
  62. package/dist/booking-engine/orders.js +49 -0
  63. package/dist/booking-engine/owned-handler.d.ts +166 -0
  64. package/dist/booking-engine/owned-handler.d.ts.map +1 -0
  65. package/dist/booking-engine/owned-handler.js +50 -0
  66. package/dist/booking-engine/owned-handler.test.d.ts +2 -0
  67. package/dist/booking-engine/owned-handler.test.d.ts.map +1 -0
  68. package/dist/booking-engine/owned-handler.test.js +63 -0
  69. package/dist/booking-engine/promotions-contract.d.ts +8 -0
  70. package/dist/booking-engine/promotions-contract.d.ts.map +1 -0
  71. package/dist/booking-engine/promotions-contract.js +7 -0
  72. package/dist/booking-engine/quote-enricher.test.d.ts +12 -0
  73. package/dist/booking-engine/quote-enricher.test.d.ts.map +1 -0
  74. package/dist/booking-engine/quote-enricher.test.js +138 -0
  75. package/dist/booking-engine/quote.d.ts +163 -0
  76. package/dist/booking-engine/quote.d.ts.map +1 -0
  77. package/dist/booking-engine/quote.js +259 -0
  78. package/dist/booking-engine/registry.d.ts +85 -0
  79. package/dist/booking-engine/registry.d.ts.map +1 -0
  80. package/dist/booking-engine/registry.js +118 -0
  81. package/dist/booking-engine/registry.test.d.ts +2 -0
  82. package/dist/booking-engine/registry.test.d.ts.map +1 -0
  83. package/dist/booking-engine/registry.test.js +132 -0
  84. package/dist/booking-engine/routes-contracts.d.ts +169 -0
  85. package/dist/booking-engine/routes-contracts.d.ts.map +1 -0
  86. package/dist/booking-engine/routes-contracts.js +63 -0
  87. package/dist/booking-engine/routes.d.ts +7 -0
  88. package/dist/booking-engine/routes.d.ts.map +1 -0
  89. package/dist/booking-engine/routes.js +443 -0
  90. package/dist/booking-engine/routes.test.d.ts +2 -0
  91. package/dist/booking-engine/routes.test.d.ts.map +1 -0
  92. package/dist/booking-engine/routes.test.js +304 -0
  93. package/dist/booking-engine/schema.d.ts +455 -0
  94. package/dist/booking-engine/schema.d.ts.map +1 -0
  95. package/dist/booking-engine/schema.js +75 -0
  96. package/dist/booking-engine/snapshot-content.d.ts +120 -0
  97. package/dist/booking-engine/snapshot-content.d.ts.map +1 -0
  98. package/dist/booking-engine/snapshot-content.js +110 -0
  99. package/dist/booking-engine/snapshot-content.test.d.ts +2 -0
  100. package/dist/booking-engine/snapshot-content.test.d.ts.map +1 -0
  101. package/dist/booking-engine/snapshot-content.test.js +213 -0
  102. package/dist/booking-engine/sync.d.ts +136 -0
  103. package/dist/booking-engine/sync.d.ts.map +1 -0
  104. package/dist/booking-engine/sync.js +177 -0
  105. package/dist/booking-engine/sync.test.d.ts +2 -0
  106. package/dist/booking-engine/sync.test.d.ts.map +1 -0
  107. package/dist/booking-engine/sync.test.js +377 -0
  108. package/dist/contract.d.ts +2 -0
  109. package/dist/contract.d.ts.map +1 -0
  110. package/dist/contract.js +1 -0
  111. package/dist/contract.test.d.ts +2 -0
  112. package/dist/contract.test.d.ts.map +1 -0
  113. package/dist/contract.test.js +107 -0
  114. package/dist/drift/events.d.ts +2 -0
  115. package/dist/drift/events.d.ts.map +1 -0
  116. package/dist/drift/events.js +1 -0
  117. package/dist/drift/events.test.d.ts +2 -0
  118. package/dist/drift/events.test.d.ts.map +1 -0
  119. package/dist/drift/events.test.js +100 -0
  120. package/dist/embeddings/contract.d.ts +85 -0
  121. package/dist/embeddings/contract.d.ts.map +1 -0
  122. package/dist/embeddings/contract.js +42 -0
  123. package/dist/embeddings/contract.test.d.ts +2 -0
  124. package/dist/embeddings/contract.test.d.ts.map +1 -0
  125. package/dist/embeddings/contract.test.js +30 -0
  126. package/dist/embeddings/gemini.d.ts +110 -0
  127. package/dist/embeddings/gemini.d.ts.map +1 -0
  128. package/dist/embeddings/gemini.js +118 -0
  129. package/dist/embeddings/gemini.test.d.ts +2 -0
  130. package/dist/embeddings/gemini.test.d.ts.map +1 -0
  131. package/dist/embeddings/gemini.test.js +132 -0
  132. package/dist/embeddings/model-registry.d.ts +62 -0
  133. package/dist/embeddings/model-registry.d.ts.map +1 -0
  134. package/dist/embeddings/model-registry.js +78 -0
  135. package/dist/embeddings/model-registry.test.d.ts +2 -0
  136. package/dist/embeddings/model-registry.test.d.ts.map +1 -0
  137. package/dist/embeddings/model-registry.test.js +81 -0
  138. package/dist/embeddings/openai.d.ts +81 -0
  139. package/dist/embeddings/openai.d.ts.map +1 -0
  140. package/dist/embeddings/openai.js +123 -0
  141. package/dist/embeddings/openai.test.d.ts +2 -0
  142. package/dist/embeddings/openai.test.d.ts.map +1 -0
  143. package/dist/embeddings/openai.test.js +164 -0
  144. package/dist/events/taxonomy.d.ts +158 -0
  145. package/dist/events/taxonomy.d.ts.map +1 -0
  146. package/dist/events/taxonomy.js +99 -0
  147. package/dist/events/taxonomy.test.d.ts +2 -0
  148. package/dist/events/taxonomy.test.d.ts.map +1 -0
  149. package/dist/events/taxonomy.test.js +48 -0
  150. package/dist/index.d.ts +27 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +39 -0
  153. package/dist/indexer/contract.d.ts +203 -0
  154. package/dist/indexer/contract.d.ts.map +1 -0
  155. package/dist/indexer/contract.js +16 -0
  156. package/dist/indexer/typesense-search-query.d.ts +31 -0
  157. package/dist/indexer/typesense-search-query.d.ts.map +1 -0
  158. package/dist/indexer/typesense-search-query.js +185 -0
  159. package/dist/indexer/typesense.d.ts +105 -0
  160. package/dist/indexer/typesense.d.ts.map +1 -0
  161. package/dist/indexer/typesense.js +394 -0
  162. package/dist/indexer/typesense.test.d.ts +2 -0
  163. package/dist/indexer/typesense.test.d.ts.map +1 -0
  164. package/dist/indexer/typesense.test.js +253 -0
  165. package/dist/overlay/resolver.d.ts +101 -0
  166. package/dist/overlay/resolver.d.ts.map +1 -0
  167. package/dist/overlay/resolver.js +167 -0
  168. package/dist/overlay/resolver.test.d.ts +2 -0
  169. package/dist/overlay/resolver.test.d.ts.map +1 -0
  170. package/dist/overlay/resolver.test.js +179 -0
  171. package/dist/overlay/schema.d.ts +266 -0
  172. package/dist/overlay/schema.d.ts.map +1 -0
  173. package/dist/overlay/schema.js +71 -0
  174. package/dist/provenance.d.ts +2 -0
  175. package/dist/provenance.d.ts.map +1 -0
  176. package/dist/provenance.js +1 -0
  177. package/dist/schema-sourced-entries.d.ts +344 -0
  178. package/dist/schema-sourced-entries.d.ts.map +1 -0
  179. package/dist/schema-sourced-entries.js +75 -0
  180. package/dist/schema.d.ts +21 -0
  181. package/dist/schema.d.ts.map +1 -0
  182. package/dist/schema.js +20 -0
  183. package/dist/search/federate.d.ts +58 -0
  184. package/dist/search/federate.d.ts.map +1 -0
  185. package/dist/search/federate.js +103 -0
  186. package/dist/search/federate.test.d.ts +2 -0
  187. package/dist/search/federate.test.d.ts.map +1 -0
  188. package/dist/search/federate.test.js +146 -0
  189. package/dist/search/rerank.d.ts +77 -0
  190. package/dist/search/rerank.d.ts.map +1 -0
  191. package/dist/search/rerank.js +68 -0
  192. package/dist/search/rerank.test.d.ts +2 -0
  193. package/dist/search/rerank.test.d.ts.map +1 -0
  194. package/dist/search/rerank.test.js +60 -0
  195. package/dist/search/routes.d.ts +144 -0
  196. package/dist/search/routes.d.ts.map +1 -0
  197. package/dist/search/routes.js +288 -0
  198. package/dist/search/routes.test.d.ts +2 -0
  199. package/dist/search/routes.test.d.ts.map +1 -0
  200. package/dist/search/routes.test.js +322 -0
  201. package/dist/search/semantic.d.ts +63 -0
  202. package/dist/search/semantic.d.ts.map +1 -0
  203. package/dist/search/semantic.js +75 -0
  204. package/dist/search/semantic.test.d.ts +2 -0
  205. package/dist/search/semantic.test.d.ts.map +1 -0
  206. package/dist/search/semantic.test.js +143 -0
  207. package/dist/services/build-indexer-document.test.d.ts +2 -0
  208. package/dist/services/build-indexer-document.test.d.ts.map +1 -0
  209. package/dist/services/build-indexer-document.test.js +102 -0
  210. package/dist/services/content-service.d.ts +125 -0
  211. package/dist/services/content-service.d.ts.map +1 -0
  212. package/dist/services/content-service.js +139 -0
  213. package/dist/services/content-service.test.d.ts +2 -0
  214. package/dist/services/content-service.test.d.ts.map +1 -0
  215. package/dist/services/content-service.test.js +322 -0
  216. package/dist/services/indexer-service.d.ts +109 -0
  217. package/dist/services/indexer-service.d.ts.map +1 -0
  218. package/dist/services/indexer-service.js +123 -0
  219. package/dist/services/indexer-service.test.d.ts +2 -0
  220. package/dist/services/indexer-service.test.d.ts.map +1 -0
  221. package/dist/services/indexer-service.test.js +176 -0
  222. package/dist/services/overlay-service.d.ts +108 -0
  223. package/dist/services/overlay-service.d.ts.map +1 -0
  224. package/dist/services/overlay-service.js +211 -0
  225. package/dist/services/overlay-service.test.d.ts +2 -0
  226. package/dist/services/overlay-service.test.d.ts.map +1 -0
  227. package/dist/services/overlay-service.test.js +79 -0
  228. package/dist/services/snapshot-builder.test.d.ts +2 -0
  229. package/dist/services/snapshot-builder.test.d.ts.map +1 -0
  230. package/dist/services/snapshot-builder.test.js +93 -0
  231. package/dist/services/snapshot-service.d.ts +78 -0
  232. package/dist/services/snapshot-service.d.ts.map +1 -0
  233. package/dist/services/snapshot-service.js +165 -0
  234. package/dist/services/sourced-entry-service.d.ts +142 -0
  235. package/dist/services/sourced-entry-service.d.ts.map +1 -0
  236. package/dist/services/sourced-entry-service.js +203 -0
  237. package/dist/services/sourced-entry-service.test.d.ts +10 -0
  238. package/dist/services/sourced-entry-service.test.d.ts.map +1 -0
  239. package/dist/services/sourced-entry-service.test.js +66 -0
  240. package/dist/snapshot/schema.d.ts +362 -0
  241. package/dist/snapshot/schema.d.ts.map +1 -0
  242. package/dist/snapshot/schema.js +102 -0
  243. package/package.json +210 -0
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { rerank } from "./rerank.js";
3
+ const hit = (id, score) => ({
4
+ id,
5
+ score,
6
+ document: { id, fields: {} },
7
+ });
8
+ const baseResults = {
9
+ hits: [hit("a", 100), hit("b", 90), hit("c", 80)],
10
+ total: 3,
11
+ };
12
+ describe("rerank", () => {
13
+ it("resorts head by live price ascending", async () => {
14
+ const livePrice = async () => new Map([
15
+ ["a", { amount: 300, currency: "EUR", source: "live" }],
16
+ ["b", { amount: 100, currency: "EUR", source: "live" }],
17
+ ["c", { amount: 200, currency: "EUR", source: "live" }],
18
+ ]);
19
+ const reranked = await rerank(baseResults, livePrice, { market: "default", currency: "EUR" }, { topN: 3 });
20
+ expect(reranked.hits.map((h) => h.id)).toEqual(["b", "c", "a"]);
21
+ });
22
+ it("places stale fallbacks after live results", async () => {
23
+ const livePrice = async () => new Map([
24
+ ["a", { amount: 200, currency: "EUR", source: "live" }],
25
+ ["b", { amount: 100, currency: "EUR", source: "stale" }],
26
+ ["c", { amount: 50, currency: "EUR", source: "live" }],
27
+ ]);
28
+ const reranked = await rerank(baseResults, livePrice, { market: "default", currency: "EUR" }, { topN: 3 });
29
+ // Live hits sort by price (c=50, a=200), then stale hits (b=100).
30
+ expect(reranked.hits.map((h) => h.id)).toEqual(["c", "a", "b"]);
31
+ });
32
+ it("retains hits with no live price unless dropOnLiveMiss is set", async () => {
33
+ const livePrice = async () => new Map([["a", { amount: 100, currency: "EUR", source: "live" }]]);
34
+ const keepDefault = await rerank(baseResults, livePrice, { market: "default", currency: "EUR" }, { topN: 3 });
35
+ expect(keepDefault.hits.map((h) => h.id)).toContain("b");
36
+ expect(keepDefault.hits.map((h) => h.id)).toContain("c");
37
+ const dropped = await rerank(baseResults, livePrice, { market: "default", currency: "EUR" }, { topN: 3, dropOnLiveMiss: true });
38
+ expect(dropped.hits.map((h) => h.id)).toEqual(["a"]);
39
+ });
40
+ it("returns un-reranked results when the live function throws", async () => {
41
+ const livePrice = async () => {
42
+ throw new Error("upstream timeout");
43
+ };
44
+ const reranked = await rerank(baseResults, livePrice, {
45
+ market: "default",
46
+ currency: "EUR",
47
+ });
48
+ expect(reranked.hits.map((h) => h.id)).toEqual(["a", "b", "c"]);
49
+ });
50
+ it("preserves the indexer-ranked tail beyond topN", async () => {
51
+ const longResults = {
52
+ hits: [hit("a", 100), hit("b", 90), hit("c", 80), hit("d", 70), hit("e", 60)],
53
+ total: 5,
54
+ };
55
+ const livePrice = async (ids) => new Map(ids.map((id) => [id, { amount: 100, currency: "EUR", source: "live" }]));
56
+ const reranked = await rerank(longResults, livePrice, { market: "default", currency: "EUR" }, { topN: 2 });
57
+ // Tail (c, d, e) keeps its original order.
58
+ expect(reranked.hits.slice(2).map((h) => h.id)).toEqual(["c", "d", "e"]);
59
+ });
60
+ });
@@ -0,0 +1,144 @@
1
+ import type { HonoModule } from "@voyant-travel/hono/module";
2
+ import type { Context, Hono as HonoApp } from "hono";
3
+ import { z } from "zod";
4
+ import type { IndexerAdapter, IndexerSlice, SearchFilter, SearchRequest, SearchResults, SearchSortOption } from "../indexer/contract.js";
5
+ declare const searchProjectionSchema: z.ZodEnum<{
6
+ raw: "raw";
7
+ "storefront-card": "storefront-card";
8
+ }>;
9
+ declare const catalogSearchBodySchema: z.ZodObject<{
10
+ vertical: z.ZodOptional<z.ZodString>;
11
+ query: z.ZodOptional<z.ZodString>;
12
+ mode: z.ZodOptional<z.ZodEnum<{
13
+ keyword: "keyword";
14
+ hybrid: "hybrid";
15
+ semantic: "semantic";
16
+ }>>;
17
+ sort: z.ZodOptional<z.ZodEnum<{
18
+ "price-asc": "price-asc";
19
+ relevance: "relevance";
20
+ "price-desc": "price-desc";
21
+ "departure-asc": "departure-asc";
22
+ newest: "newest";
23
+ }>>;
24
+ projection: z.ZodOptional<z.ZodEnum<{
25
+ raw: "raw";
26
+ "storefront-card": "storefront-card";
27
+ }>>;
28
+ filters: z.ZodOptional<z.ZodArray<z.ZodType<SearchFilter, unknown, z.core.$ZodTypeInternals<SearchFilter, unknown>>>>;
29
+ facets: z.ZodOptional<z.ZodArray<z.ZodObject<{
30
+ field: z.ZodString;
31
+ limit: z.ZodOptional<z.ZodNumber>;
32
+ }, z.core.$strip>>>;
33
+ pagination: z.ZodOptional<z.ZodObject<{
34
+ limit: z.ZodOptional<z.ZodNumber>;
35
+ cursor: z.ZodOptional<z.ZodString>;
36
+ }, z.core.$strip>>;
37
+ alpha: z.ZodOptional<z.ZodNumber>;
38
+ distance_threshold: z.ZodOptional<z.ZodNumber>;
39
+ query_embedding: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
40
+ market: z.ZodOptional<z.ZodString>;
41
+ locale: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>;
43
+ export type CatalogSearchBody = z.infer<typeof catalogSearchBodySchema>;
44
+ export type CatalogSearchProjection = z.infer<typeof searchProjectionSchema>;
45
+ export type CatalogSearchSort = SearchSortOption;
46
+ export interface StorefrontCatalogCard {
47
+ id: string;
48
+ name: string | null;
49
+ slug: string | null;
50
+ primaryCategory: StorefrontCatalogCardTaxon | null;
51
+ media: {
52
+ thumbnailUrl: string | null;
53
+ coverMediaUrl: string | null;
54
+ };
55
+ priceFrom: {
56
+ amountCents: number;
57
+ currency: string | null;
58
+ originalAmountCents: number | null;
59
+ } | null;
60
+ offerBadges: StorefrontCatalogCardOffer[];
61
+ departures: {
62
+ upcomingCount: number | null;
63
+ nextDepartureAt: string | null;
64
+ nextDepartureDate: string | null;
65
+ months: string[];
66
+ dates: string[];
67
+ };
68
+ destinations: {
69
+ regions: string[];
70
+ countries: string[];
71
+ cities: string[];
72
+ ids: string[];
73
+ slugs: string[];
74
+ };
75
+ coordinates: {
76
+ latitude: number;
77
+ longitude: number;
78
+ } | null;
79
+ }
80
+ export interface StorefrontCatalogCardTaxon {
81
+ id: string | null;
82
+ name: string | null;
83
+ slug: string | null;
84
+ }
85
+ export interface StorefrontCatalogCardOffer {
86
+ id: string | null;
87
+ name: string | null;
88
+ discountKind: string | null;
89
+ discountPercent: number | null;
90
+ discountAmountCents: number | null;
91
+ minPax?: number | null;
92
+ }
93
+ export interface CatalogSearchRuntime {
94
+ indexer?: IndexerAdapter;
95
+ /**
96
+ * Template-owned embedding provider. Kept intentionally unknown so
97
+ * deployments can decide whether semantic search is enabled.
98
+ */
99
+ embeddings?: unknown;
100
+ defaultScope: {
101
+ locale: string;
102
+ audience: IndexerSlice["audience"];
103
+ market: string;
104
+ };
105
+ }
106
+ export interface CatalogSearchExecuteInput {
107
+ c: Context;
108
+ adapter: IndexerAdapter;
109
+ embeddings?: unknown;
110
+ slice: IndexerSlice;
111
+ request: SearchRequest;
112
+ }
113
+ export interface CatalogSearchFallbackInput extends CatalogSearchExecuteInput {
114
+ error: unknown;
115
+ }
116
+ export interface CatalogSearchRoutesOptions {
117
+ resolveRuntime(c: Context): CatalogSearchRuntime;
118
+ /**
119
+ * Optional semantic/hybrid executor. Templates that use
120
+ * `@voyant-travel/catalog/search/semantic` should pass `executeSemanticSearch` here.
121
+ * Without this hook, the route delegates directly to `adapter.search`.
122
+ */
123
+ executeSearch?(input: CatalogSearchExecuteInput): Promise<SearchResults>;
124
+ /**
125
+ * Retry semantic/hybrid requests as keyword when embedding generation or
126
+ * vector search fails. Defaults to true to preserve the operator starter's
127
+ * "best available search mode" behavior.
128
+ */
129
+ fallbackToKeywordOnSearchError?: boolean | ((input: CatalogSearchFallbackInput) => boolean);
130
+ /** Public route audience. Defaults to the customer projection. */
131
+ publicAudience?: IndexerSlice["audience"];
132
+ /** Admin route audience. Defaults to the runtime default scope audience. */
133
+ adminAudience?: IndexerSlice["audience"];
134
+ indexerUnavailableMessage?: string;
135
+ }
136
+ export type CatalogSearchSurface = "admin" | "public";
137
+ export interface CatalogSearchRoutesWithSurfaceOptions extends CatalogSearchRoutesOptions {
138
+ surface: CatalogSearchSurface;
139
+ }
140
+ export declare function createCatalogSearchRoutes(options: CatalogSearchRoutesWithSurfaceOptions): HonoApp;
141
+ export declare function createCatalogSearchHonoModule(options: CatalogSearchRoutesOptions): HonoModule;
142
+ export declare function mountCatalogSearchRoutes(hono: HonoApp, options: CatalogSearchRoutesOptions): void;
143
+ export {};
144
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/search/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,288 @@
1
+ import { parseJsonBody } from "@voyant-travel/hono";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+ const searchModeSchema = z.enum(["keyword", "semantic", "hybrid"]);
5
+ const searchSortSchema = z.enum(["relevance", "price-asc", "price-desc", "departure-asc", "newest"]);
6
+ const searchProjectionSchema = z.enum(["raw", "storefront-card"]);
7
+ const searchFilterSchema = z.lazy(() => z.union([
8
+ z.object({
9
+ kind: z.literal("eq"),
10
+ field: z.string().min(1),
11
+ value: z.union([z.string(), z.number(), z.boolean()]),
12
+ }),
13
+ z.object({
14
+ kind: z.literal("in"),
15
+ field: z.string().min(1),
16
+ values: z.array(z.union([z.string(), z.number()])),
17
+ }),
18
+ z.object({
19
+ kind: z.literal("range"),
20
+ field: z.string().min(1),
21
+ gte: z.number().optional(),
22
+ lte: z.number().optional(),
23
+ }),
24
+ z.object({
25
+ kind: z.literal("and"),
26
+ clauses: z.array(searchFilterSchema),
27
+ }),
28
+ z.object({
29
+ kind: z.literal("or"),
30
+ clauses: z.array(searchFilterSchema),
31
+ }),
32
+ ]));
33
+ const catalogSearchBodySchema = z.object({
34
+ vertical: z.string().min(1).optional(),
35
+ query: z.string().optional(),
36
+ mode: searchModeSchema.optional(),
37
+ sort: searchSortSchema.optional(),
38
+ projection: searchProjectionSchema.optional(),
39
+ filters: z.array(searchFilterSchema).optional(),
40
+ facets: z
41
+ .array(z.object({ field: z.string().min(1), limit: z.number().int().positive().optional() }))
42
+ .optional(),
43
+ pagination: z
44
+ .object({
45
+ limit: z.number().int().positive().optional(),
46
+ cursor: z.string().optional(),
47
+ })
48
+ .optional(),
49
+ alpha: z.number().optional(),
50
+ distance_threshold: z.number().optional(),
51
+ query_embedding: z.array(z.number()).optional(),
52
+ market: z.string().min(1).optional(),
53
+ locale: z.string().min(1).optional(),
54
+ });
55
+ export function createCatalogSearchRoutes(options) {
56
+ return new Hono().post("/search", async (c) => handleSearch(c, options));
57
+ }
58
+ export function createCatalogSearchHonoModule(options) {
59
+ return {
60
+ module: { name: "catalog" },
61
+ adminRoutes: createCatalogSearchRoutes({ ...options, surface: "admin" }),
62
+ publicRoutes: createCatalogSearchRoutes({ ...options, surface: "public" }),
63
+ };
64
+ }
65
+ export function mountCatalogSearchRoutes(hono, options) {
66
+ hono.route("/v1/admin/catalog", createCatalogSearchRoutes({ ...options, surface: "admin" }));
67
+ hono.route("/v1/public/catalog", createCatalogSearchRoutes({ ...options, surface: "public" }));
68
+ }
69
+ async function handleSearch(c, options) {
70
+ const body = await parseJsonBody(c, catalogSearchBodySchema);
71
+ if (!body.vertical)
72
+ return c.json({ error: "vertical is required" }, 400);
73
+ const runtime = options.resolveRuntime(c);
74
+ const indexer = runtime.indexer;
75
+ if (!indexer) {
76
+ return c.json({
77
+ error: options.indexerUnavailableMessage ??
78
+ "Search indexer is not configured (missing TYPESENSE_HOST)",
79
+ }, 503);
80
+ }
81
+ const requestedMode = body.mode ?? "hybrid";
82
+ const mode = shouldUseEmbeddingMode(requestedMode) && !runtime.embeddings && !body.query_embedding
83
+ ? "keyword"
84
+ : requestedMode;
85
+ const slice = {
86
+ vertical: body.vertical,
87
+ locale: body.locale ?? runtime.defaultScope.locale,
88
+ audience: resolveAudience(options, runtime),
89
+ market: body.market ?? runtime.defaultScope.market,
90
+ };
91
+ const request = buildSearchRequest(body, mode);
92
+ try {
93
+ const { results, responseMode } = await executeWithKeywordFallback(c, options, {
94
+ adapter: indexer,
95
+ embeddings: runtime.embeddings,
96
+ slice,
97
+ request,
98
+ });
99
+ return c.json({
100
+ vertical: body.vertical,
101
+ mode: responseMode,
102
+ total: results.total,
103
+ hits: results.hits,
104
+ cards: body.projection === "storefront-card"
105
+ ? results.hits.map((hit) => projectStorefrontCatalogCard(hit.document))
106
+ : undefined,
107
+ facets: results.facets ?? {},
108
+ });
109
+ }
110
+ catch (err) {
111
+ const message = err instanceof Error ? err.message : String(err);
112
+ return c.json({ error: message }, 500);
113
+ }
114
+ }
115
+ function resolveAudience(options, runtime) {
116
+ if (options.surface === "public")
117
+ return options.publicAudience ?? "customer";
118
+ return options.adminAudience ?? runtime.defaultScope.audience;
119
+ }
120
+ function buildSearchRequest(body, mode) {
121
+ return {
122
+ query: body.query ?? "",
123
+ mode,
124
+ sort: body.sort,
125
+ filters: body.filters,
126
+ facets: body.facets,
127
+ pagination: body.pagination,
128
+ alpha: body.alpha,
129
+ distance_threshold: body.distance_threshold,
130
+ query_embedding: body.query_embedding,
131
+ };
132
+ }
133
+ async function executeWithKeywordFallback(c, options, input) {
134
+ const executeSearch = options.executeSearch ?? defaultExecuteSearch;
135
+ const executeInput = { c, ...input };
136
+ try {
137
+ return {
138
+ results: await executeSearch(executeInput),
139
+ responseMode: input.request.mode,
140
+ };
141
+ }
142
+ catch (error) {
143
+ if (!shouldRetryAsKeyword(options, executeInput, error))
144
+ throw error;
145
+ const keywordRequest = {
146
+ ...input.request,
147
+ mode: "keyword",
148
+ query_embedding: undefined,
149
+ };
150
+ return {
151
+ results: await executeSearch({ ...executeInput, request: keywordRequest }),
152
+ responseMode: "keyword",
153
+ };
154
+ }
155
+ }
156
+ function shouldRetryAsKeyword(options, input, error) {
157
+ if (!shouldUseEmbeddingMode(input.request.mode))
158
+ return false;
159
+ const fallback = options.fallbackToKeywordOnSearchError ?? true;
160
+ if (typeof fallback === "function")
161
+ return fallback({ ...input, error });
162
+ return fallback;
163
+ }
164
+ function shouldUseEmbeddingMode(mode) {
165
+ return mode === "semantic" || mode === "hybrid";
166
+ }
167
+ async function defaultExecuteSearch(input) {
168
+ return input.adapter.search(input.slice, input.request);
169
+ }
170
+ function projectStorefrontCatalogCard(document) {
171
+ const fields = document.fields;
172
+ const priceAmount = numberField(fields, "priceFromAmountCents", "sellAmountCents");
173
+ const latitude = numberField(fields, "latitude");
174
+ const longitude = numberField(fields, "longitude");
175
+ return {
176
+ id: document.id,
177
+ name: stringField(fields, "name", "title"),
178
+ slug: stringField(fields, "slug"),
179
+ primaryCategory: taxonFromFields(fields),
180
+ media: {
181
+ thumbnailUrl: stringField(fields, "thumbnailUrl", "primaryMediaUrl", "coverMediaUrl"),
182
+ coverMediaUrl: stringField(fields, "coverMediaUrl", "primaryMediaUrl", "thumbnailUrl"),
183
+ },
184
+ priceFrom: priceAmount == null
185
+ ? null
186
+ : {
187
+ amountCents: priceAmount,
188
+ currency: stringField(fields, "priceFromCurrency", "sellCurrency"),
189
+ originalAmountCents: numberField(fields, "originalPriceFromAmountCents"),
190
+ },
191
+ offerBadges: offerBadgesFromFields(fields),
192
+ departures: {
193
+ upcomingCount: numberField(fields, "upcomingDepartureCount", "availableDeparturesCount", "availableUnitsTotal"),
194
+ nextDepartureAt: stringField(fields, "nextDepartureAt"),
195
+ nextDepartureDate: stringField(fields, "nextDepartureDate"),
196
+ months: stringArrayField(fields, "departureMonths"),
197
+ dates: stringArrayField(fields, "departureDates"),
198
+ },
199
+ destinations: {
200
+ regions: stringArrayField(fields, "regions"),
201
+ countries: stringArrayField(fields, "countries"),
202
+ cities: stringArrayField(fields, "cities"),
203
+ ids: stringArrayField(fields, "destinationIds"),
204
+ slugs: stringArrayField(fields, "destinationSlugs"),
205
+ },
206
+ coordinates: latitude == null || longitude == null
207
+ ? null
208
+ : {
209
+ latitude,
210
+ longitude,
211
+ },
212
+ };
213
+ }
214
+ function taxonFromFields(fields) {
215
+ const id = stringField(fields, "primaryCategoryId", "categoryIds");
216
+ const name = stringField(fields, "primaryCategoryName", "categories", "categoryNames");
217
+ const slug = stringField(fields, "primaryCategorySlug", "categorySlugs");
218
+ if (!id && !name && !slug)
219
+ return null;
220
+ return { id, name, slug };
221
+ }
222
+ function offerBadgesFromFields(fields) {
223
+ const badges = [];
224
+ if (booleanField(fields, "hasOffer")) {
225
+ badges.push({
226
+ id: stringField(fields, "bestOfferId"),
227
+ name: stringField(fields, "bestOfferName"),
228
+ discountKind: stringField(fields, "bestOfferDiscountKind"),
229
+ discountPercent: numberField(fields, "bestOfferDiscountPercent"),
230
+ discountAmountCents: numberField(fields, "bestOfferDiscountAmountCents"),
231
+ });
232
+ }
233
+ if (booleanField(fields, "hasConditionalOffer")) {
234
+ badges.push({
235
+ id: stringField(fields, "conditionalOfferId"),
236
+ name: stringField(fields, "conditionalOfferName"),
237
+ discountKind: stringField(fields, "conditionalOfferDiscountKind"),
238
+ discountPercent: numberField(fields, "conditionalOfferDiscountPercent"),
239
+ discountAmountCents: numberField(fields, "conditionalOfferDiscountAmountCents"),
240
+ minPax: numberField(fields, "conditionalOfferMinPax"),
241
+ });
242
+ }
243
+ return badges;
244
+ }
245
+ function stringField(fields, ...keys) {
246
+ for (const key of keys) {
247
+ const value = fieldValue(fields, key);
248
+ if (typeof value === "string" && value.length > 0)
249
+ return value;
250
+ if (Array.isArray(value)) {
251
+ const first = value.find((item) => typeof item === "string" && item.length > 0);
252
+ if (typeof first === "string")
253
+ return first;
254
+ }
255
+ }
256
+ return null;
257
+ }
258
+ function stringArrayField(fields, key) {
259
+ const value = fieldValue(fields, key);
260
+ if (!Array.isArray(value))
261
+ return [];
262
+ return value.filter((item) => typeof item === "string" && item.length > 0);
263
+ }
264
+ function numberField(fields, ...keys) {
265
+ for (const key of keys) {
266
+ const value = fieldValue(fields, key);
267
+ if (typeof value === "number" && Number.isFinite(value))
268
+ return value;
269
+ if (typeof value === "string" && value.trim().length > 0) {
270
+ const parsed = Number(value);
271
+ if (Number.isFinite(parsed))
272
+ return parsed;
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+ function booleanField(fields, key) {
278
+ const value = fieldValue(fields, key);
279
+ return value === true || value === "true";
280
+ }
281
+ function fieldValue(fields, key) {
282
+ if (key in fields)
283
+ return fields[key];
284
+ const collectionKey = `${key}[]`;
285
+ if (collectionKey in fields)
286
+ return fields[collectionKey];
287
+ return undefined;
288
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=routes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.test.d.ts","sourceRoot":"","sources":["../../src/search/routes.test.ts"],"names":[],"mappings":""}