feat: Create product category flow (#7034)

* feat: Create product category

* address PR comments
This commit is contained in:
Oli Juhl
2024-04-15 17:11:42 +02:00
committed by GitHub
parent fd83e75e4b
commit bc081a7777
17 changed files with 1470 additions and 1261 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ export * from "./customer-group"
export * from "./defaults"
export * from "./definition"
export * from "./definitions"
export * from "./file"
export * from "./fulfillment"
export * as Handlers from "./handlers"
export * from "./inventory"
@@ -15,6 +16,7 @@ export * from "./payment"
export * from "./price-list"
export * from "./pricing"
export * from "./product"
export * from "./product-category"
export * from "./promotion"
export * from "./reservation"
export * from "./region"
@@ -24,4 +26,3 @@ export * from "./stock-location"
export * from "./store"
export * from "./tax"
export * from "./user"
export * from "./file"

View File

@@ -0,0 +1,2 @@
export * from "./steps"
export * from "./workflows"

View File

@@ -0,0 +1,35 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
CreateProductCategoryDTO,
IProductModuleService,
} from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type CreateProductCategoryStepInput = {
product_category: CreateProductCategoryDTO
}
export const createProductCategoryStepId = "create-product-category"
export const createProductCategoryStep = createStep(
createProductCategoryStepId,
async (data: CreateProductCategoryStepInput, { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const created = await service.createCategory(data.product_category)
return new StepResponse(created, created.id)
},
async (createdId, { container }) => {
if (!createdId) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.deleteCategory(createdId)
}
)

View File

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

View File

@@ -0,0 +1,16 @@
import { ProductCategoryWorkflow } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { createProductCategoryStep } from "../steps"
type WorkflowInputData =
ProductCategoryWorkflow.CreateProductCategoryWorkflowInput
export const createProductCategoryWorkflowId = "create-product-category"
export const createProductCategoryWorkflow = createWorkflow(
createProductCategoryWorkflowId,
(input: WorkflowData<WorkflowInputData>) => {
const category = createProductCategoryStep(input)
return category
}
)

View File

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

View File

@@ -1,8 +1,10 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
import * as QueryConfig from "./query-config"
import {
AdminCreateProductCategory,
AdminProductCategoriesParams,
AdminProductCategoryParams,
} from "./validators"
@@ -33,4 +35,15 @@ export const adminProductCategoryRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["POST"],
matcher: "/admin/product-categories",
middlewares: [
validateAndTransformBody(AdminCreateProductCategory),
validateAndTransformQuery(
AdminProductCategoryParams,
QueryConfig.retrieveProductCategoryConfig
),
],
},
]

View File

@@ -11,10 +11,24 @@ export const defaults = [
"updated_at",
"metadata",
"parent_category.id",
"parent_category.name",
"category_children.id",
"category_children.name",
"*category_children",
]
export const allowed = [
"id",
"name",
"description",
"handle",
"is_active",
"is_internal",
"rank",
"parent_category_id",
"created_at",
"updated_at",
"metadata",
"*parent_category",
"*category_children",
]
export const retrieveProductCategoryConfig = {

View File

@@ -1,4 +1,8 @@
import { AdminProductCategoryListResponse } from "@medusajs/types"
import { createProductCategoryWorkflow } from "@medusajs/core-flows"
import {
AdminProductCategoryListResponse,
AdminProductCategoryResponse,
} from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
@@ -7,7 +11,10 @@ import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { AdminProductCategoriesParamsType } from "./validators"
import {
AdminCreateProductCategoryType,
AdminProductCategoriesParamsType,
} from "./validators"
export const GET = async (
req: AuthenticatedMedusaRequest<AdminProductCategoriesParamsType>,
@@ -33,3 +40,33 @@ export const GET = async (
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminCreateProductCategoryType>,
res: MedusaResponse<AdminProductCategoryResponse>
) => {
const { result, errors } = await createProductCategoryWorkflow(req.scope).run(
{
input: { product_category: req.validatedBody },
throwOnError: false,
}
)
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_category",
variables: {
filters: { id: result.id },
},
fields: req.remoteQueryConfig.fields,
})
const [product_category] = await remoteQuery(queryObject)
res.status(200).json({ product_category })
}

View File

@@ -43,6 +43,14 @@ export const AdminProductCategoriesParams = createFindParams({
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
z.boolean().optional()
),
is_internal: z.preprocess(
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
z.boolean().optional()
),
is_active: z.preprocess(
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
z.boolean().optional()
),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
@@ -50,3 +58,19 @@ export const AdminProductCategoriesParams = createFindParams({
$or: z.lazy(() => AdminProductCategoriesParams.array()).optional(),
})
)
export const AdminCreateProductCategory = z
.object({
name: z.string(),
description: z.string().optional(),
handle: z.string().optional(),
is_internal: z.boolean().optional(),
is_active: z.boolean().optional(),
parent_category_id: z.string().optional(),
metadata: z.record(z.unknown()).optional(),
})
.strict()
export type AdminCreateProductCategoryType = z.infer<
typeof AdminCreateProductCategory
>

View File

@@ -1,10 +1,9 @@
import { Modules } from "@medusajs/modules-sdk"
import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { Product, ProductCategory } from "@models"
import { MockEventBusService } from "medusa-test-utils"
import { MockEventBusService, SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils"
import { createProductCategories } from "../../../__fixtures__/product-category"
import { productCategoriesRankData } from "../../../__fixtures__/product-category/data"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
jest.setTimeout(30000)

View File

@@ -4,11 +4,7 @@ import {
ProductCategoryTransformOptions,
ProductTypes,
} from "@medusajs/types"
import {
DALUtils,
MedusaError,
isDefined
} from "@medusajs/utils"
import { DALUtils, MedusaError, isDefined } from "@medusajs/utils"
import {
LoadStrategy,
FilterQuery as MikroFilterQuery,

View File

@@ -915,11 +915,21 @@ export default class ProductModuleService<
)
}
@InjectTransactionManager("baseRepository_")
@InjectManager("baseRepository_")
async createCategory(
data: ProductTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductCategoryDTO> {
const result = await this.createCategory_(data, sharedContext)
return await this.baseRepository_.serialize(result)
}
@InjectTransactionManager("baseRepository_")
async createCategory_(
data: ProductTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory> {
const productCategory = await this.productCategoryService_.create(
data,
sharedContext
@@ -930,9 +940,7 @@ export default class ProductModuleService<
{ id: productCategory.id }
)
return await this.baseRepository_.serialize(productCategory, {
populate: true,
})
return productCategory
}
@InjectTransactionManager("baseRepository_")
@@ -1115,8 +1123,10 @@ export default class ProductModuleService<
data: ProductTypes.CreateProductDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TProduct[]> {
const normalizedInput = await Promise.all(
data.map((d) => this.normalizeCreateProductInput(d, sharedContext))
const normalizedInput = await promiseAll(
data.map(
async (d) => await this.normalizeCreateProductInput(d, sharedContext)
)
)
const productData = await this.productService_.upsertWithReplace(
@@ -1171,8 +1181,10 @@ export default class ProductModuleService<
data: UpdateProductInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<TProduct[]> {
const normalizedInput = await Promise.all(
data.map((d) => this.normalizeUpdateProductInput(d, sharedContext))
const normalizedInput = await promiseAll(
data.map(
async (d) => await this.normalizeUpdateProductInput(d, sharedContext)
)
)
const productData = await this.productService_.upsertWithReplace(
@@ -1258,7 +1270,8 @@ export default class ProductModuleService<
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.CreateProductDTO> {
const productData = (await this.normalizeUpdateProductInput(
product as UpdateProductInput
product as UpdateProductInput,
sharedContext
)) as ProductTypes.CreateProductDTO
if (!productData.handle && productData.title) {

View File

@@ -358,11 +358,8 @@ export interface CreateProductCategoryDTO {
rank?: number
/**
* The ID of the parent product category, if it has any.
*
* @privateRemarks
* Shouldn't this be optional?
*/
parent_category_id: string | null
parent_category_id?: string | null
/**
* Holds custom data in key-value pairs.
*/

View File

@@ -1,10 +1,12 @@
export * as CartWorkflow from "./cart"
export * as CommonWorkflow from "./common"
export * as ProductWorkflow from "./product"
export * as InventoryWorkflow from "./inventory"
export * as PriceListWorkflow from "./price-list"
export * as UserWorkflow from "./user"
export * as RegionWorkflow from "./region"
export * as InviteWorkflow from "./invite"
export * as FulfillmentWorkflow from "./fulfillment"
export * as InventoryWorkflow from "./inventory"
export * as InviteWorkflow from "./invite"
export * as PriceListWorkflow from "./price-list"
export * as ProductWorkflow from "./product"
export * as ProductCategoryWorkflow from "./product-category"
export * as RegionWorkflow from "./region"
export * as ReservationWorkflow from "./reservation"
export * as UserWorkflow from "./user"

View File

@@ -0,0 +1,5 @@
import { CreateProductCategoryDTO } from "../../product"
export interface CreateProductCategoryWorkflowInput {
product_category: CreateProductCategoryDTO
}