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
@@ -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,
})
}
@@ -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] })
}
@@ -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,
})
}
@@ -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,
})
}
@@ -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] })
}
@@ -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: [],
},
]
@@ -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"]
@@ -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] })
}
@@ -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
}