@voyantjs/products 0.2.0 → 0.3.1
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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/routes-public.d.ts +337 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +32 -0
- package/dist/routes.d.ts +4 -4
- package/dist/schema-core.d.ts +897 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +77 -0
- package/dist/schema-itinerary.d.ts +828 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +77 -0
- package/dist/schema-relations.d.ts +99 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +155 -0
- package/dist/schema-settings.d.ts +1749 -0
- package/dist/schema-settings.d.ts.map +1 -0
- package/dist/schema-settings.js +175 -0
- package/dist/schema-shared.d.ts +15 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +91 -0
- package/dist/schema-taxonomy.d.ts +573 -0
- package/dist/schema-taxonomy.d.ts.map +1 -0
- package/dist/schema-taxonomy.js +65 -0
- package/dist/schema.d.ts +6 -4155
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -653
- package/dist/service-public.d.ts +259 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +479 -0
- package/dist/service.d.ts +4 -4
- package/dist/validation-config.d.ts +233 -0
- package/dist/validation-config.d.ts.map +1 -0
- package/dist/validation-config.js +73 -0
- package/dist/validation-content.d.ts +358 -0
- package/dist/validation-content.d.ts.map +1 -0
- package/dist/validation-content.js +177 -0
- package/dist/validation-core.d.ts +268 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +91 -0
- package/dist/validation-public.d.ts +454 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +130 -0
- package/dist/validation-shared.d.ts +108 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +87 -0
- package/dist/validation.d.ts +5 -854
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +5 -433
- package/package.json +12 -4
|
@@ -0,0 +1,479 @@
|
|
|
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";
|
|
3
|
+
function impossibleCondition() {
|
|
4
|
+
return sql `1 = 0`;
|
|
5
|
+
}
|
|
6
|
+
function normalizeDate(value) {
|
|
7
|
+
if (!value) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
11
|
+
}
|
|
12
|
+
function normalizeLanguageTag(value) {
|
|
13
|
+
const normalized = value?.trim().toLowerCase();
|
|
14
|
+
return normalized || null;
|
|
15
|
+
}
|
|
16
|
+
async function listProductIdsForCategory(db, categoryId) {
|
|
17
|
+
const rows = await db
|
|
18
|
+
.select({ productId: productCategoryProducts.productId })
|
|
19
|
+
.from(productCategoryProducts)
|
|
20
|
+
.innerJoin(productCategories, eq(productCategories.id, productCategoryProducts.categoryId))
|
|
21
|
+
.where(and(eq(productCategoryProducts.categoryId, categoryId), eq(productCategories.active, true)));
|
|
22
|
+
return rows.map((row) => row.productId);
|
|
23
|
+
}
|
|
24
|
+
async function listProductIdsForTag(db, tagId) {
|
|
25
|
+
const rows = await db
|
|
26
|
+
.select({ productId: productTagProducts.productId })
|
|
27
|
+
.from(productTagProducts)
|
|
28
|
+
.where(eq(productTagProducts.tagId, tagId));
|
|
29
|
+
return rows.map((row) => row.productId);
|
|
30
|
+
}
|
|
31
|
+
async function listFeaturedProductIds(db) {
|
|
32
|
+
const rows = await db
|
|
33
|
+
.select({ productId: productVisibilitySettings.productId })
|
|
34
|
+
.from(productVisibilitySettings)
|
|
35
|
+
.where(eq(productVisibilitySettings.isFeatured, true));
|
|
36
|
+
return rows.map((row) => row.productId);
|
|
37
|
+
}
|
|
38
|
+
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
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function orderProducts(query) {
|
|
295
|
+
const direction = query.direction === "desc" ? desc : asc;
|
|
296
|
+
switch (query.sort) {
|
|
297
|
+
case "createdAt":
|
|
298
|
+
return direction(products.createdAt);
|
|
299
|
+
case "startDate":
|
|
300
|
+
return direction(products.startDate);
|
|
301
|
+
case "price":
|
|
302
|
+
return direction(products.sellAmountCents);
|
|
303
|
+
default:
|
|
304
|
+
return direction(products.name);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
export const publicProductsService = {
|
|
308
|
+
async listCatalogProducts(db, query) {
|
|
309
|
+
const conditions = [
|
|
310
|
+
eq(products.status, "active"),
|
|
311
|
+
eq(products.activated, true),
|
|
312
|
+
eq(products.visibility, "public"),
|
|
313
|
+
];
|
|
314
|
+
if (query.search) {
|
|
315
|
+
const term = `%${query.search}%`;
|
|
316
|
+
const searchCondition = or(ilike(products.name, term), ilike(products.description, term));
|
|
317
|
+
if (searchCondition) {
|
|
318
|
+
conditions.push(searchCondition);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (query.bookingMode) {
|
|
322
|
+
conditions.push(eq(products.bookingMode, query.bookingMode));
|
|
323
|
+
}
|
|
324
|
+
if (query.capacityMode) {
|
|
325
|
+
conditions.push(eq(products.capacityMode, query.capacityMode));
|
|
326
|
+
}
|
|
327
|
+
if (query.productTypeId) {
|
|
328
|
+
conditions.push(eq(products.productTypeId, query.productTypeId));
|
|
329
|
+
}
|
|
330
|
+
if (query.categoryId) {
|
|
331
|
+
const productIds = await listProductIdsForCategory(db, query.categoryId);
|
|
332
|
+
conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
|
|
333
|
+
}
|
|
334
|
+
if (query.tagId) {
|
|
335
|
+
const productIds = await listProductIdsForTag(db, query.tagId);
|
|
336
|
+
conditions.push(productIds.length > 0 ? inArray(products.id, productIds) : impossibleCondition());
|
|
337
|
+
}
|
|
338
|
+
if (query.featured !== undefined) {
|
|
339
|
+
const productIds = await listFeaturedProductIds(db);
|
|
340
|
+
conditions.push(query.featured
|
|
341
|
+
? productIds.length > 0
|
|
342
|
+
? inArray(products.id, productIds)
|
|
343
|
+
: impossibleCondition()
|
|
344
|
+
: productIds.length > 0
|
|
345
|
+
? notInArray(products.id, productIds)
|
|
346
|
+
: sql `1 = 1`);
|
|
347
|
+
}
|
|
348
|
+
const where = and(...conditions);
|
|
349
|
+
const [rows, countResult] = await Promise.all([
|
|
350
|
+
db
|
|
351
|
+
.select()
|
|
352
|
+
.from(products)
|
|
353
|
+
.where(where)
|
|
354
|
+
.orderBy(orderProducts(query), asc(products.id))
|
|
355
|
+
.limit(query.limit)
|
|
356
|
+
.offset(query.offset),
|
|
357
|
+
db.select({ count: sql `count(*)::int` }).from(products).where(where),
|
|
358
|
+
]);
|
|
359
|
+
return {
|
|
360
|
+
data: await hydrateCatalogProducts(db, rows, {
|
|
361
|
+
languageTag: normalizeLanguageTag(query.languageTag),
|
|
362
|
+
}),
|
|
363
|
+
total: countResult[0]?.count ?? 0,
|
|
364
|
+
limit: query.limit,
|
|
365
|
+
offset: query.offset,
|
|
366
|
+
};
|
|
367
|
+
},
|
|
368
|
+
async getCatalogProductById(db, id, query = {}) {
|
|
369
|
+
const [row] = await db
|
|
370
|
+
.select()
|
|
371
|
+
.from(products)
|
|
372
|
+
.where(and(eq(products.id, id), eq(products.status, "active"), eq(products.activated, true), eq(products.visibility, "public")))
|
|
373
|
+
.limit(1);
|
|
374
|
+
if (!row) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const [product] = await hydrateCatalogProducts(db, [row], {
|
|
378
|
+
includeContent: true,
|
|
379
|
+
languageTag: normalizeLanguageTag(query.languageTag),
|
|
380
|
+
});
|
|
381
|
+
return product ?? null;
|
|
382
|
+
},
|
|
383
|
+
async getCatalogProductBySlug(db, slug, query = {}) {
|
|
384
|
+
const normalizedSlug = slug.trim().toLowerCase();
|
|
385
|
+
const normalizedLanguageTag = normalizeLanguageTag(query.languageTag);
|
|
386
|
+
const conditions = [
|
|
387
|
+
sql `lower(${productTranslations.slug}) = ${normalizedSlug}`,
|
|
388
|
+
eq(products.status, "active"),
|
|
389
|
+
eq(products.activated, true),
|
|
390
|
+
eq(products.visibility, "public"),
|
|
391
|
+
];
|
|
392
|
+
if (normalizedLanguageTag) {
|
|
393
|
+
conditions.push(eq(productTranslations.languageTag, normalizedLanguageTag));
|
|
394
|
+
}
|
|
395
|
+
const [row] = await db
|
|
396
|
+
.select({
|
|
397
|
+
productId: products.id,
|
|
398
|
+
languageTag: productTranslations.languageTag,
|
|
399
|
+
})
|
|
400
|
+
.from(productTranslations)
|
|
401
|
+
.innerJoin(products, eq(products.id, productTranslations.productId))
|
|
402
|
+
.where(and(...conditions))
|
|
403
|
+
.orderBy(desc(productTranslations.updatedAt))
|
|
404
|
+
.limit(1);
|
|
405
|
+
if (!row) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
return this.getCatalogProductById(db, row.productId, {
|
|
409
|
+
languageTag: normalizedLanguageTag ?? row.languageTag,
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
async listCatalogCategories(db, query) {
|
|
413
|
+
const conditions = [eq(productCategories.active, true)];
|
|
414
|
+
if (query.parentId) {
|
|
415
|
+
conditions.push(eq(productCategories.parentId, query.parentId));
|
|
416
|
+
}
|
|
417
|
+
if (query.search) {
|
|
418
|
+
const term = `%${query.search}%`;
|
|
419
|
+
const searchCondition = or(ilike(productCategories.name, term), ilike(productCategories.slug, term));
|
|
420
|
+
if (searchCondition) {
|
|
421
|
+
conditions.push(searchCondition);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const where = and(...conditions);
|
|
425
|
+
const [rows, countResult] = await Promise.all([
|
|
426
|
+
db
|
|
427
|
+
.select({
|
|
428
|
+
id: productCategories.id,
|
|
429
|
+
parentId: productCategories.parentId,
|
|
430
|
+
name: productCategories.name,
|
|
431
|
+
slug: productCategories.slug,
|
|
432
|
+
description: productCategories.description,
|
|
433
|
+
sortOrder: productCategories.sortOrder,
|
|
434
|
+
})
|
|
435
|
+
.from(productCategories)
|
|
436
|
+
.where(where)
|
|
437
|
+
.orderBy(asc(productCategories.sortOrder), asc(productCategories.name))
|
|
438
|
+
.limit(query.limit)
|
|
439
|
+
.offset(query.offset),
|
|
440
|
+
db.select({ count: sql `count(*)::int` }).from(productCategories).where(where),
|
|
441
|
+
]);
|
|
442
|
+
return {
|
|
443
|
+
data: rows.map((row) => ({
|
|
444
|
+
...row,
|
|
445
|
+
parentId: row.parentId ?? null,
|
|
446
|
+
description: row.description ?? null,
|
|
447
|
+
})),
|
|
448
|
+
total: countResult[0]?.count ?? 0,
|
|
449
|
+
limit: query.limit,
|
|
450
|
+
offset: query.offset,
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
async listCatalogTags(db, query) {
|
|
454
|
+
const conditions = [];
|
|
455
|
+
if (query.search) {
|
|
456
|
+
conditions.push(ilike(productTags.name, `%${query.search}%`));
|
|
457
|
+
}
|
|
458
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
459
|
+
const [rows, countResult] = await Promise.all([
|
|
460
|
+
db
|
|
461
|
+
.select({
|
|
462
|
+
id: productTags.id,
|
|
463
|
+
name: productTags.name,
|
|
464
|
+
})
|
|
465
|
+
.from(productTags)
|
|
466
|
+
.where(where)
|
|
467
|
+
.orderBy(asc(productTags.name))
|
|
468
|
+
.limit(query.limit)
|
|
469
|
+
.offset(query.offset),
|
|
470
|
+
db.select({ count: sql `count(*)::int` }).from(productTags).where(where),
|
|
471
|
+
]);
|
|
472
|
+
return {
|
|
473
|
+
data: rows,
|
|
474
|
+
total: countResult[0]?.count ?? 0,
|
|
475
|
+
limit: query.limit,
|
|
476
|
+
offset: query.offset,
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
};
|
package/dist/service.d.ts
CHANGED
|
@@ -352,8 +352,8 @@ export declare const productsService: {
|
|
|
352
352
|
createdAt: Date;
|
|
353
353
|
updatedAt: Date;
|
|
354
354
|
id: string;
|
|
355
|
-
capability: "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "accommodation" | "transport";
|
|
356
355
|
notes: string | null;
|
|
356
|
+
capability: "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "accommodation" | "transport";
|
|
357
357
|
} | null>;
|
|
358
358
|
updateCapability(db: PostgresJsDatabase, id: string, data: UpdateProductCapabilityInput): Promise<{
|
|
359
359
|
id: string;
|
|
@@ -439,8 +439,8 @@ export declare const productsService: {
|
|
|
439
439
|
description: string | null;
|
|
440
440
|
id: string;
|
|
441
441
|
sortOrder: number;
|
|
442
|
-
featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
|
|
443
442
|
title: string;
|
|
443
|
+
featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
|
|
444
444
|
} | null>;
|
|
445
445
|
updateFeature(db: PostgresJsDatabase, id: string, data: UpdateProductFeatureInput): Promise<{
|
|
446
446
|
id: string;
|
|
@@ -1169,8 +1169,8 @@ export declare const productsService: {
|
|
|
1169
1169
|
updatedAt: Date;
|
|
1170
1170
|
description: string | null;
|
|
1171
1171
|
id: string;
|
|
1172
|
-
title: string | null;
|
|
1173
1172
|
dayNumber: number;
|
|
1173
|
+
title: string | null;
|
|
1174
1174
|
location: string | null;
|
|
1175
1175
|
} | null | undefined>;
|
|
1176
1176
|
updateDay(db: PostgresJsDatabase, dayId: string, data: UpdateDayInput): Promise<{
|
|
@@ -1618,11 +1618,11 @@ export declare const productsService: {
|
|
|
1618
1618
|
id: string;
|
|
1619
1619
|
costAmountCents: number;
|
|
1620
1620
|
sortOrder: number | null;
|
|
1621
|
-
notes: string | null;
|
|
1622
1621
|
dayId: string;
|
|
1623
1622
|
serviceType: "other" | "transfer" | "accommodation" | "experience" | "guide" | "meal";
|
|
1624
1623
|
costCurrency: string;
|
|
1625
1624
|
quantity: number;
|
|
1625
|
+
notes: string | null;
|
|
1626
1626
|
} | null | undefined>;
|
|
1627
1627
|
updateDayService(db: PostgresJsDatabase, productId: string, serviceId: string, data: UpdateDayServiceInput): Promise<{
|
|
1628
1628
|
id: string;
|