@medusajs/product 3.0.0-snapshot-20251202221811 → 3.0.0-snapshot-20251208164410
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/migrations/Migration20251022153442.d.ts +6 -0
- package/dist/migrations/Migration20251022153442.d.ts.map +1 -0
- package/dist/migrations/Migration20251022153442.js +104 -0
- package/dist/migrations/Migration20251022153442.js.map +1 -0
- package/dist/migrations/Migration20251029150809.d.ts +6 -0
- package/dist/migrations/Migration20251029150809.d.ts.map +1 -0
- package/dist/migrations/Migration20251029150809.js +14 -0
- package/dist/migrations/Migration20251029150809.js.map +1 -0
- package/dist/migrations/Migration20251110180907.d.ts +6 -0
- package/dist/migrations/Migration20251110180907.d.ts.map +1 -0
- package/dist/migrations/Migration20251110180907.js +21 -0
- package/dist/migrations/Migration20251110180907.js.map +1 -0
- package/dist/migrations/Migration20251113183352.d.ts +6 -0
- package/dist/migrations/Migration20251113183352.d.ts.map +1 -0
- package/dist/migrations/Migration20251113183352.js +32 -0
- package/dist/migrations/Migration20251113183352.js.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +5 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/product-category.d.ts +155 -6
- package/dist/models/product-category.d.ts.map +1 -1
- package/dist/models/product-collection.d.ts +155 -6
- package/dist/models/product-collection.d.ts.map +1 -1
- package/dist/models/product-image.d.ts +418 -14
- package/dist/models/product-image.d.ts.map +1 -1
- package/dist/models/product-option-value.d.ts +369 -12
- package/dist/models/product-option-value.d.ts.map +1 -1
- package/dist/models/product-option-value.js +14 -3
- package/dist/models/product-option-value.js.map +1 -1
- package/dist/models/product-option.d.ts +233 -6
- package/dist/models/product-option.d.ts.map +1 -1
- package/dist/models/product-option.js +4 -12
- package/dist/models/product-option.js.map +1 -1
- package/dist/models/product-product-option-value.d.ts +2213 -0
- package/dist/models/product-product-option-value.d.ts.map +1 -0
- package/dist/models/product-product-option-value.js +19 -0
- package/dist/models/product-product-option-value.js.map +1 -0
- package/dist/models/product-product-option.d.ts +1272 -0
- package/dist/models/product-product-option.d.ts.map +1 -0
- package/dist/models/product-product-option.js +24 -0
- package/dist/models/product-product-option.js.map +1 -0
- package/dist/models/product-tag.d.ts +155 -6
- package/dist/models/product-tag.d.ts.map +1 -1
- package/dist/models/product-type.d.ts +155 -6
- package/dist/models/product-type.d.ts.map +1 -1
- package/dist/models/product-variant-product-image.d.ts +780 -26
- package/dist/models/product-variant-product-image.d.ts.map +1 -1
- package/dist/models/product-variant.d.ts +362 -12
- package/dist/models/product-variant.d.ts.map +1 -1
- package/dist/models/product.d.ts +155 -6
- package/dist/models/product.d.ts.map +1 -1
- package/dist/models/product.js +7 -2
- package/dist/models/product.js.map +1 -1
- package/dist/repositories/product-category.d.ts +621 -15
- package/dist/repositories/product-category.d.ts.map +1 -1
- package/dist/repositories/product.d.ts +171 -6
- package/dist/repositories/product.d.ts.map +1 -1
- package/dist/repositories/product.js +50 -5
- package/dist/repositories/product.js.map +1 -1
- package/dist/services/product-module-service.d.ts +35 -2
- package/dist/services/product-module-service.d.ts.map +1 -1
- package/dist/services/product-module-service.js +660 -76
- package/dist/services/product-module-service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -16,8 +16,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
const types_1 = require("@medusajs/framework/types");
|
|
17
17
|
const _models_1 = require("../models");
|
|
18
18
|
const utils_1 = require("@medusajs/framework/utils");
|
|
19
|
-
const events_1 = require("../utils/events");
|
|
20
19
|
const joiner_config_1 = require("./../joiner-config");
|
|
20
|
+
const events_1 = require("../utils/events");
|
|
21
21
|
class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
22
22
|
Product: _models_1.Product,
|
|
23
23
|
ProductCategory: _models_1.ProductCategory,
|
|
@@ -29,7 +29,7 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
29
29
|
ProductVariant: _models_1.ProductVariant,
|
|
30
30
|
ProductImage: _models_1.ProductImage,
|
|
31
31
|
}) {
|
|
32
|
-
constructor({ baseRepository, productRepository, productService, productVariantService, productTagService, productCategoryService, productCollectionService, productImageService, productTypeService, productOptionService, productOptionValueService, productVariantProductImageService, [utils_1.Modules.EVENT_BUS]: eventBusModuleService, }, moduleDeclaration) {
|
|
32
|
+
constructor({ baseRepository, productRepository, productService, productVariantService, productTagService, productCategoryService, productCollectionService, productImageService, productTypeService, productOptionService, productProductOptionService, productProductOptionValueService, productOptionValueService, productVariantProductImageService, [utils_1.Modules.EVENT_BUS]: eventBusModuleService, }, moduleDeclaration) {
|
|
33
33
|
// @ts-ignore
|
|
34
34
|
// eslint-disable-next-line prefer-rest-params
|
|
35
35
|
super(...arguments);
|
|
@@ -44,6 +44,8 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
44
44
|
this.productImageService_ = productImageService;
|
|
45
45
|
this.productTypeService_ = productTypeService;
|
|
46
46
|
this.productOptionService_ = productOptionService;
|
|
47
|
+
this.productProductOptionService_ = productProductOptionService;
|
|
48
|
+
this.productProductOptionValueService_ = productProductOptionValueService;
|
|
47
49
|
this.productOptionValueService_ = productOptionValueService;
|
|
48
50
|
this.productVariantProductImageService_ = productVariantProductImageService;
|
|
49
51
|
this.eventBusModuleService_ = eventBusModuleService;
|
|
@@ -55,10 +57,16 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
55
57
|
async retrieveProduct(productId, config, sharedContext) {
|
|
56
58
|
const relationsSet = new Set(config?.relations ?? []);
|
|
57
59
|
const shouldLoadVariantImages = relationsSet.has("variants.images");
|
|
60
|
+
const shouldFilterOptionValues = relationsSet.has("options.values");
|
|
58
61
|
if (shouldLoadVariantImages) {
|
|
59
62
|
relationsSet.add("variants");
|
|
60
63
|
relationsSet.add("images");
|
|
61
64
|
}
|
|
65
|
+
if (shouldFilterOptionValues) {
|
|
66
|
+
relationsSet.add("options");
|
|
67
|
+
relationsSet.add("product_options");
|
|
68
|
+
relationsSet.add("product_options.values");
|
|
69
|
+
}
|
|
62
70
|
const product = await this.productService_.retrieve(productId, this.getProductFindConfig_({
|
|
63
71
|
...config,
|
|
64
72
|
relations: Array.from(relationsSet),
|
|
@@ -66,6 +74,9 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
66
74
|
if (shouldLoadVariantImages && product.variants && product.images) {
|
|
67
75
|
await this.buildVariantImagesFromProduct(product.variants, product.images, sharedContext);
|
|
68
76
|
}
|
|
77
|
+
if (shouldFilterOptionValues) {
|
|
78
|
+
this.filterOptionValuesByProduct(product);
|
|
79
|
+
}
|
|
69
80
|
return this.baseRepository_.serialize(product);
|
|
70
81
|
}
|
|
71
82
|
// @ts-ignore
|
|
@@ -76,6 +87,11 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
76
87
|
relationsSet.add("variants");
|
|
77
88
|
relationsSet.add("images");
|
|
78
89
|
}
|
|
90
|
+
const shouldFilterOptionValues = relationsSet.has("options.values");
|
|
91
|
+
if (shouldFilterOptionValues) {
|
|
92
|
+
relationsSet.add("product_options");
|
|
93
|
+
relationsSet.add("product_options.values");
|
|
94
|
+
}
|
|
79
95
|
const products = await this.productService_.list(filters, this.getProductFindConfig_({
|
|
80
96
|
...config,
|
|
81
97
|
relations: Array.from(relationsSet),
|
|
@@ -87,11 +103,15 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
106
|
+
if (shouldFilterOptionValues) {
|
|
107
|
+
this.filterOptionValuesByProducts(products);
|
|
108
|
+
}
|
|
90
109
|
return this.baseRepository_.serialize(products);
|
|
91
110
|
}
|
|
92
111
|
// @ts-ignore
|
|
93
112
|
async listAndCountProducts(filters, config, sharedContext) {
|
|
94
113
|
const shouldLoadVariantImages = config?.relations?.includes("variants.images");
|
|
114
|
+
const shouldFilterOptionValues = config?.relations?.includes("options.values");
|
|
95
115
|
// Ensure we load necessary relations
|
|
96
116
|
const relations = [...(config?.relations || [])];
|
|
97
117
|
if (shouldLoadVariantImages) {
|
|
@@ -102,6 +122,14 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
102
122
|
relations.push("images");
|
|
103
123
|
}
|
|
104
124
|
}
|
|
125
|
+
if (shouldFilterOptionValues) {
|
|
126
|
+
if (!relations.includes("product_options")) {
|
|
127
|
+
relations.push("product_options");
|
|
128
|
+
}
|
|
129
|
+
if (!relations.includes("product_options.values")) {
|
|
130
|
+
relations.push("product_options.values");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
105
133
|
const [products, count] = await this.productService_.listAndCount(filters, this.getProductFindConfig_({ ...config, relations }), sharedContext);
|
|
106
134
|
if (shouldLoadVariantImages) {
|
|
107
135
|
for (const product of products) {
|
|
@@ -110,6 +138,9 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
110
138
|
}
|
|
111
139
|
}
|
|
112
140
|
}
|
|
141
|
+
if (shouldFilterOptionValues) {
|
|
142
|
+
this.filterOptionValuesByProducts(products);
|
|
143
|
+
}
|
|
113
144
|
const serializedProducts = await this.baseRepository_.serialize(products);
|
|
114
145
|
return [serializedProducts, count];
|
|
115
146
|
}
|
|
@@ -142,9 +173,11 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
142
173
|
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Unable to create variants without specifying a product_id");
|
|
143
174
|
}
|
|
144
175
|
const productOptions = await this.productOptionService_.list({
|
|
145
|
-
|
|
176
|
+
products: {
|
|
177
|
+
id: [...new Set(data.map((v) => v.product_id))],
|
|
178
|
+
},
|
|
146
179
|
}, {
|
|
147
|
-
relations: ["values"],
|
|
180
|
+
relations: ["values", "products"],
|
|
148
181
|
}, sharedContext);
|
|
149
182
|
const variants = await this.productVariantService_.list({
|
|
150
183
|
product_id: [...new Set(data.map((v) => v.product_id))],
|
|
@@ -204,8 +237,10 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
204
237
|
product_id: v.product_id,
|
|
205
238
|
}));
|
|
206
239
|
const productOptions = await this.productOptionService_.list({
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
products: {
|
|
241
|
+
id: Array.from(new Set(variantsWithProductId.map((v) => v.product_id))),
|
|
242
|
+
},
|
|
243
|
+
}, { relations: ["values", "products"] }, sharedContext);
|
|
209
244
|
const productVariantsWithOptions = ProductModuleService.assignOptionsToVariants(variantsWithProductId, productOptions);
|
|
210
245
|
if (data.some((d) => !!d.options)) {
|
|
211
246
|
ProductModuleService.checkIfVariantWithOptionsAlreadyExists(productVariantsWithOptions, allVariants);
|
|
@@ -313,18 +348,25 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
313
348
|
return Array.isArray(data) ? createdOptions : createdOptions[0];
|
|
314
349
|
}
|
|
315
350
|
async createOptions_(data, sharedContext = {}) {
|
|
316
|
-
if (data.some((v) => !v.product_id)) {
|
|
317
|
-
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Tried to create options without specifying a product_id");
|
|
318
|
-
}
|
|
319
351
|
const normalizedInput = data.map((opt) => {
|
|
352
|
+
Object.keys(opt.ranks ?? []).forEach((value) => {
|
|
353
|
+
if (!opt.values.includes(value)) {
|
|
354
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Value "${value}" is assigned a rank but is not defined in the list of values.`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
320
357
|
return {
|
|
321
358
|
...opt,
|
|
322
359
|
values: opt.values?.map((v) => {
|
|
323
|
-
|
|
360
|
+
// Normalize each value into an object and attach rank if available
|
|
361
|
+
const valueObj = (0, utils_1.isString)(v) ? { value: v } : v;
|
|
362
|
+
const rank = opt.ranks && (0, utils_1.isString)(v)
|
|
363
|
+
? opt.ranks[v]
|
|
364
|
+
: opt.ranks?.[valueObj.value];
|
|
365
|
+
return rank !== undefined ? { ...valueObj, rank } : valueObj;
|
|
324
366
|
}),
|
|
325
367
|
};
|
|
326
368
|
});
|
|
327
|
-
return
|
|
369
|
+
return this.productOptionService_.create(normalizedInput, sharedContext);
|
|
328
370
|
}
|
|
329
371
|
async upsertProductOptions(data, sharedContext = {}) {
|
|
330
372
|
const input = Array.isArray(data) ? data : [data];
|
|
@@ -371,35 +413,255 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
371
413
|
}
|
|
372
414
|
// Data normalization
|
|
373
415
|
const normalizedInput = data.map((opt) => {
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
? {
|
|
382
|
-
// Oftentimes the options are only passed by value without an id, even if they exist in the DB
|
|
383
|
-
values: normalizedValues.map((normVal) => {
|
|
384
|
-
if ("id" in normVal) {
|
|
385
|
-
return normVal;
|
|
386
|
-
}
|
|
387
|
-
const dbVal = dbValues.find((dbVal) => dbVal.value === normVal.value);
|
|
388
|
-
if (!dbVal) {
|
|
389
|
-
return normVal;
|
|
390
|
-
}
|
|
391
|
-
return {
|
|
392
|
-
id: dbVal.id,
|
|
393
|
-
value: normVal.value,
|
|
394
|
-
};
|
|
395
|
-
}),
|
|
416
|
+
const dbOption = dbOptions.find(({ id }) => id === opt.id);
|
|
417
|
+
const dbValues = dbOption?.values || [];
|
|
418
|
+
if (opt.ranks) {
|
|
419
|
+
const validValues = opt.values ?? dbValues.map((v) => v.value);
|
|
420
|
+
Object.keys(opt.ranks).forEach((value) => {
|
|
421
|
+
if (!validValues.includes(value)) {
|
|
422
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Value "${value}" is assigned a rank but is not defined in the list of values.`);
|
|
396
423
|
}
|
|
397
|
-
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
let normalizedValues;
|
|
427
|
+
if (opt.values) {
|
|
428
|
+
// If new values are provided → normalize and apply ranks
|
|
429
|
+
normalizedValues = opt.values.map((v) => {
|
|
430
|
+
const valueObj = (0, utils_1.isString)(v) ? { value: v } : v;
|
|
431
|
+
const rank = opt.ranks && (0, utils_1.isString)(v)
|
|
432
|
+
? opt.ranks[v]
|
|
433
|
+
: opt.ranks?.[valueObj.value];
|
|
434
|
+
const rankedValue = rank !== undefined ? { ...valueObj, rank } : valueObj;
|
|
435
|
+
if ("id" in rankedValue) {
|
|
436
|
+
return rankedValue;
|
|
437
|
+
}
|
|
438
|
+
const dbVal = dbValues.find((dbVal) => dbVal.value === rankedValue.value);
|
|
439
|
+
if (!dbVal) {
|
|
440
|
+
return rankedValue;
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
id: dbVal.id,
|
|
444
|
+
...rankedValue,
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else if (opt.ranks) {
|
|
449
|
+
// If only ranks were provided → update existing DB values with ranks
|
|
450
|
+
normalizedValues = dbValues.map((dbVal) => {
|
|
451
|
+
const rank = opt.ranks[dbVal.value];
|
|
452
|
+
return rank !== undefined
|
|
453
|
+
? { id: dbVal.id, value: dbVal.value, rank }
|
|
454
|
+
: { id: dbVal.id, value: dbVal.value };
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const { ranks, ...cleanOpt } = opt;
|
|
458
|
+
return {
|
|
459
|
+
...cleanOpt,
|
|
460
|
+
...(normalizedValues ? { values: normalizedValues } : {}),
|
|
398
461
|
};
|
|
399
462
|
});
|
|
400
463
|
const { entities: productOptions } = await this.productOptionService_.upsertWithReplace(normalizedInput, { relations: ["values"] }, sharedContext);
|
|
401
464
|
return productOptions;
|
|
402
465
|
}
|
|
466
|
+
async addProductOptionToProduct(data, sharedContext = {}) {
|
|
467
|
+
const productOptionProducts = await this.addProductOptionToProduct_(data, sharedContext);
|
|
468
|
+
return productOptionProducts;
|
|
469
|
+
}
|
|
470
|
+
async addProductOptionToProduct_(data, sharedContext = {}) {
|
|
471
|
+
const pairs = Array.isArray(data) ? data : [data];
|
|
472
|
+
const existingProductOptions = await this.productProductOptionService_.list({
|
|
473
|
+
$or: pairs.map((pair) => ({
|
|
474
|
+
product_id: pair.product_id,
|
|
475
|
+
product_option_id: pair.product_option_id,
|
|
476
|
+
})),
|
|
477
|
+
}, { relations: ["values"] }, sharedContext);
|
|
478
|
+
// Validate value removals when product_option_value_ids are provided - this sets new values for the product <> options meaning some values may be removed thus we need removal here as well
|
|
479
|
+
const validationPairs = [];
|
|
480
|
+
for (const pair of pairs) {
|
|
481
|
+
if (pair.product_option_value_ids) {
|
|
482
|
+
const existingProductOption = existingProductOptions.find((epo) => epo.product_id === pair.product_id &&
|
|
483
|
+
epo.product_option_id === pair.product_option_id);
|
|
484
|
+
if (existingProductOption) {
|
|
485
|
+
const currentValues = Array.isArray(existingProductOption.values)
|
|
486
|
+
? existingProductOption.values
|
|
487
|
+
: existingProductOption.values?.toArray?.() ?? [];
|
|
488
|
+
const currentValueIds = new Set(currentValues.map((v) => v.id));
|
|
489
|
+
const newValueIds = new Set(pair.product_option_value_ids);
|
|
490
|
+
// Find values being removed
|
|
491
|
+
const removedValueIds = Array.from(currentValueIds).filter((id) => !newValueIds.has(id));
|
|
492
|
+
if (removedValueIds.length > 0) {
|
|
493
|
+
validationPairs.push({
|
|
494
|
+
productId: pair.product_id,
|
|
495
|
+
optionId: pair.product_option_id,
|
|
496
|
+
valueIdsToCheck: removedValueIds,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (validationPairs.length > 0) {
|
|
503
|
+
await this.validateOptionRemoval_(validationPairs, sharedContext);
|
|
504
|
+
}
|
|
505
|
+
// Separate pairs: those that need PPO creation vs those that just need value updates
|
|
506
|
+
const pairsNeedingPPOCreation = [];
|
|
507
|
+
for (const pair of pairs) {
|
|
508
|
+
const hasExistingPPO = existingProductOptions.some((epo) => epo.product_id === pair.product_id &&
|
|
509
|
+
epo.product_option_id === pair.product_option_id);
|
|
510
|
+
// Only create PPO if it doesn't exist
|
|
511
|
+
if (!hasExistingPPO) {
|
|
512
|
+
pairsNeedingPPOCreation.push(pair);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
let createdPPOs = [];
|
|
516
|
+
if (pairsNeedingPPOCreation.length > 0) {
|
|
517
|
+
const productProductOptions = await this.productProductOptionService_.create(pairsNeedingPPOCreation, sharedContext);
|
|
518
|
+
createdPPOs = (Array.isArray(productProductOptions)
|
|
519
|
+
? productProductOptions
|
|
520
|
+
: [productProductOptions]);
|
|
521
|
+
}
|
|
522
|
+
// Map all PPOs (existing and newly created) to their pairs
|
|
523
|
+
const ppoMap = new Map();
|
|
524
|
+
// First, add existing PPOs (these take precedence for value updates)
|
|
525
|
+
for (const existingPPO of existingProductOptions) {
|
|
526
|
+
const key = `${existingPPO.product_id}_${existingPPO.product_option_id}`;
|
|
527
|
+
ppoMap.set(key, existingPPO);
|
|
528
|
+
}
|
|
529
|
+
// Then add newly created PPOs (only if they don't already exist in the map)
|
|
530
|
+
for (const ppo of createdPPOs) {
|
|
531
|
+
const key = `${ppo.product_id}_${ppo.product_option_id}`;
|
|
532
|
+
if (!ppoMap.has(key)) {
|
|
533
|
+
ppoMap.set(key, ppo);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const uniqueOptionIds = [...new Set(pairs.map((p) => p.product_option_id))];
|
|
537
|
+
const options = await this.productOptionService_.list({ id: uniqueOptionIds }, { relations: ["values"] }, sharedContext);
|
|
538
|
+
const optionValuesMap = new Map(options.map((opt) => [opt.id, opt.values || []]));
|
|
539
|
+
const valuePairsToCreate = [];
|
|
540
|
+
for (const pair of pairs) {
|
|
541
|
+
const key = `${pair.product_id}_${pair.product_option_id}`;
|
|
542
|
+
const ppo = ppoMap.get(key);
|
|
543
|
+
if (!ppo)
|
|
544
|
+
continue;
|
|
545
|
+
const originalPair = pair;
|
|
546
|
+
if (originalPair) {
|
|
547
|
+
const allValues = optionValuesMap.get(originalPair.product_option_id) || [];
|
|
548
|
+
// If specific value IDs were provided, use only those; otherwise use all values
|
|
549
|
+
const valuesToLink = originalPair.product_option_value_ids
|
|
550
|
+
? allValues.filter((v) => originalPair.product_option_value_ids.includes(v.id))
|
|
551
|
+
: allValues;
|
|
552
|
+
// If updating with specific value_ids, remove old values that aren't in the new list
|
|
553
|
+
if (originalPair.product_option_value_ids) {
|
|
554
|
+
const existingProductOption = existingProductOptions.find((epo) => epo.product_id === originalPair.product_id &&
|
|
555
|
+
epo.product_option_id === originalPair.product_option_id);
|
|
556
|
+
if (existingProductOption) {
|
|
557
|
+
const currentValues = Array.isArray(existingProductOption.values)
|
|
558
|
+
? existingProductOption.values
|
|
559
|
+
: existingProductOption.values?.toArray?.() ?? [];
|
|
560
|
+
const currentValueIds = new Set(currentValues.map((v) => v.id));
|
|
561
|
+
const newValueIds = new Set(originalPair.product_option_value_ids);
|
|
562
|
+
// Find values to remove
|
|
563
|
+
const valuesToRemove = Array.from(currentValueIds).filter((id) => !newValueIds.has(id));
|
|
564
|
+
// Delete removed value links
|
|
565
|
+
if (valuesToRemove.length > 0) {
|
|
566
|
+
await this.productProductOptionValueService_.delete({
|
|
567
|
+
product_product_option_id: existingProductOption.id,
|
|
568
|
+
product_option_value_id: valuesToRemove,
|
|
569
|
+
}, sharedContext);
|
|
570
|
+
// Flush to ensure deletion is persisted
|
|
571
|
+
await sharedContext.transactionManager?.flush?.();
|
|
572
|
+
}
|
|
573
|
+
// refetch PPOs after deletion
|
|
574
|
+
const [reloadedPPO] = await this.productProductOptionService_.list({
|
|
575
|
+
id: existingProductOption.id,
|
|
576
|
+
}, { relations: ["values"] }, sharedContext);
|
|
577
|
+
const reloadedValues = reloadedPPO
|
|
578
|
+
? Array.isArray(reloadedPPO.values)
|
|
579
|
+
? reloadedPPO.values
|
|
580
|
+
: reloadedPPO.values?.toArray?.() ?? []
|
|
581
|
+
: [];
|
|
582
|
+
const reloadedValueIds = new Set(reloadedValues.map((v) => v.id));
|
|
583
|
+
// Only create links for values that don't already exist (check against reloaded values)
|
|
584
|
+
const existingValueIdsSet = reloadedValueIds;
|
|
585
|
+
for (const value of valuesToLink) {
|
|
586
|
+
if (!existingValueIdsSet.has(value.id)) {
|
|
587
|
+
valuePairsToCreate.push({
|
|
588
|
+
product_product_option_id: existingProductOption.id,
|
|
589
|
+
product_option_value_id: value.id,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
// No existing link, create all value links
|
|
596
|
+
for (const value of valuesToLink) {
|
|
597
|
+
valuePairsToCreate.push({
|
|
598
|
+
product_product_option_id: ppo.id,
|
|
599
|
+
product_option_value_id: value.id,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
// No specific value_ids, create all value links (only if PPO is newly created)
|
|
606
|
+
if (!existingProductOptions.find((epo) => epo.product_id === pair.product_id &&
|
|
607
|
+
epo.product_option_id === pair.product_option_id)) {
|
|
608
|
+
for (const value of valuesToLink) {
|
|
609
|
+
valuePairsToCreate.push({
|
|
610
|
+
product_product_option_id: ppo.id,
|
|
611
|
+
product_option_value_id: value.id,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (valuePairsToCreate.length > 0) {
|
|
619
|
+
await this.productProductOptionValueService_.create(valuePairsToCreate, sharedContext);
|
|
620
|
+
}
|
|
621
|
+
// Get all PPOs (existing + created) for return value
|
|
622
|
+
const allPPOsForReturn = [];
|
|
623
|
+
for (const pair of pairs) {
|
|
624
|
+
const key = `${pair.product_id}_${pair.product_option_id}`;
|
|
625
|
+
const ppo = ppoMap.get(key);
|
|
626
|
+
if (ppo) {
|
|
627
|
+
allPPOsForReturn.push(ppo);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (Array.isArray(data)) {
|
|
631
|
+
return allPPOsForReturn.map((ppo) => ({ id: ppo.id }));
|
|
632
|
+
}
|
|
633
|
+
return { id: allPPOsForReturn[0]?.id || createdPPOs[0]?.id };
|
|
634
|
+
}
|
|
635
|
+
async removeProductOptionFromProduct(data, sharedContext = {}) {
|
|
636
|
+
await this.removeProductOptionFromProduct_(data, sharedContext);
|
|
637
|
+
}
|
|
638
|
+
async removeProductOptionFromProduct_(data, sharedContext = {}) {
|
|
639
|
+
const pairs = Array.isArray(data) ? data : [data];
|
|
640
|
+
const productOptionsProducts = await this.productProductOptionService_.list({
|
|
641
|
+
$or: pairs,
|
|
642
|
+
}, { relations: [] }, sharedContext);
|
|
643
|
+
// Validate that no variants are using the options before removal
|
|
644
|
+
const validationPairs = productOptionsProducts
|
|
645
|
+
.map((productOptionProduct) => {
|
|
646
|
+
const productId = productOptionProduct.product_id;
|
|
647
|
+
const optionId = productOptionProduct.product_option_id;
|
|
648
|
+
if (productId && optionId) {
|
|
649
|
+
return {
|
|
650
|
+
productId,
|
|
651
|
+
optionId,
|
|
652
|
+
// Check all values of the option
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
})
|
|
657
|
+
.filter((p) => p !== null);
|
|
658
|
+
if (validationPairs.length > 0) {
|
|
659
|
+
await this.validateOptionRemoval_(validationPairs, sharedContext);
|
|
660
|
+
}
|
|
661
|
+
const productOptionsProductIds = productOptionsProducts.map(({ id }) => id);
|
|
662
|
+
await this.productProductOptionValueService_.delete(productOptionsProductIds.map((id) => ({ product_product_option_id: id })), sharedContext);
|
|
663
|
+
await this.productProductOptionService_.delete(productOptionsProductIds, sharedContext);
|
|
664
|
+
}
|
|
403
665
|
// @ts-expect-error
|
|
404
666
|
async createProductCollections(data, sharedContext = {}) {
|
|
405
667
|
const input = Array.isArray(data) ? data : [data];
|
|
@@ -624,7 +886,41 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
624
886
|
return (0, utils_1.isString)(idOrSelector) ? updatedProducts[0] : updatedProducts;
|
|
625
887
|
}
|
|
626
888
|
async createProducts_(data, sharedContext = {}) {
|
|
627
|
-
const
|
|
889
|
+
const existingOptionIds = data
|
|
890
|
+
.flatMap((p) => p.options ?? [])
|
|
891
|
+
.filter((o) => "id" in o)
|
|
892
|
+
.map((o) => o.id);
|
|
893
|
+
let existingOptions = [];
|
|
894
|
+
if (existingOptionIds.length > 0) {
|
|
895
|
+
existingOptions = await this.productOptionService_.list({ id: existingOptionIds }, { relations: ["values"] }, sharedContext);
|
|
896
|
+
const fetchedIds = new Set(existingOptions.map((opt) => opt.id));
|
|
897
|
+
const missingIds = existingOptionIds.filter((id) => !fetchedIds.has(id));
|
|
898
|
+
if (missingIds.length) {
|
|
899
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Some product options were not found: [${missingIds.join(", ")}]`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const existingOptionsMap = new Map(existingOptions.map((opt) => [opt.id, opt]));
|
|
903
|
+
const hydratedData = data.map((product) => {
|
|
904
|
+
if (!product.options?.length)
|
|
905
|
+
return product;
|
|
906
|
+
const hydratedOptions = product.options.map((option) => {
|
|
907
|
+
if ("id" in option) {
|
|
908
|
+
const dbOption = existingOptionsMap.get(option.id);
|
|
909
|
+
if (!dbOption) {
|
|
910
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Product option with id ${option.id} not found.`);
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
id: dbOption.id,
|
|
914
|
+
title: dbOption.title,
|
|
915
|
+
values: dbOption.values?.map((v) => ({ value: v.value })),
|
|
916
|
+
value_ids: option.value_ids,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
return option;
|
|
920
|
+
});
|
|
921
|
+
return { ...product, options: hydratedOptions };
|
|
922
|
+
});
|
|
923
|
+
const normalizedProducts = (await this.normalizeCreateProductInput(hydratedData, sharedContext));
|
|
628
924
|
for (const product of normalizedProducts) {
|
|
629
925
|
this.validateProductCreatePayload(product);
|
|
630
926
|
}
|
|
@@ -638,6 +934,7 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
638
934
|
}, {}, sharedContext);
|
|
639
935
|
}
|
|
640
936
|
const existingTagsMap = new Map(existingTags.map((tag) => [tag.id, tag]));
|
|
937
|
+
const productOptionsToCreate = new Map();
|
|
641
938
|
const productsToCreate = normalizedProducts.map((product) => {
|
|
642
939
|
const productId = (0, utils_1.generateEntityId)(product.id, "prod");
|
|
643
940
|
product.id = productId;
|
|
@@ -645,6 +942,12 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
645
942
|
;
|
|
646
943
|
product.categories = product.categories.map((category) => category.id);
|
|
647
944
|
}
|
|
945
|
+
if (product.options?.length) {
|
|
946
|
+
const newOptions = product.options.filter((o) => !("id" in o));
|
|
947
|
+
if (newOptions.length) {
|
|
948
|
+
productOptionsToCreate.set(productId, newOptions);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
648
951
|
if (product.variants?.length) {
|
|
649
952
|
const normalizedVariants = product.variants.map((variant) => {
|
|
650
953
|
const variantId = (0, utils_1.generateEntityId)(variant.id, "variant");
|
|
@@ -670,10 +973,79 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
670
973
|
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Tag with id ${tag.id} not found. Please create the tag before associating it with the product.`);
|
|
671
974
|
});
|
|
672
975
|
}
|
|
976
|
+
delete product.options;
|
|
673
977
|
return product;
|
|
674
978
|
});
|
|
675
|
-
const
|
|
676
|
-
|
|
979
|
+
const productToOptionIdsMap = new Map();
|
|
980
|
+
const allOptionsWithIds = [];
|
|
981
|
+
for (const [productId, options] of productOptionsToCreate.entries()) {
|
|
982
|
+
const optionIds = [];
|
|
983
|
+
for (const option of options) {
|
|
984
|
+
const optionId = (0, utils_1.generateEntityId)(undefined, "opt");
|
|
985
|
+
optionIds.push(optionId);
|
|
986
|
+
allOptionsWithIds.push({
|
|
987
|
+
...option,
|
|
988
|
+
id: optionId,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
productToOptionIdsMap.set(productId, optionIds);
|
|
992
|
+
}
|
|
993
|
+
const [createdProducts] = await Promise.all([
|
|
994
|
+
this.productService_.create(productsToCreate, sharedContext),
|
|
995
|
+
allOptionsWithIds.length > 0
|
|
996
|
+
? this.createOptions_(allOptionsWithIds, sharedContext)
|
|
997
|
+
: Promise.resolve([]),
|
|
998
|
+
]);
|
|
999
|
+
const linkPairs = [];
|
|
1000
|
+
for (const product of createdProducts) {
|
|
1001
|
+
const hydratedProduct = hydratedData.find((p) => p.title === product.title);
|
|
1002
|
+
const existingOptions = [];
|
|
1003
|
+
if (hydratedProduct?.options?.length) {
|
|
1004
|
+
for (const option of hydratedProduct.options) {
|
|
1005
|
+
if ("id" in option) {
|
|
1006
|
+
existingOptions.push({
|
|
1007
|
+
id: option.id,
|
|
1008
|
+
value_ids: option.value_ids,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const newOptionIds = productToOptionIdsMap.get(product.id) ?? [];
|
|
1014
|
+
const newOptions = newOptionIds.map((id) => ({ id }));
|
|
1015
|
+
const allOptions = [...existingOptions, ...newOptions];
|
|
1016
|
+
for (const option of allOptions) {
|
|
1017
|
+
const pair = {
|
|
1018
|
+
product_id: product.id,
|
|
1019
|
+
product_option_id: option.id,
|
|
1020
|
+
product_option_value_ids: option.value_ids
|
|
1021
|
+
? option.value_ids
|
|
1022
|
+
: undefined,
|
|
1023
|
+
};
|
|
1024
|
+
linkPairs.push(pair);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (linkPairs.length > 0) {
|
|
1028
|
+
await this.addProductOptionToProduct_(linkPairs, sharedContext);
|
|
1029
|
+
}
|
|
1030
|
+
await sharedContext.transactionManager.flush();
|
|
1031
|
+
const productIds = createdProducts.map((p) => p.id);
|
|
1032
|
+
const productsWithOptions = await this.productService_.list({ id: productIds }, {
|
|
1033
|
+
relations: [
|
|
1034
|
+
"options",
|
|
1035
|
+
"options.values",
|
|
1036
|
+
"options.products",
|
|
1037
|
+
"product_options",
|
|
1038
|
+
"product_options.values",
|
|
1039
|
+
"variants",
|
|
1040
|
+
"images",
|
|
1041
|
+
"tags",
|
|
1042
|
+
],
|
|
1043
|
+
}, sharedContext);
|
|
1044
|
+
// Filter option values to only include those associated with each product
|
|
1045
|
+
this.filterOptionValuesByProducts(productsWithOptions);
|
|
1046
|
+
const productIdOrder = new Map(productIds.map((id, index) => [id, index]));
|
|
1047
|
+
const orderedProductsWithOptions = [...productsWithOptions].sort((a, b) => (productIdOrder.get(a.id) ?? 0) - (productIdOrder.get(b.id) ?? 0));
|
|
1048
|
+
return orderedProductsWithOptions;
|
|
677
1049
|
}
|
|
678
1050
|
async updateProducts_(data, sharedContext = {}) {
|
|
679
1051
|
// We have to do that manually because this method is bypassing the product service and goes
|
|
@@ -686,23 +1058,73 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
686
1058
|
.getEventManager()
|
|
687
1059
|
.registerSubscriber(new subscriber(sharedContext));
|
|
688
1060
|
}
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
id
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1061
|
+
const allOptionIds = data
|
|
1062
|
+
.flatMap((p) => p.option_ids ?? [])
|
|
1063
|
+
.filter((id) => !!id);
|
|
1064
|
+
const [originalProducts, existingOptions] = await Promise.all([
|
|
1065
|
+
this.productService_.list({ id: data.map((d) => d.id) }, {
|
|
1066
|
+
relations: ["options", "options.values", "options.products", "tags"],
|
|
1067
|
+
}, sharedContext),
|
|
1068
|
+
allOptionIds.length
|
|
1069
|
+
? this.productOptionService_.list({ id: allOptionIds }, {
|
|
1070
|
+
relations: ["values", "products"],
|
|
1071
|
+
}, sharedContext)
|
|
1072
|
+
: Promise.resolve([]),
|
|
1073
|
+
]);
|
|
1074
|
+
if (allOptionIds.length && existingOptions.length !== allOptionIds.length) {
|
|
1075
|
+
const found = new Set(existingOptions.map((opt) => opt.id));
|
|
1076
|
+
const missing = allOptionIds.filter((id) => !found.has(id));
|
|
1077
|
+
if (missing.length) {
|
|
1078
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Some product options were not found: [${missing.join(", ")}]`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const linkPairs = [];
|
|
1082
|
+
const unlinkPairs = [];
|
|
1083
|
+
for (const product of data) {
|
|
1084
|
+
if (!product.option_ids) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const newOptionIds = new Set(product.option_ids);
|
|
1088
|
+
const existingOptionIds = new Set(originalProducts
|
|
1089
|
+
.find((p) => p.id === product.id)
|
|
1090
|
+
?.options?.map((o) => o.id) ?? []);
|
|
1091
|
+
for (const optionId of newOptionIds) {
|
|
1092
|
+
if (!existingOptionIds.has(optionId)) {
|
|
1093
|
+
linkPairs.push({
|
|
1094
|
+
product_id: product.id,
|
|
1095
|
+
product_option_id: optionId,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
for (const optionId of existingOptionIds) {
|
|
1100
|
+
if (!newOptionIds.has(optionId)) {
|
|
1101
|
+
unlinkPairs.push({
|
|
1102
|
+
product_id: product.id,
|
|
1103
|
+
product_option_id: optionId,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
delete product.option_ids;
|
|
1108
|
+
}
|
|
1109
|
+
await Promise.all([
|
|
1110
|
+
linkPairs.length &&
|
|
1111
|
+
this.addProductOptionToProduct_(linkPairs, sharedContext),
|
|
1112
|
+
unlinkPairs.length &&
|
|
1113
|
+
this.removeProductOptionFromProduct_(unlinkPairs, sharedContext),
|
|
1114
|
+
]);
|
|
1115
|
+
await sharedContext.transactionManager.flush();
|
|
1116
|
+
const normalizedProducts = (await this.normalizeUpdateProductInput(data));
|
|
697
1117
|
for (const product of normalizedProducts) {
|
|
698
1118
|
this.validateProductUpdatePayload(product);
|
|
699
1119
|
}
|
|
700
1120
|
const updatedProducts = await this.productRepository_.deepUpdate(normalizedProducts, ProductModuleService.validateVariantOptions, sharedContext);
|
|
1121
|
+
// Filter option values to only include those associated with each product
|
|
1122
|
+
this.filterOptionValuesByProducts(updatedProducts);
|
|
701
1123
|
return updatedProducts;
|
|
702
1124
|
}
|
|
703
1125
|
// @ts-expect-error
|
|
704
1126
|
async updateProductOptionValues(idOrSelector, data, sharedContext = {}) {
|
|
705
|
-
// TODO: There is a
|
|
1127
|
+
// TODO: There is a mismatch in the API which lead to function with different number of
|
|
706
1128
|
// arguments. Therefore, applying the MedusaContext() decorator to the function will not work
|
|
707
1129
|
// because the context arg index will differ from method to method.
|
|
708
1130
|
sharedContext.messageAggregator ??= new utils_1.MessageAggregator();
|
|
@@ -777,7 +1199,22 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
777
1199
|
async normalizeCreateProductInput(products, sharedContext = {}) {
|
|
778
1200
|
const products_ = Array.isArray(products) ? products : [products];
|
|
779
1201
|
const normalizedProducts = (await this.normalizeUpdateProductInput(products_));
|
|
780
|
-
for (const productData of normalizedProducts) {
|
|
1202
|
+
for await (const productData of normalizedProducts) {
|
|
1203
|
+
if (productData.options?.length) {
|
|
1204
|
+
;
|
|
1205
|
+
productData.options = productData.options?.map((option) => {
|
|
1206
|
+
return {
|
|
1207
|
+
title: option.title,
|
|
1208
|
+
values: option.values?.map((value) => {
|
|
1209
|
+
return {
|
|
1210
|
+
value: value,
|
|
1211
|
+
};
|
|
1212
|
+
}),
|
|
1213
|
+
is_exclusive: option.is_exclusive ?? true, // Always default to true for options created from product creation
|
|
1214
|
+
...(option.id ? { id: option.id } : {}),
|
|
1215
|
+
};
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
781
1218
|
if (!productData.handle && productData.title) {
|
|
782
1219
|
productData.handle = (0, utils_1.toHandle)(productData.title);
|
|
783
1220
|
}
|
|
@@ -820,39 +1257,12 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
820
1257
|
*/
|
|
821
1258
|
async normalizeUpdateProductInput(products, originalProducts) {
|
|
822
1259
|
const products_ = Array.isArray(products) ? products : [products];
|
|
823
|
-
const productsIds = products_.map((p) => p.id).filter(Boolean);
|
|
824
|
-
let dbOptions = [];
|
|
825
|
-
if (productsIds.length) {
|
|
826
|
-
// Re map options to handle non serialized data as well
|
|
827
|
-
dbOptions =
|
|
828
|
-
originalProducts
|
|
829
|
-
?.flatMap((originalProduct) => originalProduct.options.map((option) => option))
|
|
830
|
-
.filter(Boolean) ?? [];
|
|
831
|
-
}
|
|
832
1260
|
const normalizedProducts = [];
|
|
833
1261
|
for (const product of products_) {
|
|
834
1262
|
const productData = { ...product };
|
|
835
1263
|
if (productData.is_giftcard) {
|
|
836
1264
|
productData.discountable = false;
|
|
837
1265
|
}
|
|
838
|
-
if (productData.options?.length) {
|
|
839
|
-
;
|
|
840
|
-
productData.options = productData.options?.map((option) => {
|
|
841
|
-
const dbOption = dbOptions.find((o) => (o.title === option.title || o.id === option.id) &&
|
|
842
|
-
o.product_id === productData.id);
|
|
843
|
-
return {
|
|
844
|
-
title: option.title,
|
|
845
|
-
values: option.values?.map((value) => {
|
|
846
|
-
const dbValue = dbOption?.values?.find((val) => val.value === value);
|
|
847
|
-
return {
|
|
848
|
-
value: value,
|
|
849
|
-
...(dbValue ? { id: dbValue.id } : {}),
|
|
850
|
-
};
|
|
851
|
-
}),
|
|
852
|
-
...(dbOption ? { id: dbOption.id } : {}),
|
|
853
|
-
};
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
1266
|
if (productData.tag_ids) {
|
|
857
1267
|
;
|
|
858
1268
|
productData.tags = productData.tag_ids.map((cid) => ({
|
|
@@ -891,7 +1301,10 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
891
1301
|
const variantsWithOptions = ProductModuleService.assignOptionsToVariants(variants.map((v) => ({
|
|
892
1302
|
...v,
|
|
893
1303
|
// adding product_id to the variant to make it valid for the assignOptionsToVariants function
|
|
894
|
-
|
|
1304
|
+
// get product_id from the first product in the products array of the first option
|
|
1305
|
+
...(options.length && options[0].products?.length
|
|
1306
|
+
? { product_id: options[0].products[0].id }
|
|
1307
|
+
: {}),
|
|
895
1308
|
})), options);
|
|
896
1309
|
ProductModuleService.checkIfVariantsHaveUniqueOptionsCombinations(variantsWithOptions);
|
|
897
1310
|
}
|
|
@@ -901,7 +1314,13 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
901
1314
|
}
|
|
902
1315
|
const variantsWithOptions = variants.map((variant) => {
|
|
903
1316
|
const numOfProvidedVariantOptionValues = Object.keys(variant.options || {}).length;
|
|
904
|
-
const productsOptions = options.filter((o) =>
|
|
1317
|
+
const productsOptions = options.filter((o) => {
|
|
1318
|
+
// products could be a Collection object or array, normalize to array
|
|
1319
|
+
const productsArray = Array.isArray(o.products)
|
|
1320
|
+
? o.products
|
|
1321
|
+
: o.products?.toArray?.() ?? [];
|
|
1322
|
+
return productsArray.some((p) => p.id === variant.product_id);
|
|
1323
|
+
});
|
|
905
1324
|
if (numOfProvidedVariantOptionValues &&
|
|
906
1325
|
productsOptions.length !== numOfProvidedVariantOptionValues) {
|
|
907
1326
|
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Product has ${productsOptions.length} option values but there were ${numOfProvidedVariantOptionValues} provided option values for the variant: ${variant.title}.`);
|
|
@@ -1100,6 +1519,134 @@ class ProductModuleService extends (0, utils_1.MedusaService)({
|
|
|
1100
1519
|
}
|
|
1101
1520
|
return result;
|
|
1102
1521
|
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Validates that no variants are using the specified option or option values
|
|
1524
|
+
* before they are removed from a product.
|
|
1525
|
+
*
|
|
1526
|
+
* @param pairs - Array of validation pairs: { productId, optionId, valueIdsToCheck? }
|
|
1527
|
+
* @param sharedContext - The shared context
|
|
1528
|
+
* @throws MedusaError if any variants are using the option/values
|
|
1529
|
+
*/
|
|
1530
|
+
async validateOptionRemoval_(pairs, sharedContext = {}) {
|
|
1531
|
+
if (pairs.length === 0) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
// Filter pairs that need validation (for option removal, check if option is linked)
|
|
1535
|
+
const pairsToValidate = [];
|
|
1536
|
+
// For option removals (no valueIdsToCheck), check if options are linked to products
|
|
1537
|
+
const optionRemovalPairs = pairs.filter((p) => !p.valueIdsToCheck);
|
|
1538
|
+
if (optionRemovalPairs.length > 0) {
|
|
1539
|
+
const existingProductOptions = await this.productProductOptionService_.list({
|
|
1540
|
+
$or: optionRemovalPairs.map((p) => ({
|
|
1541
|
+
product_id: p.productId,
|
|
1542
|
+
product_option_id: p.optionId,
|
|
1543
|
+
})),
|
|
1544
|
+
}, {}, sharedContext);
|
|
1545
|
+
const existingPairsSet = new Set(existingProductOptions.map((epo) => `${epo.product_id}_${epo.product_option_id}`));
|
|
1546
|
+
// Only validate pairs that are actually linked
|
|
1547
|
+
for (const pair of optionRemovalPairs) {
|
|
1548
|
+
const key = `${pair.productId}_${pair.optionId}`;
|
|
1549
|
+
if (existingPairsSet.has(key)) {
|
|
1550
|
+
pairsToValidate.push(pair);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
// For value removals (with valueIdsToCheck), always validate
|
|
1555
|
+
const valueRemovalPairs = pairs.filter((p) => p.valueIdsToCheck);
|
|
1556
|
+
pairsToValidate.push(...valueRemovalPairs);
|
|
1557
|
+
if (pairsToValidate.length === 0) {
|
|
1558
|
+
return; // Nothing to validate
|
|
1559
|
+
}
|
|
1560
|
+
// Get all unique option IDs to fetch options with their values
|
|
1561
|
+
const uniqueOptionIds = [...new Set(pairsToValidate.map((p) => p.optionId))];
|
|
1562
|
+
const options = await this.productOptionService_.list({ id: uniqueOptionIds }, { relations: ["values"] }, sharedContext);
|
|
1563
|
+
const optionsMap = new Map(options.map((opt) => [opt.id, opt]));
|
|
1564
|
+
const bulkValidationPairs = [];
|
|
1565
|
+
for (const pair of pairsToValidate) {
|
|
1566
|
+
const option = optionsMap.get(pair.optionId);
|
|
1567
|
+
if (!option) {
|
|
1568
|
+
continue; // Option doesn't exist, skip
|
|
1569
|
+
}
|
|
1570
|
+
// if no subset is provided we check the whole option values
|
|
1571
|
+
const valueIdsToValidate = pair.valueIdsToCheck
|
|
1572
|
+
? pair.valueIdsToCheck
|
|
1573
|
+
: (option.values || []).map((v) => v.id);
|
|
1574
|
+
if (valueIdsToValidate.length === 0) {
|
|
1575
|
+
continue; // No values to check
|
|
1576
|
+
}
|
|
1577
|
+
bulkValidationPairs.push({
|
|
1578
|
+
productId: pair.productId,
|
|
1579
|
+
optionValueIds: valueIdsToValidate,
|
|
1580
|
+
pair,
|
|
1581
|
+
option,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
if (bulkValidationPairs.length > 0) {
|
|
1585
|
+
const conflictingVariantsMap = await this.productRepository_.checkVariantsUsingOptionValues(bulkValidationPairs.map((p) => ({
|
|
1586
|
+
productId: p.productId,
|
|
1587
|
+
optionValueIds: p.optionValueIds,
|
|
1588
|
+
})), sharedContext);
|
|
1589
|
+
for (const bulkPair of bulkValidationPairs) {
|
|
1590
|
+
const allConflictingVariants = [];
|
|
1591
|
+
for (const valueId of bulkPair.optionValueIds) {
|
|
1592
|
+
const key = `${bulkPair.productId}_${valueId}`;
|
|
1593
|
+
const variants = conflictingVariantsMap.get(key) || [];
|
|
1594
|
+
// Deduplicate variants by variant_id
|
|
1595
|
+
for (const variant of variants) {
|
|
1596
|
+
if (!allConflictingVariants.some((v) => v.variant_id === variant.variant_id)) {
|
|
1597
|
+
allConflictingVariants.push({
|
|
1598
|
+
variant_id: variant.variant_id,
|
|
1599
|
+
title: variant.title,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (allConflictingVariants.length > 0) {
|
|
1605
|
+
const variantNames = allConflictingVariants.map((v) => v.title || v.variant_id);
|
|
1606
|
+
if (bulkPair.pair.valueIdsToCheck) {
|
|
1607
|
+
// Specific values being removed
|
|
1608
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Cannot unassign option values from product because the following variant(s) are using it: ${variantNames.join(", ")}`);
|
|
1609
|
+
}
|
|
1610
|
+
else {
|
|
1611
|
+
// Entire option being removed
|
|
1612
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Cannot unassign product option from product which has variants for that option`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
filterOptionValuesByProducts(products) {
|
|
1619
|
+
for (const product of products) {
|
|
1620
|
+
this.filterOptionValuesByProduct(product);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
filterOptionValuesByProduct(product) {
|
|
1624
|
+
if (!product.options || !product.product_options) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const productOptions = Array.isArray(product.product_options)
|
|
1628
|
+
? product.product_options
|
|
1629
|
+
: product.product_options?.toArray?.() ?? [];
|
|
1630
|
+
// Build a Set of value IDs that are actually associated with this product
|
|
1631
|
+
const allowedValueIds = new Set();
|
|
1632
|
+
for (const productOption of productOptions) {
|
|
1633
|
+
const values = Array.isArray(productOption.values)
|
|
1634
|
+
? productOption.values
|
|
1635
|
+
: productOption.values?.toArray?.() ?? [];
|
|
1636
|
+
for (const value of values) {
|
|
1637
|
+
allowedValueIds.add(value.id);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
// Filter the values in each option to only include allowed ones
|
|
1641
|
+
if (product.options) {
|
|
1642
|
+
for (const option of product.options) {
|
|
1643
|
+
if (option.values) {
|
|
1644
|
+
option.values = option.values.filter((value) => allowedValueIds.has(value.id));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
delete product.product_options;
|
|
1649
|
+
}
|
|
1103
1650
|
async buildVariantImagesFromProduct(variants, productImages, sharedContext = {}) {
|
|
1104
1651
|
// Create a clean map of images without problematic collections
|
|
1105
1652
|
const imagesMap = new Map();
|
|
@@ -1313,6 +1860,36 @@ __decorate([
|
|
|
1313
1860
|
__metadata("design:paramtypes", [Array, Object]),
|
|
1314
1861
|
__metadata("design:returntype", Promise)
|
|
1315
1862
|
], ProductModuleService.prototype, "updateOptions_", null);
|
|
1863
|
+
__decorate([
|
|
1864
|
+
(0, utils_1.InjectManager)(),
|
|
1865
|
+
(0, utils_1.EmitEvents)(),
|
|
1866
|
+
__param(1, (0, utils_1.MedusaContext)()),
|
|
1867
|
+
__metadata("design:type", Function),
|
|
1868
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1869
|
+
__metadata("design:returntype", Promise)
|
|
1870
|
+
], ProductModuleService.prototype, "addProductOptionToProduct", null);
|
|
1871
|
+
__decorate([
|
|
1872
|
+
(0, utils_1.InjectTransactionManager)(),
|
|
1873
|
+
__param(1, (0, utils_1.MedusaContext)()),
|
|
1874
|
+
__metadata("design:type", Function),
|
|
1875
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1876
|
+
__metadata("design:returntype", Promise)
|
|
1877
|
+
], ProductModuleService.prototype, "addProductOptionToProduct_", null);
|
|
1878
|
+
__decorate([
|
|
1879
|
+
(0, utils_1.InjectManager)(),
|
|
1880
|
+
(0, utils_1.EmitEvents)(),
|
|
1881
|
+
__param(1, (0, utils_1.MedusaContext)()),
|
|
1882
|
+
__metadata("design:type", Function),
|
|
1883
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1884
|
+
__metadata("design:returntype", Promise)
|
|
1885
|
+
], ProductModuleService.prototype, "removeProductOptionFromProduct", null);
|
|
1886
|
+
__decorate([
|
|
1887
|
+
(0, utils_1.InjectTransactionManager)(),
|
|
1888
|
+
__param(1, (0, utils_1.MedusaContext)()),
|
|
1889
|
+
__metadata("design:type", Function),
|
|
1890
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1891
|
+
__metadata("design:returntype", Promise)
|
|
1892
|
+
], ProductModuleService.prototype, "removeProductOptionFromProduct_", null);
|
|
1316
1893
|
__decorate([
|
|
1317
1894
|
(0, utils_1.InjectManager)(),
|
|
1318
1895
|
(0, utils_1.EmitEvents)()
|
|
@@ -1530,4 +2107,11 @@ __decorate([
|
|
|
1530
2107
|
__metadata("design:paramtypes", [Array, Object]),
|
|
1531
2108
|
__metadata("design:returntype", Promise)
|
|
1532
2109
|
], ProductModuleService.prototype, "getVariantImages", null);
|
|
2110
|
+
__decorate([
|
|
2111
|
+
(0, utils_1.InjectTransactionManager)(),
|
|
2112
|
+
__param(1, (0, utils_1.MedusaContext)()),
|
|
2113
|
+
__metadata("design:type", Function),
|
|
2114
|
+
__metadata("design:paramtypes", [Array, Object]),
|
|
2115
|
+
__metadata("design:returntype", Promise)
|
|
2116
|
+
], ProductModuleService.prototype, "validateOptionRemoval_", null);
|
|
1533
2117
|
//# sourceMappingURL=product-module-service.js.map
|