feat: Add collection and category endpoints to store (#7155)

* feat: Add collection endpoints to store

* feat: Add category store endpoints
This commit is contained in:
Stevche Radevski authored and GitHub committed 2024-05-01 09:54:51 +02:00
1 parent 2c807df99d
commit ec37576dd0
25 files changed
+621 -279

No files matched your search

@@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/store/collections /store/collections lists collections 1`] = `
Object {
"collections": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"handle": "test-collection2",
"id": "test-collection2",
"metadata": null,
"title": "Test collection 2",
"updated_at": Any<String>,
},
Object {
"created_at": Any<String>,
"deleted_at": null,
"handle": "test-collection1",
"id": "test-collection1",
"metadata": null,
"title": "Test collection 1",
"updated_at": Any<String>,
},
Object {
"created_at": Any<String>,
"deleted_at": null,
"handle": "test-collection",
"id": "test-collection",
"metadata": null,
"title": "Test collection",
"updated_at": Any<String>,
},
],
"count": 3,
"limit": 10,
"offset": 0,
}
`;
exports[`/store/collections /store/collections/:id gets collection 1`] = `
Object {
"collection": Object {
"created_at": Any<String>,
"deleted_at": null,
"handle": "test-collection",
"id": "test-collection",
"metadata": null,
"title": "Test collection",
"updated_at": Any<String>,
},
}
`;
@@ -1,90 +0,0 @@
const { ProductCollection } = require("@medusajs/medusa")
const path = require("path")
const setupServer = require("../../../environment-helpers/setup-server")
const { useApi } = require("../../../environment-helpers/use-api")
const { initDb, useDb } = require("../../../environment-helpers/use-db")
const productSeeder = require("../../../helpers/product-seeder")
jest.setTimeout(30000)
describe("/store/collections", () => {
let medusaProcess
let dbConnection
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd })
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
medusaProcess.kill()
})
describe("/store/collections/:id", () => {
beforeEach(async () => {
await productSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("gets collection", async () => {
const api = useApi()
const response = await api.get("/store/collections/test-collection")
expect(response.data).toMatchSnapshot({
collection: {
id: "test-collection",
created_at: expect.any(String),
updated_at: expect.any(String),
},
})
})
})
describe("/store/collections", () => {
beforeEach(async () => {
await productSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("lists collections", async () => {
const api = useApi()
const response = await api.get("/store/collections")
expect(response.data).toMatchSnapshot({
collections: [
{
id: "test-collection2",
created_at: expect.any(String),
updated_at: expect.any(String),
},
{
id: "test-collection1",
created_at: expect.any(String),
updated_at: expect.any(String),
},
{
id: "test-collection",
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
count: 3,
limit: 10,
offset: 0,
})
})
})
})
@@ -0,0 +1,92 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import {
createAdminUser,
adminHeaders,
} from "../../../helpers/create-admin-user"
jest.setTimeout(30000)
medusaIntegrationTestRunner({
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
testSuite: ({ dbConnection, getContainer, api }) => {
let baseCollection
let baseCollection1
let baseCollection2
beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
baseCollection = (
await api.post(
"/admin/collections",
{ title: "test-collection" },
adminHeaders
)
).data.collection
baseCollection1 = (
await api.post(
"/admin/collections",
{ title: "test-collection1" },
adminHeaders
)
).data.collection
baseCollection2 = (
await api.post(
"/admin/collections",
{ title: "test-collection2" },
adminHeaders
)
).data.collection
})
describe("/store/collections", () => {
describe("/store/collections/:id", () => {
it("gets collection", async () => {
const response = await api.get(
`/store/collections/${baseCollection.id}`
)
expect(response.data.collection).toEqual(
expect.objectContaining({
id: baseCollection.id,
created_at: expect.any(String),
updated_at: expect.any(String),
})
)
})
})
describe("/store/collections", () => {
it("lists collections", async () => {
const response = await api.get("/store/collections")
expect(response.data).toEqual({
collections: [
expect.objectContaining({
id: baseCollection2.id,
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: baseCollection1.id,
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: baseCollection.id,
created_at: expect.any(String),
updated_at: expect.any(String),
}),
],
count: 3,
limit: 10,
offset: 0,
})
})
})
})
},
})
@@ -1,102 +1,113 @@
import {ProductCategory} from "@medusajs/medusa" import { medusaIntegrationTestRunner } from "medusa-test-utils"
import path from "path" import {
adminHeaders,
import startServerWithEnvironment createAdminUser,
from "../../../environment-helpers/start-server-with-environment" } from "../../../helpers/create-admin-user"
import {useApi} from "../../../environment-helpers/use-api" import { breaking } from "../../../helpers/breaking"
import {useDb} from "../../../environment-helpers/use-db"
import {simpleProductCategoryFactory} from "../../../factories"
jest.setTimeout(30000) jest.setTimeout(30000)
describe("/store/product-categories", () => { medusaIntegrationTestRunner({
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 }, env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
}) testSuite: ({ dbConnection, getContainer, api }) => {
dbConnection = connection let productCategoryParent
medusaProcess = process let productCategory
}) let productCategoryChild
let productCategoryChild2
afterAll(async () => { let productCategoryChild3
const db = useDb() let productCategoryChild4
await db.shutdown()
medusaProcess.kill()
})
beforeEach(async () => { beforeEach(async () => {
productCategoryParent = await simpleProductCategoryFactory(dbConnection, { const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
productCategoryParent = (
await api.post(
"/admin/product-categories",
{
name: "category parent", name: "category parent",
description: "test description", description: "test description",
is_active: true, is_active: true,
is_internal: false, is_internal: false,
rank: 0, },
}) adminHeaders
)
).data.product_category
productCategory = await simpleProductCategoryFactory(dbConnection, { productCategory = (
await api.post(
"/admin/product-categories",
{
name: "category", name: "category",
parent_category: productCategoryParent, parent_category_id: productCategoryParent.id,
is_active: true, is_active: true,
rank: 0, },
}) adminHeaders
)
).data.product_category
productCategoryChild = await simpleProductCategoryFactory(dbConnection, { // The order in which the children are created is intentional as in v1 there was no way to explicitly set the rank.
name: "category child", productCategoryChild2 = (
parent_category: productCategory, await api.post(
is_active: true, "/admin/product-categories",
is_internal: false, {
rank: 3,
})
productCategoryChild2 = await simpleProductCategoryFactory(dbConnection, {
name: "category child 2", name: "category child 2",
parent_category: productCategory, parent_category_id: productCategory.id,
is_internal: true, is_internal: true,
is_active: true, is_active: true,
rank: 0, },
}) adminHeaders
)
).data.product_category
productCategoryChild3 = await simpleProductCategoryFactory(dbConnection, { productCategoryChild3 = (
await api.post(
"/admin/product-categories",
{
name: "category child 3", name: "category child 3",
parent_category: productCategory, parent_category_id: productCategory.id,
is_active: false,
is_internal: false, is_internal: false,
rank: 1, is_active: false,
}) },
adminHeaders
)
).data.product_category
productCategoryChild4 = await simpleProductCategoryFactory(dbConnection, { productCategoryChild4 = (
await api.post(
"/admin/product-categories",
{
name: "category child 4", name: "category child 4",
parent_category: productCategory, 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_active: true,
is_internal: false, is_internal: false,
rank: 2, },
}) adminHeaders
)
).data.product_category
}) })
describe("/store/product-categories", () => {
describe("GET /store/product-categories/:id", () => { describe("GET /store/product-categories/:id", () => {
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("gets product category with children tree and parent", async () => { it("gets product category with children tree and parent", async () => {
const api = useApi()
const response = await api.get( const response = await api.get(
`/store/product-categories/${productCategory.id}?fields=handle,name,description` `/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(response.data.product_category).toEqual(
@@ -111,7 +122,7 @@ describe("/store/product-categories", () => {
name: productCategoryParent.name, name: productCategoryParent.name,
description: "test description", description: "test description",
}), }),
category_children: [ category_children: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: productCategoryChild4.id, id: productCategoryChild4.id,
handle: productCategoryChild4.handle, handle: productCategoryChild4.handle,
@@ -122,16 +133,15 @@ describe("/store/product-categories", () => {
handle: productCategoryChild.handle, handle: productCategoryChild.handle,
name: productCategoryChild.name, name: productCategoryChild.name,
}), }),
], ]),
}) })
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
}) })
// 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 () => { it("throws error on querying not allowed fields", async () => {
const api = useApi()
const error = await api const error = await api
.get(`/store/product-categories/${productCategory.id}?fields=mpath`) .get(`/store/product-categories/${productCategory.id}?fields=mpath`)
.catch((e) => e) .catch((e) => e)
@@ -144,8 +154,6 @@ describe("/store/product-categories", () => {
}) })
it("throws error on querying for internal product category", async () => { it("throws error on querying for internal product category", async () => {
const api = useApi()
const error = await api const error = await api
.get(`/store/product-categories/${productCategoryChild2.id}`) .get(`/store/product-categories/${productCategoryChild2.id}`)
.catch((e) => e) .catch((e) => e)
@@ -153,13 +161,16 @@ describe("/store/product-categories", () => {
expect(error.response.status).toEqual(404) expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual("not_found") expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual( expect(error.response.data.message).toEqual(
`ProductCategory with id: ${productCategoryChild2.id}, is_internal: false, is_active: true was not found` 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("throws error on querying for inactive product category", async () => { it("throws error on querying for inactive product category", async () => {
const api = useApi()
const error = await api const error = await api
.get(`/store/product-categories/${productCategoryChild3.id}`) .get(`/store/product-categories/${productCategoryChild3.id}`)
.catch((e) => e) .catch((e) => e)
@@ -167,21 +178,25 @@ describe("/store/product-categories", () => {
expect(error.response.status).toEqual(404) expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual("not_found") expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual( expect(error.response.data.message).toEqual(
`ProductCategory with id: ${productCategoryChild3.id}, is_internal: false, is_active: true was not found` 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", () => { describe("GET /store/product-categories", () => {
afterEach(async () => { //TODO: The listing results in V2 are unexpected and differ from v1, we need to investigate where the issue is
const db = useDb()
return await db.teardown()
})
it("gets list of product category with immediate children and parents", async () => { 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${breaking(
const response = await api.get(`/store/product-categories?limit=10`) () => "",
() => "&include_ancestors_tree=true&include_descendants_tree=true"
)}`
)
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.count).toEqual(4) expect(response.data.count).toEqual(4)
@@ -195,7 +210,7 @@ describe("/store/product-categories", () => {
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: productCategoryParent.id, id: productCategoryParent.id,
}), }),
category_children: [ category_children: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: productCategoryChild4.id, id: productCategoryChild4.id,
rank: 2, rank: 2,
@@ -204,7 +219,7 @@ describe("/store/product-categories", () => {
id: productCategoryChild.id, id: productCategoryChild.id,
rank: 3, rank: 3,
}), }),
], ]),
}), }),
expect.objectContaining({ expect.objectContaining({
id: productCategoryParent.id, id: productCategoryParent.id,
@@ -235,9 +250,8 @@ describe("/store/product-categories", () => {
]) ])
}) })
// TODO: It seems filtering using null doesn't work.
it("gets list of product category with all childrens when include_descendants_tree=true", async () => { it("gets list of product category with all childrens when include_descendants_tree=true", async () => {
const api = useApi()
const response = await api.get( const response = await api.get(
`/store/product-categories?parent_category_id=null&include_descendants_tree=true&limit=10` `/store/product-categories?parent_category_id=null&include_descendants_tree=true&limit=10`
) )
@@ -277,8 +291,6 @@ describe("/store/product-categories", () => {
}) })
it("throws error when querying not allowed fields", async () => { it("throws error when querying not allowed fields", async () => {
const api = useApi()
const error = await api const error = await api
.get(`/store/product-categories?is_internal=true&limit=10`) .get(`/store/product-categories?is_internal=true&limit=10`)
.catch((e) => e) .catch((e) => e)
@@ -291,8 +303,6 @@ describe("/store/product-categories", () => {
}) })
it("filters based on free text on name and handle columns", async () => { it("filters based on free text on name and handle columns", async () => {
const api = useApi()
const response = await api.get( const response = await api.get(
`/store/product-categories?q=category-parent&limit=10` `/store/product-categories?q=category-parent&limit=10`
) )
@@ -305,20 +315,18 @@ describe("/store/product-categories", () => {
}) })
it("filters based on handle attribute of the data model", async () => { it("filters based on handle attribute of the data model", async () => {
const api = useApi()
const response = await api.get( const response = await api.get(
`/store/product-categories?handle=${productCategory.handle}&limit=10` `/store/product-categories?handle=${productCategory.handle}&limit=10`
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1) expect(response.data.count).toEqual(1)
expect(response.data.product_categories[0].id).toEqual(productCategory.id) expect(response.data.product_categories[0].id).toEqual(
productCategory.id
)
}) })
it("filters based on parent category", async () => { it("filters based on parent category", async () => {
const api = useApi()
const response = await api.get( const response = await api.get(
`/store/product-categories?parent_category_id=${productCategory.id}&limit=10` `/store/product-categories?parent_category_id=${productCategory.id}&limit=10`
) )
@@ -355,4 +363,6 @@ describe("/store/product-categories", () => {
) )
}) })
}) })
})
},
}) })
@@ -10,7 +10,7 @@ export const defaults = [
"created_at", "created_at",
"updated_at", "updated_at",
"metadata", "metadata",
"*parent_category",
"*category_children", "*category_children",
] ]
@@ -34,9 +34,11 @@ import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-execution
import { authRoutesMiddlewares } from "./auth/middlewares" import { authRoutesMiddlewares } from "./auth/middlewares"
import { hooksRoutesMiddlewares } from "./hooks/middlewares" import { hooksRoutesMiddlewares } from "./hooks/middlewares"
import { storeCartRoutesMiddlewares } from "./store/carts/middlewares" import { storeCartRoutesMiddlewares } from "./store/carts/middlewares"
import { storeCollectionRoutesMiddlewares } from "./store/collections/middlewares"
import { storeCurrencyRoutesMiddlewares } from "./store/currencies/middlewares" import { storeCurrencyRoutesMiddlewares } from "./store/currencies/middlewares"
import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares" import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares"
import { storeProductRoutesMiddlewares } from "./store/products/middlewares" import { storeProductRoutesMiddlewares } from "./store/products/middlewares"
import { storeProductCategoryRoutesMiddlewares } from "./store/product-categories/middlewares"
import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares" import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares"
export const config: MiddlewaresConfig = { export const config: MiddlewaresConfig = {
@@ -48,6 +50,8 @@ export const config: MiddlewaresConfig = {
...storeCartRoutesMiddlewares, ...storeCartRoutesMiddlewares,
...storeCustomerRoutesMiddlewares, ...storeCustomerRoutesMiddlewares,
...storeCartRoutesMiddlewares, ...storeCartRoutesMiddlewares,
...storeCollectionRoutesMiddlewares,
...storeProductCategoryRoutesMiddlewares,
...authRoutesMiddlewares, ...authRoutesMiddlewares,
...adminWorkflowsExecutionsMiddlewares, ...adminWorkflowsExecutionsMiddlewares,
...storeRegionRoutesMiddlewares, ...storeRegionRoutesMiddlewares,
@@ -0,0 +1,18 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import { refetchCollection } from "../helpers"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const collection = await refetchCollection(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ collection })
}
@@ -0,0 +1,23 @@
import { MedusaContainer } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
export const refetchCollection = async (
collectionId: string,
scope: MedusaContainer,
fields: string[]
) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_collection",
variables: {
filters: { id: collectionId },
},
fields: fields,
})
const collections = await remoteQuery(queryObject)
return collections[0]
}
@@ -0,0 +1,30 @@
import * as QueryConfig from "./query-config"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { validateAndTransformQuery } from "../../utils/validate-query"
import {
StoreGetCollectionParams,
StoreGetCollectionsParams,
} from "./validators"
export const storeCollectionRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["GET"],
matcher: "/store/collections",
middlewares: [
validateAndTransformQuery(
StoreGetCollectionsParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/store/collections/:id",
middlewares: [
validateAndTransformQuery(
StoreGetCollectionParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
]
@@ -0,0 +1,18 @@
export const defaultStoreCollectionFields = [
"id",
"title",
"handle",
"created_at",
"updated_at",
]
export const retrieveTransformQueryConfig = {
defaults: defaultStoreCollectionFields,
isList: false,
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
defaultLimit: 10,
isList: true,
}
@@ -0,0 +1,34 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const query = remoteQueryObjectFromString({
entryPoint: "product_collection",
variables: {
filters: req.filterableFields,
...req.remoteQueryConfig.pagination,
},
fields: req.remoteQueryConfig.fields,
})
const { rows: collections, metadata } = await remoteQuery(query)
res.json({
collections,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
@@ -0,0 +1,28 @@
import {
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../utils/validators"
import { z } from "zod"
export const StoreGetCollectionParams = createSelectParams()
export type StoreGetCollectionsParamsType = z.infer<
typeof StoreGetCollectionsParams
>
export const StoreGetCollectionsParams = createFindParams({
offset: 0,
limit: 10,
order: "-created_at",
}).merge(
z.object({
q: z.string().optional(),
title: z.union([z.string(), z.array(z.string())]).optional(),
handle: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
$and: z.lazy(() => StoreGetCollectionsParams.array()).optional(),
$or: z.lazy(() => StoreGetCollectionsParams.array()).optional(),
})
)
@@ -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<StoreProductCategoryParamsType>,
res: MedusaResponse<StoreProductCategoryResponse>
) => {
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 })
}
@@ -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<string, any> = {}
) => {
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()
}
@@ -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,
],
},
]
@@ -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,
}
@@ -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<StoreProductCategoriesParamsType>,
res: MedusaResponse<StoreProductCategoryListResponse>
) => {
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,
})
}
@@ -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(),
})
)
@@ -55,6 +55,7 @@ class ProductCategory {
@Property({ columnType: "text", default: "", nullable: false }) @Property({ columnType: "text", default: "", nullable: false })
description?: string description?: string
@Searchable()
@Property({ columnType: "text", nullable: false }) @Property({ columnType: "text", nullable: false })
handle?: string handle?: string
@@ -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[]
}
@@ -1 +0,0 @@
export * from "./product-category"
@@ -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[]
}
@@ -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[]
}
@@ -1 +1,2 @@
export * from "./admin" export * from "./admin"
export * from "./store"
@@ -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[]
}