@voyantjs/products 0.1.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.
- package/LICENSE +109 -0
- package/README.md +43 -0
- package/dist/booking-extension.d.ts +278 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +163 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/routes.d.ts +3674 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +819 -0
- package/dist/schema.d.ts +4156 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +655 -0
- package/dist/service.d.ts +2395 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +1501 -0
- package/dist/tasks/generate-pdf.d.ts +8 -0
- package/dist/tasks/generate-pdf.d.ts.map +1 -0
- package/dist/tasks/generate-pdf.js +102 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +1 -0
- package/dist/validation.d.ts +855 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +433 -0
- package/package.json +60 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,1501 @@
|
|
|
1
|
+
import { and, asc, desc, eq, ilike, or, sql } from "drizzle-orm";
|
|
2
|
+
import { optionUnits, optionUnitTranslations, productActivationSettings, productCapabilities, productCategories, productCategoryProducts, productDayServices, productDays, productDeliveryFormats, productFaqs, productFeatures, productLocations, productMedia, productNotes, productOptions, productOptionTranslations, products, productTagProducts, productTags, productTicketSettings, productTranslations, productTypes, productVersions, productVisibilitySettings, } from "./schema.js";
|
|
3
|
+
async function recalculateProductCost(db, productId) {
|
|
4
|
+
const [result] = await db
|
|
5
|
+
.select({
|
|
6
|
+
totalCost: sql `coalesce(sum(${productDayServices.costAmountCents} * ${productDayServices.quantity}), 0)::int`,
|
|
7
|
+
})
|
|
8
|
+
.from(productDayServices)
|
|
9
|
+
.innerJoin(productDays, eq(productDayServices.dayId, productDays.id))
|
|
10
|
+
.where(eq(productDays.productId, productId));
|
|
11
|
+
const costAmountCents = result?.totalCost ?? 0;
|
|
12
|
+
const [product] = await db
|
|
13
|
+
.select({ sellAmountCents: products.sellAmountCents })
|
|
14
|
+
.from(products)
|
|
15
|
+
.where(eq(products.id, productId))
|
|
16
|
+
.limit(1);
|
|
17
|
+
const sellAmountCents = product?.sellAmountCents ?? 0;
|
|
18
|
+
const marginPercent = sellAmountCents > 0
|
|
19
|
+
? Math.round(((sellAmountCents - costAmountCents) / sellAmountCents) * 100)
|
|
20
|
+
: 0;
|
|
21
|
+
await db
|
|
22
|
+
.update(products)
|
|
23
|
+
.set({ costAmountCents, marginPercent, updatedAt: new Date() })
|
|
24
|
+
.where(eq(products.id, productId));
|
|
25
|
+
return { costAmountCents, marginPercent };
|
|
26
|
+
}
|
|
27
|
+
async function ensureProductExists(db, productId) {
|
|
28
|
+
const [product] = await db
|
|
29
|
+
.select({ id: products.id })
|
|
30
|
+
.from(products)
|
|
31
|
+
.where(eq(products.id, productId))
|
|
32
|
+
.limit(1);
|
|
33
|
+
return product ?? null;
|
|
34
|
+
}
|
|
35
|
+
export const productsService = {
|
|
36
|
+
async listProducts(db, query) {
|
|
37
|
+
const conditions = [];
|
|
38
|
+
if (query.status) {
|
|
39
|
+
conditions.push(eq(products.status, query.status));
|
|
40
|
+
}
|
|
41
|
+
if (query.bookingMode) {
|
|
42
|
+
conditions.push(eq(products.bookingMode, query.bookingMode));
|
|
43
|
+
}
|
|
44
|
+
if (query.visibility) {
|
|
45
|
+
conditions.push(eq(products.visibility, query.visibility));
|
|
46
|
+
}
|
|
47
|
+
if (query.activated !== undefined) {
|
|
48
|
+
conditions.push(eq(products.activated, query.activated));
|
|
49
|
+
}
|
|
50
|
+
if (query.facilityId) {
|
|
51
|
+
conditions.push(eq(products.facilityId, query.facilityId));
|
|
52
|
+
}
|
|
53
|
+
if (query.search) {
|
|
54
|
+
const term = `%${query.search}%`;
|
|
55
|
+
conditions.push(or(ilike(products.name, term), ilike(products.description, term)));
|
|
56
|
+
}
|
|
57
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
58
|
+
const [rows, countResult] = await Promise.all([
|
|
59
|
+
db
|
|
60
|
+
.select()
|
|
61
|
+
.from(products)
|
|
62
|
+
.where(where)
|
|
63
|
+
.limit(query.limit)
|
|
64
|
+
.offset(query.offset)
|
|
65
|
+
.orderBy(products.createdAt),
|
|
66
|
+
db.select({ count: sql `count(*)::int` }).from(products).where(where),
|
|
67
|
+
]);
|
|
68
|
+
return {
|
|
69
|
+
data: rows,
|
|
70
|
+
total: countResult[0]?.count ?? 0,
|
|
71
|
+
limit: query.limit,
|
|
72
|
+
offset: query.offset,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
async getProductById(db, id) {
|
|
76
|
+
const [row] = await db.select().from(products).where(eq(products.id, id)).limit(1);
|
|
77
|
+
return row ?? null;
|
|
78
|
+
},
|
|
79
|
+
async createProduct(db, data) {
|
|
80
|
+
const [row] = await db.insert(products).values(data).returning();
|
|
81
|
+
return row;
|
|
82
|
+
},
|
|
83
|
+
async updateProduct(db, id, data) {
|
|
84
|
+
const [row] = await db
|
|
85
|
+
.update(products)
|
|
86
|
+
.set({ ...data, updatedAt: new Date() })
|
|
87
|
+
.where(eq(products.id, id))
|
|
88
|
+
.returning();
|
|
89
|
+
return row ?? null;
|
|
90
|
+
},
|
|
91
|
+
async deleteProduct(db, id) {
|
|
92
|
+
const [row] = await db
|
|
93
|
+
.delete(products)
|
|
94
|
+
.where(eq(products.id, id))
|
|
95
|
+
.returning({ id: products.id });
|
|
96
|
+
return row ?? null;
|
|
97
|
+
},
|
|
98
|
+
async listActivationSettings(db, query) {
|
|
99
|
+
const conditions = [];
|
|
100
|
+
if (query.productId) {
|
|
101
|
+
conditions.push(eq(productActivationSettings.productId, query.productId));
|
|
102
|
+
}
|
|
103
|
+
if (query.activationMode) {
|
|
104
|
+
conditions.push(eq(productActivationSettings.activationMode, query.activationMode));
|
|
105
|
+
}
|
|
106
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
107
|
+
const [rows, countResult] = await Promise.all([
|
|
108
|
+
db
|
|
109
|
+
.select()
|
|
110
|
+
.from(productActivationSettings)
|
|
111
|
+
.where(where)
|
|
112
|
+
.limit(query.limit)
|
|
113
|
+
.offset(query.offset)
|
|
114
|
+
.orderBy(asc(productActivationSettings.createdAt)),
|
|
115
|
+
db.select({ count: sql `count(*)::int` }).from(productActivationSettings).where(where),
|
|
116
|
+
]);
|
|
117
|
+
return {
|
|
118
|
+
data: rows,
|
|
119
|
+
total: countResult[0]?.count ?? 0,
|
|
120
|
+
limit: query.limit,
|
|
121
|
+
offset: query.offset,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
async getActivationSettingById(db, id) {
|
|
125
|
+
const [row] = await db
|
|
126
|
+
.select()
|
|
127
|
+
.from(productActivationSettings)
|
|
128
|
+
.where(eq(productActivationSettings.id, id))
|
|
129
|
+
.limit(1);
|
|
130
|
+
return row ?? null;
|
|
131
|
+
},
|
|
132
|
+
async upsertActivationSetting(db, productId, data) {
|
|
133
|
+
const product = await ensureProductExists(db, productId);
|
|
134
|
+
if (!product) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const [row] = await db
|
|
138
|
+
.insert(productActivationSettings)
|
|
139
|
+
.values({
|
|
140
|
+
productId,
|
|
141
|
+
...data,
|
|
142
|
+
activateAt: data.activateAt ? new Date(data.activateAt) : null,
|
|
143
|
+
deactivateAt: data.deactivateAt ? new Date(data.deactivateAt) : null,
|
|
144
|
+
sellAt: data.sellAt ? new Date(data.sellAt) : null,
|
|
145
|
+
stopSellAt: data.stopSellAt ? new Date(data.stopSellAt) : null,
|
|
146
|
+
})
|
|
147
|
+
.onConflictDoUpdate({
|
|
148
|
+
target: productActivationSettings.productId,
|
|
149
|
+
set: {
|
|
150
|
+
...data,
|
|
151
|
+
activateAt: data.activateAt ? new Date(data.activateAt) : null,
|
|
152
|
+
deactivateAt: data.deactivateAt ? new Date(data.deactivateAt) : null,
|
|
153
|
+
sellAt: data.sellAt ? new Date(data.sellAt) : null,
|
|
154
|
+
stopSellAt: data.stopSellAt ? new Date(data.stopSellAt) : null,
|
|
155
|
+
updatedAt: new Date(),
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
.returning();
|
|
159
|
+
return row ?? null;
|
|
160
|
+
},
|
|
161
|
+
async updateActivationSetting(db, id, data) {
|
|
162
|
+
const [row] = await db
|
|
163
|
+
.update(productActivationSettings)
|
|
164
|
+
.set({
|
|
165
|
+
...data,
|
|
166
|
+
activateAt: data.activateAt === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: data.activateAt
|
|
169
|
+
? new Date(data.activateAt)
|
|
170
|
+
: null,
|
|
171
|
+
deactivateAt: data.deactivateAt === undefined
|
|
172
|
+
? undefined
|
|
173
|
+
: data.deactivateAt
|
|
174
|
+
? new Date(data.deactivateAt)
|
|
175
|
+
: null,
|
|
176
|
+
sellAt: data.sellAt === undefined ? undefined : data.sellAt ? new Date(data.sellAt) : null,
|
|
177
|
+
stopSellAt: data.stopSellAt === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: data.stopSellAt
|
|
180
|
+
? new Date(data.stopSellAt)
|
|
181
|
+
: null,
|
|
182
|
+
updatedAt: new Date(),
|
|
183
|
+
})
|
|
184
|
+
.where(eq(productActivationSettings.id, id))
|
|
185
|
+
.returning();
|
|
186
|
+
return row ?? null;
|
|
187
|
+
},
|
|
188
|
+
async deleteActivationSetting(db, id) {
|
|
189
|
+
const [row] = await db
|
|
190
|
+
.delete(productActivationSettings)
|
|
191
|
+
.where(eq(productActivationSettings.id, id))
|
|
192
|
+
.returning({ id: productActivationSettings.id });
|
|
193
|
+
return row ?? null;
|
|
194
|
+
},
|
|
195
|
+
async listTicketSettings(db, query) {
|
|
196
|
+
const conditions = [];
|
|
197
|
+
if (query.productId) {
|
|
198
|
+
conditions.push(eq(productTicketSettings.productId, query.productId));
|
|
199
|
+
}
|
|
200
|
+
if (query.fulfillmentMode) {
|
|
201
|
+
conditions.push(eq(productTicketSettings.fulfillmentMode, query.fulfillmentMode));
|
|
202
|
+
}
|
|
203
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
204
|
+
const [rows, countResult] = await Promise.all([
|
|
205
|
+
db
|
|
206
|
+
.select()
|
|
207
|
+
.from(productTicketSettings)
|
|
208
|
+
.where(where)
|
|
209
|
+
.limit(query.limit)
|
|
210
|
+
.offset(query.offset)
|
|
211
|
+
.orderBy(asc(productTicketSettings.createdAt)),
|
|
212
|
+
db.select({ count: sql `count(*)::int` }).from(productTicketSettings).where(where),
|
|
213
|
+
]);
|
|
214
|
+
return {
|
|
215
|
+
data: rows,
|
|
216
|
+
total: countResult[0]?.count ?? 0,
|
|
217
|
+
limit: query.limit,
|
|
218
|
+
offset: query.offset,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
async getTicketSettingById(db, id) {
|
|
222
|
+
const [row] = await db
|
|
223
|
+
.select()
|
|
224
|
+
.from(productTicketSettings)
|
|
225
|
+
.where(eq(productTicketSettings.id, id))
|
|
226
|
+
.limit(1);
|
|
227
|
+
return row ?? null;
|
|
228
|
+
},
|
|
229
|
+
async upsertTicketSetting(db, productId, data) {
|
|
230
|
+
const product = await ensureProductExists(db, productId);
|
|
231
|
+
if (!product) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const [row] = await db
|
|
235
|
+
.insert(productTicketSettings)
|
|
236
|
+
.values({ productId, ...data })
|
|
237
|
+
.onConflictDoUpdate({
|
|
238
|
+
target: productTicketSettings.productId,
|
|
239
|
+
set: { ...data, updatedAt: new Date() },
|
|
240
|
+
})
|
|
241
|
+
.returning();
|
|
242
|
+
return row ?? null;
|
|
243
|
+
},
|
|
244
|
+
async updateTicketSetting(db, id, data) {
|
|
245
|
+
const [row] = await db
|
|
246
|
+
.update(productTicketSettings)
|
|
247
|
+
.set({ ...data, updatedAt: new Date() })
|
|
248
|
+
.where(eq(productTicketSettings.id, id))
|
|
249
|
+
.returning();
|
|
250
|
+
return row ?? null;
|
|
251
|
+
},
|
|
252
|
+
async deleteTicketSetting(db, id) {
|
|
253
|
+
const [row] = await db
|
|
254
|
+
.delete(productTicketSettings)
|
|
255
|
+
.where(eq(productTicketSettings.id, id))
|
|
256
|
+
.returning({ id: productTicketSettings.id });
|
|
257
|
+
return row ?? null;
|
|
258
|
+
},
|
|
259
|
+
async listVisibilitySettings(db, query) {
|
|
260
|
+
const conditions = [];
|
|
261
|
+
if (query.productId) {
|
|
262
|
+
conditions.push(eq(productVisibilitySettings.productId, query.productId));
|
|
263
|
+
}
|
|
264
|
+
if (query.isSearchable !== undefined) {
|
|
265
|
+
conditions.push(eq(productVisibilitySettings.isSearchable, query.isSearchable));
|
|
266
|
+
}
|
|
267
|
+
if (query.isBookable !== undefined) {
|
|
268
|
+
conditions.push(eq(productVisibilitySettings.isBookable, query.isBookable));
|
|
269
|
+
}
|
|
270
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
271
|
+
const [rows, countResult] = await Promise.all([
|
|
272
|
+
db
|
|
273
|
+
.select()
|
|
274
|
+
.from(productVisibilitySettings)
|
|
275
|
+
.where(where)
|
|
276
|
+
.limit(query.limit)
|
|
277
|
+
.offset(query.offset)
|
|
278
|
+
.orderBy(asc(productVisibilitySettings.createdAt)),
|
|
279
|
+
db.select({ count: sql `count(*)::int` }).from(productVisibilitySettings).where(where),
|
|
280
|
+
]);
|
|
281
|
+
return {
|
|
282
|
+
data: rows,
|
|
283
|
+
total: countResult[0]?.count ?? 0,
|
|
284
|
+
limit: query.limit,
|
|
285
|
+
offset: query.offset,
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
async getVisibilitySettingById(db, id) {
|
|
289
|
+
const [row] = await db
|
|
290
|
+
.select()
|
|
291
|
+
.from(productVisibilitySettings)
|
|
292
|
+
.where(eq(productVisibilitySettings.id, id))
|
|
293
|
+
.limit(1);
|
|
294
|
+
return row ?? null;
|
|
295
|
+
},
|
|
296
|
+
async upsertVisibilitySetting(db, productId, data) {
|
|
297
|
+
const product = await ensureProductExists(db, productId);
|
|
298
|
+
if (!product) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const [row] = await db
|
|
302
|
+
.insert(productVisibilitySettings)
|
|
303
|
+
.values({ productId, ...data })
|
|
304
|
+
.onConflictDoUpdate({
|
|
305
|
+
target: productVisibilitySettings.productId,
|
|
306
|
+
set: { ...data, updatedAt: new Date() },
|
|
307
|
+
})
|
|
308
|
+
.returning();
|
|
309
|
+
return row ?? null;
|
|
310
|
+
},
|
|
311
|
+
async updateVisibilitySetting(db, id, data) {
|
|
312
|
+
const [row] = await db
|
|
313
|
+
.update(productVisibilitySettings)
|
|
314
|
+
.set({ ...data, updatedAt: new Date() })
|
|
315
|
+
.where(eq(productVisibilitySettings.id, id))
|
|
316
|
+
.returning();
|
|
317
|
+
return row ?? null;
|
|
318
|
+
},
|
|
319
|
+
async deleteVisibilitySetting(db, id) {
|
|
320
|
+
const [row] = await db
|
|
321
|
+
.delete(productVisibilitySettings)
|
|
322
|
+
.where(eq(productVisibilitySettings.id, id))
|
|
323
|
+
.returning({ id: productVisibilitySettings.id });
|
|
324
|
+
return row ?? null;
|
|
325
|
+
},
|
|
326
|
+
async listCapabilities(db, query) {
|
|
327
|
+
const conditions = [];
|
|
328
|
+
if (query.productId) {
|
|
329
|
+
conditions.push(eq(productCapabilities.productId, query.productId));
|
|
330
|
+
}
|
|
331
|
+
if (query.capability) {
|
|
332
|
+
conditions.push(eq(productCapabilities.capability, query.capability));
|
|
333
|
+
}
|
|
334
|
+
if (query.enabled !== undefined) {
|
|
335
|
+
conditions.push(eq(productCapabilities.enabled, query.enabled));
|
|
336
|
+
}
|
|
337
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
338
|
+
const [rows, countResult] = await Promise.all([
|
|
339
|
+
db
|
|
340
|
+
.select()
|
|
341
|
+
.from(productCapabilities)
|
|
342
|
+
.where(where)
|
|
343
|
+
.limit(query.limit)
|
|
344
|
+
.offset(query.offset)
|
|
345
|
+
.orderBy(asc(productCapabilities.capability), asc(productCapabilities.createdAt)),
|
|
346
|
+
db.select({ count: sql `count(*)::int` }).from(productCapabilities).where(where),
|
|
347
|
+
]);
|
|
348
|
+
return {
|
|
349
|
+
data: rows,
|
|
350
|
+
total: countResult[0]?.count ?? 0,
|
|
351
|
+
limit: query.limit,
|
|
352
|
+
offset: query.offset,
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
async getCapabilityById(db, id) {
|
|
356
|
+
const [row] = await db
|
|
357
|
+
.select()
|
|
358
|
+
.from(productCapabilities)
|
|
359
|
+
.where(eq(productCapabilities.id, id))
|
|
360
|
+
.limit(1);
|
|
361
|
+
return row ?? null;
|
|
362
|
+
},
|
|
363
|
+
async createCapability(db, productId, data) {
|
|
364
|
+
const product = await ensureProductExists(db, productId);
|
|
365
|
+
if (!product) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const [row] = await db
|
|
369
|
+
.insert(productCapabilities)
|
|
370
|
+
.values({ productId, ...data })
|
|
371
|
+
.onConflictDoUpdate({
|
|
372
|
+
target: [productCapabilities.productId, productCapabilities.capability],
|
|
373
|
+
set: {
|
|
374
|
+
enabled: data.enabled,
|
|
375
|
+
notes: data.notes ?? null,
|
|
376
|
+
updatedAt: new Date(),
|
|
377
|
+
},
|
|
378
|
+
})
|
|
379
|
+
.returning();
|
|
380
|
+
return row ?? null;
|
|
381
|
+
},
|
|
382
|
+
async updateCapability(db, id, data) {
|
|
383
|
+
const [row] = await db
|
|
384
|
+
.update(productCapabilities)
|
|
385
|
+
.set({ ...data, updatedAt: new Date() })
|
|
386
|
+
.where(eq(productCapabilities.id, id))
|
|
387
|
+
.returning();
|
|
388
|
+
return row ?? null;
|
|
389
|
+
},
|
|
390
|
+
async deleteCapability(db, id) {
|
|
391
|
+
const [row] = await db
|
|
392
|
+
.delete(productCapabilities)
|
|
393
|
+
.where(eq(productCapabilities.id, id))
|
|
394
|
+
.returning({ id: productCapabilities.id });
|
|
395
|
+
return row ?? null;
|
|
396
|
+
},
|
|
397
|
+
async listDeliveryFormats(db, query) {
|
|
398
|
+
const conditions = [];
|
|
399
|
+
if (query.productId) {
|
|
400
|
+
conditions.push(eq(productDeliveryFormats.productId, query.productId));
|
|
401
|
+
}
|
|
402
|
+
if (query.format) {
|
|
403
|
+
conditions.push(eq(productDeliveryFormats.format, query.format));
|
|
404
|
+
}
|
|
405
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
406
|
+
const [rows, countResult] = await Promise.all([
|
|
407
|
+
db
|
|
408
|
+
.select()
|
|
409
|
+
.from(productDeliveryFormats)
|
|
410
|
+
.where(where)
|
|
411
|
+
.limit(query.limit)
|
|
412
|
+
.offset(query.offset)
|
|
413
|
+
.orderBy(desc(productDeliveryFormats.isDefault), asc(productDeliveryFormats.createdAt)),
|
|
414
|
+
db.select({ count: sql `count(*)::int` }).from(productDeliveryFormats).where(where),
|
|
415
|
+
]);
|
|
416
|
+
return {
|
|
417
|
+
data: rows,
|
|
418
|
+
total: countResult[0]?.count ?? 0,
|
|
419
|
+
limit: query.limit,
|
|
420
|
+
offset: query.offset,
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
async getDeliveryFormatById(db, id) {
|
|
424
|
+
const [row] = await db
|
|
425
|
+
.select()
|
|
426
|
+
.from(productDeliveryFormats)
|
|
427
|
+
.where(eq(productDeliveryFormats.id, id))
|
|
428
|
+
.limit(1);
|
|
429
|
+
return row ?? null;
|
|
430
|
+
},
|
|
431
|
+
async createDeliveryFormat(db, productId, data) {
|
|
432
|
+
const product = await ensureProductExists(db, productId);
|
|
433
|
+
if (!product) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
if (data.isDefault) {
|
|
437
|
+
await db
|
|
438
|
+
.update(productDeliveryFormats)
|
|
439
|
+
.set({ isDefault: false, updatedAt: new Date() })
|
|
440
|
+
.where(eq(productDeliveryFormats.productId, productId));
|
|
441
|
+
}
|
|
442
|
+
const [row] = await db
|
|
443
|
+
.insert(productDeliveryFormats)
|
|
444
|
+
.values({ productId, ...data })
|
|
445
|
+
.onConflictDoUpdate({
|
|
446
|
+
target: [productDeliveryFormats.productId, productDeliveryFormats.format],
|
|
447
|
+
set: {
|
|
448
|
+
isDefault: data.isDefault ?? false,
|
|
449
|
+
updatedAt: new Date(),
|
|
450
|
+
},
|
|
451
|
+
})
|
|
452
|
+
.returning();
|
|
453
|
+
return row ?? null;
|
|
454
|
+
},
|
|
455
|
+
async updateDeliveryFormat(db, id, data) {
|
|
456
|
+
const [current] = await db
|
|
457
|
+
.select({ id: productDeliveryFormats.id, productId: productDeliveryFormats.productId })
|
|
458
|
+
.from(productDeliveryFormats)
|
|
459
|
+
.where(eq(productDeliveryFormats.id, id))
|
|
460
|
+
.limit(1);
|
|
461
|
+
if (!current) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
if (data.isDefault) {
|
|
465
|
+
await db
|
|
466
|
+
.update(productDeliveryFormats)
|
|
467
|
+
.set({ isDefault: false, updatedAt: new Date() })
|
|
468
|
+
.where(eq(productDeliveryFormats.productId, current.productId));
|
|
469
|
+
}
|
|
470
|
+
const [row] = await db
|
|
471
|
+
.update(productDeliveryFormats)
|
|
472
|
+
.set({ ...data, updatedAt: new Date() })
|
|
473
|
+
.where(eq(productDeliveryFormats.id, id))
|
|
474
|
+
.returning();
|
|
475
|
+
return row ?? null;
|
|
476
|
+
},
|
|
477
|
+
async deleteDeliveryFormat(db, id) {
|
|
478
|
+
const [row] = await db
|
|
479
|
+
.delete(productDeliveryFormats)
|
|
480
|
+
.where(eq(productDeliveryFormats.id, id))
|
|
481
|
+
.returning({ id: productDeliveryFormats.id });
|
|
482
|
+
return row ?? null;
|
|
483
|
+
},
|
|
484
|
+
async listFeatures(db, query) {
|
|
485
|
+
const conditions = [];
|
|
486
|
+
if (query.productId) {
|
|
487
|
+
conditions.push(eq(productFeatures.productId, query.productId));
|
|
488
|
+
}
|
|
489
|
+
if (query.featureType) {
|
|
490
|
+
conditions.push(eq(productFeatures.featureType, query.featureType));
|
|
491
|
+
}
|
|
492
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
493
|
+
const [rows, countResult] = await Promise.all([
|
|
494
|
+
db
|
|
495
|
+
.select()
|
|
496
|
+
.from(productFeatures)
|
|
497
|
+
.where(where)
|
|
498
|
+
.limit(query.limit)
|
|
499
|
+
.offset(query.offset)
|
|
500
|
+
.orderBy(asc(productFeatures.sortOrder), asc(productFeatures.createdAt)),
|
|
501
|
+
db.select({ count: sql `count(*)::int` }).from(productFeatures).where(where),
|
|
502
|
+
]);
|
|
503
|
+
return {
|
|
504
|
+
data: rows,
|
|
505
|
+
total: countResult[0]?.count ?? 0,
|
|
506
|
+
limit: query.limit,
|
|
507
|
+
offset: query.offset,
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
async getFeatureById(db, id) {
|
|
511
|
+
const [row] = await db.select().from(productFeatures).where(eq(productFeatures.id, id)).limit(1);
|
|
512
|
+
return row ?? null;
|
|
513
|
+
},
|
|
514
|
+
async createFeature(db, productId, data) {
|
|
515
|
+
const product = await ensureProductExists(db, productId);
|
|
516
|
+
if (!product) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const [row] = await db.insert(productFeatures).values({ productId, ...data }).returning();
|
|
520
|
+
return row ?? null;
|
|
521
|
+
},
|
|
522
|
+
async updateFeature(db, id, data) {
|
|
523
|
+
const [row] = await db
|
|
524
|
+
.update(productFeatures)
|
|
525
|
+
.set({ ...data, updatedAt: new Date() })
|
|
526
|
+
.where(eq(productFeatures.id, id))
|
|
527
|
+
.returning();
|
|
528
|
+
return row ?? null;
|
|
529
|
+
},
|
|
530
|
+
async deleteFeature(db, id) {
|
|
531
|
+
const [row] = await db
|
|
532
|
+
.delete(productFeatures)
|
|
533
|
+
.where(eq(productFeatures.id, id))
|
|
534
|
+
.returning({ id: productFeatures.id });
|
|
535
|
+
return row ?? null;
|
|
536
|
+
},
|
|
537
|
+
async listFaqs(db, query) {
|
|
538
|
+
const conditions = [];
|
|
539
|
+
if (query.productId) {
|
|
540
|
+
conditions.push(eq(productFaqs.productId, query.productId));
|
|
541
|
+
}
|
|
542
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
543
|
+
const [rows, countResult] = await Promise.all([
|
|
544
|
+
db
|
|
545
|
+
.select()
|
|
546
|
+
.from(productFaqs)
|
|
547
|
+
.where(where)
|
|
548
|
+
.limit(query.limit)
|
|
549
|
+
.offset(query.offset)
|
|
550
|
+
.orderBy(asc(productFaqs.sortOrder), asc(productFaqs.createdAt)),
|
|
551
|
+
db.select({ count: sql `count(*)::int` }).from(productFaqs).where(where),
|
|
552
|
+
]);
|
|
553
|
+
return {
|
|
554
|
+
data: rows,
|
|
555
|
+
total: countResult[0]?.count ?? 0,
|
|
556
|
+
limit: query.limit,
|
|
557
|
+
offset: query.offset,
|
|
558
|
+
};
|
|
559
|
+
},
|
|
560
|
+
async getFaqById(db, id) {
|
|
561
|
+
const [row] = await db.select().from(productFaqs).where(eq(productFaqs.id, id)).limit(1);
|
|
562
|
+
return row ?? null;
|
|
563
|
+
},
|
|
564
|
+
async createFaq(db, productId, data) {
|
|
565
|
+
const product = await ensureProductExists(db, productId);
|
|
566
|
+
if (!product) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
const [row] = await db.insert(productFaqs).values({ productId, ...data }).returning();
|
|
570
|
+
return row ?? null;
|
|
571
|
+
},
|
|
572
|
+
async updateFaq(db, id, data) {
|
|
573
|
+
const [row] = await db
|
|
574
|
+
.update(productFaqs)
|
|
575
|
+
.set({ ...data, updatedAt: new Date() })
|
|
576
|
+
.where(eq(productFaqs.id, id))
|
|
577
|
+
.returning();
|
|
578
|
+
return row ?? null;
|
|
579
|
+
},
|
|
580
|
+
async deleteFaq(db, id) {
|
|
581
|
+
const [row] = await db
|
|
582
|
+
.delete(productFaqs)
|
|
583
|
+
.where(eq(productFaqs.id, id))
|
|
584
|
+
.returning({ id: productFaqs.id });
|
|
585
|
+
return row ?? null;
|
|
586
|
+
},
|
|
587
|
+
async listLocations(db, query) {
|
|
588
|
+
const conditions = [];
|
|
589
|
+
if (query.productId) {
|
|
590
|
+
conditions.push(eq(productLocations.productId, query.productId));
|
|
591
|
+
}
|
|
592
|
+
if (query.locationType) {
|
|
593
|
+
conditions.push(eq(productLocations.locationType, query.locationType));
|
|
594
|
+
}
|
|
595
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
596
|
+
const [rows, countResult] = await Promise.all([
|
|
597
|
+
db
|
|
598
|
+
.select()
|
|
599
|
+
.from(productLocations)
|
|
600
|
+
.where(where)
|
|
601
|
+
.limit(query.limit)
|
|
602
|
+
.offset(query.offset)
|
|
603
|
+
.orderBy(asc(productLocations.sortOrder), asc(productLocations.createdAt)),
|
|
604
|
+
db.select({ count: sql `count(*)::int` }).from(productLocations).where(where),
|
|
605
|
+
]);
|
|
606
|
+
return {
|
|
607
|
+
data: rows,
|
|
608
|
+
total: countResult[0]?.count ?? 0,
|
|
609
|
+
limit: query.limit,
|
|
610
|
+
offset: query.offset,
|
|
611
|
+
};
|
|
612
|
+
},
|
|
613
|
+
async getLocationById(db, id) {
|
|
614
|
+
const [row] = await db.select().from(productLocations).where(eq(productLocations.id, id)).limit(1);
|
|
615
|
+
return row ?? null;
|
|
616
|
+
},
|
|
617
|
+
async createLocation(db, productId, data) {
|
|
618
|
+
const product = await ensureProductExists(db, productId);
|
|
619
|
+
if (!product) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
const [row] = await db.insert(productLocations).values({ productId, ...data }).returning();
|
|
623
|
+
return row ?? null;
|
|
624
|
+
},
|
|
625
|
+
async updateLocation(db, id, data) {
|
|
626
|
+
const [row] = await db
|
|
627
|
+
.update(productLocations)
|
|
628
|
+
.set({ ...data, updatedAt: new Date() })
|
|
629
|
+
.where(eq(productLocations.id, id))
|
|
630
|
+
.returning();
|
|
631
|
+
return row ?? null;
|
|
632
|
+
},
|
|
633
|
+
async deleteLocation(db, id) {
|
|
634
|
+
const [row] = await db
|
|
635
|
+
.delete(productLocations)
|
|
636
|
+
.where(eq(productLocations.id, id))
|
|
637
|
+
.returning({ id: productLocations.id });
|
|
638
|
+
return row ?? null;
|
|
639
|
+
},
|
|
640
|
+
async listOptions(db, query) {
|
|
641
|
+
const conditions = [];
|
|
642
|
+
if (query.productId) {
|
|
643
|
+
conditions.push(eq(productOptions.productId, query.productId));
|
|
644
|
+
}
|
|
645
|
+
if (query.status) {
|
|
646
|
+
conditions.push(eq(productOptions.status, query.status));
|
|
647
|
+
}
|
|
648
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
649
|
+
const [rows, countResult] = await Promise.all([
|
|
650
|
+
db
|
|
651
|
+
.select()
|
|
652
|
+
.from(productOptions)
|
|
653
|
+
.where(where)
|
|
654
|
+
.limit(query.limit)
|
|
655
|
+
.offset(query.offset)
|
|
656
|
+
.orderBy(asc(productOptions.sortOrder), asc(productOptions.createdAt)),
|
|
657
|
+
db.select({ count: sql `count(*)::int` }).from(productOptions).where(where),
|
|
658
|
+
]);
|
|
659
|
+
return {
|
|
660
|
+
data: rows,
|
|
661
|
+
total: countResult[0]?.count ?? 0,
|
|
662
|
+
limit: query.limit,
|
|
663
|
+
offset: query.offset,
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
async getOptionById(db, id) {
|
|
667
|
+
const [row] = await db.select().from(productOptions).where(eq(productOptions.id, id)).limit(1);
|
|
668
|
+
return row ?? null;
|
|
669
|
+
},
|
|
670
|
+
async createOption(db, productId, data) {
|
|
671
|
+
const [product] = await db
|
|
672
|
+
.select({ id: products.id })
|
|
673
|
+
.from(products)
|
|
674
|
+
.where(eq(products.id, productId))
|
|
675
|
+
.limit(1);
|
|
676
|
+
if (!product) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
if (data.isDefault) {
|
|
680
|
+
await db
|
|
681
|
+
.update(productOptions)
|
|
682
|
+
.set({ isDefault: false, updatedAt: new Date() })
|
|
683
|
+
.where(eq(productOptions.productId, productId));
|
|
684
|
+
}
|
|
685
|
+
const [row] = await db
|
|
686
|
+
.insert(productOptions)
|
|
687
|
+
.values({ ...data, productId })
|
|
688
|
+
.returning();
|
|
689
|
+
return row;
|
|
690
|
+
},
|
|
691
|
+
async updateOption(db, id, data) {
|
|
692
|
+
const [current] = await db
|
|
693
|
+
.select({ id: productOptions.id, productId: productOptions.productId })
|
|
694
|
+
.from(productOptions)
|
|
695
|
+
.where(eq(productOptions.id, id))
|
|
696
|
+
.limit(1);
|
|
697
|
+
if (!current) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
if (data.isDefault) {
|
|
701
|
+
await db
|
|
702
|
+
.update(productOptions)
|
|
703
|
+
.set({ isDefault: false, updatedAt: new Date() })
|
|
704
|
+
.where(eq(productOptions.productId, current.productId));
|
|
705
|
+
}
|
|
706
|
+
const [row] = await db
|
|
707
|
+
.update(productOptions)
|
|
708
|
+
.set({ ...data, updatedAt: new Date() })
|
|
709
|
+
.where(eq(productOptions.id, id))
|
|
710
|
+
.returning();
|
|
711
|
+
return row ?? null;
|
|
712
|
+
},
|
|
713
|
+
async deleteOption(db, id) {
|
|
714
|
+
const [row] = await db
|
|
715
|
+
.delete(productOptions)
|
|
716
|
+
.where(eq(productOptions.id, id))
|
|
717
|
+
.returning({ id: productOptions.id });
|
|
718
|
+
return row ?? null;
|
|
719
|
+
},
|
|
720
|
+
async listUnits(db, query) {
|
|
721
|
+
const conditions = [];
|
|
722
|
+
if (query.optionId) {
|
|
723
|
+
conditions.push(eq(optionUnits.optionId, query.optionId));
|
|
724
|
+
}
|
|
725
|
+
if (query.unitType) {
|
|
726
|
+
conditions.push(eq(optionUnits.unitType, query.unitType));
|
|
727
|
+
}
|
|
728
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
729
|
+
const [rows, countResult] = await Promise.all([
|
|
730
|
+
db
|
|
731
|
+
.select()
|
|
732
|
+
.from(optionUnits)
|
|
733
|
+
.where(where)
|
|
734
|
+
.limit(query.limit)
|
|
735
|
+
.offset(query.offset)
|
|
736
|
+
.orderBy(asc(optionUnits.sortOrder), asc(optionUnits.createdAt)),
|
|
737
|
+
db.select({ count: sql `count(*)::int` }).from(optionUnits).where(where),
|
|
738
|
+
]);
|
|
739
|
+
return {
|
|
740
|
+
data: rows,
|
|
741
|
+
total: countResult[0]?.count ?? 0,
|
|
742
|
+
limit: query.limit,
|
|
743
|
+
offset: query.offset,
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
async getUnitById(db, id) {
|
|
747
|
+
const [row] = await db.select().from(optionUnits).where(eq(optionUnits.id, id)).limit(1);
|
|
748
|
+
return row ?? null;
|
|
749
|
+
},
|
|
750
|
+
async createUnit(db, optionId, data) {
|
|
751
|
+
const [option] = await db
|
|
752
|
+
.select({ id: productOptions.id })
|
|
753
|
+
.from(productOptions)
|
|
754
|
+
.where(eq(productOptions.id, optionId))
|
|
755
|
+
.limit(1);
|
|
756
|
+
if (!option) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
const [row] = await db
|
|
760
|
+
.insert(optionUnits)
|
|
761
|
+
.values({ ...data, optionId })
|
|
762
|
+
.returning();
|
|
763
|
+
return row;
|
|
764
|
+
},
|
|
765
|
+
async updateUnit(db, id, data) {
|
|
766
|
+
const [row] = await db
|
|
767
|
+
.update(optionUnits)
|
|
768
|
+
.set({ ...data, updatedAt: new Date() })
|
|
769
|
+
.where(eq(optionUnits.id, id))
|
|
770
|
+
.returning();
|
|
771
|
+
return row ?? null;
|
|
772
|
+
},
|
|
773
|
+
async deleteUnit(db, id) {
|
|
774
|
+
const [row] = await db
|
|
775
|
+
.delete(optionUnits)
|
|
776
|
+
.where(eq(optionUnits.id, id))
|
|
777
|
+
.returning({ id: optionUnits.id });
|
|
778
|
+
return row ?? null;
|
|
779
|
+
},
|
|
780
|
+
async listProductTranslations(db, query) {
|
|
781
|
+
const conditions = [];
|
|
782
|
+
if (query.productId) {
|
|
783
|
+
conditions.push(eq(productTranslations.productId, query.productId));
|
|
784
|
+
}
|
|
785
|
+
if (query.languageTag) {
|
|
786
|
+
conditions.push(eq(productTranslations.languageTag, query.languageTag));
|
|
787
|
+
}
|
|
788
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
789
|
+
const [rows, countResult] = await Promise.all([
|
|
790
|
+
db
|
|
791
|
+
.select()
|
|
792
|
+
.from(productTranslations)
|
|
793
|
+
.where(where)
|
|
794
|
+
.limit(query.limit)
|
|
795
|
+
.offset(query.offset)
|
|
796
|
+
.orderBy(asc(productTranslations.languageTag), asc(productTranslations.createdAt)),
|
|
797
|
+
db.select({ count: sql `count(*)::int` }).from(productTranslations).where(where),
|
|
798
|
+
]);
|
|
799
|
+
return {
|
|
800
|
+
data: rows,
|
|
801
|
+
total: countResult[0]?.count ?? 0,
|
|
802
|
+
limit: query.limit,
|
|
803
|
+
offset: query.offset,
|
|
804
|
+
};
|
|
805
|
+
},
|
|
806
|
+
async getProductTranslationById(db, id) {
|
|
807
|
+
const [row] = await db
|
|
808
|
+
.select()
|
|
809
|
+
.from(productTranslations)
|
|
810
|
+
.where(eq(productTranslations.id, id))
|
|
811
|
+
.limit(1);
|
|
812
|
+
return row ?? null;
|
|
813
|
+
},
|
|
814
|
+
async createProductTranslation(db, productId, data) {
|
|
815
|
+
const [product] = await db
|
|
816
|
+
.select({ id: products.id })
|
|
817
|
+
.from(products)
|
|
818
|
+
.where(eq(products.id, productId))
|
|
819
|
+
.limit(1);
|
|
820
|
+
if (!product) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const [row] = await db
|
|
824
|
+
.insert(productTranslations)
|
|
825
|
+
.values({ ...data, productId })
|
|
826
|
+
.returning();
|
|
827
|
+
return row ?? null;
|
|
828
|
+
},
|
|
829
|
+
async updateProductTranslation(db, id, data) {
|
|
830
|
+
const [row] = await db
|
|
831
|
+
.update(productTranslations)
|
|
832
|
+
.set({ ...data, updatedAt: new Date() })
|
|
833
|
+
.where(eq(productTranslations.id, id))
|
|
834
|
+
.returning();
|
|
835
|
+
return row ?? null;
|
|
836
|
+
},
|
|
837
|
+
async deleteProductTranslation(db, id) {
|
|
838
|
+
const [row] = await db
|
|
839
|
+
.delete(productTranslations)
|
|
840
|
+
.where(eq(productTranslations.id, id))
|
|
841
|
+
.returning({ id: productTranslations.id });
|
|
842
|
+
return row ?? null;
|
|
843
|
+
},
|
|
844
|
+
async listOptionTranslations(db, query) {
|
|
845
|
+
const conditions = [];
|
|
846
|
+
if (query.optionId) {
|
|
847
|
+
conditions.push(eq(productOptionTranslations.optionId, query.optionId));
|
|
848
|
+
}
|
|
849
|
+
if (query.languageTag) {
|
|
850
|
+
conditions.push(eq(productOptionTranslations.languageTag, query.languageTag));
|
|
851
|
+
}
|
|
852
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
853
|
+
const [rows, countResult] = await Promise.all([
|
|
854
|
+
db
|
|
855
|
+
.select()
|
|
856
|
+
.from(productOptionTranslations)
|
|
857
|
+
.where(where)
|
|
858
|
+
.limit(query.limit)
|
|
859
|
+
.offset(query.offset)
|
|
860
|
+
.orderBy(asc(productOptionTranslations.languageTag), asc(productOptionTranslations.createdAt)),
|
|
861
|
+
db.select({ count: sql `count(*)::int` }).from(productOptionTranslations).where(where),
|
|
862
|
+
]);
|
|
863
|
+
return {
|
|
864
|
+
data: rows,
|
|
865
|
+
total: countResult[0]?.count ?? 0,
|
|
866
|
+
limit: query.limit,
|
|
867
|
+
offset: query.offset,
|
|
868
|
+
};
|
|
869
|
+
},
|
|
870
|
+
async getOptionTranslationById(db, id) {
|
|
871
|
+
const [row] = await db
|
|
872
|
+
.select()
|
|
873
|
+
.from(productOptionTranslations)
|
|
874
|
+
.where(eq(productOptionTranslations.id, id))
|
|
875
|
+
.limit(1);
|
|
876
|
+
return row ?? null;
|
|
877
|
+
},
|
|
878
|
+
async createOptionTranslation(db, optionId, data) {
|
|
879
|
+
const [option] = await db
|
|
880
|
+
.select({ id: productOptions.id })
|
|
881
|
+
.from(productOptions)
|
|
882
|
+
.where(eq(productOptions.id, optionId))
|
|
883
|
+
.limit(1);
|
|
884
|
+
if (!option) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const [row] = await db
|
|
888
|
+
.insert(productOptionTranslations)
|
|
889
|
+
.values({ ...data, optionId })
|
|
890
|
+
.returning();
|
|
891
|
+
return row ?? null;
|
|
892
|
+
},
|
|
893
|
+
async updateOptionTranslation(db, id, data) {
|
|
894
|
+
const [row] = await db
|
|
895
|
+
.update(productOptionTranslations)
|
|
896
|
+
.set({ ...data, updatedAt: new Date() })
|
|
897
|
+
.where(eq(productOptionTranslations.id, id))
|
|
898
|
+
.returning();
|
|
899
|
+
return row ?? null;
|
|
900
|
+
},
|
|
901
|
+
async deleteOptionTranslation(db, id) {
|
|
902
|
+
const [row] = await db
|
|
903
|
+
.delete(productOptionTranslations)
|
|
904
|
+
.where(eq(productOptionTranslations.id, id))
|
|
905
|
+
.returning({ id: productOptionTranslations.id });
|
|
906
|
+
return row ?? null;
|
|
907
|
+
},
|
|
908
|
+
async listUnitTranslations(db, query) {
|
|
909
|
+
const conditions = [];
|
|
910
|
+
if (query.unitId) {
|
|
911
|
+
conditions.push(eq(optionUnitTranslations.unitId, query.unitId));
|
|
912
|
+
}
|
|
913
|
+
if (query.languageTag) {
|
|
914
|
+
conditions.push(eq(optionUnitTranslations.languageTag, query.languageTag));
|
|
915
|
+
}
|
|
916
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
917
|
+
const [rows, countResult] = await Promise.all([
|
|
918
|
+
db
|
|
919
|
+
.select()
|
|
920
|
+
.from(optionUnitTranslations)
|
|
921
|
+
.where(where)
|
|
922
|
+
.limit(query.limit)
|
|
923
|
+
.offset(query.offset)
|
|
924
|
+
.orderBy(asc(optionUnitTranslations.languageTag), asc(optionUnitTranslations.createdAt)),
|
|
925
|
+
db.select({ count: sql `count(*)::int` }).from(optionUnitTranslations).where(where),
|
|
926
|
+
]);
|
|
927
|
+
return {
|
|
928
|
+
data: rows,
|
|
929
|
+
total: countResult[0]?.count ?? 0,
|
|
930
|
+
limit: query.limit,
|
|
931
|
+
offset: query.offset,
|
|
932
|
+
};
|
|
933
|
+
},
|
|
934
|
+
async getUnitTranslationById(db, id) {
|
|
935
|
+
const [row] = await db
|
|
936
|
+
.select()
|
|
937
|
+
.from(optionUnitTranslations)
|
|
938
|
+
.where(eq(optionUnitTranslations.id, id))
|
|
939
|
+
.limit(1);
|
|
940
|
+
return row ?? null;
|
|
941
|
+
},
|
|
942
|
+
async createUnitTranslation(db, unitId, data) {
|
|
943
|
+
const [unit] = await db
|
|
944
|
+
.select({ id: optionUnits.id })
|
|
945
|
+
.from(optionUnits)
|
|
946
|
+
.where(eq(optionUnits.id, unitId))
|
|
947
|
+
.limit(1);
|
|
948
|
+
if (!unit) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
const [row] = await db
|
|
952
|
+
.insert(optionUnitTranslations)
|
|
953
|
+
.values({ ...data, unitId })
|
|
954
|
+
.returning();
|
|
955
|
+
return row ?? null;
|
|
956
|
+
},
|
|
957
|
+
async updateUnitTranslation(db, id, data) {
|
|
958
|
+
const [row] = await db
|
|
959
|
+
.update(optionUnitTranslations)
|
|
960
|
+
.set({ ...data, updatedAt: new Date() })
|
|
961
|
+
.where(eq(optionUnitTranslations.id, id))
|
|
962
|
+
.returning();
|
|
963
|
+
return row ?? null;
|
|
964
|
+
},
|
|
965
|
+
async deleteUnitTranslation(db, id) {
|
|
966
|
+
const [row] = await db
|
|
967
|
+
.delete(optionUnitTranslations)
|
|
968
|
+
.where(eq(optionUnitTranslations.id, id))
|
|
969
|
+
.returning({ id: optionUnitTranslations.id });
|
|
970
|
+
return row ?? null;
|
|
971
|
+
},
|
|
972
|
+
listDays(db, productId) {
|
|
973
|
+
return db
|
|
974
|
+
.select()
|
|
975
|
+
.from(productDays)
|
|
976
|
+
.where(eq(productDays.productId, productId))
|
|
977
|
+
.orderBy(asc(productDays.dayNumber));
|
|
978
|
+
},
|
|
979
|
+
async createDay(db, productId, data) {
|
|
980
|
+
const [product] = await db
|
|
981
|
+
.select({ id: products.id })
|
|
982
|
+
.from(products)
|
|
983
|
+
.where(eq(products.id, productId))
|
|
984
|
+
.limit(1);
|
|
985
|
+
if (!product) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const [row] = await db
|
|
989
|
+
.insert(productDays)
|
|
990
|
+
.values({ ...data, productId })
|
|
991
|
+
.returning();
|
|
992
|
+
return row;
|
|
993
|
+
},
|
|
994
|
+
async updateDay(db, dayId, data) {
|
|
995
|
+
const [row] = await db
|
|
996
|
+
.update(productDays)
|
|
997
|
+
.set({ ...data, updatedAt: new Date() })
|
|
998
|
+
.where(eq(productDays.id, dayId))
|
|
999
|
+
.returning();
|
|
1000
|
+
return row ?? null;
|
|
1001
|
+
},
|
|
1002
|
+
async deleteDay(db, dayId) {
|
|
1003
|
+
const [row] = await db
|
|
1004
|
+
.delete(productDays)
|
|
1005
|
+
.where(eq(productDays.id, dayId))
|
|
1006
|
+
.returning({ id: productDays.id });
|
|
1007
|
+
return row ?? null;
|
|
1008
|
+
},
|
|
1009
|
+
listDayServices(db, dayId) {
|
|
1010
|
+
return db
|
|
1011
|
+
.select()
|
|
1012
|
+
.from(productDayServices)
|
|
1013
|
+
.where(eq(productDayServices.dayId, dayId))
|
|
1014
|
+
.orderBy(asc(productDayServices.sortOrder));
|
|
1015
|
+
},
|
|
1016
|
+
async createDayService(db, productId, dayId, data) {
|
|
1017
|
+
const [day] = await db
|
|
1018
|
+
.select({ id: productDays.id })
|
|
1019
|
+
.from(productDays)
|
|
1020
|
+
.where(eq(productDays.id, dayId))
|
|
1021
|
+
.limit(1);
|
|
1022
|
+
if (!day) {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
const [row] = await db
|
|
1026
|
+
.insert(productDayServices)
|
|
1027
|
+
.values({ ...data, dayId })
|
|
1028
|
+
.returning();
|
|
1029
|
+
await recalculateProductCost(db, productId);
|
|
1030
|
+
return row;
|
|
1031
|
+
},
|
|
1032
|
+
async updateDayService(db, productId, serviceId, data) {
|
|
1033
|
+
const [row] = await db
|
|
1034
|
+
.update(productDayServices)
|
|
1035
|
+
.set(data)
|
|
1036
|
+
.where(eq(productDayServices.id, serviceId))
|
|
1037
|
+
.returning();
|
|
1038
|
+
if (!row) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
await recalculateProductCost(db, productId);
|
|
1042
|
+
return row;
|
|
1043
|
+
},
|
|
1044
|
+
async deleteDayService(db, productId, serviceId) {
|
|
1045
|
+
const [row] = await db
|
|
1046
|
+
.delete(productDayServices)
|
|
1047
|
+
.where(eq(productDayServices.id, serviceId))
|
|
1048
|
+
.returning({ id: productDayServices.id });
|
|
1049
|
+
if (!row) {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
await recalculateProductCost(db, productId);
|
|
1053
|
+
return row;
|
|
1054
|
+
},
|
|
1055
|
+
listVersions(db, productId) {
|
|
1056
|
+
return db
|
|
1057
|
+
.select()
|
|
1058
|
+
.from(productVersions)
|
|
1059
|
+
.where(eq(productVersions.productId, productId))
|
|
1060
|
+
.orderBy(desc(productVersions.versionNumber));
|
|
1061
|
+
},
|
|
1062
|
+
async createVersion(db, productId, userId, data) {
|
|
1063
|
+
const [product] = await db.select().from(products).where(eq(products.id, productId)).limit(1);
|
|
1064
|
+
if (!product) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
const days = await db
|
|
1068
|
+
.select()
|
|
1069
|
+
.from(productDays)
|
|
1070
|
+
.where(eq(productDays.productId, productId))
|
|
1071
|
+
.orderBy(asc(productDays.dayNumber));
|
|
1072
|
+
const options = await db
|
|
1073
|
+
.select()
|
|
1074
|
+
.from(productOptions)
|
|
1075
|
+
.where(eq(productOptions.productId, productId))
|
|
1076
|
+
.orderBy(asc(productOptions.sortOrder), asc(productOptions.createdAt));
|
|
1077
|
+
const optionsWithUnits = await Promise.all(options.map(async (option) => {
|
|
1078
|
+
const units = await db
|
|
1079
|
+
.select()
|
|
1080
|
+
.from(optionUnits)
|
|
1081
|
+
.where(eq(optionUnits.optionId, option.id))
|
|
1082
|
+
.orderBy(asc(optionUnits.sortOrder), asc(optionUnits.createdAt));
|
|
1083
|
+
return { ...option, units };
|
|
1084
|
+
}));
|
|
1085
|
+
const daysWithServices = await Promise.all(days.map(async (day) => {
|
|
1086
|
+
const services = await db
|
|
1087
|
+
.select()
|
|
1088
|
+
.from(productDayServices)
|
|
1089
|
+
.where(eq(productDayServices.dayId, day.id))
|
|
1090
|
+
.orderBy(asc(productDayServices.sortOrder));
|
|
1091
|
+
return { ...day, services };
|
|
1092
|
+
}));
|
|
1093
|
+
const [maxVersion] = await db
|
|
1094
|
+
.select({ max: sql `coalesce(max(${productVersions.versionNumber}), 0)` })
|
|
1095
|
+
.from(productVersions)
|
|
1096
|
+
.where(eq(productVersions.productId, productId));
|
|
1097
|
+
const [row] = await db
|
|
1098
|
+
.insert(productVersions)
|
|
1099
|
+
.values({
|
|
1100
|
+
productId,
|
|
1101
|
+
versionNumber: (maxVersion?.max ?? 0) + 1,
|
|
1102
|
+
snapshot: { ...product, options: optionsWithUnits, days: daysWithServices },
|
|
1103
|
+
authorId: userId,
|
|
1104
|
+
notes: data.notes,
|
|
1105
|
+
})
|
|
1106
|
+
.returning();
|
|
1107
|
+
return row;
|
|
1108
|
+
},
|
|
1109
|
+
listNotes(db, productId) {
|
|
1110
|
+
return db
|
|
1111
|
+
.select()
|
|
1112
|
+
.from(productNotes)
|
|
1113
|
+
.where(eq(productNotes.productId, productId))
|
|
1114
|
+
.orderBy(productNotes.createdAt);
|
|
1115
|
+
},
|
|
1116
|
+
async createNote(db, productId, userId, data) {
|
|
1117
|
+
const [product] = await db
|
|
1118
|
+
.select({ id: products.id })
|
|
1119
|
+
.from(products)
|
|
1120
|
+
.where(eq(products.id, productId))
|
|
1121
|
+
.limit(1);
|
|
1122
|
+
if (!product) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
const [row] = await db
|
|
1126
|
+
.insert(productNotes)
|
|
1127
|
+
.values({
|
|
1128
|
+
productId,
|
|
1129
|
+
authorId: userId,
|
|
1130
|
+
content: data.content,
|
|
1131
|
+
})
|
|
1132
|
+
.returning();
|
|
1133
|
+
return row;
|
|
1134
|
+
},
|
|
1135
|
+
async recalculate(db, productId) {
|
|
1136
|
+
const [product] = await db
|
|
1137
|
+
.select({ id: products.id })
|
|
1138
|
+
.from(products)
|
|
1139
|
+
.where(eq(products.id, productId))
|
|
1140
|
+
.limit(1);
|
|
1141
|
+
if (!product) {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
return recalculateProductCost(db, productId);
|
|
1145
|
+
},
|
|
1146
|
+
// ==========================================================================
|
|
1147
|
+
// Product Types
|
|
1148
|
+
// ==========================================================================
|
|
1149
|
+
async listProductTypes(db, query) {
|
|
1150
|
+
const conditions = [];
|
|
1151
|
+
if (query.active !== undefined) {
|
|
1152
|
+
conditions.push(eq(productTypes.active, query.active));
|
|
1153
|
+
}
|
|
1154
|
+
if (query.search) {
|
|
1155
|
+
const term = `%${query.search}%`;
|
|
1156
|
+
conditions.push(or(ilike(productTypes.name, term), ilike(productTypes.code, term)));
|
|
1157
|
+
}
|
|
1158
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
1159
|
+
const [rows, countResult] = await Promise.all([
|
|
1160
|
+
db
|
|
1161
|
+
.select()
|
|
1162
|
+
.from(productTypes)
|
|
1163
|
+
.where(where)
|
|
1164
|
+
.limit(query.limit)
|
|
1165
|
+
.offset(query.offset)
|
|
1166
|
+
.orderBy(asc(productTypes.sortOrder), asc(productTypes.name)),
|
|
1167
|
+
db.select({ count: sql `count(*)::int` }).from(productTypes).where(where),
|
|
1168
|
+
]);
|
|
1169
|
+
return {
|
|
1170
|
+
data: rows,
|
|
1171
|
+
total: countResult[0]?.count ?? 0,
|
|
1172
|
+
limit: query.limit,
|
|
1173
|
+
offset: query.offset,
|
|
1174
|
+
};
|
|
1175
|
+
},
|
|
1176
|
+
async getProductTypeById(db, id) {
|
|
1177
|
+
const [row] = await db.select().from(productTypes).where(eq(productTypes.id, id)).limit(1);
|
|
1178
|
+
return row ?? null;
|
|
1179
|
+
},
|
|
1180
|
+
async createProductType(db, data) {
|
|
1181
|
+
const [row] = await db.insert(productTypes).values(data).returning();
|
|
1182
|
+
return row;
|
|
1183
|
+
},
|
|
1184
|
+
async updateProductType(db, id, data) {
|
|
1185
|
+
const [row] = await db
|
|
1186
|
+
.update(productTypes)
|
|
1187
|
+
.set({ ...data, updatedAt: new Date() })
|
|
1188
|
+
.where(eq(productTypes.id, id))
|
|
1189
|
+
.returning();
|
|
1190
|
+
return row ?? null;
|
|
1191
|
+
},
|
|
1192
|
+
async deleteProductType(db, id) {
|
|
1193
|
+
const [row] = await db
|
|
1194
|
+
.delete(productTypes)
|
|
1195
|
+
.where(eq(productTypes.id, id))
|
|
1196
|
+
.returning({ id: productTypes.id });
|
|
1197
|
+
return row ?? null;
|
|
1198
|
+
},
|
|
1199
|
+
// ==========================================================================
|
|
1200
|
+
// Product Categories
|
|
1201
|
+
// ==========================================================================
|
|
1202
|
+
async listProductCategories(db, query) {
|
|
1203
|
+
const conditions = [];
|
|
1204
|
+
if (query.parentId) {
|
|
1205
|
+
conditions.push(eq(productCategories.parentId, query.parentId));
|
|
1206
|
+
}
|
|
1207
|
+
if (query.active !== undefined) {
|
|
1208
|
+
conditions.push(eq(productCategories.active, query.active));
|
|
1209
|
+
}
|
|
1210
|
+
if (query.search) {
|
|
1211
|
+
const term = `%${query.search}%`;
|
|
1212
|
+
conditions.push(or(ilike(productCategories.name, term), ilike(productCategories.slug, term)));
|
|
1213
|
+
}
|
|
1214
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
1215
|
+
const [rows, countResult] = await Promise.all([
|
|
1216
|
+
db
|
|
1217
|
+
.select()
|
|
1218
|
+
.from(productCategories)
|
|
1219
|
+
.where(where)
|
|
1220
|
+
.limit(query.limit)
|
|
1221
|
+
.offset(query.offset)
|
|
1222
|
+
.orderBy(asc(productCategories.sortOrder), asc(productCategories.name)),
|
|
1223
|
+
db.select({ count: sql `count(*)::int` }).from(productCategories).where(where),
|
|
1224
|
+
]);
|
|
1225
|
+
return {
|
|
1226
|
+
data: rows,
|
|
1227
|
+
total: countResult[0]?.count ?? 0,
|
|
1228
|
+
limit: query.limit,
|
|
1229
|
+
offset: query.offset,
|
|
1230
|
+
};
|
|
1231
|
+
},
|
|
1232
|
+
async getProductCategoryById(db, id) {
|
|
1233
|
+
const [row] = await db
|
|
1234
|
+
.select()
|
|
1235
|
+
.from(productCategories)
|
|
1236
|
+
.where(eq(productCategories.id, id))
|
|
1237
|
+
.limit(1);
|
|
1238
|
+
return row ?? null;
|
|
1239
|
+
},
|
|
1240
|
+
async createProductCategory(db, data) {
|
|
1241
|
+
const [row] = await db.insert(productCategories).values(data).returning();
|
|
1242
|
+
return row;
|
|
1243
|
+
},
|
|
1244
|
+
async updateProductCategory(db, id, data) {
|
|
1245
|
+
const [row] = await db
|
|
1246
|
+
.update(productCategories)
|
|
1247
|
+
.set({ ...data, updatedAt: new Date() })
|
|
1248
|
+
.where(eq(productCategories.id, id))
|
|
1249
|
+
.returning();
|
|
1250
|
+
return row ?? null;
|
|
1251
|
+
},
|
|
1252
|
+
async deleteProductCategory(db, id) {
|
|
1253
|
+
const [row] = await db
|
|
1254
|
+
.delete(productCategories)
|
|
1255
|
+
.where(eq(productCategories.id, id))
|
|
1256
|
+
.returning({ id: productCategories.id });
|
|
1257
|
+
return row ?? null;
|
|
1258
|
+
},
|
|
1259
|
+
// ==========================================================================
|
|
1260
|
+
// Product Tags
|
|
1261
|
+
// ==========================================================================
|
|
1262
|
+
async listProductTags(db, query) {
|
|
1263
|
+
const conditions = [];
|
|
1264
|
+
if (query.search) {
|
|
1265
|
+
const term = `%${query.search}%`;
|
|
1266
|
+
conditions.push(ilike(productTags.name, term));
|
|
1267
|
+
}
|
|
1268
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
1269
|
+
const [rows, countResult] = await Promise.all([
|
|
1270
|
+
db
|
|
1271
|
+
.select()
|
|
1272
|
+
.from(productTags)
|
|
1273
|
+
.where(where)
|
|
1274
|
+
.limit(query.limit)
|
|
1275
|
+
.offset(query.offset)
|
|
1276
|
+
.orderBy(asc(productTags.name)),
|
|
1277
|
+
db.select({ count: sql `count(*)::int` }).from(productTags).where(where),
|
|
1278
|
+
]);
|
|
1279
|
+
return {
|
|
1280
|
+
data: rows,
|
|
1281
|
+
total: countResult[0]?.count ?? 0,
|
|
1282
|
+
limit: query.limit,
|
|
1283
|
+
offset: query.offset,
|
|
1284
|
+
};
|
|
1285
|
+
},
|
|
1286
|
+
async getProductTagById(db, id) {
|
|
1287
|
+
const [row] = await db.select().from(productTags).where(eq(productTags.id, id)).limit(1);
|
|
1288
|
+
return row ?? null;
|
|
1289
|
+
},
|
|
1290
|
+
async createProductTag(db, data) {
|
|
1291
|
+
const [row] = await db.insert(productTags).values(data).returning();
|
|
1292
|
+
return row;
|
|
1293
|
+
},
|
|
1294
|
+
async updateProductTag(db, id, data) {
|
|
1295
|
+
const [row] = await db
|
|
1296
|
+
.update(productTags)
|
|
1297
|
+
.set({ ...data, updatedAt: new Date() })
|
|
1298
|
+
.where(eq(productTags.id, id))
|
|
1299
|
+
.returning();
|
|
1300
|
+
return row ?? null;
|
|
1301
|
+
},
|
|
1302
|
+
async deleteProductTag(db, id) {
|
|
1303
|
+
const [row] = await db
|
|
1304
|
+
.delete(productTags)
|
|
1305
|
+
.where(eq(productTags.id, id))
|
|
1306
|
+
.returning({ id: productTags.id });
|
|
1307
|
+
return row ?? null;
|
|
1308
|
+
},
|
|
1309
|
+
// ==========================================================================
|
|
1310
|
+
// Product <-> Category associations
|
|
1311
|
+
// ==========================================================================
|
|
1312
|
+
async addProductToCategory(db, productId, categoryId, sortOrder = 0) {
|
|
1313
|
+
const [row] = await db
|
|
1314
|
+
.insert(productCategoryProducts)
|
|
1315
|
+
.values({ productId, categoryId, sortOrder })
|
|
1316
|
+
.onConflictDoNothing()
|
|
1317
|
+
.returning();
|
|
1318
|
+
return row ?? null;
|
|
1319
|
+
},
|
|
1320
|
+
async removeProductFromCategory(db, productId, categoryId) {
|
|
1321
|
+
const [row] = await db
|
|
1322
|
+
.delete(productCategoryProducts)
|
|
1323
|
+
.where(and(eq(productCategoryProducts.productId, productId), eq(productCategoryProducts.categoryId, categoryId)))
|
|
1324
|
+
.returning({ productId: productCategoryProducts.productId });
|
|
1325
|
+
return row ?? null;
|
|
1326
|
+
},
|
|
1327
|
+
async listProductCategories_(db, productId) {
|
|
1328
|
+
const rows = await db
|
|
1329
|
+
.select({ category: productCategories })
|
|
1330
|
+
.from(productCategoryProducts)
|
|
1331
|
+
.innerJoin(productCategories, eq(productCategoryProducts.categoryId, productCategories.id))
|
|
1332
|
+
.where(eq(productCategoryProducts.productId, productId))
|
|
1333
|
+
.orderBy(asc(productCategoryProducts.sortOrder));
|
|
1334
|
+
return rows.map((r) => r.category);
|
|
1335
|
+
},
|
|
1336
|
+
// ==========================================================================
|
|
1337
|
+
// Product <-> Tag associations
|
|
1338
|
+
// ==========================================================================
|
|
1339
|
+
async addProductTag(db, productId, tagId) {
|
|
1340
|
+
const [row] = await db
|
|
1341
|
+
.insert(productTagProducts)
|
|
1342
|
+
.values({ productId, tagId })
|
|
1343
|
+
.onConflictDoNothing()
|
|
1344
|
+
.returning();
|
|
1345
|
+
return row ?? null;
|
|
1346
|
+
},
|
|
1347
|
+
async removeProductTag(db, productId, tagId) {
|
|
1348
|
+
const [row] = await db
|
|
1349
|
+
.delete(productTagProducts)
|
|
1350
|
+
.where(and(eq(productTagProducts.productId, productId), eq(productTagProducts.tagId, tagId)))
|
|
1351
|
+
.returning({ productId: productTagProducts.productId });
|
|
1352
|
+
return row ?? null;
|
|
1353
|
+
},
|
|
1354
|
+
async listProductTags_(db, productId) {
|
|
1355
|
+
const rows = await db
|
|
1356
|
+
.select({ tag: productTags })
|
|
1357
|
+
.from(productTagProducts)
|
|
1358
|
+
.innerJoin(productTags, eq(productTagProducts.tagId, productTags.id))
|
|
1359
|
+
.where(eq(productTagProducts.productId, productId))
|
|
1360
|
+
.orderBy(asc(productTags.name));
|
|
1361
|
+
return rows.map((r) => r.tag);
|
|
1362
|
+
},
|
|
1363
|
+
// ==========================================================================
|
|
1364
|
+
// Product Media
|
|
1365
|
+
// ==========================================================================
|
|
1366
|
+
async listMedia(db, productId, query) {
|
|
1367
|
+
const conditions = [eq(productMedia.productId, productId)];
|
|
1368
|
+
if (query.dayId !== undefined) {
|
|
1369
|
+
conditions.push(eq(productMedia.dayId, query.dayId));
|
|
1370
|
+
}
|
|
1371
|
+
if (query.mediaType) {
|
|
1372
|
+
conditions.push(eq(productMedia.mediaType, query.mediaType));
|
|
1373
|
+
}
|
|
1374
|
+
const where = and(...conditions);
|
|
1375
|
+
const [rows, countResult] = await Promise.all([
|
|
1376
|
+
db
|
|
1377
|
+
.select()
|
|
1378
|
+
.from(productMedia)
|
|
1379
|
+
.where(where)
|
|
1380
|
+
.limit(query.limit)
|
|
1381
|
+
.offset(query.offset)
|
|
1382
|
+
.orderBy(desc(productMedia.isCover), asc(productMedia.sortOrder), asc(productMedia.createdAt)),
|
|
1383
|
+
db.select({ count: sql `count(*)::int` }).from(productMedia).where(where),
|
|
1384
|
+
]);
|
|
1385
|
+
return {
|
|
1386
|
+
data: rows,
|
|
1387
|
+
total: countResult[0]?.count ?? 0,
|
|
1388
|
+
limit: query.limit,
|
|
1389
|
+
offset: query.offset,
|
|
1390
|
+
};
|
|
1391
|
+
},
|
|
1392
|
+
async listProductLevelMedia(db, productId, query) {
|
|
1393
|
+
const conditions = [
|
|
1394
|
+
eq(productMedia.productId, productId),
|
|
1395
|
+
sql `${productMedia.dayId} is null`,
|
|
1396
|
+
];
|
|
1397
|
+
if (query.mediaType) {
|
|
1398
|
+
conditions.push(eq(productMedia.mediaType, query.mediaType));
|
|
1399
|
+
}
|
|
1400
|
+
const where = and(...conditions);
|
|
1401
|
+
const [rows, countResult] = await Promise.all([
|
|
1402
|
+
db
|
|
1403
|
+
.select()
|
|
1404
|
+
.from(productMedia)
|
|
1405
|
+
.where(where)
|
|
1406
|
+
.limit(query.limit)
|
|
1407
|
+
.offset(query.offset)
|
|
1408
|
+
.orderBy(desc(productMedia.isCover), asc(productMedia.sortOrder), asc(productMedia.createdAt)),
|
|
1409
|
+
db.select({ count: sql `count(*)::int` }).from(productMedia).where(where),
|
|
1410
|
+
]);
|
|
1411
|
+
return {
|
|
1412
|
+
data: rows,
|
|
1413
|
+
total: countResult[0]?.count ?? 0,
|
|
1414
|
+
limit: query.limit,
|
|
1415
|
+
offset: query.offset,
|
|
1416
|
+
};
|
|
1417
|
+
},
|
|
1418
|
+
async getMediaById(db, id) {
|
|
1419
|
+
const [row] = await db.select().from(productMedia).where(eq(productMedia.id, id)).limit(1);
|
|
1420
|
+
return row ?? null;
|
|
1421
|
+
},
|
|
1422
|
+
async createMedia(db, productId, data) {
|
|
1423
|
+
const product = await ensureProductExists(db, productId);
|
|
1424
|
+
if (!product) {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
if (data.dayId) {
|
|
1428
|
+
const [day] = await db
|
|
1429
|
+
.select({ id: productDays.id, productId: productDays.productId })
|
|
1430
|
+
.from(productDays)
|
|
1431
|
+
.where(eq(productDays.id, data.dayId))
|
|
1432
|
+
.limit(1);
|
|
1433
|
+
if (!day || day.productId !== productId) {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const [row] = await db
|
|
1438
|
+
.insert(productMedia)
|
|
1439
|
+
.values({
|
|
1440
|
+
productId,
|
|
1441
|
+
dayId: data.dayId ?? null,
|
|
1442
|
+
mediaType: data.mediaType,
|
|
1443
|
+
name: data.name,
|
|
1444
|
+
url: data.url,
|
|
1445
|
+
storageKey: data.storageKey ?? null,
|
|
1446
|
+
mimeType: data.mimeType ?? null,
|
|
1447
|
+
fileSize: data.fileSize ?? null,
|
|
1448
|
+
altText: data.altText ?? null,
|
|
1449
|
+
sortOrder: data.sortOrder,
|
|
1450
|
+
isCover: data.isCover,
|
|
1451
|
+
})
|
|
1452
|
+
.returning();
|
|
1453
|
+
return row ?? null;
|
|
1454
|
+
},
|
|
1455
|
+
async updateMedia(db, id, data) {
|
|
1456
|
+
const [row] = await db
|
|
1457
|
+
.update(productMedia)
|
|
1458
|
+
.set({ ...data, updatedAt: new Date() })
|
|
1459
|
+
.where(eq(productMedia.id, id))
|
|
1460
|
+
.returning();
|
|
1461
|
+
return row ?? null;
|
|
1462
|
+
},
|
|
1463
|
+
async deleteMedia(db, id) {
|
|
1464
|
+
const [row] = await db
|
|
1465
|
+
.delete(productMedia)
|
|
1466
|
+
.where(eq(productMedia.id, id))
|
|
1467
|
+
.returning();
|
|
1468
|
+
return row ?? null;
|
|
1469
|
+
},
|
|
1470
|
+
async setCoverMedia(db, productId, mediaId, dayId) {
|
|
1471
|
+
// Unset existing cover in the same scope (product-level or day-level)
|
|
1472
|
+
const scopeConditions = [eq(productMedia.productId, productId)];
|
|
1473
|
+
if (dayId) {
|
|
1474
|
+
scopeConditions.push(eq(productMedia.dayId, dayId));
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
scopeConditions.push(sql `${productMedia.dayId} is null`);
|
|
1478
|
+
}
|
|
1479
|
+
await db
|
|
1480
|
+
.update(productMedia)
|
|
1481
|
+
.set({ isCover: false, updatedAt: new Date() })
|
|
1482
|
+
.where(and(...scopeConditions));
|
|
1483
|
+
const [row] = await db
|
|
1484
|
+
.update(productMedia)
|
|
1485
|
+
.set({ isCover: true, updatedAt: new Date() })
|
|
1486
|
+
.where(eq(productMedia.id, mediaId))
|
|
1487
|
+
.returning();
|
|
1488
|
+
return row ?? null;
|
|
1489
|
+
},
|
|
1490
|
+
async reorderMedia(db, data) {
|
|
1491
|
+
const results = await Promise.all(data.items.map(async (item) => {
|
|
1492
|
+
const [row] = await db
|
|
1493
|
+
.update(productMedia)
|
|
1494
|
+
.set({ sortOrder: item.sortOrder, updatedAt: new Date() })
|
|
1495
|
+
.where(eq(productMedia.id, item.id))
|
|
1496
|
+
.returning({ id: productMedia.id });
|
|
1497
|
+
return row;
|
|
1498
|
+
}));
|
|
1499
|
+
return results.filter((r) => r != null);
|
|
1500
|
+
},
|
|
1501
|
+
};
|