@webbycrown/webbycommerce 1.2.0 → 2.0.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/README.md +26 -3
- package/admin/app.js +3 -0
- package/admin/jsconfig.json +20 -0
- package/admin/src/components/ApiCollectionsContent.jsx +4626 -0
- package/admin/src/components/CompareContent.jsx +300 -0
- package/admin/src/components/ConfigureContent.jsx +407 -0
- package/admin/src/components/Initializer.jsx +64 -0
- package/admin/src/components/LoginRegisterContent.jsx +280 -0
- package/admin/src/components/PluginIcon.jsx +6 -0
- package/admin/src/components/ShippingTypeContent.jsx +230 -0
- package/admin/src/components/SmtpContent.jsx +316 -0
- package/admin/src/components/WishlistContent.jsx +273 -0
- package/admin/src/index.js +81 -0
- package/admin/src/pages/ApiCollections.jsx +169 -0
- package/admin/src/pages/Configure.jsx +55 -0
- package/admin/src/pages/Settings.jsx +93 -0
- package/admin/src/pluginId.js +4 -0
- package/{dist/_chunks/en-CiQ97iC8.js → admin/src/translations/en.json} +712 -574
- package/bin/setup.js +50 -3
- package/package.json +14 -13
- package/server/bootstrap.js +3 -0
- package/server/register.js +3 -0
- package/server/src/bootstrap.js +3826 -0
- package/server/src/components/content-block.json +37 -0
- package/server/src/components/shipping-zone-location.json +27 -0
- package/server/src/config/index.js +7 -0
- package/server/src/content-types/address/index.js +7 -0
- package/server/src/content-types/address/schema.json +74 -0
- package/server/src/content-types/cart/index.js +61 -0
- package/server/src/content-types/cart-item/index.js +79 -0
- package/server/src/content-types/compare.js +73 -0
- package/server/src/content-types/coupon/index.js +7 -0
- package/server/src/content-types/coupon/schema.json +67 -0
- package/server/src/content-types/index.js +42 -0
- package/server/src/content-types/order/index.js +7 -0
- package/server/src/content-types/order/schema.json +121 -0
- package/server/src/content-types/payment-transaction/index.js +7 -0
- package/server/src/content-types/payment-transaction/schema.json +73 -0
- package/server/src/content-types/product/index.js +7 -0
- package/server/src/content-types/product/schema.json +104 -0
- package/server/src/content-types/product-attribute/index.js +7 -0
- package/server/src/content-types/product-attribute/schema.json +80 -0
- package/server/src/content-types/product-attribute-value/index.js +7 -0
- package/server/src/content-types/product-attribute-value/schema.json +52 -0
- package/server/src/content-types/product-category/index.js +7 -0
- package/server/src/content-types/product-category/schema.json +54 -0
- package/server/src/content-types/product-tag/index.js +7 -0
- package/server/src/content-types/product-tag/schema.json +38 -0
- package/server/src/content-types/product-variation/index.js +7 -0
- package/server/src/content-types/product-variation/schema.json +74 -0
- package/server/src/content-types/shipping-method/index.js +7 -0
- package/server/src/content-types/shipping-method/schema.json +91 -0
- package/server/src/content-types/shipping-rate/index.js +7 -0
- package/server/src/content-types/shipping-rate/schema.json +73 -0
- package/server/src/content-types/shipping-rule/index.js +7 -0
- package/server/src/content-types/shipping-rule/schema.json +84 -0
- package/server/src/content-types/shipping-zone/index.js +7 -0
- package/server/src/content-types/shipping-zone/schema.json +57 -0
- package/server/src/content-types/wishlist.js +66 -0
- package/server/src/controllers/address.js +374 -0
- package/server/src/controllers/auth.js +1409 -0
- package/server/src/controllers/cart.js +337 -0
- package/server/src/controllers/category.js +388 -0
- package/server/src/controllers/compare.js +246 -0
- package/server/src/controllers/controller.js +168 -0
- package/server/src/controllers/ecommerce.js +20 -0
- package/server/src/controllers/index.js +34 -0
- package/server/src/controllers/order.js +1100 -0
- package/server/src/controllers/payment.js +243 -0
- package/server/src/controllers/product.js +1006 -0
- package/server/src/controllers/productTag.js +370 -0
- package/server/src/controllers/productVariation.js +181 -0
- package/server/src/controllers/shipping.js +1046 -0
- package/server/src/controllers/wishlist.js +332 -0
- package/server/src/destroy.js +6 -0
- package/server/src/index.js +26 -0
- package/server/src/middlewares/index.js +4 -0
- package/server/src/policies/index.js +4 -0
- package/server/src/register.js +67 -0
- package/server/src/routes/index.js +1130 -0
- package/server/src/services/cart.js +531 -0
- package/server/src/services/compare.js +300 -0
- package/server/src/services/index.js +16 -0
- package/server/src/services/service.js +19 -0
- package/server/src/services/shipping.js +513 -0
- package/server/src/services/wishlist.js +238 -0
- package/server/src/utils/check-ecommerce-permission.js +204 -0
- package/server/src/utils/extend-user-schema.js +161 -0
- package/server/src/utils/seed-data.js +639 -0
- package/server/src/utils/send-email.js +98 -0
- package/strapi-server.js +1 -6
- package/dist/_chunks/Settings-DZXAkI24.js +0 -31539
- package/dist/_chunks/Settings-yLx-YvVy.mjs +0 -31520
- package/dist/_chunks/en-DE15m4xZ.mjs +0 -574
- package/dist/_chunks/index-CXGrFKp6.mjs +0 -128
- package/dist/_chunks/index-DgocXUgC.js +0 -127
- package/dist/admin/index.js +0 -3
- package/dist/admin/index.mjs +0 -4
- package/dist/robots.txt +0 -3
- package/dist/server/index.js +0 -27078
- package/dist/uploads/.gitkeep +0 -0
- package/dist/uploads/accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/large_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/medium_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/medium_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/medium_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/medium_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/medium_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/medium_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/medium_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/medium_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/medium_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/small_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/small_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/small_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/small_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/small_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/small_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/small_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/small_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/small_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/small_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_accessories_category_2a5631094b.jpeg +0 -0
- package/dist/uploads/thumbnail_beauty_personal_care_category_57f8a8f1e3.jpeg +0 -0
- package/dist/uploads/thumbnail_books_category_a9a253eada.jpeg +0 -0
- package/dist/uploads/thumbnail_classic_cotton_tshirt_1_cd713425f6.png +0 -0
- package/dist/uploads/thumbnail_clothing_category_d5c60ef07b.jpeg +0 -0
- package/dist/uploads/thumbnail_daviddoe_strapi_adbcd41787.jpeg +0 -0
- package/dist/uploads/thumbnail_electronics_category_fc3e5ef571.jpeg +0 -0
- package/dist/uploads/thumbnail_ergonomic_office_chair_1_c751cffb07.png +0 -0
- package/dist/uploads/thumbnail_home_garden_category_4f6eb3f8d6.jpeg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_11f295b9c0.jpg +0 -0
- package/dist/uploads/thumbnail_istockphoto_1188462138_612x612_396fb272fd.jpg +0 -0
- package/dist/uploads/thumbnail_leather_travel_backpack_1_238bc1ae4d.png +0 -0
- package/dist/uploads/thumbnail_mechanical_keyboard_pro_1_0cd391a6ac.png +0 -0
- package/dist/uploads/thumbnail_predictive_maintenance_icons_industry_automation_600nw_2685943461_e18a8aa3b0.webp +0 -0
- package/dist/uploads/thumbnail_smart_watch_series_5_1_cdc2511fb7.png +0 -0
- package/dist/uploads/thumbnail_smartphone_x_pro_1_c3f0cbd080.png +0 -0
- package/dist/uploads/thumbnail_the_great_gatsby_special_1_2e7c76d997.png +0 -0
- package/dist/uploads/thumbnail_wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/thumbnail_yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- package/dist/uploads/webby-commerce.png +0 -0
- package/dist/uploads/wireless_headphones_1_fa75cd50c3.png +0 -0
- package/dist/uploads/yoga_mat_premium_1_01f9a3b5fa.png +0 -0
- /package/{dist → server/src}/data/demo-data.json +0 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = 'webbycommerce';
|
|
4
|
+
const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
|
|
5
|
+
|
|
6
|
+
const uniqBy = (items, keyFn) => {
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
return (Array.isArray(items) ? items : []).filter((item) => {
|
|
9
|
+
const key = keyFn(item);
|
|
10
|
+
if (seen.has(key)) return false;
|
|
11
|
+
seen.add(key);
|
|
12
|
+
return true;
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
/**
|
|
18
|
+
* List tags
|
|
19
|
+
*/
|
|
20
|
+
async getTags(ctx) {
|
|
21
|
+
try {
|
|
22
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { search, limit = 50, start = 0 } = ctx.query;
|
|
27
|
+
|
|
28
|
+
// Strapi v5 stores drafts + published versions as separate rows (document model).
|
|
29
|
+
// For storefront APIs we only want published records, otherwise you can see duplicates.
|
|
30
|
+
const where = { publishedAt: { $notNull: true } };
|
|
31
|
+
if (search) {
|
|
32
|
+
where.name = { $containsi: search };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tags = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
|
|
36
|
+
where,
|
|
37
|
+
limit: parseInt(limit, 10),
|
|
38
|
+
start: parseInt(start, 10),
|
|
39
|
+
orderBy: { createdAt: 'desc' },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const total = await strapi.db.query('plugin::webbycommerce.product-tag').count({ where });
|
|
43
|
+
|
|
44
|
+
ctx.send({ data: tags, meta: { total, limit: parseInt(limit, 10), start: parseInt(start, 10) } });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getTags:`, error);
|
|
47
|
+
ctx.internalServerError('Failed to fetch tags. Please try again.');
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get single tag
|
|
53
|
+
*/
|
|
54
|
+
async getTag(ctx) {
|
|
55
|
+
try {
|
|
56
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rawId = ctx.params?.id;
|
|
61
|
+
const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
|
|
62
|
+
|
|
63
|
+
if (!idOrSlug) {
|
|
64
|
+
return ctx.badRequest('Tag ID or slug is required.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if it's a numeric ID or a slug
|
|
68
|
+
const isNumericId = /^[0-9]+$/.test(idOrSlug);
|
|
69
|
+
let tag;
|
|
70
|
+
|
|
71
|
+
if (isNumericId) {
|
|
72
|
+
// Query by ID
|
|
73
|
+
tag = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({
|
|
74
|
+
where: { id: idOrSlug },
|
|
75
|
+
populate: {
|
|
76
|
+
products: {
|
|
77
|
+
where: { publishedAt: { $notNull: true } },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
// Query by slug
|
|
83
|
+
const results = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
|
|
84
|
+
where: { slug: idOrSlug, publishedAt: { $notNull: true } },
|
|
85
|
+
limit: 1,
|
|
86
|
+
orderBy: { publishedAt: 'desc', id: 'desc' },
|
|
87
|
+
populate: {
|
|
88
|
+
products: {
|
|
89
|
+
where: { publishedAt: { $notNull: true } },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
tag = results?.[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!tag) {
|
|
98
|
+
return ctx.notFound('Tag not found.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Filter and deduplicate products by documentId (Strapi v5 document model)
|
|
102
|
+
if (Array.isArray(tag?.products)) {
|
|
103
|
+
const publishedOnly = tag.products.filter((p) => p && p.publishedAt);
|
|
104
|
+
tag.products = uniqBy(publishedOnly, (p) => p.documentId || String(p.id));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ctx.send({ data: tag });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in getTag:`, error);
|
|
110
|
+
ctx.internalServerError('Failed to fetch tag. Please try again.');
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create tag
|
|
116
|
+
*/
|
|
117
|
+
async createTag(ctx) {
|
|
118
|
+
try {
|
|
119
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { name, slug } = ctx.request.body || {};
|
|
124
|
+
|
|
125
|
+
if (!name) {
|
|
126
|
+
return ctx.badRequest('Name is required.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Use entityService for Strapi v5 document model compatibility
|
|
130
|
+
// This ensures tags appear in the content manager
|
|
131
|
+
const tag = await strapi.entityService.create('plugin::webbycommerce.product-tag', {
|
|
132
|
+
data: {
|
|
133
|
+
name,
|
|
134
|
+
slug: slug || undefined,
|
|
135
|
+
publishedAt: new Date(), // Set publishedAt so it appears in content manager
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Re-fetch with populated relations
|
|
140
|
+
const populated = await strapi.entityService.findOne('plugin::webbycommerce.product-tag', tag.id, {
|
|
141
|
+
populate: ['products'],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
ctx.send({ data: populated || tag });
|
|
145
|
+
} catch (error) {
|
|
146
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in createTag:`, error);
|
|
147
|
+
ctx.internalServerError('Failed to create tag. Please try again.');
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Update tag
|
|
153
|
+
*/
|
|
154
|
+
async updateTag(ctx) {
|
|
155
|
+
try {
|
|
156
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { id } = ctx.params;
|
|
161
|
+
const updateData = ctx.request.body || {};
|
|
162
|
+
|
|
163
|
+
const updated = await strapi.db.query('plugin::webbycommerce.product-tag').update({
|
|
164
|
+
where: { id },
|
|
165
|
+
data: updateData,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!updated) {
|
|
169
|
+
return ctx.notFound('Tag not found.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ctx.send({ data: updated });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in updateTag:`, error);
|
|
175
|
+
ctx.internalServerError('Failed to update tag. Please try again.');
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Delete tag
|
|
181
|
+
*/
|
|
182
|
+
async deleteTag(ctx) {
|
|
183
|
+
try {
|
|
184
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { id } = ctx.params;
|
|
189
|
+
|
|
190
|
+
const existing = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({ where: { id } });
|
|
191
|
+
if (!existing) {
|
|
192
|
+
return ctx.notFound('Tag not found.');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await strapi.db.query('plugin::webbycommerce.product-tag').delete({ where: { id } });
|
|
196
|
+
|
|
197
|
+
ctx.send({ data: { id } });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in deleteTag:`, error);
|
|
200
|
+
ctx.internalServerError('Failed to delete tag. Please try again.');
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Standard Strapi controller methods for content manager
|
|
205
|
+
async find(ctx) {
|
|
206
|
+
try {
|
|
207
|
+
const { search, limit = 25, start = 0, sort = 'createdAt:desc' } = ctx.query;
|
|
208
|
+
|
|
209
|
+
const where = { publishedAt: { $notNull: true } };
|
|
210
|
+
|
|
211
|
+
if (search) {
|
|
212
|
+
where.name = { $containsi: search };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const tags = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
|
|
216
|
+
where,
|
|
217
|
+
limit: parseInt(limit, 10),
|
|
218
|
+
start: parseInt(start, 10),
|
|
219
|
+
orderBy: sort.split(':').reduce((acc, val, i) => {
|
|
220
|
+
if (i === 0) acc[val] = 'asc';
|
|
221
|
+
else acc[val] = 'desc';
|
|
222
|
+
return acc;
|
|
223
|
+
}, {}),
|
|
224
|
+
populate: ['products'],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const total = await strapi.db.query('plugin::webbycommerce.product-tag').count({ where });
|
|
228
|
+
|
|
229
|
+
ctx.send({
|
|
230
|
+
data: tags,
|
|
231
|
+
meta: {
|
|
232
|
+
pagination: {
|
|
233
|
+
page: Math.floor(parseInt(start, 10) / parseInt(limit, 10)) + 1,
|
|
234
|
+
pageSize: parseInt(limit, 10),
|
|
235
|
+
pageCount: Math.ceil(total / parseInt(limit, 10)),
|
|
236
|
+
total,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
} catch (error) {
|
|
241
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in find:`, error);
|
|
242
|
+
ctx.internalServerError('Failed to fetch tags.');
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
async findOne(ctx) {
|
|
247
|
+
try {
|
|
248
|
+
const rawId = ctx.params?.id;
|
|
249
|
+
const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
|
|
250
|
+
|
|
251
|
+
if (!idOrSlug) {
|
|
252
|
+
return ctx.badRequest('Tag ID or slug is required.');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if it's a numeric ID or a slug
|
|
256
|
+
const isNumericId = /^[0-9]+$/.test(idOrSlug);
|
|
257
|
+
let tag;
|
|
258
|
+
|
|
259
|
+
if (isNumericId) {
|
|
260
|
+
// Query by ID
|
|
261
|
+
tag = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({
|
|
262
|
+
where: { id: idOrSlug, publishedAt: { $notNull: true } },
|
|
263
|
+
populate: {
|
|
264
|
+
products: {
|
|
265
|
+
where: { publishedAt: { $notNull: true } },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
// Query by slug
|
|
271
|
+
const results = await strapi.db.query('plugin::webbycommerce.product-tag').findMany({
|
|
272
|
+
where: { slug: idOrSlug, publishedAt: { $notNull: true } },
|
|
273
|
+
limit: 1,
|
|
274
|
+
orderBy: { publishedAt: 'desc', id: 'desc' },
|
|
275
|
+
populate: {
|
|
276
|
+
products: {
|
|
277
|
+
where: { publishedAt: { $notNull: true } },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
tag = results?.[0];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!tag) {
|
|
286
|
+
return ctx.notFound('Tag not found.');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Filter and deduplicate products by documentId (Strapi v5 document model)
|
|
290
|
+
if (Array.isArray(tag?.products)) {
|
|
291
|
+
const publishedOnly = tag.products.filter((p) => p && p.publishedAt);
|
|
292
|
+
tag.products = uniqBy(publishedOnly, (p) => p.documentId || String(p.id));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
ctx.send({ data: tag });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in findOne:`, error);
|
|
298
|
+
ctx.internalServerError('Failed to fetch tag.');
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
async create(ctx) {
|
|
303
|
+
try {
|
|
304
|
+
const data = ctx.request.body.data || ctx.request.body;
|
|
305
|
+
|
|
306
|
+
if (!data.publishedAt) {
|
|
307
|
+
data.publishedAt = new Date();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Use entityService for Strapi v5 document model compatibility
|
|
311
|
+
const tag = await strapi.entityService.create('plugin::webbycommerce.product-tag', {
|
|
312
|
+
data,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const populated = await strapi.entityService.findOne('plugin::webbycommerce.product-tag', tag.id, {
|
|
316
|
+
populate: ['products'],
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
ctx.send({ data: populated || tag });
|
|
320
|
+
} catch (error) {
|
|
321
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in create:`, error);
|
|
322
|
+
ctx.internalServerError('Failed to create tag.');
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
async update(ctx) {
|
|
327
|
+
try {
|
|
328
|
+
const { id } = ctx.params;
|
|
329
|
+
const data = ctx.request.body.data || ctx.request.body;
|
|
330
|
+
|
|
331
|
+
const tag = await strapi.db.query('plugin::webbycommerce.product-tag').update({
|
|
332
|
+
where: { id },
|
|
333
|
+
data,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!tag) {
|
|
337
|
+
return ctx.notFound('Tag not found.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const populated = await strapi.db.query('plugin::webbycommerce.product-tag').findOne({
|
|
341
|
+
where: { id: tag.id },
|
|
342
|
+
populate: ['products'],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
ctx.send({ data: populated || tag });
|
|
346
|
+
} catch (error) {
|
|
347
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in update:`, error);
|
|
348
|
+
ctx.internalServerError('Failed to update tag.');
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
async delete(ctx) {
|
|
353
|
+
try {
|
|
354
|
+
const { id } = ctx.params;
|
|
355
|
+
|
|
356
|
+
const tag = await strapi.db.query('plugin::webbycommerce.product-tag').delete({
|
|
357
|
+
where: { id },
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (!tag) {
|
|
361
|
+
return ctx.notFound('Tag not found.');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
ctx.send({ data: tag });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in delete:`, error);
|
|
367
|
+
ctx.internalServerError('Failed to delete tag.');
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = 'webbycommerce';
|
|
4
|
+
const { ensureEcommercePermission } = require('../utils/check-ecommerce-permission');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
// Standard Strapi controller methods for content manager
|
|
8
|
+
async find(ctx) {
|
|
9
|
+
try {
|
|
10
|
+
// Check ecommerce permission
|
|
11
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { limit = 25, start = 0, sort = 'createdAt:desc' } = ctx.query;
|
|
16
|
+
|
|
17
|
+
const variations = await strapi.db.query('plugin::webbycommerce.product-variation').findMany({
|
|
18
|
+
limit: parseInt(limit, 10),
|
|
19
|
+
start: parseInt(start, 10),
|
|
20
|
+
orderBy: sort.split(':').reduce((acc, val, i) => {
|
|
21
|
+
if (i === 0) acc[val] = 'asc';
|
|
22
|
+
else acc[val] = 'desc';
|
|
23
|
+
return acc;
|
|
24
|
+
}, {}),
|
|
25
|
+
populate: ['product', 'attributes', 'attributeValues'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const total = await strapi.db.query('plugin::webbycommerce.product-variation').count();
|
|
29
|
+
|
|
30
|
+
ctx.send({
|
|
31
|
+
data: variations,
|
|
32
|
+
meta: {
|
|
33
|
+
pagination: {
|
|
34
|
+
page: Math.floor(parseInt(start, 10) / parseInt(limit, 10)) + 1,
|
|
35
|
+
pageSize: parseInt(limit, 10),
|
|
36
|
+
pageCount: Math.ceil(total / parseInt(limit, 10)),
|
|
37
|
+
total,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in find:`, error);
|
|
43
|
+
ctx.internalServerError('Failed to fetch product variations.');
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async findOne(ctx) {
|
|
48
|
+
try {
|
|
49
|
+
// Check ecommerce permission
|
|
50
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rawId = ctx.params?.id;
|
|
55
|
+
const idOrSlug = typeof rawId === 'string' ? decodeURIComponent(rawId).trim() : '';
|
|
56
|
+
|
|
57
|
+
if (!idOrSlug) {
|
|
58
|
+
return ctx.badRequest('Variation ID or slug is required.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check if it's a numeric ID or a slug
|
|
62
|
+
const isNumericId = /^[0-9]+$/.test(idOrSlug);
|
|
63
|
+
let variation;
|
|
64
|
+
|
|
65
|
+
if (isNumericId) {
|
|
66
|
+
// Query by ID
|
|
67
|
+
variation = await strapi.db.query('plugin::webbycommerce.product-variation').findOne({
|
|
68
|
+
where: { id: idOrSlug },
|
|
69
|
+
populate: ['product', 'attributes', 'attributeValues'],
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
// Query by slug
|
|
73
|
+
const results = await strapi.db.query('plugin::webbycommerce.product-variation').findMany({
|
|
74
|
+
where: { slug: idOrSlug },
|
|
75
|
+
limit: 1,
|
|
76
|
+
orderBy: { id: 'desc' },
|
|
77
|
+
populate: ['product', 'attributes', 'attributeValues'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
variation = results?.[0];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!variation) {
|
|
84
|
+
return ctx.notFound('Product variation not found.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ctx.send({ data: variation });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in findOne:`, error);
|
|
90
|
+
ctx.internalServerError('Failed to fetch product variation.');
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async create(ctx) {
|
|
95
|
+
try {
|
|
96
|
+
// Check ecommerce permission
|
|
97
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = ctx.request.body.data || ctx.request.body;
|
|
102
|
+
|
|
103
|
+
// Ensure slug is handled properly - if not provided, Strapi will auto-generate from name
|
|
104
|
+
if (data.slug === '') {
|
|
105
|
+
data.slug = undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const variation = await strapi.db.query('plugin::webbycommerce.product-variation').create({ data });
|
|
109
|
+
|
|
110
|
+
const populated = await strapi.db.query('plugin::webbycommerce.product-variation').findOne({
|
|
111
|
+
where: { id: variation.id },
|
|
112
|
+
populate: ['product', 'attributes', 'attributeValues'],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ctx.send({ data: populated || variation });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in create:`, error);
|
|
118
|
+
ctx.internalServerError('Failed to create product variation.');
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async update(ctx) {
|
|
123
|
+
try {
|
|
124
|
+
// Check ecommerce permission
|
|
125
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { id } = ctx.params;
|
|
130
|
+
const data = ctx.request.body.data || ctx.request.body;
|
|
131
|
+
|
|
132
|
+
// Ensure slug is handled properly - if empty string, set to undefined to allow auto-generation
|
|
133
|
+
if (data.slug === '') {
|
|
134
|
+
data.slug = undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const variation = await strapi.db.query('plugin::webbycommerce.product-variation').update({
|
|
138
|
+
where: { id },
|
|
139
|
+
data,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!variation) {
|
|
143
|
+
return ctx.notFound('Product variation not found.');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const populated = await strapi.db.query('plugin::webbycommerce.product-variation').findOne({
|
|
147
|
+
where: { id: variation.id },
|
|
148
|
+
populate: ['product', 'attributes', 'attributeValues'],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
ctx.send({ data: populated || variation });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in update:`, error);
|
|
154
|
+
ctx.internalServerError('Failed to update product variation.');
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async delete(ctx) {
|
|
159
|
+
try {
|
|
160
|
+
// Check ecommerce permission
|
|
161
|
+
if (!(await ensureEcommercePermission(ctx))) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { id } = ctx.params;
|
|
166
|
+
|
|
167
|
+
const variation = await strapi.db.query('plugin::webbycommerce.product-variation').delete({
|
|
168
|
+
where: { id },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!variation) {
|
|
172
|
+
return ctx.notFound('Product variation not found.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
ctx.send({ data: variation });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
strapi.log.error(`[${PLUGIN_ID}] Error in delete:`, error);
|
|
178
|
+
ctx.internalServerError('Failed to delete product variation.');
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|