@86d-app/products 0.0.4 → 0.0.13

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 (196) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/AGENTS.md +41 -41
  3. package/README.md +266 -5
  4. package/dist/__tests__/controllers.test.d.ts +2 -0
  5. package/dist/__tests__/controllers.test.d.ts.map +1 -0
  6. package/dist/__tests__/endpoint-security.test.d.ts +2 -0
  7. package/dist/__tests__/endpoint-security.test.d.ts.map +1 -0
  8. package/dist/__tests__/service-impl.test.d.ts +2 -0
  9. package/dist/__tests__/service-impl.test.d.ts.map +1 -0
  10. package/dist/__tests__/state.test.d.ts +2 -0
  11. package/dist/__tests__/state.test.d.ts.map +1 -0
  12. package/dist/admin/components/categories-admin.d.ts +2 -0
  13. package/dist/admin/components/categories-admin.d.ts.map +1 -0
  14. package/dist/admin/components/category-form.d.ts +7 -0
  15. package/dist/admin/components/category-form.d.ts.map +1 -0
  16. package/dist/admin/components/category-list.d.ts +7 -0
  17. package/dist/admin/components/category-list.d.ts.map +1 -0
  18. package/dist/admin/components/collections-admin.d.ts +2 -0
  19. package/dist/admin/components/collections-admin.d.ts.map +1 -0
  20. package/dist/admin/components/index.d.ts +9 -0
  21. package/dist/admin/components/index.d.ts.map +1 -0
  22. package/dist/admin/components/product-detail.d.ts +7 -0
  23. package/dist/admin/components/product-detail.d.ts.map +1 -0
  24. package/dist/admin/components/product-edit.d.ts +6 -0
  25. package/dist/admin/components/product-edit.d.ts.map +1 -0
  26. package/dist/admin/components/product-form.d.ts +7 -0
  27. package/dist/admin/components/product-form.d.ts.map +1 -0
  28. package/dist/admin/components/product-list.d.ts +2 -0
  29. package/dist/admin/components/product-list.d.ts.map +1 -0
  30. package/dist/admin/components/product-new.d.ts +2 -0
  31. package/dist/admin/components/product-new.d.ts.map +1 -0
  32. package/dist/admin/endpoints/add-collection-product.d.ts +15 -0
  33. package/dist/admin/endpoints/add-collection-product.d.ts.map +1 -0
  34. package/dist/admin/endpoints/bulk-action.d.ts +17 -0
  35. package/dist/admin/endpoints/bulk-action.d.ts.map +1 -0
  36. package/dist/admin/endpoints/create-category.d.ts +23 -0
  37. package/dist/admin/endpoints/create-category.d.ts.map +1 -0
  38. package/dist/admin/endpoints/create-collection.d.ts +22 -0
  39. package/dist/admin/endpoints/create-collection.d.ts.map +1 -0
  40. package/dist/admin/endpoints/create-product.d.ts +44 -0
  41. package/dist/admin/endpoints/create-product.d.ts.map +1 -0
  42. package/dist/admin/endpoints/create-variant.d.ts +35 -0
  43. package/dist/admin/endpoints/create-variant.d.ts.map +1 -0
  44. package/dist/admin/endpoints/delete-category.d.ts +18 -0
  45. package/dist/admin/endpoints/delete-category.d.ts.map +1 -0
  46. package/dist/admin/endpoints/delete-collection.d.ts +8 -0
  47. package/dist/admin/endpoints/delete-collection.d.ts.map +1 -0
  48. package/dist/admin/endpoints/delete-product.d.ts +18 -0
  49. package/dist/admin/endpoints/delete-product.d.ts.map +1 -0
  50. package/dist/admin/endpoints/delete-variant.d.ts +18 -0
  51. package/dist/admin/endpoints/delete-variant.d.ts.map +1 -0
  52. package/dist/admin/endpoints/get-product.d.ts +16 -0
  53. package/dist/admin/endpoints/get-product.d.ts.map +1 -0
  54. package/dist/admin/endpoints/import-products.d.ts +36 -0
  55. package/dist/admin/endpoints/import-products.d.ts.map +1 -0
  56. package/dist/admin/endpoints/index.d.ts +418 -0
  57. package/dist/admin/endpoints/index.d.ts.map +1 -0
  58. package/dist/admin/endpoints/list-categories.d.ts +11 -0
  59. package/dist/admin/endpoints/list-categories.d.ts.map +1 -0
  60. package/dist/admin/endpoints/list-collections.d.ts +11 -0
  61. package/dist/admin/endpoints/list-collections.d.ts.map +1 -0
  62. package/dist/admin/endpoints/list-products.d.ts +27 -0
  63. package/dist/admin/endpoints/list-products.d.ts.map +1 -0
  64. package/dist/admin/endpoints/remove-collection-product.d.ts +9 -0
  65. package/dist/admin/endpoints/remove-collection-product.d.ts.map +1 -0
  66. package/dist/admin/endpoints/update-category.d.ts +26 -0
  67. package/dist/admin/endpoints/update-category.d.ts.map +1 -0
  68. package/dist/admin/endpoints/update-collection.d.ts +19 -0
  69. package/dist/admin/endpoints/update-collection.d.ts.map +1 -0
  70. package/dist/admin/endpoints/update-product.d.ts +47 -0
  71. package/dist/admin/endpoints/update-product.d.ts.map +1 -0
  72. package/dist/admin/endpoints/update-variant.d.ts +35 -0
  73. package/dist/admin/endpoints/update-variant.d.ts.map +1 -0
  74. package/dist/controllers.d.ts +130 -0
  75. package/dist/controllers.d.ts.map +1 -0
  76. package/dist/index.d.ts +20 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/markdown.d.ts +6 -0
  79. package/dist/markdown.d.ts.map +1 -0
  80. package/dist/schema.d.ts +351 -0
  81. package/dist/schema.d.ts.map +1 -0
  82. package/dist/service-impl.d.ts +4 -0
  83. package/dist/service-impl.d.ts.map +1 -0
  84. package/dist/service.d.ts +280 -0
  85. package/dist/service.d.ts.map +1 -0
  86. package/dist/state.d.ts +38 -0
  87. package/dist/state.d.ts.map +1 -0
  88. package/dist/store/components/_hooks.d.ts +88 -0
  89. package/dist/store/components/_hooks.d.ts.map +1 -0
  90. package/dist/store/components/_types.d.ts +70 -0
  91. package/dist/store/components/_types.d.ts.map +1 -0
  92. package/dist/store/components/_utils.d.ts +5 -0
  93. package/dist/store/components/_utils.d.ts.map +1 -0
  94. package/dist/store/components/back-in-stock-notify.d.ts +8 -0
  95. package/dist/store/components/back-in-stock-notify.d.ts.map +1 -0
  96. package/dist/store/components/collection-card.d.ts +6 -0
  97. package/dist/store/components/collection-card.d.ts.map +1 -0
  98. package/dist/store/components/collection-detail.d.ts +6 -0
  99. package/dist/store/components/collection-detail.d.ts.map +1 -0
  100. package/dist/store/components/collection-grid.d.ts +6 -0
  101. package/dist/store/components/collection-grid.d.ts.map +1 -0
  102. package/dist/store/components/featured-products.d.ts +6 -0
  103. package/dist/store/components/featured-products.d.ts.map +1 -0
  104. package/dist/store/components/filter-chip.d.ts +6 -0
  105. package/dist/store/components/filter-chip.d.ts.map +1 -0
  106. package/dist/store/components/index.d.ts +37 -0
  107. package/dist/store/components/index.d.ts.map +1 -0
  108. package/dist/store/components/product-card.d.ts +7 -0
  109. package/dist/store/components/product-card.d.ts.map +1 -0
  110. package/dist/store/components/product-detail.d.ts +6 -0
  111. package/dist/store/components/product-detail.d.ts.map +1 -0
  112. package/dist/store/components/product-listing.d.ts +7 -0
  113. package/dist/store/components/product-listing.d.ts.map +1 -0
  114. package/dist/store/components/product-qa-section.d.ts +5 -0
  115. package/dist/store/components/product-qa-section.d.ts.map +1 -0
  116. package/dist/store/components/product-reviews-section.d.ts +5 -0
  117. package/dist/store/components/product-reviews-section.d.ts.map +1 -0
  118. package/dist/store/components/recently-viewed.d.ts +12 -0
  119. package/dist/store/components/recently-viewed.d.ts.map +1 -0
  120. package/dist/store/components/recommended-products.d.ts +7 -0
  121. package/dist/store/components/recommended-products.d.ts.map +1 -0
  122. package/dist/store/components/related-products.d.ts +7 -0
  123. package/dist/store/components/related-products.d.ts.map +1 -0
  124. package/dist/store/components/star-display.d.ts +6 -0
  125. package/dist/store/components/star-display.d.ts.map +1 -0
  126. package/dist/store/components/star-picker.d.ts +6 -0
  127. package/dist/store/components/star-picker.d.ts.map +1 -0
  128. package/dist/store/components/stock-badge.d.ts +5 -0
  129. package/dist/store/components/stock-badge.d.ts.map +1 -0
  130. package/dist/store/endpoints/get-category.d.ts +22 -0
  131. package/dist/store/endpoints/get-category.d.ts.map +1 -0
  132. package/dist/store/endpoints/get-collection.d.ts +17 -0
  133. package/dist/store/endpoints/get-collection.d.ts.map +1 -0
  134. package/dist/store/endpoints/get-featured.d.ts +10 -0
  135. package/dist/store/endpoints/get-featured.d.ts.map +1 -0
  136. package/dist/store/endpoints/get-product.d.ts +17 -0
  137. package/dist/store/endpoints/get-product.d.ts.map +1 -0
  138. package/dist/store/endpoints/get-related.d.ts +11 -0
  139. package/dist/store/endpoints/get-related.d.ts.map +1 -0
  140. package/dist/store/endpoints/index.d.ts +129 -0
  141. package/dist/store/endpoints/index.d.ts.map +1 -0
  142. package/dist/store/endpoints/list-categories.d.ts +6 -0
  143. package/dist/store/endpoints/list-categories.d.ts.map +1 -0
  144. package/dist/store/endpoints/list-collections.d.ts +10 -0
  145. package/dist/store/endpoints/list-collections.d.ts.map +1 -0
  146. package/dist/store/endpoints/list-products.d.ts +26 -0
  147. package/dist/store/endpoints/list-products.d.ts.map +1 -0
  148. package/dist/store/endpoints/search-products.d.ts +11 -0
  149. package/dist/store/endpoints/search-products.d.ts.map +1 -0
  150. package/dist/store/endpoints/store-search.d.ts +18 -0
  151. package/dist/store/endpoints/store-search.d.ts.map +1 -0
  152. package/package.json +3 -3
  153. package/src/__tests__/endpoint-security.test.ts +457 -0
  154. package/src/__tests__/service-impl.test.ts +1745 -0
  155. package/src/admin/endpoints/create-category.ts +5 -2
  156. package/src/admin/endpoints/create-collection.ts +1 -1
  157. package/src/admin/endpoints/create-product.ts +5 -2
  158. package/src/admin/endpoints/delete-category.ts +1 -1
  159. package/src/admin/endpoints/delete-collection.ts +1 -1
  160. package/src/admin/endpoints/delete-product.ts +1 -1
  161. package/src/admin/endpoints/delete-variant.ts +1 -1
  162. package/src/admin/endpoints/list-categories.ts +1 -1
  163. package/src/admin/endpoints/list-collections.ts +1 -1
  164. package/src/admin/endpoints/list-products.ts +1 -1
  165. package/src/admin/endpoints/remove-collection-product.ts +1 -1
  166. package/src/admin/endpoints/update-category.ts +5 -2
  167. package/src/admin/endpoints/update-collection.ts +1 -1
  168. package/src/admin/endpoints/update-product.ts +5 -2
  169. package/src/admin/endpoints/update-variant.ts +1 -1
  170. package/src/service-impl.ts +1139 -0
  171. package/src/service.ts +312 -0
  172. package/src/store/components/_hooks.ts +81 -0
  173. package/src/store/components/_utils.ts +8 -0
  174. package/src/store/components/collection-detail.tsx +21 -1
  175. package/src/store/components/collection-grid.tsx +5 -1
  176. package/src/store/components/featured-products.tsx +5 -1
  177. package/src/store/components/index.tsx +2 -0
  178. package/src/store/components/product-card.mdx +1 -1
  179. package/src/store/components/product-card.tsx +25 -5
  180. package/src/store/components/product-detail.mdx +2 -0
  181. package/src/store/components/product-detail.tsx +55 -8
  182. package/src/store/components/product-listing.tsx +25 -4
  183. package/src/store/components/product-qa-section.mdx +21 -0
  184. package/src/store/components/product-qa-section.tsx +503 -0
  185. package/src/store/components/recommended-products.mdx +6 -0
  186. package/src/store/components/recommended-products.tsx +119 -0
  187. package/src/store/endpoints/get-category.ts +2 -2
  188. package/src/store/endpoints/get-collection.ts +1 -1
  189. package/src/store/endpoints/get-featured.ts +1 -1
  190. package/src/store/endpoints/get-product.ts +1 -1
  191. package/src/store/endpoints/get-related.ts +2 -2
  192. package/src/store/endpoints/list-collections.ts +3 -3
  193. package/src/store/endpoints/list-products.ts +9 -9
  194. package/src/store/endpoints/search-products.ts +4 -6
  195. package/src/store/endpoints/store-search.ts +1 -1
  196. package/COMPONENTS.md +0 -231
@@ -0,0 +1,1139 @@
1
+ import type { ModuleDataService } from "@86d-app/core";
2
+ import type {
3
+ Category,
4
+ CategoryWithChildren,
5
+ Collection,
6
+ CollectionProduct,
7
+ CollectionWithProducts,
8
+ CreateCategoryParams,
9
+ CreateCollectionParams,
10
+ CreateProductParams,
11
+ CreateVariantParams,
12
+ ImportError,
13
+ ImportProductRow,
14
+ ImportResult,
15
+ ListProductsParams,
16
+ Product,
17
+ ProductController,
18
+ ProductVariant,
19
+ ProductWithVariants,
20
+ } from "./service";
21
+
22
+ function generateSlug(name: string): string {
23
+ return name
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, "-")
26
+ .replace(/^-+|-+$/g, "");
27
+ }
28
+
29
+ export function createProductController(
30
+ data: ModuleDataService,
31
+ ): ProductController {
32
+ return {
33
+ // ── Products ──────────────────────────────────────────────────────
34
+
35
+ async createProduct(params: CreateProductParams): Promise<Product> {
36
+ const now = new Date();
37
+ const id = `prod_${crypto.randomUUID()}`;
38
+
39
+ const product: Product = {
40
+ id,
41
+ name: params.name,
42
+ slug: params.slug,
43
+ description: params.description,
44
+ shortDescription: params.shortDescription,
45
+ price: params.price,
46
+ compareAtPrice: params.compareAtPrice,
47
+ costPrice: params.costPrice,
48
+ sku: params.sku,
49
+ barcode: params.barcode,
50
+ inventory: params.inventory ?? 0,
51
+ trackInventory: params.trackInventory ?? true,
52
+ allowBackorder: params.allowBackorder ?? false,
53
+ status: params.status ?? "draft",
54
+ categoryId: params.categoryId,
55
+ images: params.images ?? [],
56
+ tags: params.tags ?? [],
57
+ metadata: params.metadata ?? {},
58
+ weight: params.weight,
59
+ weightUnit: params.weightUnit ?? "kg",
60
+ isFeatured: params.isFeatured ?? false,
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ };
64
+
65
+ await data.upsert(
66
+ "product",
67
+ id,
68
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
69
+ product as unknown as Record<string, any>,
70
+ );
71
+ return product;
72
+ },
73
+
74
+ async getProduct(id: string): Promise<Product | null> {
75
+ return (await data.get("product", id)) as Product | null;
76
+ },
77
+
78
+ async getProductBySlug(slug: string): Promise<Product | null> {
79
+ const products = (await data.findMany("product", {
80
+ where: { slug },
81
+ })) as Product[];
82
+ return products[0] ?? null;
83
+ },
84
+
85
+ async getProductWithVariants(
86
+ id: string,
87
+ ): Promise<ProductWithVariants | null> {
88
+ const product = (await data.get("product", id)) as Product | null;
89
+ if (!product) return null;
90
+
91
+ const variants = (await data.findMany("productVariant", {
92
+ where: { productId: id },
93
+ })) as ProductVariant[];
94
+
95
+ let category: Category | undefined;
96
+ if (product.categoryId) {
97
+ category = (await data.get("category", product.categoryId)) as
98
+ | Category
99
+ | undefined;
100
+ }
101
+
102
+ return { ...product, variants, category };
103
+ },
104
+
105
+ async listProducts(params?: ListProductsParams) {
106
+ const page = params?.page ?? 1;
107
+ const limit = params?.limit ?? 20;
108
+
109
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
110
+ const where: Record<string, any> = {};
111
+ if (params?.category) where.categoryId = params.category;
112
+ if (params?.status) where.status = params.status;
113
+ if (params?.featured) where.isFeatured = true;
114
+
115
+ let allProducts = (await data.findMany("product", {
116
+ where,
117
+ orderBy: params?.sort
118
+ ? { [params.sort]: (params.order ?? "desc") as "asc" | "desc" }
119
+ : { createdAt: "desc" as const },
120
+ })) as Product[];
121
+
122
+ if (params?.minPrice !== undefined) {
123
+ const min = params.minPrice;
124
+ allProducts = allProducts.filter((p) => p.price >= min);
125
+ }
126
+ if (params?.maxPrice !== undefined) {
127
+ const max = params.maxPrice;
128
+ allProducts = allProducts.filter((p) => p.price <= max);
129
+ }
130
+ if (params?.inStock) {
131
+ allProducts = allProducts.filter((p) => p.inventory > 0);
132
+ }
133
+ if (params?.tag) {
134
+ const tagLower = params.tag.toLowerCase();
135
+ allProducts = allProducts.filter((p) =>
136
+ p.tags.some((t) => t.toLowerCase() === tagLower),
137
+ );
138
+ }
139
+ if (params?.search) {
140
+ const searchLower = params.search.toLowerCase();
141
+ allProducts = allProducts.filter(
142
+ (p) =>
143
+ p.name.toLowerCase().includes(searchLower) ||
144
+ p.description?.toLowerCase().includes(searchLower) ||
145
+ p.tags.some((t) => t.toLowerCase().includes(searchLower)),
146
+ );
147
+ }
148
+
149
+ const total = allProducts.length;
150
+ const paged = allProducts.slice((page - 1) * limit, page * limit);
151
+
152
+ const productsWithVariants: ProductWithVariants[] = await Promise.all(
153
+ paged.map(async (product) => {
154
+ const variants = (await data.findMany("productVariant", {
155
+ where: { productId: product.id },
156
+ })) as ProductVariant[];
157
+
158
+ let category: Category | undefined;
159
+ if (product.categoryId) {
160
+ category = (await data.get("category", product.categoryId)) as
161
+ | Category
162
+ | undefined;
163
+ }
164
+
165
+ return { ...product, variants, category };
166
+ }),
167
+ );
168
+
169
+ return { products: productsWithVariants, total, page, limit };
170
+ },
171
+
172
+ async searchProducts(q: string, limit?: number): Promise<Product[]> {
173
+ const products = (await data.findMany("product", {
174
+ where: { status: "active" },
175
+ })) as Product[];
176
+
177
+ const queryLower = q.toLowerCase();
178
+ const results = products.filter(
179
+ (p) =>
180
+ p.name.toLowerCase().includes(queryLower) ||
181
+ p.description?.toLowerCase().includes(queryLower) ||
182
+ p.tags.some((t) => t.toLowerCase().includes(queryLower)),
183
+ );
184
+
185
+ return results.slice(0, limit ?? 20);
186
+ },
187
+
188
+ async getFeaturedProducts(limit?: number): Promise<Product[]> {
189
+ return (await data.findMany("product", {
190
+ where: { isFeatured: true, status: "active" },
191
+ take: limit ?? 10,
192
+ })) as Product[];
193
+ },
194
+
195
+ async getProductsByCategory(categoryId: string): Promise<Product[]> {
196
+ return (await data.findMany("product", {
197
+ where: { categoryId, status: "active" },
198
+ })) as Product[];
199
+ },
200
+
201
+ async getRelatedProducts(
202
+ id: string,
203
+ limit?: number,
204
+ ): Promise<{ products: Product[] }> {
205
+ const maxResults = limit ?? 4;
206
+ const product = (await data.get("product", id)) as Product | null;
207
+ if (!product) return { products: [] };
208
+
209
+ const all = (
210
+ (await data.findMany("product", {
211
+ where: { status: "active" },
212
+ })) as Product[]
213
+ ).filter((p) => p.id !== id);
214
+
215
+ const scored = all.map((p) => {
216
+ let score = 0;
217
+ if (product.categoryId && p.categoryId === product.categoryId) {
218
+ score += 10;
219
+ }
220
+ const sharedTags = p.tags.filter((t) => product.tags.includes(t));
221
+ score += sharedTags.length;
222
+ return { product: p, score };
223
+ });
224
+
225
+ scored.sort((a, b) => b.score - a.score);
226
+ return { products: scored.slice(0, maxResults).map((s) => s.product) };
227
+ },
228
+
229
+ async updateProduct(
230
+ id: string,
231
+ params: Partial<Product>,
232
+ ): Promise<Product> {
233
+ const existing = (await data.get("product", id)) as Product | null;
234
+ if (!existing) throw new Error(`Product ${id} not found`);
235
+
236
+ const updated: Product = {
237
+ ...existing,
238
+ ...params,
239
+ updatedAt: new Date(),
240
+ };
241
+ await data.upsert(
242
+ "product",
243
+ id,
244
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
245
+ updated as unknown as Record<string, any>,
246
+ );
247
+ return updated;
248
+ },
249
+
250
+ async deleteProduct(id: string): Promise<{ success: boolean }> {
251
+ const variants = (await data.findMany("productVariant", {
252
+ where: { productId: id },
253
+ })) as ProductVariant[];
254
+
255
+ for (const variant of variants) {
256
+ await data.delete("productVariant", variant.id);
257
+ }
258
+
259
+ await data.delete("product", id);
260
+ return { success: true };
261
+ },
262
+
263
+ // ── Inventory ─────────────────────────────────────────────────────
264
+
265
+ async checkAvailability(
266
+ productId: string,
267
+ variantId?: string,
268
+ quantity?: number,
269
+ ) {
270
+ const qty = quantity ?? 1;
271
+
272
+ if (variantId) {
273
+ const variant = (await data.get(
274
+ "productVariant",
275
+ variantId,
276
+ )) as ProductVariant | null;
277
+ if (!variant)
278
+ return { available: false, inventory: 0, allowBackorder: false };
279
+
280
+ const product = (await data.get(
281
+ "product",
282
+ productId,
283
+ )) as Product | null;
284
+ const allowBackorder = product?.allowBackorder ?? false;
285
+
286
+ return {
287
+ available: variant.inventory >= qty || allowBackorder,
288
+ inventory: variant.inventory,
289
+ allowBackorder,
290
+ };
291
+ }
292
+
293
+ const product = (await data.get("product", productId)) as Product | null;
294
+ if (!product)
295
+ return { available: false, inventory: 0, allowBackorder: false };
296
+
297
+ if (!product.trackInventory) {
298
+ return {
299
+ available: true,
300
+ inventory: product.inventory,
301
+ allowBackorder: product.allowBackorder,
302
+ };
303
+ }
304
+
305
+ return {
306
+ available: product.inventory >= qty || product.allowBackorder,
307
+ inventory: product.inventory,
308
+ allowBackorder: product.allowBackorder,
309
+ };
310
+ },
311
+
312
+ async decrementInventory(
313
+ productId: string,
314
+ quantity: number,
315
+ variantId?: string,
316
+ ) {
317
+ if (variantId) {
318
+ const variant = (await data.get(
319
+ "productVariant",
320
+ variantId,
321
+ )) as ProductVariant | null;
322
+ if (variant) {
323
+ await data.upsert("productVariant", variantId, {
324
+ ...variant,
325
+ inventory: variant.inventory - quantity,
326
+ updatedAt: new Date(),
327
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
328
+ } as Record<string, any>);
329
+ }
330
+ } else {
331
+ const product = (await data.get(
332
+ "product",
333
+ productId,
334
+ )) as Product | null;
335
+ if (product?.trackInventory) {
336
+ await data.upsert("product", productId, {
337
+ ...product,
338
+ inventory: product.inventory - quantity,
339
+ updatedAt: new Date(),
340
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
341
+ } as Record<string, any>);
342
+ }
343
+ }
344
+ return { success: true };
345
+ },
346
+
347
+ async incrementInventory(
348
+ productId: string,
349
+ quantity: number,
350
+ variantId?: string,
351
+ ) {
352
+ if (variantId) {
353
+ const variant = (await data.get(
354
+ "productVariant",
355
+ variantId,
356
+ )) as ProductVariant | null;
357
+ if (variant) {
358
+ await data.upsert("productVariant", variantId, {
359
+ ...variant,
360
+ inventory: variant.inventory + quantity,
361
+ updatedAt: new Date(),
362
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
363
+ } as Record<string, any>);
364
+ }
365
+ } else {
366
+ const product = (await data.get(
367
+ "product",
368
+ productId,
369
+ )) as Product | null;
370
+ if (product?.trackInventory) {
371
+ await data.upsert("product", productId, {
372
+ ...product,
373
+ inventory: product.inventory + quantity,
374
+ updatedAt: new Date(),
375
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
376
+ } as Record<string, any>);
377
+ }
378
+ }
379
+ return { success: true };
380
+ },
381
+
382
+ // ── Variants ──────────────────────────────────────────────────────
383
+
384
+ async getVariant(id: string): Promise<ProductVariant | null> {
385
+ return (await data.get("productVariant", id)) as ProductVariant | null;
386
+ },
387
+
388
+ async getVariantsByProduct(productId: string): Promise<ProductVariant[]> {
389
+ const variants = (await data.findMany("productVariant", {
390
+ where: { productId },
391
+ })) as ProductVariant[];
392
+ return variants.sort((a, b) => a.position - b.position);
393
+ },
394
+
395
+ async createVariant(params: CreateVariantParams): Promise<ProductVariant> {
396
+ const now = new Date();
397
+ const id = `var_${crypto.randomUUID()}`;
398
+
399
+ const variant: ProductVariant = {
400
+ id,
401
+ productId: params.productId,
402
+ name: params.name,
403
+ sku: params.sku,
404
+ barcode: params.barcode,
405
+ price: params.price,
406
+ compareAtPrice: params.compareAtPrice,
407
+ costPrice: params.costPrice,
408
+ inventory: params.inventory ?? 0,
409
+ options: params.options,
410
+ images: params.images ?? [],
411
+ weight: params.weight,
412
+ weightUnit: params.weightUnit,
413
+ position: params.position ?? 0,
414
+ createdAt: now,
415
+ updatedAt: now,
416
+ };
417
+
418
+ await data.upsert(
419
+ "productVariant",
420
+ id,
421
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
422
+ variant as unknown as Record<string, any>,
423
+ );
424
+
425
+ // Update product timestamp
426
+ const product = (await data.get(
427
+ "product",
428
+ params.productId,
429
+ )) as Product | null;
430
+ if (product) {
431
+ await data.upsert("product", params.productId, {
432
+ ...product,
433
+ updatedAt: now,
434
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
435
+ } as unknown as Record<string, any>);
436
+ }
437
+
438
+ return variant;
439
+ },
440
+
441
+ async updateVariant(
442
+ id: string,
443
+ params: Partial<ProductVariant>,
444
+ ): Promise<ProductVariant> {
445
+ const existing = (await data.get(
446
+ "productVariant",
447
+ id,
448
+ )) as ProductVariant | null;
449
+ if (!existing) throw new Error(`Variant ${id} not found`);
450
+
451
+ const now = new Date();
452
+ const updated: ProductVariant = {
453
+ ...existing,
454
+ ...params,
455
+ updatedAt: now,
456
+ };
457
+ await data.upsert(
458
+ "productVariant",
459
+ id,
460
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
461
+ updated as unknown as Record<string, any>,
462
+ );
463
+
464
+ // Update product timestamp
465
+ const product = (await data.get(
466
+ "product",
467
+ existing.productId,
468
+ )) as Product | null;
469
+ if (product) {
470
+ await data.upsert("product", existing.productId, {
471
+ ...product,
472
+ updatedAt: now,
473
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
474
+ } as unknown as Record<string, any>);
475
+ }
476
+
477
+ return updated;
478
+ },
479
+
480
+ async deleteVariant(id: string): Promise<{ success: boolean }> {
481
+ const variant = (await data.get(
482
+ "productVariant",
483
+ id,
484
+ )) as ProductVariant | null;
485
+ if (variant) {
486
+ await data.delete("productVariant", id);
487
+
488
+ const product = (await data.get(
489
+ "product",
490
+ variant.productId,
491
+ )) as Product | null;
492
+ if (product) {
493
+ await data.upsert("product", variant.productId, {
494
+ ...product,
495
+ updatedAt: new Date(),
496
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
497
+ } as unknown as Record<string, any>);
498
+ }
499
+ }
500
+
501
+ return { success: true };
502
+ },
503
+
504
+ // ── Categories ────────────────────────────────────────────────────
505
+
506
+ async getCategory(id: string): Promise<Category | null> {
507
+ return (await data.get("category", id)) as Category | null;
508
+ },
509
+
510
+ async getCategoryBySlug(slug: string): Promise<Category | null> {
511
+ const categories = (await data.findMany("category", {
512
+ where: { slug },
513
+ })) as Category[];
514
+ return categories[0] ?? null;
515
+ },
516
+
517
+ async listCategories(params?: {
518
+ page?: number;
519
+ limit?: number;
520
+ parentId?: string;
521
+ visible?: boolean;
522
+ }) {
523
+ const page = params?.page ?? 1;
524
+ const limit = params?.limit ?? 50;
525
+
526
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
527
+ const where: Record<string, any> = {};
528
+ if (params?.parentId) where.parentId = params.parentId;
529
+ if (params?.visible) where.isVisible = true;
530
+
531
+ const categories = (await data.findMany("category", {
532
+ where,
533
+ take: limit,
534
+ skip: (page - 1) * limit,
535
+ })) as Category[];
536
+
537
+ return {
538
+ categories: categories.sort((a, b) => a.position - b.position),
539
+ page,
540
+ limit,
541
+ };
542
+ },
543
+
544
+ async getCategoryTree(): Promise<CategoryWithChildren[]> {
545
+ const allCategories = (await data.findMany("category", {
546
+ where: { isVisible: true },
547
+ })) as Category[];
548
+
549
+ const rootCategories: CategoryWithChildren[] = [];
550
+ const categoryMap = new Map<string, CategoryWithChildren>();
551
+
552
+ for (const cat of allCategories) {
553
+ categoryMap.set(cat.id, { ...cat, children: [] });
554
+ }
555
+
556
+ for (const cat of allCategories) {
557
+ // biome-ignore lint/style/noNonNullAssertion: categoryMap is populated from allCategories
558
+ const catWithChildren = categoryMap.get(cat.id)!;
559
+ if (cat.parentId) {
560
+ const parent = categoryMap.get(cat.parentId);
561
+ if (parent) {
562
+ parent.children.push(catWithChildren);
563
+ } else {
564
+ rootCategories.push(catWithChildren);
565
+ }
566
+ } else {
567
+ rootCategories.push(catWithChildren);
568
+ }
569
+ }
570
+
571
+ rootCategories.sort((a, b) => a.position - b.position);
572
+ for (const cat of categoryMap.values()) {
573
+ cat.children.sort((a, b) => a.position - b.position);
574
+ }
575
+
576
+ return rootCategories;
577
+ },
578
+
579
+ async createCategory(params: CreateCategoryParams): Promise<Category> {
580
+ const now = new Date();
581
+ const id = `cat_${crypto.randomUUID()}`;
582
+
583
+ const category: Category = {
584
+ id,
585
+ name: params.name,
586
+ slug: params.slug,
587
+ description: params.description,
588
+ parentId: params.parentId,
589
+ image: params.image,
590
+ position: params.position ?? 0,
591
+ isVisible: params.isVisible ?? true,
592
+ metadata: params.metadata ?? {},
593
+ createdAt: now,
594
+ updatedAt: now,
595
+ };
596
+
597
+ await data.upsert(
598
+ "category",
599
+ id,
600
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
601
+ category as unknown as Record<string, any>,
602
+ );
603
+ return category;
604
+ },
605
+
606
+ async updateCategory(
607
+ id: string,
608
+ params: Partial<Category>,
609
+ ): Promise<Category> {
610
+ const existing = (await data.get("category", id)) as Category | null;
611
+ if (!existing) throw new Error(`Category ${id} not found`);
612
+
613
+ const updated: Category = {
614
+ ...existing,
615
+ ...params,
616
+ updatedAt: new Date(),
617
+ };
618
+ await data.upsert(
619
+ "category",
620
+ id,
621
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
622
+ updated as unknown as Record<string, any>,
623
+ );
624
+ return updated;
625
+ },
626
+
627
+ async deleteCategory(id: string): Promise<{ success: boolean }> {
628
+ // Orphan products
629
+ const products = (await data.findMany("product", {
630
+ where: { categoryId: id },
631
+ })) as Product[];
632
+
633
+ for (const product of products) {
634
+ await data.upsert("product", product.id, {
635
+ ...product,
636
+ categoryId: undefined,
637
+ updatedAt: new Date(),
638
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
639
+ } as unknown as Record<string, any>);
640
+ }
641
+
642
+ // Orphan subcategories
643
+ const subcategories = (await data.findMany("category", {
644
+ where: { parentId: id },
645
+ })) as Category[];
646
+
647
+ for (const subcat of subcategories) {
648
+ await data.upsert("category", subcat.id, {
649
+ ...subcat,
650
+ parentId: undefined,
651
+ updatedAt: new Date(),
652
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
653
+ } as unknown as Record<string, any>);
654
+ }
655
+
656
+ await data.delete("category", id);
657
+ return { success: true };
658
+ },
659
+
660
+ // ── Bulk ──────────────────────────────────────────────────────────
661
+
662
+ async bulkUpdateStatus(
663
+ ids: string[],
664
+ status: "draft" | "active" | "archived",
665
+ ) {
666
+ if (!ids.length) return { updated: 0 };
667
+
668
+ const now = new Date();
669
+ let updated = 0;
670
+
671
+ for (const id of ids) {
672
+ const product = (await data.get("product", id)) as Product | null;
673
+ if (product) {
674
+ await data.upsert("product", id, {
675
+ ...product,
676
+ status,
677
+ updatedAt: now,
678
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
679
+ } as unknown as Record<string, any>);
680
+ updated++;
681
+ }
682
+ }
683
+
684
+ return { updated };
685
+ },
686
+
687
+ async bulkDelete(ids: string[]) {
688
+ if (!ids.length) return { deleted: 0 };
689
+
690
+ let deleted = 0;
691
+
692
+ for (const id of ids) {
693
+ const product = (await data.get("product", id)) as Product | null;
694
+ if (!product) continue;
695
+
696
+ const variants = (await data.findMany("productVariant", {
697
+ where: { productId: id },
698
+ })) as ProductVariant[];
699
+
700
+ for (const variant of variants) {
701
+ await data.delete("productVariant", variant.id);
702
+ }
703
+
704
+ await data.delete("product", id);
705
+ deleted++;
706
+ }
707
+
708
+ return { deleted };
709
+ },
710
+
711
+ // ── Import ────────────────────────────────────────────────────────
712
+
713
+ async importProducts(rows: ImportProductRow[]): Promise<ImportResult> {
714
+ const created: string[] = [];
715
+ const updated: string[] = [];
716
+ const errors: ImportError[] = [];
717
+
718
+ // Pre-fetch categories for name→id resolution
719
+ const allCategories = (await data.findMany("category", {
720
+ where: {},
721
+ })) as Category[];
722
+ const categoryByName = new Map<string, string>();
723
+ for (const cat of allCategories) {
724
+ categoryByName.set(cat.name.toLowerCase(), cat.id);
725
+ }
726
+
727
+ // Pre-fetch SKUs for update-by-SKU matching
728
+ const allProducts = (await data.findMany("product", {
729
+ where: {},
730
+ })) as Product[];
731
+ const productBySku = new Map<string, Product>();
732
+ const slugSet = new Set<string>();
733
+ for (const p of allProducts) {
734
+ if (p.sku) productBySku.set(p.sku, p);
735
+ slugSet.add(p.slug);
736
+ }
737
+
738
+ for (let i = 0; i < rows.length; i++) {
739
+ const row = rows[i];
740
+ try {
741
+ if (!row.name || row.name.trim() === "") {
742
+ errors.push({
743
+ row: i + 1,
744
+ field: "name",
745
+ message: "Name is required",
746
+ });
747
+ continue;
748
+ }
749
+ if (row.price === undefined || row.price === null) {
750
+ errors.push({
751
+ row: i + 1,
752
+ field: "price",
753
+ message: "Price is required",
754
+ });
755
+ continue;
756
+ }
757
+ const price = Math.round(Number(row.price) * 100);
758
+ if (Number.isNaN(price) || price <= 0) {
759
+ errors.push({
760
+ row: i + 1,
761
+ field: "price",
762
+ message: "Price must be a positive number",
763
+ });
764
+ continue;
765
+ }
766
+
767
+ const existingBySku = row.sku ? productBySku.get(row.sku) : undefined;
768
+ if (existingBySku) {
769
+ const updateFields: Partial<Product> = {
770
+ name: row.name,
771
+ price,
772
+ updatedAt: new Date(),
773
+ };
774
+ if (row.description !== undefined)
775
+ updateFields.description = row.description;
776
+ if (row.shortDescription !== undefined)
777
+ updateFields.shortDescription = row.shortDescription;
778
+ if (row.compareAtPrice !== undefined)
779
+ updateFields.compareAtPrice = Math.round(
780
+ Number(row.compareAtPrice) * 100,
781
+ );
782
+ if (row.costPrice !== undefined)
783
+ updateFields.costPrice = Math.round(Number(row.costPrice) * 100);
784
+ if (row.inventory !== undefined)
785
+ updateFields.inventory = Number(row.inventory);
786
+ if (row.status !== undefined)
787
+ updateFields.status = row.status as
788
+ | "draft"
789
+ | "active"
790
+ | "archived";
791
+ if (row.category) {
792
+ const catId = categoryByName.get(row.category.toLowerCase());
793
+ if (catId) updateFields.categoryId = catId;
794
+ }
795
+ if (row.tags !== undefined) updateFields.tags = row.tags;
796
+ if (row.weight !== undefined)
797
+ updateFields.weight = Number(row.weight);
798
+ if (row.weightUnit !== undefined)
799
+ updateFields.weightUnit = row.weightUnit as
800
+ | "kg"
801
+ | "lb"
802
+ | "oz"
803
+ | "g";
804
+ if (row.featured !== undefined)
805
+ updateFields.isFeatured = row.featured;
806
+ if (row.trackInventory !== undefined)
807
+ updateFields.trackInventory = row.trackInventory;
808
+ if (row.allowBackorder !== undefined)
809
+ updateFields.allowBackorder = row.allowBackorder;
810
+
811
+ const updatedProduct = { ...existingBySku, ...updateFields };
812
+ await data.upsert(
813
+ "product",
814
+ existingBySku.id,
815
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
816
+ updatedProduct as unknown as Record<string, any>,
817
+ );
818
+ updated.push(existingBySku.id);
819
+ continue;
820
+ }
821
+
822
+ let slug = row.slug || generateSlug(row.name);
823
+ let slugAttempt = 0;
824
+ const baseSlug = slug;
825
+ while (slugSet.has(slug)) {
826
+ slugAttempt++;
827
+ slug = `${baseSlug}-${slugAttempt}`;
828
+ }
829
+ slugSet.add(slug);
830
+
831
+ let categoryId: string | undefined;
832
+ if (row.category) {
833
+ categoryId = categoryByName.get(row.category.toLowerCase());
834
+ }
835
+
836
+ const now = new Date();
837
+ const id = `prod_${crypto.randomUUID()}`;
838
+
839
+ const product: Product = {
840
+ id,
841
+ name: row.name.trim(),
842
+ slug,
843
+ description: row.description,
844
+ shortDescription: row.shortDescription,
845
+ price,
846
+ compareAtPrice: row.compareAtPrice
847
+ ? Math.round(Number(row.compareAtPrice) * 100)
848
+ : undefined,
849
+ costPrice: row.costPrice
850
+ ? Math.round(Number(row.costPrice) * 100)
851
+ : undefined,
852
+ sku: row.sku,
853
+ barcode: row.barcode,
854
+ inventory: row.inventory !== undefined ? Number(row.inventory) : 0,
855
+ trackInventory: row.trackInventory ?? true,
856
+ allowBackorder: row.allowBackorder ?? false,
857
+ status: (row.status as "draft" | "active" | "archived") || "draft",
858
+ categoryId,
859
+ images: [],
860
+ tags: row.tags ?? [],
861
+ metadata: {},
862
+ weight: row.weight !== undefined ? Number(row.weight) : undefined,
863
+ weightUnit: (row.weightUnit as "kg" | "lb" | "oz" | "g") || "kg",
864
+ isFeatured: row.featured ?? false,
865
+ createdAt: now,
866
+ updatedAt: now,
867
+ };
868
+
869
+ await data.upsert(
870
+ "product",
871
+ id,
872
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
873
+ product as unknown as Record<string, any>,
874
+ );
875
+ created.push(id);
876
+ } catch (err) {
877
+ errors.push({
878
+ row: i + 1,
879
+ field: "unknown",
880
+ message: err instanceof Error ? err.message : "Unknown error",
881
+ });
882
+ }
883
+ }
884
+
885
+ return { created: created.length, updated: updated.length, errors };
886
+ },
887
+
888
+ // ── Collections ───────────────────────────────────────────────────
889
+
890
+ async getCollection(id: string): Promise<Collection | null> {
891
+ return (await data.get("collection", id)) as Collection | null;
892
+ },
893
+
894
+ async getCollectionBySlug(slug: string): Promise<Collection | null> {
895
+ const collections = (await data.findMany("collection", {
896
+ where: { slug },
897
+ })) as Collection[];
898
+ return collections[0] ?? null;
899
+ },
900
+
901
+ async listCollections(params?: {
902
+ page?: number;
903
+ limit?: number;
904
+ featured?: boolean;
905
+ visible?: boolean;
906
+ }) {
907
+ const page = params?.page ?? 1;
908
+ const limit = params?.limit ?? 50;
909
+
910
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
911
+ const where: Record<string, any> = {};
912
+ if (params?.featured) where.isFeatured = true;
913
+ if (params?.visible) where.isVisible = true;
914
+
915
+ const collections = (await data.findMany("collection", {
916
+ where,
917
+ take: limit,
918
+ skip: (page - 1) * limit,
919
+ })) as Collection[];
920
+
921
+ return {
922
+ collections: collections.sort((a, b) => a.position - b.position),
923
+ page,
924
+ limit,
925
+ };
926
+ },
927
+
928
+ async searchCollections(q: string, limit?: number): Promise<Collection[]> {
929
+ const collections = (await data.findMany("collection", {
930
+ where: { isVisible: true },
931
+ })) as Collection[];
932
+
933
+ const queryLower = q.toLowerCase();
934
+ const results = collections.filter(
935
+ (c) =>
936
+ c.name.toLowerCase().includes(queryLower) ||
937
+ c.slug.toLowerCase().includes(queryLower) ||
938
+ c.description?.toLowerCase().includes(queryLower),
939
+ );
940
+
941
+ return results
942
+ .sort((a, b) => a.position - b.position)
943
+ .slice(0, limit ?? 10);
944
+ },
945
+
946
+ async getCollectionWithProducts(
947
+ id: string,
948
+ ): Promise<CollectionWithProducts | null> {
949
+ const collection = (await data.get(
950
+ "collection",
951
+ id,
952
+ )) as Collection | null;
953
+ if (!collection) return null;
954
+
955
+ const links = (await data.findMany("collectionProduct", {
956
+ where: { collectionId: id },
957
+ })) as CollectionProduct[];
958
+
959
+ links.sort((a, b) => a.position - b.position);
960
+
961
+ const products: Product[] = [];
962
+ for (const link of links) {
963
+ const product = (await data.get(
964
+ "product",
965
+ link.productId,
966
+ )) as Product | null;
967
+ if (product && product.status === "active") {
968
+ products.push(product);
969
+ }
970
+ }
971
+
972
+ return { ...collection, products };
973
+ },
974
+
975
+ async createCollection(
976
+ params: CreateCollectionParams,
977
+ ): Promise<Collection> {
978
+ const now = new Date();
979
+ const id = `col_${crypto.randomUUID()}`;
980
+
981
+ const collection: Collection = {
982
+ id,
983
+ name: params.name,
984
+ slug: params.slug,
985
+ description: params.description,
986
+ image: params.image,
987
+ isFeatured: params.isFeatured ?? false,
988
+ isVisible: params.isVisible ?? true,
989
+ position: params.position ?? 0,
990
+ metadata: params.metadata ?? {},
991
+ createdAt: now,
992
+ updatedAt: now,
993
+ };
994
+
995
+ await data.upsert(
996
+ "collection",
997
+ id,
998
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
999
+ collection as unknown as Record<string, any>,
1000
+ );
1001
+ return collection;
1002
+ },
1003
+
1004
+ async updateCollection(
1005
+ id: string,
1006
+ params: Partial<Collection>,
1007
+ ): Promise<Collection> {
1008
+ const existing = (await data.get("collection", id)) as Collection | null;
1009
+ if (!existing) throw new Error(`Collection ${id} not found`);
1010
+
1011
+ const updated: Collection = {
1012
+ ...existing,
1013
+ ...params,
1014
+ updatedAt: new Date(),
1015
+ };
1016
+ await data.upsert(
1017
+ "collection",
1018
+ id,
1019
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
1020
+ updated as unknown as Record<string, any>,
1021
+ );
1022
+ return updated;
1023
+ },
1024
+
1025
+ async deleteCollection(id: string): Promise<{ success: boolean }> {
1026
+ const links = (await data.findMany("collectionProduct", {
1027
+ where: { collectionId: id },
1028
+ })) as CollectionProduct[];
1029
+
1030
+ for (const link of links) {
1031
+ await data.delete("collectionProduct", link.id);
1032
+ }
1033
+
1034
+ await data.delete("collection", id);
1035
+ return { success: true };
1036
+ },
1037
+
1038
+ async addProductToCollection(
1039
+ collectionId: string,
1040
+ productId: string,
1041
+ position?: number,
1042
+ ): Promise<CollectionProduct> {
1043
+ const collection = (await data.get(
1044
+ "collection",
1045
+ collectionId,
1046
+ )) as Collection | null;
1047
+ if (!collection) throw new Error(`Collection ${collectionId} not found`);
1048
+
1049
+ // Check duplicate
1050
+ const existing = (await data.findMany("collectionProduct", {
1051
+ where: { collectionId, productId },
1052
+ })) as CollectionProduct[];
1053
+ if (existing.length > 0) {
1054
+ return existing[0];
1055
+ }
1056
+
1057
+ const linkId = `cp_${crypto.randomUUID()}`;
1058
+ const link: CollectionProduct = {
1059
+ id: linkId,
1060
+ collectionId,
1061
+ productId,
1062
+ position: position ?? 0,
1063
+ createdAt: new Date(),
1064
+ };
1065
+
1066
+ await data.upsert(
1067
+ "collectionProduct",
1068
+ linkId,
1069
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
1070
+ link as unknown as Record<string, any>,
1071
+ );
1072
+
1073
+ // Update collection timestamp
1074
+ await data.upsert("collection", collectionId, {
1075
+ ...collection,
1076
+ updatedAt: new Date(),
1077
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
1078
+ } as unknown as Record<string, any>);
1079
+
1080
+ return link;
1081
+ },
1082
+
1083
+ async removeProductFromCollection(
1084
+ collectionId: string,
1085
+ productId: string,
1086
+ ): Promise<{ success: boolean }> {
1087
+ const links = (await data.findMany("collectionProduct", {
1088
+ where: { collectionId, productId },
1089
+ })) as CollectionProduct[];
1090
+
1091
+ for (const link of links) {
1092
+ await data.delete("collectionProduct", link.id);
1093
+ }
1094
+
1095
+ const collection = (await data.get(
1096
+ "collection",
1097
+ collectionId,
1098
+ )) as Collection | null;
1099
+ if (collection) {
1100
+ await data.upsert("collection", collectionId, {
1101
+ ...collection,
1102
+ updatedAt: new Date(),
1103
+ // biome-ignore lint/suspicious/noExplicitAny: data service requires Record<string, any>
1104
+ } as unknown as Record<string, any>);
1105
+ }
1106
+
1107
+ return { success: true };
1108
+ },
1109
+
1110
+ async listCollectionProducts(
1111
+ collectionId: string,
1112
+ ): Promise<{ products: Product[] }> {
1113
+ const collection = (await data.get(
1114
+ "collection",
1115
+ collectionId,
1116
+ )) as Collection | null;
1117
+ if (!collection) return { products: [] };
1118
+
1119
+ const links = (await data.findMany("collectionProduct", {
1120
+ where: { collectionId },
1121
+ })) as CollectionProduct[];
1122
+
1123
+ links.sort((a, b) => a.position - b.position);
1124
+
1125
+ const products: Product[] = [];
1126
+ for (const link of links) {
1127
+ const product = (await data.get(
1128
+ "product",
1129
+ link.productId,
1130
+ )) as Product | null;
1131
+ if (product) {
1132
+ products.push(product);
1133
+ }
1134
+ }
1135
+
1136
+ return { products };
1137
+ },
1138
+ };
1139
+ }