@medusajs/product
Version:
Medusa Product module
1,074 lines • 54.9 kB
JavaScript
"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