Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(medusa): Nested Categories Admin Update Endpoint #2986

Merged
merged 22 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2bbcf6b
chore: added admin create endpoint for nested categories
riqwan Jan 11, 2023
f0d7e42
Merge branch 'develop' into feat/nested-category-create-endpoint
riqwan Jan 11, 2023
d8d59f6
chore: move factory create into test
riqwan Jan 11, 2023
ff45176
chore: added admin endpoint for update category
riqwan Jan 11, 2023
989cdf9
chore: address pr review comments
riqwan Jan 11, 2023
ada3937
Merge branch 'develop' into feat/nested-category-create-endpoint
riqwan Jan 11, 2023
f3e91e0
chore: merge with latest base
riqwan Jan 11, 2023
107f28d
chore: remove optional comment from oas
riqwan Jan 11, 2023
a23b73e
chore: added trasnform query, minor improvements
riqwan Jan 11, 2023
246fb17
Update packages/medusa/src/services/product-category.ts
riqwan Jan 11, 2023
b2611de
chore: added changeset
riqwan Jan 11, 2023
98d2f93
chore: make handle optional, create before insert
riqwan Jan 11, 2023
9bfcd68
Merge branch 'feat/nested-category-create-endpoint' into feat/nested-…
riqwan Jan 11, 2023
ada69b0
chore: remove optional handle from req type
riqwan Jan 11, 2023
1399cb4
chore: update oas description for handle
riqwan Jan 11, 2023
8595c82
Update packages/medusa/src/api/routes/admin/product-categories/update…
riqwan Jan 11, 2023
3caec20
Update packages/medusa/src/services/product-category.ts
riqwan Jan 11, 2023
48a6bd3
Merge branch 'feat/nested-category-create-endpoint' into feat/nested-…
riqwan Jan 11, 2023
be45fd9
chore: merge develop into branch
riqwan Jan 11, 2023
cbfb248
chore: switch back from merge + oas fix
riqwan Jan 11, 2023
f36e5c9
Merge branch 'develop' into feat/nested-category-update-endpoint
kodiakhq[bot] Jan 11, 2023
ddefcab
Merge branch 'develop' into feat/nested-category-update-endpoint
riqwan Jan 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hungry-starfishes-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

feat(medusa): added admin create endpoint for product categories
5 changes: 5 additions & 0 deletions .changeset/ninety-hairs-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

feat(medusa): added admin endpoint to update product categories
157 changes: 157 additions & 0 deletions integration-tests/api/__tests__/admin/product-category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("/admin/product-categories", () => {
let medusaProcess
let dbConnection
let productCategory = null
let productCategory2 = null
let productCategoryChild = null
let productCategoryParent = null
let productCategoryChild2 = null
Expand Down Expand Up @@ -234,6 +235,71 @@ describe("/admin/product-categories", () => {
})
})

describe("POST /admin/product-categories", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
})

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 () => {
productCategoryParent = await simpleProductCategoryFactory(dbConnection, {
name: "category parent",
handle: "category-parent",
})

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: expect.objectContaining({
id: productCategoryParent.id
}),
category_children: []
}),
})
)
})
})

describe("DELETE /admin/product-categories/:id", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
Expand Down Expand Up @@ -305,4 +371,95 @@ describe("/admin/product-categories", () => {
expect(errorFetchingDeleted.response.status).toEqual(404)
})
})

describe("POST /admin/product-categories/:id", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)

productCategory = await simpleProductCategoryFactory(dbConnection, {
name: "skinny jeans",
handle: "skinny-jeans",
})

productCategory2 = await simpleProductCategoryFactory(dbConnection, {
name: "sweater",
handle: "sweater",
})
})

afterEach(async () => {
const db = useDb()
return await db.teardown()
})

it("throws an error if invalid ID is sent", async () => {
const api = useApi()

const error = await api.post(
`/admin/product-categories/not-found-id`,
{
name: 'testing'
},
adminHeaders
).catch(e => e)

expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual(
"ProductCategory with id: not-found-id was not found"
)
})

it("throws an error if invalid attribute is sent", async () => {
const api = useApi()

const error = await api.post(
`/admin/product-categories/${productCategory.id}`,
{
invalid_property: 'string'
},
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 invalid_property should not exist"
)
})

it("successfully updates a product category", async () => {
const api = useApi()

const response = await api.post(
`/admin/product-categories/${productCategory.id}`,
{
name: "test",
handle: "test",
is_internal: true,
is_active: true,
parent_category_id: productCategory2.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: true,
created_at: expect.any(String),
updated_at: expect.any(String),
parent_category: expect.objectContaining({
id: productCategory2.id,
}),
category_children: []
}),
})
)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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"
import { FindParams } from "../../../../types/common"

/**
* @oas [post] /product-categories
* operationId: "PostProductCategories"
* 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:
* 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',
* handle: '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",
* "handle": "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,
req.retrieveConfig,
)

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
* parent_category_id:
* type: string
* description: The ID of the parent product category
*/
// eslint-disable-next-line max-len
export class AdminPostProductCategoriesReq extends AdminProductCategoriesReqBase {
@IsString()
@IsNotEmpty()
name: string

@IsString()
@IsNotEmpty()
handle: string
}

export class AdminPostProductCategoriesParams extends FindParams {}
Loading