@voyantjs/products 0.3.1 → 0.4.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 (52) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -1
  4. package/dist/routes-public.d.ts +167 -12
  5. package/dist/routes-public.d.ts.map +1 -1
  6. package/dist/routes-public.js +13 -1
  7. package/dist/routes.d.ts +669 -0
  8. package/dist/routes.d.ts.map +1 -1
  9. package/dist/routes.js +117 -1
  10. package/dist/schema-itinerary.d.ts +51 -0
  11. package/dist/schema-itinerary.d.ts.map +1 -1
  12. package/dist/schema-itinerary.js +3 -0
  13. package/dist/schema-relations.d.ts +14 -0
  14. package/dist/schema-relations.d.ts.map +1 -1
  15. package/dist/schema-relations.js +28 -1
  16. package/dist/schema-taxonomy.d.ts +435 -0
  17. package/dist/schema-taxonomy.d.ts.map +1 -1
  18. package/dist/schema-taxonomy.js +47 -0
  19. package/dist/service-catalog.d.ts +237 -0
  20. package/dist/service-catalog.d.ts.map +1 -0
  21. package/dist/service-catalog.js +478 -0
  22. package/dist/service-public.d.ts +136 -12
  23. package/dist/service-public.d.ts.map +1 -1
  24. package/dist/service-public.js +146 -260
  25. package/dist/service.d.ts +292 -1
  26. package/dist/service.d.ts.map +1 -1
  27. package/dist/service.js +388 -2
  28. package/dist/tasks/brochure-printers.d.ts +29 -0
  29. package/dist/tasks/brochure-printers.d.ts.map +1 -0
  30. package/dist/tasks/brochure-printers.js +94 -0
  31. package/dist/tasks/brochure-templates.d.ts +36 -0
  32. package/dist/tasks/brochure-templates.d.ts.map +1 -0
  33. package/dist/tasks/brochure-templates.js +98 -0
  34. package/dist/tasks/brochures.d.ts +42 -0
  35. package/dist/tasks/brochures.d.ts.map +1 -0
  36. package/dist/tasks/brochures.js +69 -0
  37. package/dist/tasks/index.d.ts +3 -0
  38. package/dist/tasks/index.d.ts.map +1 -1
  39. package/dist/tasks/index.js +3 -0
  40. package/dist/validation-catalog.d.ts +388 -0
  41. package/dist/validation-catalog.d.ts.map +1 -0
  42. package/dist/validation-catalog.js +54 -0
  43. package/dist/validation-content.d.ts +109 -0
  44. package/dist/validation-content.d.ts.map +1 -1
  45. package/dist/validation-content.js +63 -1
  46. package/dist/validation-public.d.ts +208 -19
  47. package/dist/validation-public.d.ts.map +1 -1
  48. package/dist/validation-public.js +39 -2
  49. package/dist/validation-shared.d.ts +6 -0
  50. package/dist/validation-shared.d.ts.map +1 -1
  51. package/dist/validation-shared.js +1 -0
  52. package/package.json +6 -4
@@ -1,5 +1,5 @@
1
1
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
- import type { PublicCatalogCategoryListQuery, PublicCatalogProductListQuery, PublicCatalogProductLookupBySlugQuery, PublicCatalogTagListQuery } from "./validation-public.js";
2
+ import type { PublicCatalogCategoryListQuery, PublicCatalogDestinationListQuery, PublicCatalogProductListQuery, PublicCatalogProductLookupBySlugQuery, PublicCatalogTagListQuery } from "./validation-public.js";
3
3
  export declare const publicProductsService: {
4
4
  listCatalogProducts(db: PostgresJsDatabase, query: PublicCatalogProductListQuery): Promise<{
5
5
  data: ({
@@ -38,6 +38,28 @@ export declare const publicProductsService: {
38
38
  name: string;
39
39
  }[];
40
40
  capabilities: string[];
41
+ destinations: {
42
+ id: string;
43
+ parentId: string | null;
44
+ slug: string;
45
+ name: string;
46
+ description: string | null;
47
+ seoTitle: string | null;
48
+ seoDescription: string | null;
49
+ destinationType: string;
50
+ sortOrder: number;
51
+ }[];
52
+ locations: {
53
+ id: string;
54
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
55
+ title: string;
56
+ address: string | null;
57
+ city: string | null;
58
+ countryCode: string | null;
59
+ latitude: number | null;
60
+ longitude: number | null;
61
+ sortOrder: number;
62
+ }[];
41
63
  coverMedia: {
42
64
  id: string;
43
65
  mediaType: "image" | "video" | "document";
@@ -47,9 +69,25 @@ export declare const publicProductsService: {
47
69
  altText: string | null;
48
70
  sortOrder: number;
49
71
  isCover: boolean;
72
+ isBrochure: boolean;
73
+ isBrochureCurrent: boolean;
74
+ brochureVersion: number | null;
50
75
  } | null;
51
76
  isFeatured: boolean;
52
77
  } | {
78
+ brochure: {
79
+ id: string;
80
+ mediaType: "image" | "video" | "document";
81
+ name: string;
82
+ url: string;
83
+ mimeType: string | null;
84
+ altText: string | null;
85
+ sortOrder: number;
86
+ isCover: boolean;
87
+ isBrochure: boolean;
88
+ isBrochureCurrent: boolean;
89
+ brochureVersion: number | null;
90
+ } | null;
53
91
  media: {
54
92
  id: string;
55
93
  mediaType: "image" | "video" | "document";
@@ -59,6 +97,9 @@ export declare const publicProductsService: {
59
97
  altText: string | null;
60
98
  sortOrder: number;
61
99
  isCover: boolean;
100
+ isBrochure: boolean;
101
+ isBrochureCurrent: boolean;
102
+ brochureVersion: number | null;
62
103
  }[];
63
104
  features: {
64
105
  id: string;
@@ -73,17 +114,6 @@ export declare const publicProductsService: {
73
114
  answer: string;
74
115
  sortOrder: number;
75
116
  }[];
76
- locations: {
77
- id: string;
78
- locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
79
- title: string;
80
- address: string | null;
81
- city: string | null;
82
- countryCode: string | null;
83
- latitude: number | null;
84
- longitude: number | null;
85
- sortOrder: number;
86
- }[];
87
117
  id: string;
88
118
  name: string;
89
119
  description: string | null;
@@ -119,6 +149,28 @@ export declare const publicProductsService: {
119
149
  name: string;
120
150
  }[];
121
151
  capabilities: string[];
152
+ destinations: {
153
+ id: string;
154
+ parentId: string | null;
155
+ slug: string;
156
+ name: string;
157
+ description: string | null;
158
+ seoTitle: string | null;
159
+ seoDescription: string | null;
160
+ destinationType: string;
161
+ sortOrder: number;
162
+ }[];
163
+ locations: {
164
+ id: string;
165
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
166
+ title: string;
167
+ address: string | null;
168
+ city: string | null;
169
+ countryCode: string | null;
170
+ latitude: number | null;
171
+ longitude: number | null;
172
+ sortOrder: number;
173
+ }[];
122
174
  coverMedia: {
123
175
  id: string;
124
176
  mediaType: "image" | "video" | "document";
@@ -128,6 +180,9 @@ export declare const publicProductsService: {
128
180
  altText: string | null;
129
181
  sortOrder: number;
130
182
  isCover: boolean;
183
+ isBrochure: boolean;
184
+ isBrochureCurrent: boolean;
185
+ brochureVersion: number | null;
131
186
  } | null;
132
187
  isFeatured: boolean;
133
188
  })[];
@@ -173,6 +228,28 @@ export declare const publicProductsService: {
173
228
  name: string;
174
229
  }[];
175
230
  capabilities: string[];
231
+ destinations: {
232
+ id: string;
233
+ parentId: string | null;
234
+ slug: string;
235
+ name: string;
236
+ description: string | null;
237
+ seoTitle: string | null;
238
+ seoDescription: string | null;
239
+ destinationType: string;
240
+ sortOrder: number;
241
+ }[];
242
+ locations: {
243
+ id: string;
244
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
245
+ title: string;
246
+ address: string | null;
247
+ city: string | null;
248
+ countryCode: string | null;
249
+ latitude: number | null;
250
+ longitude: number | null;
251
+ sortOrder: number;
252
+ }[];
176
253
  coverMedia: {
177
254
  id: string;
178
255
  mediaType: "image" | "video" | "document";
@@ -182,6 +259,9 @@ export declare const publicProductsService: {
182
259
  altText: string | null;
183
260
  sortOrder: number;
184
261
  isCover: boolean;
262
+ isBrochure: boolean;
263
+ isBrochureCurrent: boolean;
264
+ brochureVersion: number | null;
185
265
  } | null;
186
266
  isFeatured: boolean;
187
267
  } | null>;
@@ -221,6 +301,28 @@ export declare const publicProductsService: {
221
301
  name: string;
222
302
  }[];
223
303
  capabilities: string[];
304
+ destinations: {
305
+ id: string;
306
+ parentId: string | null;
307
+ slug: string;
308
+ name: string;
309
+ description: string | null;
310
+ seoTitle: string | null;
311
+ seoDescription: string | null;
312
+ destinationType: string;
313
+ sortOrder: number;
314
+ }[];
315
+ locations: {
316
+ id: string;
317
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
318
+ title: string;
319
+ address: string | null;
320
+ city: string | null;
321
+ countryCode: string | null;
322
+ latitude: number | null;
323
+ longitude: number | null;
324
+ sortOrder: number;
325
+ }[];
224
326
  coverMedia: {
225
327
  id: string;
226
328
  mediaType: "image" | "video" | "document";
@@ -230,9 +332,15 @@ export declare const publicProductsService: {
230
332
  altText: string | null;
231
333
  sortOrder: number;
232
334
  isCover: boolean;
335
+ isBrochure: boolean;
336
+ isBrochureCurrent: boolean;
337
+ brochureVersion: number | null;
233
338
  } | null;
234
339
  isFeatured: boolean;
235
340
  } | null>;
341
+ getCatalogProductBrochure(db: PostgresJsDatabase, productId: string, query?: {
342
+ languageTag?: string | null;
343
+ }): Promise<unknown>;
236
344
  listCatalogCategories(db: PostgresJsDatabase, query: PublicCatalogCategoryListQuery): Promise<{
237
345
  data: {
238
346
  parentId: string | null;
@@ -255,5 +363,21 @@ export declare const publicProductsService: {
255
363
  limit: number;
256
364
  offset: number;
257
365
  }>;
366
+ listCatalogDestinations(db: PostgresJsDatabase, query: PublicCatalogDestinationListQuery): Promise<{
367
+ data: {
368
+ id: string;
369
+ parentId: string | null;
370
+ slug: string;
371
+ name: string;
372
+ description: string | null;
373
+ seoTitle: string | null;
374
+ seoDescription: string | null;
375
+ destinationType: string;
376
+ sortOrder: number;
377
+ }[];
378
+ total: number;
379
+ limit: number;
380
+ offset: number;
381
+ }>;
258
382
  };
259
383
  //# sourceMappingURL=service-public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAiBjE,OAAO,KAAK,EACV,8BAA8B,EAC9B,6BAA6B,EAC7B,qCAAqC,EACrC,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AAiY/B,eAAO,MAAM,qBAAqB;4BACF,kBAAkB,SAAS,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA8EhF,kBAAkB,MAClB,MAAM,UACH;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA2BlC,kBAAkB,QAChB,MAAM,UACL,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAmCd,kBAAkB,SAAS,8BAA8B;;;;;;;;;;;;;wBAkD/D,kBAAkB,SAAS,yBAAyB;;;;;;;;;CA8B/E,CAAA"}
1
+ {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAgBjE,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,6BAA6B,EAC7B,qCAAqC,EACrC,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AAgI/B,eAAO,MAAM,qBAAqB;4BACF,kBAAkB,SAAS,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA2HhF,kBAAkB,MAClB,MAAM,UACH;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA2BlC,kBAAkB,QAChB,MAAM,UACL,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAoCxC,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;;;;;;;;;;;;;;;;CA8F/F,CAAA"}
@@ -1,14 +1,9 @@
1
1
  import { and, asc, desc, eq, ilike, inArray, notInArray, or, sql } from "drizzle-orm";
2
- import { productCapabilities, productCategories, productCategoryProducts, productFaqs, productFeatures, productLocations, productMedia, products, productTagProducts, productTags, productTranslations, productTypes, productVisibilitySettings, } from "./schema.js";
2
+ import { destinations, destinationTranslations, productCategories, productCategoryProducts, productDestinations, productLocations, products, productTagProducts, productTags, productTranslations, productVisibilitySettings, } from "./schema.js";
3
+ import { catalogProductsService } from "./service-catalog.js";
3
4
  function impossibleCondition() {
4
5
  return sql `1 = 0`;
5
6
  }
6
- function normalizeDate(value) {
7
- if (!value) {
8
- return null;
9
- }
10
- return value instanceof Date ? value.toISOString() : value;
11
- }
12
7
  function normalizeLanguageTag(value) {
13
8
  const normalized = value?.trim().toLowerCase();
14
9
  return normalized || null;
@@ -28,6 +23,21 @@ async function listProductIdsForTag(db, tagId) {
28
23
  .where(eq(productTagProducts.tagId, tagId));
29
24
  return rows.map((row) => row.productId);
30
25
  }
26
+ async function listProductIdsForDestinationId(db, destinationId) {
27
+ const rows = await db
28
+ .select({ productId: productDestinations.productId })
29
+ .from(productDestinations)
30
+ .where(eq(productDestinations.destinationId, destinationId));
31
+ return rows.map((row) => row.productId);
32
+ }
33
+ async function listProductIdsForDestinationSlug(db, slug) {
34
+ const rows = await db
35
+ .select({ productId: productDestinations.productId })
36
+ .from(productDestinations)
37
+ .innerJoin(destinations, eq(destinations.id, productDestinations.destinationId))
38
+ .where(eq(destinations.slug, slug));
39
+ return rows.map((row) => row.productId);
40
+ }
31
41
  async function listFeaturedProductIds(db) {
32
42
  const rows = await db
33
43
  .select({ productId: productVisibilitySettings.productId })
@@ -35,260 +45,39 @@ async function listFeaturedProductIds(db) {
35
45
  .where(eq(productVisibilitySettings.isFeatured, true));
36
46
  return rows.map((row) => row.productId);
37
47
  }
48
+ async function listProductIdsForLocationTitle(db, title) {
49
+ const rows = await db
50
+ .select({ productId: productLocations.productId })
51
+ .from(productLocations)
52
+ .where(ilike(productLocations.title, title));
53
+ return rows.map((row) => row.productId);
54
+ }
55
+ async function listProductIdsForLocationCity(db, city) {
56
+ const rows = await db
57
+ .select({ productId: productLocations.productId })
58
+ .from(productLocations)
59
+ .where(ilike(productLocations.city, city));
60
+ return rows.map((row) => row.productId);
61
+ }
62
+ async function listProductIdsForLocationCountryCode(db, countryCode) {
63
+ const rows = await db
64
+ .select({ productId: productLocations.productId })
65
+ .from(productLocations)
66
+ .where(eq(productLocations.countryCode, countryCode));
67
+ return rows.map((row) => row.productId);
68
+ }
69
+ async function listProductIdsForLocationType(db, locationType) {
70
+ const rows = await db
71
+ .select({ productId: productLocations.productId })
72
+ .from(productLocations)
73
+ .where(eq(productLocations.locationType, locationType));
74
+ return rows.map((row) => row.productId);
75
+ }
38
76
  async function hydrateCatalogProducts(db, productRows, options) {
39
- if (productRows.length === 0) {
40
- return [];
41
- }
42
- const productIds = productRows.map((product) => product.id);
43
- const productTypeIds = Array.from(new Set(productRows
44
- .map((product) => product.productTypeId)
45
- .filter((value) => Boolean(value))));
46
- const [categoryRows, tagRows, translationRows, typeRows, capabilityRows, mediaRows, featuredRows, featureRows, faqRows, locationRows,] = await Promise.all([
47
- db
48
- .select({
49
- productId: productCategoryProducts.productId,
50
- id: productCategories.id,
51
- parentId: productCategories.parentId,
52
- name: productCategories.name,
53
- slug: productCategories.slug,
54
- description: productCategories.description,
55
- sortOrder: productCategories.sortOrder,
56
- })
57
- .from(productCategoryProducts)
58
- .innerJoin(productCategories, eq(productCategories.id, productCategoryProducts.categoryId))
59
- .where(and(inArray(productCategoryProducts.productId, productIds), eq(productCategories.active, true)))
60
- .orderBy(asc(productCategoryProducts.sortOrder), asc(productCategories.sortOrder), asc(productCategories.name)),
61
- db
62
- .select({
63
- productId: productTagProducts.productId,
64
- id: productTags.id,
65
- name: productTags.name,
66
- })
67
- .from(productTagProducts)
68
- .innerJoin(productTags, eq(productTags.id, productTagProducts.tagId))
69
- .where(inArray(productTagProducts.productId, productIds))
70
- .orderBy(asc(productTags.name)),
71
- options?.languageTag
72
- ? db
73
- .select({
74
- productId: productTranslations.productId,
75
- languageTag: productTranslations.languageTag,
76
- slug: productTranslations.slug,
77
- name: productTranslations.name,
78
- shortDescription: productTranslations.shortDescription,
79
- description: productTranslations.description,
80
- seoTitle: productTranslations.seoTitle,
81
- seoDescription: productTranslations.seoDescription,
82
- })
83
- .from(productTranslations)
84
- .where(and(inArray(productTranslations.productId, productIds), eq(productTranslations.languageTag, options.languageTag)))
85
- : Promise.resolve([]),
86
- productTypeIds.length > 0
87
- ? db
88
- .select({
89
- id: productTypes.id,
90
- code: productTypes.code,
91
- name: productTypes.name,
92
- description: productTypes.description,
93
- })
94
- .from(productTypes)
95
- .where(and(inArray(productTypes.id, productTypeIds), eq(productTypes.active, true)))
96
- : Promise.resolve([]),
97
- db
98
- .select({
99
- productId: productCapabilities.productId,
100
- capability: productCapabilities.capability,
101
- })
102
- .from(productCapabilities)
103
- .where(and(inArray(productCapabilities.productId, productIds), eq(productCapabilities.enabled, true)))
104
- .orderBy(asc(productCapabilities.capability)),
105
- db
106
- .select({
107
- productId: productMedia.productId,
108
- id: productMedia.id,
109
- mediaType: productMedia.mediaType,
110
- name: productMedia.name,
111
- url: productMedia.url,
112
- mimeType: productMedia.mimeType,
113
- altText: productMedia.altText,
114
- sortOrder: productMedia.sortOrder,
115
- isCover: productMedia.isCover,
116
- })
117
- .from(productMedia)
118
- .where(inArray(productMedia.productId, productIds))
119
- .orderBy(desc(productMedia.isCover), asc(productMedia.sortOrder), asc(productMedia.createdAt)),
120
- db
121
- .select({ productId: productVisibilitySettings.productId })
122
- .from(productVisibilitySettings)
123
- .where(and(inArray(productVisibilitySettings.productId, productIds), eq(productVisibilitySettings.isFeatured, true))),
124
- options?.includeContent
125
- ? db
126
- .select({
127
- productId: productFeatures.productId,
128
- id: productFeatures.id,
129
- featureType: productFeatures.featureType,
130
- title: productFeatures.title,
131
- description: productFeatures.description,
132
- sortOrder: productFeatures.sortOrder,
133
- })
134
- .from(productFeatures)
135
- .where(inArray(productFeatures.productId, productIds))
136
- .orderBy(asc(productFeatures.sortOrder), asc(productFeatures.createdAt))
137
- : Promise.resolve([]),
138
- options?.includeContent
139
- ? db
140
- .select({
141
- productId: productFaqs.productId,
142
- id: productFaqs.id,
143
- question: productFaqs.question,
144
- answer: productFaqs.answer,
145
- sortOrder: productFaqs.sortOrder,
146
- })
147
- .from(productFaqs)
148
- .where(inArray(productFaqs.productId, productIds))
149
- .orderBy(asc(productFaqs.sortOrder), asc(productFaqs.createdAt))
150
- : Promise.resolve([]),
151
- options?.includeContent
152
- ? db
153
- .select({
154
- productId: productLocations.productId,
155
- id: productLocations.id,
156
- locationType: productLocations.locationType,
157
- title: productLocations.title,
158
- address: productLocations.address,
159
- city: productLocations.city,
160
- countryCode: productLocations.countryCode,
161
- latitude: productLocations.latitude,
162
- longitude: productLocations.longitude,
163
- sortOrder: productLocations.sortOrder,
164
- })
165
- .from(productLocations)
166
- .where(inArray(productLocations.productId, productIds))
167
- .orderBy(asc(productLocations.sortOrder), asc(productLocations.createdAt))
168
- : Promise.resolve([]),
169
- ]);
170
- const categoriesByProduct = new Map();
171
- for (const row of categoryRows) {
172
- const existing = categoriesByProduct.get(row.productId) ?? [];
173
- existing.push(row);
174
- categoriesByProduct.set(row.productId, existing);
175
- }
176
- const tagsByProduct = new Map();
177
- for (const row of tagRows) {
178
- const existing = tagsByProduct.get(row.productId) ?? [];
179
- existing.push(row);
180
- tagsByProduct.set(row.productId, existing);
181
- }
182
- const translationByProduct = new Map(translationRows.map((row) => [row.productId, row]));
183
- const capabilitiesByProduct = new Map();
184
- for (const row of capabilityRows) {
185
- const existing = capabilitiesByProduct.get(row.productId) ?? [];
186
- existing.push(row.capability);
187
- capabilitiesByProduct.set(row.productId, existing);
188
- }
189
- const mediaByProduct = new Map();
190
- for (const row of mediaRows) {
191
- const existing = mediaByProduct.get(row.productId) ?? [];
192
- existing.push(row);
193
- mediaByProduct.set(row.productId, existing);
194
- }
195
- const featuresByProduct = new Map();
196
- for (const row of featureRows) {
197
- const existing = featuresByProduct.get(row.productId) ?? [];
198
- existing.push(row);
199
- featuresByProduct.set(row.productId, existing);
200
- }
201
- const faqsByProduct = new Map();
202
- for (const row of faqRows) {
203
- const existing = faqsByProduct.get(row.productId) ?? [];
204
- existing.push(row);
205
- faqsByProduct.set(row.productId, existing);
206
- }
207
- const locationsByProduct = new Map();
208
- for (const row of locationRows) {
209
- const existing = locationsByProduct.get(row.productId) ?? [];
210
- existing.push(row);
211
- locationsByProduct.set(row.productId, existing);
212
- }
213
- const typeById = new Map(typeRows.map((row) => [row.id, row]));
214
- const featuredIds = new Set(featuredRows.map((row) => row.productId));
215
- return productRows.map((product) => {
216
- const translation = translationByProduct.get(product.id) ?? null;
217
- const media = (mediaByProduct.get(product.id) ?? []).map((row) => ({
218
- id: row.id,
219
- mediaType: row.mediaType,
220
- name: row.name,
221
- url: row.url,
222
- mimeType: row.mimeType ?? null,
223
- altText: row.altText ?? null,
224
- sortOrder: row.sortOrder,
225
- isCover: row.isCover,
226
- }));
227
- const base = {
228
- id: product.id,
229
- name: translation?.name ?? product.name,
230
- description: translation?.description ?? product.description ?? null,
231
- contentLanguageTag: translation?.languageTag ?? null,
232
- slug: translation?.slug ?? null,
233
- shortDescription: translation?.shortDescription ?? null,
234
- seoTitle: translation?.seoTitle ?? null,
235
- seoDescription: translation?.seoDescription ?? null,
236
- bookingMode: product.bookingMode,
237
- capacityMode: product.capacityMode,
238
- visibility: product.visibility,
239
- sellCurrency: product.sellCurrency,
240
- sellAmountCents: product.sellAmountCents ?? null,
241
- startDate: normalizeDate(product.startDate),
242
- endDate: normalizeDate(product.endDate),
243
- pax: product.pax ?? null,
244
- productType: product.productTypeId ? (typeById.get(product.productTypeId) ?? null) : null,
245
- categories: (categoriesByProduct.get(product.id) ?? []).map((row) => ({
246
- id: row.id,
247
- parentId: row.parentId ?? null,
248
- name: row.name,
249
- slug: row.slug,
250
- description: row.description ?? null,
251
- sortOrder: row.sortOrder,
252
- })),
253
- tags: (tagsByProduct.get(product.id) ?? []).map((row) => ({
254
- id: row.id,
255
- name: row.name,
256
- })),
257
- capabilities: capabilitiesByProduct.get(product.id) ?? [],
258
- coverMedia: media.find((item) => item.isCover) ?? media[0] ?? null,
259
- isFeatured: featuredIds.has(product.id),
260
- };
261
- if (!options?.includeContent) {
262
- return base;
263
- }
264
- return {
265
- ...base,
266
- media,
267
- features: (featuresByProduct.get(product.id) ?? []).map((row) => ({
268
- id: row.id,
269
- featureType: row.featureType,
270
- title: row.title,
271
- description: row.description ?? null,
272
- sortOrder: row.sortOrder,
273
- })),
274
- faqs: (faqsByProduct.get(product.id) ?? []).map((row) => ({
275
- id: row.id,
276
- question: row.question,
277
- answer: row.answer,
278
- sortOrder: row.sortOrder,
279
- })),
280
- locations: (locationsByProduct.get(product.id) ?? []).map((row) => ({
281
- id: row.id,
282
- locationType: row.locationType,
283
- title: row.title,
284
- address: row.address ?? null,
285
- city: row.city ?? null,
286
- countryCode: row.countryCode ?? null,
287
- latitude: row.latitude ?? null,
288
- longitude: row.longitude ?? null,
289
- sortOrder: row.sortOrder,
290
- })),
291
- };
77
+ return catalogProductsService.hydrateProducts(db, productRows, {
78
+ includeContent: options?.includeContent,
79
+ languageTag: options?.languageTag,
80
+ fallbackLanguageTags: options?.languageTag ? [options.languageTag] : [],
292
81
  });
293
82
  }
294
83
  function orderProducts(query) {
@@ -335,6 +124,14 @@ export const publicProductsService = {
335
124
  const productIds = await listProductIdsForTag(db, query.tagId);
336
125
  conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
337
126
  }
127
+ if (query.destinationId) {
128
+ const productIds = await listProductIdsForDestinationId(db, query.destinationId);
129
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
130
+ }
131
+ if (query.destinationSlug) {
132
+ const productIds = await listProductIdsForDestinationSlug(db, query.destinationSlug);
133
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
134
+ }
338
135
  if (query.featured !== undefined) {
339
136
  const productIds = await listFeaturedProductIds(db);
340
137
  conditions.push(query.featured
@@ -345,6 +142,22 @@ export const publicProductsService = {
345
142
  ? notInArray(products.id, productIds)
346
143
  : sql `1 = 1`);
347
144
  }
145
+ if (query.locationTitle) {
146
+ const productIds = await listProductIdsForLocationTitle(db, query.locationTitle);
147
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
148
+ }
149
+ if (query.locationCity) {
150
+ const productIds = await listProductIdsForLocationCity(db, query.locationCity);
151
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
152
+ }
153
+ if (query.locationCountryCode) {
154
+ const productIds = await listProductIdsForLocationCountryCode(db, query.locationCountryCode.toUpperCase());
155
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
156
+ }
157
+ if (query.locationType) {
158
+ const productIds = await listProductIdsForLocationType(db, query.locationType);
159
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
160
+ }
348
161
  const where = and(...conditions);
349
162
  const [rows, countResult] = await Promise.all([
350
163
  db
@@ -409,6 +222,10 @@ export const publicProductsService = {
409
222
  languageTag: normalizedLanguageTag ?? row.languageTag,
410
223
  });
411
224
  },
225
+ async getCatalogProductBrochure(db, productId, query = {}) {
226
+ const product = await this.getCatalogProductById(db, productId, query);
227
+ return product && "brochure" in product ? product.brochure : null;
228
+ },
412
229
  async listCatalogCategories(db, query) {
413
230
  const conditions = [eq(productCategories.active, true)];
414
231
  if (query.parentId) {
@@ -476,4 +293,73 @@ export const publicProductsService = {
476
293
  offset: query.offset,
477
294
  };
478
295
  },
296
+ async listCatalogDestinations(db, query) {
297
+ const conditions = [];
298
+ if (query.parentId) {
299
+ conditions.push(eq(destinations.parentId, query.parentId));
300
+ }
301
+ if (query.active !== undefined) {
302
+ conditions.push(eq(destinations.active, query.active));
303
+ }
304
+ if (query.destinationType) {
305
+ conditions.push(eq(destinations.destinationType, query.destinationType));
306
+ }
307
+ if (query.search) {
308
+ const translationRows = await db
309
+ .select({ destinationId: destinationTranslations.destinationId })
310
+ .from(destinationTranslations)
311
+ .where(and(...(query.languageTag
312
+ ? [eq(destinationTranslations.languageTag, query.languageTag)]
313
+ : []), or(ilike(destinationTranslations.name, `%${query.search}%`), ilike(destinationTranslations.description, `%${query.search}%`))));
314
+ const destinationIds = translationRows.map((row) => row.destinationId);
315
+ conditions.push(destinationIds.length > 0
316
+ ? inArray(destinations.id, destinationIds)
317
+ : impossibleCondition());
318
+ }
319
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
320
+ const [rows, countResult] = await Promise.all([
321
+ db
322
+ .select()
323
+ .from(destinations)
324
+ .where(where)
325
+ .limit(query.limit)
326
+ .offset(query.offset)
327
+ .orderBy(asc(destinations.sortOrder), asc(destinations.slug)),
328
+ db.select({ count: sql `count(*)::int` }).from(destinations).where(where),
329
+ ]);
330
+ const destinationIds = rows.map((row) => row.id);
331
+ const translations = destinationIds.length > 0
332
+ ? await db
333
+ .select()
334
+ .from(destinationTranslations)
335
+ .where(and(inArray(destinationTranslations.destinationId, destinationIds), ...(query.languageTag
336
+ ? [eq(destinationTranslations.languageTag, query.languageTag)]
337
+ : [])))
338
+ : [];
339
+ const translationByDestination = new Map();
340
+ for (const row of translations) {
341
+ if (!translationByDestination.has(row.destinationId)) {
342
+ translationByDestination.set(row.destinationId, row);
343
+ }
344
+ }
345
+ return {
346
+ data: rows.map((row) => {
347
+ const translation = translationByDestination.get(row.id);
348
+ return {
349
+ id: row.id,
350
+ parentId: row.parentId ?? null,
351
+ slug: row.slug,
352
+ name: translation?.name ?? row.slug,
353
+ description: translation?.description ?? null,
354
+ seoTitle: translation?.seoTitle ?? null,
355
+ seoDescription: translation?.seoDescription ?? null,
356
+ destinationType: row.destinationType,
357
+ sortOrder: row.sortOrder,
358
+ };
359
+ }),
360
+ total: countResult[0]?.count ?? 0,
361
+ limit: query.limit,
362
+ offset: query.offset,
363
+ };
364
+ },
479
365
  };