From 4156a92b5e637513e9902dd143635da3eda7e384 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Fri, 26 Apr 2024 12:26:33 +0200 Subject: [PATCH] feat: Add category store endpoints --- .../api/__tests__/store/product-category.ts | 646 +++++++++--------- .../admin/product-categories/query-config.ts | 2 +- packages/medusa/src/api-v2/middlewares.ts | 2 + .../store/product-categories/[id]/route.ts | 28 + .../store/product-categories/helpers.ts | 38 ++ .../store/product-categories/middlewares.ts | 33 + .../store/product-categories/query-config.ts | 24 + .../api-v2/store/product-categories/route.ts | 35 + .../store/product-categories/validators.ts | 52 ++ .../product/src/models/product-category.ts | 1 + .../types/src/http/product-category/admin.ts | 16 + .../src/http/product-category/admin/index.ts | 1 - .../admin/product-category.ts | 34 - .../types/src/http/product-category/common.ts | 18 + .../types/src/http/product-category/index.ts | 1 + .../types/src/http/product-category/store.ts | 16 + 16 files changed, 593 insertions(+), 354 deletions(-) create mode 100644 packages/medusa/src/api-v2/store/product-categories/[id]/route.ts create mode 100644 packages/medusa/src/api-v2/store/product-categories/helpers.ts create mode 100644 packages/medusa/src/api-v2/store/product-categories/middlewares.ts create mode 100644 packages/medusa/src/api-v2/store/product-categories/query-config.ts create mode 100644 packages/medusa/src/api-v2/store/product-categories/route.ts create mode 100644 packages/medusa/src/api-v2/store/product-categories/validators.ts create mode 100644 packages/types/src/http/product-category/admin.ts delete mode 100644 packages/types/src/http/product-category/admin/index.ts delete mode 100644 packages/types/src/http/product-category/admin/product-category.ts create mode 100644 packages/types/src/http/product-category/common.ts create mode 100644 packages/types/src/http/product-category/store.ts diff --git a/integration-tests/api/__tests__/store/product-category.ts b/integration-tests/api/__tests__/store/product-category.ts index 40f6b21afe06..6be092bf35c4 100644 --- a/integration-tests/api/__tests__/store/product-category.ts +++ b/integration-tests/api/__tests__/store/product-category.ts @@ -1,358 +1,368 @@ -import {ProductCategory} from "@medusajs/medusa" -import path from "path" - -import startServerWithEnvironment - from "../../../environment-helpers/start-server-with-environment" -import {useApi} from "../../../environment-helpers/use-api" -import {useDb} from "../../../environment-helpers/use-db" -import {simpleProductCategoryFactory} from "../../../factories" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../helpers/create-admin-user" +import { breaking } from "../../../helpers/breaking" jest.setTimeout(30000) -describe("/store/product-categories", () => { - let medusaProcess - let dbConnection - let productCategory!: ProductCategory - let productCategory2!: ProductCategory - let productCategoryChild!: ProductCategory - let productCategoryParent!: ProductCategory - let productCategoryChild2!: ProductCategory - let productCategoryChild3!: ProductCategory - let productCategoryChild4!: ProductCategory - - beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) - const [process, connection] = await startServerWithEnvironment({ - cwd, - env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }, - }) - dbConnection = connection - medusaProcess = process - }) - - afterAll(async () => { - const db = useDb() - await db.shutdown() - - medusaProcess.kill() - }) - - beforeEach(async () => { - productCategoryParent = await simpleProductCategoryFactory(dbConnection, { - name: "category parent", - description: "test description", - is_active: true, - is_internal: false, - rank: 0, - }) - - productCategory = await simpleProductCategoryFactory(dbConnection, { - name: "category", - parent_category: productCategoryParent, - is_active: true, - rank: 0, - }) - - productCategoryChild = await simpleProductCategoryFactory(dbConnection, { - name: "category child", - parent_category: productCategory, - is_active: true, - is_internal: false, - rank: 3, - }) - - productCategoryChild2 = await simpleProductCategoryFactory(dbConnection, { - name: "category child 2", - parent_category: productCategory, - is_internal: true, - is_active: true, - rank: 0, +medusaIntegrationTestRunner({ + env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }, + testSuite: ({ dbConnection, getContainer, api }) => { + let productCategoryParent + let productCategory + let productCategoryChild + let productCategoryChild2 + let productCategoryChild3 + let productCategoryChild4 + + beforeEach(async () => { + const container = getContainer() + await createAdminUser(dbConnection, adminHeaders, container) + + productCategoryParent = ( + await api.post( + "/admin/product-categories", + { + name: "category parent", + description: "test description", + is_active: true, + is_internal: false, + }, + adminHeaders + ) + ).data.product_category + + productCategory = ( + await api.post( + "/admin/product-categories", + { + name: "category", + parent_category_id: productCategoryParent.id, + is_active: true, + }, + adminHeaders + ) + ).data.product_category + + // The order in which the children are created is intentional as in v1 there was no way to explicitly set the rank. + productCategoryChild2 = ( + await api.post( + "/admin/product-categories", + { + name: "category child 2", + parent_category_id: productCategory.id, + is_internal: true, + is_active: true, + }, + adminHeaders + ) + ).data.product_category + + productCategoryChild3 = ( + await api.post( + "/admin/product-categories", + { + name: "category child 3", + parent_category_id: productCategory.id, + is_internal: false, + is_active: false, + }, + adminHeaders + ) + ).data.product_category + + productCategoryChild4 = ( + await api.post( + "/admin/product-categories", + { + name: "category child 4", + parent_category_id: productCategory.id, + is_internal: false, + is_active: true, + }, + adminHeaders + ) + ).data.product_category + + productCategoryChild = ( + await api.post( + "/admin/product-categories", + { + name: "category child", + parent_category_id: productCategory.id, + is_active: true, + is_internal: false, + }, + adminHeaders + ) + ).data.product_category }) - productCategoryChild3 = await simpleProductCategoryFactory(dbConnection, { - name: "category child 3", - parent_category: productCategory, - is_active: false, - is_internal: false, - rank: 1, - }) + describe("/store/product-categories", () => { + describe("GET /store/product-categories/:id", () => { + it("gets product category with children tree and parent", async () => { + const response = await api.get( + `/store/product-categories/${productCategory.id}?${breaking( + () => "fields=handle,name,description", + () => "include_ancestors_tree=true&include_descendants_tree=true" + )}` + ) + + expect(response.data.product_category).toEqual( + expect.objectContaining({ + id: productCategory.id, + handle: productCategory.handle, + name: productCategory.name, + description: "", + parent_category: expect.objectContaining({ + id: productCategoryParent.id, + handle: productCategoryParent.handle, + name: productCategoryParent.name, + description: "test description", + }), + category_children: expect.arrayContaining([ + expect.objectContaining({ + id: productCategoryChild4.id, + handle: productCategoryChild4.handle, + name: productCategoryChild4.name, + }), + expect.objectContaining({ + id: productCategoryChild.id, + handle: productCategoryChild.handle, + name: productCategoryChild.name, + }), + ]), + }) + ) + + expect(response.status).toEqual(200) + }) - productCategoryChild4 = await simpleProductCategoryFactory(dbConnection, { - name: "category child 4", - parent_category: productCategory, - is_active: true, - is_internal: false, - rank: 2, - }) - }) + // TODO: This one is failing since we don't validate allowed fields currently. We should add that as part of our validators + it("throws error on querying not allowed fields", async () => { + const error = await api + .get(`/store/product-categories/${productCategory.id}?fields=mpath`) + .catch((e) => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.type).toEqual("invalid_data") + expect(error.response.data.message).toEqual( + "Requested fields [mpath] are not valid" + ) + }) - describe("GET /store/product-categories/:id", () => { - afterEach(async () => { - const db = useDb() - return await db.teardown() - }) + it("throws error on querying for internal product category", async () => { + const error = await api + .get(`/store/product-categories/${productCategoryChild2.id}`) + .catch((e) => e) + + expect(error.response.status).toEqual(404) + expect(error.response.data.type).toEqual("not_found") + expect(error.response.data.message).toEqual( + breaking( + () => + `ProductCategory with id: ${productCategoryChild2.id}, is_internal: false, is_active: true was not found`, + () => + `Product category with id: ${productCategoryChild2.id} was not found` + ) + ) + }) - it("gets product category with children tree and parent", async () => { - const api = useApi() - - const response = await api.get( - `/store/product-categories/${productCategory.id}?fields=handle,name,description` - ) - - expect(response.data.product_category).toEqual( - expect.objectContaining({ - id: productCategory.id, - handle: productCategory.handle, - name: productCategory.name, - description: "", - parent_category: expect.objectContaining({ - id: productCategoryParent.id, - handle: productCategoryParent.handle, - name: productCategoryParent.name, - description: "test description", - }), - category_children: [ + it("throws error on querying for inactive product category", async () => { + const error = await api + .get(`/store/product-categories/${productCategoryChild3.id}`) + .catch((e) => e) + + expect(error.response.status).toEqual(404) + expect(error.response.data.type).toEqual("not_found") + expect(error.response.data.message).toEqual( + breaking( + () => + `ProductCategory with id: ${productCategoryChild3.id}, is_internal: false, is_active: true was not found`, + () => + `Product category with id: ${productCategoryChild3.id} was not found` + ) + ) + }) + }) + + describe("GET /store/product-categories", () => { + //TODO: The listing results in V2 are unexpected and differ from v1, we need to investigate where the issue is + it("gets list of product category with immediate children and parents", async () => { + const response = await api.get( + `/store/product-categories?limit=10${breaking( + () => "", + () => "&include_ancestors_tree=true&include_descendants_tree=true" + )}` + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(4) + expect(response.data.offset).toEqual(0) + expect(response.data.limit).toEqual(10) + + expect(response.data.product_categories).toEqual([ expect.objectContaining({ - id: productCategoryChild4.id, - handle: productCategoryChild4.handle, - name: productCategoryChild4.name, + id: productCategory.id, + rank: 0, + parent_category: expect.objectContaining({ + id: productCategoryParent.id, + }), + category_children: expect.arrayContaining([ + expect.objectContaining({ + id: productCategoryChild4.id, + rank: 2, + }), + expect.objectContaining({ + id: productCategoryChild.id, + rank: 3, + }), + ]), }), expect.objectContaining({ - id: productCategoryChild.id, - handle: productCategoryChild.handle, - name: productCategoryChild.name, + id: productCategoryParent.id, + parent_category: null, + rank: 0, + category_children: [ + expect.objectContaining({ + id: productCategory.id, + }), + ], }), - ], - }) - ) - - expect(response.status).toEqual(200) - }) - - it("throws error on querying not allowed fields", async () => { - const api = useApi() - - const error = await api - .get(`/store/product-categories/${productCategory.id}?fields=mpath`) - .catch((e) => e) - - expect(error.response.status).toEqual(400) - expect(error.response.data.type).toEqual("invalid_data") - expect(error.response.data.message).toEqual( - "Requested fields [mpath] are not valid" - ) - }) - - it("throws error on querying for internal product category", async () => { - const api = useApi() - - const error = await api - .get(`/store/product-categories/${productCategoryChild2.id}`) - .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: ${productCategoryChild2.id}, is_internal: false, is_active: true was not found` - ) - }) - - it("throws error on querying for inactive product category", async () => { - const api = useApi() - - const error = await api - .get(`/store/product-categories/${productCategoryChild3.id}`) - .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: ${productCategoryChild3.id}, is_internal: false, is_active: true was not found` - ) - }) - }) - - describe("GET /store/product-categories", () => { - afterEach(async () => { - const db = useDb() - return await db.teardown() - }) - - it("gets list of product category with immediate children and parents", async () => { - const api = useApi() - - const response = await api.get(`/store/product-categories?limit=10`) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(4) - expect(response.data.offset).toEqual(0) - expect(response.data.limit).toEqual(10) - - expect(response.data.product_categories).toEqual([ - expect.objectContaining({ - id: productCategory.id, - rank: 0, - parent_category: expect.objectContaining({ - id: productCategoryParent.id, - }), - category_children: [ expect.objectContaining({ id: productCategoryChild4.id, rank: 2, + parent_category: expect.objectContaining({ + id: productCategory.id, + }), + category_children: [], }), expect.objectContaining({ id: productCategoryChild.id, rank: 3, + parent_category: expect.objectContaining({ + id: productCategory.id, + }), + category_children: [], }), - ], - }), - expect.objectContaining({ - id: productCategoryParent.id, - parent_category: null, - rank: 0, - category_children: [ - expect.objectContaining({ - id: productCategory.id, - }), - ], - }), - expect.objectContaining({ - id: productCategoryChild4.id, - rank: 2, - parent_category: expect.objectContaining({ - id: productCategory.id, - }), - category_children: [], - }), - expect.objectContaining({ - id: productCategoryChild.id, - rank: 3, - parent_category: expect.objectContaining({ - id: productCategory.id, - }), - category_children: [], - }), - ]) - }) + ]) + }) + + // TODO: It seems filtering using null doesn't work. + it("gets list of product category with all childrens when include_descendants_tree=true", async () => { + const response = await api.get( + `/store/product-categories?parent_category_id=null&include_descendants_tree=true&limit=10` + ) - it("gets list of product category with all childrens when include_descendants_tree=true", async () => { - const api = useApi() - - const response = await api.get( - `/store/product-categories?parent_category_id=null&include_descendants_tree=true&limit=10` - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.product_categories).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: productCategoryParent.id, - parent_category: null, - rank: 0, - category_children: [ + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.product_categories).toEqual( + expect.arrayContaining([ expect.objectContaining({ - id: productCategory.id, - parent_category_id: productCategoryParent.id, + id: productCategoryParent.id, + parent_category: null, rank: 0, category_children: [ expect.objectContaining({ - id: productCategoryChild4.id, - parent_category_id: productCategory.id, - category_children: [], - rank: 2, - }), - expect.objectContaining({ - id: productCategoryChild.id, - parent_category_id: productCategory.id, - category_children: [], - rank: 3, + id: productCategory.id, + parent_category_id: productCategoryParent.id, + rank: 0, + category_children: [ + expect.objectContaining({ + id: productCategoryChild4.id, + parent_category_id: productCategory.id, + category_children: [], + rank: 2, + }), + expect.objectContaining({ + id: productCategoryChild.id, + parent_category_id: productCategory.id, + category_children: [], + rank: 3, + }), + ], }), ], }), - ], - }), - ]) - ) - }) + ]) + ) + }) - it("throws error when querying not allowed fields", async () => { - const api = useApi() + it("throws error when querying not allowed fields", async () => { + const error = await api + .get(`/store/product-categories?is_internal=true&limit=10`) + .catch((e) => e) - const error = await api - .get(`/store/product-categories?is_internal=true&limit=10`) - .catch((e) => e) + expect(error.response.status).toEqual(400) + expect(error.response.data.type).toEqual("invalid_data") + expect(error.response.data.message).toEqual( + "property is_internal should not exist" + ) + }) - expect(error.response.status).toEqual(400) - expect(error.response.data.type).toEqual("invalid_data") - expect(error.response.data.message).toEqual( - "property is_internal should not exist" - ) - }) + it("filters based on free text on name and handle columns", async () => { + const response = await api.get( + `/store/product-categories?q=category-parent&limit=10` + ) - it("filters based on free text on name and handle columns", async () => { - const api = useApi() + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.product_categories[0].id).toEqual( + productCategoryParent.id + ) + }) - const response = await api.get( - `/store/product-categories?q=category-parent&limit=10` - ) + it("filters based on handle attribute of the data model", async () => { + const response = await api.get( + `/store/product-categories?handle=${productCategory.handle}&limit=10` + ) - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.product_categories[0].id).toEqual( - productCategoryParent.id - ) - }) + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.product_categories[0].id).toEqual( + productCategory.id + ) + }) - it("filters based on handle attribute of the data model", async () => { - const api = useApi() + it("filters based on parent category", async () => { + const response = await api.get( + `/store/product-categories?parent_category_id=${productCategory.id}&limit=10` + ) - const response = await api.get( - `/store/product-categories?handle=${productCategory.handle}&limit=10` - ) + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.product_categories).toEqual([ + expect.objectContaining({ + id: productCategoryChild4.id, + category_children: [], + parent_category: expect.objectContaining({ + id: productCategory.id, + }), + rank: 2, + }), + expect.objectContaining({ + id: productCategoryChild.id, + category_children: [], + parent_category: expect.objectContaining({ + id: productCategory.id, + }), + rank: 3, + }), + ]) - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(1) - expect(response.data.product_categories[0].id).toEqual(productCategory.id) - }) + const nullCategoryResponse = await api + .get(`/store/product-categories?parent_category_id=null`) + .catch((e) => e) - it("filters based on parent category", async () => { - const api = useApi() - - const response = await api.get( - `/store/product-categories?parent_category_id=${productCategory.id}&limit=10` - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(2) - expect(response.data.product_categories).toEqual([ - expect.objectContaining({ - id: productCategoryChild4.id, - category_children: [], - parent_category: expect.objectContaining({ - id: productCategory.id, - }), - rank: 2, - }), - expect.objectContaining({ - id: productCategoryChild.id, - category_children: [], - parent_category: expect.objectContaining({ - id: productCategory.id, - }), - rank: 3, - }), - ]) - - const nullCategoryResponse = await api - .get(`/store/product-categories?parent_category_id=null`) - .catch((e) => e) - - expect(nullCategoryResponse.status).toEqual(200) - expect(nullCategoryResponse.data.count).toEqual(1) - expect(nullCategoryResponse.data.product_categories[0].id).toEqual( - productCategoryParent.id - ) + expect(nullCategoryResponse.status).toEqual(200) + expect(nullCategoryResponse.data.count).toEqual(1) + expect(nullCategoryResponse.data.product_categories[0].id).toEqual( + productCategoryParent.id + ) + }) + }) }) - }) + }, }) diff --git a/packages/medusa/src/api-v2/admin/product-categories/query-config.ts b/packages/medusa/src/api-v2/admin/product-categories/query-config.ts index 75d8470aa03b..5524e119ae25 100644 --- a/packages/medusa/src/api-v2/admin/product-categories/query-config.ts +++ b/packages/medusa/src/api-v2/admin/product-categories/query-config.ts @@ -10,7 +10,7 @@ export const defaults = [ "created_at", "updated_at", "metadata", - + "*parent_category", "*category_children", ] diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index ab3378a917cc..65ceef43c96c 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -37,6 +37,7 @@ import { storeCartRoutesMiddlewares } from "./store/carts/middlewares" import { storeCollectionRoutesMiddlewares } from "./store/collections/middlewares" import { storeCurrencyRoutesMiddlewares } from "./store/currencies/middlewares" import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares" +import { storeProductCategoryRoutesMiddlewares } from "./store/product-categories/middlewares" import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares" export const config: MiddlewaresConfig = { @@ -49,6 +50,7 @@ export const config: MiddlewaresConfig = { ...storeCustomerRoutesMiddlewares, ...storeCartRoutesMiddlewares, ...storeCollectionRoutesMiddlewares, + ...storeProductCategoryRoutesMiddlewares, ...authRoutesMiddlewares, ...adminWorkflowsExecutionsMiddlewares, ...storeRegionRoutesMiddlewares, diff --git a/packages/medusa/src/api-v2/store/product-categories/[id]/route.ts b/packages/medusa/src/api-v2/store/product-categories/[id]/route.ts new file mode 100644 index 000000000000..2816e4ff370b --- /dev/null +++ b/packages/medusa/src/api-v2/store/product-categories/[id]/route.ts @@ -0,0 +1,28 @@ +import { StoreProductCategoryResponse } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" +import { refetchCategory } from "../helpers" +import { StoreProductCategoryParamsType } from "../validators" +import { MedusaError } from "@medusajs/utils" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const category = await refetchCategory( + req.params.id, + req.scope, + req.remoteQueryConfig.fields, + req.filterableFields + ) + + if (!category) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Product category with id: ${req.params.id} was not found` + ) + } + res.json({ product_category: category }) +} diff --git a/packages/medusa/src/api-v2/store/product-categories/helpers.ts b/packages/medusa/src/api-v2/store/product-categories/helpers.ts new file mode 100644 index 000000000000..64bf4afe89e9 --- /dev/null +++ b/packages/medusa/src/api-v2/store/product-categories/helpers.ts @@ -0,0 +1,38 @@ +import { MedusaContainer } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" + +export const refetchCategory = async ( + categoryId: string, + scope: MedusaContainer, + fields: string[], + filterableFields: Record = {} +) => { + const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const queryObject = remoteQueryObjectFromString({ + entryPoint: "product_category", + variables: { + filters: { ...filterableFields, id: categoryId }, + }, + fields: fields, + }) + + const categories = await remoteQuery(queryObject) + return categories[0] +} + +export const applyCategoryFilters = (req, res, next) => { + if (!req.filterableFields) { + req.filterableFields = {} + } + + req.filterableFields = { + ...req.filterableFields, + is_active: true, + is_internal: false, + } + + next() +} diff --git a/packages/medusa/src/api-v2/store/product-categories/middlewares.ts b/packages/medusa/src/api-v2/store/product-categories/middlewares.ts new file mode 100644 index 000000000000..93fdc48bc5dd --- /dev/null +++ b/packages/medusa/src/api-v2/store/product-categories/middlewares.ts @@ -0,0 +1,33 @@ +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { validateAndTransformQuery } from "../../utils/validate-query" +import { applyCategoryFilters } from "./helpers" +import * as QueryConfig from "./query-config" +import { + StoreProductCategoriesParams, + StoreProductCategoryParams, +} from "./validators" + +export const storeProductCategoryRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["GET"], + matcher: "/store/product-categories", + middlewares: [ + validateAndTransformQuery( + StoreProductCategoriesParams, + QueryConfig.listProductCategoryConfig + ), + applyCategoryFilters, + ], + }, + { + method: ["GET"], + matcher: "/store/product-categories/:id", + middlewares: [ + validateAndTransformQuery( + StoreProductCategoryParams, + QueryConfig.retrieveProductCategoryConfig + ), + applyCategoryFilters, + ], + }, +] diff --git a/packages/medusa/src/api-v2/store/product-categories/query-config.ts b/packages/medusa/src/api-v2/store/product-categories/query-config.ts new file mode 100644 index 000000000000..bc49dc30b9c7 --- /dev/null +++ b/packages/medusa/src/api-v2/store/product-categories/query-config.ts @@ -0,0 +1,24 @@ +export const defaults = [ + "id", + "name", + "description", + "handle", + "rank", + "parent_category_id", + "created_at", + "updated_at", + "metadata", + "*parent_category", + "*category_children", +] + +export const retrieveProductCategoryConfig = { + defaults, + isList: false, +} + +export const listProductCategoryConfig = { + defaults, + defaultLimit: 50, + isList: true, +} diff --git a/packages/medusa/src/api-v2/store/product-categories/route.ts b/packages/medusa/src/api-v2/store/product-categories/route.ts new file mode 100644 index 000000000000..9640f7cd6b1a --- /dev/null +++ b/packages/medusa/src/api-v2/store/product-categories/route.ts @@ -0,0 +1,35 @@ +import { StoreProductCategoryListResponse } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" +import { StoreProductCategoriesParamsType } from "./validators" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "product_category", + variables: { + filters: req.filterableFields, + ...req.remoteQueryConfig.pagination, + }, + fields: req.remoteQueryConfig.fields, + }) + + const { rows: product_categories, metadata } = await remoteQuery(queryObject) + + res.json({ + product_categories, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} diff --git a/packages/medusa/src/api-v2/store/product-categories/validators.ts b/packages/medusa/src/api-v2/store/product-categories/validators.ts new file mode 100644 index 000000000000..bb8283774fb7 --- /dev/null +++ b/packages/medusa/src/api-v2/store/product-categories/validators.ts @@ -0,0 +1,52 @@ +import { z } from "zod" +import { optionalBooleanMapper } from "../../../utils/validators/is-boolean" +import { + createFindParams, + createOperatorMap, + createSelectParams, +} from "../../utils/validators" + +export type StoreProductCategoryParamsType = z.infer< + typeof StoreProductCategoryParams +> +export const StoreProductCategoryParams = createSelectParams().merge( + z.object({ + include_ancestors_tree: z.preprocess( + (val: any) => optionalBooleanMapper.get(val?.toLowerCase()), + z.boolean().optional() + ), + include_descendants_tree: z.preprocess( + (val: any) => optionalBooleanMapper.get(val?.toLowerCase()), + z.boolean().optional() + ), + }) +) + +export type StoreProductCategoriesParamsType = z.infer< + typeof StoreProductCategoriesParams +> +export const StoreProductCategoriesParams = createFindParams({ + offset: 0, + limit: 50, +}).merge( + z.object({ + q: z.string().optional(), + id: z.union([z.string(), z.array(z.string())]).optional(), + description: z.union([z.string(), z.array(z.string())]).optional(), + handle: z.union([z.string(), z.array(z.string())]).optional(), + parent_category_id: z.union([z.string(), z.array(z.string())]).optional(), + include_ancestors_tree: z.preprocess( + (val: any) => optionalBooleanMapper.get(val?.toLowerCase()), + z.boolean().optional() + ), + include_descendants_tree: z.preprocess( + (val: any) => optionalBooleanMapper.get(val?.toLowerCase()), + z.boolean().optional() + ), + created_at: createOperatorMap().optional(), + updated_at: createOperatorMap().optional(), + deleted_at: createOperatorMap().optional(), + $and: z.lazy(() => StoreProductCategoriesParams.array()).optional(), + $or: z.lazy(() => StoreProductCategoriesParams.array()).optional(), + }) +) diff --git a/packages/product/src/models/product-category.ts b/packages/product/src/models/product-category.ts index eceed4d89fc6..e7fb24efae37 100644 --- a/packages/product/src/models/product-category.ts +++ b/packages/product/src/models/product-category.ts @@ -55,6 +55,7 @@ class ProductCategory { @Property({ columnType: "text", default: "", nullable: false }) description?: string + @Searchable() @Property({ columnType: "text", nullable: false }) handle?: string diff --git a/packages/types/src/http/product-category/admin.ts b/packages/types/src/http/product-category/admin.ts new file mode 100644 index 000000000000..2cf02ef7da7e --- /dev/null +++ b/packages/types/src/http/product-category/admin.ts @@ -0,0 +1,16 @@ +import { PaginatedResponse } from "../../common" +import { ProductCategoryResponse } from "./common" + +/** + * @experimental + */ +export interface AdminProductCategoryResponse { + product_category: ProductCategoryResponse +} + +/** + * @experimental + */ +export interface AdminProductCategoryListResponse extends PaginatedResponse { + product_categories: ProductCategoryResponse[] +} diff --git a/packages/types/src/http/product-category/admin/index.ts b/packages/types/src/http/product-category/admin/index.ts deleted file mode 100644 index 96720aa4738a..000000000000 --- a/packages/types/src/http/product-category/admin/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./product-category" diff --git a/packages/types/src/http/product-category/admin/product-category.ts b/packages/types/src/http/product-category/admin/product-category.ts deleted file mode 100644 index afc9f12d5a35..000000000000 --- a/packages/types/src/http/product-category/admin/product-category.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PaginatedResponse } from "../../../common" - -/** - * @experimental - */ -interface ProductCategoryResponse { - id: string - name: string - description: string | null - handle: string | null - is_active: boolean - is_internal: boolean - rank: number | null - parent_category_id: string | null - created_at: string | Date - updated_at: string | Date - - parent_category: ProductCategoryResponse - category_children: ProductCategoryResponse[] -} - -/** - * @experimental - */ -export interface AdminProductCategoryResponse { - product_category: ProductCategoryResponse -} - -/** - * @experimental - */ -export interface AdminProductCategoryListResponse extends PaginatedResponse { - product_categories: ProductCategoryResponse[] -} diff --git a/packages/types/src/http/product-category/common.ts b/packages/types/src/http/product-category/common.ts new file mode 100644 index 000000000000..32b2aa425027 --- /dev/null +++ b/packages/types/src/http/product-category/common.ts @@ -0,0 +1,18 @@ +/** + * @experimental + */ +export interface ProductCategoryResponse { + id: string + name: string + description: string | null + handle: string | null + is_active: boolean + is_internal: boolean + rank: number | null + parent_category_id: string | null + created_at: string | Date + updated_at: string | Date + + parent_category: ProductCategoryResponse + category_children: ProductCategoryResponse[] +} diff --git a/packages/types/src/http/product-category/index.ts b/packages/types/src/http/product-category/index.ts index 26b8eb9dadfe..3bd2bd2cc018 100644 --- a/packages/types/src/http/product-category/index.ts +++ b/packages/types/src/http/product-category/index.ts @@ -1 +1,2 @@ export * from "./admin" +export * from "./store" diff --git a/packages/types/src/http/product-category/store.ts b/packages/types/src/http/product-category/store.ts new file mode 100644 index 000000000000..82da2fdec484 --- /dev/null +++ b/packages/types/src/http/product-category/store.ts @@ -0,0 +1,16 @@ +import { PaginatedResponse } from "../../common" +import { ProductCategoryResponse } from "./common" + +/** + * @experimental + */ +export interface StoreProductCategoryResponse { + product_category: ProductCategoryResponse +} + +/** + * @experimental + */ +export interface StoreProductCategoryListResponse extends PaginatedResponse { + product_categories: ProductCategoryResponse[] +}