feat(types, product): added product module update (#4504)
* Feat: create product with product module * feat: create product wip * feat: create product wip * feat: update product relation and generate image migration * lint * conitnue implementation * continue implementation and add integration tests for produceService.create * Add integration tests for product creation at the module level for the complete flow * only use persist since write operations are always wrapped in a transaction which will be committed and flushed * simplify the transaction wrapper to make future changes easier * feat: move some utils to the utils package to simplify its usage * tests: fix unit tests * feat: create variants along side the product * Add more integration tests an update migrations * chore: Update actions workflow to include packages integration tests * small types and utils cleanup * chore: Add support for database debug option * chore: Add missing types in package.json from types and util, validate that all the models are sync with medusa * expose retrieve method * fix types issues * fix unit tests and move integration tests workflow with the plugins integration tests * chore: remove migration function export from the definition to prevent them to be ran by the medusa cli just in case * fix package.json script * chore: workflows * feat: start creating the create product workflow * feat: add empty step for prices and sales channel * tests: update scripts and action envs * fix imports * feat: Add proper soft deleted support + add product deletion service public api * chore: update migrations * chore: update migrations * chore: update todo * feat: Add product deletion to the create-product workflow as compensation * chore: cleanup product utils * feat: Add support for cascade soft-remove * feat: refactor repository to take into account withDeleted * fix integration tests * Add support for force delete -> delete, cleanup repositories and improvements * Add support for restoring a product and add integration tests * cleaup + tests * types * fix integration tests * remove unnecessary comments * move specific mikro orm usage to the DAL * Cleanup workflow functions * Make deleted_at optional at the property level and add url index for the images * address feedback + cleanup * fix export * merge migrations into one * feat(product, types): added missing product variant methods (#4475) * chore: added missing product variant methods * chore: address PR feedback * chore: catch undefined case for retrieve + specs for variant service * chore: align TEntity + add changeset * chore: revert changeset, TEntity to ProductVariant * chore: write tests for pagination, unskip the test * Create chilled-mice-deliver.md * update integration fixtuers * update pipeline node version * rename github action * fix pipeline * feat(medusa, types): added missing category tests and service methods (#4499) * chore: added missing category tests and service methods * chore: added type changes to module service * chore: address pr feedback * chore: added product module update * chore: use status enum type from common types * chore: remove flushing at repo level, pass in relation instead of ID * chore: update error message for missing id * update repositories manager usage and serialisation from the write public API * move serializisation to the DAL * rename template args * chore: address feedback * chore: wip * chore: added collection methods for module and collection service (#4505) * chore: added collection methods for module and collection service * Create fresh-islands-teach.md * chore: move retrieve entity to utils package * chore: make products optional in DTO type --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * chore: added categories, collections and other relations to update * feat(product): Apply transaction decorators to the services (#4512) * chore: handle variant update, create and delete through products update * chore: cleanup types, self review * chore: remove relations that are not present in collection * chore: address reviews p1 * chore: add test for incorrect ID + remove extra check on variant id existance * chore: cleanup + add changeset * chore: wip * chore: add todos for getter method --------- Co-authored-by: adrien2p <adrien.deperetti@gmail.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Oliver Windall Juhl
adrien2p
Carlos R. L. Rodrigues
parent
585ebf2454
commit
caea44ebfd
@@ -26,13 +26,17 @@ import {
|
||||
JoinerServiceConfig,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import { serialize } from "@mikro-orm/core"
|
||||
|
||||
import ProductImageService from "./product-image"
|
||||
import { ProductServiceTypes, ProductVariantServiceTypes } from "../types/services"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
isString,
|
||||
kebabCase,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
} from "@medusajs/utils"
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
import { joinerConfig } from "./../joiner-config"
|
||||
@@ -118,10 +122,12 @@ export default class ProductModuleService<
|
||||
|
||||
async retrieve(
|
||||
productId: string,
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO> {
|
||||
const product = await this.productService_.retrieve(
|
||||
productId,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -282,7 +288,7 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(categories))
|
||||
}
|
||||
|
||||
async create(data: ProductTypes.CreateProductDTO[], sharedContext?: Context) {
|
||||
async create(data: ProductTypes.CreateProductDTO[], sharedContext?: Context): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.create_(data, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, {
|
||||
@@ -290,6 +296,19 @@ export default class ProductModuleService<
|
||||
})
|
||||
}
|
||||
|
||||
async update(
|
||||
data: ProductTypes.UpdateProductDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.update_(data, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async create_(
|
||||
data: ProductTypes.CreateProductDTO[],
|
||||
@@ -329,30 +348,9 @@ export default class ProductModuleService<
|
||||
productData.discountable = false
|
||||
}
|
||||
|
||||
if (productData.images?.length) {
|
||||
productData.images = await this.productImageService_.upsert(
|
||||
productData.images.map((image) =>
|
||||
isString(image) ? image : image.url
|
||||
),
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
if (productData.tags?.length) {
|
||||
productData.tags = await this.productTagService_.upsert(
|
||||
productData.tags,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
if (isDefined(productData.type)) {
|
||||
productData.type = (
|
||||
await this.productTypeService_.upsert(
|
||||
[productData.type as ProductTypes.CreateProductTypeDTO],
|
||||
sharedContext
|
||||
)
|
||||
)?.[0]!
|
||||
}
|
||||
await this.upsertAndAssignImagesToProductData(productData, sharedContext)
|
||||
await this.upsertAndAssignProductTagsToProductData(productData, sharedContext)
|
||||
await this.upsertAndAssignProductTypeToProductData(productData, sharedContext)
|
||||
|
||||
return productData as CreateProductOnlyDTO
|
||||
})
|
||||
@@ -408,6 +406,223 @@ export default class ProductModuleService<
|
||||
return products
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async update_(
|
||||
data: ProductTypes.UpdateProductDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
const productIds = data.map(pd => pd.id)
|
||||
const existingProductVariants = await this.productVariantService_.list(
|
||||
{ product_id: productIds },
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const existingProductVariantsMap = new Map<
|
||||
string,
|
||||
ProductVariant[]
|
||||
>(
|
||||
data.map((productData) => {
|
||||
const productVariantsForProduct = existingProductVariants
|
||||
.filter((variant) => variant.product_id === productData.id)
|
||||
|
||||
return [
|
||||
productData.id,
|
||||
productVariantsForProduct,
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
const productVariantsMap = new Map<
|
||||
string,
|
||||
(ProductTypes.CreateProductVariantDTO | ProductTypes.UpdateProductVariantDTO)[]
|
||||
>()
|
||||
|
||||
const productOptionsMap = new Map<
|
||||
string,
|
||||
ProductTypes.CreateProductOptionDTO[]
|
||||
>()
|
||||
|
||||
const productsData = await Promise.all(
|
||||
data.map(async (product) => {
|
||||
const { variants, options, ...productData } = product
|
||||
|
||||
if (!isDefined(productData.id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Cannot update product without id`
|
||||
)
|
||||
}
|
||||
|
||||
productVariantsMap.set(productData.id, variants ?? [])
|
||||
productOptionsMap.set(productData.id, options ?? [])
|
||||
|
||||
if (productData.is_giftcard) {
|
||||
productData.discountable = false
|
||||
}
|
||||
|
||||
await this.upsertAndAssignImagesToProductData(productData, sharedContext)
|
||||
await this.upsertAndAssignProductTagsToProductData(productData, sharedContext)
|
||||
await this.upsertAndAssignProductTypeToProductData(productData, sharedContext)
|
||||
|
||||
return productData as ProductServiceTypes.UpdateProductDTO
|
||||
})
|
||||
)
|
||||
|
||||
const products = await this.productService_.update(
|
||||
productsData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const productByIdMap = new Map<string, TProduct>(
|
||||
products.map((product) => [product.id, product])
|
||||
)
|
||||
|
||||
const productOptionsData = [...productOptionsMap]
|
||||
.map(([id, options]) => options.map((option) => ({
|
||||
...option,
|
||||
product: productByIdMap.get(id)!,
|
||||
})))
|
||||
.flat()
|
||||
|
||||
const productOptions = await this.productOptionService_.create(
|
||||
productOptionsData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const productVariantIdsToDelete: string[] = []
|
||||
const productVariantsToCreateMap = new Map<
|
||||
string,
|
||||
ProductTypes.CreateProductVariantDTO[]
|
||||
>()
|
||||
|
||||
const productVariantsToUpdateMap = new Map<
|
||||
string,
|
||||
ProductTypes.UpdateProductVariantDTO[]
|
||||
>()
|
||||
|
||||
for (const [productId, variants] of productVariantsMap) {
|
||||
const variantsToCreate: ProductTypes.CreateProductVariantDTO[] = []
|
||||
const variantsToUpdate: ProductTypes.UpdateProductVariantDTO[] = []
|
||||
const existingVariants = existingProductVariantsMap.get(productId)
|
||||
|
||||
variants.forEach((variant) => {
|
||||
const isVariantIdDefined = ("id" in variant) && isDefined(variant.id)
|
||||
|
||||
if (isVariantIdDefined) {
|
||||
variantsToUpdate.push(variant as ProductTypes.UpdateProductVariantDTO)
|
||||
} else {
|
||||
variantsToCreate.push(variant as ProductTypes.CreateProductVariantDTO)
|
||||
}
|
||||
|
||||
const variantOptions = variant.options?.map((option, index) => {
|
||||
const productOption = productOptions[index]
|
||||
return {
|
||||
option: productOption,
|
||||
value: option.value,
|
||||
}
|
||||
})
|
||||
|
||||
if (variantOptions) {
|
||||
variant.options = variantOptions
|
||||
}
|
||||
})
|
||||
|
||||
productVariantsToCreateMap.set(productId, variantsToCreate)
|
||||
productVariantsToUpdateMap.set(productId, variantsToUpdate)
|
||||
|
||||
const variantsToUpdateIds = variantsToUpdate.map(v => v?.id) as string[]
|
||||
const existingVariantIds = existingVariants?.map(v => v.id) || []
|
||||
const variantsToUpdateSet = new Set(variantsToUpdateIds)
|
||||
|
||||
productVariantIdsToDelete.push(
|
||||
...new Set(
|
||||
existingVariantIds.filter(x => !variantsToUpdateSet.has(x))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const promises: Promise<any>[] = []
|
||||
|
||||
productVariantsToCreateMap.forEach((variants, productId) => {
|
||||
promises.push(
|
||||
this.productVariantService_.create(
|
||||
productByIdMap.get(productId)!,
|
||||
variants as unknown as ProductTypes.CreateProductVariantOnlyDTO[],
|
||||
sharedContext
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
productVariantsToUpdateMap.forEach((variants, productId) => {
|
||||
promises.push(
|
||||
this.productVariantService_.update(
|
||||
productByIdMap.get(productId)!,
|
||||
variants as unknown as ProductVariantServiceTypes.UpdateProductVariantDTO[],
|
||||
sharedContext
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
if (productVariantIdsToDelete.length) {
|
||||
promises.push(
|
||||
this.productVariantService_.delete(productVariantIdsToDelete, sharedContext)
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
protected async upsertAndAssignImagesToProductData(
|
||||
productData: ProductTypes.CreateProductDTO | ProductTypes.UpdateProductDTO,
|
||||
sharedContext: Context = {}
|
||||
) {
|
||||
if (!productData.thumbnail && productData.images?.length) {
|
||||
productData.thumbnail = isString(productData.images[0])
|
||||
? (productData.images[0] as string)
|
||||
: (productData.images[0] as { url: string }).url
|
||||
}
|
||||
|
||||
if (productData.images?.length) {
|
||||
productData.images = await this.productImageService_.upsert(
|
||||
productData.images.map((image) =>
|
||||
isString(image) ? image : image.url
|
||||
),
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected async upsertAndAssignProductTagsToProductData(
|
||||
productData: ProductTypes.CreateProductDTO | ProductTypes.UpdateProductDTO,
|
||||
sharedContext: Context = {}
|
||||
) {
|
||||
if (productData.tags?.length) {
|
||||
productData.tags = await this.productTagService_.upsert(
|
||||
productData.tags,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected async upsertAndAssignProductTypeToProductData(
|
||||
productData: ProductTypes.CreateProductDTO | ProductTypes.UpdateProductDTO,
|
||||
sharedContext: Context = {}
|
||||
) {
|
||||
if (isDefined(productData.type)) {
|
||||
const productType = (
|
||||
await this.productTypeService_.upsert(
|
||||
[productData.type as ProductTypes.CreateProductTypeDTO],
|
||||
sharedContext
|
||||
)
|
||||
)
|
||||
|
||||
productData.type = productType?.[0]
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async delete(
|
||||
productIds: string[],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Product, ProductVariant } from "@models"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import { ProductVariantRepository } from "@repositories"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
isString,
|
||||
@@ -8,9 +9,9 @@ import {
|
||||
retrieveEntity,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { ProductVariantServiceTypes } from "../types/services"
|
||||
import ProductService from "./product"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductVariantRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productVariantRepository: DAL.RepositoryService
|
||||
@@ -91,7 +92,8 @@ export default class ProductVariantService<
|
||||
|
||||
if (isString(productOrId)) {
|
||||
product = await this.productService_.retrieve(
|
||||
productOrId as string,
|
||||
productOrId,
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
@@ -112,4 +114,38 @@ export default class ProductVariantService<
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||
async update(
|
||||
productOrId: TProduct | string,
|
||||
data: ProductVariantServiceTypes.UpdateProductVariantDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
let product = productOrId as unknown as Product
|
||||
|
||||
if (isString(productOrId)) {
|
||||
product = await this.productService_.retrieve(
|
||||
productOrId,
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
const variantsData = [...data]
|
||||
variantsData.forEach((variant) => Object.assign(variant, { product }))
|
||||
|
||||
return await (this.productVariantRepository_ as ProductVariantRepository).update(variantsData, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
}) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
return await this.productVariantRepository_.delete(ids, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ import {
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
isDefined,
|
||||
} from "@medusajs/utils"
|
||||
import { ProductRepository } from "@repositories"
|
||||
|
||||
import { ProductServiceTypes } from "../types/services"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -27,10 +30,22 @@ export default class ProductService<TEntity extends Product = Product> {
|
||||
this.productRepository_ = productRepository
|
||||
}
|
||||
|
||||
async retrieve(productId: string, sharedContext?: Context): Promise<TEntity> {
|
||||
async retrieve(
|
||||
productId: string,
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity> {
|
||||
if (!isDefined(productId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"productId" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
|
||||
id: productId,
|
||||
})
|
||||
}, config)
|
||||
|
||||
const product = await this.productRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
@@ -116,6 +131,22 @@ export default class ProductService<TEntity extends Product = Product> {
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async update(
|
||||
data: ProductServiceTypes.UpdateProductDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await (this.productRepository_ as ProductRepository).update(
|
||||
data as WithRequiredProperty<
|
||||
ProductServiceTypes.UpdateProductDTO,
|
||||
"id"
|
||||
>[],
|
||||
{
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
}
|
||||
) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async delete(
|
||||
ids: string[],
|
||||
|
||||
Reference in New Issue
Block a user