fix: Ensure sales channel updates don't remove sales channel on other products (#7510)
* fix: Make all product tests pass * fix: Ensure product update doesnt remove sales channels on other products
This commit is contained in:
1 parent
dbb23d20be
commit
af7a885b5b
6 files changed
+190
-54
No files matched your search
@@ -45,7 +45,6 @@ const getProductFixture = (overrides) => ({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
@@ -1075,13 +1074,15 @@ medusaIntegrationTestRunner({
|
|||||||
return [baseProduct.id, salesChannel.id]
|
return [baseProduct.id, salesChannel.id]
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const salesChannel = await simpleSalesChannelFactory(
|
const salesChannel = (
|
||||||
dbConnection,
|
await api.post(
|
||||||
{
|
"/admin/sales-channels",
|
||||||
name: "test name",
|
{
|
||||||
description: "test description",
|
name: "Sales",
|
||||||
}
|
},
|
||||||
)
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
|
||||||
// Currently the product update doesn't support managing sales channels
|
// Currently the product update doesn't support managing sales channels
|
||||||
const newProduct = (
|
const newProduct = (
|
||||||
@@ -1423,7 +1424,6 @@ medusaIntegrationTestRunner({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1446,6 +1446,7 @@ medusaIntegrationTestRunner({
|
|||||||
it("Sets variant ranks when creating a product", async () => {
|
it("Sets variant ranks when creating a product", async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
title: "Test product - 1",
|
title: "Test product - 1",
|
||||||
|
handle: "test-1",
|
||||||
description: "test-product-description 1",
|
description: "test-product-description 1",
|
||||||
images: breaking(
|
images: breaking(
|
||||||
() => ["test-image.png", "test-image-2.png"],
|
() => ["test-image.png", "test-image-2.png"],
|
||||||
@@ -1456,12 +1457,10 @@ medusaIntegrationTestRunner({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Test variant 1",
|
title: "Test variant 1",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Test variant 2",
|
title: "Test variant 2",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1632,7 +1631,6 @@ medusaIntegrationTestRunner({
|
|||||||
upc: "test-upc",
|
upc: "test-upc",
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
id: baseProduct.variants[0].id,
|
id: baseProduct.variants[0].id,
|
||||||
inventory_quantity: 10,
|
|
||||||
manage_inventory: true,
|
manage_inventory: true,
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
@@ -1866,6 +1864,83 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("updates multiple products that have the same sales channel", async () => {
|
||||||
|
const salesChannel = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{
|
||||||
|
name: "Sales",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}`,
|
||||||
|
{
|
||||||
|
sales_channels: [{ id: salesChannel.id }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${proposedProduct.id}`,
|
||||||
|
{
|
||||||
|
sales_channels: [{ id: salesChannel.id }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
let res = await api.get(
|
||||||
|
`/admin/products?fields=*sales_channels&sales_channel_id[]=${salesChannel.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.products).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: baseProduct.id,
|
||||||
|
sales_channels: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel.id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: proposedProduct.id,
|
||||||
|
sales_channels: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel.id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${proposedProduct.id}`,
|
||||||
|
{
|
||||||
|
sales_channels: [],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
res = await api.get(
|
||||||
|
`/admin/products?fields=*sales_channels&sales_channel_id[]=${salesChannel.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.products).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: baseProduct.id,
|
||||||
|
sales_channels: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel.id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("fails to update product with invalid status", async () => {
|
it("fails to update product with invalid status", async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
status: null,
|
status: null,
|
||||||
@@ -1896,15 +1971,12 @@ medusaIntegrationTestRunner({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "first",
|
title: "first",
|
||||||
inventory_quantity: 10,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "second",
|
title: "second",
|
||||||
inventory_quantity: 10,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "third",
|
title: "third",
|
||||||
inventory_quantity: 10,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -2528,7 +2600,6 @@ medusaIntegrationTestRunner({
|
|||||||
ean: "new-ean",
|
ean: "new-ean",
|
||||||
upc: "new-upc",
|
upc: "new-upc",
|
||||||
barcode: "new-barcode",
|
barcode: "new-barcode",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
@@ -2790,7 +2861,6 @@ medusaIntegrationTestRunner({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -2821,7 +2891,6 @@ medusaIntegrationTestRunner({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -2833,7 +2902,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(error.response.data.message).toMatch(
|
expect(error.response.data.message).toMatch(
|
||||||
breaking(
|
breaking(
|
||||||
() => "Product with handle base-product already exists.",
|
() => "Product with handle base-product already exists.",
|
||||||
() => "Product with handle: base-product already exists."
|
() => "Product with handle: base-product, already exists."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2890,7 +2959,7 @@ medusaIntegrationTestRunner({
|
|||||||
() =>
|
() =>
|
||||||
`Product_collection with handle ${baseCollection.handle} already exists.`,
|
`Product_collection with handle ${baseCollection.handle} already exists.`,
|
||||||
() =>
|
() =>
|
||||||
`Product collection with handle: ${baseCollection.handle} already exists.`
|
`Product collection with handle: ${baseCollection.handle}, already exists.`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3059,7 +3128,6 @@ medusaIntegrationTestRunner({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Variant 1",
|
title: "Variant 1",
|
||||||
inventory_quantity: 5,
|
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
@@ -3069,7 +3137,6 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Variant 2",
|
title: "Variant 2",
|
||||||
inventory_quantity: 20,
|
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
@@ -3090,7 +3157,6 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const createPayload = {
|
const createPayload = {
|
||||||
title: "Test batch create variant",
|
title: "Test batch create variant",
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
|
|||||||
@@ -5,29 +5,40 @@ const DB_USERNAME = process.env.DB_USERNAME
|
|||||||
const DB_PASSWORD = process.env.DB_PASSWORD
|
const DB_PASSWORD = process.env.DB_PASSWORD
|
||||||
const DB_NAME = process.env.DB_TEMP_NAME
|
const DB_NAME = process.env.DB_TEMP_NAME
|
||||||
const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`
|
const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`
|
||||||
|
|
||||||
const redisUrl = process.env.REDIS_URL
|
|
||||||
const cacheTTL = process.env.CACHE_TTL ?? 15
|
|
||||||
const enableResponseCompression =
|
|
||||||
process.env.ENABLE_RESPONSE_COMPRESSION || true
|
|
||||||
const enableMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
|
|
||||||
|
|
||||||
process.env.POSTGRES_URL = DB_URL
|
process.env.POSTGRES_URL = DB_URL
|
||||||
process.env.LOG_LEVEL = "error"
|
process.env.LOG_LEVEL = "error"
|
||||||
|
|
||||||
|
const enableMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
|
||||||
|
|
||||||
|
const customPaymentProvider = {
|
||||||
|
resolve: {
|
||||||
|
services: [require("@medusajs/payment/dist/providers/system").default],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
config: {
|
||||||
|
default_2: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const customFulfillmentProvider = {
|
||||||
|
resolve: "@medusajs/fulfillment-manual",
|
||||||
|
options: {
|
||||||
|
config: {
|
||||||
|
"test-provider": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [],
|
|
||||||
admin: {
|
admin: {
|
||||||
disable: true,
|
disable: true,
|
||||||
},
|
},
|
||||||
|
plugins: [],
|
||||||
projectConfig: {
|
projectConfig: {
|
||||||
redisUrl: redisUrl,
|
|
||||||
databaseUrl: DB_URL,
|
databaseUrl: DB_URL,
|
||||||
databaseType: "postgres",
|
databaseType: "postgres",
|
||||||
http: {
|
http: {
|
||||||
compression: {
|
|
||||||
enabled: enableResponseCompression,
|
|
||||||
},
|
|
||||||
jwtSecret: "test",
|
jwtSecret: "test",
|
||||||
cookieSecret: "test",
|
cookieSecret: "test",
|
||||||
},
|
},
|
||||||
@@ -36,11 +47,6 @@ module.exports = {
|
|||||||
medusa_v2: enableMedusaV2,
|
medusa_v2: enableMedusaV2,
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
cacheService: {
|
|
||||||
resolve: "@medusajs/cache-inmemory",
|
|
||||||
options: { ttl: cacheTTL },
|
|
||||||
},
|
|
||||||
workflows: true,
|
|
||||||
[Modules.AUTH]: true,
|
[Modules.AUTH]: true,
|
||||||
[Modules.USER]: {
|
[Modules.USER]: {
|
||||||
scope: "internal",
|
scope: "internal",
|
||||||
@@ -80,6 +86,7 @@ module.exports = {
|
|||||||
[Modules.PRODUCT]: true,
|
[Modules.PRODUCT]: true,
|
||||||
[Modules.PRICING]: true,
|
[Modules.PRICING]: true,
|
||||||
[Modules.PROMOTION]: true,
|
[Modules.PROMOTION]: true,
|
||||||
|
[Modules.REGION]: true,
|
||||||
[Modules.CUSTOMER]: true,
|
[Modules.CUSTOMER]: true,
|
||||||
[Modules.SALES_CHANNEL]: true,
|
[Modules.SALES_CHANNEL]: true,
|
||||||
[Modules.CART]: true,
|
[Modules.CART]: true,
|
||||||
@@ -89,7 +96,37 @@ module.exports = {
|
|||||||
[Modules.STORE]: true,
|
[Modules.STORE]: true,
|
||||||
[Modules.TAX]: true,
|
[Modules.TAX]: true,
|
||||||
[Modules.CURRENCY]: true,
|
[Modules.CURRENCY]: true,
|
||||||
[Modules.PAYMENT]: true,
|
[Modules.ORDER]: true,
|
||||||
[Modules.FULFILLMENT]: true,
|
[Modules.PAYMENT]: {
|
||||||
|
resolve: "@medusajs/payment",
|
||||||
|
/** @type {import('@medusajs/payment').PaymentModuleOptions}*/
|
||||||
|
options: {
|
||||||
|
providers: [customPaymentProvider],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Modules.FULFILLMENT]: {
|
||||||
|
/** @type {import('@medusajs/fulfillment').FulfillmentModuleOptions} */
|
||||||
|
options: {
|
||||||
|
providers: [customFulfillmentProvider],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Modules.NOTIFICATION]: {
|
||||||
|
/** @type {import('@medusajs/types').LocalNotificationServiceOptions} */
|
||||||
|
options: {
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
resolve: "@medusajs/notification-local",
|
||||||
|
options: {
|
||||||
|
config: {
|
||||||
|
"local-notification-provider": {
|
||||||
|
name: "Local Notification Provider",
|
||||||
|
channels: ["log", "email"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./steps/remove-remote-links"
|
export * from "./steps/remove-remote-links"
|
||||||
export * from "./steps/use-remote-query"
|
export * from "./steps/use-remote-query"
|
||||||
export * from "./steps/create-remote-links"
|
export * from "./steps/create-remote-links"
|
||||||
|
export * from "./steps/dismiss-remote-links"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
|
||||||
|
type DismissRemoteLinksStepInput = LinkDefinition | LinkDefinition[]
|
||||||
|
|
||||||
|
export const dismissRemoteLinkStepId = "dismiss-remote-links"
|
||||||
|
export const dismissRemoteLinkStep = createStep(
|
||||||
|
dismissRemoteLinkStepId,
|
||||||
|
async (data: DismissRemoteLinksStepInput, { container }) => {
|
||||||
|
const entries = Array.isArray(data) ? data : [data]
|
||||||
|
const link = container.resolve<RemoteLink>(
|
||||||
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
|
)
|
||||||
|
await link.dismiss(entries)
|
||||||
|
|
||||||
|
return new StepResponse(entries, entries)
|
||||||
|
},
|
||||||
|
async (dismissdLinks, { container }) => {
|
||||||
|
if (!dismissdLinks) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = container.resolve<RemoteLink>(
|
||||||
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
|
)
|
||||||
|
await link.create(dismissdLinks)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ProductTypes } from "@medusajs/types"
|
|
||||||
import {
|
|
||||||
createWorkflow,
|
|
||||||
transform,
|
|
||||||
WorkflowData,
|
|
||||||
} from "@medusajs/workflows-sdk"
|
|
||||||
import { updateProductsStep } from "../steps/update-products"
|
import { updateProductsStep } from "../steps/update-products"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
dismissRemoteLinkStep,
|
||||||
createLinkStep,
|
createLinkStep,
|
||||||
removeRemoteLinkStep,
|
|
||||||
useRemoteQueryStep,
|
useRemoteQueryStep,
|
||||||
} from "../../common"
|
} from "../../common"
|
||||||
import { arrayDifference } from "@medusajs/utils"
|
import { arrayDifference } from "@medusajs/utils"
|
||||||
import { DeleteEntityInput, Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
import { ProductTypes } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
type UpdateProductsStepInputSelector = {
|
type UpdateProductsStepInputSelector = {
|
||||||
selector: ProductTypes.FilterableProductProps
|
selector: ProductTypes.FilterableProductProps
|
||||||
@@ -63,15 +63,16 @@ function updateProductIds({
|
|||||||
updatedProducts: ProductTypes.ProductDTO[]
|
updatedProducts: ProductTypes.ProductDTO[]
|
||||||
input: WorkflowInput
|
input: WorkflowInput
|
||||||
}) {
|
}) {
|
||||||
|
let productIds = updatedProducts.map((p) => p.id)
|
||||||
|
|
||||||
if ("products" in input) {
|
if ("products" in input) {
|
||||||
let productIds = updatedProducts.map((p) => p.id)
|
|
||||||
const discardedProductIds: string[] = input.products
|
const discardedProductIds: string[] = input.products
|
||||||
.filter((p) => !p.sales_channels)
|
.filter((p) => !p.sales_channels)
|
||||||
.map((p) => p.id as string)
|
.map((p) => p.id as string)
|
||||||
return arrayDifference(productIds, discardedProductIds)
|
return arrayDifference(productIds, discardedProductIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
return !input.update.sales_channels ? [] : undefined
|
return !input.update.sales_channels ? [] : productIds
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareSalesChannelLinks({
|
function prepareSalesChannelLinks({
|
||||||
@@ -148,12 +149,12 @@ export const updateProductsWorkflow = createWorkflow(
|
|||||||
const currentLinks = useRemoteQueryStep({
|
const currentLinks = useRemoteQueryStep({
|
||||||
entry_point: "product_sales_channel",
|
entry_point: "product_sales_channel",
|
||||||
fields: ["product_id", "sales_channel_id"],
|
fields: ["product_id", "sales_channel_id"],
|
||||||
variables: { product_id: updatedProductIds },
|
variables: { filters: { product_id: updatedProductIds } },
|
||||||
})
|
})
|
||||||
|
|
||||||
const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks)
|
const toDeleteLinks = transform({ currentLinks }, prepareToDeleteLinks)
|
||||||
|
|
||||||
removeRemoteLinkStep(toDeleteLinks as DeleteEntityInput[])
|
dismissRemoteLinkStep(toDeleteLinks)
|
||||||
|
|
||||||
const salesChannelLinks = transform(
|
const salesChannelLinks = transform(
|
||||||
{ input, updatedProducts },
|
{ input, updatedProducts },
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const AdminGetProductsParams = createFindParams({
|
|||||||
.object({
|
.object({
|
||||||
variants: AdminGetProductVariantsParams.optional(),
|
variants: AdminGetProductVariantsParams.optional(),
|
||||||
price_list_id: z.string().array().optional(),
|
price_list_id: z.string().array().optional(),
|
||||||
|
status: statusEnum.array().optional(),
|
||||||
$and: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
$and: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
||||||
$or: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
$or: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in new issue
Block a user