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
+845 -503

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,358 +1,368 @@
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 env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
let dbConnection testSuite: ({ dbConnection, getContainer, api }) => {
let productCategory!: ProductCategory let productCategoryParent
let productCategory2!: ProductCategory let productCategory
let productCategoryChild!: ProductCategory let productCategoryChild
let productCategoryParent!: ProductCategory let productCategoryChild2
let productCategoryChild2!: ProductCategory let productCategoryChild3
let productCategoryChild3!: ProductCategory let productCategoryChild4
let productCategoryChild4!: ProductCategory
beforeAll(async () => { beforeEach(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..")) const container = getContainer()
const [process, connection] = await startServerWithEnvironment({ await createAdminUser(dbConnection, adminHeaders, container)
cwd,
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
})
dbConnection = connection
medusaProcess = process
})
afterAll(async () => { productCategoryParent = (
const db = useDb() await api.post(
await db.shutdown() "/admin/product-categories",
{
medusaProcess.kill() name: "category parent",
})
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,
description: "test description", description: "test description",
}), is_active: true,
category_children: [ 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({ expect.objectContaining({
id: productCategoryChild4.id, id: productCategory.id,
handle: productCategoryChild4.handle, handle: productCategory.handle,
name: productCategoryChild4.name, name: productCategory.name,
}), description: "",
expect.objectContaining({ parent_category: expect.objectContaining({
id: productCategoryChild.id, id: productCategoryParent.id,
handle: productCategoryChild.handle, handle: productCategoryParent.handle,
name: productCategoryChild.name, 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 () => { expect(error.response.status).toEqual(400)
const api = useApi() expect(error.response.data.type).toEqual("invalid_data")
expect(error.response.data.message).toEqual(
"Requested fields [mpath] are not valid"
)
})
const error = await api it("throws error on querying for internal product category", async () => {
.get(`/store/product-categories/${productCategory.id}?fields=mpath`) const error = await api
.catch((e) => e) .get(`/store/product-categories/${productCategoryChild2.id}`)
.catch((e) => e)
expect(error.response.status).toEqual(400) expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual("invalid_data") expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual( expect(error.response.data.message).toEqual(
"Requested fields [mpath] are not valid" 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 () => { 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)
const error = await api expect(error.response.status).toEqual(404)
.get(`/store/product-categories/${productCategoryChild2.id}`) expect(error.response.data.type).toEqual("not_found")
.catch((e) => e) 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) describe("GET /store/product-categories", () => {
expect(error.response.data.type).toEqual("not_found") //TODO: The listing results in V2 are unexpected and differ from v1, we need to investigate where the issue is
expect(error.response.data.message).toEqual( it("gets list of product category with immediate children and parents", async () => {
`ProductCategory with id: ${productCategoryChild2.id}, is_internal: false, is_active: true was not found` 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 () => { expect(response.status).toEqual(200)
const api = useApi() expect(response.data.count).toEqual(4)
expect(response.data.offset).toEqual(0)
expect(response.data.limit).toEqual(10)
const error = await api expect(response.data.product_categories).toEqual([
.get(`/store/product-categories/${productCategoryChild3.id}`) expect.objectContaining({
.catch((e) => e) id: productCategory.id,
rank: 0,
expect(error.response.status).toEqual(404) parent_category: expect.objectContaining({
expect(error.response.data.type).toEqual("not_found") id: productCategoryParent.id,
expect(error.response.data.message).toEqual( }),
`ProductCategory with id: ${productCategoryChild3.id}, is_internal: false, is_active: true was not found` category_children: expect.arrayContaining([
) expect.objectContaining({
}) id: productCategoryChild4.id,
}) rank: 2,
}),
describe("GET /store/product-categories", () => { expect.objectContaining({
afterEach(async () => { id: productCategoryChild.id,
const db = useDb() rank: 3,
return await db.teardown() }),
}) ]),
}),
it("gets list of product category with immediate children and parents", async () => { expect.objectContaining({
const api = useApi() id: productCategoryParent.id,
parent_category: null,
const response = await api.get(`/store/product-categories?limit=10`) rank: 0,
category_children: [
expect(response.status).toEqual(200) expect.objectContaining({
expect(response.data.count).toEqual(4) id: productCategory.id,
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({ expect.objectContaining({
id: productCategoryChild4.id, id: productCategoryChild4.id,
rank: 2, rank: 2,
parent_category: expect.objectContaining({
id: productCategory.id,
}),
category_children: [],
}), }),
expect.objectContaining({ expect.objectContaining({
id: productCategoryChild.id, id: productCategoryChild.id,
rank: 3, rank: 3,
}), parent_category: expect.objectContaining({
],
}),
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({
id: productCategory.id, 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, rank: 0,
category_children: [ category_children: [
expect.objectContaining({ expect.objectContaining({
id: productCategoryChild4.id, id: productCategory.id,
parent_category_id: productCategory.id, parent_category_id: productCategoryParent.id,
category_children: [], rank: 0,
rank: 2, category_children: [
}), expect.objectContaining({
expect.objectContaining({ id: productCategoryChild4.id,
id: productCategoryChild.id, parent_category_id: productCategory.id,
parent_category_id: productCategory.id, category_children: [],
category_children: [], rank: 2,
rank: 3, }),
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
)
})
})
}) })
@@ -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[]
}