UNPKG

@medusajs/product

Version:
1,074 lines • 54.9 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); const types_1 = require("@medusajs/framework/types"); const _models_1 = require("../models"); const utils_1 = require("@medusajs/framework/utils"); const utils_2 = require("../utils"); const joiner_config_1 = require("./../joiner-config"); class ProductModuleService extends (0, utils_1.MedusaService)({ Product: _models_1.Product, ProductCategory: _models_1.ProductCategory, ProductCollection: _models_1.ProductCollection, ProductOption: _models_1.ProductOption, ProductOptionValue: _models_1.ProductOptionValue, ProductTag: _models_1.ProductTag, ProductType: _models_1.ProductType, ProductVariant: _models_1.ProductVariant, ProductImage: _models_1.ProductImage, }) { constructor({ baseRepository, productRepository, productService, productVariantService, productTagService, productCategoryService, productCollectionService, productImageService, productTypeService, productOptionService, productOptionValueService, [utils_1.Modules.EVENT_BUS]: eventBusModuleService, }, moduleDeclaration) { // @ts-ignore // eslint-disable-next-line prefer-rest-params super(...arguments); this.moduleDeclaration = moduleDeclaration; this.baseRepository_ = baseRepository; this.productRepository_ = productRepository; this.productService_ = productService; this.productVariantService_ = productVariantService; this.productTagService_ = productTagService; this.productCategoryService_ = productCategoryService; this.productCollectionService_ = productCollectionService; this.productImageService_ = productImageService; this.productTypeService_ = productTypeService; this.productOptionService_ = productOptionService; this.productOptionValueService_ = productOptionValueService; this.eventBusModuleService_ = eventBusModuleService; } __joinerConfig() { return joiner_config_1.joinerConfig; } // @ts-ignore async retrieveProduct(productId, config, sharedContext) { const product = await this.productService_.retrieve(productId, this.getProductFindConfig_(config), sharedContext); return this.baseRepository_.serialize(product); } // @ts-ignore async listProducts(filters, config, sharedContext) { const products = await this.productService_.list(filters, this.getProductFindConfig_(config), sharedContext); return this.baseRepository_.serialize(products); } // @ts-ignore async listAndCountProducts(filters, config, sharedContext) { const [products, count] = await this.productService_.listAndCount(filters, this.getProductFindConfig_(config), sharedContext); const serializedProducts = await this.baseRepository_.serialize(products); return [serializedProducts, count]; } getProductFindConfig_(config) { const hasImagesRelation = config?.relations?.includes("images"); return { ...config, order: { ...(config?.order ?? { id: "ASC" }), ...(hasImagesRelation ? { images: { rank: "ASC", ...(config?.order?.images ?? {}), }, } : {}), }, }; } // @ts-expect-error async createProductVariants(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const variants = await this.createVariants_(input, sharedContext); const createdVariants = await this.baseRepository_.serialize(variants); return Array.isArray(data) ? createdVariants : createdVariants[0]; } async createVariants_(data, sharedContext = {}) { if (data.some((v) => !v.product_id)) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Unable to create variants without specifying a product_id"); } const productOptions = await this.productOptionService_.list({ product_id: [...new Set(data.map((v) => v.product_id))], }, { relations: ["values"], }, sharedContext); const variants = await this.productVariantService_.list({ product_id: [...new Set(data.map((v) => v.product_id))], }, { relations: ["options"], }, sharedContext); const productVariantsWithOptions = ProductModuleService.assignOptionsToVariants(data, productOptions); ProductModuleService.checkIfVariantWithOptionsAlreadyExists(productVariantsWithOptions, variants); const createdVariants = await this.productVariantService_.create(productVariantsWithOptions, sharedContext); utils_2.eventBuilders.createdProductVariant({ data: createdVariants, sharedContext, }); return createdVariants; } async upsertProductVariants(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const forUpdate = input.filter((variant) => !!variant.id); const forCreate = input.filter((variant) => !variant.id); let created = []; let updated = []; if (forCreate.length) { created = await this.createVariants_(forCreate, sharedContext); } if (forUpdate.length) { updated = await this.updateVariants_(forUpdate, sharedContext); } const result = [...created, ...updated]; const allVariants = await this.baseRepository_.serialize(result); return Array.isArray(data) ? allVariants : allVariants[0]; } // @ts-expect-error async updateProductVariants(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { normalizedInput = [{ id: idOrSelector, ...data }]; } else { const variants = await this.productVariantService_.list(idOrSelector, {}, sharedContext); normalizedInput = variants.map((variant) => ({ id: variant.id, ...data, })); } const variants = await this.updateVariants_(normalizedInput, sharedContext); const updatedVariants = await this.baseRepository_.serialize(variants); return (0, utils_1.isString)(idOrSelector) ? updatedVariants[0] : updatedVariants; } async updateVariants_(data, sharedContext = {}) { // Validation step const variantIdsToUpdate = data.map(({ id }) => id); const variants = await this.productVariantService_.list({ id: variantIdsToUpdate }, {}, sharedContext); const allVariants = await this.productVariantService_.list({ product_id: variants.map((v) => v.product_id) }, { relations: ["options"] }, sharedContext); if (variants.length !== data.length) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Cannot update non-existing variants with ids: ${(0, utils_1.arrayDifference)(variantIdsToUpdate, variants.map(({ id }) => id)).join(", ")}`); } // Data normalization const variantsWithProductId = variants.map((v) => ({ ...data.find((d) => d.id === v.id), id: v.id, product_id: v.product_id, })); const productOptions = await this.productOptionService_.list({ product_id: Array.from(new Set(variantsWithProductId.map((v) => v.product_id))), }, { relations: ["values"] }, sharedContext); const productVariantsWithOptions = ProductModuleService.assignOptionsToVariants(variantsWithProductId, productOptions); if (data.some((d) => !!d.options)) { ProductModuleService.checkIfVariantWithOptionsAlreadyExists(productVariantsWithOptions, allVariants); } const { entities: productVariants, performedActions } = await this.productVariantService_.upsertWithReplace(productVariantsWithOptions, { relations: ["options"], }, sharedContext); utils_2.eventBuilders.createdProductVariant({ data: performedActions.created[_models_1.ProductVariant.name] ?? [], sharedContext, }); utils_2.eventBuilders.updatedProductVariant({ data: performedActions.updated[_models_1.ProductVariant.name] ?? [], sharedContext, }); utils_2.eventBuilders.deletedProductVariant({ data: performedActions.deleted[_models_1.ProductVariant.name] ?? [], sharedContext, }); return productVariants; } // @ts-expect-error async createProductTags(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const tags = await this.productTagService_.create(input, sharedContext); const createdTags = await this.baseRepository_.serialize(tags); utils_2.eventBuilders.createdProductTag({ data: createdTags, sharedContext, }); return Array.isArray(data) ? createdTags : createdTags[0]; } async upsertProductTags(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const forUpdate = input.filter((tag) => !!tag.id); const forCreate = input.filter((tag) => !tag.id); let created = []; let updated = []; if (forCreate.length) { created = await this.productTagService_.create(forCreate, sharedContext); utils_2.eventBuilders.createdProductTag({ data: created, sharedContext, }); } if (forUpdate.length) { updated = await this.productTagService_.update(forUpdate, sharedContext); utils_2.eventBuilders.updatedProductTag({ data: updated, sharedContext, }); } const result = [...created, ...updated]; const allTags = await this.baseRepository_.serialize(result); return Array.isArray(data) ? allTags : allTags[0]; } // @ts-expect-error async updateProductTags(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { // Check if the tag exists in the first place await this.productTagService_.retrieve(idOrSelector, {}, sharedContext); normalizedInput = [{ id: idOrSelector, ...data }]; } else { const tags = await this.productTagService_.list(idOrSelector, {}, sharedContext); normalizedInput = tags.map((tag) => ({ id: tag.id, ...data, })); } const tags = await this.productTagService_.update(normalizedInput, sharedContext); const updatedTags = await this.baseRepository_.serialize(tags); utils_2.eventBuilders.updatedProductTag({ data: updatedTags, sharedContext, }); return (0, utils_1.isString)(idOrSelector) ? updatedTags[0] : updatedTags; } // @ts-expect-error async createProductTypes(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const types = await this.productTypeService_.create(input, sharedContext); const createdTypes = await this.baseRepository_.serialize(types); return Array.isArray(data) ? createdTypes : createdTypes[0]; } async upsertProductTypes(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const forUpdate = input.filter((type) => !!type.id); const forCreate = input.filter((type) => !type.id); let created = []; let updated = []; if (forCreate.length) { created = await this.productTypeService_.create(forCreate, sharedContext); } if (forUpdate.length) { updated = await this.productTypeService_.update(forUpdate, sharedContext); } const result = [...created, ...updated]; const allTypes = await this.baseRepository_.serialize(result); return Array.isArray(data) ? allTypes : allTypes[0]; } // @ts-expect-error async updateProductTypes(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { // Check if the type exists in the first place await this.productTypeService_.retrieve(idOrSelector, {}, sharedContext); normalizedInput = [{ id: idOrSelector, ...data }]; } else { const types = await this.productTypeService_.list(idOrSelector, {}, sharedContext); normalizedInput = types.map((type) => ({ id: type.id, ...data, })); } const types = await this.productTypeService_.update(normalizedInput, sharedContext); const updatedTypes = await this.baseRepository_.serialize(types); return (0, utils_1.isString)(idOrSelector) ? updatedTypes[0] : updatedTypes; } // @ts-expect-error async createProductOptions(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const options = await this.createOptions_(input, sharedContext); const createdOptions = await this.baseRepository_.serialize(options); return Array.isArray(data) ? createdOptions : createdOptions[0]; } async createOptions_(data, sharedContext = {}) { if (data.some((v) => !v.product_id)) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Tried to create options without specifying a product_id"); } const normalizedInput = data.map((opt) => { return { ...opt, values: opt.values?.map((v) => { return typeof v === "string" ? { value: v } : v; }), }; }); return await this.productOptionService_.create(normalizedInput, sharedContext); } async upsertProductOptions(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const forUpdate = input.filter((option) => !!option.id); const forCreate = input.filter((option) => !option.id); let created = []; let updated = []; if (forCreate.length) { created = await this.createOptions_(forCreate, sharedContext); } if (forUpdate.length) { updated = await this.updateOptions_(forUpdate, sharedContext); } const result = [...created, ...updated]; const allOptions = await this.baseRepository_.serialize(result); return Array.isArray(data) ? allOptions : allOptions[0]; } // @ts-expect-error async updateProductOptions(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { await this.productOptionService_.retrieve(idOrSelector, {}, sharedContext); normalizedInput = [{ id: idOrSelector, ...data }]; } else { const options = await this.productOptionService_.list(idOrSelector, {}, sharedContext); normalizedInput = options.map((option) => ({ id: option.id, ...data, })); } const options = await this.updateOptions_(normalizedInput, sharedContext); const updatedOptions = await this.baseRepository_.serialize(options); return (0, utils_1.isString)(idOrSelector) ? updatedOptions[0] : updatedOptions; } async updateOptions_(data, sharedContext = {}) { // Validation step if (data.some((option) => !option.id)) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Tried to update options without specifying an ID"); } const dbOptions = await this.productOptionService_.list({ id: data.map(({ id }) => id) }, { relations: ["values"] }, sharedContext); if (dbOptions.length !== data.length) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Cannot update non-existing options with ids: ${(0, utils_1.arrayDifference)(data.map(({ id }) => id), dbOptions.map(({ id }) => id)).join(", ")}`); } // Data normalization const normalizedInput = data.map((opt) => { const dbValues = dbOptions.find(({ id }) => id === opt.id)?.values || []; const normalizedValues = opt.values?.map((v) => { return typeof v === "string" ? { value: v } : v; }); return { ...opt, ...(normalizedValues ? { // Oftentimes the options are only passed by value without an id, even if they exist in the DB values: normalizedValues.map((normVal) => { if ("id" in normVal) { return normVal; } const dbVal = dbValues.find((dbVal) => dbVal.value === normVal.value); if (!dbVal) { return normVal; } return { id: dbVal.id, value: normVal.value, }; }), } : {}), }; }); const { entities: productOptions } = await this.productOptionService_.upsertWithReplace(normalizedInput, { relations: ["values"] }, sharedContext); return productOptions; } // @ts-expect-error async createProductCollections(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const collections = await this.createCollections_(input, sharedContext); const createdCollections = await this.baseRepository_.serialize(collections); utils_2.eventBuilders.createdProductCollection({ data: collections, sharedContext, }); return Array.isArray(data) ? createdCollections : createdCollections[0]; } async createCollections_(data, sharedContext = {}) { const normalizedInput = data.map(ProductModuleService.normalizeCreateProductCollectionInput); // It's safe to use upsertWithReplace here since we only have product IDs and the only operation to do is update the product // with the collection ID const { entities: productCollections } = await this.productCollectionService_.upsertWithReplace(normalizedInput, { relations: ["products"] }, sharedContext); return productCollections; } async upsertProductCollections(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const forUpdate = input.filter((collection) => !!collection.id); const forCreate = input.filter((collection) => !collection.id); let created = []; let updated = []; if (forCreate.length) { created = await this.createCollections_(forCreate, sharedContext); } if (forUpdate.length) { updated = await this.updateCollections_(forUpdate, sharedContext); } const result = [...created, ...updated]; const allCollections = await this.baseRepository_.serialize(result); if (created.length) { utils_2.eventBuilders.createdProductCollection({ data: created, sharedContext, }); } if (updated.length) { utils_2.eventBuilders.updatedProductCollection({ data: updated, sharedContext, }); } return Array.isArray(data) ? allCollections : allCollections[0]; } // @ts-expect-error async updateProductCollections(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { await this.productCollectionService_.retrieve(idOrSelector, {}, sharedContext); normalizedInput = [{ id: idOrSelector, ...data }]; } else { const collections = await this.productCollectionService_.list(idOrSelector, {}, sharedContext); normalizedInput = collections.map((collection) => ({ id: collection.id, ...data, })); } const collections = await this.updateCollections_(normalizedInput, sharedContext); const updatedCollections = await this.baseRepository_.serialize(collections); utils_2.eventBuilders.updatedProductCollection({ data: updatedCollections, sharedContext, }); return (0, utils_1.isString)(idOrSelector) ? updatedCollections[0] : updatedCollections; } async updateCollections_(data, sharedContext = {}) { const normalizedInput = data.map(ProductModuleService.normalizeUpdateProductCollectionInput); // TODO: Maybe we can update upsertWithReplace to not remove oneToMany entities, but just disassociate them? With that we can remove the code below. // Another alternative is to not allow passing product_ids to a collection, and instead set the collection_id through the product update call. const updatedCollections = await this.productCollectionService_.update(normalizedInput.map((c) => (0, utils_1.removeUndefined)({ ...c, products: undefined })), sharedContext); const collections = []; const toUpdate = []; updatedCollections.forEach((collectionData) => { const input = normalizedInput.find((c) => c.id === collectionData.id); const productsToUpdate = input?.products; const dissociateSelector = { collection_id: collectionData.id, }; const associateSelector = {}; if ((0, utils_1.isDefined)(productsToUpdate)) { const productIds = productsToUpdate.map((p) => p.id); dissociateSelector["id"] = { $nin: productIds }; associateSelector["id"] = { $in: productIds }; } if ((0, utils_1.isPresent)(dissociateSelector["id"])) { toUpdate.push({ selector: dissociateSelector, data: { collection_id: null, }, }); } if ((0, utils_1.isPresent)(associateSelector["id"])) { toUpdate.push({ selector: associateSelector, data: { collection_id: collectionData.id, }, }); } collections.push({ ...collectionData, products: productsToUpdate ?? [], }); }); if (toUpdate.length) { await this.productService_.update(toUpdate, sharedContext); } return collections; } // @ts-expect-error async createProductCategories(data, sharedContext = {}) { const input = (Array.isArray(data) ? data : [data]).map((productCategory) => { productCategory.handle ??= (0, utils_1.kebabCase)(productCategory.name); return productCategory; }); const categories = await this.productCategoryService_.create(input, sharedContext); const createdCategories = await this.baseRepository_.serialize(categories); utils_2.eventBuilders.createdProductCategory({ data: createdCategories, sharedContext, }); return Array.isArray(data) ? createdCategories : createdCategories[0]; } async upsertProductCategories(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const forUpdate = input.filter((category) => !!category.id); const forCreate = input.filter((category) => !category.id); let created = []; let updated = []; if (forCreate.length) { created = await this.productCategoryService_.create(forCreate, sharedContext); } if (forUpdate.length) { updated = await this.productCategoryService_.update(forUpdate, sharedContext); } const createdCategories = await this.baseRepository_.serialize(created); const updatedCategories = await this.baseRepository_.serialize(updated); utils_2.eventBuilders.createdProductCategory({ data: createdCategories, sharedContext, }); utils_2.eventBuilders.updatedProductCategory({ data: updatedCategories, sharedContext, }); const result = [...createdCategories, ...updatedCategories]; return Array.isArray(data) ? result : result[0]; } // @ts-expect-error async updateProductCategories(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { // Check if the type exists in the first place await this.productCategoryService_.retrieve(idOrSelector, {}, sharedContext); normalizedInput = [{ id: idOrSelector, ...data }]; } else { const categories = await this.productCategoryService_.list(idOrSelector, {}, sharedContext); normalizedInput = categories.map((type) => ({ id: type.id, ...data, })); } const categories = await this.productCategoryService_.update(normalizedInput, sharedContext); const updatedCategories = await this.baseRepository_.serialize(categories); utils_2.eventBuilders.updatedProductCategory({ data: updatedCategories, sharedContext, }); return (0, utils_1.isString)(idOrSelector) ? updatedCategories[0] : updatedCategories; } // @ts-expect-error async createProducts(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const products = await this.createProducts_(input, sharedContext); const createdProducts = await this.baseRepository_.serialize(products); utils_2.eventBuilders.createdProduct({ data: createdProducts, sharedContext, }); return Array.isArray(data) ? createdProducts : createdProducts[0]; } async upsertProducts(data, sharedContext = {}) { const input = Array.isArray(data) ? data : [data]; const forUpdate = input.filter((product) => !!product.id); const forCreate = input.filter((product) => !product.id); let created = []; let updated = []; if (forCreate.length) { created = await this.createProducts_(forCreate, sharedContext); } if (forUpdate.length) { updated = await this.updateProducts_(forUpdate, sharedContext); } const result = [...created, ...updated]; const allProducts = await this.baseRepository_.serialize(result); if (created.length) { utils_2.eventBuilders.createdProduct({ data: created, sharedContext, }); } if (updated.length) { utils_2.eventBuilders.updatedProduct({ data: updated, sharedContext, }); } return Array.isArray(data) ? allProducts : allProducts[0]; } // @ts-expect-error async updateProducts(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { // This will throw if the product does not exist await this.productService_.retrieve(idOrSelector, {}, sharedContext); normalizedInput = [{ id: idOrSelector, ...data }]; } else { const products = await this.productService_.list(idOrSelector, {}, sharedContext); normalizedInput = products.map((product) => ({ id: product.id, ...data, })); } const products = await this.updateProducts_(normalizedInput, sharedContext); const updatedProducts = await this.baseRepository_.serialize(products); utils_2.eventBuilders.updatedProduct({ data: updatedProducts, sharedContext, }); return (0, utils_1.isString)(idOrSelector) ? updatedProducts[0] : updatedProducts; } async createProducts_(data, sharedContext = {}) { const normalizedProducts = await this.normalizeCreateProductInput(data, sharedContext); for (const product of normalizedProducts) { this.validateProductCreatePayload(product); } const tagIds = normalizedProducts .flatMap((d) => d.tags ?? []) .map((t) => t.id); let existingTags = []; if (tagIds.length) { existingTags = await this.productTagService_.list({ id: tagIds, }, {}, sharedContext); } const existingTagsMap = new Map(existingTags.map((tag) => [tag.id, tag])); const productsToCreate = normalizedProducts.map((product) => { const productId = (0, utils_1.generateEntityId)(product.id, "prod"); product.id = productId; if (product.categories?.length) { ; product.categories = product.categories.map((category) => category.id); } if (product.variants?.length) { const normalizedVariants = product.variants.map((variant) => { const variantId = (0, utils_1.generateEntityId)(variant.id, "variant"); variant.id = variantId; Object.entries(variant.options ?? {}).forEach(([key, value]) => { const productOption = product.options?.find((option) => option.title === key); const productOptionValue = productOption.values?.find((optionValue) => optionValue.value === value); productOptionValue.variants ??= []; productOptionValue.variants.push(variant); }); delete variant.options; return variant; }); product.variants = normalizedVariants; } if (product.tags?.length) { ; product.tags = product.tags.map((tag) => { const existingTag = existingTagsMap.get(tag.id); if (existingTag) { return existingTag; } 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.`); }); } return product; }); const createdProducts = await this.productService_.create(productsToCreate, sharedContext); return createdProducts; } async updateProducts_(data, sharedContext = {}) { const normalizedProducts = await this.normalizeUpdateProductInput(data, sharedContext); for (const product of normalizedProducts) { this.validateProductUpdatePayload(product); } return this.productRepository_.deepUpdate(normalizedProducts, ProductModuleService.validateVariantOptions, sharedContext); } // @ts-expect-error async updateProductOptionValues(idOrSelector, data, sharedContext = {}) { let normalizedInput = []; if ((0, utils_1.isString)(idOrSelector)) { // This will throw if the product option value does not exist await this.productOptionValueService_.retrieve(idOrSelector, {}, sharedContext); normalizedInput = [{ id: idOrSelector, ...data }]; } else { const productOptionValues = await this.productOptionValueService_.list(idOrSelector, {}, sharedContext); normalizedInput = productOptionValues.map((product) => ({ id: product.id, ...data, })); } const productOptionValues = await super.updateProductOptionValues(normalizedInput, sharedContext); const updatedProductOptionValues = await this.baseRepository_.serialize(productOptionValues); utils_2.eventBuilders.updatedProductOptionValue({ data: updatedProductOptionValues, sharedContext: sharedContext, }); return (0, utils_1.isString)(idOrSelector) ? updatedProductOptionValues[0] : updatedProductOptionValues; } /** * Validates the manually provided handle value of the product * to be URL-safe */ validateProductPayload(productData) { if (productData.handle && !(0, utils_1.isValidHandle)(productData.handle)) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Invalid product handle '${productData.handle}'. It must contain URL safe characters`); } } validateProductCreatePayload(productData) { this.validateProductPayload(productData); if (!productData.title) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Product title is required`); } const options = productData.options; const missingOptionsVariants = []; if (options?.length) { productData.variants?.forEach((variant) => { options.forEach((option) => { if (!variant.options?.[option.title]) { missingOptionsVariants.push(variant.title); } }); }); } if (missingOptionsVariants.length) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Product "${productData.title}" has variants with missing options: [${missingOptionsVariants.join(", ")}]`); } } validateProductUpdatePayload(productData) { this.validateProductPayload(productData); } async normalizeCreateProductInput(products, sharedContext = {}) { const products_ = Array.isArray(products) ? products : [products]; const normalizedProducts = (await this.normalizeUpdateProductInput(products_, sharedContext)); for (const productData of normalizedProducts) { if (!productData.handle && productData.title) { productData.handle = (0, utils_1.toHandle)(productData.title); } if (!productData.status) { productData.status = utils_1.ProductStatus.DRAFT; } if (!productData.thumbnail && productData.images?.length) { productData.thumbnail = productData.images[0].url; } if (productData.images?.length) { productData.images = productData.images.map((image, index) => image.rank != null ? image : { ...image, rank: index, }); } } return (Array.isArray(products) ? normalizedProducts : normalizedProducts[0]); } async normalizeUpdateProductInput(products, sharedContext = {}) { const products_ = Array.isArray(products) ? products : [products]; const productsIds = products_.map((p) => p.id).filter(Boolean); let dbOptions = []; if (productsIds.length) { dbOptions = await this.productOptionService_.list({ product_id: productsIds }, { relations: ["values"] }, sharedContext); } const normalizedProducts = []; for (const product of products_) { const productData = { ...product }; if (productData.is_giftcard) { productData.discountable = false; } if (productData.options?.length) { ; productData.options = productData.options?.map((option) => { const dbOption = dbOptions.find((o) => o.title === option.title && o.product_id === productData.id); return { title: option.title, values: option.values?.map((value) => { const dbValue = dbOption?.values?.find((val) => val.value === value); return { value: value, ...(dbValue ? { id: dbValue.id } : {}), }; }), ...(dbOption ? { id: dbOption.id } : {}), }; }); } if (productData.tag_ids) { ; productData.tags = productData.tag_ids.map((cid) => ({ id: cid, })); delete productData.tag_ids; } if (productData.category_ids) { ; productData.categories = productData.category_ids.map((cid) => ({ id: cid, })); delete productData.category_ids; } normalizedProducts.push(productData); } return (Array.isArray(products) ? normalizedProducts : normalizedProducts[0]); } static normalizeCreateProductCollectionInput(collection) { const collectionData = ProductModuleService.normalizeUpdateProductCollectionInput(collection); if (!collectionData.handle && collectionData.title) { collectionData.handle = (0, utils_1.kebabCase)(collectionData.title); } return collectionData; } static normalizeUpdateProductCollectionInput(collection) { const collectionData = { ...collection }; if (Array.isArray(collectionData.product_ids)) { ; collectionData.products = collectionData.product_ids.map((pid) => ({ id: pid })); delete collectionData.product_ids; } return collectionData; } static validateVariantOptions(variants, options) { const variantsWithOptions = ProductModuleService.assignOptionsToVariants(variants.map((v) => ({ ...v, // adding product_id to the variant to make it valid for the assignOptionsToVariants function ...(options.length ? { product_id: options[0].product_id } : {}), })), options); ProductModuleService.checkIfVariantsHaveUniqueOptionsCombinations(variantsWithOptions); } static assignOptionsToVariants(variants, options) { if (!variants.length) { return variants; } const variantsWithOptions = variants.map((variant) => { const numOfProvidedVariantOptionValues = Object.keys(variant.options || {}).length; const productsOptions = options.filter((o) => o.product_id === variant.product_id); if (numOfProvidedVariantOptionValues && productsOptions.length !== numOfProvidedVariantOptionValues) { 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}.`); } const variantOptions = Object.entries(variant.options || {}).map(([key, val]) => { const option = productsOptions.find((o) => o.title === key); const optionValue = option?.values?.find((v) => (v.value?.value ?? v.value) === val); if (!optionValue) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Option value ${val} does not exist for option ${key}`); } return { id: optionValue.id, }; }); if (!variantOptions.length) { return variant; } return { ...variant, options: variantOptions, }; }); return variantsWithOptions; } /** * Validate that `data` doesn't create or update a variant to have same options combination * as an existing variant on the product. * @param data - create / update payloads * @param variants - existing variants * @protected */ static checkIfVariantWithOptionsAlreadyExists(data, variants) { for (const variantData of data) { const existingVariant = variants.find((v) => { if (variantData.product_id !== v.product_id || !variantData.options?.length) { return false; } if (variantData?.id === v.id) { return false; } return variantData.options.every((optionValue) => { const variantOptionValue = v.options.find((vo) => vo.id === optionValue.id); return !!variantOptionValue; }); }); if (existingVariant) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Variant (${existingVariant.title}) with provided options already exists.`); } } } /** * Validate that array of variants that we are upserting doesn't have variants with the same options. * @param variants - * @protected */ static checkIfVariantsHaveUniqueOptionsCombinations(variants) { for (let i = 0; i < variants.length; i++) { const variant = variants[i]; for (let j = i + 1; j < variants.length; j++) { const compareVariant = variants[j]; const exists = variant.options?.every((optionValue) => !!compareVariant.options.find((compareOptionValue) => compareOptionValue.id === optionValue.id)); if (exists) { throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Variant "${variant.title}" has same combination of option values as "${compareVariant.title}".`); } } } } } exports.default = ProductModuleService; __decorate([ (0, utils_1.InjectManager)() // @ts-ignore , __param(2, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "retrieveProduct", null); __decorate([ (0, utils_1.InjectManager)() // @ts-ignore , __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "listProducts", null); __decorate([ (0, utils_1.InjectManager)() // @ts-ignore , __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "listAndCountProducts", null); __decorate([ (0, utils_1.InjectManager)(), (0, utils_1.EmitEvents)() // @ts-expect-error , __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "createProductVariants", null); __decorate([ (0, utils_1.InjectTransactionManager)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Array, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "createVariants_", null); __decorate([ (0, utils_1.InjectTransactionManager)(), (0, utils_1.EmitEvents)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "upsertProductVariants", null); __decorate([ (0, utils_1.InjectManager)(), (0, utils_1.EmitEvents)() // @ts-expect-error , __param(2, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "updateProductVariants", null); __decorate([ (0, utils_1.InjectTransactionManager)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Array, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "updateVariants_", null); __decorate([ (0, utils_1.InjectManager)(), (0, utils_1.EmitEvents)() // @ts-expect-error , __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "createProductTags", null); __decorate([ (0, utils_1.InjectTransactionManager)(), (0, utils_1.EmitEvents)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "upsertProductTags", null); __decorate([ (0, utils_1.InjectManager)(), (0, utils_1.EmitEvents)() // @ts-expect-error , __param(2, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "updateProductTags", null); __decorate([ (0, utils_1.InjectManager)() // @ts-expect-error , __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "createProductTypes", null); __decorate([ (0, utils_1.InjectTransactionManager)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "upsertProductTypes", null); __decorate([ (0, utils_1.InjectManager)() // @ts-expect-error , __param(2, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "updateProductTypes", null); __decorate([ (0, utils_1.InjectManager)() // @ts-expect-error , __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "createProductOptions", null); __decorate([ (0, utils_1.InjectTransactionManager)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Array, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "createOptions_", null); __decorate([ (0, utils_1.InjectTransactionManager)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "upsertProductOptions", null); __decorate([ (0, utils_1.InjectManager)() // @ts-expect-error , __param(2, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "updateProductOptions", null); __decorate([ (0, utils_1.InjectTransactionManager)(), __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Array, Object]), __metadata("design:returntype", Promise) ], ProductModuleService.prototype, "updateOptions_", null); __decorate([ (0, utils_1.InjectManager)(), (0, utils_1.EmitEvents)() // @ts-expect-error , __param(1, (0, utils_1.MedusaContext)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Obj