feat(product): Create (+ workflow), delete, restore (#4459)

* 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

* update repositories manager usage and serialisation from the write public API

* move serializisation to the DAL

* rename template args

* 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>

* feat(product): Apply transaction decorators to the services (#4512)

---------

Co-authored-by: Riqwan Thamir <rmthamir@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:
Adrien de Peretti
2023-07-16 20:19:23 +02:00
committed by GitHub
co-authored by Oliver Windall Juhl Riqwan Thamir Carlos R. L. Rodrigues
parent 5b91a3503a
commit befc2f1c80
98 changed files with 5444 additions and 688 deletions
@@ -0,0 +1,20 @@
import { asClass, asValue, createContainer } from "awilix"
import { ProductService } from "@services"
export const nonExistingProductId = "non-existing-id"
export const mockContainer = createContainer()
mockContainer.register({
transaction: asValue(async (task) => await task()),
productRepository: asValue({
find: jest.fn().mockImplementation(async ({ where: { id } }) => {
if (id === nonExistingProductId) {
return []
}
return [{}]
}),
findAndCount: jest.fn().mockResolvedValue([[], 0]),
}),
productService: asClass(ProductService),
})
@@ -1,29 +1,65 @@
import { asClass, asValue, createContainer } from "awilix"
import { ProductService } from "@services"
const container = createContainer()
container.register({
productRepository: asValue({
find: jest.fn().mockResolvedValue([]),
findAndCount: jest.fn().mockResolvedValue([[], 0]),
}),
productVariantService: asValue({
list: jest.fn().mockResolvedValue([]),
}),
productTagService: asValue({
list: jest.fn().mockResolvedValue([]),
}),
productService: asClass(ProductService),
})
import { mockContainer, nonExistingProductId } from "../__fixtures__/product"
describe("Product service", function () {
beforeEach(function () {
jest.clearAllMocks()
})
it("should retrieve a product", async function () {
const productService = mockContainer.resolve("productService")
const productRepository = mockContainer.resolve("productRepository")
const productId = "existing-product"
await productService.retrieve(productId)
expect(productRepository.find).toHaveBeenCalledWith(
{
where: {
id: productId,
},
options: {
fields: undefined,
limit: 15,
offset: undefined,
populate: [],
withDeleted: undefined,
},
},
undefined
)
})
it("should fail to retrieve a product", async function () {
const productService = mockContainer.resolve("productService")
const productRepository = mockContainer.resolve("productRepository")
const err = await productService
.retrieve(nonExistingProductId)
.catch((e) => e)
expect(productRepository.find).toHaveBeenCalledWith(
{
where: {
id: nonExistingProductId,
},
options: {
fields: undefined,
limit: 15,
offset: undefined,
populate: [],
withDeleted: undefined,
},
},
undefined
)
expect(err.message).toBe(
`Product with id: ${nonExistingProductId} was not found`
)
})
it("should list products", async function () {
const productService = container.resolve("productService")
const productRepository = container.resolve("productRepository")
const productService = mockContainer.resolve("productService")
const productRepository = mockContainer.resolve("productRepository")
const filters = {}
const config = {
@@ -32,27 +68,31 @@ describe("Product service", function () {
await productService.list(filters, config)
expect(productRepository.find).toHaveBeenCalledWith({
where: {},
options: {
fields: undefined,
limit: undefined,
offset: undefined,
populate: [],
expect(productRepository.find).toHaveBeenCalledWith(
{
where: {},
options: {
fields: undefined,
limit: 15,
offset: undefined,
populate: [],
withDeleted: undefined,
},
},
})
undefined
)
})
it("should list products with filters", async function () {
const productService = container.resolve("productService")
const productRepository = container.resolve("productRepository")
const productService = mockContainer.resolve("productService")
const productRepository = mockContainer.resolve("productRepository")
const filters = {
tags: {
value: {
$in: ["test"],
}
}
},
},
}
const config = {
relations: [],
@@ -60,33 +100,37 @@ describe("Product service", function () {
await productService.list(filters, config)
expect(productRepository.find).toHaveBeenCalledWith({
where: {
tags: {
value: {
$in: ["test"]
}
expect(productRepository.find).toHaveBeenCalledWith(
{
where: {
tags: {
value: {
$in: ["test"],
},
},
},
options: {
fields: undefined,
limit: 15,
offset: undefined,
populate: [],
withDeleted: undefined,
},
},
options: {
fields: undefined,
limit: undefined,
offset: undefined,
populate: [],
},
})
undefined
)
})
it("should list products with filters and relations", async function () {
const productService = container.resolve("productService")
const productRepository = container.resolve("productRepository")
const productService = mockContainer.resolve("productService")
const productRepository = mockContainer.resolve("productRepository")
const filters = {
tags: {
value: {
$in: ["test"],
}
}
},
},
}
const config = {
relations: ["tags"],
@@ -94,20 +138,62 @@ describe("Product service", function () {
await productService.list(filters, config)
expect(productRepository.find).toHaveBeenCalledWith({
where: {
tags: {
value: {
$in: ["test"]
}
expect(productRepository.find).toHaveBeenCalledWith(
{
where: {
tags: {
value: {
$in: ["test"],
},
},
},
options: {
fields: undefined,
limit: 15,
offset: undefined,
withDeleted: undefined,
populate: ["tags"],
},
},
options: {
fields: undefined,
limit: undefined,
offset: undefined,
populate: ["tags"],
undefined
)
})
it("should list and count the products with filters and relations", async function () {
const productService = mockContainer.resolve("productService")
const productRepository = mockContainer.resolve("productRepository")
const filters = {
tags: {
value: {
$in: ["test"],
},
},
})
}
const config = {
relations: ["tags"],
}
await productService.listAndCount(filters, config)
expect(productRepository.findAndCount).toHaveBeenCalledWith(
{
where: {
tags: {
value: {
$in: ["test"],
},
},
},
options: {
fields: undefined,
limit: 15,
offset: undefined,
withDeleted: undefined,
populate: ["tags"],
},
},
undefined
)
})
})
+3
View File
@@ -4,3 +4,6 @@ export { default as ProductTagService } from "./product-tag"
export { default as ProductVariantService } from "./product-variant"
export { default as ProductCollectionService } from "./product-collection"
export { default as ProductCategoryService } from "./product-category"
export { default as ProductTypeService } from "./product-type"
export { default as ProductOptionService } from "./product-option"
export { default as ProductImageService } from "./product-image"
@@ -1,34 +1,99 @@
import { ProductCategory } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
import { buildQuery } from "../utils"
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { ModulesSdkUtils, MedusaError, isDefined } from "@medusajs/utils"
type InjectedDependencies = {
productCategoryRepository: DAL.RepositoryService
productCategoryRepository: DAL.TreeRepositoryService
}
export default class ProductCategoryService<TEntity = ProductCategory> {
protected readonly productCategoryRepository_: DAL.RepositoryService
export default class ProductCategoryService<
TEntity extends ProductCategory = ProductCategory
> {
protected readonly productCategoryRepository_: DAL.TreeRepositoryService
constructor({ productCategoryRepository }: InjectedDependencies) {
this.productCategoryRepository_ = productCategoryRepository
}
async retrieve(
productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
): Promise<TEntity> {
if (!isDefined(productCategoryId)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"productCategoryId" must be defined`
)
}
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>({
id: productCategoryId,
}, config)
const transformOptions = {
includeDescendantsTree: true,
}
const productCategories = await this.productCategoryRepository_.find(
queryOptions,
transformOptions,
sharedContext
)
if (!productCategories?.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ProductCategory with id: ${productCategoryId} was not found`
)
}
return productCategories[0] as TEntity
}
async list(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<TEntity[]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false
includeDescendantsTree: filters?.include_descendants_tree || false,
}
delete filters.include_descendants_tree
const queryOptions = buildQuery<TEntity>(filters, config)
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
filters,
config
)
queryOptions.where ??= {}
return await this.productCategoryRepository_.find(
return (await this.productCategoryRepository_.find(
queryOptions,
transformOptions,
sharedContext
)) as TEntity[]
}
async listAndCount(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
): Promise<[TEntity[], number]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false,
}
delete filters.include_descendants_tree
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
filters,
config
)
queryOptions.where ??= {}
return (await this.productCategoryRepository_.findAndCount(
queryOptions,
transformOptions,
sharedContext
)) as [TEntity[], number]
}
}
@@ -1,30 +1,74 @@
import { ProductCollection } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
import { buildQuery } from "../utils"
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { ModulesSdkUtils, retrieveEntity } from "@medusajs/utils"
type InjectedDependencies = {
productCollectionRepository: DAL.RepositoryService
}
export default class ProductCollectionService<TEntity = ProductCollection> {
protected readonly productCollectionRepository_: DAL.RepositoryService<TEntity>
export default class ProductCollectionService<
TEntity extends ProductCollection = ProductCollection
> {
protected readonly productCollectionRepository_: DAL.TreeRepositoryService
constructor({ productCollectionRepository }: InjectedDependencies) {
this.productCollectionRepository_ = productCollectionRepository
}
async retrieve(
productCollectionId: string,
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
): Promise<TEntity> {
return (await retrieveEntity<
ProductCollection,
ProductTypes.ProductCollectionDTO
>({
id: productCollectionId,
entityName: ProductCollection.name,
repository: this.productCollectionRepository_,
config,
sharedContext,
})) as TEntity
}
async list(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<TEntity[]> {
const queryOptions = buildQuery<TEntity>(filters, config)
return (await this.productCollectionRepository_.find(
this.buildListQueryOptions(filters, config),
sharedContext
)) as TEntity[]
}
async listAndCount(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
): Promise<[TEntity[], number]> {
return (await this.productCollectionRepository_.findAndCount(
this.buildListQueryOptions(filters, config),
sharedContext
)) as [TEntity[], number]
}
protected buildListQueryOptions(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {}
) {
const queryOptions = ModulesSdkUtils.buildQuery<ProductCollection>(
filters,
config
)
queryOptions.where ??= {}
if (filters.title) {
queryOptions.where["title"] = { $like: filters.title }
}
return await this.productCollectionRepository_.find(queryOptions)
return queryOptions
}
}
@@ -0,0 +1,26 @@
import { Image } from "@models"
import { Context, DAL } from "@medusajs/types"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import { doNotForceTransaction } from "../utils"
import { ProductImageRepository } from "@repositories"
type InjectedDependencies = {
productImageRepository: DAL.RepositoryService
}
export default class ProductImageService<TEntity extends Image = Image> {
protected readonly productImageRepository_: DAL.RepositoryService
constructor({ productImageRepository }: InjectedDependencies) {
this.productImageRepository_ = productImageRepository
}
@InjectTransactionManager(doNotForceTransaction, "productImageRepository_")
async upsert(
urls: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productImageRepository_ as ProductImageRepository)
.upsert!(urls, sharedContext)) as TEntity[]
}
}
@@ -1,66 +1,105 @@
import {
ProductCategoryService,
ProductCollectionService,
ProductOptionService,
ProductService,
ProductTagService,
ProductTypeService,
ProductVariantService,
} from "@services"
import {
Image,
Product,
ProductCategory,
ProductCollection,
ProductOption,
ProductTag,
ProductType,
ProductVariant,
} from "@models"
import { FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
import {
Context,
CreateProductOnlyDTO,
DAL,
FindConfig,
InternalModuleDeclaration,
ProductTypes,
} from "@medusajs/types"
import ProductImageService from "./product-image"
import {
InjectTransactionManager,
isDefined,
isString,
kebabCase,
MedusaContext,
} from "@medusajs/utils"
import { shouldForceTransaction } from "../utils"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
productService: ProductService<any>
productVariantService: ProductVariantService<any>
productVariantService: ProductVariantService<any, any>
productTagService: ProductTagService<any>
productCategoryService: ProductCategoryService<any>
productCollectionService: ProductCollectionService<any>
productImageService: ProductImageService<any>
productTypeService: ProductTypeService<any>
productOptionService: ProductOptionService<any>
}
export default class ProductModuleService<
TProduct = Product,
TProductVariant = ProductVariant,
TProductTag = ProductTag,
TProductCollection = ProductCollection,
TProductCategory = ProductCategory
> implements
ProductTypes.IProductModuleService<
TProduct,
TProductVariant,
TProductTag,
TProductCollection,
TProductCategory
>
TProduct extends Product = Product,
TProductVariant extends ProductVariant = ProductVariant,
TProductTag extends ProductTag = ProductTag,
TProductCollection extends ProductCollection = ProductCollection,
TProductCategory extends ProductCategory = ProductCategory,
TProductImage extends Image = Image,
TProductType extends ProductType = ProductType,
TProductOption extends ProductOption = ProductOption
> implements ProductTypes.IProductModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly productService_: ProductService<TProduct>
protected readonly productVariantService: ProductVariantService<TProductVariant>
protected readonly productCategoryService: ProductCategoryService<TProductCategory>
protected readonly productTagService: ProductTagService<TProductTag>
protected readonly productCollectionService: ProductCollectionService<TProductCollection>
protected readonly productVariantService_: ProductVariantService<
TProductVariant,
TProduct
>
protected readonly productCategoryService_: ProductCategoryService<TProductCategory>
protected readonly productTagService_: ProductTagService<TProductTag>
protected readonly productCollectionService_: ProductCollectionService<TProductCollection>
protected readonly productImageService_: ProductImageService<TProductImage>
protected readonly productTypeService_: ProductTypeService<TProductType>
protected readonly productOptionService_: ProductOptionService<TProductOption>
constructor({
productService,
productVariantService,
productTagService,
productCategoryService,
productCollectionService,
}: InjectedDependencies) {
constructor(
{
baseRepository,
productService,
productVariantService,
productTagService,
productCategoryService,
productCollectionService,
productImageService,
productTypeService,
productOptionService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
this.productService_ = productService
this.productVariantService = productVariantService
this.productTagService = productTagService
this.productCategoryService = productCategoryService
this.productCollectionService = productCollectionService
this.productVariantService_ = productVariantService
this.productTagService_ = productTagService
this.productCategoryService_ = productCategoryService
this.productCollectionService_ = productCollectionService
this.productImageService_ = productImageService
this.productTypeService_ = productTypeService
this.productOptionService_ = productOptionService
}
async list(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<ProductTypes.ProductDTO[]> {
const products = await this.productService_.list(
filters,
@@ -71,10 +110,22 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(products))
}
async retrieve(
productId: string,
sharedContext?: Context
): Promise<ProductTypes.ProductDTO> {
const product = await this.productService_.retrieve(
productId,
sharedContext
)
return JSON.parse(JSON.stringify(product))
}
async listAndCount(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<[ProductTypes.ProductDTO[], number]> {
const [products, count] = await this.productService_.listAndCount(
filters,
@@ -85,12 +136,26 @@ export default class ProductModuleService<
return [JSON.parse(JSON.stringify(products)), count]
}
async retrieveVariant(
productVariantId: string,
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
): Promise<ProductTypes.ProductVariantDTO> {
const productVariant = await this.productVariantService_.retrieve(
productVariantId,
config,
sharedContext
)
return JSON.parse(JSON.stringify(productVariant))
}
async listVariants(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<ProductTypes.ProductVariantDTO[]> {
const variants = await this.productVariantService.list(
const variants = await this.productVariantService_.list(
filters,
config,
sharedContext
@@ -99,12 +164,26 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(variants))
}
async listAndCountVariants(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
): Promise<[ProductTypes.ProductVariantDTO[], number]> {
const [variants, count] = await this.productVariantService_.listAndCount(
filters,
config,
sharedContext
)
return [JSON.parse(JSON.stringify(variants)), count]
}
async listTags(
filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO[]> {
const tags = await this.productTagService.list(
const tags = await this.productTagService_.list(
filters,
config,
sharedContext
@@ -113,12 +192,26 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(tags))
}
async retrieveCollection(
productCollectionId: string,
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
): Promise<ProductTypes.ProductCollectionDTO> {
const productCollection = await this.productCollectionService_.retrieve(
productCollectionId,
config,
sharedContext
)
return JSON.parse(JSON.stringify(productCollection))
}
async listCollections(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<ProductTypes.ProductCollectionDTO[]> {
const collections = await this.productCollectionService.list(
const collections = await this.productCollectionService_.list(
filters,
config,
sharedContext
@@ -127,12 +220,40 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(collections))
}
async listAndCountCollections(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
): Promise<[ProductTypes.ProductCollectionDTO[], number]> {
const collections = await this.productCollectionService_.listAndCount(
filters,
config,
sharedContext
)
return JSON.parse(JSON.stringify(collections))
}
async retrieveCategory(
productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO> {
const productCategory = await this.productCategoryService_.retrieve(
productCategoryId,
config,
sharedContext
)
return JSON.parse(JSON.stringify(productCategory))
}
async listCategories(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO[]> {
const categories = await this.productCategoryService.list(
const categories = await this.productCategoryService_.list(
filters,
config,
sharedContext
@@ -140,4 +261,199 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(categories))
}
async listAndCountCategories(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
): Promise<[ProductTypes.ProductCategoryDTO[], number]> {
const categories = await this.productCategoryService_.listAndCount(
filters,
config,
sharedContext
)
return JSON.parse(JSON.stringify(categories))
}
async create(data: ProductTypes.CreateProductDTO[], sharedContext?: Context) {
const products = await this.create_(data, sharedContext)
return this.baseRepository_.serialize<
TProduct[],
ProductTypes.ProductDTO[]
>(products, {
populate: true,
})
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
protected async create_(
data: ProductTypes.CreateProductDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TProduct[]> {
const productVariantsMap = new Map<
string,
ProductTypes.CreateProductVariantDTO[]
>()
const productOptionsMap = new Map<
string,
ProductTypes.CreateProductOptionDTO[]
>()
const productsData = await Promise.all(
data.map(async (product) => {
const productData = { ...product }
if (!productData.handle) {
productData.handle = kebabCase(product.title)
}
const variants = productData.variants
const options = productData.options
delete productData.options
delete productData.variants
productVariantsMap.set(productData.handle!, variants ?? [])
productOptionsMap.set(productData.handle!, options ?? [])
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.is_giftcard) {
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_id = (
await this.productTypeService_.upsert(
[productData.type as ProductTypes.CreateProductTypeDTO],
sharedContext
)
)?.[0]!.id
}
return productData as CreateProductOnlyDTO
})
)
const products = await this.productService_.create(
productsData,
sharedContext
)
const productByHandleMap = new Map<string, TProduct>(
products.map((product) => [product.handle!, product])
)
const productOptionsData = [...productOptionsMap]
.map(([handle, options]) => {
return options.map((option) => {
return {
...option,
product: productByHandleMap.get(handle)!,
}
})
})
.flat()
const productOptions = await this.productOptionService_.create(
productOptionsData,
sharedContext
)
for (const variants of productVariantsMap.values()) {
variants.forEach((variant) => {
variant.options = variant.options?.map((option, index) => {
const productOption = productOptions[index]
return {
option: productOption,
value: option.value,
}
})
})
}
await Promise.all(
[...productVariantsMap].map(async ([handle, variants]) => {
return await this.productVariantService_.create(
productByHandleMap.get(handle)!,
variants as unknown as ProductTypes.CreateProductVariantOnlyDTO[],
sharedContext
)
})
)
return products
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async delete(
productIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productService_.delete(productIds, sharedContext)
}
async softDelete(
productIds: string[],
sharedContext: Context = {}
): Promise<ProductTypes.ProductDTO[]> {
const products = await this.softDelete_(productIds, sharedContext)
return this.baseRepository_.serialize<
TProduct[],
ProductTypes.ProductDTO[]
>(products, {
populate: true,
})
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
protected async softDelete_(
productIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<TProduct[]> {
return await this.productService_.softDelete(productIds, sharedContext)
}
async restore(
productIds: string[],
sharedContext: Context = {}
): Promise<ProductTypes.ProductDTO[]> {
const products = await this.restore_(productIds, sharedContext)
return this.baseRepository_.serialize<
TProduct[],
ProductTypes.ProductDTO[]
>(products, {
populate: true,
})
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async restore_(
productIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<TProduct[]> {
return await this.productService_.restore(productIds, sharedContext)
}
}
@@ -0,0 +1,32 @@
import { ProductOption } from "@models"
import { Context, DAL, ProductTypes } from "@medusajs/types"
import { ProductOptionRepository } from "@repositories"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import { doNotForceTransaction } from "../utils"
type InjectedDependencies = {
productOptionRepository: DAL.RepositoryService
}
export default class ProductOptionService<
TEntity extends ProductOption = ProductOption
> {
protected readonly productOptionRepository_: DAL.RepositoryService
constructor({ productOptionRepository }: InjectedDependencies) {
this.productOptionRepository_ =
productOptionRepository as ProductOptionRepository
}
@InjectTransactionManager(doNotForceTransaction, "productOptionRepository_")
async create(
data: ProductTypes.CreateProductOptionOnlyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.productOptionRepository_ as ProductOptionRepository
).create(data, {
transactionManager: sharedContext.transactionManager,
})) as TEntity[]
}
}
+35 -7
View File
@@ -1,13 +1,27 @@
import { ProductTag } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
import { buildQuery } from "../utils"
import {
Context,
CreateProductTagDTO,
DAL,
FindConfig,
ProductTypes,
} from "@medusajs/types"
import {
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
} from "@medusajs/utils"
import { doNotForceTransaction } from "../utils"
import { ProductTagRepository } from "@repositories"
type InjectedDependencies = {
productTagRepository: DAL.RepositoryService
}
export default class ProductTagService<TEntity = ProductTag> {
protected readonly productTagRepository_: DAL.RepositoryService<TEntity>
export default class ProductTagService<
TEntity extends ProductTag = ProductTag
> {
protected readonly productTagRepository_: DAL.RepositoryService
constructor({ productTagRepository }: InjectedDependencies) {
this.productTagRepository_ = productTagRepository
@@ -16,14 +30,28 @@ export default class ProductTagService<TEntity = ProductTag> {
async list(
filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<TEntity[]> {
const queryOptions = buildQuery<TEntity>(filters, config)
const queryOptions = ModulesSdkUtils.buildQuery<ProductTag>(filters, config)
if (filters.value) {
queryOptions.where["value"] = { $ilike: filters.value }
}
return await this.productTagRepository_.find(queryOptions)
return (await this.productTagRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, "productTagRepository_")
async upsert(
tags: CreateProductTagDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTagRepository_ as ProductTagRepository).upsert!(
tags,
sharedContext
)) as TEntity[]
}
}
@@ -0,0 +1,28 @@
import { ProductType } from "@models"
import { Context, CreateProductTypeDTO, DAL } from "@medusajs/types"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import { doNotForceTransaction } from "../utils"
import { ProductTypeRepository } from "@repositories"
type InjectedDependencies = {
productTypeRepository: DAL.RepositoryService
}
export default class ProductTypeService<
TEntity extends ProductType = ProductType
> {
protected readonly productTypeRepository_: DAL.RepositoryService
constructor({ productTypeRepository }: InjectedDependencies) {
this.productTypeRepository_ = productTypeRepository
}
@InjectTransactionManager(doNotForceTransaction, "productTypeRepository_")
async upsert(
types: CreateProductTypeDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTypeRepository_ as ProductTypeRepository)
.upsert!(types, sharedContext)) as TEntity[]
}
}
@@ -1,24 +1,115 @@
import { ProductVariant } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
import { buildQuery } from "../utils"
import { Product, ProductVariant } from "@models"
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import {
InjectTransactionManager,
isString,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import ProductService from "./product"
import { doNotForceTransaction } from "../utils"
import { ProductVariantRepository } from "@repositories"
type InjectedDependencies = {
productVariantRepository: DAL.RepositoryService
productService: ProductService<any>
}
export default class ProductVariantService<TEntity = ProductVariant> {
protected readonly productVariantRepository_: DAL.RepositoryService<TEntity>
export default class ProductVariantService<
TEntity extends ProductVariant = ProductVariant,
TProduct extends Product = Product
> {
protected readonly productVariantRepository_: DAL.RepositoryService
protected readonly productService_: ProductService<TProduct>
constructor({ productVariantRepository }: InjectedDependencies) {
constructor({
productVariantRepository,
productService,
}: InjectedDependencies) {
this.productVariantRepository_ = productVariantRepository
this.productService_ = productService
}
async retrieve(
productVariantId: string,
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
): Promise<TEntity> {
return (await retrieveEntity<
ProductVariant,
ProductTypes.ProductVariantDTO
>({
id: productVariantId,
entityName: ProductVariant.name,
repository: this.productVariantRepository_,
config,
sharedContext,
})) as TEntity
}
async list(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<TEntity[]> {
const queryOptions = buildQuery<TEntity>(filters, config)
return await this.productVariantRepository_.find(queryOptions)
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
filters,
config
)
return (await this.productVariantRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
async listAndCount(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
filters,
config
)
return (await this.productVariantRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
async create(
productOrId: TProduct | string,
data: ProductTypes.CreateProductVariantOnlyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
let product = productOrId as unknown as Product
if (isString(productOrId)) {
product = await this.productService_.retrieve(
productOrId as string,
sharedContext
)
}
let computedRank = product.variants.toArray().length
const data_ = [...data]
data_.forEach((variant) => {
Object.assign(variant, {
variant_rank: computedRank++,
product,
})
})
return (await (
this.productVariantRepository_ as ProductVariantRepository
).create(data_, {
transactionManager: sharedContext.transactionManager,
})) as TEntity[]
}
}
+99 -13
View File
@@ -1,25 +1,55 @@
import { ProductTagService, ProductVariantService } from "@services"
import { Product } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
import { buildQuery } from "../utils"
import {
Context,
DAL,
FindConfig,
ProductStatus,
ProductTypes,
WithRequiredProperty,
} from "@medusajs/types"
import {
InjectTransactionManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
import { ProductRepository } from "@repositories"
import { doNotForceTransaction } from "../utils"
type InjectedDependencies = {
productRepository: DAL.RepositoryService
productVariantService: ProductVariantService
productTagService: ProductTagService
}
export default class ProductService<TEntity = Product> {
protected readonly productRepository_: DAL.RepositoryService<TEntity>
export default class ProductService<TEntity extends Product = Product> {
protected readonly productRepository_: DAL.RepositoryService
constructor({ productRepository }: InjectedDependencies) {
this.productRepository_ = productRepository
}
async retrieve(productId: string, sharedContext?: Context): Promise<TEntity> {
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
id: productId,
})
const product = await this.productRepository_.find(
queryOptions,
sharedContext
)
if (!product?.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id: ${productId} was not found`
)
}
return product[0] as TEntity
}
async list(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<TEntity[]> {
if (filters.category_ids) {
if (Array.isArray(filters.category_ids)) {
@@ -34,14 +64,17 @@ export default class ProductService<TEntity = Product> {
delete filters.category_ids
}
const queryOptions = buildQuery<TEntity>(filters, config)
return await this.productRepository_.find(queryOptions)
const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
return (await this.productRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
async listAndCount(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext
sharedContext?: Context
): Promise<[TEntity[], number]> {
if (filters.category_ids) {
if (Array.isArray(filters.category_ids)) {
@@ -56,7 +89,60 @@ export default class ProductService<TEntity = Product> {
delete filters.category_ids
}
const queryOptions = buildQuery<TEntity>(filters, config)
return await this.productRepository_.findAndCount(queryOptions)
const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
return (await this.productRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
async create(
data: ProductTypes.CreateProductOnlyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
data.forEach((product) => {
product.status ??= ProductStatus.DRAFT
})
return (await (this.productRepository_ as ProductRepository).create(
data as WithRequiredProperty<
ProductTypes.CreateProductOnlyDTO,
"status"
>[],
{
transactionManager: sharedContext.transactionManager,
}
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productRepository_.delete(ids, {
transactionManager: sharedContext.transactionManager,
})
}
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
async softDelete(
productIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return await this.productRepository_.softDelete(productIds, {
transactionManager: sharedContext.transactionManager,
})
}
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
async restore(
productIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return await this.productRepository_.restore(productIds, {
transactionManager: sharedContext.transactionManager,
})
}
}