@stacksjs/commerce 0.70.54 → 0.70.55
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/package.json +3 -2
- package/src/carts/destroy.ts +54 -0
- package/src/carts/fetch.ts +20 -0
- package/src/carts/index.ts +13 -0
- package/src/carts/store.ts +75 -0
- package/src/carts/update.ts +78 -0
- package/src/coupons/destroy.ts +49 -0
- package/src/coupons/fetch.ts +510 -0
- package/src/coupons/index.ts +30 -0
- package/src/coupons/store.ts +46 -0
- package/src/coupons/update.ts +147 -0
- package/src/customers/destroy.ts +62 -0
- package/src/customers/fetch.ts +20 -0
- package/src/customers/index.ts +17 -0
- package/src/customers/store.ts +45 -0
- package/src/customers/update.ts +44 -0
- package/src/devices/destroy.ts +54 -0
- package/src/devices/export.ts +98 -0
- package/src/devices/fetch.ts +125 -0
- package/src/devices/index.ts +31 -0
- package/src/devices/store.ts +82 -0
- package/src/devices/update.ts +125 -0
- package/src/errors/destroy.ts +54 -0
- package/src/errors/fetch.ts +197 -0
- package/src/errors/index.ts +28 -0
- package/src/errors/update.ts +116 -0
- package/src/gift-cards/destroy.ts +87 -0
- package/src/gift-cards/fetch.ts +431 -0
- package/src/gift-cards/index.ts +26 -0
- package/src/gift-cards/store.ts +56 -0
- package/src/gift-cards/update.ts +102 -0
- package/src/index.ts +78 -0
- package/src/orders/destroy.ts +111 -0
- package/src/orders/events.ts +121 -0
- package/src/orders/export.ts +158 -0
- package/src/orders/fetch.ts +379 -0
- package/src/orders/guards.ts +261 -0
- package/src/orders/index.ts +91 -0
- package/src/orders/place-order.ts +274 -0
- package/src/orders/store.ts +51 -0
- package/src/orders/totals.ts +190 -0
- package/src/orders/update.ts +164 -0
- package/src/orders/webhook.ts +312 -0
- package/src/payments/destroy.ts +56 -0
- package/src/payments/fetch.ts +288 -0
- package/src/payments/index.ts +16 -0
- package/src/payments/store.ts +63 -0
- package/src/payments/update.ts +52 -0
- package/src/product.ts +0 -0
- package/src/products/categories/destroy.ts +103 -0
- package/src/products/categories/fetch.ts +285 -0
- package/src/products/categories/index.ts +36 -0
- package/src/products/categories/store.ts +87 -0
- package/src/products/categories/update.ts +251 -0
- package/src/products/index.ts +35 -0
- package/src/products/items/destroy.ts +54 -0
- package/src/products/items/fetch.ts +282 -0
- package/src/products/items/index.ts +26 -0
- package/src/products/items/store.ts +84 -0
- package/src/products/items/update.ts +324 -0
- package/src/products/manufacturers/destroy.ts +56 -0
- package/src/products/manufacturers/fetch.ts +117 -0
- package/src/products/manufacturers/index.ts +24 -0
- package/src/products/manufacturers/store.ts +82 -0
- package/src/products/manufacturers/update.ts +113 -0
- package/src/products/reviews/destroy.ts +56 -0
- package/src/products/reviews/fetch.ts +63 -0
- package/src/products/reviews/index.ts +25 -0
- package/src/products/reviews/store.ts +46 -0
- package/src/products/reviews/update.ts +82 -0
- package/src/products/units/destroy.ts +56 -0
- package/src/products/units/fetch.ts +20 -0
- package/src/products/units/index.ts +13 -0
- package/src/products/units/store.ts +162 -0
- package/src/products/units/update.ts +159 -0
- package/src/products/variants/destroy.ts +56 -0
- package/src/products/variants/fetch.ts +20 -0
- package/src/products/variants/index.ts +21 -0
- package/src/products/variants/store.ts +136 -0
- package/src/products/variants/update.ts +104 -0
- package/src/receipts/destroy.ts +54 -0
- package/src/receipts/export.ts +0 -0
- package/src/receipts/fetch.ts +222 -0
- package/src/receipts/index.ts +24 -0
- package/src/receipts/printer.ts +69 -0
- package/src/receipts/store.ts +69 -0
- package/src/receipts/update.ts +124 -0
- package/src/shippings/delivery-routes/destroy.ts +54 -0
- package/src/shippings/delivery-routes/fetch.ts +42 -0
- package/src/shippings/delivery-routes/index.ts +26 -0
- package/src/shippings/delivery-routes/store.ts +69 -0
- package/src/shippings/delivery-routes/update.ts +120 -0
- package/src/shippings/digital-deliveries/destroy.ts +111 -0
- package/src/shippings/digital-deliveries/fetch.ts +20 -0
- package/src/shippings/digital-deliveries/index.ts +26 -0
- package/src/shippings/digital-deliveries/store.ts +76 -0
- package/src/shippings/digital-deliveries/update.ts +129 -0
- package/src/shippings/drivers/destroy.ts +78 -0
- package/src/shippings/drivers/fetch.ts +20 -0
- package/src/shippings/drivers/index.ts +24 -0
- package/src/shippings/drivers/store.ts +69 -0
- package/src/shippings/drivers/update.ts +117 -0
- package/src/shippings/index.ts +39 -0
- package/src/shippings/license-keys/destroy.ts +111 -0
- package/src/shippings/license-keys/fetch.ts +20 -0
- package/src/shippings/license-keys/index.ts +26 -0
- package/src/shippings/license-keys/store.ts +77 -0
- package/src/shippings/license-keys/update.ts +117 -0
- package/src/shippings/shipping-methods/destroy.ts +111 -0
- package/src/shippings/shipping-methods/fetch.ts +62 -0
- package/src/shippings/shipping-methods/index.ts +28 -0
- package/src/shippings/shipping-methods/store.ts +123 -0
- package/src/shippings/shipping-methods/update.ts +120 -0
- package/src/shippings/shipping-rates/destroy.ts +102 -0
- package/src/shippings/shipping-rates/fetch.ts +320 -0
- package/src/shippings/shipping-rates/index.ts +36 -0
- package/src/shippings/shipping-rates/store.ts +102 -0
- package/src/shippings/shipping-rates/update.ts +139 -0
- package/src/shippings/shipping-zones/destroy.ts +111 -0
- package/src/shippings/shipping-zones/fetch.ts +96 -0
- package/src/shippings/shipping-zones/index.ts +26 -0
- package/src/shippings/shipping-zones/store.ts +74 -0
- package/src/shippings/shipping-zones/update.ts +156 -0
- package/src/tax/destroy.ts +54 -0
- package/src/tax/fetch.ts +20 -0
- package/src/tax/index.ts +24 -0
- package/src/tax/store.ts +69 -0
- package/src/tax/update.ts +119 -0
- package/src/tests/coupons.test.ts +90 -0
- package/src/tests/customers.test.ts +39 -0
- package/src/tests/device.test.ts +111 -0
- package/src/tests/digitals.test.ts +31 -0
- package/src/tests/drivers.test.ts +25 -0
- package/src/tests/gift-cards.test.ts +36 -0
- package/src/tests/license.test.ts +31 -0
- package/src/tests/manufacturers.test.ts +80 -0
- package/src/tests/orders.test.ts +25 -0
- package/src/tests/payments.test.ts +75 -0
- package/src/tests/rates.test.ts +367 -0
- package/src/tests/receipts.test.ts +287 -0
- package/src/tests/restaurant.test.ts +522 -0
- package/src/tests/reviews.test.ts +165 -0
- package/src/tests/routes.test.ts +51 -0
- package/src/tests/setup.ts +149 -0
- package/src/tests/shipping.test.ts +88 -0
- package/src/tests/tax.test.ts +25 -0
- package/src/tests/units.test.ts +110 -0
- package/src/tests/variants.test.ts +133 -0
- package/src/tests/waitlist.test.ts +303 -0
- package/src/tests/zones.test.ts +86 -0
- package/src/types.ts +312 -0
- package/src/utils/typed-stats.ts +67 -0
- package/src/waitlists/index.ts +19 -0
- package/src/waitlists/products/destroy.ts +65 -0
- package/src/waitlists/products/export.ts +113 -0
- package/src/waitlists/products/fetch.ts +362 -0
- package/src/waitlists/products/index.ts +33 -0
- package/src/waitlists/products/store.ts +112 -0
- package/src/waitlists/products/update.ts +125 -0
- package/src/waitlists/restaurant/destroy.ts +65 -0
- package/src/waitlists/restaurant/export.ts +107 -0
- package/src/waitlists/restaurant/fetch.ts +538 -0
- package/src/waitlists/restaurant/index.ts +43 -0
- package/src/waitlists/restaurant/store.ts +109 -0
- package/src/waitlists/restaurant/update.ts +198 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { CategoryStats } from '../../types'
|
|
2
|
+
import type { StacksExpressionBuilder } from '@stacksjs/database'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { formatDate } from '@stacksjs/orm'
|
|
5
|
+
type CategoryJsonResponse = ModelRow<typeof Category>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fetch all categories from the database
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchAll(): Promise<CategoryJsonResponse[]> {
|
|
11
|
+
const categories = await db
|
|
12
|
+
.selectFrom('categories')
|
|
13
|
+
.selectAll()
|
|
14
|
+
.execute()
|
|
15
|
+
|
|
16
|
+
return categories as CategoryJsonResponse[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetch a category by ID
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchById(id: number): Promise<CategoryJsonResponse | undefined> {
|
|
23
|
+
return await db
|
|
24
|
+
.selectFrom('categories')
|
|
25
|
+
.where('id', '=', id)
|
|
26
|
+
.selectAll()
|
|
27
|
+
.executeTakeFirst() as CategoryJsonResponse | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch a category by name
|
|
32
|
+
*/
|
|
33
|
+
export async function fetchByName(name: string): Promise<CategoryJsonResponse | undefined> {
|
|
34
|
+
return await db
|
|
35
|
+
.selectFrom('categories')
|
|
36
|
+
.where('name', '=', name)
|
|
37
|
+
.selectAll()
|
|
38
|
+
.executeTakeFirst() as CategoryJsonResponse | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetch active categories (is_active = true)
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchActive(): Promise<CategoryJsonResponse[]> {
|
|
45
|
+
const categories = await db
|
|
46
|
+
.selectFrom('categories')
|
|
47
|
+
.where('is_active', '=', true)
|
|
48
|
+
.selectAll()
|
|
49
|
+
.execute()
|
|
50
|
+
|
|
51
|
+
return categories as CategoryJsonResponse[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fetch root categories (parent_category_id is null)
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchRootCategories(): Promise<CategoryJsonResponse[]> {
|
|
58
|
+
const categories = await db
|
|
59
|
+
.selectFrom('categories')
|
|
60
|
+
.where('parent_category_id', 'is', null)
|
|
61
|
+
.where('is_active', '=', true)
|
|
62
|
+
.selectAll()
|
|
63
|
+
.execute()
|
|
64
|
+
|
|
65
|
+
return categories as CategoryJsonResponse[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fetch child categories for a given parent category ID
|
|
70
|
+
*/
|
|
71
|
+
export async function fetchChildCategories(parentId: string): Promise<CategoryJsonResponse[]> {
|
|
72
|
+
const categories = await db
|
|
73
|
+
.selectFrom('categories')
|
|
74
|
+
.where('parent_category_id', '=', parentId)
|
|
75
|
+
.where('is_active', '=', true)
|
|
76
|
+
.selectAll()
|
|
77
|
+
.execute()
|
|
78
|
+
|
|
79
|
+
return categories as CategoryJsonResponse[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fetch categories sorted by display order
|
|
84
|
+
*/
|
|
85
|
+
export async function fetchByDisplayOrder(ascending: boolean = true): Promise<CategoryJsonResponse[]> {
|
|
86
|
+
const query = db
|
|
87
|
+
.selectFrom('categories')
|
|
88
|
+
.where('is_active', '=', true)
|
|
89
|
+
.selectAll()
|
|
90
|
+
|
|
91
|
+
if (ascending) {
|
|
92
|
+
return await query.orderBy('display_order', 'asc').execute() as CategoryJsonResponse[]
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return await query.orderBy('display_order', 'desc').execute() as CategoryJsonResponse[]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get category statistics
|
|
101
|
+
*/
|
|
102
|
+
export async function fetchStats(): Promise<CategoryStats> {
|
|
103
|
+
// Total categories
|
|
104
|
+
const totalCategories = await db
|
|
105
|
+
.selectFrom('categories')
|
|
106
|
+
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
107
|
+
.executeTakeFirst() as { count: number } | undefined
|
|
108
|
+
|
|
109
|
+
// Active categories
|
|
110
|
+
const activeCategories = await db
|
|
111
|
+
.selectFrom('categories')
|
|
112
|
+
.where('is_active', '=', true)
|
|
113
|
+
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
114
|
+
.executeTakeFirst() as { count: number } | undefined
|
|
115
|
+
|
|
116
|
+
// Root vs child categories
|
|
117
|
+
const rootCategories = await db
|
|
118
|
+
.selectFrom('categories')
|
|
119
|
+
.where('parent_category_id', 'is', null)
|
|
120
|
+
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
121
|
+
.executeTakeFirst() as { count: number } | undefined
|
|
122
|
+
|
|
123
|
+
// Categories with images
|
|
124
|
+
const categoriesWithImages = await db
|
|
125
|
+
.selectFrom('categories')
|
|
126
|
+
.where('image_url', 'is not', null)
|
|
127
|
+
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
128
|
+
.executeTakeFirst() as { count: number } | undefined
|
|
129
|
+
|
|
130
|
+
// Recently added categories (last 30 days)
|
|
131
|
+
const thirtyDaysAgo = new Date()
|
|
132
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
|
133
|
+
const thirtyDaysAgoStr = formatDate(thirtyDaysAgo)
|
|
134
|
+
|
|
135
|
+
const recentlyAddedCategories = await db
|
|
136
|
+
.selectFrom('categories')
|
|
137
|
+
.where('created_at', '>=', thirtyDaysAgoStr)
|
|
138
|
+
.selectAll()
|
|
139
|
+
.limit(5)
|
|
140
|
+
.execute()
|
|
141
|
+
|
|
142
|
+
// Categories by parent (top 5 parents with most children)
|
|
143
|
+
const categoriesByParent = await db
|
|
144
|
+
.selectFrom('categories as c')
|
|
145
|
+
.leftJoin('categories as parent', 'c.parent_category_id', '=', 'parent.id')
|
|
146
|
+
.where('c.parent_category_id', 'is not', null)
|
|
147
|
+
.select([
|
|
148
|
+
'c.parent_category_id',
|
|
149
|
+
'parent.name as parent_name',
|
|
150
|
+
(eb: StacksExpressionBuilder) => eb.fn.count('c.id').as('child_count'),
|
|
151
|
+
] as any)
|
|
152
|
+
.groupBy(['c.parent_category_id', 'parent.name'] as any)
|
|
153
|
+
.orderBy('child_count', 'desc')
|
|
154
|
+
.limit(5)
|
|
155
|
+
.execute() as { parent_category_id: string, parent_name: string, child_count: number }[]
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
total: Number(totalCategories?.count || 0),
|
|
159
|
+
active: Number(activeCategories?.count || 0),
|
|
160
|
+
root_categories: Number(rootCategories?.count || 0),
|
|
161
|
+
child_categories: Number(totalCategories?.count || 0) - Number(rootCategories?.count || 0),
|
|
162
|
+
with_images: Number(categoriesWithImages?.count || 0),
|
|
163
|
+
recently_added: recentlyAddedCategories,
|
|
164
|
+
top_parent_categories: categoriesByParent.map((item: any) => ({
|
|
165
|
+
id: String(item.parent_category_id || ''),
|
|
166
|
+
name: String(item.parent_name || ''),
|
|
167
|
+
child_count: Number(item.child_count),
|
|
168
|
+
})),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Compare category growth between different time periods
|
|
174
|
+
* @param daysRange Number of days to look back (7, 30, 60, etc.)
|
|
175
|
+
*/
|
|
176
|
+
export async function compareCategoryGrowth(_daysRange: number = 30): Promise<{
|
|
177
|
+
current_period: number
|
|
178
|
+
previous_period: number
|
|
179
|
+
difference: number
|
|
180
|
+
percentage_change: number
|
|
181
|
+
days_range: number
|
|
182
|
+
}> {
|
|
183
|
+
const today = new Date()
|
|
184
|
+
const todayStr = formatDate(today)
|
|
185
|
+
|
|
186
|
+
// Current period (last N days)
|
|
187
|
+
const currentPeriodStart = new Date()
|
|
188
|
+
currentPeriodStart.setDate(today.getDate() - _daysRange)
|
|
189
|
+
const currentPeriodStartStr = formatDate(currentPeriodStart)
|
|
190
|
+
|
|
191
|
+
// Previous period (N days before the current period)
|
|
192
|
+
const previousPeriodEnd = new Date(currentPeriodStart)
|
|
193
|
+
previousPeriodEnd.setDate(previousPeriodEnd.getDate() - 1)
|
|
194
|
+
const previousPeriodEndStr = formatDate(previousPeriodEnd)
|
|
195
|
+
|
|
196
|
+
const previousPeriodStart = new Date(previousPeriodEnd)
|
|
197
|
+
previousPeriodStart.setDate(previousPeriodEnd.getDate() - _daysRange)
|
|
198
|
+
const previousPeriodStartStr = formatDate(previousPeriodStart)
|
|
199
|
+
|
|
200
|
+
// Get categories for current period
|
|
201
|
+
const currentPeriodCategories = await db
|
|
202
|
+
.selectFrom('categories')
|
|
203
|
+
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
204
|
+
.where('created_at', '>=', currentPeriodStartStr)
|
|
205
|
+
.where('created_at', '<=', todayStr)
|
|
206
|
+
.executeTakeFirst() as { count: number } | undefined
|
|
207
|
+
|
|
208
|
+
// Get categories for previous period
|
|
209
|
+
const previousPeriodCategories = await db
|
|
210
|
+
.selectFrom('categories')
|
|
211
|
+
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
212
|
+
.where('created_at', '>=', previousPeriodStartStr)
|
|
213
|
+
.where('created_at', '<=', previousPeriodEndStr)
|
|
214
|
+
.executeTakeFirst() as { count: number } | undefined
|
|
215
|
+
|
|
216
|
+
const currentCount = Number(currentPeriodCategories?.count || 0)
|
|
217
|
+
const previousCount = Number(previousPeriodCategories?.count || 0)
|
|
218
|
+
const difference = currentCount - previousCount
|
|
219
|
+
|
|
220
|
+
// Calculate percentage change, handling division by zero
|
|
221
|
+
const percentageChange = previousCount !== 0
|
|
222
|
+
? (difference / previousCount) * 100
|
|
223
|
+
: (currentCount > 0 ? 100 : 0)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
current_period: currentCount,
|
|
227
|
+
previous_period: previousCount,
|
|
228
|
+
difference,
|
|
229
|
+
percentage_change: percentageChange,
|
|
230
|
+
days_range: _daysRange,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build category tree structure
|
|
236
|
+
*/
|
|
237
|
+
export async function fetchCategoryTree(): Promise<any[]> {
|
|
238
|
+
// First get all categories
|
|
239
|
+
const allCategories = await fetchAll()
|
|
240
|
+
|
|
241
|
+
// Create a map for quick access
|
|
242
|
+
const categoryMap = new Map<any, any>()
|
|
243
|
+
allCategories.forEach((category: any) => {
|
|
244
|
+
categoryMap.set(category.id, {
|
|
245
|
+
...category,
|
|
246
|
+
children: [],
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Build the tree
|
|
251
|
+
const rootCategories: any[] = []
|
|
252
|
+
|
|
253
|
+
allCategories.forEach((category: any) => {
|
|
254
|
+
const categoryWithChildren = categoryMap.get(category.id)
|
|
255
|
+
|
|
256
|
+
if (category.parent_category_id) {
|
|
257
|
+
// This is a child category
|
|
258
|
+
const parent = categoryMap.get(category.parent_category_id)
|
|
259
|
+
if (parent) {
|
|
260
|
+
parent.children.push(categoryWithChildren)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// This is a root category
|
|
265
|
+
rootCategories.push(categoryWithChildren)
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Sort root categories by display_order
|
|
270
|
+
rootCategories.sort((a, b) => a.display_order - b.display_order)
|
|
271
|
+
|
|
272
|
+
// Sort children by display_order
|
|
273
|
+
const sortChildrenByDisplayOrder = (categories: any[]) => {
|
|
274
|
+
categories.sort((a, b) => a.display_order - b.display_order)
|
|
275
|
+
categories.forEach((category: any) => {
|
|
276
|
+
if (category.children.length > 0) {
|
|
277
|
+
sortChildrenByDisplayOrder(category.children)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
sortChildrenByDisplayOrder(rootCategories)
|
|
283
|
+
|
|
284
|
+
return rootCategories
|
|
285
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Export all functions from destroy.ts
|
|
2
|
+
export {
|
|
3
|
+
bulkRemove,
|
|
4
|
+
deactivate,
|
|
5
|
+
deactivateChildCategories,
|
|
6
|
+
remove,
|
|
7
|
+
removeChildCategories,
|
|
8
|
+
} from './destroy'
|
|
9
|
+
|
|
10
|
+
// Export all functions from fetch.ts
|
|
11
|
+
export {
|
|
12
|
+
compareCategoryGrowth,
|
|
13
|
+
fetchActive,
|
|
14
|
+
fetchAll,
|
|
15
|
+
fetchByDisplayOrder,
|
|
16
|
+
fetchById,
|
|
17
|
+
fetchByName,
|
|
18
|
+
fetchCategoryTree,
|
|
19
|
+
fetchChildCategories,
|
|
20
|
+
fetchRootCategories,
|
|
21
|
+
fetchStats,
|
|
22
|
+
} from './fetch'
|
|
23
|
+
|
|
24
|
+
// Export all functions from store.ts
|
|
25
|
+
export {
|
|
26
|
+
findOrCreateByName,
|
|
27
|
+
store,
|
|
28
|
+
} from './store'
|
|
29
|
+
|
|
30
|
+
// Export all functions from update.ts
|
|
31
|
+
export {
|
|
32
|
+
update,
|
|
33
|
+
updateActiveStatus,
|
|
34
|
+
updateDisplayOrder,
|
|
35
|
+
updateParent,
|
|
36
|
+
} from './update'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
type CategoryJsonResponse = ModelRow<typeof Category>
|
|
2
|
+
type NewCategory = NewModelData<typeof Category>
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { slug } from '@stacksjs/strings'
|
|
5
|
+
import { fetchById } from './fetch'
|
|
6
|
+
|
|
7
|
+
export interface CategorizableTable {
|
|
8
|
+
id: number
|
|
9
|
+
name: string
|
|
10
|
+
slug: string
|
|
11
|
+
description?: string
|
|
12
|
+
is_active: boolean
|
|
13
|
+
categorizable_type: string
|
|
14
|
+
created_at?: string
|
|
15
|
+
updated_at?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a new category
|
|
20
|
+
*
|
|
21
|
+
* @param data The category data to store
|
|
22
|
+
* @returns The newly created category record
|
|
23
|
+
*/
|
|
24
|
+
export async function store(data: NewCategory): Promise<CategoryJsonResponse> {
|
|
25
|
+
try {
|
|
26
|
+
// Add missing required properties
|
|
27
|
+
const categoryData = {
|
|
28
|
+
...data,
|
|
29
|
+
slug: data.name?.toLowerCase().replace(/\s+/g, '-') || '',
|
|
30
|
+
categorizable_type: 'product',
|
|
31
|
+
display_order: 0,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await db
|
|
35
|
+
.insertInto('categories')
|
|
36
|
+
.values(categoryData)
|
|
37
|
+
.executeTakeFirst()
|
|
38
|
+
|
|
39
|
+
if (!result)
|
|
40
|
+
throw new Error('Failed to create category')
|
|
41
|
+
|
|
42
|
+
const insertId = Number(result.insertId) || Number(result.numInsertedOrUpdatedRows)
|
|
43
|
+
|
|
44
|
+
const model = await fetchById(insertId)
|
|
45
|
+
|
|
46
|
+
if (!model)
|
|
47
|
+
throw new Error('Failed to create category')
|
|
48
|
+
|
|
49
|
+
return model
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
if (error.message.includes('Duplicate entry') && error.message.includes('name')) {
|
|
54
|
+
throw new Error('A category with this name already exists')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error(`Failed to create category: ${error.message}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function findOrCreateByName(data: Partial<CategorizableTable>): Promise<CategoryJsonResponse> {
|
|
65
|
+
if (!data.name)
|
|
66
|
+
throw new Error('Name is required')
|
|
67
|
+
|
|
68
|
+
const existingCategory = await db
|
|
69
|
+
.selectFrom('categories')
|
|
70
|
+
.selectAll()
|
|
71
|
+
.where('name', '=', data.name)
|
|
72
|
+
.executeTakeFirst()
|
|
73
|
+
|
|
74
|
+
if (existingCategory)
|
|
75
|
+
return existingCategory as CategoryJsonResponse
|
|
76
|
+
|
|
77
|
+
const categoryData = {
|
|
78
|
+
name: data.name,
|
|
79
|
+
slug: slug(data.name),
|
|
80
|
+
is_active: data.is_active ?? true,
|
|
81
|
+
categorizable_type: data.categorizable_type ?? 'default',
|
|
82
|
+
display_order: 0,
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return await store(categoryData)
|
|
87
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { db } from '@stacksjs/database'
|
|
2
|
+
import { formatDate } from '@stacksjs/orm'
|
|
3
|
+
import { fetchById } from './fetch'
|
|
4
|
+
|
|
5
|
+
type CategoryRow = ModelRow<typeof Category>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Update a category by ID
|
|
9
|
+
*
|
|
10
|
+
* @param id The ID of the category to update
|
|
11
|
+
* @param request The updated category data
|
|
12
|
+
* @returns The updated category record
|
|
13
|
+
*/
|
|
14
|
+
export async function update(id: number, request: RequestInstance<typeof Category>): Promise<CategoryRow | undefined> {
|
|
15
|
+
// Validate the request data
|
|
16
|
+
await request.validate()
|
|
17
|
+
|
|
18
|
+
// Check if category exists
|
|
19
|
+
const existingCategory = await fetchById(id)
|
|
20
|
+
if (!existingCategory) {
|
|
21
|
+
throw new Error(`Category with ID ${id} not found`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create update data object using request fields
|
|
25
|
+
const updateData = {
|
|
26
|
+
name: request.get('name'),
|
|
27
|
+
description: request.get('description'),
|
|
28
|
+
imageUrl: request.get('imageUrl'),
|
|
29
|
+
isActive: request.get('isActive'),
|
|
30
|
+
parentCategoryId: request.get('parentCategoryId'),
|
|
31
|
+
displayOrder: request.get('displayOrder'),
|
|
32
|
+
updated_at: formatDate(new Date()),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If no fields to update, just return the existing category
|
|
36
|
+
if (Object.keys(updateData).length === 0) {
|
|
37
|
+
return existingCategory
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Update the category
|
|
42
|
+
await db
|
|
43
|
+
.updateTable('categories')
|
|
44
|
+
.set(updateData)
|
|
45
|
+
.where('id', '=', id)
|
|
46
|
+
.execute()
|
|
47
|
+
|
|
48
|
+
// Fetch and return the updated category
|
|
49
|
+
return await fetchById(id)
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
// Handle duplicate name error
|
|
54
|
+
if (error.message.includes('Duplicate entry') && error.message.includes('name')) {
|
|
55
|
+
throw new Error('A category with this name already exists')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(`Failed to update category: ${error.message}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update category display order
|
|
67
|
+
*
|
|
68
|
+
* @param id The ID of the category
|
|
69
|
+
* @param newOrder The new display order value
|
|
70
|
+
* @returns The updated category
|
|
71
|
+
*/
|
|
72
|
+
export async function updateDisplayOrder(id: number, newOrder: number): Promise<CategoryRow | undefined> {
|
|
73
|
+
// Check if category exists
|
|
74
|
+
const category = await fetchById(id)
|
|
75
|
+
|
|
76
|
+
if (!category) {
|
|
77
|
+
throw new Error(`Category with ID ${id} not found`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Update the category's display order
|
|
82
|
+
await db
|
|
83
|
+
.updateTable('categories')
|
|
84
|
+
.set({
|
|
85
|
+
display_order: newOrder,
|
|
86
|
+
updated_at: formatDate(new Date()),
|
|
87
|
+
})
|
|
88
|
+
.where('id', '=', id)
|
|
89
|
+
.execute()
|
|
90
|
+
|
|
91
|
+
// Fetch the updated category
|
|
92
|
+
return await fetchById(id)
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
throw new TypeError(`Failed to update category display order: ${error.message}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Update category active status
|
|
105
|
+
*
|
|
106
|
+
* @param id The ID of the category
|
|
107
|
+
* @param isActive Whether the category should be active
|
|
108
|
+
* @returns The updated category
|
|
109
|
+
*/
|
|
110
|
+
export async function updateActiveStatus(id: number, isActive: boolean): Promise<CategoryRow | undefined> {
|
|
111
|
+
// Check if category exists
|
|
112
|
+
const category = await fetchById(id)
|
|
113
|
+
|
|
114
|
+
if (!category) {
|
|
115
|
+
throw new Error(`Category with ID ${id} not found`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Update the category's active status
|
|
120
|
+
await db
|
|
121
|
+
.updateTable('categories')
|
|
122
|
+
.set({
|
|
123
|
+
is_active: isActive,
|
|
124
|
+
updated_at: formatDate(new Date()),
|
|
125
|
+
})
|
|
126
|
+
.where('id', '=', id)
|
|
127
|
+
.execute()
|
|
128
|
+
|
|
129
|
+
// Fetch the updated category
|
|
130
|
+
return await fetchById(id)
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
throw new TypeError(`Failed to update category active status: ${error.message}`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw error
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Move category to a different parent
|
|
143
|
+
*
|
|
144
|
+
* @param id The ID of the category to move
|
|
145
|
+
* @param newParentId The ID of the new parent category, or null to make it a root category
|
|
146
|
+
* @returns The updated category
|
|
147
|
+
*/
|
|
148
|
+
export async function updateParent(id: number, newParentId: string | null): Promise<CategoryRow | undefined> {
|
|
149
|
+
// Check if category exists
|
|
150
|
+
const category = await fetchById(id)
|
|
151
|
+
|
|
152
|
+
if (!category) {
|
|
153
|
+
throw new Error(`Category with ID ${id} not found`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If moving to a parent, check that the parent exists and is not the same category
|
|
157
|
+
if (newParentId) {
|
|
158
|
+
// Convert to number for comparison since id is number
|
|
159
|
+
const newParentIdNum = Number(newParentId)
|
|
160
|
+
|
|
161
|
+
if (newParentIdNum === id) {
|
|
162
|
+
throw new Error('A category cannot be its own parent')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const parentCategory = await fetchById(newParentIdNum)
|
|
166
|
+
if (!parentCategory) {
|
|
167
|
+
throw new Error(`Parent category with ID ${newParentId} not found`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for circular reference
|
|
171
|
+
if (await wouldCreateCircularReference(id, newParentId)) {
|
|
172
|
+
throw new Error('This operation would create a circular reference in the category hierarchy')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Update the category's parent
|
|
178
|
+
const updateObject: { updated_at: string, parent_category_id?: string | undefined } = {
|
|
179
|
+
updated_at: formatDate(new Date()),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Set parent_category_id explicitly based on whether newParentId is null
|
|
183
|
+
if (newParentId === null) {
|
|
184
|
+
await db
|
|
185
|
+
.updateTable('categories')
|
|
186
|
+
.set({
|
|
187
|
+
...updateObject,
|
|
188
|
+
parent_category_id: undefined,
|
|
189
|
+
})
|
|
190
|
+
.where('id', '=', id)
|
|
191
|
+
.execute()
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
await db
|
|
195
|
+
.updateTable('categories')
|
|
196
|
+
.set({
|
|
197
|
+
...updateObject,
|
|
198
|
+
parent_category_id: newParentId,
|
|
199
|
+
})
|
|
200
|
+
.where('id', '=', id)
|
|
201
|
+
.execute()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fetch the updated category
|
|
205
|
+
return await fetchById(id)
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof Error) {
|
|
209
|
+
throw new TypeError(`Failed to update category parent: ${error.message}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
throw error
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Helper function to check if changing a category's parent would create a circular reference
|
|
218
|
+
* @param categoryId ID of the category being moved
|
|
219
|
+
* @param newParentId ID of the new parent
|
|
220
|
+
* @returns boolean indicating if a circular reference would be created
|
|
221
|
+
*/
|
|
222
|
+
async function wouldCreateCircularReference(categoryId: number, newParentId: string): Promise<boolean> {
|
|
223
|
+
// Convert to number for consistency in the check
|
|
224
|
+
let currentParentId = Number(newParentId)
|
|
225
|
+
const visited = new Set<number>()
|
|
226
|
+
|
|
227
|
+
while (currentParentId) {
|
|
228
|
+
// If we've seen this ID before, we have a cycle
|
|
229
|
+
if (visited.has(currentParentId)) {
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// If we've reached the original category, we have a cycle
|
|
234
|
+
if (currentParentId === categoryId) {
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
visited.add(currentParentId)
|
|
239
|
+
|
|
240
|
+
// Get the parent's parent
|
|
241
|
+
const parent = await fetchById(currentParentId)
|
|
242
|
+
if (!parent || !parent.parentCategoryId) {
|
|
243
|
+
// We've reached a root category, no cycle
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
currentParentId = Number(parent.parentCategoryId)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as categories from './categories'
|
|
2
|
+
import * as items from './items'
|
|
3
|
+
import * as manufacturers from './manufacturers'
|
|
4
|
+
import * as reviews from './reviews'
|
|
5
|
+
import * as units from './units'
|
|
6
|
+
import * as variants from './variants'
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
categories,
|
|
10
|
+
items,
|
|
11
|
+
manufacturers,
|
|
12
|
+
reviews,
|
|
13
|
+
units,
|
|
14
|
+
variants,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProductsNamespace {
|
|
18
|
+
categories: typeof categories
|
|
19
|
+
items: typeof items
|
|
20
|
+
manufacturers: typeof manufacturers
|
|
21
|
+
reviews: typeof reviews
|
|
22
|
+
units: typeof units
|
|
23
|
+
variants: typeof variants
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const products: ProductsNamespace = {
|
|
27
|
+
categories,
|
|
28
|
+
items,
|
|
29
|
+
manufacturers,
|
|
30
|
+
reviews,
|
|
31
|
+
units,
|
|
32
|
+
variants,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default products
|