feat(product, types, utils): tags, types, options, categories and collections for product module (#4535)

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

* chore: added tags and types methods for product module

* chore: remove console log

* chore: fork managers on every repository call

* chore: add forked manager decorator

* chore: rename to inject manager from inject forked manager

* chore: added product tag and type create/update/delete

* chore: added collection create/update/delete

* chore: fix naming

* update injectManager and related behaviour

* simplify get active manager

* chore: reset package.json

* makes the base repository methods related to manager generic

* makes some wrapper generic enough

* add some todos

* chore: added options CRUD

* clean up

* chore: fix issue with deleted_at not being updated

* wip

* chore: added categories write operations

* chore: remove async nature of injection decorator

* chore: added changeset

* chore: reset package.json

* chore: add manager injection to all contextable methods

* chore: fix unit tests

---------

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:
Riqwan Thamir
2023-07-27 13:22:17 +05:30
committed by GitHub
parent f561601bf6
commit 131477faf0
43 changed files with 4218 additions and 229 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/product": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(product,types,utils): Add tags, types, categories, collection and options CRUD to product module services

View File

@@ -2,7 +2,7 @@ export const productCategoriesData = [
{
id: "category-0",
name: "category 0",
parent_category_id: null
parent_category_id: null,
},
{
id: "category-1",
@@ -26,3 +26,43 @@ export const productCategoriesData = [
parent_category_id: "category-1-b"
},
]
export const productCategoriesRankData = [
{
id: "category-0-0",
name: "category 0 0",
parent_category_id: null,
rank: 0,
},
{
id: "category-0-1",
name: "category 0 1",
parent_category_id: null,
rank: 1,
},
{
id: "category-0-2",
name: "category 0 2",
parent_category_id: null,
rank: 2,
},
{
id: "category-0-0-0",
name: "category 0 0-0",
parent_category_id: "category-0-0",
rank: 0,
},
{
id: "category-0-0-1",
name: "category 0 0-1",
parent_category_id: "category-0-0",
rank: 1,
},
{
id: "category-0-0-2",
name: "category 0 0-2",
parent_category_id: "category-0-0",
rank: 2,
},
]

View File

@@ -30,6 +30,24 @@ export async function createProductAndTags(
return products
}
export async function createProductAndTypes(
manager: SqlEntityManager,
data: {
id?: string
title: string
status: ProductTypes.ProductStatus
type?: { id: string; value: string }
}[]
) {
const products: any[] = data.map((productData) => {
return manager.create(Product, productData)
})
await manager.persistAndFlush(products)
return products
}
export async function createProductVariants(
manager: SqlEntityManager,
data: any[]

View File

@@ -5,7 +5,7 @@ import { ProductCategoryRepository } from "@repositories"
import { ProductCategoryService } from "@services"
import { createProductCategories } from "../../../__fixtures__/product-category"
import { productCategoriesData } from "../../../__fixtures__/product-category/data"
import { productCategoriesData, productCategoriesRankData } from "../../../__fixtures__/product-category/data"
import { TestDatabase } from "../../../utils"
jest.setTimeout(30000)
@@ -24,7 +24,7 @@ describe("Product category Service", () => {
manager: repositoryManager,
})
service = new ProductCategoryService({
service = new ProductCategoryService<ProductCategory>({
productCategoryRepository,
})
})
@@ -560,4 +560,287 @@ describe("Product category Service", () => {
])
})
})
describe("create", () => {
it("should create a category successfully", async () => {
await service.create({
name: "New Category",
parent_category_id: null,
})
const [productCategory] = await service.list({
name: "New Category"
}, {
select: ["name", "rank"]
})
expect(productCategory).toEqual(
expect.objectContaining({
name: "New Category",
rank: "0",
})
)
})
it("should append rank from an existing category depending on parent", async () => {
await service.create({
name: "New Category",
parent_category_id: null,
rank: 0
})
await service.create({
name: "New Category 2",
parent_category_id: null,
})
const [productCategoryNew] = await service.list({
name: "New Category 2"
}, {
select: ["name", "rank"]
})
expect(productCategoryNew).toEqual(
expect.objectContaining({
name: "New Category 2",
rank: "1",
})
)
await service.create({
name: "New Category 2.1",
parent_category_id: productCategoryNew.id,
})
const [productCategoryWithParent] = await service.list({
name: "New Category 2.1"
}, {
select: ["name", "rank", "parent_category_id"]
})
expect(productCategoryWithParent).toEqual(
expect.objectContaining({
name: "New Category 2.1",
parent_category_id: productCategoryNew.id,
rank: "0",
})
)
})
})
describe("update", () => {
let productCategoryZero
let productCategoryOne
let productCategoryTwo
let productCategoryZeroZero
let productCategoryZeroOne
let productCategoryZeroTwo
let categories
beforeEach(async () => {
testManager = await TestDatabase.forkManager()
categories = await createProductCategories(
testManager,
productCategoriesRankData
)
productCategoryZero = categories[0]
productCategoryOne = categories[1]
productCategoryTwo = categories[2]
productCategoryZeroZero = categories[3]
productCategoryZeroOne = categories[4]
productCategoryZeroTwo = categories[5]
})
it("should update the name of the category successfully", async () => {
await service.update(productCategoryZero.id, {
name: "New Category"
})
const productCategory = await service.retrieve(productCategoryZero.id, {
select: ["name"]
})
expect(productCategory.name).toEqual("New Category")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.update("does-not-exist", {
name: "New Category"
})
} catch (e) {
error = e
}
expect(error.message).toEqual(`ProductCategory not found ({ id: 'does-not-exist' })`)
})
it("should reorder rank successfully in the same parent", async () => {
await service.update(productCategoryTwo.id, {
rank: 0,
})
const productCategories = await service.list({
parent_category_id: null
}, {
select: ["name", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryTwo.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryZero.id,
rank: "1",
}),
expect.objectContaining({
id: productCategoryOne.id,
rank: "2",
})
])
)
})
it("should reorder rank successfully when changing parent", async () => {
await service.update(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id
})
const productCategories = await service.list({
parent_category_id: productCategoryZero.id
}, {
select: ["name", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryTwo.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryZeroZero.id,
rank: "1",
}),
expect.objectContaining({
id: productCategoryZeroOne.id,
rank: "2",
}),
expect.objectContaining({
id: productCategoryZeroTwo.id,
rank: "3",
})
])
)
})
it("should reorder rank successfully when changing parent and in first position", async () => {
await service.update(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id
})
const productCategories = await service.list({
parent_category_id: productCategoryZero.id
}, {
select: ["name", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryTwo.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryZeroZero.id,
rank: "1",
}),
expect.objectContaining({
id: productCategoryZeroOne.id,
rank: "2",
}),
expect.objectContaining({
id: productCategoryZeroTwo.id,
rank: "3",
})
])
)
})
})
describe("delete", () => {
let productCategoryZero
let productCategoryOne
let productCategoryTwo
let categories
beforeEach(async () => {
testManager = await TestDatabase.forkManager()
categories = await createProductCategories(
testManager,
productCategoriesRankData
)
productCategoryZero = categories[0]
productCategoryOne = categories[1]
productCategoryTwo = categories[2]
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.delete("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(`ProductCategory not found ({ id: 'does-not-exist' })`)
})
it("should throw an error when it has children", async () => {
let error
try {
await service.delete(productCategoryZero.id)
} catch (e) {
error = e
}
expect(error.message).toEqual(`Deleting ProductCategory (category-0-0) with category children is not allowed`)
})
it("should reorder siblings rank successfully on deleting", async () => {
await service.delete(productCategoryOne.id)
const productCategories = await service.list({
parent_category_id: null
}, {
select: ["id", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryZero.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryTwo.id,
rank: "1",
})
])
)
})
})
})

View File

@@ -273,4 +273,90 @@ describe("Product collection Service", () => {
)
})
})
describe("delete", () => {
const collectionId = "collection-1"
const collectionData = {
id: collectionId,
title: "collection 1",
}
beforeEach(async () => {
testManager = await TestDatabase.forkManager()
await createCollections(testManager, [collectionData])
})
it("should delete the product collection given an ID successfully", async () => {
await service.delete(
[collectionId],
)
const collections = await service.list({
id: collectionId
})
expect(collections).toHaveLength(0)
})
})
describe("update", () => {
const collectionId = "collection-1"
const collectionData = {
id: collectionId,
title: "collection 1",
}
beforeEach(async () => {
testManager = await TestDatabase.forkManager()
await createCollections(testManager, [collectionData])
})
it("should update the value of the collection successfully", async () => {
await service.update(
[{
id: collectionId,
title: "New Collection"
}]
)
const productCollection = await service.retrieve(collectionId)
expect(productCollection.title).toEqual("New Collection")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
title: "New Collection"
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductCollection with id "does-not-exist" not found')
})
})
describe("create", () => {
it("should create a collection successfully", async () => {
await service.create(
[{
title: "New Collection"
}]
)
const [productCollection] = await service.list({
title: "New Collection"
})
expect(productCollection.title).toEqual("New Collection")
})
})
})

View File

@@ -6,6 +6,7 @@ import { ProductTypes } from "@medusajs/types"
import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils"
import { createProductCategories } from "../../../__fixtures__/product-category"
import { productCategoriesRankData } from "../../../__fixtures__/product-category/data"
describe("ProductModuleService product categories", () => {
let service: IProductModuleService
@@ -201,7 +202,9 @@ describe("ProductModuleService product categories", () => {
describe("retrieveCategory", () => {
it("should return the requested category", async () => {
const result = await service.retrieveCategory(productCategoryOne.id)
const result = await service.retrieveCategory(productCategoryOne.id, {
select: ["id", "name"]
})
expect(result).toEqual(
expect.objectContaining({
@@ -244,5 +247,288 @@ describe("ProductModuleService product categories", () => {
expect(error.message).toEqual("ProductCategory with id: does-not-exist was not found")
})
})
describe("createCategory", () => {
it("should create a category successfully", async () => {
await service.createCategory({
name: "New Category",
parent_category_id: productCategoryOne.id,
})
const [productCategory] = await service.listCategories({
name: "New Category"
}, {
select: ["name", "rank"]
})
expect(productCategory).toEqual(
expect.objectContaining({
name: "New Category",
rank: "0",
})
)
})
it("should append rank from an existing category depending on parent", async () => {
await service.createCategory({
name: "New Category",
parent_category_id: productCategoryOne.id,
rank: 0
})
await service.createCategory({
name: "New Category 2",
parent_category_id: productCategoryOne.id,
})
const [productCategoryNew] = await service.listCategories({
name: "New Category 2"
}, {
select: ["name", "rank"]
})
expect(productCategoryNew).toEqual(
expect.objectContaining({
name: "New Category 2",
rank: "1",
})
)
await service.createCategory({
name: "New Category 2.1",
parent_category_id: productCategoryNew.id,
})
const [productCategoryWithParent] = await service.listCategories({
name: "New Category 2.1"
}, {
select: ["name", "rank", "parent_category_id"]
})
expect(productCategoryWithParent).toEqual(
expect.objectContaining({
name: "New Category 2.1",
parent_category_id: productCategoryNew.id,
rank: "0",
})
)
})
})
describe("updateCategory", () => {
let productCategoryZero
let productCategoryOne
let productCategoryTwo
let productCategoryZeroZero
let productCategoryZeroOne
let productCategoryZeroTwo
let categories
beforeEach(async () => {
testManager = await TestDatabase.forkManager()
categories = await createProductCategories(
testManager,
productCategoriesRankData
)
productCategoryZero = categories[0]
productCategoryOne = categories[1]
productCategoryTwo = categories[2]
productCategoryZeroZero = categories[3]
productCategoryZeroOne = categories[4]
productCategoryZeroTwo = categories[5]
})
it("should update the name of the category successfully", async () => {
await service.updateCategory(productCategoryZero.id, {
name: "New Category"
})
const productCategory = await service.retrieveCategory(productCategoryZero.id, {
select: ["name"]
})
expect(productCategory.name).toEqual("New Category")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.updateCategory("does-not-exist", {
name: "New Category"
})
} catch (e) {
error = e
}
expect(error.message).toEqual(`ProductCategory not found ({ id: 'does-not-exist' })`)
})
it("should reorder rank successfully in the same parent", async () => {
await service.updateCategory(productCategoryTwo.id, {
rank: 0,
})
const productCategories = await service.listCategories({
parent_category_id: null
}, {
select: ["name", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryTwo.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryZero.id,
rank: "1",
}),
expect.objectContaining({
id: productCategoryOne.id,
rank: "2",
})
])
)
})
it("should reorder rank successfully when changing parent", async () => {
await service.updateCategory(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id
})
const productCategories = await service.listCategories({
parent_category_id: productCategoryZero.id
}, {
select: ["name", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryTwo.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryZeroZero.id,
rank: "1",
}),
expect.objectContaining({
id: productCategoryZeroOne.id,
rank: "2",
}),
expect.objectContaining({
id: productCategoryZeroTwo.id,
rank: "3",
})
])
)
})
it("should reorder rank successfully when changing parent and in first position", async () => {
await service.updateCategory(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id
})
const productCategories = await service.listCategories({
parent_category_id: productCategoryZero.id
}, {
select: ["name", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryTwo.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryZeroZero.id,
rank: "1",
}),
expect.objectContaining({
id: productCategoryZeroOne.id,
rank: "2",
}),
expect.objectContaining({
id: productCategoryZeroTwo.id,
rank: "3",
})
])
)
})
})
describe("deleteCategory", () => {
let productCategoryZero
let productCategoryOne
let productCategoryTwo
let categories
beforeEach(async () => {
testManager = await TestDatabase.forkManager()
categories = await createProductCategories(
testManager,
productCategoriesRankData
)
productCategoryZero = categories[0]
productCategoryOne = categories[1]
productCategoryTwo = categories[2]
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.deleteCategory("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(`ProductCategory not found ({ id: 'does-not-exist' })`)
})
it("should throw an error when it has children", async () => {
let error
try {
await service.deleteCategory(productCategoryZero.id)
} catch (e) {
error = e
}
expect(error.message).toEqual(`Deleting ProductCategory (category-0-0) with category children is not allowed`)
})
it("should reorder siblings rank successfully on deleting", async () => {
await service.deleteCategory(productCategoryOne.id)
const productCategories = await service.listCategories({
parent_category_id: null
}, {
select: ["id", "rank"]
})
expect(productCategories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryZero.id,
rank: "0",
}),
expect.objectContaining({
id: productCategoryTwo.id,
rank: "1",
})
])
)
})
})
})

View File

@@ -246,5 +246,71 @@ describe("ProductModuleService product collections", () => {
expect(error.message).toEqual("ProductCollection with id: does-not-exist was not found")
})
})
describe("deleteCollections", () => {
const collectionId = "test-1"
it("should delete the product collection given an ID successfully", async () => {
await service.deleteCollections(
[collectionId],
)
const collections = await service.listCollections({
id: collectionId
})
expect(collections).toHaveLength(0)
})
})
describe("updateCollections", () => {
const collectionId = "test-1"
it("should update the value of the collection successfully", async () => {
await service.updateCollections(
[{
id: collectionId,
title: "New Collection"
}]
)
const productCollection = await service.retrieveCollection(collectionId)
expect(productCollection.title).toEqual("New Collection")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.updateCollections([
{
id: "does-not-exist",
title: "New Collection"
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductCollection with id "does-not-exist" not found')
})
})
describe("createCollections", () => {
it("should create a collection successfully", async () => {
const res = await service.createCollections(
[{
title: "New Collection"
}]
)
const [productCollection] = await service.listCollections({
title: "New Collection"
})
expect(productCollection.title).toEqual("New Collection")
})
})
})

View File

@@ -0,0 +1,300 @@
import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils"
import { IProductModuleService } from "@medusajs/types"
import { Product, ProductOption } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductTypes } from "@medusajs/types"
describe("ProductModuleService product options", () => {
let service: IProductModuleService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let optionOne: ProductOption
let optionTwo: ProductOption
let productOne: Product
let productTwo: Product
beforeEach(async () => {
await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
},
})
testManager = await TestDatabase.forkManager()
productOne = testManager.create(Product, {
id: "product-1",
title: "product 1",
status: ProductTypes.ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
id: "product-2",
title: "product 2",
status: ProductTypes.ProductStatus.PUBLISHED,
})
optionOne = testManager.create(ProductOption, {
id: "option-1",
title: "option 1",
product: productOne,
})
optionTwo = testManager.create(ProductOption, {
id: "option-2",
title: "option 1",
product: productTwo,
})
await testManager.persistAndFlush([optionOne, optionTwo])
})
afterEach(async () => {
await TestDatabase.clearDatabase()
})
describe("listOptions", () => {
it("should return options and count queried by ID", async () => {
const options = await service.listOptions({
id: optionOne.id,
})
expect(options).toEqual([
expect.objectContaining({
id: optionOne.id,
}),
])
})
it("should return options and count based on the options and filter parameter", async () => {
let options = await service.listOptions(
{
id: optionOne.id,
},
{
take: 1,
}
)
expect(options).toEqual([
expect.objectContaining({
id: optionOne.id,
}),
])
options = await service.listOptions({}, { take: 1, skip: 1 })
expect(options).toEqual([
expect.objectContaining({
id: optionTwo.id,
}),
])
})
it("should return only requested fields and relations for options", async () => {
const options = await service.listOptions(
{
id: optionOne.id,
},
{
select: ["title", "product.id"],
relations: ["product"],
take: 1
}
)
expect(options).toEqual([
{
id: optionOne.id,
title: optionOne.title,
product_id: productOne.id,
product: {
id: productOne.id,
},
},
])
})
})
describe("listAndCountOptions", () => {
it("should return options and count queried by ID", async () => {
const [options, count] = await service.listAndCountOptions({
id: optionOne.id,
})
expect(count).toEqual(1)
expect(options).toEqual([
expect.objectContaining({
id: optionOne.id,
}),
])
})
it("should return options and count based on the options and filter parameter", async () => {
let [options, count] = await service.listAndCountOptions(
{
id: optionOne.id,
},
{
take: 1,
}
)
expect(count).toEqual(1)
expect(options).toEqual([
expect.objectContaining({
id: optionOne.id,
}),
])
;[options, count] = await service.listAndCountOptions({}, { take: 1 })
expect(count).toEqual(2)
;[options, count] = await service.listAndCountOptions({}, { take: 1, skip: 1 })
expect(count).toEqual(2)
expect(options).toEqual([
expect.objectContaining({
id: optionTwo.id,
}),
])
})
it("should return only requested fields and relations for options", async () => {
const [options, count] = await service.listAndCountOptions(
{
id: optionOne.id,
},
{
select: ["title", "product.id"],
relations: ["product"],
take: 1
}
)
expect(count).toEqual(1)
expect(options).toEqual([{
id: optionOne.id,
title: optionOne.title,
product_id: productOne.id,
product: {
id: productOne.id,
},
}])
})
})
describe("retrieveOption", () => {
it("should return the requested option", async () => {
const option = await service.retrieveOption(optionOne.id)
expect(option).toEqual(
expect.objectContaining({
id: optionOne.id,
}),
)
})
it("should return requested attributes when requested through config", async () => {
const option = await service.retrieveOption(
optionOne.id,
{
select: ["id", "product.title"],
relations: ["product"],
}
)
expect(option).toEqual(
expect.objectContaining({
id: optionOne.id,
product: {
id: "product-1",
title: "product 1",
},
}),
)
})
it("should throw an error when a option with ID does not exist", async () => {
let error
try {
await service.retrieveOption("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual("ProductOption with id: does-not-exist was not found")
})
})
describe("deleteOptions", () => {
const optionId = "option-1"
it("should delete the product option given an ID successfully", async () => {
await service.deleteOptions(
[optionId],
)
const options = await service.listOptions({
id: optionId
})
expect(options).toHaveLength(0)
})
})
describe("updateOptions", () => {
const optionId = "option-1"
it("should update the title of the option successfully", async () => {
await service.updateOptions(
[{
id: optionId,
title: "new test"
}]
)
const productOption = await service.retrieveOption(optionId)
expect(productOption.title).toEqual("new test")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.updateOptions([
{
id: "does-not-exist",
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductOption with id "does-not-exist" not found')
})
})
describe("createOptions", () => {
it("should create a option successfully", async () => {
const res = await service.createOptions([{
title: "test",
product_id: productOne.id
}])
const productOption = await service.listOptions({
title: "test"
})
expect(productOption[0]?.title).toEqual("test")
})
})
})

View File

@@ -0,0 +1,303 @@
import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils"
import { IProductModuleService } from "@medusajs/types"
import { Product, ProductTag } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductTypes } from "@medusajs/types"
describe("ProductModuleService product tags", () => {
let service: IProductModuleService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let tagOne: ProductTag
let tagTwo: ProductTag
let productOne: Product
let productTwo: Product
beforeEach(async () => {
await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
},
})
testManager = await TestDatabase.forkManager()
productOne = testManager.create(Product, {
id: "product-1",
title: "product 1",
status: ProductTypes.ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
id: "product-2",
title: "product 2",
status: ProductTypes.ProductStatus.PUBLISHED,
})
tagOne = testManager.create(ProductTag, {
id: "tag-1",
value: "tag 1",
products: [productOne],
})
tagTwo = testManager.create(ProductTag, {
id: "tag-2",
value: "tag",
products: [productTwo],
})
await testManager.persistAndFlush([tagOne, tagTwo])
})
afterEach(async () => {
await TestDatabase.clearDatabase()
})
describe("listTags", () => {
it("should return tags and count queried by ID", async () => {
const tags = await service.listTags({
id: tagOne.id,
})
expect(tags).toEqual([
expect.objectContaining({
id: tagOne.id,
}),
])
})
it("should return tags and count based on the options and filter parameter", async () => {
let tags = await service.listTags(
{
id: tagOne.id,
},
{
take: 1,
}
)
expect(tags).toEqual([
expect.objectContaining({
id: tagOne.id,
}),
])
tags = await service.listTags({}, { take: 1, skip: 1 })
expect(tags).toEqual([
expect.objectContaining({
id: tagTwo.id,
}),
])
})
it("should return only requested fields and relations for tags", async () => {
const tags = await service.listTags(
{
id: tagOne.id,
},
{
select: ["value", "products.id"],
relations: ["products"],
take: 1
}
)
expect(tags).toEqual([
{
id: tagOne.id,
value: tagOne.value,
products: [{
id: productOne.id,
}],
},
])
})
})
describe("listAndCountTags", () => {
it("should return tags and count queried by ID", async () => {
const [tags, count] = await service.listAndCountTags({
id: tagOne.id,
})
expect(count).toEqual(1)
expect(tags).toEqual([
expect.objectContaining({
id: tagOne.id,
}),
])
})
it("should return tags and count based on the options and filter parameter", async () => {
let [tags, count] = await service.listAndCountTags(
{
id: tagOne.id,
},
{
take: 1,
}
)
expect(count).toEqual(1)
expect(tags).toEqual([
expect.objectContaining({
id: tagOne.id,
}),
])
;[tags, count] = await service.listAndCountTags({}, { take: 1 })
expect(count).toEqual(2)
;[tags, count] = await service.listAndCountTags({}, { take: 1, skip: 1 })
expect(count).toEqual(2)
expect(tags).toEqual([
expect.objectContaining({
id: tagTwo.id,
}),
])
})
it("should return only requested fields and relations for tags", async () => {
const [tags, count] = await service.listAndCountTags(
{
id: tagOne.id,
},
{
select: ["value", "products.id"],
relations: ["products"],
take: 1
}
)
expect(count).toEqual(1)
expect(tags).toEqual([
{
id: tagOne.id,
value: tagOne.value,
products: [{
id: productOne.id,
}],
},
])
})
})
describe("retrieveTag", () => {
it("should return the requested tag", async () => {
const tag = await service.retrieveTag(tagOne.id)
expect(tag).toEqual(
expect.objectContaining({
id: tagOne.id,
}),
)
})
it("should return requested attributes when requested through config", async () => {
const tag = await service.retrieveTag(
tagOne.id,
{
select: ["id", "value", "products.title"],
relations: ["products"],
}
)
expect(tag).toEqual(
expect.objectContaining({
id: tagOne.id,
value: tagOne.value,
products: [{
id: "product-1",
title: "product 1",
}],
}),
)
})
it("should throw an error when a tag with ID does not exist", async () => {
let error
try {
await service.retrieveTag("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual("ProductTag with id: does-not-exist was not found")
})
})
describe("deleteTags", () => {
const tagId = "tag-1"
it("should delete the product tag given an ID successfully", async () => {
await service.deleteTags(
[tagId],
)
const tags = await service.listTags({
id: tagId
})
expect(tags).toHaveLength(0)
})
})
describe("updateTags", () => {
const tagId = "tag-1"
it("should update the value of the tag successfully", async () => {
await service.updateTags(
[{
id: tagId,
value: "UK"
}]
)
const productTag = await service.retrieveTag(tagId)
expect(productTag.value).toEqual("UK")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.updateTags([
{
id: "does-not-exist",
value: "UK"
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductTag with id "does-not-exist" not found')
})
})
describe("createTags", () => {
it("should create a tag successfully", async () => {
const res = await service.createTags(
[{
value: "UK"
}]
)
const productTag = await service.listTags({
value: "UK"
})
expect(productTag[0]?.value).toEqual("UK")
})
})
})

View File

@@ -0,0 +1,275 @@
import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils"
import { IProductModuleService } from "@medusajs/types"
import { ProductType } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductTypes } from "@medusajs/types"
describe("ProductModuleService product types", () => {
let service: IProductModuleService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let typeOne: ProductType
let typeTwo: ProductType
beforeEach(async () => {
await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
},
})
testManager = await TestDatabase.forkManager()
typeOne = testManager.create(ProductType, {
id: "type-1",
value: "type 1",
})
typeTwo = testManager.create(ProductType, {
id: "type-2",
value: "type",
})
await testManager.persistAndFlush([typeOne, typeTwo])
})
afterEach(async () => {
await TestDatabase.clearDatabase()
})
describe("listTypes", () => {
it("should return types and count queried by ID", async () => {
const types = await service.listTypes({
id: typeOne.id,
})
expect(types).toEqual([
expect.objectContaining({
id: typeOne.id,
}),
])
})
it("should return types and count based on the options and filter parameter", async () => {
let types = await service.listTypes(
{
id: typeOne.id,
},
{
take: 1,
}
)
expect(types).toEqual([
expect.objectContaining({
id: typeOne.id,
}),
])
types = await service.listTypes({}, { take: 1, skip: 1 })
expect(types).toEqual([
expect.objectContaining({
id: typeTwo.id,
}),
])
})
it("should return only requested fields for types", async () => {
const types = await service.listTypes(
{
id: typeOne.id,
},
{
select: ["value"],
take: 1
}
)
expect(types).toEqual([
{
id: typeOne.id,
value: typeOne.value,
},
])
})
})
describe("listAndCountTypes", () => {
it("should return types and count queried by ID", async () => {
const [types, count] = await service.listAndCountTypes({
id: typeOne.id,
})
expect(count).toEqual(1)
expect(types).toEqual([
expect.objectContaining({
id: typeOne.id,
}),
])
})
it("should return types and count based on the options and filter parameter", async () => {
let [types, count] = await service.listAndCountTypes(
{
id: typeOne.id,
},
{
take: 1,
}
)
expect(count).toEqual(1)
expect(types).toEqual([
expect.objectContaining({
id: typeOne.id,
}),
])
;[types, count] = await service.listAndCountTypes({}, { take: 1 })
expect(count).toEqual(2)
;[types, count] = await service.listAndCountTypes({}, { take: 1, skip: 1 })
expect(count).toEqual(2)
expect(types).toEqual([
expect.objectContaining({
id: typeTwo.id,
}),
])
})
it("should return only requested fields for types", async () => {
const [types, count] = await service.listAndCountTypes(
{
id: typeOne.id,
},
{
select: ["value"],
take: 1
}
)
expect(count).toEqual(1)
expect(types).toEqual([
{
id: typeOne.id,
value: typeOne.value,
},
])
})
})
describe("retrieveType", () => {
it("should return the requested type", async () => {
const type = await service.retrieveType(typeOne.id)
expect(type).toEqual(
expect.objectContaining({
id: typeOne.id,
}),
)
})
it("should return requested attributes when requested through config", async () => {
const type = await service.retrieveType(
typeOne.id,
{
select: ["id", "value"],
}
)
expect(type).toEqual(
{
id: typeOne.id,
value: typeOne.value,
},
)
})
it("should throw an error when a type with ID does not exist", async () => {
let error
try {
await service.retrieveType("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual("ProductType with id: does-not-exist was not found")
})
})
describe("deleteTypes", () => {
const typeId = "type-1"
it("should delete the product type given an ID successfully", async () => {
await service.deleteTypes(
[typeId],
)
const types = await service.listTypes({
id: typeId
})
expect(types).toHaveLength(0)
})
})
describe("updateTypes", () => {
const typeId = "type-1"
it("should update the value of the type successfully", async () => {
await service.updateTypes(
[{
id: typeId,
value: "UK"
}]
)
const productType = await service.retrieveType(typeId)
expect(productType.value).toEqual("UK")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.updateTypes([
{
id: "does-not-exist",
value: "UK"
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductType with id "does-not-exist" not found')
})
})
describe("createTypes", () => {
it("should create a type successfully", async () => {
const res = await service.createTypes(
[{
value: "UK"
}]
)
const productType = await service.listTypes({
value: "UK"
})
expect(productType[0]?.value).toEqual("UK")
})
})
})

View File

@@ -0,0 +1,320 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductOptionService } from "@services"
import { ProductOptionRepository } from "@repositories"
import { Product } from "@models"
import { TestDatabase } from "../../../utils"
import { createOptions } from "../../../__fixtures__/product"
import { ProductTypes } from "@medusajs/types"
jest.setTimeout(30000)
describe("ProductOption Service", () => {
let service: ProductOptionService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let productOne: Product
let productTwo: Product
let data!: Product[]
const productOneData = {
id: "product-1",
title: "product 1",
status: ProductTypes.ProductStatus.PUBLISHED,
}
const productTwoData = {
id: "product-2",
title: "product 2",
status: ProductTypes.ProductStatus.PUBLISHED,
}
beforeEach(async () => {
await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager()
const productOptionRepository = new ProductOptionRepository({
manager: repositoryManager,
})
service = new ProductOptionService({
productOptionRepository,
})
testManager = await TestDatabase.forkManager()
productOne = testManager.create(Product, productOneData)
productTwo = testManager.create(Product, productTwoData)
data = await createOptions(testManager, [
{
id: "option-1",
title: "Option 1",
product: productOne,
},
{
id: "option-2",
title: "Option 2",
product: productOne,
},
])
})
afterEach(async () => {
await TestDatabase.clearDatabase()
})
describe("list", () => {
it("list product option", async () => {
const optionResults = await service.list()
expect(optionResults).toEqual([
expect.objectContaining({
id: "option-1",
title: "Option 1",
}),
expect.objectContaining({
id: "option-2",
title: "Option 2",
}),
])
})
it("list product option by id", async () => {
const optionResults = await service.list({ id: "option-2" })
expect(optionResults).toEqual([
expect.objectContaining({
id: "option-2",
title: "Option 2",
}),
])
})
it("list product option by title matching string", async () => {
const optionResults = await service.list({ title: "Option 1" })
expect(optionResults).toEqual([
expect.objectContaining({
id: "option-1",
title: "Option 1",
}),
])
})
})
describe("listAndCount", () => {
it("should return product option and count", async () => {
const [optionResults, count] = await service.listAndCount()
expect(count).toEqual(2)
expect(optionResults).toEqual([
expect.objectContaining({
id: "option-1",
title: "Option 1",
}),
expect.objectContaining({
id: "option-2",
title: "Option 2",
}),
])
})
it("should return product option and count when filtered", async () => {
const [optionResults, count] = await service.listAndCount({ id: "option-2" })
expect(count).toEqual(1)
expect(optionResults).toEqual([
expect.objectContaining({
id: "option-2",
}),
])
})
it("should return product option and count when using skip and take", async () => {
const [optionResults, count] = await service.listAndCount({}, { skip: 1, take: 1 })
expect(count).toEqual(2)
expect(optionResults).toEqual([
expect.objectContaining({
id: "option-2",
}),
])
})
it("should return requested fields", async () => {
const [optionResults, count] = await service.listAndCount({}, {
take: 1,
select: ["title"],
})
const serialized = JSON.parse(JSON.stringify(optionResults))
expect(count).toEqual(2)
expect(serialized).toEqual([
expect.objectContaining({
id: "option-1",
}),
])
})
})
describe("retrieve", () => {
const optionId = "option-1"
const optionValue = "Option 1"
it("should return option for the given id", async () => {
const option = await service.retrieve(
optionId,
)
expect(option).toEqual(
expect.objectContaining({
id: optionId
})
)
})
it("should throw an error when option with id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductOption with id: does-not-exist was not found')
})
it("should throw an error when an id is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"productOptionId" must be defined')
})
it("should return option based on config select param", async () => {
const option = await service.retrieve(
optionId,
{
select: ["id", "title"],
}
)
const serialized = JSON.parse(JSON.stringify(option))
expect(serialized).toEqual(
{
id: optionId,
title: optionValue,
}
)
})
})
describe("delete", () => {
const optionId = "option-1"
it("should delete the product option given an ID successfully", async () => {
await service.delete(
[optionId],
)
const options = await service.list({
id: optionId
})
expect(options).toHaveLength(0)
})
})
describe("update", () => {
const optionId = "option-1"
it("should update the title of the option successfully", async () => {
await service.update(
[{
id: optionId,
title: "UK",
}]
)
const productOption = await service.retrieve(optionId)
expect(productOption.title).toEqual("UK")
})
it("should update the relationship of the option successfully", async () => {
await service.update(
[{
id: optionId,
product_id: productTwo.id,
}]
)
const productOption = await service.retrieve(optionId, {
relations: ["product"]
})
expect(productOption).toEqual(
expect.objectContaining({
id: optionId,
product: expect.objectContaining({
id: productTwo.id
})
})
)
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
title: "UK",
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductOption with id "does-not-exist" not found')
})
})
describe("create", () => {
it("should create a option successfully", async () => {
await service.create(
[{
title: "UK",
product: productOne
}]
)
const [productOption] = await service.list({
title: "UK"
}, {
relations: ["product"],
})
expect(productOption).toEqual(
expect.objectContaining({
title: "UK",
product: expect.objectContaining({
id: productOne.id
})
})
)
})
})
})

View File

@@ -10,12 +10,45 @@ import { TestDatabase } from "../../../utils"
jest.setTimeout(30000)
describe("Product tag Service", () => {
describe("ProductTag Service", () => {
let service: ProductTagService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let data!: Product[]
const productsData = [
{
id: "test-1",
title: "product 1",
status: ProductTypes.ProductStatus.PUBLISHED,
tags: [
{
id: "tag-1",
value: "France",
},
],
},
{
id: "test-2",
title: "product",
status: ProductTypes.ProductStatus.PUBLISHED,
tags: [
{
id: "tag-2",
value: "Germany",
},
{
id: "tag-3",
value: "United States",
},
{
id: "tag-4",
value: "United Kingdom",
},
],
},
]
beforeEach(async () => {
await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager()
@@ -27,6 +60,10 @@ describe("Product tag Service", () => {
service = new ProductTagService({
productTagRepository,
})
testManager = await TestDatabase.forkManager()
data = await createProductAndTags(testManager, productsData)
})
afterEach(async () => {
@@ -34,45 +71,6 @@ describe("Product tag Service", () => {
})
describe("list", () => {
const productsData = [
{
id: "test-1",
title: "product 1",
status: ProductTypes.ProductStatus.PUBLISHED,
tags: [
{
id: "tag-1",
value: "France",
},
],
},
{
id: "test-2",
title: "product",
status: ProductTypes.ProductStatus.PUBLISHED,
tags: [
{
id: "tag-2",
value: "Germany",
},
{
id: "tag-3",
value: "United States",
},
{
id: "tag-4",
value: "United Kingdom",
},
],
},
]
beforeEach(async () => {
testManager = await TestDatabase.forkManager()
data = await createProductAndTags(testManager, productsData)
})
it("list product tags", async () => {
const tagsResults = await service.list()
@@ -118,4 +116,223 @@ describe("Product tag Service", () => {
])
})
})
describe("listAndCount", () => {
it("should return product tags and count", async () => {
const [tagsResults, count] = await service.listAndCount()
expect(count).toEqual(4)
expect(tagsResults).toEqual([
expect.objectContaining({
id: "tag-1",
value: "France",
}),
expect.objectContaining({
id: "tag-2",
value: "Germany",
}),
expect.objectContaining({
id: "tag-3",
value: "United States",
}),
expect.objectContaining({
id: "tag-4",
value: "United Kingdom",
}),
])
})
it("should return product tags and count when filtered", async () => {
const [tagsResults, count] = await service.listAndCount({ id: data[0].tags![0].id })
expect(count).toEqual(1)
expect(tagsResults).toEqual([
expect.objectContaining({
id: "tag-1",
}),
])
})
it("should return product tags and count when using skip and take", async () => {
const [tagsResults, count] = await service.listAndCount({}, { skip: 1, take: 2 })
expect(count).toEqual(4)
expect(tagsResults).toEqual([
expect.objectContaining({
id: "tag-2",
}),
expect.objectContaining({
id: "tag-3",
}),
])
})
it("should return requested fields and relations", async () => {
const [tagsResults, count] = await service.listAndCount({}, {
take: 1,
select: ["value", "products.id"],
relations: ["products"]
})
const serialized = JSON.parse(JSON.stringify(tagsResults))
expect(count).toEqual(4)
expect(serialized).toEqual([
expect.objectContaining({
id: "tag-1",
products: [{
id: "test-1"
}]
}),
])
})
})
describe("retrieve", () => {
const tagId = "tag-1"
const tagValue = "France"
const productId = "test-1"
it("should return tag for the given id", async () => {
const tag = await service.retrieve(
tagId,
)
expect(tag).toEqual(
expect.objectContaining({
id: tagId
})
)
})
it("should throw an error when tag with id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductTag with id: does-not-exist was not found')
})
it("should throw an error when an id is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"productTagId" must be defined')
})
it("should return tag based on config select param", async () => {
const tag = await service.retrieve(
tagId,
{
select: ["id", "value"],
}
)
const serialized = JSON.parse(JSON.stringify(tag))
expect(serialized).toEqual(
{
id: tagId,
value: tagValue,
}
)
})
it("should return tag based on config relation param", async () => {
const tag = await service.retrieve(
tagId,
{
select: ["id", "value", "products.id"],
relations: ["products"]
}
)
const serialized = JSON.parse(JSON.stringify(tag))
expect(serialized).toEqual(
{
id: tagId,
value: tagValue,
products: [{
id: productId
}]
}
)
})
})
describe("delete", () => {
const tagId = "tag-1"
it("should delete the product tag given an ID successfully", async () => {
await service.delete(
[tagId],
)
const tags = await service.list({
id: tagId
})
expect(tags).toHaveLength(0)
})
})
describe("update", () => {
const tagId = "tag-1"
it("should update the value of the tag successfully", async () => {
await service.update(
[{
id: tagId,
value: "UK"
}]
)
const productTag = await service.retrieve(tagId)
expect(productTag.value).toEqual("UK")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
value: "UK"
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductTag with id "does-not-exist" not found')
})
})
describe("create", () => {
it("should create a tag successfully", async () => {
await service.create(
[{
value: "UK"
}]
)
const [productTag] = await service.list({
value: "UK"
})
expect(productTag.value).toEqual("UK")
})
})
})

View File

@@ -0,0 +1,280 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductTypeService } from "@services"
import { ProductTypeRepository } from "@repositories"
import { Product } from "@models"
import { TestDatabase } from "../../../utils"
import { createProductAndTypes } from "../../../__fixtures__/product"
import { ProductTypes } from "@medusajs/types"
jest.setTimeout(30000)
describe("ProductType Service", () => {
let service: ProductTypeService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let data!: Product[]
const productsData = [
{
id: "product-1",
title: "product 1",
status: ProductTypes.ProductStatus.PUBLISHED,
type: {
id: "type-1",
value: "Type 1",
},
},
{
id: "product-2",
title: "product",
status: ProductTypes.ProductStatus.PUBLISHED,
type: {
id: "type-2",
value: "Type 2",
}
},
]
beforeEach(async () => {
await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager()
const productTypeRepository = new ProductTypeRepository({
manager: repositoryManager,
})
service = new ProductTypeService({
productTypeRepository,
})
testManager = await TestDatabase.forkManager()
data = await createProductAndTypes(testManager, productsData)
})
afterEach(async () => {
await TestDatabase.clearDatabase()
})
describe("list", () => {
it("list product type", async () => {
const typeResults = await service.list()
expect(typeResults).toEqual([
expect.objectContaining({
id: "type-1",
value: "Type 1",
}),
expect.objectContaining({
id: "type-2",
value: "Type 2",
}),
])
})
it("list product type by id", async () => {
const typeResults = await service.list({ id: data[0].type.id })
expect(typeResults).toEqual([
expect.objectContaining({
id: "type-1",
value: "Type 1",
}),
])
})
it("list product type by value matching string", async () => {
const typeResults = await service.list({ value: "Type 1" })
expect(typeResults).toEqual([
expect.objectContaining({
id: "type-1",
value: "Type 1",
}),
])
})
})
describe("listAndCount", () => {
it("should return product type and count", async () => {
const [typeResults, count] = await service.listAndCount()
expect(count).toEqual(2)
expect(typeResults).toEqual([
expect.objectContaining({
id: "type-1",
value: "Type 1",
}),
expect.objectContaining({
id: "type-2",
value: "Type 2",
}),
])
})
it("should return product type and count when filtered", async () => {
const [typeResults, count] = await service.listAndCount({ id: data[0].type.id })
expect(count).toEqual(1)
expect(typeResults).toEqual([
expect.objectContaining({
id: "type-1",
}),
])
})
it("should return product type and count when using skip and take", async () => {
const [typeResults, count] = await service.listAndCount({}, { skip: 1, take: 1 })
expect(count).toEqual(2)
expect(typeResults).toEqual([
expect.objectContaining({
id: "type-2",
}),
])
})
it("should return requested fields", async () => {
const [typeResults, count] = await service.listAndCount({}, {
take: 1,
select: ["value"],
})
const serialized = JSON.parse(JSON.stringify(typeResults))
expect(count).toEqual(2)
expect(serialized).toEqual([
expect.objectContaining({
id: "type-1",
}),
])
})
})
describe("retrieve", () => {
const typeId = "type-1"
const typeValue = "Type 1"
it("should return type for the given id", async () => {
const type = await service.retrieve(
typeId,
)
expect(type).toEqual(
expect.objectContaining({
id: typeId
})
)
})
it("should throw an error when type with id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductType with id: does-not-exist was not found')
})
it("should throw an error when an id is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"productTypeId" must be defined')
})
it("should return type based on config select param", async () => {
const type = await service.retrieve(
typeId,
{
select: ["id", "value"],
}
)
const serialized = JSON.parse(JSON.stringify(type))
expect(serialized).toEqual(
{
id: typeId,
value: typeValue,
}
)
})
})
describe("delete", () => {
const typeId = "type-1"
it("should delete the product type given an ID successfully", async () => {
await service.delete(
[typeId],
)
const types = await service.list({
id: typeId
})
expect(types).toHaveLength(0)
})
})
describe("update", () => {
const typeId = "type-1"
it("should update the value of the type successfully", async () => {
await service.update(
[{
id: typeId,
value: "UK"
}]
)
const productType = await service.retrieve(typeId)
expect(productType.value).toEqual("UK")
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
value: "UK"
}
])
} catch (e) {
error = e
}
expect(error.message).toEqual('ProductType with id "does-not-exist" not found')
})
})
describe("create", () => {
it("should create a type successfully", async () => {
await service.create(
[{
value: "UK"
}]
)
const [productType] = await service.list({
value: "UK"
})
expect(productType.value).toEqual("UK")
})
})
})

View File

@@ -50,10 +50,10 @@ class ProductCategory {
rank?: number
@Property({ columnType: "text", nullable: true })
parent_category_id?: string
parent_category_id?: string | null
@ManyToOne(() => ProductCategory, { nullable: true })
parent_category: ProductCategory
parent_category?: ProductCategory
@OneToMany({
entity: () => ProductCategory,
@@ -62,14 +62,14 @@ class ProductCategory {
category_children = new Collection<ProductCategory>(this)
@Property({ onCreate: () => new Date(), columnType: "timestamptz" })
created_at: Date
created_at?: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
})
updated_at: Date
updated_at?: Date
@ManyToMany(() => Product, (product) => product.categories)
products = new Collection<Product>(this)

View File

@@ -7,9 +7,9 @@ import {
} from "@medusajs/utils"
import { serialize } from "@mikro-orm/core"
// TODO: Should we create a mikro orm specific package for this and the soft deletable decorator util?
// TODO: move to utils package
async function transactionWrapper(
async function transactionWrapper<TManager = unknown>(
this: any,
task: (transactionManager: unknown) => Promise<any>,
{
@@ -18,7 +18,7 @@ async function transactionWrapper(
enableNestedTransactions = false,
}: {
isolationLevel?: string
transaction?: unknown
transaction?: TManager
enableNestedTransactions?: boolean
} = {}
): Promise<any> {
@@ -40,26 +40,34 @@ async function transactionWrapper(
return await (this.manager_ as SqlEntityManager).transactional(task, options)
}
const updateDeletedAtRecursively = async <T extends object = any>(
manager: SqlEntityManager,
// TODO: move to utils package
const mikroOrmUpdateDeletedAtRecursively = async <T extends object = any>(
manager: any,
entities: T[],
value: Date | null
) => {
for await (const entity of entities) {
for (const entity of entities) {
if (!("deleted_at" in entity)) continue
;(entity as any).deleted_at = value
const relations = manager
.getDriver()
.getMetadata()
.get(entities[0].constructor.name).relations
.get(entity.constructor.name).relations
const relationsToCascade = relations.filter((relation) =>
relation.cascade.includes("soft-remove" as any)
)
for (const relation of relationsToCascade) {
const relationEntities = (await entity[relation.name].init()).getItems({
let collectionRelation = entity[relation.name]
if (!collectionRelation.isInitialized()) {
await collectionRelation.init()
}
const relationEntities = await collectionRelation.getItems({
filters: {
[DAL.SoftDeletableFilterKey]: {
withDeleted: true,
@@ -67,10 +75,10 @@ const updateDeletedAtRecursively = async <T extends object = any>(
},
})
await updateDeletedAtRecursively(manager, relationEntities, value)
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
}
await manager.persist(entities)
await manager.persist(entity)
}
}
@@ -83,17 +91,29 @@ const serializer = <TOutput extends object>(
return result as unknown as Promise<TOutput>
}
export abstract class AbstractBaseRepository<T = any>
implements DAL.RepositoryService<T>
{
// TODO: move to utils package
class AbstractBase<T = any> {
protected readonly manager_: SqlEntityManager
protected constructor({ manager }) {
this.manager_ = manager
}
async transaction(
task: (transactionManager: unknown) => Promise<any>,
getFreshManager<TManager = unknown>(): TManager {
return (this.manager_.fork
? this.manager_.fork()
: this.manager_) as unknown as TManager
}
getActiveManager<TManager = unknown>(
@MedusaContext()
{ transactionManager, manager }: Context = {}
): TManager {
return (transactionManager ?? manager ?? this.manager_) as TManager
}
async transaction<TManager = unknown>(
task: (transactionManager: TManager) => Promise<any>,
{
transaction,
isolationLevel,
@@ -101,7 +121,7 @@ export abstract class AbstractBaseRepository<T = any>
}: {
isolationLevel?: string
enableNestedTransactions?: boolean
transaction?: unknown
transaction?: TManager
} = {}
): Promise<any> {
return await transactionWrapper.apply(this, arguments)
@@ -113,7 +133,12 @@ export abstract class AbstractBaseRepository<T = any>
): Promise<TOutput> {
return await serializer<TOutput>(data, options)
}
}
export abstract class AbstractBaseRepository<T = any>
extends AbstractBase
implements DAL.RepositoryService<T>
{
abstract find(options?: DAL.FindOptions<T>, context?: Context)
abstract findAndCount(
@@ -132,9 +157,9 @@ export abstract class AbstractBaseRepository<T = any>
{ transactionManager: manager }: Context = {}
): Promise<T[]> {
const entities = await this.find({ where: { id: { $in: ids } } as any })
const date = new Date()
await updateDeletedAtRecursively(
await mikroOrmUpdateDeletedAtRecursively(
manager as SqlEntityManager,
entities,
date
@@ -158,7 +183,7 @@ export abstract class AbstractBaseRepository<T = any>
const entities = await this.find(query)
await updateDeletedAtRecursively(
await mikroOrmUpdateDeletedAtRecursively(
manager as SqlEntityManager,
entities,
null
@@ -168,8 +193,9 @@ export abstract class AbstractBaseRepository<T = any>
}
}
// TODO: move to utils package
export abstract class AbstractTreeRepositoryBase<T = any>
extends AbstractBaseRepository<T>
extends AbstractBase<T>
implements DAL.TreeRepositoryService<T>
{
protected constructor({ manager }) {
@@ -188,12 +214,18 @@ export abstract class AbstractTreeRepositoryBase<T = any>
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<[T[], number]>
abstract create(data: unknown, context?: Context): Promise<T>
abstract delete(id: string, context?: Context): Promise<void>
}
// TODO: move to utils package
/**
* Only used internally in order to be able to wrap in transaction from a
* non identified repository
*/
export class BaseRepository extends AbstractBaseRepository {
constructor({ manager }) {
// @ts-ignore
@@ -219,3 +251,35 @@ export class BaseRepository extends AbstractBaseRepository {
throw new Error("Method not implemented.")
}
}
export class BaseTreeRepository extends AbstractTreeRepositoryBase {
constructor({ manager }) {
// @ts-ignore
super(...arguments)
}
find(
options?: DAL.FindOptions,
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<any[]> {
throw new Error("Method not implemented.")
}
findAndCount(
options?: DAL.FindOptions,
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<[any[], number]> {
throw new Error("Method not implemented.")
}
create(data: unknown, context?: Context): Promise<any> {
throw new Error("Method not implemented.")
}
delete(id: string, context?: Context): Promise<void> {
throw new Error("Method not implemented.")
}
}

View File

@@ -6,11 +6,26 @@ import {
import { Product, ProductCategory } from "@models"
import { Context, DAL, ProductCategoryTransformOptions } from "@medusajs/types"
import groupBy from "lodash/groupBy"
import { AbstractTreeRepositoryBase } from "./base"
import { BaseTreeRepository } from "./base"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import { InjectTransactionManager, MedusaContext, isDefined, MedusaError } from "@medusajs/utils"
export class ProductCategoryRepository extends AbstractTreeRepositoryBase<ProductCategory> {
import { ProductCategoryServiceTypes } from "../types"
export type ReorderConditions = {
targetCategoryId: string
originalParentId: string | null
targetParentId: string | null | undefined
originalRank: number
targetRank: number | undefined
shouldChangeParent: boolean
shouldChangeRank: boolean
shouldIncrementRank: boolean
shouldDeleteElement: boolean
}
export const tempReorderRank = 99999
export class ProductCategoryRepository extends BaseTreeRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -24,8 +39,7 @@ export class ProductCategoryRepository extends AbstractTreeRepositoryBase<Produc
transformOptions: ProductCategoryTransformOptions = {},
context: Context = {}
): Promise<ProductCategory[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
const { includeDescendantsTree } = transformOptions
@@ -65,8 +79,7 @@ export class ProductCategoryRepository extends AbstractTreeRepositoryBase<Produc
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
context: Context = {}
): Promise<ProductCategory[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
for (let productCategory of productCategories) {
const whereOptions = {
@@ -112,8 +125,7 @@ export class ProductCategoryRepository extends AbstractTreeRepositoryBase<Produc
transformOptions: ProductCategoryTransformOptions = {},
context: Context = {}
): Promise<[ProductCategory[], number]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
const { includeDescendantsTree } = transformOptions
@@ -153,21 +165,267 @@ export class ProductCategoryRepository extends AbstractTreeRepositoryBase<Produc
@InjectTransactionManager()
async delete(
ids: string[],
id: string,
@MedusaContext()
{ transactionManager: manager }: Context = {}
context: Context = {}
): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const productCategory = await manager.findOneOrFail(
ProductCategory,
{ id },
{
populate: ["category_children"],
}
)
if (productCategory.category_children.length > 0) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Deleting ProductCategory (${id}) with category children is not allowed`
)
}
const conditions = this.fetchReorderConditions(
productCategory,
{
parent_category_id: productCategory.parent_category_id,
rank: productCategory.rank,
},
true
)
await this.performReordering(manager, conditions)
await (manager as SqlEntityManager).nativeDelete(
Product,
{ id: { $in: ids } },
ProductCategory,
{ id: id },
{}
)
}
@InjectTransactionManager()
async create(
data: unknown[],
context: Context = {}
): Promise<ProductCategory[]> {
throw new Error("Method not implemented.")
data: ProductCategoryServiceTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory> {
const categoryData = { ...data }
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
const siblings = await manager.find(
ProductCategory,
{
parent_category_id: categoryData?.parent_category_id || null
},
)
if (!isDefined(categoryData.rank)) {
categoryData.rank = siblings.length
}
const productCategory = manager.create(ProductCategory, categoryData)
await manager.persist(productCategory)
return productCategory
}
@InjectTransactionManager()
async update(
id: string,
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
@MedusaContext() context: Context = {}
): Promise<ProductCategory> {
const categoryData = { ...data }
const manager = this.getActiveManager<SqlEntityManager>(context)
const productCategory = await manager.findOneOrFail(ProductCategory, { id })
const conditions = this.fetchReorderConditions(
productCategory,
categoryData
)
if (conditions.shouldChangeRank || conditions.shouldChangeParent) {
categoryData.rank = tempReorderRank
}
// await this.transformParentIdToEntity(categoryData)
for (const key in categoryData) {
if (isDefined(categoryData[key])) {
productCategory[key] = categoryData[key]
}
}
manager.assign(productCategory, categoryData)
manager.persist(productCategory)
await this.performReordering(manager, conditions)
return productCategory
}
protected fetchReorderConditions(
productCategory: ProductCategory,
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
shouldDeleteElement: boolean = false
): ReorderConditions {
const originalParentId = productCategory.parent_category_id || null
const targetParentId = data.parent_category_id
const originalRank = productCategory.rank || 0
const targetRank = data.rank
const shouldChangeParent =
targetParentId !== undefined && targetParentId !== originalParentId
const shouldChangeRank =
shouldChangeParent ||
(isDefined(targetRank) && originalRank !== targetRank)
return {
targetCategoryId: productCategory.id,
originalParentId,
targetParentId,
originalRank,
targetRank,
shouldChangeParent,
shouldChangeRank,
shouldIncrementRank: false,
shouldDeleteElement,
}
}
protected async performReordering(
manager: SqlEntityManager,
conditions: ReorderConditions
): Promise<void> {
const { shouldChangeParent, shouldChangeRank, shouldDeleteElement } =
conditions
if (!(shouldChangeParent || shouldChangeRank || shouldDeleteElement)) {
return
}
// If we change parent, we need to shift the siblings to eliminate the
// rank occupied by the targetCategory in the original parent.
shouldChangeParent &&
(await this.shiftSiblings(manager, {
...conditions,
targetRank: conditions.originalRank,
targetParentId: conditions.originalParentId,
}))
// If we change parent, we need to shift the siblings of the new parent
// to create a rank that the targetCategory will occupy.
shouldChangeParent &&
shouldChangeRank &&
(await this.shiftSiblings(manager, {
...conditions,
shouldIncrementRank: true,
}))
// If we only change rank, we need to shift the siblings
// to create a rank that the targetCategory will occupy.
;((!shouldChangeParent && shouldChangeRank) || shouldDeleteElement) &&
(await this.shiftSiblings(manager, {
...conditions,
targetParentId: conditions.originalParentId,
}))
}
protected async shiftSiblings(
manager: SqlEntityManager,
conditions: ReorderConditions
): Promise<void> {
let { shouldIncrementRank, targetRank } = conditions
const {
shouldChangeParent,
originalRank,
targetParentId,
targetCategoryId,
shouldDeleteElement,
} = conditions
// The current sibling count will replace targetRank if
// targetRank is greater than the count of siblings.
const siblingCount = await manager.count(
ProductCategory,
{
parent_category_id: targetParentId || null,
id: { $ne: targetCategoryId },
}
)
// The category record that will be placed at the requested rank
// We've temporarily placed it at a temporary rank that is
// beyond a reasonable value (tempReorderRank)
const targetCategory = await manager.findOne(
ProductCategory,
{
id: targetCategoryId,
parent_category_id: targetParentId || null,
rank: tempReorderRank,
}
)
// If the targetRank is not present, or if targetRank is beyond the
// rank of the last category, we set the rank as the last rank
if (targetRank === undefined || targetRank > siblingCount) {
targetRank = siblingCount
}
let rankCondition
// If parent doesn't change, we only need to get the ranks
// in between the original rank and the target rank.
if (shouldChangeParent || shouldDeleteElement) {
rankCondition = { $gte: targetRank }
} else if (originalRank > targetRank) {
shouldIncrementRank = true
rankCondition = { $gte: targetRank, $lt: originalRank }
} else {
shouldIncrementRank = false
rankCondition = { $gte: originalRank, $lt: targetRank }
}
// Scope out the list of siblings that we need to shift up or down
const siblingsToShift = await manager.find(
ProductCategory,
{
parent_category_id: targetParentId || null,
rank: rankCondition,
id: { $ne: targetCategoryId },
},
{
orderBy: { rank: shouldIncrementRank ? "DESC" : "ASC" },
}
)
// Depending on the conditions, we get a subset of the siblings
// and independently shift them up or down a rank
for (let index = 0; index < siblingsToShift.length; index++) {
const sibling = siblingsToShift[index]
// Depending on the condition, we could also have the targetCategory
// in the siblings list, we skip shifting the target until all other siblings
// have been shifted.
if (sibling.id === targetCategoryId) {
continue
}
if (!isDefined(sibling.rank)) {
throw "error"
}
const rank = shouldIncrementRank ? ++sibling.rank : --sibling.rank
manager.assign(sibling, { rank })
manager.persist(sibling)
}
// The targetCategory will not be present in the query when we are shifting
// siblings of the old parent of the targetCategory.
if (!targetCategory) {
return
}
// Place the targetCategory in the requested rank
manager.assign(targetCategory, { rank: targetRank })
manager.persist(targetCategory)
}
}

View File

@@ -1,15 +1,20 @@
import { Product, ProductCollection } from "@models"
import { ProductCollection } from "@models"
import {
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
LoadStrategy,
} from "@mikro-orm/core"
import { Context, DAL } from "@medusajs/types"
import { AbstractBaseRepository } from "./base"
import { Context, DAL, ProductTypes } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import {
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
export class ProductCollectionRepository extends AbstractBaseRepository<ProductCollection> {
import { BaseRepository } from "./base"
export class ProductCollectionRepository extends BaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -62,23 +67,75 @@ export class ProductCollectionRepository extends AbstractBaseRepository<ProductC
@InjectTransactionManager()
async delete(
ids: string[],
collectionIds: string[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
): Promise<void> {
await (manager as SqlEntityManager).nativeDelete(
Product,
{ id: { $in: ids } },
ProductCollection,
{ id: { $in: collectionIds } },
{}
)
}
@InjectTransactionManager()
async create(
data: unknown[],
data: ProductTypes.CreateProductCollectionDTO[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
context: Context = {}
): Promise<ProductCollection[]> {
throw new Error("Method not implemented.")
const manager = this.getActiveManager<SqlEntityManager>(context)
const productCollections = data.map((collectionData) => {
return manager.create(ProductCollection, collectionData)
})
await manager.persist(productCollections)
return productCollections
}
@InjectTransactionManager()
async update(
data: ProductTypes.UpdateProductCollectionDTO[],
@MedusaContext()
context: Context = {}
): Promise<ProductCollection[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const collectionIds = data.map((collectionData) => collectionData.id)
const existingCollections = await this.find(
{
where: {
id: {
$in: collectionIds,
},
},
},
context
)
const existingCollectionsMap = new Map(
existingCollections.map<[string, ProductCollection]>((collection) => [
collection.id,
collection,
])
)
const productCollections = data.map((collectionData) => {
const existingCollection = existingCollectionsMap.get(collectionData.id)
if (!existingCollection) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ProductCollection with id "${collectionData.id}" not found`
)
}
return manager.assign(existingCollection, collectionData)
})
await manager.persist(productCollections)
return productCollections
}
}

View File

@@ -22,8 +22,7 @@ export class ProductImageRepository extends AbstractBaseRepository<Image> {
findOptions: DAL.FindOptions<Image> = { where: {} },
context: Context = {}
): Promise<Image[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -43,8 +42,7 @@ export class ProductImageRepository extends AbstractBaseRepository<Image> {
findOptions: DAL.FindOptions<Image> = { where: {} },
context: Context = {}
): Promise<[Image[], number]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}

View File

@@ -7,7 +7,11 @@ import { Product, ProductOption } from "@models"
import { Context, DAL, ProductTypes } from "@medusajs/types"
import { AbstractBaseRepository } from "./base"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import {
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
export class ProductOptionRepository extends AbstractBaseRepository<ProductOption> {
protected readonly manager_: SqlEntityManager
@@ -22,8 +26,7 @@ export class ProductOptionRepository extends AbstractBaseRepository<ProductOptio
findOptions: DAL.FindOptions<ProductOption> = { where: {} },
context: Context = {}
): Promise<ProductOption[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -43,8 +46,7 @@ export class ProductOptionRepository extends AbstractBaseRepository<ProductOptio
findOptions: DAL.FindOptions<ProductOption> = { where: {} },
context: Context = {}
): Promise<[ProductOption[], number]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -64,10 +66,12 @@ export class ProductOptionRepository extends AbstractBaseRepository<ProductOptio
async delete(
ids: string[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
context: Context = {}
): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
await (manager as SqlEntityManager).nativeDelete(
Product,
ProductOption,
{ id: { $in: ids } },
{}
)
@@ -75,16 +79,81 @@ export class ProductOptionRepository extends AbstractBaseRepository<ProductOptio
@InjectTransactionManager()
async create(
data: (ProductTypes.CreateProductOptionDTO & { product: { id: string } })[],
data: ProductTypes.CreateProductOptionDTO[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
context: Context = {}
): Promise<ProductOption[]> {
const options = data.map((option) => {
return (manager as SqlEntityManager).create(ProductOption, option)
const manager = this.getActiveManager<SqlEntityManager>(context)
const productIds: string[] = []
data.forEach((d) => d.product_id && productIds.push(d.product_id))
const existingProducts = await manager.find(
Product,
{ id: { $in: productIds } },
)
const existingProductsMap = new Map(
existingProducts.map<[string, Product]>((product) => [product.id, product])
)
const productOptions = data.map((optionData) => {
const productId = optionData.product_id
delete optionData.product_id
if (productId) {
const product = existingProductsMap.get(productId)
optionData.product = product
}
return manager.create(ProductOption, optionData)
})
await (manager as SqlEntityManager).persist(options)
await manager.persist(productOptions)
return options
return productOptions
}
@InjectTransactionManager()
async update(
data: ProductTypes.UpdateProductOptionDTO[],
@MedusaContext()
context: Context = {}
): Promise<ProductOption[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const optionIds = data.map((optionData) => optionData.id)
const existingOptions = await this.find(
{
where: {
id: {
$in: optionIds,
},
},
},
context
)
const existingOptionsMap = new Map(
existingOptions.map<[string, ProductOption]>((option) => [option.id, option])
)
const productOptions = data.map((optionData) => {
const existingOption = existingOptionsMap.get(optionData.id)
if (!existingOption) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ProductOption with id "${optionData.id}" not found`
)
}
return manager.assign(existingOption, optionData)
})
await manager.persist(productOptions)
return productOptions
}
}

View File

@@ -4,13 +4,23 @@ import {
LoadStrategy,
RequiredEntityData,
} from "@mikro-orm/core"
import { Product, ProductTag } from "@models"
import { Context, CreateProductTagDTO, DAL } from "@medusajs/types"
import { AbstractBaseRepository } from "./base"
import { ProductTag } from "@models"
import {
Context,
CreateProductTagDTO,
DAL,
UpdateProductTagDTO,
UpsertProductTagDTO,
} from "@medusajs/types"
import { BaseRepository } from "./base"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import {
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
export class ProductTagRepository extends AbstractBaseRepository<ProductTag> {
export class ProductTagRepository extends BaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -23,10 +33,9 @@ export class ProductTagRepository extends AbstractBaseRepository<ProductTag> {
findOptions: DAL.FindOptions<ProductTag> = { where: {} },
context: Context = {}
): Promise<ProductTag[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
Object.assign(findOptions_.options, {
@@ -44,8 +53,7 @@ export class ProductTagRepository extends AbstractBaseRepository<ProductTag> {
findOptions: DAL.FindOptions<ProductTag> = { where: {} },
context: Context = {}
): Promise<[ProductTag[], number]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -61,14 +69,71 @@ export class ProductTagRepository extends AbstractBaseRepository<ProductTag> {
)
}
@InjectTransactionManager()
async create(
data: CreateProductTagDTO[],
@MedusaContext()
context: Context = {}
): Promise<ProductTag[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const productTags = data.map((tagData) => {
return manager.create(ProductTag, tagData)
})
await manager.persist(productTags)
return productTags
}
@InjectTransactionManager()
async update(
data: UpdateProductTagDTO[],
@MedusaContext()
context: Context = {}
): Promise<ProductTag[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const tagIds = data.map((tagData) => tagData.id)
const existingTags = await this.find(
{
where: {
id: {
$in: tagIds,
},
},
},
context
)
const existingTagsMap = new Map(
existingTags.map<[string, ProductTag]>((tag) => [tag.id, tag])
)
const productTags = data.map((tagData) => {
const existingTag = existingTagsMap.get(tagData.id)
if (!existingTag) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ProductTag with id "${tagData.id}" not found`
)
}
return manager.assign(existingTag, tagData)
})
await manager.persist(productTags)
return productTags
}
@InjectTransactionManager()
async upsert(
tags: CreateProductTagDTO[],
tags: UpsertProductTagDTO[],
@MedusaContext()
context: Context = {}
): Promise<ProductTag[]> {
const { transactionManager: manager } = context
const tagsValues = tags.map((tag) => tag.value)
const existingTags = await this.find(
{
@@ -115,21 +180,10 @@ export class ProductTagRepository extends AbstractBaseRepository<ProductTag> {
async delete(
ids: string[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
context: Context = {}
): Promise<void> {
await (manager as SqlEntityManager).nativeDelete(
Product,
{ id: { $in: ids } },
{}
)
}
const manager = this.getActiveManager<SqlEntityManager>(context)
@InjectTransactionManager()
async create(
data: unknown[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
): Promise<ProductTag[]> {
throw new Error("Method not implemented.")
await manager.nativeDelete(ProductTag, { id: { $in: ids } }, {})
}
}

View File

@@ -4,13 +4,23 @@ import {
LoadStrategy,
RequiredEntityData,
} from "@mikro-orm/core"
import { Product, ProductType } from "@models"
import { Context, CreateProductTypeDTO, DAL } from "@medusajs/types"
import { AbstractBaseRepository } from "./base"
import { ProductType } from "@models"
import {
Context,
CreateProductTypeDTO,
DAL,
UpdateProductTypeDTO,
} from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import {
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
export class ProductTypeRepository extends AbstractBaseRepository<ProductType> {
import { BaseRepository } from "./base"
export class ProductTypeRepository extends BaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -23,8 +33,7 @@ export class ProductTypeRepository extends AbstractBaseRepository<ProductType> {
findOptions: DAL.FindOptions<ProductType> = { where: {} },
context: Context = {}
): Promise<ProductType[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -44,8 +53,7 @@ export class ProductTypeRepository extends AbstractBaseRepository<ProductType> {
findOptions: DAL.FindOptions<ProductType> = { where: {} },
context: Context = {}
): Promise<[ProductType[], number]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -118,7 +126,7 @@ export class ProductTypeRepository extends AbstractBaseRepository<ProductType> {
{ transactionManager: manager }: Context = {}
): Promise<void> {
await (manager as SqlEntityManager).nativeDelete(
Product,
ProductType,
{ id: { $in: ids } },
{}
)
@@ -126,10 +134,59 @@ export class ProductTypeRepository extends AbstractBaseRepository<ProductType> {
@InjectTransactionManager()
async create(
data: unknown[],
data: CreateProductTypeDTO[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
context: Context = {}
): Promise<ProductType[]> {
throw new Error("Method not implemented.")
const manager = this.getActiveManager<SqlEntityManager>(context)
const productTypes = data.map((typeData) => {
return manager.create(ProductType, typeData)
})
await manager.persist(productTypes)
return productTypes
}
@InjectTransactionManager()
async update(
data: UpdateProductTypeDTO[],
@MedusaContext()
context: Context = {}
): Promise<ProductType[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const typeIds = data.map((typeData) => typeData.id)
const existingTypes = await this.find(
{
where: {
id: {
$in: typeIds,
},
},
},
context
)
const existingTypesMap = new Map(
existingTypes.map<[string, ProductType]>((type) => [type.id, type])
)
const productTypes = data.map((typeData) => {
const existingType = existingTypesMap.get(typeData.id)
if (!existingType) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ProductType with id "${typeData.id}" not found`
)
}
return manager.assign(existingType, typeData)
})
await manager.persist(productTypes)
return productTypes
}
}

View File

@@ -31,8 +31,7 @@ export class ProductVariantRepository extends AbstractBaseRepository<ProductVari
findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
context: Context = {}
): Promise<ProductVariant[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -52,8 +51,7 @@ export class ProductVariantRepository extends AbstractBaseRepository<ProductVari
findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
context: Context = {}
): Promise<[ProductVariant[], number]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}

View File

@@ -38,9 +38,7 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
findOptions: DAL.FindOptions<Product> = { where: {} },
context: Context = {}
): Promise<Product[]> {
// TODO: use the getter method (getActiveManager)
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
@@ -62,20 +60,18 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
findOptions: DAL.FindOptions<Product> = { where: {} },
context: Context = {}
): Promise<[Product[], number]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
if (context.transactionManager) {
Object.assign(findOptions_.options, { ctx: context.transactionManager })
}
Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN,
})
await this.mutateNotInCategoriesConstraints(findOptions_)
return await this.manager_.findAndCount(
return await manager.findAndCount(
Product,
findOptions_.where as MikroFilterQuery<Product>,
findOptions_.options as MikroOptions<Product>
@@ -90,9 +86,7 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
findOptions: DAL.FindOptions<Product> = { where: {} },
context: Context = {}
): Promise<void> {
// TODO: use the getter method (getActiveManager)
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const manager = this.getActiveManager<SqlEntityManager>(context)
if (findOptions.where.categories?.id?.["$nin"]) {
const productsInCategories = await manager.find(

View File

@@ -15,6 +15,7 @@ mockContainer.register({
return [{}]
}),
findAndCount: jest.fn().mockResolvedValue([[], 0]),
getFreshManager: jest.fn().mockResolvedValue({}),
}),
productService: asClass(ProductService),
})

View File

@@ -8,9 +8,10 @@ describe("Product service", function () {
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: {
@@ -24,7 +25,7 @@ describe("Product service", function () {
withDeleted: undefined,
},
},
undefined
expect.any(Object)
)
})
@@ -49,7 +50,7 @@ describe("Product service", function () {
withDeleted: undefined,
},
},
undefined
expect.any(Object)
)
expect(err.message).toBe(
@@ -79,7 +80,7 @@ describe("Product service", function () {
withDeleted: undefined,
},
},
undefined
expect.any(Object)
)
})
@@ -117,7 +118,7 @@ describe("Product service", function () {
withDeleted: undefined,
},
},
undefined
expect.any(Object)
)
})
@@ -155,7 +156,7 @@ describe("Product service", function () {
populate: ["tags"],
},
},
undefined
expect.any(Object)
)
})
@@ -193,7 +194,7 @@ describe("Product service", function () {
populate: ["tags"],
},
},
undefined
expect.any(Object)
)
})
})

View File

@@ -1,6 +1,17 @@
import { ProductCategory } from "@models"
import { ProductCategoryRepository } from "@repositories"
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { ModulesSdkUtils, MedusaError, isDefined } from "@medusajs/utils"
import {
ModulesSdkUtils,
MedusaError,
isDefined,
InjectTransactionManager,
InjectManager,
MedusaContext
} from "@medusajs/utils"
import { shouldForceTransaction } from "../utils"
import { ProductCategoryServiceTypes } from "../types"
type InjectedDependencies = {
productCategoryRepository: DAL.TreeRepositoryService
@@ -15,10 +26,11 @@ export default class ProductCategoryService<
this.productCategoryRepository_ = productCategoryRepository
}
@InjectManager("productCategoryRepository_")
async retrieve(
productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
if (!isDefined(productCategoryId)) {
throw new MedusaError(
@@ -51,10 +63,11 @@ export default class ProductCategoryService<
return productCategories[0] as TEntity
}
@InjectManager("productCategoryRepository_")
async list(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false,
@@ -74,10 +87,11 @@ export default class ProductCategoryService<
)) as TEntity[]
}
@InjectManager("productCategoryRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false,
@@ -96,4 +110,36 @@ export default class ProductCategoryService<
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
async create(
data: ProductCategoryServiceTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await (this.productCategoryRepository_ as ProductCategoryRepository).create(
data,
sharedContext
)) as TEntity
}
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
async update(
id: string,
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await (this.productCategoryRepository_ as ProductCategoryRepository).update(
id,
data,
sharedContext
)) as TEntity
}
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
async delete(
id: string,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productCategoryRepository_.delete(id, sharedContext)
}
}

View File

@@ -1,5 +1,14 @@
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { ModulesSdkUtils, retrieveEntity } from "@medusajs/utils"
import {
ModulesSdkUtils,
retrieveEntity,
InjectTransactionManager,
MedusaContext,
InjectManager,
} from "@medusajs/utils"
import { shouldForceTransaction } from "../utils"
import { ProductCollectionRepository } from "../repositories"
import { ProductCollection } from "@models"
@@ -10,16 +19,17 @@ type InjectedDependencies = {
export default class ProductCollectionService<
TEntity extends ProductCollection = ProductCollection
> {
protected readonly productCollectionRepository_: DAL.TreeRepositoryService
protected readonly productCollectionRepository_: DAL.RepositoryService
constructor({ productCollectionRepository }: InjectedDependencies) {
this.productCollectionRepository_ = productCollectionRepository
}
@InjectManager("productCollectionRepository_")
async retrieve(
productCollectionId: string,
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
ProductCollection,
@@ -33,10 +43,11 @@ export default class ProductCollectionService<
})) as TEntity
}
@InjectManager("productCollectionRepository_")
async list(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await this.productCollectionRepository_.find(
this.buildListQueryOptions(filters, config),
@@ -44,10 +55,11 @@ export default class ProductCollectionService<
)) as TEntity[]
}
@InjectManager("productCollectionRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
return (await this.productCollectionRepository_.findAndCount(
this.buildListQueryOptions(filters, config),
@@ -72,4 +84,34 @@ export default class ProductCollectionService<
return queryOptions
}
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
async create(
data: ProductTypes.CreateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productCollectionRepository_ as ProductCollectionRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
async update(
data: ProductTypes.UpdateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productCollectionRepository_ as ProductCollectionRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productCollectionRepository_.delete(ids, sharedContext)
}
}

View File

@@ -31,6 +31,7 @@ import { serialize } from "@mikro-orm/core"
import ProductImageService from "./product-image"
import { ProductServiceTypes, ProductVariantServiceTypes } from "../types/services"
import {
InjectManager,
InjectTransactionManager,
isDefined,
isString,
@@ -38,8 +39,10 @@ import {
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import { shouldForceTransaction } from "../utils"
import { joinerConfig } from "./../joiner-config"
import { ProductCategoryServiceTypes } from "../types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -106,10 +109,11 @@ export default class ProductModuleService<
return joinerConfig
}
@InjectManager("baseRepository_")
async list(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductDTO[]> {
const products = await this.productService_.list(
filters,
@@ -120,10 +124,11 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(products))
}
@InjectManager("baseRepository_")
async retrieve(
productId: string,
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductDTO> {
const product = await this.productService_.retrieve(
productId,
@@ -134,10 +139,11 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(product))
}
@InjectManager("baseRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductTypes.ProductDTO[], number]> {
const [products, count] = await this.productService_.listAndCount(
filters,
@@ -148,10 +154,11 @@ export default class ProductModuleService<
return [JSON.parse(JSON.stringify(products)), count]
}
@InjectManager("baseRepository_")
async retrieveVariant(
productVariantId: string,
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductVariantDTO> {
const productVariant = await this.productVariantService_.retrieve(
productVariantId,
@@ -162,10 +169,11 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(productVariant))
}
@InjectManager("baseRepository_")
async listVariants(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductVariantDTO[]> {
const variants = await this.productVariantService_.list(
filters,
@@ -176,10 +184,11 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(variants))
}
@InjectManager("baseRepository_")
async listAndCountVariants(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductTypes.ProductVariantDTO[], number]> {
const [variants, count] = await this.productVariantService_.listAndCount(
filters,
@@ -190,10 +199,26 @@ export default class ProductModuleService<
return [JSON.parse(JSON.stringify(variants)), count]
}
@InjectManager("baseRepository_")
async retrieveTag(
tagId: string,
config: FindConfig<ProductTypes.ProductTagDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTagDTO> {
const productTag = await this.productTagService_.retrieve(
tagId,
config,
sharedContext
)
return JSON.parse(JSON.stringify(productTag))
}
@InjectManager("baseRepository_")
async listTags(
filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTagDTO[]> {
const tags = await this.productTagService_.list(
filters,
@@ -204,10 +229,218 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(tags))
}
@InjectManager("baseRepository_")
async listAndCountTags(
filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductTypes.ProductTagDTO[], number]> {
const [tags, count] = await this.productTagService_.listAndCount(
filters,
config,
sharedContext
)
return [JSON.parse(JSON.stringify(tags)), count]
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async createTags(
data: ProductTypes.CreateProductTagDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productTags = await this.productTagService_.create(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productTags))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async updateTags(
data: ProductTypes.UpdateProductTagDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productTags = await this.productTagService_.update(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productTags))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async deleteTags(
productTagIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productTagService_.delete(productTagIds, sharedContext)
}
@InjectManager("baseRepository_")
async retrieveType(
typeId: string,
config: FindConfig<ProductTypes.ProductTypeDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTypeDTO> {
const productType = await this.productTypeService_.retrieve(
typeId,
config,
sharedContext
)
return JSON.parse(JSON.stringify(productType))
}
@InjectManager("baseRepository_")
async listTypes(
filters: ProductTypes.FilterableProductTypeProps = {},
config: FindConfig<ProductTypes.ProductTypeDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTypeDTO[]> {
const types = await this.productTypeService_.list(
filters,
config,
sharedContext
)
return JSON.parse(JSON.stringify(types))
}
@InjectManager("baseRepository_")
async listAndCountTypes(
filters: ProductTypes.FilterableProductTypeProps = {},
config: FindConfig<ProductTypes.ProductTypeDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductTypes.ProductTypeDTO[], number]> {
const [types, count] = await this.productTypeService_.listAndCount(
filters,
config,
sharedContext
)
return [JSON.parse(JSON.stringify(types)), count]
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async createTypes(
data: ProductTypes.CreateProductTypeDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productTypes = await this.productTypeService_.create(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productTypes))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async updateTypes(
data: ProductTypes.UpdateProductTypeDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productTypes = await this.productTypeService_.update(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productTypes))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async deleteTypes(
productTypeIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productTypeService_.delete(productTypeIds, sharedContext)
}
@InjectManager("baseRepository_")
async retrieveOption(
optionId: string,
config: FindConfig<ProductTypes.ProductOptionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductOptionDTO> {
const productOptions = await this.productOptionService_.retrieve(
optionId,
config,
sharedContext
)
return JSON.parse(JSON.stringify(productOptions))
}
@InjectManager("baseRepository_")
async listOptions(
filters: ProductTypes.FilterableProductTypeProps = {},
config: FindConfig<ProductTypes.ProductOptionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductOptionDTO[]> {
const productOptions = await this.productOptionService_.list(
filters,
config,
sharedContext
)
return JSON.parse(JSON.stringify(productOptions))
}
@InjectManager("baseRepository_")
async listAndCountOptions(
filters: ProductTypes.FilterableProductTypeProps = {},
config: FindConfig<ProductTypes.ProductOptionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductTypes.ProductOptionDTO[], number]> {
const [productOptions, count] = await this.productOptionService_.listAndCount(
filters,
config,
sharedContext
)
return [JSON.parse(JSON.stringify(productOptions)), count]
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async createOptions(
data: ProductTypes.CreateProductOptionDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productOptions = await this.productOptionService_.create(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productOptions))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async updateOptions(
data: ProductTypes.UpdateProductOptionDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productOptions = await this.productOptionService_.update(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productOptions))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async deleteOptions(
productOptionIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productOptionService_.delete(productOptionIds, sharedContext)
}
@InjectManager("baseRepository_")
async retrieveCollection(
productCollectionId: string,
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductCollectionDTO> {
const productCollection = await this.productCollectionService_.retrieve(
productCollectionId,
@@ -218,10 +451,11 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(productCollection))
}
@InjectManager("baseRepository_")
async listCollections(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductCollectionDTO[]> {
const collections = await this.productCollectionService_.list(
filters,
@@ -232,10 +466,11 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(collections))
}
@InjectManager("baseRepository_")
async listAndCountCollections(
filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductTypes.ProductCollectionDTO[], number]> {
const collections = await this.productCollectionService_.listAndCount(
filters,
@@ -246,10 +481,48 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(collections))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async createCollections(
data: ProductTypes.CreateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productCollections = await this.productCollectionService_.create(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productCollections))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async updateCollections(
data: ProductTypes.UpdateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const productCollections = await this.productCollectionService_.update(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productCollections))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async deleteCollections(
productCollectionIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productCollectionService_.delete(
productCollectionIds,
sharedContext
)
}
@InjectManager("baseRepository_")
async retrieveCategory(
productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductCategoryDTO> {
const productCategory = await this.productCategoryService_.retrieve(
productCategoryId,
@@ -260,10 +533,11 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(productCategory))
}
@InjectManager("baseRepository_")
async listCategories(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductCategoryDTO[]> {
const categories = await this.productCategoryService_.list(
filters,
@@ -274,10 +548,47 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(categories))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async createCategory(
data: ProductCategoryServiceTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
) {
const productCategory = await this.productCategoryService_.create(
data,
sharedContext
)
return JSON.parse(JSON.stringify(productCategory))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async updateCategory(
categoryId: string,
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
) {
const productCategory = await this.productCategoryService_.update(
categoryId,
data,
sharedContext
)
return JSON.parse(JSON.stringify(productCategory))
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async deleteCategory(
categoryId: string,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productCategoryService_.delete(categoryId, sharedContext)
}
@InjectManager("baseRepository_")
async listAndCountCategories(
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductTypes.ProductCategoryDTO[], number]> {
const categories = await this.productCategoryService_.listAndCount(
filters,
@@ -298,7 +609,7 @@ export default class ProductModuleService<
async update(
data: ProductTypes.UpdateProductDTO[],
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductDTO[]> {
const products = await this.update_(data, sharedContext)
@@ -368,10 +679,17 @@ export default class ProductModuleService<
const productOptionsData = [...productOptionsMap]
.map(([handle, options]) => {
return options.map((option) => {
return {
...option,
product: productByHandleMap.get(handle)!,
const productOptionsData: ProductTypes.CreateProductOptionOnlyDTO = { ...option }
const product = productByHandleMap.get(handle)
const productId = product?.id!
if (productId) {
productOptionsData.product_id = productId
} else if (product) {
productOptionsData.product = product
}
return productOptionsData
})
})
.flat()
@@ -614,12 +932,12 @@ export default class ProductModuleService<
if (isDefined(productData.type)) {
const productType = (
await this.productTypeService_.upsert(
[productData.type as ProductTypes.CreateProductTypeDTO],
[productData.type],
sharedContext
)
)
productData.type = productType?.[0]
productData.type_id = productType?.[0]?.id
}
}

View File

@@ -1,8 +1,20 @@
import { ProductOption } from "@models"
import { Context, DAL, ProductTypes } from "@medusajs/types"
import {
Context,
DAL,
FindConfig,
ProductTypes,
} from "@medusajs/types"
import { ProductOptionRepository } from "@repositories"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import { doNotForceTransaction } from "../utils"
import {
InjectTransactionManager,
InjectManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { doNotForceTransaction, shouldForceTransaction } from "../utils"
type InjectedDependencies = {
productOptionRepository: DAL.RepositoryService
@@ -18,7 +30,62 @@ export default class ProductOptionService<
productOptionRepository as ProductOptionRepository
}
@InjectTransactionManager(doNotForceTransaction, "productOptionRepository_")
@InjectManager("productOptionRepository_")
async retrieve(
productOptionId: string,
config: FindConfig<ProductTypes.ProductOptionDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<TEntity> {
return (await retrieveEntity<
ProductOption,
ProductTypes.ProductOptionDTO
>({
id: productOptionId,
entityName: ProductOption.name,
repository: this.productOptionRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("productOptionRepository_")
async list(
filters: ProductTypes.FilterableProductOptionProps = {},
config: FindConfig<ProductTypes.ProductOptionDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<TEntity[]> {
return (await this.productOptionRepository_.find(
this.buildQueryForList(filters, config),
sharedContext
)) as TEntity[]
}
@InjectManager("productOptionRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductOptionProps = {},
config: FindConfig<ProductTypes.ProductOptionDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<[TEntity[], number]> {
return (await this.productOptionRepository_.findAndCount(
this.buildQueryForList(filters, config),
sharedContext
)) as [TEntity[], number]
}
private buildQueryForList(
filters: ProductTypes.FilterableProductOptionProps = {},
config: FindConfig<ProductTypes.ProductOptionDTO> = {},
) {
const queryOptions = ModulesSdkUtils.buildQuery<ProductOption>(filters, config)
if (filters.title) {
queryOptions.where["title"] = { $ilike: filters.title }
}
return queryOptions
}
@InjectTransactionManager(shouldForceTransaction, "productOptionRepository_")
async create(
data: ProductTypes.CreateProductOptionOnlyDTO[],
@MedusaContext() sharedContext: Context = {}
@@ -29,4 +96,23 @@ export default class ProductOptionService<
transactionManager: sharedContext.transactionManager,
})) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "productOptionRepository_")
async update(
data: ProductTypes.UpdateProductOptionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productOptionRepository_ as ProductOptionRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, "productOptionRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
return await this.productOptionRepository_.delete(ids, sharedContext)
}
}

View File

@@ -2,18 +2,23 @@ import { ProductTag } from "@models"
import {
Context,
CreateProductTagDTO,
UpdateProductTagDTO,
UpsertProductTagDTO,
DAL,
FindConfig,
ProductTypes,
} from "@medusajs/types"
import {
InjectTransactionManager,
InjectManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { doNotForceTransaction } from "../utils"
import { ProductTagRepository } from "@repositories"
import { doNotForceTransaction, shouldForceTransaction } from "../utils"
type InjectedDependencies = {
productTagRepository: DAL.RepositoryService
}
@@ -27,30 +32,98 @@ export default class ProductTagService<
this.productTagRepository_ = productTagRepository
}
@InjectManager("productTagRepository_")
async retrieve(
productTagId: string,
config: FindConfig<ProductTypes.ProductTagDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
ProductTag,
ProductTypes.ProductTagDTO
>({
id: productTagId,
entityName: ProductTag.name,
repository: this.productTagRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("productTagRepository_")
async list(
filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await this.productTagRepository_.find(
this.buildQueryForList(filters, config),
sharedContext
)) as TEntity[]
}
@InjectManager("productTagRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
return (await this.productTagRepository_.findAndCount(
this.buildQueryForList(filters, config),
sharedContext
)) as [TEntity[], number]
}
private buildQueryForList(
filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {},
) {
const queryOptions = ModulesSdkUtils.buildQuery<ProductTag>(filters, config)
if (filters.value) {
queryOptions.where["value"] = { $ilike: filters.value }
}
return (await this.productTagRepository_.find(
queryOptions,
return queryOptions
}
@InjectTransactionManager(shouldForceTransaction, "productTagRepository_")
async create(
data: CreateProductTagDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTagRepository_ as ProductTagRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "productTagRepository_")
async update(
data: UpdateProductTagDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTagRepository_ as ProductTagRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, "productTagRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productTagRepository_.delete(ids, sharedContext)
}
@InjectTransactionManager(doNotForceTransaction, "productTagRepository_")
async upsert(
tags: CreateProductTagDTO[],
data: UpsertProductTagDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTagRepository_ as ProductTagRepository).upsert!(
tags,
data,
sharedContext
)) as TEntity[]
}

View File

@@ -1,8 +1,23 @@
import { ProductType } from "@models"
import { Context, CreateProductTypeDTO, DAL } from "@medusajs/types"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import { doNotForceTransaction } from "../utils"
import {
Context,
CreateProductTypeDTO,
UpsertProductTypeDTO,
UpdateProductTypeDTO,
DAL,
FindConfig,
ProductTypes
} from "@medusajs/types"
import { ProductTypeRepository } from "@repositories"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { doNotForceTransaction, shouldForceTransaction } from "../utils"
type InjectedDependencies = {
productTypeRepository: DAL.RepositoryService
@@ -17,12 +32,97 @@ export default class ProductTypeService<
this.productTypeRepository_ = productTypeRepository
}
@InjectManager("productTypeRepository_")
async retrieve(
productTypeId: string,
config: FindConfig<ProductTypes.ProductTypeDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
ProductType,
ProductTypes.ProductTypeDTO
>({
id: productTypeId,
entityName: ProductType.name,
repository: this.productTypeRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("productTypeRepository_")
async list(
filters: ProductTypes.FilterableProductTypeProps = {},
config: FindConfig<ProductTypes.ProductTypeDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await this.productTypeRepository_.find(
this.buildQueryForList(filters, config),
sharedContext
)) as TEntity[]
}
@InjectManager("productTypeRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductTypeProps = {},
config: FindConfig<ProductTypes.ProductTypeDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
return (await this.productTypeRepository_.findAndCount(
this.buildQueryForList(filters, config),
sharedContext
)) as [TEntity[], number]
}
private buildQueryForList(
filters: ProductTypes.FilterableProductTypeProps = {},
config: FindConfig<ProductTypes.ProductTypeDTO> = {},
) {
const queryOptions = ModulesSdkUtils.buildQuery<ProductType>(filters, config)
if (filters.value) {
queryOptions.where["value"] = { $ilike: filters.value }
}
return queryOptions
}
@InjectTransactionManager(doNotForceTransaction, "productTypeRepository_")
async upsert(
types: CreateProductTypeDTO[],
types: UpsertProductTypeDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTypeRepository_ as ProductTypeRepository)
.upsert!(types, sharedContext)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "productTypeRepository_")
async create(
data: CreateProductTypeDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTypeRepository_ as ProductTypeRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "productTypeRepository_")
async update(
data: UpdateProductTypeDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.productTypeRepository_ as ProductTypeRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, "productTypeRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productTypeRepository_.delete(ids, sharedContext)
}
}

View File

@@ -2,6 +2,7 @@ import { Product, ProductVariant } from "@models"
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { ProductVariantRepository } from "@repositories"
import {
InjectManager,
InjectTransactionManager,
isString,
MedusaContext,
@@ -33,10 +34,11 @@ export default class ProductVariantService<
this.productService_ = productService
}
@InjectManager("productVariantRepository_")
async retrieve(
productVariantId: string,
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
ProductVariant,
@@ -50,10 +52,11 @@ export default class ProductVariantService<
})) as TEntity
}
@InjectManager("productVariantRepository_")
async list(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
filters,
@@ -66,10 +69,11 @@ export default class ProductVariantService<
)) as TEntity[]
}
@InjectManager("productVariantRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
filters,

View File

@@ -8,6 +8,7 @@ import {
WithRequiredProperty,
} from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
@@ -30,10 +31,11 @@ export default class ProductService<TEntity extends Product = Product> {
this.productRepository_ = productRepository
}
@InjectManager("productRepository_")
async retrieve(
productId: string,
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
if (!isDefined(productId)) {
throw new MedusaError(
@@ -61,10 +63,11 @@ export default class ProductService<TEntity extends Product = Product> {
return product[0] as TEntity
}
@InjectManager("productRepository_")
async list(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
if (filters.category_ids) {
if (Array.isArray(filters.category_ids)) {
@@ -86,10 +89,11 @@ export default class ProductService<TEntity extends Product = Product> {
)) as TEntity[]
}
@InjectManager("productRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
if (filters.category_ids) {
if (Array.isArray(filters.category_ids)) {

View File

@@ -1,3 +1,5 @@
export * from "./services"
import { IEventBusService } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {

View File

@@ -1,2 +1,3 @@
export * as ProductCategoryServiceTypes from "./product-category"
export * as ProductServiceTypes from "./product"
export * as ProductVariantServiceTypes from "./product-variant"

View File

@@ -0,0 +1,19 @@
export interface CreateProductCategoryDTO {
name: string
handle?: string
is_active?: boolean
is_internal?: boolean
rank?: number
parent_category_id: string | null
metadata?: Record<string, unknown>
}
export interface UpdateProductCategoryDTO {
name?: string
handle?: string
is_active?: boolean
is_internal?: boolean
rank?: number
parent_category_id?: string | null
metadata?: Record<string, unknown>
}

View File

@@ -7,21 +7,27 @@ import { Context } from "../shared-context"
* This layer helps to separate the business logic (service layer) from accessing the
* ORM directly and allows to switch to another ORM without changing the business logic.
*/
export interface RepositoryService<T = any> {
transaction(
task: (transactionManager: unknown) => Promise<any>,
interface BaseRepositoryService<T = any> {
transaction<TManager = unknown>(
task: (transactionManager: TManager) => Promise<any>,
context?: {
isolationLevel?: string
transaction?: unknown
transaction?: TManager
enableNestedTransactions?: boolean
}
): Promise<any>
getFreshManager<TManager = unknown>(): TManager
getActiveManager<TManager = unknown>(): TManager
serialize<TOutput extends object | object[]>(
data: any,
options?: any
): Promise<TOutput>
}
export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
find(options?: FindOptions<T>, context?: Context): Promise<T[]>
findAndCount(
@@ -44,7 +50,7 @@ export interface RepositoryService<T = any> {
restore(ids: string[], context?: Context): Promise<T[]>
}
export interface TreeRepositoryService<T = any> extends RepositoryService<T> {
export interface TreeRepositoryService<T = any> extends BaseRepositoryService<T> {
find(
options?: FindOptions<T>,
transformOptions?: RepositoryTransformOptions,
@@ -56,4 +62,8 @@ export interface TreeRepositoryService<T = any> extends RepositoryService<T> {
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<[T[], number]>
create(data: unknown, context?: Context): Promise<T>
delete(id: string, context?: Context): Promise<void>
}

View File

@@ -85,6 +85,26 @@ export interface ProductCategoryDTO {
updated_at: string | Date
}
export interface CreateProductCategoryDTO {
name: string
handle?: string
is_active?: boolean
is_internal?: boolean
rank?: number
parent_category_id: string | null
metadata?: Record<string, unknown>
}
export interface UpdateProductCategoryDTO {
name?: string
handle?: string
is_active?: boolean
is_internal?: boolean
rank?: number
parent_category_id?: string | null
metadata?: Record<string, unknown>
}
export interface ProductTagDTO {
id: string
value: string
@@ -153,6 +173,19 @@ export interface FilterableProductTagProps
value?: string
}
export interface FilterableProductTypeProps
extends BaseFilterable<FilterableProductTypeProps> {
id?: string | string[]
value?: string
}
export interface FilterableProductOptionProps
extends BaseFilterable<FilterableProductOptionProps> {
id?: string | string[]
title?: string
product_id?: string | string[]
}
export interface FilterableProductCollectionProps
extends BaseFilterable<FilterableProductCollectionProps> {
id?: string | string[]
@@ -170,6 +203,7 @@ export interface FilterableProductVariantProps
export interface FilterableProductCategoryProps
extends BaseFilterable<FilterableProductCategoryProps> {
id?: string | string[]
name?: string | string[]
parent_category_id?: string | string[] | null
handle?: string | string[]
is_active?: boolean
@@ -181,18 +215,63 @@ export interface FilterableProductCategoryProps
* Write DTO (module API input)
*/
export interface CreateProductCollectionDTO {
title: string
handle?: string
products?: ProductDTO[]
metadata?: Record<string, unknown>
}
export interface UpdateProductCollectionDTO {
id: string
value?: string
title?: string
handle?: string
products?: ProductDTO[]
metadata?: Record<string, unknown>
}
export interface CreateProductTypeDTO {
id?: string
value: string
metadata?: Record<string, unknown>
}
export interface UpsertProductTypeDTO {
id?: string
value: string
}
export interface UpdateProductTypeDTO {
id: string
value?: string
metadata?: Record<string, unknown>
}
export interface CreateProductTagDTO {
value: string
}
export interface UpsertProductTagDTO {
id?: string
value: string
}
export interface UpdateProductTagDTO {
id: string
value?: string
}
export interface CreateProductOptionDTO {
title: string
product_id?: string
product?: Record<any, any>
}
export interface UpdateProductOptionDTO {
id: string
title?: string
product_id?: string
}
export interface CreateProductVariantOptionDTO {
@@ -368,6 +447,7 @@ export interface UpdateProductVariantOnlyDTO {
}
export interface CreateProductOptionOnlyDTO {
product: { id: string }
product_id?: string
product?: Record<any, any>
title: string
}

View File

@@ -1,16 +1,30 @@
import {
CreateProductDTO,
CreateProductTagDTO,
CreateProductTypeDTO,
CreateProductOptionDTO,
CreateProductCategoryDTO,
UpdateProductTagDTO,
UpdateProductTypeDTO,
UpdateProductOptionDTO,
UpdateProductCategoryDTO,
UpdateProductDTO,
FilterableProductCategoryProps,
FilterableProductCollectionProps,
FilterableProductProps,
FilterableProductTagProps,
FilterableProductTypeProps,
FilterableProductOptionProps,
FilterableProductVariantProps,
ProductCategoryDTO,
ProductCollectionDTO,
ProductDTO,
ProductTagDTO,
ProductTypeDTO,
ProductOptionDTO,
ProductVariantDTO,
CreateProductCollectionDTO,
UpdateProductCollectionDTO,
} from "./common"
import { Context } from "../shared-context"
@@ -38,12 +52,105 @@ export interface IProductModuleService {
sharedContext?: Context
): Promise<[ProductDTO[], number]>
retrieveTag(
tagId: string,
config?: FindConfig<ProductTagDTO>,
sharedContext?: Context
): Promise<ProductTagDTO>
listTags(
filters?: FilterableProductTagProps,
config?: FindConfig<ProductTagDTO>,
sharedContext?: Context
): Promise<ProductTagDTO[]>
listAndCountTags(
filters?: FilterableProductTagProps,
config?: FindConfig<ProductTagDTO>,
sharedContext?: Context
): Promise<[ProductTagDTO[], number]>
createTags(
data: CreateProductTagDTO[],
sharedContext?: Context,
): Promise<ProductTagDTO[]>
updateTags(
data: UpdateProductTagDTO[],
sharedContext?: Context,
): Promise<ProductTagDTO[]>
deleteTags(
productTagIds: string[],
sharedContext?: Context,
): Promise<void>
retrieveType(
typeId: string,
config?: FindConfig<ProductTypeDTO>,
sharedContext?: Context
): Promise<ProductTypeDTO>
listTypes(
filters?: FilterableProductTypeProps,
config?: FindConfig<ProductTypeDTO>,
sharedContext?: Context
): Promise<ProductTypeDTO[]>
listAndCountTypes(
filters?: FilterableProductTypeProps,
config?: FindConfig<ProductTypeDTO>,
sharedContext?: Context
): Promise<[ProductTypeDTO[], number]>
createTypes(
data: CreateProductTypeDTO[],
sharedContext?: Context,
): Promise<ProductTypeDTO[]>
updateTypes(
data: UpdateProductTypeDTO[],
sharedContext?: Context,
): Promise<ProductTypeDTO[]>
deleteTypes(
productTypeIds: string[],
sharedContext?: Context,
): Promise<void>
retrieveOption(
optionId: string,
config?: FindConfig<ProductOptionDTO>,
sharedContext?: Context
): Promise<ProductOptionDTO>
listOptions(
filters?: FilterableProductOptionProps,
config?: FindConfig<ProductOptionDTO>,
sharedContext?: Context
): Promise<ProductOptionDTO[]>
listAndCountOptions(
filters?: FilterableProductOptionProps,
config?: FindConfig<ProductOptionDTO>,
sharedContext?: Context
): Promise<[ProductOptionDTO[], number]>
createOptions(
data: CreateProductOptionDTO[],
sharedContext?: Context,
): Promise<ProductOptionDTO[]>
updateOptions(
data: UpdateProductOptionDTO[],
sharedContext?: Context,
): Promise<ProductOptionDTO[]>
deleteOptions(
productOptionIds: string[],
sharedContext?: Context,
): Promise<void>
retrieveVariant(
productVariantId: string,
config?: FindConfig<ProductVariantDTO>,
@@ -80,6 +187,21 @@ export interface IProductModuleService {
sharedContext?: Context
): Promise<[ProductCollectionDTO[], number]>
createCollections(
data: CreateProductCollectionDTO[],
sharedContext?: Context,
): Promise<ProductCollectionDTO[]>
updateCollections(
data: UpdateProductCollectionDTO[],
sharedContext?: Context,
): Promise<ProductCollectionDTO[]>
deleteCollections(
productCollectionIds: string[],
sharedContext?: Context,
): Promise<void>
retrieveCategory(
productCategoryId: string,
config?: FindConfig<ProductCategoryDTO>,
@@ -98,6 +220,22 @@ export interface IProductModuleService {
sharedContext?: Context
): Promise<[ProductCategoryDTO[], number]>
createCategory(
data: CreateProductCategoryDTO,
sharedContext?: Context,
): Promise<ProductCategoryDTO>
updateCategory(
categoryId: string,
data: UpdateProductCategoryDTO,
sharedContext?: Context,
): Promise<ProductCategoryDTO>
deleteCategory(
categoryId: string,
sharedContext?: Context,
): Promise<void>
create(
data: CreateProductDTO[],
sharedContext?: Context

View File

@@ -2,10 +2,12 @@ import { EntityManager } from "typeorm"
export type SharedContext = {
transactionManager?: EntityManager
manager?: EntityManager
}
export type Context<TManager = unknown> = {
transactionManager?: TManager
manager?: TManager
isolationLevel?: string
enableNestedTransactions?: boolean
transactionId?: string

View File

@@ -1 +1,2 @@
export * from "./inject-transaction-manager"
export * from "./inject-manager"

View File

@@ -0,0 +1,31 @@
import { Context, SharedContext } from "@medusajs/types"
export function InjectManager(managerProperty?: string): MethodDecorator {
return function (
target: any,
propertyKey: string | symbol,
descriptor: any
): void {
if (!target.MedusaContextIndex_) {
throw new Error(
`To apply @InjectManager you have to flag a parameter using @MedusaContext`
)
}
const originalMethod = descriptor.value
const argIndex = target.MedusaContextIndex_[propertyKey]
descriptor.value = function (...args: any[]) {
const context: SharedContext | Context = args[argIndex] ?? {}
const resourceWithManager = (!managerProperty
? this
: this[managerProperty])
context.manager =
context.manager ?? resourceWithManager.getFreshManager()
args[argIndex] = context
return originalMethod.apply(this, args)
}
}
}

View File

@@ -5,7 +5,7 @@ import { buildQuery } from "./build-query"
type RetrieveEntityParams<TDTO> = {
id: string,
entityName: string,
repository: DAL.TreeRepositoryService
repository: DAL.TreeRepositoryService | DAL.RepositoryService
config: FindConfig<TDTO>
sharedContext?: Context
}