@voyantjs/products 0.3.0 → 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 (55) hide show
  1. package/dist/index.d.ts +9 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +10 -1
  4. package/dist/routes-public.d.ts +492 -0
  5. package/dist/routes-public.d.ts.map +1 -0
  6. package/dist/routes-public.js +44 -0
  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 +383 -0
  23. package/dist/service-public.d.ts.map +1 -0
  24. package/dist/service-public.js +365 -0
  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 +643 -0
  47. package/dist/validation-public.d.ts.map +1 -0
  48. package/dist/validation-public.js +167 -0
  49. package/dist/validation-shared.d.ts +11 -0
  50. package/dist/validation-shared.d.ts.map +1 -1
  51. package/dist/validation-shared.js +2 -0
  52. package/dist/validation.d.ts +1 -0
  53. package/dist/validation.d.ts.map +1 -1
  54. package/dist/validation.js +1 -0
  55. package/package.json +14 -4
@@ -0,0 +1,365 @@
1
+ import { and, asc, desc, eq, ilike, inArray, notInArray, or, sql } from "drizzle-orm";
2
+ import { destinations, destinationTranslations, productCategories, productCategoryProducts, productDestinations, productLocations, products, productTagProducts, productTags, productTranslations, productVisibilitySettings, } from "./schema.js";
3
+ import { catalogProductsService } from "./service-catalog.js";
4
+ function impossibleCondition() {
5
+ return sql `1 = 0`;
6
+ }
7
+ function normalizeLanguageTag(value) {
8
+ const normalized = value?.trim().toLowerCase();
9
+ return normalized || null;
10
+ }
11
+ async function listProductIdsForCategory(db, categoryId) {
12
+ const rows = await db
13
+ .select({ productId: productCategoryProducts.productId })
14
+ .from(productCategoryProducts)
15
+ .innerJoin(productCategories, eq(productCategories.id, productCategoryProducts.categoryId))
16
+ .where(and(eq(productCategoryProducts.categoryId, categoryId), eq(productCategories.active, true)));
17
+ return rows.map((row) => row.productId);
18
+ }
19
+ async function listProductIdsForTag(db, tagId) {
20
+ const rows = await db
21
+ .select({ productId: productTagProducts.productId })
22
+ .from(productTagProducts)
23
+ .where(eq(productTagProducts.tagId, tagId));
24
+ return rows.map((row) => row.productId);
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
+ }
41
+ async function listFeaturedProductIds(db) {
42
+ const rows = await db
43
+ .select({ productId: productVisibilitySettings.productId })
44
+ .from(productVisibilitySettings)
45
+ .where(eq(productVisibilitySettings.isFeatured, true));
46
+ return rows.map((row) => row.productId);
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
+ }
76
+ async function hydrateCatalogProducts(db, productRows, options) {
77
+ return catalogProductsService.hydrateProducts(db, productRows, {
78
+ includeContent: options?.includeContent,
79
+ languageTag: options?.languageTag,
80
+ fallbackLanguageTags: options?.languageTag ? [options.languageTag] : [],
81
+ });
82
+ }
83
+ function orderProducts(query) {
84
+ const direction = query.direction === "desc" ? desc : asc;
85
+ switch (query.sort) {
86
+ case "createdAt":
87
+ return direction(products.createdAt);
88
+ case "startDate":
89
+ return direction(products.startDate);
90
+ case "price":
91
+ return direction(products.sellAmountCents);
92
+ default:
93
+ return direction(products.name);
94
+ }
95
+ }
96
+ export const publicProductsService = {
97
+ async listCatalogProducts(db, query) {
98
+ const conditions = [
99
+ eq(products.status, "active"),
100
+ eq(products.activated, true),
101
+ eq(products.visibility, "public"),
102
+ ];
103
+ if (query.search) {
104
+ const term = `%${query.search}%`;
105
+ const searchCondition = or(ilike(products.name, term), ilike(products.description, term));
106
+ if (searchCondition) {
107
+ conditions.push(searchCondition);
108
+ }
109
+ }
110
+ if (query.bookingMode) {
111
+ conditions.push(eq(products.bookingMode, query.bookingMode));
112
+ }
113
+ if (query.capacityMode) {
114
+ conditions.push(eq(products.capacityMode, query.capacityMode));
115
+ }
116
+ if (query.productTypeId) {
117
+ conditions.push(eq(products.productTypeId, query.productTypeId));
118
+ }
119
+ if (query.categoryId) {
120
+ const productIds = await listProductIdsForCategory(db, query.categoryId);
121
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
122
+ }
123
+ if (query.tagId) {
124
+ const productIds = await listProductIdsForTag(db, query.tagId);
125
+ conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
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
+ }
135
+ if (query.featured !== undefined) {
136
+ const productIds = await listFeaturedProductIds(db);
137
+ conditions.push(query.featured
138
+ ? productIds.length > 0
139
+ ? inArray(products.id, productIds)
140
+ : impossibleCondition()
141
+ : productIds.length > 0
142
+ ? notInArray(products.id, productIds)
143
+ : sql `1 = 1`);
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
+ }
161
+ const where = and(...conditions);
162
+ const [rows, countResult] = await Promise.all([
163
+ db
164
+ .select()
165
+ .from(products)
166
+ .where(where)
167
+ .orderBy(orderProducts(query), asc(products.id))
168
+ .limit(query.limit)
169
+ .offset(query.offset),
170
+ db.select({ count: sql `count(*)::int` }).from(products).where(where),
171
+ ]);
172
+ return {
173
+ data: await hydrateCatalogProducts(db, rows, {
174
+ languageTag: normalizeLanguageTag(query.languageTag),
175
+ }),
176
+ total: countResult[0]?.count ?? 0,
177
+ limit: query.limit,
178
+ offset: query.offset,
179
+ };
180
+ },
181
+ async getCatalogProductById(db, id, query = {}) {
182
+ const [row] = await db
183
+ .select()
184
+ .from(products)
185
+ .where(and(eq(products.id, id), eq(products.status, "active"), eq(products.activated, true), eq(products.visibility, "public")))
186
+ .limit(1);
187
+ if (!row) {
188
+ return null;
189
+ }
190
+ const [product] = await hydrateCatalogProducts(db, [row], {
191
+ includeContent: true,
192
+ languageTag: normalizeLanguageTag(query.languageTag),
193
+ });
194
+ return product ?? null;
195
+ },
196
+ async getCatalogProductBySlug(db, slug, query = {}) {
197
+ const normalizedSlug = slug.trim().toLowerCase();
198
+ const normalizedLanguageTag = normalizeLanguageTag(query.languageTag);
199
+ const conditions = [
200
+ sql `lower(${productTranslations.slug}) = ${normalizedSlug}`,
201
+ eq(products.status, "active"),
202
+ eq(products.activated, true),
203
+ eq(products.visibility, "public"),
204
+ ];
205
+ if (normalizedLanguageTag) {
206
+ conditions.push(eq(productTranslations.languageTag, normalizedLanguageTag));
207
+ }
208
+ const [row] = await db
209
+ .select({
210
+ productId: products.id,
211
+ languageTag: productTranslations.languageTag,
212
+ })
213
+ .from(productTranslations)
214
+ .innerJoin(products, eq(products.id, productTranslations.productId))
215
+ .where(and(...conditions))
216
+ .orderBy(desc(productTranslations.updatedAt))
217
+ .limit(1);
218
+ if (!row) {
219
+ return null;
220
+ }
221
+ return this.getCatalogProductById(db, row.productId, {
222
+ languageTag: normalizedLanguageTag ?? row.languageTag,
223
+ });
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
+ },
229
+ async listCatalogCategories(db, query) {
230
+ const conditions = [eq(productCategories.active, true)];
231
+ if (query.parentId) {
232
+ conditions.push(eq(productCategories.parentId, query.parentId));
233
+ }
234
+ if (query.search) {
235
+ const term = `%${query.search}%`;
236
+ const searchCondition = or(ilike(productCategories.name, term), ilike(productCategories.slug, term));
237
+ if (searchCondition) {
238
+ conditions.push(searchCondition);
239
+ }
240
+ }
241
+ const where = and(...conditions);
242
+ const [rows, countResult] = await Promise.all([
243
+ db
244
+ .select({
245
+ id: productCategories.id,
246
+ parentId: productCategories.parentId,
247
+ name: productCategories.name,
248
+ slug: productCategories.slug,
249
+ description: productCategories.description,
250
+ sortOrder: productCategories.sortOrder,
251
+ })
252
+ .from(productCategories)
253
+ .where(where)
254
+ .orderBy(asc(productCategories.sortOrder), asc(productCategories.name))
255
+ .limit(query.limit)
256
+ .offset(query.offset),
257
+ db.select({ count: sql `count(*)::int` }).from(productCategories).where(where),
258
+ ]);
259
+ return {
260
+ data: rows.map((row) => ({
261
+ ...row,
262
+ parentId: row.parentId ?? null,
263
+ description: row.description ?? null,
264
+ })),
265
+ total: countResult[0]?.count ?? 0,
266
+ limit: query.limit,
267
+ offset: query.offset,
268
+ };
269
+ },
270
+ async listCatalogTags(db, query) {
271
+ const conditions = [];
272
+ if (query.search) {
273
+ conditions.push(ilike(productTags.name, `%${query.search}%`));
274
+ }
275
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
276
+ const [rows, countResult] = await Promise.all([
277
+ db
278
+ .select({
279
+ id: productTags.id,
280
+ name: productTags.name,
281
+ })
282
+ .from(productTags)
283
+ .where(where)
284
+ .orderBy(asc(productTags.name))
285
+ .limit(query.limit)
286
+ .offset(query.offset),
287
+ db.select({ count: sql `count(*)::int` }).from(productTags).where(where),
288
+ ]);
289
+ return {
290
+ data: rows,
291
+ total: countResult[0]?.count ?? 0,
292
+ limit: query.limit,
293
+ offset: query.offset,
294
+ };
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
+ },
365
+ };