diff --git a/.changeset/few-cars-act.md b/.changeset/few-cars-act.md new file mode 100644 index 0000000000..022cb1c1cb --- /dev/null +++ b/.changeset/few-cars-act.md @@ -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 diff --git a/packages/product/integration-tests/__fixtures__/product-category/data/index.ts b/packages/product/integration-tests/__fixtures__/product-category/data/index.ts index 9b75d1e42b..28f9f5b908 100644 --- a/packages/product/integration-tests/__fixtures__/product-category/data/index.ts +++ b/packages/product/integration-tests/__fixtures__/product-category/data/index.ts @@ -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, + }, +] + diff --git a/packages/product/integration-tests/__fixtures__/product/index.ts b/packages/product/integration-tests/__fixtures__/product/index.ts index b769251dcf..6709b496fc 100644 --- a/packages/product/integration-tests/__fixtures__/product/index.ts +++ b/packages/product/integration-tests/__fixtures__/product/index.ts @@ -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[] diff --git a/packages/product/integration-tests/__tests__/services/product-category/index.ts b/packages/product/integration-tests/__tests__/services/product-category/index.ts index c200323ae0..483c7bbe32 100644 --- a/packages/product/integration-tests/__tests__/services/product-category/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-category/index.ts @@ -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({ 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", + }) + ]) + ) + }) + }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-collection/index.ts b/packages/product/integration-tests/__tests__/services/product-collection/index.ts index 28b620dd56..31a29c06d0 100644 --- a/packages/product/integration-tests/__tests__/services/product-collection/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-collection/index.ts @@ -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") + }) + }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts index 20ba70618f..8ac9d6f03b 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts @@ -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", + }) + ]) + ) + }) + }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts index 9bba10d633..aeed5958e3 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts @@ -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") + }) + }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts new file mode 100644 index 0000000000..88000d26ee --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts @@ -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") + }) + }) +}) + diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts new file mode 100644 index 0000000000..b3340499d4 --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts @@ -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") + }) + }) +}) + diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts new file mode 100644 index 0000000000..1f08c28f50 --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts @@ -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") + }) + }) +}) + diff --git a/packages/product/integration-tests/__tests__/services/product-option/index.ts b/packages/product/integration-tests/__tests__/services/product-option/index.ts new file mode 100644 index 0000000000..514f03a1a3 --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-option/index.ts @@ -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 + }) + }) + ) + }) + }) +}) diff --git a/packages/product/integration-tests/__tests__/services/product-tag/index.ts b/packages/product/integration-tests/__tests__/services/product-tag/index.ts index 57ce42f4b6..aed57e8951 100644 --- a/packages/product/integration-tests/__tests__/services/product-tag/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-tag/index.ts @@ -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") + }) + }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-type/index.ts b/packages/product/integration-tests/__tests__/services/product-type/index.ts new file mode 100644 index 0000000000..40ed973f6e --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-type/index.ts @@ -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") + }) + }) +}) diff --git a/packages/product/src/models/product-category.ts b/packages/product/src/models/product-category.ts index eedc7a155a..229b9b7c51 100644 --- a/packages/product/src/models/product-category.ts +++ b/packages/product/src/models/product-category.ts @@ -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(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(this) diff --git a/packages/product/src/repositories/base.ts b/packages/product/src/repositories/base.ts index b9f52a5833..8dbe406526 100644 --- a/packages/product/src/repositories/base.ts +++ b/packages/product/src/repositories/base.ts @@ -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( this: any, task: (transactionManager: unknown) => Promise, { @@ -18,7 +18,7 @@ async function transactionWrapper( enableNestedTransactions = false, }: { isolationLevel?: string - transaction?: unknown + transaction?: TManager enableNestedTransactions?: boolean } = {} ): Promise { @@ -40,26 +40,34 @@ async function transactionWrapper( return await (this.manager_ as SqlEntityManager).transactional(task, options) } -const updateDeletedAtRecursively = async ( - manager: SqlEntityManager, +// TODO: move to utils package +const mikroOrmUpdateDeletedAtRecursively = async ( + 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 ( }, }) - await updateDeletedAtRecursively(manager, relationEntities, value) + await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value) } - await manager.persist(entities) + await manager.persist(entity) } } @@ -83,17 +91,29 @@ const serializer = ( return result as unknown as Promise } -export abstract class AbstractBaseRepository - implements DAL.RepositoryService -{ +// TODO: move to utils package +class AbstractBase { protected readonly manager_: SqlEntityManager protected constructor({ manager }) { this.manager_ = manager } - async transaction( - task: (transactionManager: unknown) => Promise, + getFreshManager(): TManager { + return (this.manager_.fork + ? this.manager_.fork() + : this.manager_) as unknown as TManager + } + + getActiveManager( + @MedusaContext() + { transactionManager, manager }: Context = {} + ): TManager { + return (transactionManager ?? manager ?? this.manager_) as TManager + } + + async transaction( + task: (transactionManager: TManager) => Promise, { transaction, isolationLevel, @@ -101,7 +121,7 @@ export abstract class AbstractBaseRepository }: { isolationLevel?: string enableNestedTransactions?: boolean - transaction?: unknown + transaction?: TManager } = {} ): Promise { return await transactionWrapper.apply(this, arguments) @@ -113,7 +133,12 @@ export abstract class AbstractBaseRepository ): Promise { return await serializer(data, options) } +} +export abstract class AbstractBaseRepository + extends AbstractBase + implements DAL.RepositoryService +{ abstract find(options?: DAL.FindOptions, context?: Context) abstract findAndCount( @@ -132,9 +157,9 @@ export abstract class AbstractBaseRepository { transactionManager: manager }: Context = {} ): Promise { 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 const entities = await this.find(query) - await updateDeletedAtRecursively( + await mikroOrmUpdateDeletedAtRecursively( manager as SqlEntityManager, entities, null @@ -168,8 +193,9 @@ export abstract class AbstractBaseRepository } } +// TODO: move to utils package export abstract class AbstractTreeRepositoryBase - extends AbstractBaseRepository + extends AbstractBase implements DAL.TreeRepositoryService { protected constructor({ manager }) { @@ -188,12 +214,18 @@ export abstract class AbstractTreeRepositoryBase transformOptions?: RepositoryTransformOptions, context?: Context ): Promise<[T[], number]> + + abstract create(data: unknown, context?: Context): Promise + + abstract delete(id: string, context?: Context): Promise } +// 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 { + 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 { + throw new Error("Method not implemented.") + } + + delete(id: string, context?: Context): Promise { + throw new Error("Method not implemented.") + } +} diff --git a/packages/product/src/repositories/product-category.ts b/packages/product/src/repositories/product-category.ts index b02b00d775..309715b430 100644 --- a/packages/product/src/repositories/product-category.ts +++ b/packages/product/src/repositories/product-category.ts @@ -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 { +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 { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } const { includeDescendantsTree } = transformOptions @@ -65,8 +79,7 @@ export class ProductCategoryRepository extends AbstractTreeRepositoryBase = { where: {} }, context: Context = {} ): Promise { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) for (let productCategory of productCategories) { const whereOptions = { @@ -112,8 +125,7 @@ export class ProductCategoryRepository extends AbstractTreeRepositoryBase { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } const { includeDescendantsTree } = transformOptions @@ -153,21 +165,267 @@ export class ProductCategoryRepository extends AbstractTreeRepositoryBase { + const manager = this.getActiveManager(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 { - throw new Error("Method not implemented.") + data: ProductCategoryServiceTypes.CreateProductCategoryDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const categoryData = { ...data } + const manager = this.getActiveManager(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 { + const categoryData = { ...data } + const manager = this.getActiveManager(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 { + 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 { + 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) } } diff --git a/packages/product/src/repositories/product-collection.ts b/packages/product/src/repositories/product-collection.ts index adde4237a7..1c223926f4 100644 --- a/packages/product/src/repositories/product-collection.ts +++ b/packages/product/src/repositories/product-collection.ts @@ -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 { +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 { 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 { - throw new Error("Method not implemented.") + const manager = this.getActiveManager(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 { + const manager = this.getActiveManager(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 } } diff --git a/packages/product/src/repositories/product-image.ts b/packages/product/src/repositories/product-image.ts index 084f712c38..1c664c610b 100644 --- a/packages/product/src/repositories/product-image.ts +++ b/packages/product/src/repositories/product-image.ts @@ -22,8 +22,7 @@ export class ProductImageRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -43,8 +42,7 @@ export class ProductImageRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise<[Image[], number]> { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} diff --git a/packages/product/src/repositories/product-option.ts b/packages/product/src/repositories/product-option.ts index b3fa0138f8..4751e885b3 100644 --- a/packages/product/src/repositories/product-option.ts +++ b/packages/product/src/repositories/product-option.ts @@ -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 { protected readonly manager_: SqlEntityManager @@ -22,8 +26,7 @@ export class ProductOptionRepository extends AbstractBaseRepository = { where: {} }, context: Context = {} ): Promise { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -43,8 +46,7 @@ export class ProductOptionRepository extends AbstractBaseRepository = { where: {} }, context: Context = {} ): Promise<[ProductOption[], number]> { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -64,10 +66,12 @@ export class ProductOptionRepository extends AbstractBaseRepository { + const manager = this.getActiveManager(context) + await (manager as SqlEntityManager).nativeDelete( - Product, + ProductOption, { id: { $in: ids } }, {} ) @@ -75,16 +79,81 @@ export class ProductOptionRepository extends AbstractBaseRepository { - const options = data.map((option) => { - return (manager as SqlEntityManager).create(ProductOption, option) + const manager = this.getActiveManager(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 { + const manager = this.getActiveManager(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 } } diff --git a/packages/product/src/repositories/product-tag.ts b/packages/product/src/repositories/product-tag.ts index 1dbb318ad1..124c20f541 100644 --- a/packages/product/src/repositories/product-tag.ts +++ b/packages/product/src/repositories/product-tag.ts @@ -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 { +export class ProductTagRepository extends BaseRepository { protected readonly manager_: SqlEntityManager constructor({ manager }: { manager: SqlEntityManager }) { @@ -23,10 +33,9 @@ export class ProductTagRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager - + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } + findOptions_.options ??= {} Object.assign(findOptions_.options, { @@ -44,8 +53,7 @@ export class ProductTagRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise<[ProductTag[], number]> { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -61,14 +69,71 @@ export class ProductTagRepository extends AbstractBaseRepository { ) } + @InjectTransactionManager() + async create( + data: CreateProductTagDTO[], + @MedusaContext() + context: Context = {} + ): Promise { + const manager = this.getActiveManager(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 { + const manager = this.getActiveManager(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 { 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 { async delete( ids: string[], @MedusaContext() - { transactionManager: manager }: Context = {} + context: Context = {} ): Promise { - await (manager as SqlEntityManager).nativeDelete( - Product, - { id: { $in: ids } }, - {} - ) - } + const manager = this.getActiveManager(context) - @InjectTransactionManager() - async create( - data: unknown[], - @MedusaContext() - { transactionManager: manager }: Context = {} - ): Promise { - throw new Error("Method not implemented.") + await manager.nativeDelete(ProductTag, { id: { $in: ids } }, {}) } } diff --git a/packages/product/src/repositories/product-type.ts b/packages/product/src/repositories/product-type.ts index 6d69cce031..8e99eb2f85 100644 --- a/packages/product/src/repositories/product-type.ts +++ b/packages/product/src/repositories/product-type.ts @@ -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 { +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 { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -44,8 +53,7 @@ export class ProductTypeRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise<[ProductType[], number]> { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -118,7 +126,7 @@ export class ProductTypeRepository extends AbstractBaseRepository { { transactionManager: manager }: Context = {} ): Promise { await (manager as SqlEntityManager).nativeDelete( - Product, + ProductType, { id: { $in: ids } }, {} ) @@ -126,10 +134,59 @@ export class ProductTypeRepository extends AbstractBaseRepository { @InjectTransactionManager() async create( - data: unknown[], + data: CreateProductTypeDTO[], @MedusaContext() - { transactionManager: manager }: Context = {} + context: Context = {} ): Promise { - throw new Error("Method not implemented.") + const manager = this.getActiveManager(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 { + const manager = this.getActiveManager(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 } } diff --git a/packages/product/src/repositories/product-variant.ts b/packages/product/src/repositories/product-variant.ts index 7562eb38db..b6827ed640 100644 --- a/packages/product/src/repositories/product-variant.ts +++ b/packages/product/src/repositories/product-variant.ts @@ -31,8 +31,7 @@ export class ProductVariantRepository extends AbstractBaseRepository = { where: {} }, context: Context = {} ): Promise { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -52,8 +51,7 @@ export class ProductVariantRepository extends AbstractBaseRepository = { where: {} }, context: Context = {} ): Promise<[ProductVariant[], number]> { - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} diff --git a/packages/product/src/repositories/product.ts b/packages/product/src/repositories/product.ts index a1ca522c6c..ee0b9ed732 100644 --- a/packages/product/src/repositories/product.ts +++ b/packages/product/src/repositories/product.ts @@ -38,9 +38,7 @@ export class ProductRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise { - // TODO: use the getter method (getActiveManager) - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } findOptions_.options ??= {} @@ -62,20 +60,18 @@ export class ProductRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise<[Product[], number]> { + const manager = this.getActiveManager(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, findOptions_.options as MikroOptions @@ -90,9 +86,7 @@ export class ProductRepository extends AbstractBaseRepository { findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise { - // TODO: use the getter method (getActiveManager) - const manager = (context.transactionManager ?? - this.manager_) as SqlEntityManager + const manager = this.getActiveManager(context) if (findOptions.where.categories?.id?.["$nin"]) { const productsInCategories = await manager.find( diff --git a/packages/product/src/services/__fixtures__/product.ts b/packages/product/src/services/__fixtures__/product.ts index aa4377bef8..ec32ccbb28 100644 --- a/packages/product/src/services/__fixtures__/product.ts +++ b/packages/product/src/services/__fixtures__/product.ts @@ -15,6 +15,7 @@ mockContainer.register({ return [{}] }), findAndCount: jest.fn().mockResolvedValue([[], 0]), + getFreshManager: jest.fn().mockResolvedValue({}), }), productService: asClass(ProductService), }) diff --git a/packages/product/src/services/__tests__/product.spec.ts b/packages/product/src/services/__tests__/product.spec.ts index ab43e62d5e..a2d4b2cb39 100644 --- a/packages/product/src/services/__tests__/product.spec.ts +++ b/packages/product/src/services/__tests__/product.spec.ts @@ -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) ) }) }) diff --git a/packages/product/src/services/product-category.ts b/packages/product/src/services/product-category.ts index d8bb6b70fc..a6f9df6b54 100644 --- a/packages/product/src/services/product-category.ts +++ b/packages/product/src/services/product-category.ts @@ -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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - 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 { + 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 { + return (await (this.productCategoryRepository_ as ProductCategoryRepository).update( + id, + data, + sharedContext + )) as TEntity + } + + @InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_") + async delete( + id: string, + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.productCategoryRepository_.delete(id, sharedContext) + } } diff --git a/packages/product/src/services/product-collection.ts b/packages/product/src/services/product-collection.ts index 0ce5fd549d..45d3cf5747 100644 --- a/packages/product/src/services/product-collection.ts +++ b/packages/product/src/services/product-collection.ts @@ -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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return (await retrieveEntity< ProductCollection, @@ -33,10 +43,11 @@ export default class ProductCollectionService< })) as TEntity } + @InjectManager("productCollectionRepository_") async list( filters: ProductTypes.FilterableProductCollectionProps = {}, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - 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 { + return (await (this.productCollectionRepository_ as ProductCollectionRepository).create( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_") + async update( + data: ProductTypes.UpdateProductCollectionDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.productCollectionRepository_ as ProductCollectionRepository).update( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.productCollectionRepository_.delete(ids, sharedContext) + } } diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 4bb1afbe5b..8309135396 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - 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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - 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 = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productTag = await this.productTagService_.retrieve( + tagId, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(productTag)) + } + + @InjectManager("baseRepository_") async listTags( filters: ProductTypes.FilterableProductTagProps = {}, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, + @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 { + await this.productTagService_.delete(productTagIds, sharedContext) + } + + @InjectManager("baseRepository_") + async retrieveType( + typeId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productType = await this.productTypeService_.retrieve( + typeId, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(productType)) + } + + @InjectManager("baseRepository_") + async listTypes( + filters: ProductTypes.FilterableProductTypeProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const types = await this.productTypeService_.list( + filters, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(types)) + } + + @InjectManager("baseRepository_") + async listAndCountTypes( + filters: ProductTypes.FilterableProductTypeProps = {}, + config: FindConfig = {}, + @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 { + await this.productTypeService_.delete(productTypeIds, sharedContext) + } + + @InjectManager("baseRepository_") + async retrieveOption( + optionId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productOptions = await this.productOptionService_.retrieve( + optionId, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(productOptions)) + } + + @InjectManager("baseRepository_") + async listOptions( + filters: ProductTypes.FilterableProductTypeProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productOptions = await this.productOptionService_.list( + filters, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(productOptions)) + } + + @InjectManager("baseRepository_") + async listAndCountOptions( + filters: ProductTypes.FilterableProductTypeProps = {}, + config: FindConfig = {}, + @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 { + await this.productOptionService_.delete(productOptionIds, sharedContext) + } + + @InjectManager("baseRepository_") async retrieveCollection( productCollectionId: string, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - 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 { + await this.productCollectionService_.delete( + productCollectionIds, + sharedContext + ) + } + + @InjectManager("baseRepository_") async retrieveCategory( productCategoryId: string, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { 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 { + await this.productCategoryService_.delete(categoryId, sharedContext) + } + + @InjectManager("baseRepository_") async listAndCountCategories( filters: ProductTypes.FilterableProductCategoryProps = {}, config: FindConfig = {}, - 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 { 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 } } diff --git a/packages/product/src/services/product-option.ts b/packages/product/src/services/product-option.ts index 50bf2ff9b3..eecc0f20ac 100644 --- a/packages/product/src/services/product-option.ts +++ b/packages/product/src/services/product-option.ts @@ -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 = {}, + @MedusaContext() sharedContext?: Context + ): Promise { + 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 = {}, + @MedusaContext() sharedContext?: Context + ): Promise { + return (await this.productOptionRepository_.find( + this.buildQueryForList(filters, config), + sharedContext + )) as TEntity[] + } + + @InjectManager("productOptionRepository_") + async listAndCount( + filters: ProductTypes.FilterableProductOptionProps = {}, + config: FindConfig = {}, + @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 = {}, + ) { + const queryOptions = ModulesSdkUtils.buildQuery(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 { + return (await (this.productOptionRepository_ as ProductOptionRepository).update( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(doNotForceTransaction, "productOptionRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return await this.productOptionRepository_.delete(ids, sharedContext) + } } diff --git a/packages/product/src/services/product-tag.ts b/packages/product/src/services/product-tag.ts index 132c4dd0ea..91874fd22e 100644 --- a/packages/product/src/services/product-tag.ts +++ b/packages/product/src/services/product-tag.ts @@ -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 = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + 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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { + return (await this.productTagRepository_.find( + this.buildQueryForList(filters, config), + sharedContext + )) as TEntity[] + } + + @InjectManager("productTagRepository_") + async listAndCount( + filters: ProductTypes.FilterableProductTagProps = {}, + config: FindConfig = {}, + @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 = {}, + ) { const queryOptions = ModulesSdkUtils.buildQuery(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 { + return (await (this.productTagRepository_ as ProductTagRepository).create( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(shouldForceTransaction, "productTagRepository_") + async update( + data: UpdateProductTagDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.productTagRepository_ as ProductTagRepository).update( + data, sharedContext )) as TEntity[] } + @InjectTransactionManager(doNotForceTransaction, "productTagRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.productTagRepository_.delete(ids, sharedContext) + } + @InjectTransactionManager(doNotForceTransaction, "productTagRepository_") async upsert( - tags: CreateProductTagDTO[], + data: UpsertProductTagDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { return (await (this.productTagRepository_ as ProductTagRepository).upsert!( - tags, + data, sharedContext )) as TEntity[] } diff --git a/packages/product/src/services/product-type.ts b/packages/product/src/services/product-type.ts index 60f25106ee..995a8beaa4 100644 --- a/packages/product/src/services/product-type.ts +++ b/packages/product/src/services/product-type.ts @@ -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 = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + 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 = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.productTypeRepository_.find( + this.buildQueryForList(filters, config), + sharedContext + )) as TEntity[] + } + + @InjectManager("productTypeRepository_") + async listAndCount( + filters: ProductTypes.FilterableProductTypeProps = {}, + config: FindConfig = {}, + @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 = {}, + ) { + const queryOptions = ModulesSdkUtils.buildQuery(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 { return (await (this.productTypeRepository_ as ProductTypeRepository) .upsert!(types, sharedContext)) as TEntity[] } + + @InjectTransactionManager(shouldForceTransaction, "productTypeRepository_") + async create( + data: CreateProductTypeDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.productTypeRepository_ as ProductTypeRepository).create( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(shouldForceTransaction, "productTypeRepository_") + async update( + data: UpdateProductTypeDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.productTypeRepository_ as ProductTypeRepository).update( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(doNotForceTransaction, "productTypeRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.productTypeRepository_.delete(ids, sharedContext) + } } diff --git a/packages/product/src/services/product-variant.ts b/packages/product/src/services/product-variant.ts index 762a3d40b0..36146378a7 100644 --- a/packages/product/src/services/product-variant.ts +++ b/packages/product/src/services/product-variant.ts @@ -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 = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return (await retrieveEntity< ProductVariant, @@ -50,10 +52,11 @@ export default class ProductVariantService< })) as TEntity } + @InjectManager("productVariantRepository_") async list( filters: ProductTypes.FilterableProductVariantProps = {}, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const queryOptions = ModulesSdkUtils.buildQuery( filters, @@ -66,10 +69,11 @@ export default class ProductVariantService< )) as TEntity[] } + @InjectManager("productVariantRepository_") async listAndCount( filters: ProductTypes.FilterableProductVariantProps = {}, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { const queryOptions = ModulesSdkUtils.buildQuery( filters, diff --git a/packages/product/src/services/product.ts b/packages/product/src/services/product.ts index 78b8024547..bbf8b6722e 100644 --- a/packages/product/src/services/product.ts +++ b/packages/product/src/services/product.ts @@ -8,6 +8,7 @@ import { WithRequiredProperty, } from "@medusajs/types" import { + InjectManager, InjectTransactionManager, MedusaContext, MedusaError, @@ -30,10 +31,11 @@ export default class ProductService { this.productRepository_ = productRepository } + @InjectManager("productRepository_") async retrieve( productId: string, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { if (!isDefined(productId)) { throw new MedusaError( @@ -61,10 +63,11 @@ export default class ProductService { return product[0] as TEntity } + @InjectManager("productRepository_") async list( filters: ProductTypes.FilterableProductProps = {}, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { if (filters.category_ids) { if (Array.isArray(filters.category_ids)) { @@ -86,10 +89,11 @@ export default class ProductService { )) as TEntity[] } + @InjectManager("productRepository_") async listAndCount( filters: ProductTypes.FilterableProductProps = {}, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { if (filters.category_ids) { if (Array.isArray(filters.category_ids)) { diff --git a/packages/product/src/types/index.ts b/packages/product/src/types/index.ts index 1c7b8e8c52..0d290a8666 100644 --- a/packages/product/src/types/index.ts +++ b/packages/product/src/types/index.ts @@ -1,3 +1,5 @@ +export * from "./services" + import { IEventBusService } from "@medusajs/types" export type InitializeModuleInjectableDependencies = { diff --git a/packages/product/src/types/services/index.ts b/packages/product/src/types/services/index.ts index e2797cdc2f..d6e12795aa 100644 --- a/packages/product/src/types/services/index.ts +++ b/packages/product/src/types/services/index.ts @@ -1,2 +1,3 @@ +export * as ProductCategoryServiceTypes from "./product-category" export * as ProductServiceTypes from "./product" export * as ProductVariantServiceTypes from "./product-variant" diff --git a/packages/product/src/types/services/product-category.ts b/packages/product/src/types/services/product-category.ts new file mode 100644 index 0000000000..85decef2a5 --- /dev/null +++ b/packages/product/src/types/services/product-category.ts @@ -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 +} + +export interface UpdateProductCategoryDTO { + name?: string + handle?: string + is_active?: boolean + is_internal?: boolean + rank?: number + parent_category_id?: string | null + metadata?: Record +} \ No newline at end of file diff --git a/packages/types/src/dal/repository-service.ts b/packages/types/src/dal/repository-service.ts index 445473aec8..062b802a62 100644 --- a/packages/types/src/dal/repository-service.ts +++ b/packages/types/src/dal/repository-service.ts @@ -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 { - transaction( - task: (transactionManager: unknown) => Promise, +interface BaseRepositoryService { + transaction( + task: (transactionManager: TManager) => Promise, context?: { isolationLevel?: string - transaction?: unknown + transaction?: TManager enableNestedTransactions?: boolean } ): Promise + getFreshManager(): TManager + + getActiveManager(): TManager + serialize( data: any, options?: any ): Promise +} +export interface RepositoryService extends BaseRepositoryService { find(options?: FindOptions, context?: Context): Promise findAndCount( @@ -44,7 +50,7 @@ export interface RepositoryService { restore(ids: string[], context?: Context): Promise } -export interface TreeRepositoryService extends RepositoryService { +export interface TreeRepositoryService extends BaseRepositoryService { find( options?: FindOptions, transformOptions?: RepositoryTransformOptions, @@ -56,4 +62,8 @@ export interface TreeRepositoryService extends RepositoryService { transformOptions?: RepositoryTransformOptions, context?: Context ): Promise<[T[], number]> + + create(data: unknown, context?: Context): Promise + + delete(id: string, context?: Context): Promise } diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 948f614e0b..43bd8c0ebd 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -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 +} + +export interface UpdateProductCategoryDTO { + name?: string + handle?: string + is_active?: boolean + is_internal?: boolean + rank?: number + parent_category_id?: string | null + metadata?: Record +} + export interface ProductTagDTO { id: string value: string @@ -153,6 +173,19 @@ export interface FilterableProductTagProps value?: string } +export interface FilterableProductTypeProps + extends BaseFilterable { + id?: string | string[] + value?: string +} + +export interface FilterableProductOptionProps + extends BaseFilterable { + id?: string | string[] + title?: string + product_id?: string | string[] +} + export interface FilterableProductCollectionProps extends BaseFilterable { id?: string | string[] @@ -170,6 +203,7 @@ export interface FilterableProductVariantProps export interface FilterableProductCategoryProps extends BaseFilterable { 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 +} + +export interface UpdateProductCollectionDTO { + id: string + value?: string + title?: string + handle?: string + products?: ProductDTO[] + metadata?: Record +} + export interface CreateProductTypeDTO { id?: string value: string + metadata?: Record +} + +export interface UpsertProductTypeDTO { + id?: string + value: string +} + +export interface UpdateProductTypeDTO { + id: string + value?: string + metadata?: Record } 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 +} + +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 title: string } diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index bf91e2ae0b..1685b71894 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -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, + sharedContext?: Context + ): Promise + listTags( filters?: FilterableProductTagProps, config?: FindConfig, sharedContext?: Context ): Promise + listAndCountTags( + filters?: FilterableProductTagProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[ProductTagDTO[], number]> + + createTags( + data: CreateProductTagDTO[], + sharedContext?: Context, + ): Promise + + updateTags( + data: UpdateProductTagDTO[], + sharedContext?: Context, + ): Promise + + deleteTags( + productTagIds: string[], + sharedContext?: Context, + ): Promise + + retrieveType( + typeId: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listTypes( + filters?: FilterableProductTypeProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCountTypes( + filters?: FilterableProductTypeProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[ProductTypeDTO[], number]> + + createTypes( + data: CreateProductTypeDTO[], + sharedContext?: Context, + ): Promise + + updateTypes( + data: UpdateProductTypeDTO[], + sharedContext?: Context, + ): Promise + + deleteTypes( + productTypeIds: string[], + sharedContext?: Context, + ): Promise + + retrieveOption( + optionId: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listOptions( + filters?: FilterableProductOptionProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCountOptions( + filters?: FilterableProductOptionProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[ProductOptionDTO[], number]> + + createOptions( + data: CreateProductOptionDTO[], + sharedContext?: Context, + ): Promise + + updateOptions( + data: UpdateProductOptionDTO[], + sharedContext?: Context, + ): Promise + + deleteOptions( + productOptionIds: string[], + sharedContext?: Context, + ): Promise + retrieveVariant( productVariantId: string, config?: FindConfig, @@ -80,6 +187,21 @@ export interface IProductModuleService { sharedContext?: Context ): Promise<[ProductCollectionDTO[], number]> + createCollections( + data: CreateProductCollectionDTO[], + sharedContext?: Context, + ): Promise + + updateCollections( + data: UpdateProductCollectionDTO[], + sharedContext?: Context, + ): Promise + + deleteCollections( + productCollectionIds: string[], + sharedContext?: Context, + ): Promise + retrieveCategory( productCategoryId: string, config?: FindConfig, @@ -98,6 +220,22 @@ export interface IProductModuleService { sharedContext?: Context ): Promise<[ProductCategoryDTO[], number]> + createCategory( + data: CreateProductCategoryDTO, + sharedContext?: Context, + ): Promise + + updateCategory( + categoryId: string, + data: UpdateProductCategoryDTO, + sharedContext?: Context, + ): Promise + + deleteCategory( + categoryId: string, + sharedContext?: Context, + ): Promise + create( data: CreateProductDTO[], sharedContext?: Context diff --git a/packages/types/src/shared-context.ts b/packages/types/src/shared-context.ts index cfc127a374..e86a6856af 100644 --- a/packages/types/src/shared-context.ts +++ b/packages/types/src/shared-context.ts @@ -2,10 +2,12 @@ import { EntityManager } from "typeorm" export type SharedContext = { transactionManager?: EntityManager + manager?: EntityManager } export type Context = { transactionManager?: TManager + manager?: TManager isolationLevel?: string enableNestedTransactions?: boolean transactionId?: string diff --git a/packages/utils/src/modules-sdk/decorators/index.ts b/packages/utils/src/modules-sdk/decorators/index.ts index ec7b2d1a77..4f18892424 100644 --- a/packages/utils/src/modules-sdk/decorators/index.ts +++ b/packages/utils/src/modules-sdk/decorators/index.ts @@ -1 +1,2 @@ export * from "./inject-transaction-manager" +export * from "./inject-manager" diff --git a/packages/utils/src/modules-sdk/decorators/inject-manager.ts b/packages/utils/src/modules-sdk/decorators/inject-manager.ts new file mode 100644 index 0000000000..e0c007d9c9 --- /dev/null +++ b/packages/utils/src/modules-sdk/decorators/inject-manager.ts @@ -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) + } + } +} diff --git a/packages/utils/src/modules-sdk/retrieve-entity.ts b/packages/utils/src/modules-sdk/retrieve-entity.ts index dd8f5b8271..47f1f660ff 100644 --- a/packages/utils/src/modules-sdk/retrieve-entity.ts +++ b/packages/utils/src/modules-sdk/retrieve-entity.ts @@ -5,7 +5,7 @@ import { buildQuery } from "./build-query" type RetrieveEntityParams = { id: string, entityName: string, - repository: DAL.TreeRepositoryService + repository: DAL.TreeRepositoryService | DAL.RepositoryService config: FindConfig sharedContext?: Context }