From 1a48fe0282a8bc1f8548a4736255e457d173da09 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Fri, 29 Mar 2024 20:27:10 +0100 Subject: [PATCH] feat: Add v2 product types endpoints (#6880) Also adjusts the product type module APIs to follow the conventions --- .changeset/fifty-badgers-crash.md | 8 + .../src/product/steps/create-product-types.ts | 30 +++ .../src/product/steps/delete-product-types.ts | 27 +++ .../core-flows/src/product/steps/index.ts | 3 + .../src/product/steps/update-product-types.ts | 42 ++++ .../product/workflows/create-product-types.ts | 15 ++ .../product/workflows/delete-product-types.ts | 12 ++ .../core-flows/src/product/workflows/index.ts | 3 + .../product/workflows/update-product-types.ts | 20 ++ .../api-v2/admin/product-types/[id]/route.ts | 78 +++++++ .../src/api-v2/admin/product-types/helpers.ts | 20 ++ .../api-v2/admin/product-types/middlewares.ts | 69 ++++++ .../admin/product-types/query-config.ts | 17 ++ .../src/api-v2/admin/product-types/route.ts | 63 ++++++ .../api-v2/admin/product-types/validators.ts | 113 ++++++++++ packages/medusa/src/api-v2/middlewares.ts | 2 + .../product-types.spec.ts | 20 +- .../src/services/product-module-service.ts | 119 ++++++++-- packages/product/src/types/index.ts | 4 + packages/types/src/product/common.ts | 2 +- packages/types/src/product/service.ts | 203 +++++++++++++++++- 21 files changed, 831 insertions(+), 39 deletions(-) create mode 100644 .changeset/fifty-badgers-crash.md create mode 100644 packages/core-flows/src/product/steps/create-product-types.ts create mode 100644 packages/core-flows/src/product/steps/delete-product-types.ts create mode 100644 packages/core-flows/src/product/steps/update-product-types.ts create mode 100644 packages/core-flows/src/product/workflows/create-product-types.ts create mode 100644 packages/core-flows/src/product/workflows/delete-product-types.ts create mode 100644 packages/core-flows/src/product/workflows/update-product-types.ts create mode 100644 packages/medusa/src/api-v2/admin/product-types/[id]/route.ts create mode 100644 packages/medusa/src/api-v2/admin/product-types/helpers.ts create mode 100644 packages/medusa/src/api-v2/admin/product-types/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/product-types/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/product-types/route.ts create mode 100644 packages/medusa/src/api-v2/admin/product-types/validators.ts diff --git a/.changeset/fifty-badgers-crash.md b/.changeset/fifty-badgers-crash.md new file mode 100644 index 0000000000000..d35b6933b5bdc --- /dev/null +++ b/.changeset/fifty-badgers-crash.md @@ -0,0 +1,8 @@ +--- +"@medusajs/product": minor +"@medusajs/medusa": minor +"@medusajs/types": minor +"@medusajs/core-flows": patch +--- + +Add v2 product type endpoints and adjust the product module diff --git a/packages/core-flows/src/product/steps/create-product-types.ts b/packages/core-flows/src/product/steps/create-product-types.ts new file mode 100644 index 0000000000000..52d61fb2365a3 --- /dev/null +++ b/packages/core-flows/src/product/steps/create-product-types.ts @@ -0,0 +1,30 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IProductModuleService, ProductTypes } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const createProductTypesStepId = "create-product-types" +export const createProductTypesStep = createStep( + createProductTypesStepId, + async (data: ProductTypes.CreateProductTypeDTO[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.PRODUCT + ) + + const created = await service.createTypes(data) + return new StepResponse( + created, + created.map((productType) => productType.id) + ) + }, + async (createdIds, { container }) => { + if (!createdIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PRODUCT + ) + + await service.deleteTypes(createdIds) + } +) diff --git a/packages/core-flows/src/product/steps/delete-product-types.ts b/packages/core-flows/src/product/steps/delete-product-types.ts new file mode 100644 index 0000000000000..d2d9cd8a0134d --- /dev/null +++ b/packages/core-flows/src/product/steps/delete-product-types.ts @@ -0,0 +1,27 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IProductModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const deleteProductTypesStepId = "delete-product-types" +export const deleteProductTypesStep = createStep( + deleteProductTypesStepId, + async (ids: string[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.PRODUCT + ) + + await service.softDeleteTypes(ids) + return new StepResponse(void 0, ids) + }, + async (prevIds, { container }) => { + if (!prevIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PRODUCT + ) + + await service.restoreTypes(prevIds) + } +) diff --git a/packages/core-flows/src/product/steps/index.ts b/packages/core-flows/src/product/steps/index.ts index 2d0eee8705ef0..10a1e673b9422 100644 --- a/packages/core-flows/src/product/steps/index.ts +++ b/packages/core-flows/src/product/steps/index.ts @@ -13,3 +13,6 @@ export * from "./delete-product-variants" export * from "./create-collections" export * from "./update-collections" export * from "./delete-collections" +export * from "./create-product-types" +export * from "./update-product-types" +export * from "./delete-product-types" diff --git a/packages/core-flows/src/product/steps/update-product-types.ts b/packages/core-flows/src/product/steps/update-product-types.ts new file mode 100644 index 0000000000000..61d1a8ca31fb2 --- /dev/null +++ b/packages/core-flows/src/product/steps/update-product-types.ts @@ -0,0 +1,42 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IProductModuleService, ProductTypes } from "@medusajs/types" +import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type UpdateProductTypesStepInput = { + selector: ProductTypes.FilterableProductTypeProps + update: ProductTypes.UpdateProductTypeDTO +} + +export const updateProductTypesStepId = "update-product-types" +export const updateProductTypesStep = createStep( + updateProductTypesStepId, + async (data: UpdateProductTypesStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.PRODUCT + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + data.update, + ]) + + const prevData = await service.listTypes(data.selector, { + select: selects, + relations, + }) + + const productTypes = await service.updateTypes(data.selector, data.update) + return new StepResponse(productTypes, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PRODUCT + ) + + await service.upsertTypes(prevData) + } +) diff --git a/packages/core-flows/src/product/workflows/create-product-types.ts b/packages/core-flows/src/product/workflows/create-product-types.ts new file mode 100644 index 0000000000000..40ff160b4cbd2 --- /dev/null +++ b/packages/core-flows/src/product/workflows/create-product-types.ts @@ -0,0 +1,15 @@ +import { ProductTypes } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createProductTypesStep } from "../steps" + +type WorkflowInput = { product_types: ProductTypes.CreateProductTypeDTO[] } + +export const createProductTypesWorkflowId = "create-product-types" +export const createProductTypesWorkflow = createWorkflow( + createProductTypesWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + return createProductTypesStep(input.product_types) + } +) diff --git a/packages/core-flows/src/product/workflows/delete-product-types.ts b/packages/core-flows/src/product/workflows/delete-product-types.ts new file mode 100644 index 0000000000000..294350909261e --- /dev/null +++ b/packages/core-flows/src/product/workflows/delete-product-types.ts @@ -0,0 +1,12 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deleteProductTypesStep } from "../steps" + +type WorkflowInput = { ids: string[] } + +export const deleteProductTypesWorkflowId = "delete-product-types" +export const deleteProductTypesWorkflow = createWorkflow( + deleteProductTypesWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteProductTypesStep(input.ids) + } +) diff --git a/packages/core-flows/src/product/workflows/index.ts b/packages/core-flows/src/product/workflows/index.ts index aaa196b1f65f9..a2b99b94f31de 100644 --- a/packages/core-flows/src/product/workflows/index.ts +++ b/packages/core-flows/src/product/workflows/index.ts @@ -10,3 +10,6 @@ export * from "./update-product-variants" export * from "./create-collections" export * from "./delete-collections" export * from "./update-collections" +export * from "./create-product-types" +export * from "./delete-product-types" +export * from "./update-product-types" diff --git a/packages/core-flows/src/product/workflows/update-product-types.ts b/packages/core-flows/src/product/workflows/update-product-types.ts new file mode 100644 index 0000000000000..0b790efc5d9f3 --- /dev/null +++ b/packages/core-flows/src/product/workflows/update-product-types.ts @@ -0,0 +1,20 @@ +import { ProductTypes } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updateProductTypesStep } from "../steps" + +type UpdateProductTypesStepInput = { + selector: ProductTypes.FilterableProductTypeProps + update: ProductTypes.UpdateProductTypeDTO +} + +type WorkflowInput = UpdateProductTypesStepInput + +export const updateProductTypesWorkflowId = "update-product-types" +export const updateProductTypesWorkflow = createWorkflow( + updateProductTypesWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + return updateProductTypesStep(input) + } +) diff --git a/packages/medusa/src/api-v2/admin/product-types/[id]/route.ts b/packages/medusa/src/api-v2/admin/product-types/[id]/route.ts new file mode 100644 index 0000000000000..bc2eab000f2b0 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/product-types/[id]/route.ts @@ -0,0 +1,78 @@ +import { + deleteProductTypesWorkflow, + updateProductTypesWorkflow, +} from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" + +import { UpdateProductTypeDTO } from "@medusajs/types" +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { refetchProductType } from "../helpers" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const variables = { id: req.params.id } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "product_type", + variables, + fields: req.remoteQueryConfig.fields, + }) + + const [product_type] = await remoteQuery(queryObject) + + res.status(200).json({ product_type }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { result, errors } = await updateProductTypesWorkflow(req.scope).run({ + input: { + selector: { id: req.params.id }, + update: req.validatedBody, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const productType = await refetchProductType( + result[0].id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ product_type: productType }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.params.id + + const { errors } = await deleteProductTypesWorkflow(req.scope).run({ + input: { ids: [id] }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ + id, + object: "product_type", + deleted: true, + }) +} diff --git a/packages/medusa/src/api-v2/admin/product-types/helpers.ts b/packages/medusa/src/api-v2/admin/product-types/helpers.ts new file mode 100644 index 0000000000000..30337bf58fe72 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/product-types/helpers.ts @@ -0,0 +1,20 @@ +import { MedusaContainer } from "@medusajs/types" +import { remoteQueryObjectFromString } from "@medusajs/utils" + +export const refetchProductType = async ( + productTypeId: string, + scope: MedusaContainer, + fields: string[] +) => { + const remoteQuery = scope.resolve("remoteQuery") + const queryObject = remoteQueryObjectFromString({ + entryPoint: "product_type", + variables: { + filters: { id: productTypeId }, + }, + fields: fields, + }) + + const productTypes = await remoteQuery(queryObject) + return productTypes[0] +} diff --git a/packages/medusa/src/api-v2/admin/product-types/middlewares.ts b/packages/medusa/src/api-v2/admin/product-types/middlewares.ts new file mode 100644 index 0000000000000..873fed157bb04 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/product-types/middlewares.ts @@ -0,0 +1,69 @@ +import * as QueryConfig from "./query-config" + +import { + AdminGetProductTypesProductTypeParams, + AdminGetProductTypesParams, + AdminPostProductTypesProductTypeReq, + AdminPostProductTypesReq, +} from "./validators" +import { transformBody, transformQuery } from "../../../api/middlewares" + +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { authenticate } from "../../../utils/authenticate-middleware" + +export const adminProductTypeRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["ALL"], + matcher: "/admin/product-types/*", + middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], + }, + + { + method: ["GET"], + matcher: "/admin/product-types", + middlewares: [ + transformQuery( + AdminGetProductTypesParams, + QueryConfig.listProductTypesTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/product-types/:id", + middlewares: [ + transformQuery( + AdminGetProductTypesProductTypeParams, + QueryConfig.retrieveProductTypeTransformQueryConfig + ), + ], + }, + // Create/update/delete methods are new in v2 + { + method: ["POST"], + matcher: "/admin/product-types", + middlewares: [ + transformBody(AdminPostProductTypesReq), + transformQuery( + AdminGetProductTypesParams, + QueryConfig.retrieveProductTypeTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/product-types/:id", + middlewares: [ + transformBody(AdminPostProductTypesProductTypeReq), + transformQuery( + AdminGetProductTypesProductTypeParams, + QueryConfig.retrieveProductTypeTransformQueryConfig + ), + ], + }, + { + method: ["DELETE"], + matcher: "/admin/product-types/:id", + middlewares: [], + }, +] diff --git a/packages/medusa/src/api-v2/admin/product-types/query-config.ts b/packages/medusa/src/api-v2/admin/product-types/query-config.ts new file mode 100644 index 0000000000000..f05f9378fa48d --- /dev/null +++ b/packages/medusa/src/api-v2/admin/product-types/query-config.ts @@ -0,0 +1,17 @@ +export const defaultAdminProductTypeFields = [ + "id", + "value", + "created_at", + "updated_at", +] + +export const retrieveProductTypeTransformQueryConfig = { + defaults: defaultAdminProductTypeFields, + isList: false, +} + +export const listProductTypesTransformQueryConfig = { + ...retrieveProductTypeTransformQueryConfig, + defaultLimit: 20, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/product-types/route.ts b/packages/medusa/src/api-v2/admin/product-types/route.ts new file mode 100644 index 0000000000000..037d3709c6ac3 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/product-types/route.ts @@ -0,0 +1,63 @@ +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" + +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AdminGetProductTypesParams, + AdminPostProductTypesReq, +} from "./validators" +import { createProductTypesWorkflow } from "@medusajs/core-flows" +import { refetchProductType } from "./helpers" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const queryObject = remoteQueryObjectFromString({ + entryPoint: "product", + variables: { + filters: req.filterableFields, + ...req.remoteQueryConfig.pagination, + }, + fields: req.remoteQueryConfig.fields, + }) + + const { rows: product_types, metadata } = await remoteQuery(queryObject) + + res.json({ + product_types: product_types, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const input = [req.validatedBody] + + const { result, errors } = await createProductTypesWorkflow(req.scope).run({ + input: { product_types: input }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const productType = await refetchProductType( + result[0].id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ product_type: productType }) +} diff --git a/packages/medusa/src/api-v2/admin/product-types/validators.ts b/packages/medusa/src/api-v2/admin/product-types/validators.ts new file mode 100644 index 0000000000000..82d68fc71d8de --- /dev/null +++ b/packages/medusa/src/api-v2/admin/product-types/validators.ts @@ -0,0 +1,113 @@ +import { OperatorMap } from "@medusajs/types" +import { Type } from "class-transformer" +import { + IsNotEmpty, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" +import { OperatorMapValidator } from "../../../types/validators/operator-map" + +export class AdminGetProductTypesProductTypeParams extends FindParams {} + +/** + * Parameters used to filter and configure the pagination of the retrieved product types. + */ +export class AdminGetProductTypesParams extends extendedFindParamsMixin({ + limit: 10, + offset: 0, +}) { + /** + * Term to search product product types by their title and handle. + */ + @IsString() + @IsOptional() + q?: string + + /** + * Id to filter product product types by. + */ + @IsOptional() + @IsString() + id?: string | string[] + + /** + * Title to filter product product types by. + */ + @IsOptional() + @IsString() + value?: string | string[] | OperatorMap + + /** + * Handle to filter product product types by. + */ + @IsOptional() + @IsString() + handle?: string | string[] + + /** + * Date filters to apply on the product product types' `created_at` date. + */ + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + created_at?: OperatorMap + + /** + * Date filters to apply on the product product types' `updated_at` date. + */ + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + updated_at?: OperatorMap + + /** + * Date filters to apply on the product product types' `deleted_at` date. + */ + @ValidateNested() + @IsOptional() + @Type(() => OperatorMapValidator) + deleted_at?: OperatorMap + + // TODO: To be added in next iteration + // /** + // * Filter product types by their associated discount condition's ID. + // */ + // @IsString() + // @IsOptional() + // discount_condition_id?: string + + // Note: These are new in v2 + // Additional filters from BaseFilterable + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetProductTypesParams) + $and?: AdminGetProductTypesParams[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetProductTypesParams) + $or?: AdminGetProductTypesParams[] +} + +export class AdminPostProductTypesReq { + @IsString() + @IsNotEmpty() + value: string + + @IsObject() + @IsOptional() + metadata?: Record +} + +export class AdminPostProductTypesProductTypeReq { + @IsString() + @IsOptional() + value?: string + + @IsObject() + @IsOptional() + metadata?: Record +} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 6776e3b9e2e23..a06d62814889c 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -12,6 +12,7 @@ import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares" import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares" import { adminPricingRoutesMiddlewares } from "./admin/pricing/middlewares" import { adminProductRoutesMiddlewares } from "./admin/products/middlewares" +import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" import { adminSalesChannelRoutesMiddlewares } from "./admin/sales-channels/middlewares" @@ -59,5 +60,6 @@ export const config: MiddlewaresConfig = { ...adminFulfillmentRoutesMiddlewares, ...adminSalesChannelRoutesMiddlewares, ...adminStockLocationRoutesMiddlewares, + ...adminProductTypeRoutesMiddlewares, ], } diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts index 0dabd13bfeca1..3719550fbe342 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts @@ -211,12 +211,9 @@ moduleIntegrationTestRunner({ const typeId = "type-1" it("should update the value of the type successfully", async () => { - await service.updateTypes([ - { - id: typeId, - value: "UK", - }, - ]) + await service.updateTypes(typeId, { + value: "UK", + }) const productType = await service.retrieveType(typeId) @@ -227,18 +224,15 @@ moduleIntegrationTestRunner({ let error try { - await service.updateTypes([ - { - id: "does-not-exist", - value: "UK", - }, - ]) + await service.updateTypes("does-not-exist", { + value: "UK", + }) } catch (e) { error = e } expect(error.message).toEqual( - 'ProductType with id "does-not-exist" not found' + "ProductType with id: does-not-exist was not found" ) }) }) diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 998ff4d689326..9dc71283413b6 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -51,6 +51,7 @@ import { UpdateProductInput, UpdateProductOptionInput, UpdateProductVariantInput, + UpdateTypeInput, } from "../types" import { entityNameToLinkableKeysMap, joinerConfig } from "./../joiner-config" @@ -425,34 +426,118 @@ export default class ProductModuleService< return await this.baseRepository_.serialize(productTags) } - @InjectTransactionManager("baseRepository_") - async createTypes( + createTypes( data: ProductTypes.CreateProductTypeDTO[], + sharedContext?: Context + ): Promise + createTypes( + data: ProductTypes.CreateProductTypeDTO, + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async createTypes( + data: + | ProductTypes.CreateProductTypeDTO[] + | ProductTypes.CreateProductTypeDTO, @MedusaContext() sharedContext: Context = {} - ): Promise { - const productTypes = await this.productTypeService_.create( - data, - sharedContext - ) + ): Promise { + const input = Array.isArray(data) ? data : [data] - return await this.baseRepository_.serialize(productTypes, { - populate: true, - }) + const types = await this.productTypeService_.create(input, sharedContext) + + const createdTypes = await this.baseRepository_.serialize< + ProductTypes.ProductTypeDTO[] + >(types) + + return Array.isArray(data) ? createdTypes : createdTypes[0] } + async upsertTypes( + data: ProductTypes.UpsertProductTypeDTO[], + sharedContext?: Context + ): Promise + async upsertTypes( + data: ProductTypes.UpsertProductTypeDTO, + sharedContext?: Context + ): Promise + @InjectTransactionManager("baseRepository_") + async upsertTypes( + data: + | ProductTypes.UpsertProductTypeDTO[] + | ProductTypes.UpsertProductTypeDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter((type): type is UpdateTypeInput => !!type.id) + const forCreate = input.filter( + (type): type is ProductTypes.CreateProductTypeDTO => !type.id + ) + + let created: ProductType[] = [] + let updated: ProductType[] = [] + + if (forCreate.length) { + created = await this.productTypeService_.create(forCreate, sharedContext) + } + if (forUpdate.length) { + updated = await this.productTypeService_.update(forUpdate, sharedContext) + } + + const result = [...created, ...updated] + const allTypes = await this.baseRepository_.serialize< + ProductTypes.ProductTypeDTO[] | ProductTypes.ProductTypeDTO + >(result) + + return Array.isArray(data) ? allTypes : allTypes[0] + } + + updateTypes( + id: string, + data: ProductTypes.UpdateProductTypeDTO, + sharedContext?: Context + ): Promise + updateTypes( + selector: ProductTypes.FilterableProductTypeProps, + data: ProductTypes.UpdateProductTypeDTO, + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") async updateTypes( - data: ProductTypes.UpdateProductTypeDTO[], + idOrSelector: string | ProductTypes.FilterableProductTypeProps, + data: ProductTypes.UpdateProductTypeDTO, @MedusaContext() sharedContext: Context = {} - ): Promise { - const productTypes = await this.productTypeService_.update( - data, + ): Promise { + let normalizedInput: UpdateTypeInput[] = [] + if (isString(idOrSelector)) { + // Check if the type exists in the first place + await this.productTypeService_.retrieve(idOrSelector, {}, sharedContext) + normalizedInput = [{ id: idOrSelector, ...data }] + } else { + const types = await this.productTypeService_.list( + idOrSelector, + {}, + sharedContext + ) + + normalizedInput = types.map((type) => ({ + id: type.id, + ...data, + })) + } + + const types = await this.productTypeService_.update( + normalizedInput, sharedContext ) - return await this.baseRepository_.serialize(productTypes, { - populate: true, - }) + const updatedTypes = await this.baseRepository_.serialize< + ProductTypes.ProductTypeDTO[] + >(types) + + return isString(idOrSelector) ? updatedTypes[0] : updatedTypes } createOptions( diff --git a/packages/product/src/types/index.ts b/packages/product/src/types/index.ts index cd54191d55001..39effeece4250 100644 --- a/packages/product/src/types/index.ts +++ b/packages/product/src/types/index.ts @@ -53,6 +53,10 @@ export type UpdateCollectionInput = ProductTypes.UpdateProductCollectionDTO & { id: string } +export type UpdateTypeInput = ProductTypes.UpdateProductTypeDTO & { + id: string +} + export type UpdateProductVariantInput = ProductTypes.UpdateProductVariantDTO & { id: string product_id?: string | null diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 47b961ddc897a..8f5b63d0ecfda 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -1002,7 +1002,7 @@ export interface UpdateProductTypeDTO { /** * Holds custom data in key-value pairs. */ - metadata?: Record + metadata?: Record | null } /** diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index be253b2e4971c..7a865561b1ce0 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -31,6 +31,7 @@ import { UpsertProductCollectionDTO, UpsertProductDTO, UpsertProductOptionDTO, + UpsertProductTypeDTO, UpsertProductVariantDTO, } from "./common" @@ -1202,32 +1203,150 @@ export interface IProductModuleService extends IModuleService { ): Promise /** - * This method is used to update a product type + * This method is used to create a product type. * - * @param {UpdateProductTypeDTO[]} data - The product types to be updated, each having the attributes that should be updated in the product type. + * @param {CreateProductTypeDTO} data - The product type to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The list of updated product types. + * @returns {Promise} The created product type. * * @example * import { * initialize as initializeProductModule, * } from "@medusajs/product" * - * async function updateProductType (id: string, value: string) { + * async function createType (title: string) { * const productModule = await initializeProductModule() * - * const productTypes = await productModule.updateTypes([ + * const type = await productModule.createTypes( + * { + * value + * } + * ) + * + * // do something with the product type or return them + * } + * + */ + createTypes( + data: CreateProductTypeDTO, + sharedContext?: Context + ): Promise + + /** + * This method updates existing types, or creates new ones if they don't exist. + * + * @param {UpsertProductTypeDTO[]} data - The attributes to update or create for each type. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated and created types. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function upsertTypes (title: string) { + * const productModule = await initializeProductModule() + * + * const createdTypes = await productModule.upsertTypes([ * { - * id, * value * } * ]) * - * // do something with the product types or return them + * // do something with the types or return them + * } + */ + upsertTypes( + data: UpsertProductTypeDTO[], + sharedContext?: Context + ): Promise + + /** + * This method updates an existing type, or creates a new one if it doesn't exist. + * + * @param {UpsertProductTypeDTO} data - The attributes to update or create for the type. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated or created type. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function upsertType (title: string) { + * const productModule = await initializeProductModule() + * + * const createdType = await productModule.upsertTypes( + * { + * value + * } + * ) + * + * // do something with the type or return it + * } + */ + upsertTypes( + data: UpsertProductTypeDTO, + sharedContext?: Context + ): Promise + + /** + * This method is used to update a type. + * + * @param {string} id - The ID of the type to be updated. + * @param {UpdateProductTypeDTO} data - The attributes of the type to be updated + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated type. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function updateType (id: string, title: string) { + * const productModule = await initializeProductModule() + * + * const type = await productModule.updateTypes(id, { + * value + * } + * ) + * + * // do something with the type or return it + * } + */ + updateTypes( + id: string, + data: UpdateProductTypeDTO, + sharedContext?: Context + ): Promise + + /** + * This method is used to update a list of types determined by the selector filters. + * + * @param {FilterableProductTypeProps} selector - The filters that will determine which types will be updated. + * @param {UpdateProductTypeDTO} data - The attributes to be updated on the selected types + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated types. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function updateTypes(ids: string[], title: string) { + * const productModule = await initializeProductModule() + * + * const types = await productModule.updateTypes({id: ids}, { + * value + * } + * ) + * + * // do something with the types or return them * } */ updateTypes( - data: UpdateProductTypeDTO[], + selector: FilterableProductTypeProps, + data: UpdateProductTypeDTO, sharedContext?: Context ): Promise @@ -1251,6 +1370,74 @@ export interface IProductModuleService extends IModuleService { */ deleteTypes(productTypeIds: string[], sharedContext?: Context): Promise + /** + * This method is used to delete types. Unlike the {@link delete} method, this method won't completely remove the type. It can still be accessed or retrieved using methods like {@link retrieve} if you pass the `withDeleted` property to the `config` object parameter. + * + * The soft-deleted types can be restored using the {@link restore} method. + * + * @param {string[]} typeIds - The IDs of the types to soft-delete. + * @param {SoftDeleteReturn} config - + * Configurations determining which relations to soft delete along with the each of the types. You can pass to its `returnLinkableKeys` + * property any of the type's relation attribute names. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise | void>} + * An object that includes the IDs of related records that were also soft deleted. The object's keys are the ID attribute names of the type entity's relations, and its value is an array of strings, each being the ID of a record associated with the type through this relation. + * + * If there are no related records, the promise resolved to `void`. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function deleteTypes (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const cascadedEntities = await productModule.softDeleteTypes(ids) + * + * // do something with the returned cascaded entity IDs or return them + * } + */ + softDeleteTypes( + typeIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + /** + * This method is used to restore types which were deleted using the {@link softDelete} method. + * + * @param {string[]} typeIds - The IDs of the types to restore. + * @param {RestoreReturn} config - + * Configurations determining which relations to restore along with each of the types. You can pass to its `returnLinkableKeys` + * property any of the type's relation attribute names. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise | void>} + * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the type entity's relations, and its value is an array of strings, each being the ID of the record associated with the type through this relation. + * + * If there are no related records that were restored, the promise resolved to `void`. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function restoreTypes (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const cascadedEntities = await productModule.restoreTypes(ids, { + * returnLinkableKeys: [] + * }) + * + * // do something with the returned cascaded entity IDs or return them + * } + */ + restoreTypes( + typeIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + /** * This method is used to retrieve a product option by its ID. *