From d71266587a8c4c18499bb57cb1fa31690f42a80e Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 25 Jan 2023 10:17:45 +0100 Subject: [PATCH 01/13] wip --- .../admin/__snapshots__/product.js.snap | 2 + .../api/__tests__/admin/product.js | 9 +++- .../api/helpers/product-category-seeder.ts | 47 +++++++++++++++++++ .../routes/admin/products/create-product.ts | 9 +++- .../src/api/routes/admin/products/index.ts | 1 + .../medusa/src/models/product-category.ts | 26 +++++----- packages/medusa/src/models/product.ts | 6 +++ packages/medusa/src/services/product.ts | 26 +++++++++- packages/medusa/src/types/product.ts | 16 +++++++ 9 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 integration-tests/api/helpers/product-category-seeder.ts diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap index 2eeafb5cdee55..4595017b1ac6a 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap @@ -111,6 +111,7 @@ Array [ exports[`/admin/products POST /admin/products creates a product 1`] = ` Object { + "categories": Array [], "collection": Object { "created_at": Any, "deleted_at": null, @@ -314,6 +315,7 @@ Object { exports[`/admin/products POST /admin/products updates a product (update prices, tags, update status, delete collection, delete type, replaces images) 1`] = ` Object { + "categories": Array [], "collection": null, "collection_id": null, "created_at": Any, diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 1e3110478df62..fabfd62ffb84e 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -6,6 +6,8 @@ const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") const productSeeder = require("../../helpers/product-seeder") +const { productCategorySeeder } = require("../../helpers/product-category-seeder") + const { ProductVariant, ProductOptionValue, @@ -38,6 +40,7 @@ describe("/admin/products", () => { dbConnection = await initDb({ cwd }) medusaProcess = await setupServer({ cwd, + verbose: true }) }) @@ -900,6 +903,7 @@ describe("/admin/products", () => { describe("POST /admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) + await productCategorySeeder(dbConnection) await adminSeeder(dbConnection) }) @@ -908,7 +912,7 @@ describe("/admin/products", () => { await db.teardown() }) - it("creates a product", async () => { + it.only("creates a product", async () => { const api = useApi() const payload = { @@ -917,6 +921,7 @@ describe("/admin/products", () => { type: { value: "test-type" }, images: ["test-image.png", "test-image-2.png"], collection_id: "test-collection", + categories: [{ id: "test-category-d2B" }, { id: "test-category-d2A" }], tags: [{ value: "123" }, { value: "456" }], options: [{ title: "size" }, { title: "color" }], variants: [ @@ -947,7 +952,7 @@ describe("/admin/products", () => { .catch((err) => { console.log(err) }) - +console.log("response.data.product - ", response.data.product) expect(response.status).toEqual(200) expect(response.data.product).toMatchSnapshot({ id: expect.stringMatching(/^prod_*/), diff --git a/integration-tests/api/helpers/product-category-seeder.ts b/integration-tests/api/helpers/product-category-seeder.ts new file mode 100644 index 0000000000000..4ab6946b0e24c --- /dev/null +++ b/integration-tests/api/helpers/product-category-seeder.ts @@ -0,0 +1,47 @@ +import { ProductCategory } from "@medusajs/medusa" + +export async function productCategorySeeder( + connection, + data = {}, +) { + const manager = connection.manager + + const categoryD0A = await manager.create(ProductCategory, { + id: "test-category-d0", + name: "test category d0", + }) + + await manager.save(categoryD0A) + + const categoryD1A = await manager.create(ProductCategory, { + id: "test-category-d1A", + name: "test category d1A", + parent_category: categoryD0A + }) + + await manager.save(categoryD1A) + + const categoryD2A = await manager.create(ProductCategory, { + id: "test-category-d2A", + name: "test category d2A", + parent_category: categoryD1A + }) + + await manager.save(categoryD2A) + + const categoryD1B = await manager.create(ProductCategory, { + id: "test-category-d1B", + name: "test category d1B", + parent_category: categoryD0A + }) + + await manager.save(categoryD1B) + + const categoryD2B = await manager.create(ProductCategory, { + id: "test-category-d2B", + name: "test category d2B", + parent_category: categoryD1B + }) + + await manager.save(categoryD2B) +} diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index 6d7c0a65ed4da..64913afbd27c6 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -17,6 +17,7 @@ import { } from "../../../../services" import { ProductSalesChannelReq, + ProductProductCategoryReq, ProductTagReq, ProductTypeReq, } from "../../../../types/product" @@ -96,7 +97,7 @@ import { validator } from "../../../../utils/validator" */ export default async (req, res) => { const validated = await validator(AdminPostProductsReq, req.body) - +console.log("validated - ", validated) const productService: ProductService = req.scope.resolve("productService") const pricingService: PricingService = req.scope.resolve("pricingService") const productVariantService: ProductVariantService = req.scope.resolve( @@ -527,6 +528,12 @@ export class AdminPostProductsReq { ]) sales_channels?: ProductSalesChannelReq[] + @IsOptional() + @Type(() => ProductProductCategoryReq) + @ValidateNested({ each: true }) + @IsArray() + categories?: ProductProductCategoryReq[] + @IsOptional() @Type(() => ProductOptionReq) @ValidateNested({ each: true }) diff --git a/packages/medusa/src/api/routes/admin/products/index.ts b/packages/medusa/src/api/routes/admin/products/index.ts index 8db38ebd8e174..22e2184f1a6b3 100644 --- a/packages/medusa/src/api/routes/admin/products/index.ts +++ b/packages/medusa/src/api/routes/admin/products/index.ts @@ -100,6 +100,7 @@ export const defaultAdminProductRelations = [ "tags", "type", "collection", + "categories", ] export const defaultAdminProductFields: (keyof Product)[] = [ diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index b42f6c7863e33..ac97fd60a0ac8 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -52,19 +52,19 @@ export class ProductCategory extends SoftDeletableEntity { @TreeChildren({ cascade: true }) category_children: ProductCategory[] - @ManyToMany(() => Product, { cascade: ["remove", "soft-remove"] }) - @JoinTable({ - name: "product_category_product", - joinColumn: { - name: "product_id", - referencedColumnName: "id", - }, - inverseJoinColumn: { - name: "product_category_id", - referencedColumnName: "id", - }, - }) - products: Product[] + // @ManyToMany(() => Product, { cascade: ["remove", "soft-remove"] }) + // @JoinTable({ + // name: "product_category_product", + // joinColumn: { + // name: "product_id", + // referencedColumnName: "id", + // }, + // inverseJoinColumn: { + // name: "product_category_id", + // referencedColumnName: "id", + // }, + // }) + // products: Product[] @BeforeInsert() private beforeInsert(): void { diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts index 42ef75f03f0e6..14ace44c79fa2 100644 --- a/packages/medusa/src/models/product.ts +++ b/packages/medusa/src/models/product.ts @@ -320,6 +320,12 @@ export class Product extends SoftDeletableEntity { * items: * type: object * description: A sales channel object. + * categories: + * description: The product's associated categories. Available if the relation `categories` is expanded. + * type: array + * items: + * type: object + * description: A category object. * created_at: * type: string * description: "The date with timezone at which the resource was created." diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index a5423d5f1b397..c52cf3eb4abd2 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -1,7 +1,7 @@ import { FlagRouter } from "../utils/flag-router" import { isDefined, MedusaError } from "medusa-core-utils" -import { EntityManager } from "typeorm" +import { EntityManager, In } from "typeorm" import { ProductVariantService, SearchService } from "." import { TransactionBaseService } from "../interfaces" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" @@ -18,6 +18,7 @@ import { FindWithoutRelationsOptions, ProductRepository, } from "../repositories/product" +import { ProductCategoryRepository } from "../repositories/product-category" import { ProductOptionRepository } from "../repositories/product-option" import { ProductTagRepository } from "../repositories/product-tag" import { ProductTypeRepository } from "../repositories/product-type" @@ -42,6 +43,7 @@ type InjectedDependencies = { productTypeRepository: typeof ProductTypeRepository productTagRepository: typeof ProductTagRepository imageRepository: typeof ImageRepository + productCategoryRepository: typeof ProductCategoryRepository productVariantService: ProductVariantService searchService: SearchService eventBusService: EventBusService @@ -58,6 +60,7 @@ class ProductService extends TransactionBaseService { protected readonly productTypeRepository_: typeof ProductTypeRepository protected readonly productTagRepository_: typeof ProductTagRepository protected readonly imageRepository_: typeof ImageRepository + protected readonly productCategoryRepository_: typeof ProductCategoryRepository protected readonly productVariantService_: ProductVariantService protected readonly searchService_: SearchService protected readonly eventBus_: EventBusService @@ -79,6 +82,7 @@ class ProductService extends TransactionBaseService { productVariantService, productTypeRepository, productTagRepository, + productCategoryRepository, imageRepository, searchService, featureFlagRouter, @@ -92,6 +96,7 @@ class ProductService extends TransactionBaseService { this.productVariantRepository_ = productVariantRepository this.eventBus_ = eventBusService this.productVariantService_ = productVariantService + this.productCategoryRepository_ = productCategoryRepository this.productTypeRepository_ = productTypeRepository this.productTagRepository_ = productTagRepository this.imageRepository_ = imageRepository @@ -386,13 +391,14 @@ class ProductService extends TransactionBaseService { const optionRepo = manager.getCustomRepository( this.productOptionRepository_ ) - +console.log("productObject - ", productObject) const { options, tags, type, images, sales_channels: salesChannels, + categories: categories, ...rest } = productObject @@ -432,6 +438,22 @@ class ProductService extends TransactionBaseService { } } } +console.log("categories - ", categories) +console.log("isDefined(categories) - ", isDefined(categories)) + if (isDefined(categories)) { + product.categories = [] + + if (categories?.length) { + const categoryIds = categories?.map((c) => c.id) + const pcRepo = manager.getCustomRepository(this.productCategoryRepository_) + + const categoryRecords = await pcRepo.find({ + where: { id: In(categoryIds) } + }) +console.log("categoryRecords - ", categoryRecords) + product.categories = categoryRecords + } + } product = await productRepo.save(product) diff --git a/packages/medusa/src/types/product.ts b/packages/medusa/src/types/product.ts index a5625832fcf33..a9ed1c856c00c 100644 --- a/packages/medusa/src/types/product.ts +++ b/packages/medusa/src/types/product.ts @@ -14,6 +14,7 @@ import { ProductOptionValue, ProductStatus, SalesChannel, + ProductCategory, } from "../models" import { FeatureFlagDecorators } from "../utils/feature-flag-decorators" import { optionalBooleanMapper } from "../utils/validators/is-boolean" @@ -74,6 +75,10 @@ export class FilterableProductProps { @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [IsOptional(), IsArray()]) sales_channel_id?: string[] + @IsArray() + @IsOptional() + category_id?: string[] + @IsString() @IsOptional() discount_condition_id?: string @@ -101,6 +106,7 @@ export type ProductSelector = discount_condition_id?: string price_list_id?: string[] | FindOperator sales_channel_id?: string[] | FindOperator + category_id?: string[] | FindOperator }) /** @@ -124,6 +130,7 @@ export type CreateProductInput = { options?: CreateProductProductOption[] variants?: CreateProductProductVariantInput[] sales_channels?: CreateProductProductSalesChannelInput[] | null + categories?: CreateProductProductCategoryInput[] | null weight?: number length?: number height?: number @@ -145,6 +152,10 @@ export type CreateProductProductSalesChannelInput = { id: string } +export type CreateProductProductCategoryInput = { + id: string +} + export type CreateProductProductTypeInput = { id?: string value: string @@ -226,6 +237,11 @@ export class ProductSalesChannelReq { id: string } +export class ProductProductCategoryReq { + @IsString() + id: string +} + export class ProductTagReq { @IsString() @IsOptional() From 149ba493933e211eb2599f2e2bb8d2f0d4753cbb Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 07:56:30 +0100 Subject: [PATCH 02/13] chore: fix issues with join table --- .../api/__tests__/admin/__snapshots__/product.js.snap | 10 +++++++++- integration-tests/api/__tests__/admin/product.js | 9 +++++++++ .../src/api/routes/admin/products/create-product.ts | 2 +- .../1674455083104-product_category_product.ts | 10 +++++----- packages/medusa/src/models/product-category.ts | 2 +- packages/medusa/src/models/product.ts | 6 +++--- packages/medusa/src/services/product.ts | 8 ++++---- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap index 4595017b1ac6a..3b2ccc5ec6ed8 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap @@ -3,6 +3,7 @@ exports[`/admin/products GET /admin/products returns a list of products with only giftcard in list 1`] = ` Array [ Object { + "categories": Array [], "collection": null, "collection_id": null, "created_at": Any, @@ -111,7 +112,14 @@ Array [ exports[`/admin/products POST /admin/products creates a product 1`] = ` Object { - "categories": Array [], + "categories": ArrayContaining [ + ObjectContaining { + "id": "test-category-d2A", + }, + ObjectContaining { + "id": "test-category-d2B", + }, + ], "collection": Object { "created_at": Any, "deleted_at": null, diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index aecf425957938..5dc877faa6781 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -1073,6 +1073,14 @@ describe("/admin/products", () => { ], }, ], + categories: expect.arrayContaining([ + expect.objectContaining({ + id: "test-category-d2A", + }), + expect.objectContaining({ + id: "test-category-d2B", + }), + ]), }) }) @@ -1431,6 +1439,7 @@ describe("/admin/products", () => { ], type: null, collection: null, + categories: [], }) ) }) diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index 64913afbd27c6..58929846ea6a4 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -97,7 +97,7 @@ import { validator } from "../../../../utils/validator" */ export default async (req, res) => { const validated = await validator(AdminPostProductsReq, req.body) -console.log("validated - ", validated) + const productService: ProductService = req.scope.resolve("productService") const pricingService: PricingService = req.scope.resolve("pricingService") const productVariantService: ProductVariantService = req.scope.resolve( diff --git a/packages/medusa/src/migrations/1674455083104-product_category_product.ts b/packages/medusa/src/migrations/1674455083104-product_category_product.ts index 2227cecb6c653..375135d829b2d 100644 --- a/packages/medusa/src/migrations/1674455083104-product_category_product.ts +++ b/packages/medusa/src/migrations/1674455083104-product_category_product.ts @@ -4,7 +4,7 @@ export class productCategoryProduct1674455083104 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( ` - CREATE TABLE "product_category_product" ( + CREATE TABLE "product_product_categories" ( "product_category_id" character varying NOT NULL, "product_id" character varying NOT NULL, CONSTRAINT "FK_product_category_id" FOREIGN KEY ("product_category_id") REFERENCES product_category("id") ON DELETE CASCADE ON UPDATE NO ACTION, @@ -16,21 +16,21 @@ export class productCategoryProduct1674455083104 implements MigrationInterface { await queryRunner.query( ` CREATE UNIQUE INDEX "IDX_upcp_product_id_product_category_id" - ON "product_category_product" ("product_category_id", "product_id") + ON "product_product_categories" ("product_category_id", "product_id") ` ) await queryRunner.query( ` CREATE INDEX "IDX_pcp_product_category_id" - ON "product_category_product" ("product_category_id") + ON "product_product_categories" ("product_category_id") ` ) await queryRunner.query( ` CREATE INDEX "IDX_pcp_product_id" - ON "product_category_product" ("product_id") + ON "product_product_categories" ("product_id") ` ) } @@ -40,6 +40,6 @@ export class productCategoryProduct1674455083104 implements MigrationInterface { await queryRunner.query(`DROP INDEX "IDX_pcp_product_category_id"`) await queryRunner.query(`DROP INDEX "IDX_pcp_product_id"`) - await queryRunner.query(`DROP TABLE "product_category_product"`) + await queryRunner.query(`DROP TABLE "product_product_categories"`) } } diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index ac97fd60a0ac8..c96c2770b68d7 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -54,7 +54,7 @@ export class ProductCategory extends SoftDeletableEntity { // @ManyToMany(() => Product, { cascade: ["remove", "soft-remove"] }) // @JoinTable({ - // name: "product_category_product", + // name: "product_product_categories", // joinColumn: { // name: "product_id", // referencedColumnName: "id", diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts index 14ace44c79fa2..b4949ac58b0b6 100644 --- a/packages/medusa/src/models/product.ts +++ b/packages/medusa/src/models/product.ts @@ -80,13 +80,13 @@ export class Product extends SoftDeletableEntity { @ManyToMany(() => ProductCategory, { cascade: ["remove", "soft-remove"] }) @JoinTable({ - name: "product_category_product", + name: "product_product_categories", joinColumn: { - name: "product_category_id", + name: "product_id", referencedColumnName: "id", }, inverseJoinColumn: { - name: "product_id", + name: "product_category_id", referencedColumnName: "id", }, }) diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index c52cf3eb4abd2..1d46f66998b32 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -438,8 +438,7 @@ console.log("productObject - ", productObject) } } } -console.log("categories - ", categories) -console.log("isDefined(categories) - ", isDefined(categories)) + if (isDefined(categories)) { product.categories = [] @@ -448,9 +447,10 @@ console.log("isDefined(categories) - ", isDefined(categories)) const pcRepo = manager.getCustomRepository(this.productCategoryRepository_) const categoryRecords = await pcRepo.find({ - where: { id: In(categoryIds) } + where: { id: In(categoryIds) }, + select: ["id"], }) -console.log("categoryRecords - ", categoryRecords) + product.categories = categoryRecords } } From abd71a0db3178c45cb85ac916bd4709c164ddb76 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 08:21:49 +0100 Subject: [PATCH 03/13] chore: fix issues --- .../api/__tests__/admin/product.js | 1 - .../admin/products/__tests__/get-product.js | 1 + .../medusa/src/models/product-category.ts | 26 +++++++++---------- packages/medusa/src/services/product.ts | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 5dc877faa6781..5ef8783f5afbc 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -40,7 +40,6 @@ describe("/admin/products", () => { dbConnection = await initDb({ cwd }) medusaProcess = await setupServer({ cwd, - verbose: true }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js index 87a54663ccaf5..de5fc0aa295d5 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js @@ -65,6 +65,7 @@ describe("GET /admin/products/:id", () => { "tags", "type", "collection", + "categories", "sales_channels", ], } diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index c96c2770b68d7..6e3d3617465f9 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -52,19 +52,19 @@ export class ProductCategory extends SoftDeletableEntity { @TreeChildren({ cascade: true }) category_children: ProductCategory[] - // @ManyToMany(() => Product, { cascade: ["remove", "soft-remove"] }) - // @JoinTable({ - // name: "product_product_categories", - // joinColumn: { - // name: "product_id", - // referencedColumnName: "id", - // }, - // inverseJoinColumn: { - // name: "product_category_id", - // referencedColumnName: "id", - // }, - // }) - // products: Product[] + @ManyToMany(() => Product, { cascade: ["remove", "soft-remove"] }) + @JoinTable({ + name: "product_product_categories", + joinColumn: { + name: "product_category_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "product_id", + referencedColumnName: "id", + }, + }) + products: Product[] @BeforeInsert() private beforeInsert(): void { diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 1d46f66998b32..02682f70bafd1 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -391,7 +391,7 @@ class ProductService extends TransactionBaseService { const optionRepo = manager.getCustomRepository( this.productOptionRepository_ ) -console.log("productObject - ", productObject) + const { options, tags, From 1d0e9b96572f44a8e87fc4f421c8e2486f7bc0da Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 08:27:26 +0100 Subject: [PATCH 04/13] chore: fix ordering issue on random failing test --- integration-tests/api/__tests__/admin/product.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 5ef8783f5afbc..0fb52da5b829f 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -1422,7 +1422,7 @@ describe("/admin/products", () => { expect(response.data.product).toEqual( expect.objectContaining({ title: "Test product", - variants: [ + variants: expect.arrayContaining([ expect.objectContaining({ id: "test-variant", title: "Test variant", @@ -1435,7 +1435,7 @@ describe("/admin/products", () => { id: "test-variant_2", title: "Test variant rank (2)", }), - ], + ]), type: null, collection: null, categories: [], From 64daf42ac375d7d24e03112c3568dc8d7d78b784 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 08:28:44 +0100 Subject: [PATCH 05/13] chore: revert table name --- .../1674455083104-product_category_product.ts | 10 +++++----- packages/medusa/src/models/product-category.ts | 2 +- packages/medusa/src/models/product.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/medusa/src/migrations/1674455083104-product_category_product.ts b/packages/medusa/src/migrations/1674455083104-product_category_product.ts index 375135d829b2d..2227cecb6c653 100644 --- a/packages/medusa/src/migrations/1674455083104-product_category_product.ts +++ b/packages/medusa/src/migrations/1674455083104-product_category_product.ts @@ -4,7 +4,7 @@ export class productCategoryProduct1674455083104 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( ` - CREATE TABLE "product_product_categories" ( + CREATE TABLE "product_category_product" ( "product_category_id" character varying NOT NULL, "product_id" character varying NOT NULL, CONSTRAINT "FK_product_category_id" FOREIGN KEY ("product_category_id") REFERENCES product_category("id") ON DELETE CASCADE ON UPDATE NO ACTION, @@ -16,21 +16,21 @@ export class productCategoryProduct1674455083104 implements MigrationInterface { await queryRunner.query( ` CREATE UNIQUE INDEX "IDX_upcp_product_id_product_category_id" - ON "product_product_categories" ("product_category_id", "product_id") + ON "product_category_product" ("product_category_id", "product_id") ` ) await queryRunner.query( ` CREATE INDEX "IDX_pcp_product_category_id" - ON "product_product_categories" ("product_category_id") + ON "product_category_product" ("product_category_id") ` ) await queryRunner.query( ` CREATE INDEX "IDX_pcp_product_id" - ON "product_product_categories" ("product_id") + ON "product_category_product" ("product_id") ` ) } @@ -40,6 +40,6 @@ export class productCategoryProduct1674455083104 implements MigrationInterface { await queryRunner.query(`DROP INDEX "IDX_pcp_product_category_id"`) await queryRunner.query(`DROP INDEX "IDX_pcp_product_id"`) - await queryRunner.query(`DROP TABLE "product_product_categories"`) + await queryRunner.query(`DROP TABLE "product_category_product"`) } } diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index 6e3d3617465f9..11305c6bce706 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -54,7 +54,7 @@ export class ProductCategory extends SoftDeletableEntity { @ManyToMany(() => Product, { cascade: ["remove", "soft-remove"] }) @JoinTable({ - name: "product_product_categories", + name: "product_category_product", joinColumn: { name: "product_category_id", referencedColumnName: "id", diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts index b4949ac58b0b6..18f1582508262 100644 --- a/packages/medusa/src/models/product.ts +++ b/packages/medusa/src/models/product.ts @@ -80,7 +80,7 @@ export class Product extends SoftDeletableEntity { @ManyToMany(() => ProductCategory, { cascade: ["remove", "soft-remove"] }) @JoinTable({ - name: "product_product_categories", + name: "product_category_product", joinColumn: { name: "product_id", referencedColumnName: "id", From 3dc0610d45a244e96a4a8d97eddeaae96d1301b1 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 08:31:13 +0100 Subject: [PATCH 06/13] chore: added oas for category --- packages/medusa/src/models/product-category.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index 11305c6bce706..9db1a75b9f28c 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -122,6 +122,12 @@ export class ProductCategory extends SoftDeletableEntity { * parent_category: * description: A product category object. Available if the relation `parent_category` is expanded. * type: object + * products: + * description: products associated with category. Available if the relation `products` is expanded. + * type: array + * items: + * type: object + * description: A product object. * created_at: * type: string * description: "The date with timezone at which the resource was created." From ca4c146b2fafdcc308b98d7e1dbd5803a99cd62b Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 09:31:55 +0100 Subject: [PATCH 07/13] chore: update categories for a product --- .../api/__tests__/admin/product.js | 65 +++++++++++++++++++ .../simple-product-category-factory.ts | 4 +- .../routes/admin/products/update-product.ts | 7 ++ packages/medusa/src/services/product.ts | 17 ++++- 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 0fb52da5b829f..6780afdd15114 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -7,6 +7,7 @@ const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") const productSeeder = require("../../helpers/product-seeder") const { productCategorySeeder } = require("../../helpers/product-category-seeder") +const { Product } = require("@medusajs/medusa") const { ProductVariant, @@ -19,6 +20,7 @@ const priceListSeeder = require("../../helpers/price-list-seeder") const { simpleProductFactory, simpleDiscountFactory, + simpleProductCategoryFactory, } = require("../../factories") const { DiscountRuleType, AllocationType } = require("@medusajs/medusa/dist") const { IdMap } = require("medusa-test-utils") @@ -1469,6 +1471,69 @@ describe("/admin/products", () => { }) ) }) + + it("updates a product's categories", async () => { + const api = useApi() + const category = await simpleProductCategoryFactory( + dbConnection, + { + id: "existing-category", + name: "existing category", + products: [{ id: "test-product" }] + } + ) + + const product = await dbConnection.manager.findOne( + Product, + { + where: { id: "test-product" }, + relations: ["categories"] + } + ) + + expect(product.categories).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "existing-category", + }) + ]) + ) + + const payload = { + categories: [{ id: "test-category-d2B" }], + } + + const response = await api + .post("/admin/products/test-product", payload, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: "test-product", + handle: "test-product", + categories: expect.arrayContaining([ + expect.objectContaining({ + id: "test-category-d2B", + }), + ]), + }) + ) + }) + + it("throws error if product categories input is incorreect", async () => { + const api = useApi() + const payload = { + categories: [{ incorrect: "test-category-d2B" }], + } + + const error = await api + .post("/admin/products/test-product", payload, adminHeaders) + .catch(e => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.type).toEqual("invalid_data") + expect(error.response.data.message).toEqual("property incorrect should not exist, id must be a string") + }) }) describe("DELETE /admin/products/:id/options/:option_id", () => { diff --git a/integration-tests/api/factories/simple-product-category-factory.ts b/integration-tests/api/factories/simple-product-category-factory.ts index 30a10bbf71a92..fa68f197201af 100644 --- a/integration-tests/api/factories/simple-product-category-factory.ts +++ b/integration-tests/api/factories/simple-product-category-factory.ts @@ -6,7 +6,7 @@ export const simpleProductCategoryFactory = async ( data: Partial = {} ): Promise => { const manager = connection.manager - const address = manager.create(ProductCategory, data) + const category = manager.create(ProductCategory, data) - return await manager.save(address) + return await manager.save(category) } diff --git a/packages/medusa/src/api/routes/admin/products/update-product.ts b/packages/medusa/src/api/routes/admin/products/update-product.ts index 59cfbfc3a649d..3186c17704def 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.ts +++ b/packages/medusa/src/api/routes/admin/products/update-product.ts @@ -17,6 +17,7 @@ import { ProductSalesChannelReq, ProductTagReq, ProductTypeReq, + ProductProductCategoryReq, } from "../../../../types/product" import { Type } from "class-transformer" @@ -459,6 +460,12 @@ export class AdminPostProductsProductReq { ]) sales_channels?: ProductSalesChannelReq[] | null + @IsOptional() + @Type(() => ProductProductCategoryReq) + @ValidateNested({ each: true }) + @IsArray() + categories?: ProductProductCategoryReq[] + @IsOptional() @Type(() => ProductVariantReq) @ValidateNested({ each: true }) diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 02682f70bafd1..20b49be9097d5 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -445,7 +445,6 @@ class ProductService extends TransactionBaseService { if (categories?.length) { const categoryIds = categories?.map((c) => c.id) const pcRepo = manager.getCustomRepository(this.productCategoryRepository_) - const categoryRecords = await pcRepo.find({ where: { id: In(categoryIds) }, select: ["id"], @@ -535,6 +534,7 @@ class ProductService extends TransactionBaseService { tags, type, sales_channels: salesChannels, + categories: categories, ...rest } = update @@ -558,6 +558,21 @@ class ProductService extends TransactionBaseService { product.tags = await productTagRepo.upsertTags(tags) } + if (isDefined(categories)) { + product.categories = [] + + if (categories?.length) { + const categoryIds = categories?.map((c) => c.id) + const pcRepo = manager.getCustomRepository(this.productCategoryRepository_) + const categoryRecords = await pcRepo.find({ + where: { id: In(categoryIds) }, + select: ["id"], + }) + + product.categories = categoryRecords + } + } + if ( this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) ) { From 700740306d5bce150bb287a1aa9d2201d28bb943 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 09:45:27 +0100 Subject: [PATCH 08/13] chore: add remove category test --- .../api/__tests__/admin/product.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 6780afdd15114..0ffef4db8626a 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -1520,6 +1520,33 @@ describe("/admin/products", () => { ) }) + it("remove all categories of a product", async () => { + const api = useApi() + const category = await simpleProductCategoryFactory( + dbConnection, + { + id: "existing-category", + name: "existing category", + products: [{ id: "test-product" }] + } + ) + + const payload = { + categories: [], + } + + const response = await api + .post("/admin/products/test-product", payload, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: "test-product", + categories: [], + }) + ) + }) + it("throws error if product categories input is incorreect", async () => { const api = useApi() const payload = { From dac86d14513b2dcabe2a52670d9cb4113b2c104c Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 13:42:11 +0100 Subject: [PATCH 09/13] chore: added changeset --- .changeset/lazy-frogs-teach.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lazy-frogs-teach.md diff --git a/.changeset/lazy-frogs-teach.md b/.changeset/lazy-frogs-teach.md new file mode 100644 index 0000000000000..95edd6e33f1cb --- /dev/null +++ b/.changeset/lazy-frogs-teach.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): add or remove categories from products From 509b78c2f6dabb0d0ad0acaf6db3d40fa4866513 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 26 Jan 2023 15:15:22 +0100 Subject: [PATCH 10/13] chore: address review comments --- .../admin/__snapshots__/product.js.snap | 9 +- .../api/__tests__/admin/product.js | 205 ++++++++++-------- .../api/helpers/product-category-seeder.ts | 47 ---- .../routes/admin/products/create-product.ts | 10 + .../routes/admin/products/update-product.ts | 10 + packages/medusa/src/services/product.ts | 17 +- 6 files changed, 146 insertions(+), 152 deletions(-) delete mode 100644 integration-tests/api/helpers/product-category-seeder.ts diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap index 3b2ccc5ec6ed8..d803fb9ad2bd3 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap @@ -112,14 +112,7 @@ Array [ exports[`/admin/products POST /admin/products creates a product 1`] = ` Object { - "categories": ArrayContaining [ - ObjectContaining { - "id": "test-category-d2A", - }, - ObjectContaining { - "id": "test-category-d2B", - }, - ], + "categories": Array [], "collection": Object { "created_at": Any, "deleted_at": null, diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 0ffef4db8626a..cd17ec513a1a8 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -6,8 +6,7 @@ const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") const productSeeder = require("../../helpers/product-seeder") -const { productCategorySeeder } = require("../../helpers/product-category-seeder") -const { Product } = require("@medusajs/medusa") +const { Product, ProductCategory } = require("@medusajs/medusa") const { ProductVariant, @@ -27,6 +26,7 @@ const { IdMap } = require("medusa-test-utils") jest.setTimeout(50000) +const testProductId = "test-product" const adminHeaders = { headers: { Authorization: "Bearer test_token", @@ -904,7 +904,6 @@ describe("/admin/products", () => { describe("POST /admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) - await productCategorySeeder(dbConnection) await adminSeeder(dbConnection) }) @@ -922,7 +921,6 @@ describe("/admin/products", () => { type: { value: "test-type" }, images: ["test-image.png", "test-image-2.png"], collection_id: "test-collection", - categories: [{ id: "test-category-d2B" }, { id: "test-category-d2A" }], tags: [{ value: "123" }, { value: "456" }], options: [{ title: "size" }, { title: "color" }], variants: [ @@ -1074,14 +1072,6 @@ describe("/admin/products", () => { ], }, ], - categories: expect.arrayContaining([ - expect.objectContaining({ - id: "test-category-d2A", - }), - expect.objectContaining({ - id: "test-category-d2B", - }), - ]), }) }) @@ -1424,7 +1414,7 @@ describe("/admin/products", () => { expect(response.data.product).toEqual( expect.objectContaining({ title: "Test product", - variants: expect.arrayContaining([ + variants: [ expect.objectContaining({ id: "test-variant", title: "Test variant", @@ -1437,7 +1427,7 @@ describe("/admin/products", () => { id: "test-variant_2", title: "Test variant rank (2)", }), - ]), + ], type: null, collection: null, categories: [], @@ -1472,94 +1462,139 @@ describe("/admin/products", () => { ) }) - it("updates a product's categories", async () => { - const api = useApi() - const category = await simpleProductCategoryFactory( - dbConnection, - { - id: "existing-category", - name: "existing category", - products: [{ id: "test-product" }] - } - ) + describe("Categories", () => { + let categoryWithProduct, categoryWithoutProduct + const categoryWithProductId = "category-with-product-id" + const categoryWithoutProductId = "category-without-product-id" - const product = await dbConnection.manager.findOne( - Product, - { - where: { id: "test-product" }, - relations: ["categories"] + beforeEach(async () => { + const manager = dbConnection.manager + categoryWithProduct = await manager.create(ProductCategory, { + id: categoryWithProductId, + name: "category with Product", + products: [{ id: testProductId }], + }) + await manager.save(categoryWithProduct) + + categoryWithoutProduct = await manager.create(ProductCategory, { + id: categoryWithoutProductId, + name: "category without product", + }) + await manager.save(categoryWithoutProduct) + }) + + it("creates a product with categories associated to it", async () => { + const api = useApi() + + const payload = { + title: "Test", + description: "test-product-description", + categories: [{ id: categoryWithProductId }, { id: categoryWithoutProductId }] } - ) - expect(product.categories).toEqual( - expect.arrayContaining([ + const response = await api + .post("/admin/products", payload, adminHeaders) + .catch(e => e) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( expect.objectContaining({ - id: "existing-category", + categories: [ + expect.objectContaining({ + id: categoryWithProductId, + }), + expect.objectContaining({ + id: categoryWithoutProductId, + }), + ], }) - ]) - ) + ) + }) - const payload = { - categories: [{ id: "test-category-d2B" }], - } + it("throws error when creating a product with invalid category ID", async () => { + const api = useApi() + const categoryNotFoundId = "category-doesnt-exist" - const response = await api - .post("/admin/products/test-product", payload, adminHeaders) + const payload = { + title: "Test", + description: "test-product-description", + categories: [{ id: categoryNotFoundId }] + } - expect(response.status).toEqual(200) - expect(response.data.product).toEqual( - expect.objectContaining({ - id: "test-product", - handle: "test-product", - categories: expect.arrayContaining([ - expect.objectContaining({ - id: "test-category-d2B", - }), - ]), - }) - ) - }) + const error = await api + .post("/admin/products", payload, adminHeaders) + .catch(e => e) - it("remove all categories of a product", async () => { - const api = useApi() - const category = await simpleProductCategoryFactory( - dbConnection, - { - id: "existing-category", - name: "existing category", - products: [{ id: "test-product" }] + expect(error.response.status).toEqual(404) + expect(error.response.data.type).toEqual("not_found") + expect(error.response.data.message).toEqual(`Product_category with product_category_id ${categoryNotFoundId} does not exist.`) + }) + + it("updates a product's categories", async () => { + const api = useApi() + + const payload = { + categories: [{ id: categoryWithoutProductId }], } - ) - const payload = { - categories: [], - } + const response = await api + .post(`/admin/products/${testProductId}`, payload, adminHeaders) - const response = await api - .post("/admin/products/test-product", payload, adminHeaders) + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: testProductId, + handle: "test-product", + categories: [ + expect.objectContaining({ + id: categoryWithoutProductId, + }), + ], + }) + ) + }) - expect(response.status).toEqual(200) - expect(response.data.product).toEqual( - expect.objectContaining({ - id: "test-product", + it("remove all categories of a product", async () => { + const api = useApi() + const category = await simpleProductCategoryFactory( + dbConnection, + { + id: "existing-category", + name: "existing category", + products: [{ id: "test-product" }] + } + ) + + const payload = { categories: [], - }) - ) - }) + } - it("throws error if product categories input is incorreect", async () => { - const api = useApi() - const payload = { - categories: [{ incorrect: "test-category-d2B" }], - } + const response = await api + .post("/admin/products/test-product", payload, adminHeaders) - const error = await api - .post("/admin/products/test-product", payload, adminHeaders) - .catch(e => e) + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: "test-product", + categories: [], + }) + ) + }) + + it("throws error if product categories input is incorreect", async () => { + const api = useApi() + const payload = { + categories: [{ incorrect: "test-category-d2B" }], + } - expect(error.response.status).toEqual(400) - expect(error.response.data.type).toEqual("invalid_data") - expect(error.response.data.message).toEqual("property incorrect should not exist, id must be a string") + const error = await api + .post("/admin/products/test-product", payload, adminHeaders) + .catch(e => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.type).toEqual("invalid_data") + expect(error.response.data.message).toEqual("property incorrect should not exist, id must be a string") + }) }) }) diff --git a/integration-tests/api/helpers/product-category-seeder.ts b/integration-tests/api/helpers/product-category-seeder.ts deleted file mode 100644 index 4ab6946b0e24c..0000000000000 --- a/integration-tests/api/helpers/product-category-seeder.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ProductCategory } from "@medusajs/medusa" - -export async function productCategorySeeder( - connection, - data = {}, -) { - const manager = connection.manager - - const categoryD0A = await manager.create(ProductCategory, { - id: "test-category-d0", - name: "test category d0", - }) - - await manager.save(categoryD0A) - - const categoryD1A = await manager.create(ProductCategory, { - id: "test-category-d1A", - name: "test category d1A", - parent_category: categoryD0A - }) - - await manager.save(categoryD1A) - - const categoryD2A = await manager.create(ProductCategory, { - id: "test-category-d2A", - name: "test category d2A", - parent_category: categoryD1A - }) - - await manager.save(categoryD2A) - - const categoryD1B = await manager.create(ProductCategory, { - id: "test-category-d1B", - name: "test category d1B", - parent_category: categoryD0A - }) - - await manager.save(categoryD1B) - - const categoryD2B = await manager.create(ProductCategory, { - id: "test-category-d2B", - name: "test category d2B", - parent_category: categoryD1B - }) - - await manager.save(categoryD2B) -} diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index 58929846ea6a4..67b2ed752c037 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -341,6 +341,16 @@ class ProductVariantReq { * id: * description: The ID of an existing Sales channel. * type: string + * categories: + * description: "Categories to add the Product to." + * type: array + * items: + * required: + * - id + * properties: + * id: + * description: The ID of a Product Category. + * type: string * options: * description: The Options that the Product should have. These define on which properties the Product's Product Variants will differ. * type: array diff --git a/packages/medusa/src/api/routes/admin/products/update-product.ts b/packages/medusa/src/api/routes/admin/products/update-product.ts index 3186c17704def..31417375670ae 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.ts +++ b/packages/medusa/src/api/routes/admin/products/update-product.ts @@ -279,6 +279,16 @@ class ProductVariantReq { * id: * description: The ID of an existing Sales channel. * type: string + * categories: + * description: "Categories to add the Product to." + * type: array + * items: + * required: + * - id + * properties: + * id: + * description: The ID of a Product Category. + * type: string * variants: * description: A list of Product Variants to create with the Product. * type: array diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 20b49be9097d5..38374a5713750 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -12,6 +12,7 @@ import { ProductType, ProductVariant, SalesChannel, + ProductCategory, } from "../models" import { ImageRepository } from "../repositories/image" import { @@ -443,12 +444,8 @@ class ProductService extends TransactionBaseService { product.categories = [] if (categories?.length) { - const categoryIds = categories?.map((c) => c.id) - const pcRepo = manager.getCustomRepository(this.productCategoryRepository_) - const categoryRecords = await pcRepo.find({ - where: { id: In(categoryIds) }, - select: ["id"], - }) + const categoryIds = categories.map((c) => c.id) + const categoryRecords = categoryIds.map((id) => ({ id } as ProductCategory)) product.categories = categoryRecords } @@ -562,12 +559,8 @@ class ProductService extends TransactionBaseService { product.categories = [] if (categories?.length) { - const categoryIds = categories?.map((c) => c.id) - const pcRepo = manager.getCustomRepository(this.productCategoryRepository_) - const categoryRecords = await pcRepo.find({ - where: { id: In(categoryIds) }, - select: ["id"], - }) + const categoryIds = categories.map((c) => c.id) + const categoryRecords = categoryIds.map((id) => ({ id } as ProductCategory)) product.categories = categoryRecords } From 7deb61f3651ec80215702768cfb243e9b4b30ee7 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 27 Jan 2023 08:22:21 +0100 Subject: [PATCH 11/13] chore: Products can be added to categories in batch request --- .changeset/tidy-students-kiss.md | 5 + .../api/__tests__/admin/product-category.ts | 111 ++++++++++++++++- .../product-categories/add-products-batch.ts | 116 ++++++++++++++++++ .../routes/admin/product-categories/index.ts | 59 ++++++--- .../medusa/src/models/product-category.ts | 3 +- .../src/repositories/product-category.ts | 17 +++ .../medusa/src/services/product-category.ts | 21 ++++ packages/medusa/src/types/product-category.ts | 5 + 8 files changed, 315 insertions(+), 22 deletions(-) create mode 100644 .changeset/tidy-students-kiss.md create mode 100644 packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts diff --git a/.changeset/tidy-students-kiss.md b/.changeset/tidy-students-kiss.md new file mode 100644 index 0000000000000..5496dbd8daa44 --- /dev/null +++ b/.changeset/tidy-students-kiss.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Products can be added to categories in batch request diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index 4b6659f4f2c1d..d43e9209276a8 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -1,10 +1,15 @@ import path from "path" +import { Product } from "@medusajs/medusa" +import { In } from "typeorm" import startServerWithEnvironment from "../../../helpers/start-server-with-environment" import { useApi } from "../../../helpers/use-api" import { useDb } from "../../../helpers/use-db" import adminSeeder from "../../helpers/admin-seeder" -import { simpleProductCategoryFactory } from "../../factories" +import { + simpleProductCategoryFactory, + simpleProductFactory, +} from "../../factories" jest.setTimeout(30000) @@ -27,7 +32,7 @@ describe("/admin/product-categories", () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) const [process, connection] = await startServerWithEnvironment({ cwd, - env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }, + env: { MEDUSA_FF_PRODUCT_CATEGORIES: true } }) dbConnection = connection medusaProcess = process @@ -462,4 +467,106 @@ describe("/admin/product-categories", () => { ) }) }) + + describe("POST /admin/product-categories/:id/products/batch", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + productCategory = await simpleProductCategoryFactory(dbConnection, { + id: "test-category", + name: "test category", + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should add products to a product category", async () => { + const api = useApi() + const testProduct1 = await simpleProductFactory(dbConnection, { + id: "test-product-1", + title: "test product 1", + }) + + const testProduct2 = await simpleProductFactory(dbConnection, { + id: "test-product-2", + title: "test product 2", + }) + + const payload = { + product_ids: [{ id: testProduct1.id }, { id: testProduct2.id }], + } + + const response = await api.post( + `/admin/product-categories/${productCategory.id}/products/batch`, + payload, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product_category).toEqual( + expect.objectContaining({ + id: productCategory.id, + created_at: expect.any(String), + updated_at: expect.any(String), + }) + ) + + const products = await dbConnection.manager.find(Product, { + where: { id: In([testProduct1.id, testProduct2.id]) }, + relations: ["categories"], + }) + + expect(products[0].categories).toEqual([ + expect.objectContaining({ + id: productCategory.id + }) + ]) + + expect(products[1].categories).toEqual([ + expect.objectContaining({ + id: productCategory.id + }) + ]) + }) + + it("throws error when product ID is invalid", async () => { + const api = useApi() + + const payload = { + product_ids: [{ id: "product-id-invalid" }], + } + + const error = await api.post( + `/admin/product-categories/${productCategory.id}/products/batch`, + payload, + adminHeaders + ).catch(e => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data).toEqual({ + errors: ["Products product-id-invalid do not exist"], + message: "Provided request body contains errors. Please check the data and retry the request" + }) + }) + + it("throws error when category ID is invalid", async () => { + const api = useApi() + const payload = { product_ids: [] } + + const error = await api.post( + `/admin/product-categories/invalid-category-id/products/batch`, + payload, + adminHeaders + ).catch(e => e) + + expect(error.response.status).toEqual(404) + expect(error.response.data).toEqual({ + message: "ProductCategory with id: invalid-category-id was not found", + type: "not_found", + }) + }) + }) }) diff --git a/packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts b/packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts new file mode 100644 index 0000000000000..378ec5f16dd12 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts @@ -0,0 +1,116 @@ +import { IsArray, ValidateNested } from "class-validator" +import { Request, Response } from "express" + +import { EntityManager } from "typeorm" +import { ProductBatchProductCategory } from "../../../../types/product-category" +import { ProductCategoryService } from "../../../../services" +import { Type } from "class-transformer" +import { FindParams } from "../../../../types/common" + +/** + * @oas [post] /product-categories/{id}/products/batch + * operationId: "PostProductCategoriesCategoryProductsBatch" + * summary: "Add Products to a category" + * description: "Assign a batch of products to a product category." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Product Category. + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/AdminPostProductCategoriesCategoryProductsBatchReq" + * x-codegen: + * method: addProducts + * x-codeSamples: + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/product-categories/afasf/products/batch' \ + * --header 'Authorization: Bearer {api_token}' \ + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "product_ids": [ + * { + * "id": "{product_id}" + * } + * ] + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Product Category + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * product_category: + * $ref: "#/components/schemas/ProductCategory" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response): Promise => { + const validatedBody = + req.validatedBody as AdminPostProductCategoriesCategoryProductsBatchReq + + const { id } = req.params + + const productCategoryService: ProductCategoryService = req.scope.resolve( + "productCategoryService" + ) + + const manager: EntityManager = req.scope.resolve("manager") + const productCategory = await manager.transaction(async (transactionManager) => { + return await productCategoryService + .withTransaction(transactionManager) + .addProducts( + id, + validatedBody.product_ids.map((p) => p.id), + req.retrieveConfig + ) + }) + + res.status(200).json({ product_category: productCategory }) +} + +/** + * @schema AdminPostProductCategoriesCategoryProductsBatchReq + * type: object + * required: + * - product_ids + * properties: + * product_ids: + * description: The IDs of the products to add to the Product Category + * type: array + * items: + * type: object + * required: + * - id + * properties: + * id: + * type: string + * description: The ID of the product + */ +export class AdminPostProductCategoriesCategoryProductsBatchReq { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ProductBatchProductCategory) + product_ids: ProductBatchProductCategory[] +} + +export class AdminPostProductCategoriesCategoryProductsBatchParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/product-categories/index.ts b/packages/medusa/src/api/routes/admin/product-categories/index.ts index b286989f4b4f3..64f40f5de00ec 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/index.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/index.ts @@ -4,9 +4,12 @@ import middlewares, { transformQuery, transformBody, } from "../../../middlewares" + import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import deleteProductCategory from "./delete-product-category" +import { validateProductsExist } from "../../../middlewares/validators/product-existence" + import getProductCategory, { AdminGetProductCategoryParams, } from "./get-product-category" @@ -25,9 +28,25 @@ import updateProductCategory, { AdminPostProductCategoriesCategoryParams, } from "./update-product-category" +import addProductsBatch, { + AdminPostProductCategoriesCategoryProductsBatchReq, + AdminPostProductCategoriesCategoryProductsBatchParams, +} from "./add-products-batch" + const route = Router() export default (app) => { + const atomicTransformQueryConfig = { + defaultFields: defaultProductCategoryFields, + defaultRelations: defaultAdminProductCategoryRelations, + isList: false, + } + + const listTransformQueryConfig = { + ...atomicTransformQueryConfig, + isList: true, + } + app.use( "/product-categories", isFeatureFlagEnabled("product_categories"), @@ -36,47 +55,49 @@ export default (app) => { route.post( "/", - transformQuery(AdminPostProductCategoriesParams, { - defaultFields: defaultProductCategoryFields, - defaultRelations: defaultAdminProductCategoryRelations, - isList: false, - }), + transformQuery( + AdminPostProductCategoriesParams, + atomicTransformQueryConfig + ), transformBody(AdminPostProductCategoriesReq), middlewares.wrap(createProductCategory) ) route.get( "/", - transformQuery(AdminGetProductCategoriesParams, { - defaultFields: defaultProductCategoryFields, - defaultRelations: defaultAdminProductCategoryRelations, - isList: true, - }), + transformQuery(AdminGetProductCategoriesParams, listTransformQueryConfig), middlewares.wrap(listProductCategories) ) route.get( "/:id", - transformQuery(AdminGetProductCategoryParams, { - defaultFields: defaultProductCategoryFields, - isList: false, - }), + transformQuery(AdminGetProductCategoryParams, atomicTransformQueryConfig), middlewares.wrap(getProductCategory) ) route.post( "/:id", - transformQuery(AdminPostProductCategoriesCategoryParams, { - defaultFields: defaultProductCategoryFields, - defaultRelations: defaultAdminProductCategoryRelations, - isList: false, - }), + transformQuery( + AdminPostProductCategoriesCategoryParams, + atomicTransformQueryConfig + ), transformBody(AdminPostProductCategoriesCategoryReq), middlewares.wrap(updateProductCategory) ) route.delete("/:id", middlewares.wrap(deleteProductCategory)) + route.post( + "/:id/products/batch", + transformQuery( + AdminPostProductCategoriesCategoryProductsBatchParams, + atomicTransformQueryConfig + ), + transformBody(AdminPostProductCategoriesCategoryProductsBatchReq), + validateProductsExist((req) => req.body.product_ids), + middlewares.wrap(addProductsBatch) + ) + return app } diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index 9db1a75b9f28c..811785c1a3100 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -20,6 +20,7 @@ import { @Entity() @Tree("materialized-path") export class ProductCategory extends SoftDeletableEntity { + static productCategoryProductJoinTable = "product_category_product" static treeRelations = ["parent_category", "category_children"] @Column() @@ -54,7 +55,7 @@ export class ProductCategory extends SoftDeletableEntity { @ManyToMany(() => Product, { cascade: ["remove", "soft-remove"] }) @JoinTable({ - name: "product_category_product", + name: ProductCategory.productCategoryProductJoinTable, joinColumn: { name: "product_category_id", referencedColumnName: "id", diff --git a/packages/medusa/src/repositories/product-category.ts b/packages/medusa/src/repositories/product-category.ts index e101f9d934e01..852e69c4eaa57 100644 --- a/packages/medusa/src/repositories/product-category.ts +++ b/packages/medusa/src/repositories/product-category.ts @@ -84,4 +84,21 @@ export class ProductCategoryRepository extends TreeRepository { return await queryBuilder.getManyAndCount() } + + async addProducts( + productCategoryId: string, + productIds: string[] + ): Promise { + await this.createQueryBuilder() + .insert() + .into(ProductCategory.productCategoryProductJoinTable) + .values( + productIds.map((id) => ({ + product_category_id: productCategoryId, + product_id: id, + })) + ) + .orIgnore() + .execute() + } } diff --git a/packages/medusa/src/services/product-category.ts b/packages/medusa/src/services/product-category.ts index d6a7d9b1ff31e..29fc59cd3cc55 100644 --- a/packages/medusa/src/services/product-category.ts +++ b/packages/medusa/src/services/product-category.ts @@ -217,6 +217,27 @@ class ProductCategoryService extends TransactionBaseService { }) }) } + + /** + * Add a batch of product to a product category + * @param productCategoryId - The id of the product category on which to add the products + * @param productIds - The products ids to attach to the product category + * @return the product category on which the products have been added + */ + async addProducts( + productCategoryId: string, + productIds: string[], + config: FindConfig = {}, + ): Promise { + return await this.atomicPhase_(async (manager) => { + const productCategoryRepository: ProductCategoryRepository = + manager.getCustomRepository(this.productCategoryRepo_) + + await productCategoryRepository.addProducts(productCategoryId, productIds) + + return await this.retrieve(productCategoryId, config) + }) + } } export default ProductCategoryService diff --git a/packages/medusa/src/types/product-category.ts b/packages/medusa/src/types/product-category.ts index 51f06ec50230a..85e96468d9b9e 100644 --- a/packages/medusa/src/types/product-category.ts +++ b/packages/medusa/src/types/product-category.ts @@ -38,3 +38,8 @@ export class AdminProductCategoriesReqBase { }) parent_category_id?: string | null } + +export class ProductBatchProductCategory { + @IsString() + id: string +} From 4ae4dacaa6e2343a142199a06d568f091ce13a1e Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 27 Jan 2023 14:25:33 +0100 Subject: [PATCH 12/13] chore: address review comments + add unit specs --- .../api/__tests__/admin/product-category.ts | 17 ++++++++++ .../validators/product-existence.ts | 30 +++++++++-------- .../product-categories/add-products-batch.ts | 13 ++++++-- .../routes/admin/product-categories/index.ts | 24 +++++++++----- .../api/routes/admin/sales-channels/index.ts | 3 +- .../services/__tests__/product-category.ts | 33 +++++++++++++++++++ .../medusa/src/services/product-category.ts | 9 ++--- 7 files changed, 98 insertions(+), 31 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index d43e9209276a8..794ab3a450641 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -568,5 +568,22 @@ describe("/admin/product-categories", () => { type: "not_found", }) }) + + it("throws error trying to expand not allowed relations", async () => { + const api = useApi() + const payload = { product_ids: [] } + + const error = await api.post( + `/admin/product-categories/invalid-category-id/products/batch?expand=products`, + payload, + adminHeaders + ).catch(e => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data).toEqual({ + message: "Relations [products] are not valid", + type: "invalid_data", + }) + }) }) }) diff --git a/packages/medusa/src/api/middlewares/validators/product-existence.ts b/packages/medusa/src/api/middlewares/validators/product-existence.ts index 8e5a04954505d..2ee40a0719c05 100644 --- a/packages/medusa/src/api/middlewares/validators/product-existence.ts +++ b/packages/medusa/src/api/middlewares/validators/product-existence.ts @@ -1,26 +1,30 @@ import { NextFunction, Request, Response } from "express" -import { ProductService } from "../../../services" -import { ProductBatchSalesChannel } from "../../../types/sales-channels" -export function validateProductsExist( - getProducts: (req) => ProductBatchSalesChannel[] | undefined +type GetProductsRequiredParams = { + id: string +} + +export function validateProductsExist( + getProducts: (req) => T[] ): (req: Request, res: Response, next: NextFunction) => Promise { return async (req: Request, res: Response, next: NextFunction) => { - const products = getProducts(req) + const requestedProducts = getProducts(req) - if (!products?.length) { + if (!requestedProducts?.length) { return next() } - const productService: ProductService = req.scope.resolve("productService") - - const productIds = products.map((product) => product.id) - const [existingProducts] = await productService.listAndCount({ - id: productIds, + const productService = req.scope.resolve("productService") + const requestedProductIds = requestedProducts.map((product) => product.id) + const [productRecords] = await productService.listAndCount({ + id: requestedProductIds, }) - const nonExistingProducts = productIds.filter( - (scId) => existingProducts.findIndex((sc) => sc.id === scId) === -1 + const nonExistingProducts = requestedProductIds.filter( + (requestedProductId) => + productRecords.findIndex( + (productRecord) => productRecord.id === requestedProductId + ) === -1 ) if (nonExistingProducts.length) { diff --git a/packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts b/packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts index 378ec5f16dd12..10094aa10ff5b 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/add-products-batch.ts @@ -15,6 +15,8 @@ import { FindParams } from "../../../../types/common" * x-authenticated: true * parameters: * - (path) id=* {string} The ID of the Product Category. + * - (query) expand {string} (Comma separated) Category fields to be expanded in the response. + * - (query) fields {string} (Comma separated) Category fields to be retrieved in the response. * requestBody: * content: * application/json: @@ -26,7 +28,8 @@ import { FindParams } from "../../../../types/common" * - lang: Shell * label: cURL * source: | - * curl --location --request POST 'https://medusa-url.com/admin/product-categories/afasf/products/batch' \ + * curl --location \ + * --request POST 'https://medusa-url.com/admin/product-categories/{product_category_id}/products/batch' \ * --header 'Authorization: Bearer {api_token}' \ * --header 'Content-Type: application/json' \ * --data-raw '{ @@ -75,16 +78,20 @@ export default async (req: Request, res: Response): Promise => { ) const manager: EntityManager = req.scope.resolve("manager") - const productCategory = await manager.transaction(async (transactionManager) => { + await manager.transaction(async (transactionManager) => { return await productCategoryService .withTransaction(transactionManager) .addProducts( id, validatedBody.product_ids.map((p) => p.id), - req.retrieveConfig ) }) + const productCategory = await productCategoryService.retrieve( + id, + req.retrieveConfig + ) + res.status(200).json({ product_category: productCategory }) } diff --git a/packages/medusa/src/api/routes/admin/product-categories/index.ts b/packages/medusa/src/api/routes/admin/product-categories/index.ts index 64f40f5de00ec..2225eb93d7cc2 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/index.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/index.ts @@ -7,7 +7,7 @@ import middlewares, { import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import deleteProductCategory from "./delete-product-category" - +import { ProductBatchProductCategory } from "../../../../types/product-category" import { validateProductsExist } from "../../../middlewares/validators/product-existence" import getProductCategory, { @@ -36,14 +36,15 @@ import addProductsBatch, { const route = Router() export default (app) => { - const atomicTransformQueryConfig = { + const retrieveTransformQueryConfig = { defaultFields: defaultProductCategoryFields, defaultRelations: defaultAdminProductCategoryRelations, + allowedRelations: allowedAdminProductCategoryRelations, isList: false, } const listTransformQueryConfig = { - ...atomicTransformQueryConfig, + ...retrieveTransformQueryConfig, isList: true, } @@ -57,7 +58,7 @@ export default (app) => { "/", transformQuery( AdminPostProductCategoriesParams, - atomicTransformQueryConfig + retrieveTransformQueryConfig ), transformBody(AdminPostProductCategoriesReq), middlewares.wrap(createProductCategory) @@ -71,7 +72,7 @@ export default (app) => { route.get( "/:id", - transformQuery(AdminGetProductCategoryParams, atomicTransformQueryConfig), + transformQuery(AdminGetProductCategoryParams, retrieveTransformQueryConfig), middlewares.wrap(getProductCategory) ) @@ -79,7 +80,7 @@ export default (app) => { "/:id", transformQuery( AdminPostProductCategoriesCategoryParams, - atomicTransformQueryConfig + retrieveTransformQueryConfig ), transformBody(AdminPostProductCategoriesCategoryReq), middlewares.wrap(updateProductCategory) @@ -91,10 +92,12 @@ export default (app) => { "/:id/products/batch", transformQuery( AdminPostProductCategoriesCategoryProductsBatchParams, - atomicTransformQueryConfig + retrieveTransformQueryConfig ), transformBody(AdminPostProductCategoriesCategoryProductsBatchReq), - validateProductsExist((req) => req.body.product_ids), + validateProductsExist( + (req) => req.body.product_ids + ), middlewares.wrap(addProductsBatch) ) @@ -111,6 +114,11 @@ export const defaultAdminProductCategoryRelations = [ "category_children", ] +export const allowedAdminProductCategoryRelations = [ + "parent_category", + "category_children", +] + export const defaultProductCategoryFields = [ "id", "name", diff --git a/packages/medusa/src/api/routes/admin/sales-channels/index.ts b/packages/medusa/src/api/routes/admin/sales-channels/index.ts index 0e7e94a548f5f..ef4d33f1928e2 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/index.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/index.ts @@ -2,6 +2,7 @@ import { Router } from "express" import "reflect-metadata" import { SalesChannel } from "../../../../models" import { DeleteResponse, PaginatedResponse } from "../../../../types/common" +import { ProductBatchSalesChannel } from "../../../../types/sales-channels" import middlewares, { transformBody, transformQuery, @@ -63,7 +64,7 @@ export default (app) => { salesChannelRouter.post( "/products/batch", transformBody(AdminPostSalesChannelsChannelProductsBatchReq), - validateProductsExist((req) => req.body.product_ids), + validateProductsExist((req) => req.body.product_ids), middlewares.wrap(require("./add-product-batch").default) ) diff --git a/packages/medusa/src/services/__tests__/product-category.ts b/packages/medusa/src/services/__tests__/product-category.ts index ef850a9368757..a54ce2e8f6d54 100644 --- a/packages/medusa/src/services/__tests__/product-category.ts +++ b/packages/medusa/src/services/__tests__/product-category.ts @@ -288,4 +288,37 @@ describe("ProductCategoryService", () => { ) }) }) + + describe("addProducts", () => { + const productCategoryRepository = { + ...MockRepository(), + addProducts: jest.fn().mockImplementation((id, productIds) => { + return Promise.resolve() + }), + } + + const productCategoryService = new ProductCategoryService({ + manager: MockManager, + productCategoryRepository, + eventBusService, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should add a list of product to a sales channel", async () => { + const result = await productCategoryService.addProducts( + IdMap.getId("product-category-id"), + [IdMap.getId("product-id")] + ) + + expect(result).toBeUndefined() + expect(productCategoryRepository.addProducts).toHaveBeenCalledTimes(1) + expect(productCategoryRepository.addProducts).toHaveBeenCalledWith( + IdMap.getId("product-category-id"), + [IdMap.getId("product-id")] + ) + }) + }) }) diff --git a/packages/medusa/src/services/product-category.ts b/packages/medusa/src/services/product-category.ts index 29fc59cd3cc55..b4e7aa724a0aa 100644 --- a/packages/medusa/src/services/product-category.ts +++ b/packages/medusa/src/services/product-category.ts @@ -59,7 +59,7 @@ class ProductCategoryService extends TransactionBaseService { take: 100, order: { created_at: "DESC" }, }, - treeSelector: QuerySelector = {}, + treeSelector: QuerySelector = {} ): Promise<[ProductCategory[], number]> { const manager = this.transactionManager_ ?? this.manager_ const productCategoryRepo = manager.getCustomRepository( @@ -226,16 +226,13 @@ class ProductCategoryService extends TransactionBaseService { */ async addProducts( productCategoryId: string, - productIds: string[], - config: FindConfig = {}, - ): Promise { + productIds: string[] + ): Promise { return await this.atomicPhase_(async (manager) => { const productCategoryRepository: ProductCategoryRepository = manager.getCustomRepository(this.productCategoryRepo_) await productCategoryRepository.addProducts(productCategoryId, productIds) - - return await this.retrieve(productCategoryId, config) }) } } From 79be9baabc63925e1e33be979a0993f591848a70 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 27 Jan 2023 14:53:13 +0100 Subject: [PATCH 13/13] chore: make template optional --- .../src/api/middlewares/validators/product-existence.ts | 2 +- .../medusa/src/api/routes/admin/product-categories/index.ts | 5 +---- packages/medusa/src/api/routes/admin/sales-channels/index.ts | 3 +-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/medusa/src/api/middlewares/validators/product-existence.ts b/packages/medusa/src/api/middlewares/validators/product-existence.ts index 2ee40a0719c05..a601b581996ca 100644 --- a/packages/medusa/src/api/middlewares/validators/product-existence.ts +++ b/packages/medusa/src/api/middlewares/validators/product-existence.ts @@ -4,7 +4,7 @@ type GetProductsRequiredParams = { id: string } -export function validateProductsExist( +export function validateProductsExist( getProducts: (req) => T[] ): (req: Request, res: Response, next: NextFunction) => Promise { return async (req: Request, res: Response, next: NextFunction) => { diff --git a/packages/medusa/src/api/routes/admin/product-categories/index.ts b/packages/medusa/src/api/routes/admin/product-categories/index.ts index 2225eb93d7cc2..e630dbaf55d33 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/index.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/index.ts @@ -7,7 +7,6 @@ import middlewares, { import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import deleteProductCategory from "./delete-product-category" -import { ProductBatchProductCategory } from "../../../../types/product-category" import { validateProductsExist } from "../../../middlewares/validators/product-existence" import getProductCategory, { @@ -95,9 +94,7 @@ export default (app) => { retrieveTransformQueryConfig ), transformBody(AdminPostProductCategoriesCategoryProductsBatchReq), - validateProductsExist( - (req) => req.body.product_ids - ), + validateProductsExist((req) => req.body.product_ids), middlewares.wrap(addProductsBatch) ) diff --git a/packages/medusa/src/api/routes/admin/sales-channels/index.ts b/packages/medusa/src/api/routes/admin/sales-channels/index.ts index ef4d33f1928e2..0e7e94a548f5f 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/index.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/index.ts @@ -2,7 +2,6 @@ import { Router } from "express" import "reflect-metadata" import { SalesChannel } from "../../../../models" import { DeleteResponse, PaginatedResponse } from "../../../../types/common" -import { ProductBatchSalesChannel } from "../../../../types/sales-channels" import middlewares, { transformBody, transformQuery, @@ -64,7 +63,7 @@ export default (app) => { salesChannelRouter.post( "/products/batch", transformBody(AdminPostSalesChannelsChannelProductsBatchReq), - validateProductsExist((req) => req.body.product_ids), + validateProductsExist((req) => req.body.product_ids), middlewares.wrap(require("./add-product-batch").default) )