From 2bbcf6b919a89a365bd8fdbfeb0573f91a2ac7ae Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 11 Jan 2023 10:40:07 +0530 Subject: [PATCH 1/6] chore: added admin create endpoint for nested categories --- .changeset/hungry-starfishes-count.md | 5 + .../api/__tests__/admin/product-category.ts | 62 +++++++++ .../create-product-category.ts | 125 ++++++++++++++++++ .../routes/admin/product-categories/index.ts | 13 +- .../services/__tests__/product-category.ts | 25 ++++ .../medusa/src/services/product-category.ts | 17 +++ packages/medusa/src/types/product-category.ts | 26 ++++ 7 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 .changeset/hungry-starfishes-count.md create mode 100644 packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts create mode 100644 packages/medusa/src/types/product-category.ts diff --git a/.changeset/hungry-starfishes-count.md b/.changeset/hungry-starfishes-count.md new file mode 100644 index 0000000000000..3c281a24149cf --- /dev/null +++ b/.changeset/hungry-starfishes-count.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): added admin create endpoint for product categories diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index 0dccf11f751e9..8f13e7499f9ac 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -234,6 +234,68 @@ describe("/admin/product-categories", () => { }) }) + describe("POST /admin/product-categories", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + productCategoryParent = await simpleProductCategoryFactory(dbConnection, { + name: "category parent", + handle: "category-parent", + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("throws an error if required fields are missing", async () => { + const api = useApi() + + const error = await api.post( + `/admin/product-categories`, + {}, + adminHeaders + ).catch(e => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.type).toEqual("invalid_data") + expect(error.response.data.message).toEqual( + "name should not be empty, name must be a string, handle should not be empty, handle must be a string" + ) + }) + + it("successfully creates a product category", async () => { + const api = useApi() + + const response = await api.post( + `/admin/product-categories`, + { + name: "test", + handle: "test", + is_internal: true, + parent_category_id: productCategoryParent.id, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data).toEqual( + expect.objectContaining({ + product_category: expect.objectContaining({ + name: "test", + handle: "test", + is_internal: true, + is_active: false, + created_at: expect.any(String), + updated_at: expect.any(String), + parent_category_id: productCategoryParent.id, + }), + }) + ) + }) + }) + describe("DELETE /admin/product-categories/:id", () => { beforeEach(async () => { await adminSeeder(dbConnection) diff --git a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts new file mode 100644 index 0000000000000..27db0bc4b5528 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts @@ -0,0 +1,125 @@ +import { + IsNotEmpty, + IsObject, + IsOptional, + IsString, + IsBoolean, +} from "class-validator" +import { ProductCategoryService } from "../../../../services" +import { Request, Response } from "express" +import { EntityManager } from "typeorm" + +import { AdminProductCategoriesReqBase } from "../../../../types/product-category" + +/** + * @oas [post] /product-categories + * operationId: "PostProductCategories" + * summary: "Create a Product Category" + * description: "Creates a Product Category." + * x-authenticated: true + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/AdminPostProductCategoriesReq" + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.productCategories.create({ + * name: 'Jeans' + * }) + * .then(({ productCategory }) => { + * console.log(productCategory.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/product-categories' \ + * --header 'Authorization: Bearer {api_token}' \ + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "name": "Jeans" + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Product Category + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * productCategory: + * $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) => { + const { validatedBody } = req as { + validatedBody: AdminPostProductCategoriesReq + } + + const productCategoryService: ProductCategoryService = req.scope.resolve( + "productCategoryService" + ) + + const manager: EntityManager = req.scope.resolve("manager") + const created = await manager.transaction(async (transactionManager) => { + return await productCategoryService + .withTransaction(transactionManager) + .create(validatedBody) + }) + + const productCategory = await productCategoryService.retrieve(created.id) + + res.status(200).json({ product_category: productCategory }) +} + +/** + * @schema AdminPostProductCategoriesReq + * type: object + * required: + * - name + * - handle + * properties: + * name: + * type: string + * description: The name to identify the Product Category by. + * handle: + * type: string + * description: An optional handle to be used in slugs, if none is provided we will kebab-case the name. + * is_internal: + * type: boolean + * description: A flag to make product category an internal category for admins + * is_active: + * type: boolean + * description: A flag to make product category visible/hidden in the store front + */ +// eslint-disable-next-line max-len +export class AdminPostProductCategoriesReq extends AdminProductCategoriesReqBase { + @IsString() + @IsNotEmpty() + name: string + + @IsString() + @IsNotEmpty() + handle: string +} 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 fc8f1f4930db3..c5615fd026eac 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/index.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/index.ts @@ -1,6 +1,6 @@ import { Router } from "express" -import middlewares, { transformQuery } from "../../../middlewares" +import middlewares, { transformQuery, transformBody } from "../../../middlewares" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import deleteProductCategory from "./delete-product-category" @@ -12,6 +12,10 @@ import listProductCategories, { AdminGetProductCategoriesParams, } from "./list-product-categories" +import createProductCategory, { + AdminPostProductCategoriesReq, +} from "./create-product-category" + const route = Router() export default (app) => { @@ -21,6 +25,12 @@ export default (app) => { route ) + route.post( + "/", + transformBody(AdminPostProductCategoriesReq), + middlewares.wrap(createProductCategory) + ) + route.get( "/", transformQuery(AdminGetProductCategoriesParams, { @@ -48,6 +58,7 @@ export default (app) => { export * from "./get-product-category" export * from "./delete-product-category" export * from "./list-product-categories" +export * from "./create-product-category" export const defaultAdminProductCategoryRelations = [ "parent_category", diff --git a/packages/medusa/src/services/__tests__/product-category.ts b/packages/medusa/src/services/__tests__/product-category.ts index 6a97426557c80..2f297e4516196 100644 --- a/packages/medusa/src/services/__tests__/product-category.ts +++ b/packages/medusa/src/services/__tests__/product-category.ts @@ -97,6 +97,31 @@ describe("ProductCategoryService", () => { }) }) + describe("create", () => { + const productCategoryRepository = MockRepository({ + findOne: query => Promise.resolve({ id: IdMap.getId("jeans") }), + }) + + const productCategoryService = new ProductCategoryService({ + manager: MockManager, + productCategoryRepository, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully creates a product collection", async () => { + await productCategoryService.create({ name: "jeans", handle: "jeans" }) + + expect(productCategoryRepository.create).toHaveBeenCalledTimes(1) + expect(productCategoryRepository.create).toHaveBeenCalledWith({ + name: "jeans", + handle: "jeans" + }) + }) + }) + describe("delete", () => { const productCategoryRepository = MockRepository({ findOne: query => { diff --git a/packages/medusa/src/services/product-category.ts b/packages/medusa/src/services/product-category.ts index 92d564763c759..5d72078f890f7 100644 --- a/packages/medusa/src/services/product-category.ts +++ b/packages/medusa/src/services/product-category.ts @@ -5,6 +5,7 @@ import { ProductCategory } from "../models" import { ProductCategoryRepository } from "../repositories/product-category" import { FindConfig, Selector, QuerySelector } from "../types/common" import { buildQuery } from "../utils" +import { CreateProductCategory } from "../types/product-category" type InjectedDependencies = { manager: EntityManager @@ -99,6 +100,22 @@ class ProductCategoryService extends TransactionBaseService { return productCategoryTree } + /** + * Creates a product category + * @param productCategory - params used to create + * @return created product category + */ + async create( + productCategory: CreateProductCategory + ): Promise { + return await this.atomicPhase_(async (manager) => { + const pcRepo = manager.getCustomRepository(this.productCategoryRepo_) + const productCategoryRecord = pcRepo.create(productCategory) + + return await pcRepo.save(productCategoryRecord) + }) + } + /** * Deletes a product category * diff --git a/packages/medusa/src/types/product-category.ts b/packages/medusa/src/types/product-category.ts new file mode 100644 index 0000000000000..d52e9300af8d8 --- /dev/null +++ b/packages/medusa/src/types/product-category.ts @@ -0,0 +1,26 @@ +import { Transform } from "class-transformer" +import { IsNotEmpty, IsOptional, IsString, IsBoolean } from "class-validator" + +export type CreateProductCategory = { + name: string + handle: string + is_internal?: boolean + is_active?: boolean +} + +export class AdminProductCategoriesReqBase { + @IsBoolean() + @IsOptional() + is_internal?: boolean + + @IsBoolean() + @IsOptional() + is_active?: boolean + + @IsString() + @IsOptional() + @Transform(({ value }) => { + return value === "null" ? null : value + }) + parent_category_id?: string | null +} From d8d59f652bdcf6bbb8f4a235d7f3c3f0d27c0852 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 11 Jan 2023 10:45:36 +0530 Subject: [PATCH 2/6] chore: move factory create into test --- .../api/__tests__/admin/product-category.ts | 10 +++++----- .../product-categories/create-product-category.ts | 10 ++-------- .../medusa/src/services/__tests__/product-category.ts | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index 8f13e7499f9ac..213102e69808e 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -237,11 +237,6 @@ describe("/admin/product-categories", () => { describe("POST /admin/product-categories", () => { beforeEach(async () => { await adminSeeder(dbConnection) - - productCategoryParent = await simpleProductCategoryFactory(dbConnection, { - name: "category parent", - handle: "category-parent", - }) }) afterEach(async () => { @@ -266,6 +261,11 @@ describe("/admin/product-categories", () => { }) it("successfully creates a product category", async () => { + productCategoryParent = await simpleProductCategoryFactory(dbConnection, { + name: "category parent", + handle: "category-parent", + }) + const api = useApi() const response = await api.post( diff --git a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts index 27db0bc4b5528..4bb62171e4133 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts @@ -1,14 +1,8 @@ -import { - IsNotEmpty, - IsObject, - IsOptional, - IsString, - IsBoolean, -} from "class-validator" -import { ProductCategoryService } from "../../../../services" +import { IsNotEmpty, IsOptional, IsString, IsBoolean } from "class-validator" import { Request, Response } from "express" import { EntityManager } from "typeorm" +import { ProductCategoryService } from "../../../../services" import { AdminProductCategoriesReqBase } from "../../../../types/product-category" /** diff --git a/packages/medusa/src/services/__tests__/product-category.ts b/packages/medusa/src/services/__tests__/product-category.ts index 2f297e4516196..5a059040d9a2a 100644 --- a/packages/medusa/src/services/__tests__/product-category.ts +++ b/packages/medusa/src/services/__tests__/product-category.ts @@ -111,7 +111,7 @@ describe("ProductCategoryService", () => { jest.clearAllMocks() }) - it("successfully creates a product collection", async () => { + it("successfully creates a product category", async () => { await productCategoryService.create({ name: "jeans", handle: "jeans" }) expect(productCategoryRepository.create).toHaveBeenCalledTimes(1) From 989cdf987de2d68013b9ff25cda262ddcf36c00f Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 11 Jan 2023 17:17:53 +0530 Subject: [PATCH 3/6] chore: address pr review comments --- .../api/__tests__/admin/product-category.ts | 5 ++++- .../create-product-category.ts | 20 ++++++++++++++++--- .../routes/admin/product-categories/index.ts | 8 ++++++++ .../medusa/src/models/product-category.ts | 2 +- .../medusa/src/services/product-category.ts | 10 ++++------ packages/medusa/src/types/product-category.ts | 4 ++-- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index 213102e69808e..f7df7fbb8b1ab 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -289,7 +289,10 @@ describe("/admin/product-categories", () => { is_active: false, created_at: expect.any(String), updated_at: expect.any(String), - parent_category_id: productCategoryParent.id, + parent_category: expect.objectContaining({ + id: productCategoryParent.id + }), + category_children: [] }), }) ) diff --git a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts index 4bb62171e4133..c8812bba0f77d 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts @@ -4,6 +4,7 @@ import { EntityManager } from "typeorm" import { ProductCategoryService } from "../../../../services" import { AdminProductCategoriesReqBase } from "../../../../types/product-category" +import { FindParams } from "../../../../types/common" /** * @oas [post] /product-categories @@ -11,6 +12,9 @@ import { AdminProductCategoriesReqBase } from "../../../../types/product-categor * summary: "Create a Product Category" * description: "Creates a Product Category." * x-authenticated: true + * parameters: + * - (query) expand {string} (Comma separated) Which fields should be expanded in each product category. + * - (query) fields {string} (Comma separated) Which fields should be retrieved in each product category. * requestBody: * content: * application/json: @@ -24,7 +28,8 @@ import { AdminProductCategoriesReqBase } from "../../../../types/product-categor * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) * // must be previously logged in or use api token * medusa.admin.productCategories.create({ - * name: 'Jeans' + * name: 'Jeans', + * handle: 'jeans', * }) * .then(({ productCategory }) => { * console.log(productCategory.id); @@ -36,7 +41,8 @@ import { AdminProductCategoriesReqBase } from "../../../../types/product-categor * --header 'Authorization: Bearer {api_token}' \ * --header 'Content-Type: application/json' \ * --data-raw '{ - * "name": "Jeans" + * "name": "Jeans", +* "handle": "jeans", * }' * security: * - api_token: [] @@ -82,7 +88,10 @@ export default async (req: Request, res: Response) => { .create(validatedBody) }) - const productCategory = await productCategoryService.retrieve(created.id) + const productCategory = await productCategoryService.retrieve( + created.id, + req.retrieveConfig, + ) res.status(200).json({ product_category: productCategory }) } @@ -106,6 +115,9 @@ export default async (req: Request, res: Response) => { * is_active: * type: boolean * description: A flag to make product category visible/hidden in the store front + * parent_category_id: + * type: string + * description: The ID of the parent product category */ // eslint-disable-next-line max-len export class AdminPostProductCategoriesReq extends AdminProductCategoriesReqBase { @@ -117,3 +129,5 @@ export class AdminPostProductCategoriesReq extends AdminProductCategoriesReqBase @IsNotEmpty() handle: string } + +export class AdminPostProductCategoriesParams 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 c5615fd026eac..69e5960f40e75 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/index.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/index.ts @@ -14,6 +14,7 @@ import listProductCategories, { import createProductCategory, { AdminPostProductCategoriesReq, + AdminPostProductCategoriesParams, } from "./create-product-category" const route = Router() @@ -27,6 +28,11 @@ export default (app) => { route.post( "/", + transformQuery(AdminPostProductCategoriesParams, { + defaultFields: defaultProductCategoryFields, + defaultRelations: defaultAdminProductCategoryRelations, + isList: false, + }), transformBody(AdminPostProductCategoriesReq), middlewares.wrap(createProductCategory) ) @@ -71,4 +77,6 @@ export const defaultProductCategoryFields = [ "handle", "is_active", "is_internal", + "created_at", + "updated_at", ] diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index b69cd252932e4..0e9a7e7ad3ba9 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -41,7 +41,7 @@ export class ProductCategory extends SoftDeletableEntity { // Typeorm also keeps track of the category's parent at all times. @Column() - parent_category_id: ProductCategory + parent_category_id: string | null @TreeChildren({ cascade: true }) category_children: ProductCategory[] diff --git a/packages/medusa/src/services/product-category.ts b/packages/medusa/src/services/product-category.ts index 5d72078f890f7..94fe2bf26717b 100644 --- a/packages/medusa/src/services/product-category.ts +++ b/packages/medusa/src/services/product-category.ts @@ -1,11 +1,11 @@ import { isDefined, MedusaError } from "medusa-core-utils" -import { EntityManager } from "typeorm" +import { EntityManager, DeepPartial } from "typeorm" import { TransactionBaseService } from "../interfaces" import { ProductCategory } from "../models" import { ProductCategoryRepository } from "../repositories/product-category" import { FindConfig, Selector, QuerySelector } from "../types/common" import { buildQuery } from "../utils" -import { CreateProductCategory } from "../types/product-category" +import { CreateProductCategoryInput } from "../types/product-category" type InjectedDependencies = { manager: EntityManager @@ -106,7 +106,7 @@ class ProductCategoryService extends TransactionBaseService { * @return created product category */ async create( - productCategory: CreateProductCategory + productCategory: CreateProductCategoryInput ): Promise { return await this.atomicPhase_(async (manager) => { const pcRepo = manager.getCustomRepository(this.productCategoryRepo_) @@ -132,7 +132,7 @@ class ProductCategoryService extends TransactionBaseService { }).catch((err) => void 0) if (!productCategory) { - return Promise.resolve() + return } if (productCategory.category_children.length > 0) { @@ -143,8 +143,6 @@ class ProductCategoryService extends TransactionBaseService { } await productCategoryRepository.delete(productCategory.id) - - return Promise.resolve() }) } } diff --git a/packages/medusa/src/types/product-category.ts b/packages/medusa/src/types/product-category.ts index d52e9300af8d8..555f191a2f037 100644 --- a/packages/medusa/src/types/product-category.ts +++ b/packages/medusa/src/types/product-category.ts @@ -1,11 +1,12 @@ import { Transform } from "class-transformer" import { IsNotEmpty, IsOptional, IsString, IsBoolean } from "class-validator" -export type CreateProductCategory = { +export type CreateProductCategoryInput = { name: string handle: string is_internal?: boolean is_active?: boolean + parent_category_id?: string | null } export class AdminProductCategoriesReqBase { @@ -17,7 +18,6 @@ export class AdminProductCategoriesReqBase { @IsOptional() is_active?: boolean - @IsString() @IsOptional() @Transform(({ value }) => { return value === "null" ? null : value From 107f28d78d860f91cad4352ebbe1bce15542abe1 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 11 Jan 2023 17:27:16 +0530 Subject: [PATCH 4/6] chore: remove optional comment from oas --- .../routes/admin/product-categories/create-product-category.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts index c8812bba0f77d..ee18a0cac3e53 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts @@ -108,7 +108,7 @@ export default async (req: Request, res: Response) => { * description: The name to identify the Product Category by. * handle: * type: string - * description: An optional handle to be used in slugs, if none is provided we will kebab-case the name. + * description: A handle to be used in slugs. * is_internal: * type: boolean * description: A flag to make product category an internal category for admins From 98d2f93eb3b59604e6a472aef36ae9ef5cd806f2 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 11 Jan 2023 17:59:51 +0530 Subject: [PATCH 5/6] chore: make handle optional, create before insert --- integration-tests/api/__tests__/admin/product-category.ts | 2 +- .../admin/product-categories/create-product-category.ts | 7 ------- packages/medusa/src/models/product-category.ts | 6 +++++- .../medusa/src/services/__tests__/product-category.ts | 3 +-- packages/medusa/src/types/product-category.ts | 8 +++++++- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index f7df7fbb8b1ab..9e06a98cfe3ba 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -256,7 +256,7 @@ describe("/admin/product-categories", () => { expect(error.response.status).toEqual(400) expect(error.response.data.type).toEqual("invalid_data") expect(error.response.data.message).toEqual( - "name should not be empty, name must be a string, handle should not be empty, handle must be a string" + "name should not be empty, name must be a string" ) }) diff --git a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts index ee18a0cac3e53..70aa70dcb5558 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts @@ -29,7 +29,6 @@ import { FindParams } from "../../../../types/common" * // must be previously logged in or use api token * medusa.admin.productCategories.create({ * name: 'Jeans', - * handle: 'jeans', * }) * .then(({ productCategory }) => { * console.log(productCategory.id); @@ -42,7 +41,6 @@ import { FindParams } from "../../../../types/common" * --header 'Content-Type: application/json' \ * --data-raw '{ * "name": "Jeans", -* "handle": "jeans", * }' * security: * - api_token: [] @@ -101,7 +99,6 @@ export default async (req: Request, res: Response) => { * type: object * required: * - name - * - handle * properties: * name: * type: string @@ -124,10 +121,6 @@ export class AdminPostProductCategoriesReq extends AdminProductCategoriesReqBase @IsString() @IsNotEmpty() name: string - - @IsString() - @IsNotEmpty() - handle: string } export class AdminPostProductCategoriesParams extends FindParams {} diff --git a/packages/medusa/src/models/product-category.ts b/packages/medusa/src/models/product-category.ts index 0e9a7e7ad3ba9..3155f2c768b72 100644 --- a/packages/medusa/src/models/product-category.ts +++ b/packages/medusa/src/models/product-category.ts @@ -1,5 +1,6 @@ import { generateEntityId } from "../utils/generate-entity-id" import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" +import { kebabCase } from "lodash" import { BeforeInsert, Index, @@ -49,6 +50,10 @@ export class ProductCategory extends SoftDeletableEntity { @BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "pcat") + + if (!this.handle) { + this.handle = kebabCase(this.name) + } } } @@ -60,7 +65,6 @@ export class ProductCategory extends SoftDeletableEntity { * type: object * required: * - name - * - handle * properties: * id: * type: string diff --git a/packages/medusa/src/services/__tests__/product-category.ts b/packages/medusa/src/services/__tests__/product-category.ts index 5a059040d9a2a..03c768efccfdf 100644 --- a/packages/medusa/src/services/__tests__/product-category.ts +++ b/packages/medusa/src/services/__tests__/product-category.ts @@ -112,12 +112,11 @@ describe("ProductCategoryService", () => { }) it("successfully creates a product category", async () => { - await productCategoryService.create({ name: "jeans", handle: "jeans" }) + await productCategoryService.create({ name: "jeans" }) expect(productCategoryRepository.create).toHaveBeenCalledTimes(1) expect(productCategoryRepository.create).toHaveBeenCalledWith({ name: "jeans", - handle: "jeans" }) }) }) diff --git a/packages/medusa/src/types/product-category.ts b/packages/medusa/src/types/product-category.ts index 555f191a2f037..624ed1fea9a03 100644 --- a/packages/medusa/src/types/product-category.ts +++ b/packages/medusa/src/types/product-category.ts @@ -3,13 +3,18 @@ import { IsNotEmpty, IsOptional, IsString, IsBoolean } from "class-validator" export type CreateProductCategoryInput = { name: string - handle: string + handle?: string is_internal?: boolean is_active?: boolean parent_category_id?: string | null } export class AdminProductCategoriesReqBase { + @IsOptional() + @IsString() + @IsNotEmpty() + handle?: string + @IsBoolean() @IsOptional() is_internal?: boolean @@ -18,6 +23,7 @@ export class AdminProductCategoriesReqBase { @IsOptional() is_active?: boolean + @IsString() @IsOptional() @Transform(({ value }) => { return value === "null" ? null : value From 1399cb490029627d89ae0702f960241d0038cf07 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 11 Jan 2023 18:07:30 +0530 Subject: [PATCH 6/6] chore: update oas description for handle --- .../admin/product-categories/create-product-category.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts index 70aa70dcb5558..de9860f9944ea 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/create-product-category.ts @@ -102,10 +102,10 @@ export default async (req: Request, res: Response) => { * properties: * name: * type: string - * description: The name to identify the Product Category by. + * description: The name to identify the Product Category by. * handle: * type: string - * description: A handle to be used in slugs. + * description: An optional handle to be used in slugs, if none is provided we will kebab-case the title. * is_internal: * type: boolean * description: A flag to make product category an internal category for admins