feat: Add batch method to collection and clean up some batch implementations (#7102)

This commit is contained in:
Stevche Radevski
2024-04-22 10:36:22 +02:00
committed by GitHub
parent 0f5b015df0
commit 89143e1032
26 changed files with 425 additions and 123 deletions

View File

@@ -2844,7 +2844,7 @@ medusaIntegrationTestRunner({
}
const response = await api.post(
"/admin/products/op/batch",
"/admin/products/batch",
{
create: [createPayload],
update: [updatePayload],
@@ -2952,7 +2952,7 @@ medusaIntegrationTestRunner({
}
const response = await api.post(
`/admin/products/${createdProduct.id}/variants/op/batch`,
`/admin/products/${createdProduct.id}/variants/batch`,
{
create: [createPayload],
update: [updatePayload],
@@ -2983,6 +2983,40 @@ medusaIntegrationTestRunner({
}
)
})
it("successfully adds and removes products to a collection", async () => {
await breaking(
() => {},
async () => {
const response = await api.post(
`/admin/collections/${baseCollection.id}/products`,
{
add: [publishedProduct.id],
remove: [baseProduct.id],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.added).toHaveLength(1)
expect(response.data.removed).toHaveLength(1)
const collection = (
await api.get(
`/admin/collections/${baseCollection.id}?fields=*products`,
adminHeaders
)
).data.collection
expect(collection.products).toHaveLength(1)
expect(collection.products[0]).toEqual(
expect.objectContaining({
id: publishedProduct.id,
})
)
}
)
})
})
// TODO: Discuss how this should be handled

View File

@@ -334,7 +334,7 @@ medusaIntegrationTestRunner({
it("should delete an inventory location level and create a new one", async () => {
const result = await api.post(
`/admin/inventory-items/${inventoryItem.id}/location-levels/op/batch`,
`/admin/inventory-items/${inventoryItem.id}/location-levels/batch`,
{
create: [
{

View File

@@ -618,23 +618,21 @@ medusaIntegrationTestRunner({
{ relations: ["prices"] }
)
const data = { product_id: [product.id] }
const data = { remove: [product.id] }
const response = await api.post(
`admin/price-lists/${priceList.id}/prices/batch`,
`admin/price-lists/${priceList.id}/products`,
data,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
created: [],
updated: [],
deleted: {
ids: ["price-to-delete-1", "price-to-delete-2"],
object: "price",
deleted: true,
},
})
expect(response.data.price_list).toEqual(
expect.objectContaining({
id: expect.any(String),
title: "test price list",
description: "test",
})
)
})
})
})

View File

@@ -0,0 +1,54 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { BatchLinkProductsToCollectionDTO } from "@medusajs/types/src"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const batchLinkProductsToCollectionStepId =
"batch-link-products-to-collection"
export const batchLinkProductsToCollectionStep = createStep(
batchLinkProductsToCollectionStepId,
async (data: BatchLinkProductsToCollectionDTO, { container }) => {
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
if (!data.add?.length && !data.remove?.length) {
return new StepResponse(void 0, null)
}
const dbCollection = await service.retrieveCollection(data.id, {
take: null,
select: ["id", "products.id"],
relations: ["products"],
})
const existingProductIds = dbCollection.products?.map((p) => p.id) ?? []
const toRemoveMap = new Map(data.remove?.map((id) => [id, true]) ?? [])
const newProductIds = [
...existingProductIds.filter((id) => !toRemoveMap.has(id)),
...(data.add ?? []),
]
await service.updateCollections(data.id, {
product_ids: newProductIds,
})
return new StepResponse(void 0, {
id: data.id,
productIds: existingProductIds,
})
},
async (prevData, { container }) => {
if (!prevData) {
return
}
const service = container.resolve<IProductModuleService>(
ModuleRegistrationName.PRODUCT
)
await service.updateCollections(prevData.id, {
product_ids: prevData.productIds,
})
}
)

View File

@@ -14,6 +14,7 @@ export * from "./batch-product-variants"
export * from "./create-collections"
export * from "./update-collections"
export * from "./delete-collections"
export * from "./batch-link-products-collection"
export * from "./create-product-types"
export * from "./update-product-types"
export * from "./delete-product-types"

View File

@@ -0,0 +1,14 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { BatchLinkProductsToCollectionDTO } from "@medusajs/types/src"
import { batchLinkProductsToCollectionStep } from "../steps/batch-link-products-collection"
export const batchLinkProductsToCollectionWorkflowId =
"batch-link-products-to-collection"
export const batchLinkProductsToCollectionWorkflow = createWorkflow(
batchLinkProductsToCollectionWorkflowId,
(
input: WorkflowData<BatchLinkProductsToCollectionDTO>
): WorkflowData<void> => {
return batchLinkProductsToCollectionStep(input)
}
)

View File

@@ -12,6 +12,7 @@ export * from "./batch-product-variants"
export * from "./create-collections"
export * from "./delete-collections"
export * from "./update-collections"
export * from "./batch-link-products-collection"
export * from "./create-product-types"
export * from "./delete-product-types"
export * from "./update-product-types"

View File

@@ -0,0 +1,39 @@
import { batchLinkProductsToCollectionWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import { LinkMethodRequest } from "@medusajs/types/src"
import { refetchCollection } from "../../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest<LinkMethodRequest>,
res: MedusaResponse
) => {
const id = req.params.id
const { add = [], remove = [] } = req.validatedBody
const workflow = batchLinkProductsToCollectionWorkflow(req.scope)
const { result, errors } = await workflow.run({
input: {
id,
add,
remove,
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const collection = await refetchCollection(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({
collection,
})
}

View File

@@ -9,6 +9,7 @@ import {
AdminUpdateCollection,
} from "./validators"
import { validateAndTransformBody } from "../../utils/validate-body"
import { createLinkBody } from "../../utils/validators"
export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
{
@@ -64,5 +65,15 @@ export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/collections/:id",
middlewares: [],
},
// TODO: There were two batch methods, they need to be handled
{
method: ["POST"],
matcher: "/admin/collections/:id/products",
middlewares: [
validateAndTransformBody(createLinkBody()),
validateAndTransformQuery(
AdminGetCollectionParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
]

View File

@@ -1,11 +1,8 @@
import {
AdminCreateInventoryLocationLevelType,
AdminUpdateInventoryLocationLevelType,
} from "../../../../validators"
import {
MedusaRequest,
MedusaResponse,
} from "../../../../../../../types/routing"
} from "../../../validators"
import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing"
import { bulkCreateDeleteLevelsWorkflow } from "@medusajs/core-flows"
import { BatchMethodRequest } from "@medusajs/types"

View File

@@ -14,6 +14,7 @@ import {
} from "./validators"
import { validateAndTransformBody } from "../../utils/validate-body"
import { createBatchBody } from "../../utils/validators"
import { unlessPath } from "../../utils/unless-path"
export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
{
@@ -84,30 +85,9 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["DELETE"],
matcher: "/admin/inventory-items/:id/location-levels/:location_id",
middlewares: [
validateAndTransformQuery(
AdminGetInventoryItemParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/inventory-items/:id/location-levels/:location_id",
middlewares: [
validateAndTransformBody(AdminUpdateInventoryLocationLevel),
validateAndTransformQuery(
AdminGetInventoryItemParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/inventory-items/:id/location-levels/op/batch",
matcher: "/admin/inventory-items/:id/location-levels/batch",
middlewares: [
validateAndTransformBody(
createBatchBody(
@@ -121,4 +101,34 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["DELETE"],
matcher: "/admin/inventory-items/:id/location-levels/:location_id",
middlewares: [
unlessPath(
/.*\/location-levels\/batch/,
validateAndTransformQuery(
AdminGetInventoryItemParams,
QueryConfig.retrieveTransformQueryConfig
)
),
],
},
{
method: ["POST"],
matcher: "/admin/inventory-items/:id/location-levels/:location_id",
middlewares: [
unlessPath(
/.*\/location-levels\/batch/,
validateAndTransformBody(AdminUpdateInventoryLocationLevel)
),
unlessPath(
/.*\/location-levels\/batch/,
validateAndTransformQuery(
AdminGetInventoryItemParams,
QueryConfig.retrieveTransformQueryConfig
)
),
],
},
]

View File

@@ -43,7 +43,7 @@ export const adminPaymentRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/payments/:id",
middlewares: [
unlessPath(
new RegExp("/admin/payments/payment-providers"),
/.*\/payments\/payment-providers/,
validateAndTransformQuery(
AdminGetPaymentParams,
queryConfig.retrieveTransformQueryConfig

View File

@@ -1,16 +1,24 @@
import { batchPriceListPricesWorkflow } from "@medusajs/core-flows"
import { promiseAll } from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import { fetchPriceListPriceIdsForProduct } from "../../../helpers"
import { listPrices } from "../../../queries"
import { adminPriceListPriceRemoteQueryFields } from "../../../query-config"
import { AdminBatchPriceListPricesType } from "../../../validators"
import { BatchMethodRequest } from "@medusajs/types"
import {
AdminCreatePriceListPriceType,
AdminUpdatePriceListPriceType,
} from "../../../validators"
import { batchPriceListPricesWorkflow } from "@medusajs/core-flows"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminBatchPriceListPricesType>,
req: AuthenticatedMedusaRequest<
BatchMethodRequest<
AdminCreatePriceListPriceType,
AdminUpdatePriceListPriceType
>
>,
res: MedusaResponse
) => {
const id = req.params.id
@@ -18,16 +26,8 @@ export const POST = async (
create = [],
update = [],
delete: deletePriceIds = [],
product_id: productIds = [],
} = req.validatedBody
const productPriceIds = await fetchPriceListPriceIdsForProduct(
id,
productIds,
req.scope
)
const priceIdsToDelete = [...deletePriceIds, ...productPriceIds]
const workflow = batchPriceListPricesWorkflow(req.scope)
const { result, errors } = await workflow.run({
input: {
@@ -35,7 +35,7 @@ export const POST = async (
id,
create,
update,
delete: priceIdsToDelete,
delete: deletePriceIds,
},
},
throwOnError: false,
@@ -62,7 +62,7 @@ export const POST = async (
created,
updated,
deleted: {
ids: priceIdsToDelete,
ids: deletePriceIds,
object: "price",
deleted: true,
},

View File

@@ -0,0 +1,60 @@
import { MedusaError } from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import { batchPriceListPricesWorkflow } from "@medusajs/core-flows"
import { LinkMethodRequest } from "@medusajs/types/src"
import { fetchPriceList, fetchPriceListPriceIdsForProduct } from "../../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest<LinkMethodRequest>,
res: MedusaResponse
) => {
const id = req.params.id
const { add, remove = [] } = req.validatedBody
if (add?.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Adding products directly to a price list is not supported, please use the /admin/price-lists/:id/prices/batch endpoint instead"
)
}
if (!remove.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No product ids passed to remove from price list"
)
}
const productPriceIds = await fetchPriceListPriceIdsForProduct(
id,
remove,
req.scope
)
const workflow = batchPriceListPricesWorkflow(req.scope)
const { result, errors } = await workflow.run({
input: {
data: {
id,
create: [],
update: [],
delete: productPriceIds,
},
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const priceList = await fetchPriceList(
id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ price_list: priceList })
}

View File

@@ -2,14 +2,16 @@ import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
import { createBatchBody, createLinkBody } from "../../utils/validators"
import * as QueryConfig from "./query-config"
import {
AdminBatchPriceListPrices,
AdminCreatePriceList,
AdminCreatePriceListPrice,
AdminGetPriceListParams,
AdminGetPriceListPricesParams,
AdminGetPriceListsParams,
AdminUpdatePriceList,
AdminUpdatePriceListPrice,
} from "./validators"
export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
@@ -60,11 +62,24 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["POST"],
matcher: "/admin/price-lists/:id/products",
middlewares: [
validateAndTransformBody(createLinkBody()),
validateAndTransformQuery(
AdminGetPriceListParams,
QueryConfig.listPriceListQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/price-lists/:id/prices/batch",
middlewares: [
validateAndTransformBody(AdminBatchPriceListPrices),
validateAndTransformBody(
createBatchBody(AdminCreatePriceListPrice, AdminUpdatePriceListPrice)
),
validateAndTransformQuery(
AdminGetPriceListPricesParams,
QueryConfig.listPriceListPriceQueryConfig

View File

@@ -40,17 +40,6 @@ export type AdminUpdatePriceListPriceType = z.infer<
typeof AdminUpdatePriceListPrice
>
export const AdminBatchPriceListPrices = z.object({
create: z.array(AdminCreatePriceListPrice).optional(),
update: z.array(AdminUpdatePriceListPrice).optional(),
delete: z.array(z.string()).optional(),
product_id: z.array(z.string()).optional(),
})
export type AdminBatchPriceListPricesType = z.infer<
typeof AdminBatchPriceListPrices
>
export const AdminCreatePriceList = z.object({
title: z.string(),
description: z.string(),

View File

@@ -2,13 +2,13 @@ import { batchProductVariantsWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../../types/routing"
} from "../../../../../../types/routing"
import {
AdminBatchUpdateProductVariantType,
AdminCreateProductType,
} from "../../../../validators"
} from "../../../validators"
import { BatchMethodRequest } from "@medusajs/types"
import { refetchBatchVariants, remapVariantResponse } from "../../../../helpers"
import { refetchBatchVariants, remapVariantResponse } from "../../../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest<

View File

@@ -2,14 +2,13 @@ import { batchProductsWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
} from "../../../../types/routing"
import {
AdminBatchUpdateProductType,
AdminCreateProductType,
} from "../../validators"
} from "../validators"
import { BatchMethodRequest } from "@medusajs/types"
import { refetchBatchProducts, remapProductResponse } from "../../helpers"
import { CreateProductDTO, UpsertProductDTO } from "@medusajs/types"
import { refetchBatchProducts, remapProductResponse } from "../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest<

View File

@@ -1,6 +1,7 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { unlessPath } from "../../utils/unless-path"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
import { createBatchBody } from "../../utils/validators"
@@ -45,16 +46,6 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
maybeApplyPriceListsFilter(),
],
},
{
method: ["GET"],
matcher: "/admin/products/:id",
middlewares: [
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/products",
@@ -68,7 +59,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
},
{
method: ["POST"],
matcher: "/admin/products/op/batch",
matcher: "/admin/products/batch",
middlewares: [
validateAndTransformBody(
createBatchBody(AdminCreateProduct, AdminBatchUpdateProduct)
@@ -79,14 +70,33 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["GET"],
matcher: "/admin/products/:id",
middlewares: [
unlessPath(
/.*\/products\/batch/,
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
)
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id",
middlewares: [
validateAndTransformBody(AdminUpdateProduct),
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
unlessPath(
/.*\/products\/batch/,
validateAndTransformBody(AdminUpdateProduct)
),
unlessPath(
/.*\/products\/batch/,
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
)
),
],
},
@@ -94,13 +104,15 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
method: ["DELETE"],
matcher: "/admin/products/:id",
middlewares: [
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
unlessPath(
/.*\/products\/batch/,
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
)
),
],
},
{
method: ["GET"],
matcher: "/admin/products/:id/variants",
@@ -113,7 +125,18 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
},
{
method: ["POST"],
matcher: "/admin/products/:id/variants/op/batch",
matcher: "/admin/products/:id/variants",
middlewares: [
validateAndTransformBody(AdminCreateProductVariant),
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id/variants/batch",
middlewares: [
validateAndTransformBody(
createBatchBody(
@@ -132,20 +155,12 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"],
matcher: "/admin/products/:id/variants/:variant_id",
middlewares: [
validateAndTransformQuery(
AdminGetProductVariantParams,
QueryConfig.retrieveVariantConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id/variants",
middlewares: [
validateAndTransformBody(AdminCreateProductVariant),
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
unlessPath(
/.*\/variants\/batch/,
validateAndTransformQuery(
AdminGetProductVariantParams,
QueryConfig.retrieveVariantConfig
)
),
],
},
@@ -153,10 +168,16 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
method: ["POST"],
matcher: "/admin/products/:id/variants/:variant_id",
middlewares: [
validateAndTransformBody(AdminUpdateProductVariant),
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
unlessPath(
/.*\/variants\/batch/,
validateAndTransformBody(AdminUpdateProductVariant)
),
unlessPath(
/.*\/variants\/batch/,
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
)
),
],
},
@@ -164,9 +185,12 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
method: ["DELETE"],
matcher: "/admin/products/:id/variants/:variant_id",
middlewares: [
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
unlessPath(
/.*\/variants\/batch/,
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
)
),
],
},

View File

@@ -8,7 +8,6 @@ export async function zodValidator<T>(
body: T
): Promise<z.ZodRawShape> {
try {
zodSchema
return await zodSchema.parseAsync(body)
} catch (err) {
if (err instanceof ZodError) {

View File

@@ -11,6 +11,13 @@ export const createBatchBody = (
})
}
export const createLinkBody = () => {
return z.object({
add: z.array(z.string()).optional(),
remove: z.array(z.string()).optional(),
})
}
export const createSelectParams = () => {
return z.object({
fields: z.string().optional(),

View File

@@ -31,6 +31,7 @@ import {
ModulesSdkUtils,
ProductStatus,
promiseAll,
removeUndefined,
} from "@medusajs/utils"
import {
ProductCategoryEventData,
@@ -771,6 +772,8 @@ export default class ProductModuleService<
ProductModuleService.normalizeCreateProductCollectionInput
)
// It's safe to use upsertWithReplace here since we only have product IDs and the only operation to do is update the product
// with the collection ID
return await this.productCollectionService_.upsertWithReplace(
normalizedInput,
{ relations: ["products"] },
@@ -906,13 +909,48 @@ export default class ProductModuleService<
): Promise<TProductCollection[]> {
const normalizedInput = data.map(
ProductModuleService.normalizeUpdateProductCollectionInput
)
) as UpdateCollectionInput[]
return await this.productCollectionService_.upsertWithReplace(
normalizedInput,
{ relations: ["products"] },
// TODO: Maybe we can update upsertWithReplace to not remove oneToMany entities, but just disassociate them? With that we can remove the code below.
// Another alternative is to not allow passing product_ids to a collection, and instead set the collection_id through the product update call.
const updatedCollections = await this.productCollectionService_.update(
normalizedInput.map((c) =>
removeUndefined({ ...c, products: undefined })
),
sharedContext
)
const collectionWithProducts = await promiseAll(
updatedCollections.map(async (collection, i) => {
const input = normalizedInput.find((c) => c.id === collection.id)
const productsToUpdate = (input as any)?.products
if (!productsToUpdate) {
return { ...collection, products: [] }
}
await this.productService_.update(
{
selector: { collection_id: collection.id },
data: { collection_id: null },
},
sharedContext
)
if (productsToUpdate.length > 0) {
await this.productService_.update(
productsToUpdate.map((p) => ({
id: p.id,
collection_id: collection.id,
})),
sharedContext
)
}
return { ...collection, products: productsToUpdate }
})
)
return collectionWithProducts
}
@InjectManager("baseRepository_")

View File

@@ -1,3 +1,8 @@
export type LinkMethodRequest = {
add?: string[]
remove?: string[]
}
export type BatchMethodRequest<TCreate extends any, TUpdate extends any> = {
create?: TCreate[]
update?: TUpdate[]

View File

@@ -1 +1,2 @@
export * from "./stock-locations"
export * from "./products"

View File

@@ -0,0 +1 @@
export * from "./mutations"

View File

@@ -0,0 +1,5 @@
import { LinkMethodRequest } from "../../common"
export interface BatchLinkProductsToCollectionDTO extends LinkMethodRequest {
id: string
}