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
2024-05-01 09:54:51 +02:00
committed by GitHub
parent 2c807df99d
commit ec37576dd0
25 changed files with 845 additions and 503 deletions

View File

@@ -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>,
},
}
`;

View File

@@ -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,
})
})
})
})

View File

@@ -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,
})
})
})
})
},
})

View File

@@ -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
medusaIntegrationTestRunner({
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
testSuite: ({ dbConnection, getContainer, api }) => {
let productCategoryParent
let productCategory
let productCategoryChild
let productCategoryChild2
let productCategoryChild3
let productCategoryChild4
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
})
beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
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,
})
productCategoryChild3 = await simpleProductCategoryFactory(dbConnection, {
name: "category child 3",
parent_category: productCategory,
is_active: false,
is_internal: false,
rank: 1,
})
productCategoryChild4 = await simpleProductCategoryFactory(dbConnection, {
name: "category child 4",
parent_category: productCategory,
is_active: true,
is_internal: false,
rank: 2,
})
})
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 () => {
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,
productCategoryParent = (
await api.post(
"/admin/product-categories",
{
name: "category parent",
description: "test description",
}),
category_children: [
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
})
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: productCategoryChild4.id,
handle: productCategoryChild4.handle,
name: productCategoryChild4.name,
}),
expect.objectContaining({
id: productCategoryChild.id,
handle: productCategoryChild.handle,
name: productCategoryChild.name,
}),
],
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)
})
)
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 () => {
const error = await api
.get(`/store/product-categories/${productCategory.id}?fields=mpath`)
.catch((e) => e)
it("throws error on querying not allowed fields", async () => {
const api = useApi()
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"
)
})
const error = await api
.get(`/store/product-categories/${productCategory.id}?fields=mpath`)
.catch((e) => e)
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(400)
expect(error.response.data.type).toEqual("invalid_data")
expect(error.response.data.message).toEqual(
"Requested fields [mpath] are not valid"
)
})
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("throws error on querying for internal product category", async () => {
const api = useApi()
it("throws error on querying for inactive product category", async () => {
const error = await api
.get(`/store/product-categories/${productCategoryChild3.id}`)
.catch((e) => e)
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: ${productCategoryChild3.id}, is_internal: false, is_active: true was not found`,
() =>
`Product category with id: ${productCategoryChild3.id} was not found`
)
)
})
})
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`
)
})
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"
)}`
)
it("throws error on querying for inactive product category", async () => {
const api = useApi()
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(4)
expect(response.data.offset).toEqual(0)
expect(response.data.limit).toEqual(10)
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(response.data.product_categories).toEqual([
expect.objectContaining({
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: 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,
}),
],
}),
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: [],
}),
])
})
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.objectContaining({
parent_category: expect.objectContaining({
id: productCategory.id,
parent_category_id: productCategoryParent.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`
)
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.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 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"
)
})
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`
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.product_categories[0].id).toEqual(
productCategoryParent.id
)
})
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(
productCategory.id
)
})
it("filters based on parent category", async () => {
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
)
})
})
})
it("throws error when querying not allowed fields", async () => {
const api = useApi()
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"
)
})
it("filters based on free text on name and handle columns", async () => {
const api = useApi()
const response = await api.get(
`/store/product-categories?q=category-parent&limit=10`
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.product_categories[0].id).toEqual(
productCategoryParent.id
)
})
it("filters based on handle attribute of the data model", async () => {
const api = useApi()
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(productCategory.id)
})
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
)
})
})
},
})

View File

@@ -10,7 +10,7 @@ export const defaults = [
"created_at",
"updated_at",
"metadata",
"*parent_category",
"*category_children",
]

View File

@@ -34,9 +34,11 @@ import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-execution
import { authRoutesMiddlewares } from "./auth/middlewares"
import { hooksRoutesMiddlewares } from "./hooks/middlewares"
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 { storeProductRoutesMiddlewares } from "./store/products/middlewares"
import { storeProductCategoryRoutesMiddlewares } from "./store/product-categories/middlewares"
import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares"
export const config: MiddlewaresConfig = {
@@ -48,6 +50,8 @@ export const config: MiddlewaresConfig = {
...storeCartRoutesMiddlewares,
...storeCustomerRoutesMiddlewares,
...storeCartRoutesMiddlewares,
...storeCollectionRoutesMiddlewares,
...storeProductCategoryRoutesMiddlewares,
...authRoutesMiddlewares,
...adminWorkflowsExecutionsMiddlewares,
...storeRegionRoutesMiddlewares,

View File

@@ -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 })
}

View File

@@ -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]
}

View File

@@ -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
),
],
},
]

View File

@@ -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,
}

View File

@@ -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,
})
}

View File

@@ -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(),
})
)

View File

@@ -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 })
}

View File

@@ -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()
}

View File

@@ -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,
],
},
]

View File

@@ -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,
}

View File

@@ -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,
})
}

View File

@@ -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(),
})
)

View File

@@ -55,6 +55,7 @@ class ProductCategory {
@Property({ columnType: "text", default: "", nullable: false })
description?: string
@Searchable()
@Property({ columnType: "text", nullable: false })
handle?: string

View File

@@ -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[]
}

View File

@@ -1 +0,0 @@
export * from "./product-category"

View File

@@ -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[]
}

View File

@@ -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[]
}

View File

@@ -1 +1,2 @@
export * from "./admin"
export * from "./store"

View File

@@ -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[]
}