feat: /store api product types (#2552)

## What
Allow users to fetch ProductTypes from the storefront API.

## Why
This endpoint will allow developers to implement better faceted product search in Medusa without the need for search plugin. Developers will be able to use this to render refinement lists based on types, like this:
![image](https://user-images.githubusercontent.com/116003638/200417828-863065de-3607-49db-bd72-62a6815129fa.png)

## How
Endpoint `GET /store/products/types` and `GET /store/product-types` (use [product types listing in admin](https://github.com/medusajs/medusa/blob/master/packages/medusa/src/api/routes/admin/products/list-types.ts) as reference)

Support added in @medusajs/medusa-js
Support added in medusa-react

## Testing
Similar automated tests as `GET /admin/products/types` and `GET /admin/product-types`

---

Resolves CORE-699
This commit is contained in:
Patrick
2022-11-09 11:10:17 -05:00
committed by GitHub
parent 2d095a0ce1
commit 7b0ceeffb4
18 changed files with 517 additions and 32 deletions
+1
View File
@@ -56,6 +56,7 @@ export * from "./routes/store/gift-cards"
export * from "./routes/store/order-edits"
export * from "./routes/store/orders"
export * from "./routes/store/products"
export * from "./routes/store/product-types"
export * from "./routes/store/regions"
export * from "./routes/store/return-reasons"
export * from "./routes/store/returns"
@@ -1,12 +1,12 @@
import {
DateComparisonOperator,
FindPaginationParams,
StringComparisonOperator,
} from "../../../../types/common"
import { IsNumber, IsOptional, IsString } from "class-validator"
import { IsOptional, IsString } from "class-validator"
import { IsType } from "../../../../utils/validators/is-type"
import ProductTypeService from "../../../../services/product-type"
import { Type } from "class-transformer"
/**
* @oas [get] /product-types
@@ -15,7 +15,7 @@ import { Type } from "class-transformer"
* description: "Retrieve a list of Product Types."
* x-authenticated: true
* parameters:
* - (query) limit=10 {integer} The number of types to return.
* - (query) limit=20 {integer} The number of types to return.
* - (query) offset=0 {integer} The number of items to skip before the results.
* - (query) order {string} The field to sort items by.
* - (query) discount_condition_id {string} The discount condition id on which to filter the product types.
@@ -154,20 +154,8 @@ export default async (req, res) => {
})
}
export class AdminGetProductTypesPaginationParams {
@IsNumber()
@IsOptional()
@Type(() => Number)
limit? = 10
@IsNumber()
@IsOptional()
@Type(() => Number)
offset? = 0
}
// eslint-disable-next-line max-len
export class AdminGetProductTypesParams extends AdminGetProductTypesPaginationParams {
export class AdminGetProductTypesParams extends FindPaginationParams {
@IsType([String, [String], StringComparisonOperator])
@IsOptional()
id?: string | string[] | StringComparisonOperator
@@ -2,6 +2,7 @@ import { ProductService } from "../../../../services"
/**
* @oas [get] /products/types
* deprecated: true
* operationId: "GetProductsTypes"
* summary: "List Product Types"
* description: "Retrieves a list of Product Types."
@@ -9,6 +9,7 @@ import giftCardRoutes from "./gift-cards"
import orderRoutes from "./orders"
import orderEditRoutes from "./order-edits"
import productRoutes from "./products"
import productTypesRoutes from "../admin/product-types"
import regionRoutes from "./regions"
import returnReasonRoutes from "./return-reasons"
import returnRoutes from "./returns"
@@ -35,6 +36,7 @@ export default (app, container, config) => {
collectionRoutes(route)
customerRoutes(route, container)
productRoutes(route)
productTypesRoutes(route)
orderRoutes(route)
orderEditRoutes(route)
cartRoutes(route, container)
@@ -0,0 +1,50 @@
import { Router } from "express"
import { ProductType } from "../../../.."
import { PaginatedResponse } from "../../../../types/common"
import middlewares, { transformQuery } from "../../../middlewares"
import "reflect-metadata"
import { StoreGetProductTypesParams } from "./list-product-types"
const route = Router()
export default (app) => {
app.use("/product-types", route)
route.get(
"/",
transformQuery(StoreGetProductTypesParams, {
defaultFields: defaultStoreProductTypeFields,
defaultRelations: defaultStoreProductTypeRelations,
allowedFields: allowedStoreProductTypeFields,
isList: true,
}),
middlewares.wrap(require("./list-product-types").default)
)
return app
}
export const allowedStoreProductTypeFields = [
"id",
"value",
"created_at",
"updated_at",
]
export const defaultStoreProductTypeFields = [
"id",
"value",
"created_at",
"updated_at",
]
export const defaultStoreProductTypeRelations = []
export type StoreProductTypesListRes = PaginatedResponse & {
product_types: ProductType[]
}
export type StoreProductTypesRes = {
product_type: ProductType
}
export * from "./list-product-types"
@@ -0,0 +1,186 @@
import {
DateComparisonOperator,
FindPaginationParams,
StringComparisonOperator,
} from "../../../../types/common"
import { IsOptional, IsString } from "class-validator"
import { IsType } from "../../../../utils/validators/is-type"
import ProductTypeService from "../../../../services/product-type"
/**
* @oas [get] /product-types
* operationId: "GetProductTypes"
* summary: "List Product Types"
* description: "Retrieve a list of Product Types."
* x-authenticated: true
* parameters:
* - (query) limit=20 {integer} The number of types to return.
* - (query) offset=0 {integer} The number of items to skip before the results.
* - (query) order {string} The field to sort items by.
* - (query) discount_condition_id {string} The discount condition id on which to filter the product types.
* - in: query
* name: value
* style: form
* explode: false
* description: The type values to search for
* schema:
* type: array
* items:
* type: string
* - in: query
* name: id
* style: form
* explode: false
* description: The type IDs to search for
* schema:
* type: array
* items:
* type: string
* - (query) q {string} A query string to search values for
* - in: query
* name: created_at
* description: Date comparison for when resulting product types were created.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* description: Date comparison for when resulting product types were updated.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.store.productTypes.list()
* .then(({ product_types }) => {
* console.log(product_types.length);
* });
* - lang: Shell
* label: cURL
* source: |
* curl --location --request GET 'https://medusa-url.com/store/product-types' \
* --header 'Authorization: Bearer {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* tags:
* - Product Type
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* properties:
* product_types:
* $ref: "#/components/schemas/product_type"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of items skipped before these items
* limit:
* type: integer
* description: The number of items per page
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const typeService: ProductTypeService =
req.scope.resolve("productTypeService")
const { listConfig, filterableFields } = req
const { skip, take } = req.listConfig
const [types, count] = await typeService.listAndCount(
filterableFields,
listConfig
)
res.status(200).json({
product_types: types,
count,
offset: skip,
limit: take,
})
}
// eslint-disable-next-line max-len
export class StoreGetProductTypesParams extends FindPaginationParams {
@IsType([String, [String], StringComparisonOperator])
@IsOptional()
id?: string | string[] | StringComparisonOperator
@IsString()
@IsOptional()
q?: string
@IsType([String, [String], StringComparisonOperator])
@IsOptional()
value?: string | string[] | StringComparisonOperator
@IsType([DateComparisonOperator])
@IsOptional()
created_at?: DateComparisonOperator
@IsType([DateComparisonOperator])
@IsOptional()
updated_at?: DateComparisonOperator
@IsString()
@IsOptional()
order?: string
@IsString()
@IsOptional()
discount_condition_id?: string
}