feat: add product admin v2 endpoints (#6579)

This implementation obviously lacks a lot of things, and there are a lot of TODOs. However, there are already a lot of questions I'd rather get answered soon, so I figured it's much easier to do the implementation in steps.

I wrote down all breaking changes, suggested changes, and new additions with comments (TODO and Note).

In a follow-up PR I will:

Add the remaining/missing models
Make the workflows handle all interactions between the different models/modules
Add integration tests
This commit is contained in:
Stevche Radevski
2024-03-05 11:24:33 +01:00
committed by GitHub
parent 7d69e6068e
commit f9ef37a2f2
35 changed files with 1924 additions and 2 deletions

View File

@@ -13,7 +13,7 @@ import { initDb, useDb } from "../../../../environment-helpers/use-db"
jest.setTimeout(50000)
describe("CreateProduct workflow", function () {
describe.skip("CreateProduct workflow", function () {
let medusaContainer
let shutdownServer

View File

@@ -14,7 +14,7 @@ import { simpleProductFactory } from "../../../../factories"
jest.setTimeout(100000)
describe("UpdateProduct workflow", function () {
describe.skip("UpdateProduct workflow", function () {
let dbConnection
let medusaContainer
let shutdownServer

View File

@@ -10,3 +10,4 @@ export * from "./user"
export * from "./tax"
export * from "./api-key"
export * from "./store"
export * from "./product"

View File

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

View File

@@ -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 createProductOptionsStepId = "create-product-options"
export const createProductOptionsStep = createStep(
createProductOptionsStepId,
async (data: ProductTypes.CreateProductOptionDTO[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const created = await service.createOptions(data)
return new StepResponse(
created,
created.map((productOption) => productOption.id)
)
},
async (createdIds, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.deleteOptions(createdIds)
}
)

View File

@@ -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 createProductVariantsStepId = "create-product-variants"
export const createProductVariantsStep = createStep(
createProductVariantsStepId,
async (data: ProductTypes.CreateProductVariantDTO[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const created = await service.createVariants(data)
return new StepResponse(
created,
created.map((productVariant) => productVariant.id)
)
},
async (createdIds, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.deleteVariants(createdIds)
}
)

View File

@@ -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 createProductsStepId = "create-products"
export const createProductsStep = createStep(
createProductsStepId,
async (data: ProductTypes.CreateProductDTO[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const created = await service.create(data)
return new StepResponse(
created,
created.map((product) => product.id)
)
},
async (createdIds, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.delete(createdIds)
}
)

View File

@@ -0,0 +1,27 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const deleteProductOptionsStepId = "delete-product-options"
export const deleteProductOptionsStep = createStep(
deleteProductOptionsStepId,
async (ids: string[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.softDeleteOptions(ids)
return new StepResponse(void 0, ids)
},
async (prevIds, { container }) => {
if (!prevIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.restoreOptions(prevIds)
}
)

View File

@@ -0,0 +1,27 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const deleteProductVariantsStepId = "delete-product-variants"
export const deleteProductVariantsStep = createStep(
deleteProductVariantsStepId,
async (ids: string[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.softDeleteVariants(ids)
return new StepResponse(void 0, ids)
},
async (prevIds, { container }) => {
if (!prevIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.restoreVariants(prevIds)
}
)

View File

@@ -0,0 +1,27 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const deleteProductsStepId = "delete-products"
export const deleteProductsStep = createStep(
deleteProductsStepId,
async (ids: string[], { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.softDelete(ids)
return new StepResponse(void 0, ids)
},
async (prevIds, { container }) => {
if (!prevIds?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.restore(prevIds)
}
)

View File

@@ -0,0 +1,9 @@
export * from "./create-products"
export * from "./update-products"
export * from "./delete-products"
export * from "./create-product-options"
export * from "./update-product-options"
export * from "./delete-product-options"
export * from "./create-product-variants"
export * from "./update-product-variants"
export * from "./delete-product-variants"

View File

@@ -0,0 +1,49 @@
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 UpdateProductOptionsStepInput = {
selector: ProductTypes.FilterableProductOptionProps
update: ProductTypes.UpdateProductOptionDTO
}
export const updateProductOptionsStepId = "update-product-options"
export const updateProductOptionsStep = createStep(
updateProductOptionsStepId,
async (data: UpdateProductOptionsStepInput, { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevData = await service.listOptions(data.selector, {
select: selects,
relations,
})
// TODO: We need to update the module's signature
// const productOptions = await service.updateOptions(data.selector, data.update)
const productOptions = []
return new StepResponse(productOptions, prevData)
},
async (prevData, { container }) => {
if (!prevData?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
// TODO: We need to update the module's signature
// await service.upsertOptions(
// prevData.map((r) => ({
// ...r,
// }))
// )
}
)

View File

@@ -0,0 +1,49 @@
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 UpdateProductVariantsStepInput = {
selector: ProductTypes.FilterableProductVariantProps
update: ProductTypes.UpdateProductVariantDTO
}
export const updateProductVariantsStepId = "update-product-variants"
export const updateProductVariantsStep = createStep(
updateProductVariantsStepId,
async (data: UpdateProductVariantsStepInput, { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevData = await service.listVariants(data.selector, {
select: selects,
relations,
})
// TODO: We need to update the module's signature
// const productVariants = await service.updateVariants(data.selector, data.update)
const productVariants = []
return new StepResponse(productVariants, prevData)
},
async (prevData, { container }) => {
if (!prevData?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
// TODO: We need to update the module's signature
// await service.upsertVariants(
// prevData.map((r) => ({
// ...r,
// }))
// )
}
)

View File

@@ -0,0 +1,49 @@
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 UpdateProductsStepInput = {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO
}
export const updateProductsStepId = "update-products"
export const updateProductsStep = createStep(
updateProductsStepId,
async (data: UpdateProductsStepInput, { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevData = await service.list(data.selector, {
select: selects,
relations,
})
// TODO: We need to update the module's signature
// const products = await service.update(data.selector, data.update)
const products = []
return new StepResponse(products, prevData)
},
async (prevData, { container }) => {
if (!prevData?.length) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
// TODO: We need to update the module's signature
// await service.upsert(
// prevData.map((r) => ({
// ...r,
// }))
// )
}
)

View File

@@ -0,0 +1,15 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { createProductOptionsStep } from "../steps"
type WorkflowInput = { product_options: ProductTypes.CreateProductOptionDTO[] }
export const createProductOptionsWorkflowId = "create-product-options"
export const createProductOptionsWorkflow = createWorkflow(
createProductOptionsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductOptionDTO[]> => {
return createProductOptionsStep(input.product_options)
}
)

View File

@@ -0,0 +1,17 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { createProductVariantsStep } from "../steps"
type WorkflowInput = {
product_variants: ProductTypes.CreateProductVariantDTO[]
}
export const createProductVariantsWorkflowId = "create-product-variants"
export const createProductVariantsWorkflow = createWorkflow(
createProductVariantsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
return createProductVariantsStep(input.product_variants)
}
)

View File

@@ -0,0 +1,15 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { createProductsStep } from "../steps"
type WorkflowInput = { products: ProductTypes.CreateProductDTO[] }
export const createProductsWorkflowId = "create-products"
export const createProductsWorkflow = createWorkflow(
createProductsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductDTO[]> => {
return createProductsStep(input.products)
}
)

View File

@@ -0,0 +1,12 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { deleteProductOptionsStep } from "../steps"
type WorkflowInput = { ids: string[] }
export const deleteProductOptionsWorkflowId = "delete-product-options"
export const deleteProductOptionsWorkflow = createWorkflow(
deleteProductOptionsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
return deleteProductOptionsStep(input.ids)
}
)

View File

@@ -0,0 +1,12 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { deleteProductVariantsStep } from "../steps"
type WorkflowInput = { ids: string[] }
export const deleteProductVariantsWorkflowId = "delete-product-variants"
export const deleteProductVariantsWorkflow = createWorkflow(
deleteProductVariantsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
return deleteProductVariantsStep(input.ids)
}
)

View File

@@ -0,0 +1,12 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { deleteProductsStep } from "../steps"
type WorkflowInput = { ids: string[] }
export const deleteProductsWorkflowId = "delete-products"
export const deleteProductsWorkflow = createWorkflow(
deleteProductsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
return deleteProductsStep(input.ids)
}
)

View File

@@ -0,0 +1,9 @@
export * from "./create-products"
export * from "./delete-products"
export * from "./update-products"
export * from "./create-product-options"
export * from "./delete-product-options"
export * from "./update-product-options"
export * from "./create-product-variants"
export * from "./delete-product-variants"
export * from "./update-product-variants"

View File

@@ -0,0 +1,20 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateProductOptionsStep } from "../steps"
type UpdateProductOptionsStepInput = {
selector: ProductTypes.FilterableProductOptionProps
update: ProductTypes.UpdateProductOptionDTO
}
type WorkflowInput = UpdateProductOptionsStepInput
export const updateProductOptionsWorkflowId = "update-product-options"
export const updateProductOptionsWorkflow = createWorkflow(
updateProductOptionsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductOptionDTO[]> => {
return updateProductOptionsStep(input)
}
)

View File

@@ -0,0 +1,20 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateProductVariantsStep } from "../steps"
type UpdateProductVariantsStepInput = {
selector: ProductTypes.FilterableProductVariantProps
update: ProductTypes.UpdateProductVariantDTO
}
type WorkflowInput = UpdateProductVariantsStepInput
export const updateProductVariantsWorkflowId = "update-product-variants"
export const updateProductVariantsWorkflow = createWorkflow(
updateProductVariantsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
return updateProductVariantsStep(input)
}
)

View File

@@ -0,0 +1,20 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateProductsStep } from "../steps"
type UpdateProductsStepInput = {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO
}
type WorkflowInput = UpdateProductsStepInput
export const updateProductsWorkflowId = "update-products"
export const updateProductsWorkflow = createWorkflow(
updateProductsWorkflowId,
(
input: WorkflowData<WorkflowInput>
): WorkflowData<ProductTypes.ProductDTO[]> => {
return updateProductsStep(input)
}
)

View File

@@ -0,0 +1,82 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import {
deleteProductOptionsWorkflow,
updateProductOptionsWorkflow,
} from "@medusajs/core-flows"
import { UpdateProductDTO } from "@medusajs/types"
import { defaultAdminProductsOptionFields } from "../../../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
// TODO: Should we allow fetching a option without knowing the product ID? In such case we'll need to change the route to /admin/products/options/:id
const productId = req.params.id
const optionId = req.params.option_id
const variables = { id: optionId, product_id: productId }
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_option",
variables,
fields: defaultAdminProductsOptionFields,
})
const [product_option] = await remoteQuery(queryObject)
res.status(200).json({ product_option })
}
export const POST = async (
req: AuthenticatedMedusaRequest<UpdateProductDTO>,
res: MedusaResponse
) => {
// TODO: Should we allow fetching a option without knowing the product ID? In such case we'll need to change the route to /admin/products/options/:id
const productId = req.params.id
const optionId = req.params.option_id
const { result, errors } = await updateProductOptionsWorkflow(req.scope).run({
input: {
selector: { id: optionId, product_id: productId },
update: req.validatedBody,
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ product_option: result[0] })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
// TODO: Should we allow fetching a option without knowing the product ID? In such case we'll need to change the route to /admin/products/options/:id
const productId = req.params.id
const optionId = req.params.option_id
// TODO: I believe here we cannot even enforce the product ID based on the standard API we provide?
const { errors } = await deleteProductOptionsWorkflow(req.scope).run({
input: { ids: [optionId] /* product_id: productId */ },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
id: optionId,
object: "product_option",
deleted: true,
})
}

View File

@@ -0,0 +1,59 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import { CreateProductOptionDTO } from "@medusajs/types"
import { createProductOptionsWorkflow } from "@medusajs/core-flows"
import { defaultAdminProductsOptionFields } from "../../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const productId = req.params.id
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_option",
variables: {
filters: { ...req.filterableFields, product_id: productId },
order: req.listConfig.order,
skip: req.listConfig.skip,
take: req.listConfig.take,
},
fields: defaultAdminProductsOptionFields,
})
const { rows: product_options, metadata } = await remoteQuery(queryObject)
res.json({
product_options,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<CreateProductOptionDTO>,
res: MedusaResponse
) => {
const input = [
{
...req.validatedBody,
},
]
const { result, errors } = await createProductOptionsWorkflow(req.scope).run({
input: { product_options: input },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ product_option: result[0] })
}

View File

@@ -0,0 +1,72 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import {
deleteProductsWorkflow,
updateProductsWorkflow,
} from "@medusajs/core-flows"
import { UpdateProductDTO } from "@medusajs/types"
import { defaultAdminProductFields } from "../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
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",
variables,
fields: defaultAdminProductFields,
})
const [product] = await remoteQuery(queryObject)
res.status(200).json({ product })
}
export const POST = async (
req: AuthenticatedMedusaRequest<UpdateProductDTO>,
res: MedusaResponse
) => {
const { result, errors } = await updateProductsWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ product: result[0] })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const id = req.params.id
const { errors } = await deleteProductsWorkflow(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",
deleted: true,
})
}

View File

@@ -0,0 +1,84 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import {
deleteProductVariantsWorkflow,
updateProductVariantsWorkflow,
} from "@medusajs/core-flows"
import { UpdateProductVariantDTO } from "@medusajs/types"
import { defaultAdminProductsVariantFields } from "../../../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
// TODO: Should we allow fetching a variant without knowing the product ID? In such case we'll need to change the route to /admin/products/variants/:id
const productId = req.params.id
const variantId = req.params.variant_id
const variables = { id: variantId, product_id: productId }
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_variant",
variables,
fields: defaultAdminProductsVariantFields,
})
const [product_variant] = await remoteQuery(queryObject)
res.status(200).json({ product_variant })
}
export const POST = async (
req: AuthenticatedMedusaRequest<UpdateProductVariantDTO>,
res: MedusaResponse
) => {
// TODO: Should we allow fetching a variant without knowing the product ID? In such case we'll need to change the route to /admin/products/variants/:id
const productId = req.params.id
const variantId = req.params.variant_id
const { result, errors } = await updateProductVariantsWorkflow(req.scope).run(
{
input: {
selector: { id: variantId, product_id: productId },
update: req.validatedBody,
},
throwOnError: false,
}
)
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ product_variant: result[0] })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
// TODO: Should we allow fetching a variant without knowing the product ID? In such case we'll need to change the route to /admin/products/variants/:id
const productId = req.params.id
const variantId = req.params.variant_id
// TODO: I believe here we cannot even enforce the product ID based on the standard API we provide?
const { errors } = await deleteProductVariantsWorkflow(req.scope).run({
input: { ids: [variantId] /* product_id: productId */ },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
id: variantId,
object: "product_variant",
deleted: true,
})
}

View File

@@ -0,0 +1,61 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import { CreateProductVariantDTO } from "@medusajs/types"
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
import { defaultAdminProductsVariantFields } from "../../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const productId = req.params.id
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_variant",
variables: {
filters: { ...req.filterableFields, product_id: productId },
order: req.listConfig.order,
skip: req.listConfig.skip,
take: req.listConfig.take,
},
fields: defaultAdminProductsVariantFields,
})
const { rows: product_variants, metadata } = await remoteQuery(queryObject)
res.json({
product_variants,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<CreateProductVariantDTO>,
res: MedusaResponse
) => {
const input = [
{
...req.validatedBody,
},
]
const { result, errors } = await createProductVariantsWorkflow(req.scope).run(
{
input: { product_variants: input },
throwOnError: false,
}
)
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ product_variant: result[0] })
}

View File

@@ -0,0 +1,139 @@
import * as QueryConfig from "./query-config"
import {
AdminGetProductsOptionsParams,
AdminGetProductsParams,
AdminGetProductsProductOptionsOptionParams,
AdminGetProductsProductParams,
AdminGetProductsProductVariantsVariantParams,
AdminGetProductsVariantsParams,
AdminPostProductsProductOptionsOptionReq,
AdminPostProductsProductOptionsReq,
AdminPostProductsProductReq,
AdminPostProductsProductVariantsReq,
AdminPostProductsProductVariantsVariantReq,
AdminPostProductsReq,
} from "./validators"
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["ALL"],
matcher: "/admin/products*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
},
{
method: ["GET"],
matcher: "/admin/products",
middlewares: [
transformQuery(
AdminGetProductsParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/products/:id",
middlewares: [
transformQuery(
AdminGetProductsProductParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/products",
middlewares: [transformBody(AdminPostProductsReq)],
},
{
method: ["POST"],
matcher: "/admin/products/:id",
middlewares: [transformBody(AdminPostProductsProductReq)],
},
{
method: ["DELETE"],
matcher: "/admin/products/:id",
middlewares: [],
},
{
method: ["GET"],
matcher: "/admin/products/:id/variants",
middlewares: [
transformQuery(
AdminGetProductsVariantsParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
// Note: New endpoint in v2
{
method: ["GET"],
matcher: "/admin/products/:id/variants/:variant_id",
middlewares: [
transformQuery(
AdminGetProductsProductVariantsVariantParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id/variants",
middlewares: [transformBody(AdminPostProductsProductVariantsReq)],
},
{
method: ["POST"],
matcher: "/admin/products/:id/variants/:variant_id",
middlewares: [transformBody(AdminPostProductsProductVariantsVariantReq)],
},
{
method: ["DELETE"],
matcher: "/admin/products/:id/variants/:variant_id",
middlewares: [],
},
// Note: New endpoint in v2
{
method: ["GET"],
matcher: "/admin/products/:id/options",
middlewares: [
transformQuery(
AdminGetProductsOptionsParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
// Note: New endpoint in v2
{
method: ["GET"],
matcher: "/admin/products/:id/options/:option_id",
middlewares: [
transformQuery(
AdminGetProductsProductOptionsOptionParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id/options",
middlewares: [transformBody(AdminPostProductsProductOptionsReq)],
},
{
method: ["POST"],
matcher: "/admin/products/:id/options/:option_id",
middlewares: [transformBody(AdminPostProductsProductOptionsOptionReq)],
},
{
method: ["DELETE"],
matcher: "/admin/products/:id/options/:option_id",
middlewares: [],
},
]

View File

@@ -0,0 +1,85 @@
export const defaultAdminProductRelations = [
"variants",
// TODO: Add in next iteration
// "variants.prices",
// TODO: See how this should be handled
// "variants.options",
"images",
// TODO: What is this?
// "profiles",
"options",
// TODO: See how this should be handled
// "options.values",
// TODO: Handle in next iteration
// "tags",
// "type",
// "collection",
]
export const allowedAdminProductRelations = [...defaultAdminProductRelations]
export const defaultAdminProductFields = [
"id",
"title",
"subtitle",
"status",
"external_id",
"description",
"handle",
"is_giftcard",
"discountable",
"thumbnail",
// TODO: Handle in next iteration
// "collection_id",
// "type_id",
"weight",
"length",
"height",
"width",
"hs_code",
"origin_country",
"mid_code",
"material",
"created_at",
"updated_at",
"deleted_at",
"metadata",
]
export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminProductFields,
defaultRelations: defaultAdminProductRelations,
allowedRelations: allowedAdminProductRelations,
isList: false,
}
export const listTransformQueryConfig = {
defaultLimit: 50,
isList: true,
}
export const defaultAdminProductsVariantFields = [
"id",
"product_id",
"title",
"sku",
"inventory_quantity",
"allow_backorder",
"manage_inventory",
"hs_code",
"origin_country",
"mid_code",
"material",
"weight",
"length",
"height",
"width",
"created_at",
"updated_at",
"deleted_at",
"metadata",
"variant_rank",
"ean",
"upc",
"barcode",
]
export const defaultAdminProductsOptionFields = ["id", "title"]

View File

@@ -0,0 +1,58 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { CreateProductDTO } from "@medusajs/types"
import { createProductsWorkflow } from "@medusajs/core-flows"
import { defaultAdminProductFields } from "./query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const queryObject = remoteQueryObjectFromString({
entryPoint: "product",
variables: {
filters: req.filterableFields,
order: req.listConfig.order,
skip: req.listConfig.skip,
take: req.listConfig.take,
},
fields: defaultAdminProductFields,
})
const { rows: products, metadata } = await remoteQuery(queryObject)
res.json({
products,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<CreateProductDTO>,
res: MedusaResponse
) => {
const input = [
{
...req.validatedBody,
},
]
const { result, errors } = await createProductsWorkflow(req.scope).run({
input: { products: input },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ product: result[0] })
}

View File

@@ -0,0 +1,662 @@
import { OperatorMap } from "@medusajs/types"
import { Transform, Type } from "class-transformer"
import {
IsArray,
IsBoolean,
IsEnum,
IsNumber,
IsObject,
IsOptional,
IsString,
NotEquals,
ValidateIf,
ValidateNested,
} from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
import { ProductStatus } from "@medusajs/utils"
import { IsType } from "../../../utils"
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
export class AdminGetProductsProductParams extends FindParams {}
export class AdminGetProductsProductVariantsVariantParams extends FindParams {}
export class AdminGetProductsProductOptionsOptionParams extends FindParams {}
/**
* Parameters used to filter and configure the pagination of the retrieved regions.
*/
export class AdminGetProductsParams extends extendedFindParamsMixin({
limit: 50,
offset: 0,
}) {
// TODO: Will search be handled the same way? Should it be part of the `findParams` class instead, or the mixin?
/**
* Search term to search products' title, description, variants' title and sku, and collections' title.
*/
@IsString()
@IsOptional()
q?: string
/**
* IDs to filter products by.
*/
@IsOptional()
@IsType([String, [String]])
id?: string | string[]
/**
* Statuses to filter products by.
*/
@IsOptional()
@IsEnum(ProductStatus, { each: true })
status?: ProductStatus[]
/**
* Title to filter products by.
*/
@IsString()
@IsOptional()
title?: string
/**
* Handle to filter products by.
*/
@IsString()
@IsOptional()
handle?: string
// TODO: Should we remove this? It makes sense for search, but not for equality comparison
/**
* Description to filter products by.
*/
@IsString()
@IsOptional()
description?: string
/**
* Filter products by whether they're gift cards.
*/
@IsBoolean()
@IsOptional()
@Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
is_giftcard?: boolean
// TODO: Add in next iteration
// /**
// * Filter products by their associated price lists' ID.
// */
// @IsArray()
// @IsOptional()
// price_list_id?: string[]
// /**
// * Filter products by their associated product collection's ID.
// */
// @IsArray()
// @IsOptional()
// collection_id?: string[]
// /**
// * Filter products by their associated tags' value.
// */
// @IsArray()
// @IsOptional()
// tags?: string[]
// /**
// * Filter products by their associated product type's ID.
// */
// @IsArray()
// @IsOptional()
// type_id?: string[]
// /**
// * Filter products by their associated sales channels' ID.
// */
// @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [IsOptional(), IsArray()])
// sales_channel_id?: string[]
// /**
// * Filter products by their associated discount condition's ID.
// */
// @IsString()
// @IsOptional()
// discount_condition_id?: string
// /**
// * Filter products by their associated product category's ID.
// */
// @IsArray()
// @IsOptional()
// category_id?: string[]
// /**
// * Whether to include product category children in the response.
// *
// * @featureFlag product_categories
// */
// @IsBoolean()
// @IsOptional()
// @Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
// include_category_children?: boolean
// TODO: The OperatorMap and DateOperator are slightly different, so the date comparisons is a breaking change.
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
created_at?: OperatorMap<string>
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
updated_at?: OperatorMap<string>
@ValidateNested()
@IsOptional()
@Type(() => OperatorMapValidator)
deleted_at?: OperatorMap<string>
// Note: These are new in v2
// Additional filters from BaseFilterable
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetProductsParams)
$and?: AdminGetProductsParams[]
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetProductsParams)
$or?: AdminGetProductsParams[]
}
export class AdminGetProductsVariantsParams extends extendedFindParamsMixin({
limit: 50,
offset: 0,
}) {
/**
* Search term to search product variants' title, sku, and products' title.
*/
@IsString()
@IsOptional()
q?: string
/**
* IDs to filter product variants by.
*/
@IsOptional()
@IsType([String, [String]])
id?: string | string[]
// TODO: This should be part of the Mixin or base FindParams
// /**
// * The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
// */
// @IsString()
// @IsOptional()
// order?: string
/**
* Filter product variants by whether their inventory is managed or not.
*/
@IsBoolean()
@IsOptional()
@Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
manage_inventory?: boolean
/**
* Filter product variants by whether they are allowed to be backordered or not.
*/
@IsBoolean()
@IsOptional()
@Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
allow_backorder?: boolean
// TODO: The OperatorMap and DateOperator are slightly different, so the date comparisons is a breaking change.
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
created_at?: OperatorMap<string>
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
updated_at?: OperatorMap<string>
@ValidateNested()
@IsOptional()
@Type(() => OperatorMapValidator)
deleted_at?: OperatorMap<string>
// Note: These are new in v2
// Additional filters from BaseFilterable
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetProductsVariantsParams)
$and?: AdminGetProductsVariantsParams[]
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetProductsVariantsParams)
$or?: AdminGetProductsVariantsParams[]
}
// Note: This model and endpoint are new in v2
export class AdminGetProductsOptionsParams extends extendedFindParamsMixin({
limit: 50,
offset: 0,
}) {
@IsOptional()
@IsString()
title?: string
// Note: These are new in v2
// Additional filters from BaseFilterable
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetProductsOptionsParams)
$and?: AdminGetProductsOptionsParams[]
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetProductsOptionsParams)
$or?: AdminGetProductsOptionsParams[]
}
export class AdminPostProductsReq {
@IsString()
title: string
@IsString()
@IsOptional()
subtitle?: string
@IsString()
@IsOptional()
description?: string
@IsBoolean()
is_giftcard = false
@IsBoolean()
discountable = true
@IsArray()
@IsOptional()
images?: string[]
@IsString()
@IsOptional()
thumbnail?: string
@IsString()
@IsOptional()
handle?: string
@IsOptional()
@IsEnum(ProductStatus)
status?: ProductStatus = ProductStatus.DRAFT
// TODO: Add in next iteration
// @IsOptional()
// @Type(() => ProductTypeReq)
// @ValidateNested()
// type?: ProductTypeReq
// @IsOptional()
// @IsString()
// collection_id?: string
// @IsOptional()
// @Type(() => ProductTagReq)
// @ValidateNested({ each: true })
// @IsArray()
// tags?: ProductTagReq[]
// @IsOptional()
// @Type(() => ProductProductCategoryReq)
// @ValidateNested({ each: true })
// @IsArray()
// categories?: ProductProductCategoryReq[]
// TODO: Deal with in next iteration
// @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [
// IsOptional(),
// Type(() => ProductSalesChannelReq),
// ValidateNested({ each: true }),
// IsArray(),
// ])
// sales_channels?: ProductSalesChannelReq[]
// TODO: I suggest we don't allow creation options and variants in 1 call, but rather do it through separate endpoints.
@IsOptional()
@Type(() => AdminPostProductsProductOptionsReq)
@ValidateNested({ each: true })
@IsArray()
options?: AdminPostProductsProductOptionsReq[]
@IsOptional()
@Type(() => AdminPostProductsProductVariantsReq)
@ValidateNested({ each: true })
@IsArray()
variants?: AdminPostProductsProductVariantsReq[]
@IsNumber()
@IsOptional()
weight?: number
@IsNumber()
@IsOptional()
length?: number
@IsNumber()
@IsOptional()
height?: number
@IsNumber()
@IsOptional()
width?: number
@IsString()
@IsOptional()
hs_code?: string
@IsString()
@IsOptional()
origin_country?: string
@IsString()
@IsOptional()
mid_code?: string
@IsString()
@IsOptional()
material?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
export class AdminPostProductsProductReq {
@IsString()
@IsOptional()
title?: string
@IsString()
@IsOptional()
subtitle?: string
@IsString()
@IsOptional()
description?: string
@IsBoolean()
@IsOptional()
discountable?: boolean
@IsArray()
@IsOptional()
images?: string[]
@IsString()
@IsOptional()
thumbnail?: string
@IsString()
@IsOptional()
handle?: string
@IsEnum(ProductStatus)
@NotEquals(null)
@ValidateIf((_, value) => value !== undefined)
status?: ProductStatus
// TODO: Deal with in next iteration
// @IsOptional()
// @Type(() => ProductTypeReq)
// @ValidateNested()
// type?: ProductTypeReq
// @IsOptional()
// @IsString()
// collection_id?: string
// @IsOptional()
// @Type(() => ProductTagReq)
// @ValidateNested({ each: true })
// @IsArray()
// tags?: ProductTagReq[]
// @IsOptional()
// @Type(() => ProductProductCategoryReq)
// @ValidateNested({ each: true })
// @IsArray()
// categories?: ProductProductCategoryReq[]
// @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [
// IsOptional(),
// Type(() => ProductSalesChannelReq),
// ValidateNested({ each: true }),
// IsArray(),
// ])
// sales_channels?: ProductSalesChannelReq[] | null
// TODO: Should we remove this on update?
// @IsOptional()
// @Type(() => ProductVariantReq)
// @ValidateNested({ each: true })
// @IsArray()
// variants?: ProductVariantReq[]
@IsNumber()
@IsOptional()
weight?: number
@IsNumber()
@IsOptional()
length?: number
@IsNumber()
@IsOptional()
height?: number
@IsNumber()
@IsOptional()
width?: number
@IsString()
@IsOptional()
hs_code?: string
@IsString()
@IsOptional()
origin_country?: string
@IsString()
@IsOptional()
mid_code?: string
@IsString()
@IsOptional()
material?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
export class AdminPostProductsProductVariantsReq {
@IsString()
title: string
@IsString()
@IsOptional()
sku?: string
@IsString()
@IsOptional()
ean?: string
@IsString()
@IsOptional()
upc?: string
@IsString()
@IsOptional()
barcode?: string
@IsString()
@IsOptional()
hs_code?: string
@IsString()
@IsOptional()
mid_code?: string
@IsNumber()
@IsOptional()
inventory_quantity?: number = 0
@IsBoolean()
@IsOptional()
allow_backorder?: boolean
@IsBoolean()
@IsOptional()
manage_inventory?: boolean = true
@IsNumber()
@IsOptional()
weight?: number
@IsNumber()
@IsOptional()
length?: number
@IsNumber()
@IsOptional()
height?: number
@IsNumber()
@IsOptional()
width?: number
@IsString()
@IsOptional()
origin_country?: string
@IsString()
@IsOptional()
material?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
// TODO: Add on next iteration
// @IsArray()
// @ValidateNested({ each: true })
// @Type(() => ProductVariantPricesCreateReq)
// prices: ProductVariantPricesCreateReq[]
// TODO: Think how these link to the `options` on the product-level
// @IsOptional()
// @Type(() => ProductVariantOptionReq)
// @ValidateNested({ each: true })
// @IsArray()
// options?: ProductVariantOptionReq[] = []
}
export class AdminPostProductsProductVariantsVariantReq {
@IsString()
@IsOptional()
title?: string
@IsString()
@IsOptional()
sku?: string
@IsString()
@IsOptional()
ean?: string
@IsString()
@IsOptional()
upc?: string
@IsString()
@IsOptional()
barcode?: string
@IsString()
@IsOptional()
hs_code?: string
@IsString()
@IsOptional()
mid_code?: string
@IsNumber()
@IsOptional()
inventory_quantity?: number
@IsBoolean()
@IsOptional()
allow_backorder?: boolean
@IsBoolean()
@IsOptional()
manage_inventory?: boolean
@IsNumber()
@IsOptional()
weight?: number
@IsNumber()
@IsOptional()
length?: number
@IsNumber()
@IsOptional()
height?: number
@IsNumber()
@IsOptional()
width?: number
@IsString()
@IsOptional()
origin_country?: string
@IsString()
@IsOptional()
material?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
// TODO: Deal with in next iteration
// @IsArray()
// @IsOptional()
// @ValidateNested({ each: true })
// @Type(() => ProductVariantPricesUpdateReq)
// prices?: ProductVariantPricesUpdateReq[]
// TODO: Align handling with the create case.
// @Type(() => ProductVariantOptionReq)
// @ValidateNested({ each: true })
// @IsOptional()
// @IsArray()
// options?: ProductVariantOptionReq[] = []
}
export class AdminPostProductsProductOptionsReq {
@IsString()
title: string
}
export class AdminPostProductsProductOptionsOptionReq {
@IsString()
title: string
}

View File

@@ -5,6 +5,7 @@ import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares"
import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares"
import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares"
import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares"
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares"
import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares"
@@ -41,5 +42,6 @@ export const config: MiddlewaresConfig = {
...adminStoreRoutesMiddlewares,
...adminCurrencyRoutesMiddlewares,
...storeCurrencyRoutesMiddlewares,
...adminProductRoutesMiddlewares,
],
}

View File

@@ -1332,6 +1332,74 @@ export interface IProductModuleService extends IModuleService {
sharedContext?: Context
): Promise<void>
/**
* This method is used to delete options. Unlike the {@link delete} method, this method won't completely remove the option. 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 options can be restored using the {@link restore} method.
*
* @param {string[]} optionIds - The IDs of the options to soft-delete.
* @param {SoftDeleteReturn<TReturnableLinkableKeys>} config -
* Configurations determining which relations to soft delete along with the each of the options. You can pass to its `returnLinkableKeys`
* property any of the option's relation attribute names, such as `option_value_id`.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<Record<string, string[]> | 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 option entity's relations, and its value is an array of strings, each being the ID of a record associated with the option through this relation.
*
* If there are no related records, the promise resolved to `void`.
*
* @example
* import {
* initialize as initializeProductModule,
* } from "@medusajs/product"
*
* async function deleteOptions (ids: string[]) {
* const productModule = await initializeProductModule()
*
* const cascadedEntities = await productModule.softDeleteOptions(ids)
*
* // do something with the returned cascaded entity IDs or return them
* }
*/
softDeleteOptions<TReturnableLinkableKeys extends string = string>(
optionIds: string[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* This method is used to restore options which were deleted using the {@link softDelete} method.
*
* @param {string[]} optionIds - The IDs of the options to restore.
* @param {RestoreReturn<TReturnableLinkableKeys>} config -
* Configurations determining which relations to restore along with each of the options. You can pass to its `returnLinkableKeys`
* property any of the option'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<Record<string, string[]> | void>}
* An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the option entity's relations, and its value is an array of strings, each being the ID of the record associated with the option 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 restoreOptions (ids: string[]) {
* const productModule = await initializeProductModule()
*
* const cascadedEntities = await productModule.restoreOptions(ids, {
* returnLinkableKeys: ["option_value_id"]
* })
*
* // do something with the returned cascaded entity IDs or return them
* }
*/
restoreOptions<TReturnableLinkableKeys extends string = string>(
optionIds: string[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* This method is used to retrieve a product variant by its ID.
*
@@ -1674,6 +1742,74 @@ export interface IProductModuleService extends IModuleService {
sharedContext?: Context
): Promise<[ProductVariantDTO[], number]>
/**
* This method is used to delete variants. Unlike the {@link delete} method, this method won't completely remove the variant. 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 variants can be restored using the {@link restore} method.
*
* @param {string[]} variantIds - The IDs of the variants to soft-delete.
* @param {SoftDeleteReturn<TReturnableLinkableKeys>} config -
* Configurations determining which relations to soft delete along with the each of the variants. You can pass to its `returnLinkableKeys`
* property any of the variant's relation attribute names, such as `option_value_id`.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<Record<string, string[]> | 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 variant entity's relations, and its value is an array of strings, each being the ID of a record associated with the variant through this relation.
*
* If there are no related records, the promise resolved to `void`.
*
* @example
* import {
* initialize as initializeProductModule,
* } from "@medusajs/product"
*
* async function deleteProductVariants (ids: string[]) {
* const productModule = await initializeProductModule()
*
* const cascadedEntities = await productModule.softDeleteVariants(ids)
*
* // do something with the returned cascaded entity IDs or return them
* }
*/
softDeleteVariants<TReturnableLinkableKeys extends string = string>(
variantIds: string[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* This method is used to restore variants which were deleted using the {@link softDelete} method.
*
* @param {string[]} variantIds - The IDs of the variants to restore.
* @param {RestoreReturn<TReturnableLinkableKeys>} config -
* Configurations determining which relations to restore along with each of the variants. You can pass to its `returnLinkableKeys`
* property any of the variant'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<Record<string, string[]> | void>}
* An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the variant entity's relations, and its value is an array of strings, each being the ID of the record associated with the variant 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 restoreVariants (ids: string[]) {
* const productModule = await initializeProductModule()
*
* const cascadedEntities = await productModule.restoreVariants(ids, {
* returnLinkableKeys: ["option_value_id"]
* })
*
* // do something with the returned cascaded entity IDs or return them
* }
*/
restoreVariants<TReturnableLinkableKeys extends string = string>(
variantIds: string[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* This method is used to retrieve a product collection by its ID.
*