From f78cdb0ce598a1e4843dee9f4e8799202c8602a5 Mon Sep 17 00:00:00 2001 From: itariv Date: Wed, 5 Nov 2025 15:30:00 +0100 Subject: [PATCH] chore: added endpoint for vendor panel to manage vendor products in collection --- .../[id]/products/route.ts | 134 ++++++++++++++++++ .../vendor/product-collections/middlewares.ts | 24 +++- .../infra/http/middlewares/check-ownership.ts | 59 ++++++++ 3 files changed, 214 insertions(+), 3 deletions(-) diff --git a/packages/modules/b2c-core/src/api/vendor/product-collections/[id]/products/route.ts b/packages/modules/b2c-core/src/api/vendor/product-collections/[id]/products/route.ts index 348061368..58dbde053 100644 --- a/packages/modules/b2c-core/src/api/vendor/product-collections/[id]/products/route.ts +++ b/packages/modules/b2c-core/src/api/vendor/product-collections/[id]/products/route.ts @@ -2,6 +2,8 @@ import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { ContainerRegistrationKeys } from '@medusajs/framework/utils' import { filterSellerProductsByCollection } from '../../utils' +import { batchLinkProductsToCollectionWorkflow } from "@medusajs/core-flows" +import { LinkMethodRequest } from "@medusajs/framework/types" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -32,3 +34,135 @@ export const GET = async ( limit: req.queryConfig.pagination?.take || 10 }) } + +/** + * @oas [post] /vendor/product-collections/{id}/products + * summary: Manage Vendor Products of a Collection + * x-sidebar-summary: Manage Vendor Products of a Collection + * description: Manage the vendor products of a collection by adding or removing them from the collection. + * x-authenticated: true + * parameters: + * - name: id + * in: path + * description: The collection's ID. + * required: true + * schema: + * type: string + * - name: fields + * in: query + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. + * required: false + * schema: + * type: string + * title: fields + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. + * externalDocs: + * url: "#select-fields-and-relations" + * security: + * - api_token: [] + * - cookie_auth: [] + * - jwt_token: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * description: The products to add or remove. + * properties: + * add: + * type: array + * description: The products to add to the collection. + * items: + * type: string + * title: add + * description: A product's ID. + * remove: + * type: array + * description: The products to remove from the collection. + * items: + * type: string + * title: remove + * description: A product's ID. + * tags: + * - Vendor Product Collections + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * products: + * type: array + * items: + * $ref: "#/components/schemas/VendorProduct" + * count: + * type: integer + * description: The total number of items available + * offset: + * type: integer + * description: The number of items skipped before these items + * limit: + * type: integer + * description: The number of items per page + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + * x-workflow: batchLinkProductsToCollectionWorkflow + * x-events: [] + * +*/ + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.params.id + const { add = [], remove = [] } = req.validatedBody + + const workflow = batchLinkProductsToCollectionWorkflow(req.scope) + await workflow.run({ + input: { + id, + add, + remove, + }, + }) + + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + const { productIds, count } = await filterSellerProductsByCollection( + req.scope, + req.params.id, + req.filterableFields.seller_id as string, + req.queryConfig.pagination?.skip || 0, + req.queryConfig.pagination?.take || 10 + ) + + const { data: products } = await query.graph({ + entity: 'product', + fields: req.queryConfig.fields, + filters: { + id: productIds + } + }) + + res.status(200).json({ + products, + count, + offset: req.queryConfig.pagination?.skip || 0, + limit: req.queryConfig.pagination?.take || 10 + }) +} diff --git a/packages/modules/b2c-core/src/api/vendor/product-collections/middlewares.ts b/packages/modules/b2c-core/src/api/vendor/product-collections/middlewares.ts index 33bd983d4..23479a05b 100644 --- a/packages/modules/b2c-core/src/api/vendor/product-collections/middlewares.ts +++ b/packages/modules/b2c-core/src/api/vendor/product-collections/middlewares.ts @@ -1,6 +1,6 @@ -import { MiddlewareRoute, validateAndTransformQuery } from '@medusajs/framework' +import { MiddlewareRoute, validateAndTransformBody, validateAndTransformQuery } from '@medusajs/framework' -import { filterBySellerId } from '../../../shared/infra/http/middlewares' +import { checkResourcesOwnershipByResourceBatch, filterBySellerId } from '../../../shared/infra/http/middlewares' import { vendorProductCollectionQueryConfig, vendorProductCollectionsProductsQueryConfig @@ -9,6 +9,8 @@ import { VendorGetProductCollectionsParams, VendorGetProductCollectionsProductsParams } from './validators' +import { createLinkBody } from '@medusajs/medusa/api/utils/validators' +import sellerProductLink from "../../../links/seller-product"; export const vendorProductCollectionsMiddlewares: MiddlewareRoute[] = [ { @@ -41,5 +43,21 @@ export const vendorProductCollectionsMiddlewares: MiddlewareRoute[] = [ ), filterBySellerId() ] - } + }, + { + method: ["POST"], + matcher: "/vendor/product-collections/:id/products", + middlewares: [ + validateAndTransformQuery( + VendorGetProductCollectionsProductsParams, + vendorProductCollectionsProductsQueryConfig.list + ), + validateAndTransformBody(createLinkBody()), + filterBySellerId(), + checkResourcesOwnershipByResourceBatch({ + entryPoint: sellerProductLink.entryPoint, + filterField: 'product_id', + }), + ], + }, ] diff --git a/packages/modules/b2c-core/src/shared/infra/http/middlewares/check-ownership.ts b/packages/modules/b2c-core/src/shared/infra/http/middlewares/check-ownership.ts index 829b7cc61..a018ad80f 100644 --- a/packages/modules/b2c-core/src/shared/infra/http/middlewares/check-ownership.ts +++ b/packages/modules/b2c-core/src/shared/infra/http/middlewares/check-ownership.ts @@ -5,6 +5,7 @@ import { ContainerRegistrationKeys, MedusaError } from '@medusajs/framework/utils' +import { LinkMethodRequest } from '@medusajs/framework/types' type CheckResourceOwnershipByResourceIdOptions = { entryPoint: string @@ -12,6 +13,12 @@ type CheckResourceOwnershipByResourceIdOptions = { resourceId?: (req: AuthenticatedMedusaRequest) => string } +type CheckResourcesOwnershipByResourceBatchOptions = { + entryPoint: string + filterField?: string + resourceIds?: (req: AuthenticatedMedusaRequest) => { add: string[], remove: string[] } +} + /** * Middleware that verifies if the authenticated member owns/has access to the requested resource. * This is done by checking if the member's seller ID matches the resource's seller ID. @@ -93,3 +100,55 @@ export const checkResourceOwnershipByResourceId = ({ next() } } + +export const checkResourcesOwnershipByResourceBatch = ({ + entryPoint, + filterField = 'id', + resourceIds = (req) => ({ add: req.validatedBody.add || [], remove: req.validatedBody.remove || [] }) +}: CheckResourcesOwnershipByResourceBatchOptions) => { + return async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse, + next: NextFunction + ) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + const { + data: [member] + } = await query.graph( + { + entity: 'member', + fields: ['seller.id'], + filters: { + id: req.auth_context.actor_id + } + }, + { throwIfKeyNotFound: true } + ) + + const { add, remove } = resourceIds(req) + const allResourceIds = add.concat(remove) + + + const { + data: resources + } = await query.graph({ + entity: entryPoint, + fields: ['seller_id', filterField], + filters: { + [filterField]: allResourceIds, + seller_id: member.seller.id + } + }) + + if (!resources.some((resource) => allResourceIds.includes(resource[filterField]))) { + res.status(404).json({ + message: `You are not allowed to perform this action`, + type: MedusaError.Types.NOT_FOUND + }) + return + } + + next() + } +}