{
  "$schema": "https://registry.mercurjs.com/registry-item.json",
  "name": "meilisearch",
  "description": "Opt-in Meilisearch search block for multi-vendor marketplaces with real-time seller-aware indexing, seller lifecycle sync, and injection-safe smart filtering.",
  "dependencies": [
    "meilisearch"
  ],
  "registryDependencies": [],
  "docs": "## Prerequisites\n\nInstall and run a Meilisearch instance:\n\n```bash\ndocker run -d -p 7700:7700 getmeili/meilisearch\n```\n\n## Environment Variables\n\nAdd to your `.env` file:\n\n```env\nMEILISEARCH_HOST=http://localhost:7700\nMEILISEARCH_API_KEY=your_master_key_here\n```\n\n## Module Configuration\n\nAdd to your `medusa-config.ts`:\n\n```ts\nexport default defineConfig({\n  modules: [\n    {\n      resolve: './modules/meilisearch',\n      options: {\n        host: process.env.MEILISEARCH_HOST,\n        apiKey: process.env.MEILISEARCH_API_KEY,\n      },\n    },\n  ],\n})\n```\n\n## Middlewares\n\nAdd to your `api/middlewares.ts`:\n\n```ts\nimport { defineMiddlewares } from \"@medusajs/medusa\";\nimport { allMeilisearchMiddlewares } from \"./meilisearch/api/middlewares\";\n\nexport default defineMiddlewares({\n  routes: [...allMeilisearchMiddlewares],\n});\n```\n\n## Initial Sync\n\nAfter starting the server, trigger a full re-index:\n\n```bash\ncurl -X POST http://localhost:9000/admin/meilisearch/sync \\\n  -H \"Authorization: Bearer YOUR_ADMIN_TOKEN\"\n```\n\n## Search\n\n```bash\ncurl -X POST http://localhost:9000/store/meilisearch/products \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-publishable-api-key: YOUR_KEY\" \\\n  -d '{\"query\": \"shoes\", \"filters\": {\"price_min\": 50, \"price_max\": 200}}'\n```",
  "categories": [
    "search"
  ],
  "files": [
    {
      "path": "meilisearch/modules/meilisearch/index.ts",
      "content": "import { Module } from '@medusajs/framework/utils'\n\nimport MeilisearchModuleService from './service'\n\nexport const MEILISEARCH_MODULE = 'meilisearch'\nexport { MeilisearchModuleService }\n\nexport default Module(MEILISEARCH_MODULE, {\n  service: MeilisearchModuleService,\n})\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/modules/meilisearch/service.ts",
      "content": "import { MeiliSearch } from 'meilisearch'\n\nimport { IndexType, MeilisearchEntity, MeilisearchSearchResult } from './types'\n\ntype ModuleOptions = {\n  host: string\n  apiKey: string\n}\n\nconst SEARCHABLE_ATTRIBUTES = [\n  'title',\n  'subtitle',\n  'description',\n  'tags.value',\n  'type.value',\n  'categories.name',\n  'collection.title',\n  'variants.title',\n  'seller.name',\n  'seller.handle',\n]\n\nconst FILTERABLE_ATTRIBUTES = [\n  'seller.status',\n  'seller.handle',\n  'categories.id',\n  'categories.name',\n  'variants.prices.amount',\n  'status',\n]\n\nconst SORTABLE_ATTRIBUTES = ['title', 'variants.prices.amount']\n\nclass MeilisearchModuleService {\n  private client_: MeiliSearch\n  private host_: string\n  private productIndex_: ReturnType<MeiliSearch['index']>\n  private settingsApplied_ = false\n\n  constructor(_: unknown, options: ModuleOptions) {\n    if (!options?.host || !options?.apiKey) {\n      const missing = [\n        !options?.host && 'MEILISEARCH_HOST',\n        !options?.apiKey && 'MEILISEARCH_API_KEY',\n      ]\n        .filter(Boolean)\n        .join(', ')\n      throw new Error(\n        `[meilisearch block] Missing required environment variables: ${missing}`\n      )\n    }\n    this.host_ = options.host\n    this.client_ = new MeiliSearch({\n      host: options.host,\n      apiKey: options.apiKey,\n    })\n    this.productIndex_ = this.client_.index(IndexType.PRODUCT)\n  }\n\n  getHost(): string {\n    return this.host_\n  }\n\n  async getStatus(): Promise<{ documentCount: number; isHealthy: boolean }> {\n    try {\n      const stats = await this.productIndex_.getStats()\n      return { documentCount: stats.numberOfDocuments, isHealthy: true }\n    } catch {\n      return { documentCount: 0, isHealthy: false }\n    }\n  }\n\n  async batchUpsert(documents: MeilisearchEntity[]): Promise<void> {\n    if (!documents.length) {\n      return\n    }\n    await this.productIndex_.addDocuments(documents, { primaryKey: 'id' })\n  }\n\n  async batchDelete(ids: string[]): Promise<void> {\n    if (!ids.length) {\n      return\n    }\n    await this.productIndex_.deleteDocuments(ids)\n  }\n\n  async search(\n    query: string,\n    options: Record<string, unknown>\n  ): Promise<MeilisearchSearchResult> {\n    const result = await this.productIndex_.search(query, options)\n    return {\n      hits: (result.hits ?? []) as Array<{ id: string }>,\n      totalHits: result.totalHits ?? result.estimatedTotalHits ?? 0,\n      page: result.page ?? 0,\n      totalPages: result.totalPages ?? 0,\n      hitsPerPage: result.hitsPerPage ?? 0,\n      processingTimeMs: result.processingTimeMs,\n      query: result.query,\n      facetDistribution: result.facetDistribution,\n    }\n  }\n\n  async ensureSettings(): Promise<void> {\n    if (this.settingsApplied_) {\n      return\n    }\n    await this.productIndex_.updateSettings({\n      searchableAttributes: SEARCHABLE_ATTRIBUTES,\n      filterableAttributes: FILTERABLE_ATTRIBUTES,\n      sortableAttributes: SORTABLE_ATTRIBUTES,\n    })\n    this.settingsApplied_ = true\n  }\n}\n\nexport default MeilisearchModuleService\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/modules/meilisearch/types.ts",
      "content": "import { z } from 'zod'\n\nexport enum IndexType {\n  PRODUCT = 'products',\n}\n\nexport enum MeilisearchEvents {\n  PRODUCTS_CHANGED = 'meilisearch.products.changed',\n  PRODUCTS_DELETED = 'meilisearch.products.deleted',\n}\n\nexport const MeilisearchVariantValidator = z.object({\n  id: z.string(),\n  title: z.string().nullish(),\n  sku: z.string().nullish(),\n  barcode: z.string().nullish(),\n  ean: z.string().nullish(),\n  allow_backorder: z.boolean(),\n  manage_inventory: z.boolean(),\n  weight: z.number().nullish(),\n  length: z.number().nullish(),\n  height: z.number().nullish(),\n  width: z.number().nullish(),\n  variant_rank: z.number().nullish(),\n  options: z.array(\n    z.object({\n      id: z.string(),\n      value: z.string(),\n      option: z.object({\n        id: z.string(),\n        title: z.string(),\n      }),\n    })\n  ),\n  prices: z.array(\n    z.object({\n      id: z.string(),\n      currency_code: z.string(),\n      amount: z.number(),\n      min_quantity: z.number().nullish(),\n      max_quantity: z.number().nullish(),\n      rules_count: z.number(),\n    })\n  ),\n}).passthrough() // allows dynamically injected option keys (e.g. { size: \"S\" })\n\nexport const MeilisearchProductValidator = z.object({\n  id: z.string(),\n  title: z.string(),\n  handle: z.string(),\n  subtitle: z.string().nullable(),\n  description: z.string().nullable(),\n  thumbnail: z.string().nullable(),\n  status: z.string(),\n  categories: z\n    .array(z.object({ id: z.string(), name: z.string() }))\n    .optional(),\n  tags: z.array(z.object({ value: z.string() })).optional(),\n  collection: z.object({ title: z.string() }).nullable().optional(),\n  type: z.object({ value: z.string() }).nullable().optional(),\n  images: z\n    .array(\n      z.object({\n        id: z.string(),\n        url: z.string(),\n        rank: z.number(),\n      })\n    )\n    .optional(),\n  options: z.array(z.record(z.string())).nullable().default(null),\n  variants: z.array(MeilisearchVariantValidator).nullable().default(null),\n  seller: z\n    .object({\n      id: z.string(),\n      handle: z.string().nullish(),\n      name: z.string().nullish(),\n      status: z.string().nullish(),\n    })\n    .nullable(),\n})\n\nexport type MeilisearchProduct = z.infer<typeof MeilisearchProductValidator>\nexport type MeilisearchEntity = MeilisearchProduct\n\nexport type MeilisearchSearchResult = {\n  hits: Array<{ id: string }>\n  totalHits: number\n  page: number\n  totalPages: number\n  hitsPerPage: number\n  processingTimeMs: number\n  query: string\n  facetDistribution?: Record<string, Record<string, number>>\n}\n\nexport interface IMeilisearchModuleService {\n  search(\n    query: string,\n    options: Record<string, unknown>\n  ): Promise<MeilisearchSearchResult>\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/subscribers/utils/meilisearch-product.ts",
      "content": "import { z } from 'zod'\n\nimport { MedusaContainer } from '@medusajs/framework'\nimport { IEventBusModuleService } from '@medusajs/framework/types'\nimport { ContainerRegistrationKeys, Modules, arrayDifference } from '@medusajs/framework/utils'\n\nimport { MeilisearchProductValidator } from '../../modules/meilisearch/types'\nimport { MeilisearchEvents } from '../../modules/meilisearch/types'\n\nconst CHUNK_SIZE = 100\n\nexport function chunkArray<T>(arr: T[], size: number): T[][] {\n  const chunks: T[][] = []\n  for (let i = 0; i < arr.length; i += size) {\n    chunks.push(arr.slice(i, i + size))\n  }\n  return chunks\n}\n\nexport async function filterProductsByStatus(\n  container: MedusaContainer,\n  ids: string[] = []\n) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n\n  const { data: products } = await query.graph({\n    entity: 'product',\n    fields: ['id', 'status'],\n    filters: { id: ids },\n  })\n\n  const published = products.filter((p) => p.status === 'published')\n  const notPublished = arrayDifference(products, published)\n\n  const existingIds = new Set(products.map((p) => p.id))\n  const deletedIds = ids.filter((id) => !existingIds.has(id))\n\n  return {\n    published: published.map((p) => p.id),\n    other: [...notPublished.map((p) => p.id), ...deletedIds],\n  }\n}\n\nfunction flattenProductOptions(options: any[]): Record<string, string>[] {\n  return (options ?? [])\n    .filter((option: any) => option?.title && option?.values)\n    .flatMap((option: any) =>\n      option.values.map((value: any) => ({\n        [option.title.toLowerCase()]: value.value,\n      }))\n    )\n}\n\nfunction flattenVariantOptions(variant: any): Record<string, unknown> {\n  return (variant.options ?? []).reduce(\n    (entry: Record<string, unknown>, item: any) => {\n      if (item?.option?.title) {\n        entry[item.option.title.toLowerCase()] = item.value\n      }\n      return entry\n    },\n    { ...variant }\n  )\n}\n\nexport async function findAndTransformMeilisearchProducts(\n  container: MedusaContainer,\n  ids: string[] = []\n) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n\n  const { data: products } = await query.graph({\n    entity: 'product',\n    fields: [\n      '*',\n      'categories.name',\n      'categories.id',\n      'collection.title',\n      'tags.value',\n      'type.value',\n      'variants.*',\n      'variants.options.*',\n      'variants.prices.*',\n      'options.*',\n      'options.values.*',\n      'images.*',\n      'seller.id',\n      'seller.handle',\n      'seller.name',\n      'seller.status',\n    ],\n    filters: ids.length\n      ? { id: ids, status: 'published' }\n      : { status: 'published' },\n  })\n\n  const transformed = products.map((product: any) => ({\n    ...product,\n    options: flattenProductOptions(product.options),\n    variants: (product.variants ?? []).map(flattenVariantOptions),\n  }))\n\n  return z.array(MeilisearchProductValidator).parse(transformed)\n}\n\nexport async function reindexSellerProducts(\n  container: MedusaContainer,\n  sellerId: string,\n  action: string\n) {\n  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n  const eventBus = container.resolve<IEventBusModuleService>(Modules.EVENT_BUS)\n\n  try {\n    const { data: sellers } = await query.graph({\n      entity: 'seller',\n      fields: ['products.id'],\n      filters: { id: sellerId },\n    })\n\n    const productIds: string[] =\n      (sellers[0] as any)?.products?.map((p: any) => p.id) ?? []\n\n    if (!productIds.length) {\n      return\n    }\n\n    logger.debug(\n      `Meilisearch: Seller ${sellerId} ${action} — re-indexing ${productIds.length} products`\n    )\n\n    const chunks = chunkArray(productIds, CHUNK_SIZE)\n    await Promise.all(\n      chunks.map((chunk) =>\n        eventBus.emit({\n          name: MeilisearchEvents.PRODUCTS_CHANGED,\n          data: { ids: chunk },\n        })\n      )\n    )\n  } catch (error: unknown) {\n    logger.error(\n      `Meilisearch: Failed to process seller.${action} for seller ${sellerId}:`,\n      error as Error\n    )\n    throw error\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/subscribers/meilisearch-product-events-bridge.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { IEventBusModuleService } from '@medusajs/framework/types'\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'\n\nimport { MeilisearchEvents } from '../modules/meilisearch/types'\n\nexport default async function meilisearchProductEventsBridgeHandler({\n  event,\n  container,\n}: SubscriberArgs<{ id: string }>) {\n  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)\n  const eventBus = container.resolve<IEventBusModuleService>(Modules.EVENT_BUS)\n\n  try {\n    const isDelete =\n      event.name === 'product.product.deleted' ||\n      event.name === 'product.deleted'\n\n    await eventBus.emit({\n      name: isDelete\n        ? MeilisearchEvents.PRODUCTS_DELETED\n        : MeilisearchEvents.PRODUCTS_CHANGED,\n      data: { ids: [event.data.id] },\n    })\n  } catch (error: unknown) {\n    logger.error(\n      `Meilisearch bridge: failed to forward event ${event.name} for product ${event.data.id}:`,\n      error as Error\n    )\n    throw error\n  }\n}\n\nexport const config: SubscriberConfig = {\n  event: [\n    'product.created',\n    'product.updated',\n    'product.deleted',\n    'product.product.created',\n    'product.product.updated',\n    'product.product.deleted',\n  ],\n  context: {\n    subscriberId: 'meilisearch-product-events-bridge',\n  },\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/subscribers/meilisearch-products-changed.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys } from '@medusajs/framework/utils'\n\nimport { MEILISEARCH_MODULE, MeilisearchModuleService } from '../modules/meilisearch'\nimport { MeilisearchEvents } from '../modules/meilisearch/types'\nimport {\n  filterProductsByStatus,\n  findAndTransformMeilisearchProducts,\n} from './utils/meilisearch-product'\n\nexport default async function meilisearchProductsChangedHandler({\n  event,\n  container,\n}: SubscriberArgs<{ ids: string[] }>) {\n  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)\n\n  try {\n    const meilisearch =\n      container.resolve<MeilisearchModuleService>(MEILISEARCH_MODULE)\n\n    const { published, other } = await filterProductsByStatus(\n      container,\n      event.data.ids\n    )\n\n    logger.debug(\n      `Meilisearch sync: Processing ${event.data.ids.length} products — ${published.length} to upsert, ${other.length} to delete`\n    )\n\n    const [documentsToUpsert] = await Promise.all([\n      published.length\n        ? findAndTransformMeilisearchProducts(container, published)\n        : Promise.resolve([]),\n      other.length\n        ? meilisearch.batchDelete(other)\n        : Promise.resolve(),\n    ])\n\n    if (documentsToUpsert.length) {\n      await meilisearch.batchUpsert(documentsToUpsert)\n    }\n\n    logger.debug(\n      `Meilisearch sync: Successfully synced ${documentsToUpsert.length} products`\n    )\n  } catch (error: unknown) {\n    logger.error(\n      `Meilisearch sync failed for products [${event.data.ids.join(', ')}]:`,\n      error as Error\n    )\n    throw error\n  }\n}\n\nexport const config: SubscriberConfig = {\n  event: MeilisearchEvents.PRODUCTS_CHANGED,\n  context: {\n    subscriberId: 'meilisearch-products-changed-handler',\n  },\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/subscribers/meilisearch-products-deleted.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys } from '@medusajs/framework/utils'\n\nimport { MEILISEARCH_MODULE, MeilisearchModuleService } from '../modules/meilisearch'\nimport { MeilisearchEvents } from '../modules/meilisearch/types'\n\nexport default async function meilisearchProductsDeletedHandler({\n  event,\n  container,\n}: SubscriberArgs<{ ids: string[] }>) {\n  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)\n\n  try {\n    const meilisearch =\n      container.resolve<MeilisearchModuleService>(MEILISEARCH_MODULE)\n\n    logger.debug(\n      `Meilisearch delete: Removing ${event.data.ids.length} products from index`\n    )\n\n    await meilisearch.batchDelete(event.data.ids)\n\n    logger.debug(\n      `Meilisearch delete: Successfully removed ${event.data.ids.length} products`\n    )\n  } catch (error: unknown) {\n    logger.error(\n      `Meilisearch delete failed for products [${event.data.ids.join(', ')}]:`,\n      error as Error\n    )\n    throw error\n  }\n}\n\nexport const config: SubscriberConfig = {\n  event: MeilisearchEvents.PRODUCTS_DELETED,\n  context: {\n    subscriberId: 'meilisearch-products-deleted-handler',\n  },\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/subscribers/meilisearch-seller-suspended.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\n\nimport { reindexSellerProducts } from './utils/meilisearch-product'\n\nexport default async function meilisearchSellerSuspendedHandler({\n  event,\n  container,\n}: SubscriberArgs<{ id: string }>) {\n  await reindexSellerProducts(container, event.data.id, 'suspended')\n}\n\nexport const config: SubscriberConfig = {\n  event: 'seller.suspended',\n  context: {\n    subscriberId: 'meilisearch-seller-suspended-handler',\n  },\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/subscribers/meilisearch-seller-unsuspended.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\n\nimport { reindexSellerProducts } from './utils/meilisearch-product'\n\nexport default async function meilisearchSellerUnsuspendedHandler({\n  event,\n  container,\n}: SubscriberArgs<{ id: string }>) {\n  await reindexSellerProducts(container, event.data.id, 'unsuspended')\n}\n\nexport const config: SubscriberConfig = {\n  event: 'seller.unsuspended',\n  context: {\n    subscriberId: 'meilisearch-seller-unsuspended-handler',\n  },\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/workflows/meilisearch/steps/sync-meilisearch-products.ts",
      "content": "import { IEventBusModuleService, RemoteQueryFunction } from '@medusajs/framework/types'\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'\nimport { StepResponse, createStep } from '@medusajs/framework/workflows-sdk'\n\nimport { MEILISEARCH_MODULE, MeilisearchModuleService } from '../../../modules/meilisearch'\nimport { MeilisearchEvents } from '../../../modules/meilisearch/types'\nimport { chunkArray } from '../../../subscribers/utils/meilisearch-product'\n\nexport const syncMeilisearchProductsStep = createStep(\n  'sync-meilisearch-products',\n  async (_: void, { container }) => {\n    const query = container.resolve<RemoteQueryFunction>(\n      ContainerRegistrationKeys.QUERY\n    )\n    const meilisearch =\n      container.resolve<MeilisearchModuleService>(MEILISEARCH_MODULE)\n    const eventBus = container.resolve<IEventBusModuleService>(Modules.EVENT_BUS)\n\n    await meilisearch.ensureSettings()\n\n    const { data: allProducts } = await query.graph({\n      entity: 'product',\n      fields: ['id', 'status'],\n    })\n\n    const toDelete: string[] = []\n    const toIndex: string[] = []\n\n    for (const product of allProducts) {\n      if (product.status === 'published') {\n        toIndex.push(product.id)\n      } else {\n        toDelete.push(product.id)\n      }\n    }\n\n    if (toDelete.length) {\n      await meilisearch.batchDelete(toDelete)\n    }\n\n    const chunks = chunkArray(toIndex, 100)\n    await Promise.all(\n      chunks.map((chunk) =>\n        eventBus.emit({\n          name: MeilisearchEvents.PRODUCTS_CHANGED,\n          data: { ids: chunk },\n        })\n      )\n    )\n\n    return new StepResponse()\n  }\n)\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/workflows/meilisearch/workflows/sync-meilisearch.ts",
      "content": "import { createWorkflow, WorkflowResponse } from '@medusajs/framework/workflows-sdk'\n\nimport { syncMeilisearchProductsStep } from '../steps/sync-meilisearch-products'\n\nexport const syncMeilisearchWorkflow = createWorkflow(\n  'sync-meilisearch-workflow',\n  function () {\n    return new WorkflowResponse(syncMeilisearchProductsStep())\n  }\n)\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/api/middlewares.ts",
      "content": "import { meilisearchStoreMiddlewares } from './store/meilisearch/products/search/middlewares'\n\nexport const allMeilisearchMiddlewares = [...meilisearchStoreMiddlewares]\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/api/admin/meilisearch/route.ts",
      "content": "import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework'\n\nimport { MEILISEARCH_MODULE, MeilisearchModuleService } from '../../../modules/meilisearch'\nimport { IndexType } from '../../../modules/meilisearch/types'\nimport { syncMeilisearchWorkflow } from '../../../workflows/meilisearch/workflows/sync-meilisearch'\n\nexport const POST = async (\n  req: AuthenticatedMedusaRequest,\n  res: MedusaResponse\n) => {\n  await syncMeilisearchWorkflow.run({\n    container: req.scope,\n  })\n\n  res.status(200).json({ message: 'Sync in progress' })\n}\n\nexport const GET = async (\n  req: AuthenticatedMedusaRequest,\n  res: MedusaResponse\n) => {\n  const meilisearch =\n    req.scope.resolve<MeilisearchModuleService>(MEILISEARCH_MODULE)\n\n  const host = meilisearch.getHost()\n  const { documentCount, isHealthy } = await meilisearch.getStatus()\n\n  res.status(200).json({\n    host,\n    index: IndexType.PRODUCT,\n    documentCount,\n    isHealthy,\n  })\n}\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/api/store/meilisearch/products/search/validators.ts",
      "content": "import { z } from 'zod'\n\nconst safeStringPattern = /^[a-zA-Z0-9_\\-]+$/\n\nexport const StoreMeilisearchFiltersSchema = z.object({\n  categories: z\n    .array(z.string().min(1).regex(safeStringPattern, 'Invalid category ID'))\n    .optional(),\n  price_min: z.number().min(0).optional(),\n  price_max: z.number().min(0).optional(),\n  seller_handle: z\n    .string()\n    .min(1)\n    .regex(safeStringPattern, 'Invalid seller handle')\n    .optional(),\n})\n\nexport const StoreMeilisearchSearchSchema = z.object({\n  query: z.string().default(''),\n  page: z.coerce.number().int().min(1).default(1),\n  hitsPerPage: z.coerce.number().int().min(1).max(100).default(12),\n  filters: StoreMeilisearchFiltersSchema.optional(),\n  currency_code: z.string().length(3).optional(),\n  region_id: z.string().optional(),\n  customer_id: z.string().optional(),\n  customer_group_id: z.array(z.string()).optional(),\n})\n\nexport type StoreMeilisearchSearchType = z.infer<typeof StoreMeilisearchSearchSchema>\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/api/store/meilisearch/products/search/middlewares.ts",
      "content": "import { MiddlewareRoute, validateAndTransformBody } from '@medusajs/framework'\n\nimport { StoreMeilisearchSearchSchema } from './validators'\n\nexport const meilisearchStoreMiddlewares: MiddlewareRoute[] = [\n  {\n    methods: ['POST'],\n    matcher: '/store/meilisearch/products/search',\n    middlewares: [validateAndTransformBody(StoreMeilisearchSearchSchema)],\n  },\n]\n",
      "type": "registry:api"
    },
    {
      "path": "meilisearch/api/store/meilisearch/products/search/route.ts",
      "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework'\nimport { ContainerRegistrationKeys, QueryContext } from '@medusajs/framework/utils'\n\nimport { MEILISEARCH_MODULE, MeilisearchModuleService } from '../../../../../modules/meilisearch'\nimport { StoreMeilisearchSearchType } from './validators'\n\nexport const POST = async (\n  req: MedusaRequest<StoreMeilisearchSearchType>,\n  res: MedusaResponse\n) => {\n  const meilisearchService =\n    req.scope.resolve<MeilisearchModuleService>(MEILISEARCH_MODULE)\n  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)\n\n  const {\n    query: searchQuery,\n    page,\n    hitsPerPage,\n    filters,\n    currency_code,\n    region_id,\n    customer_id,\n    customer_group_id,\n  } = req.validatedBody\n\n  // Build filter string server-side — seller.status = \"active\" is always enforced (FR-003)\n  const filterParts: string[] = ['seller.status = \"active\"']\n\n  if (filters?.categories?.length) {\n    const ids = filters.categories.map((c) => `\"${c}\"`).join(', ')\n    filterParts.push(`categories.id IN [${ids}]`)\n  }\n  if (filters?.price_min !== undefined) {\n    filterParts.push(`variants.prices.amount >= ${filters.price_min}`)\n  }\n  if (filters?.price_max !== undefined) {\n    filterParts.push(`variants.prices.amount <= ${filters.price_max}`)\n  }\n  if (filters?.seller_handle) {\n    filterParts.push(`seller.handle = \"${filters.seller_handle}\"`)\n  }\n\n  const filter = filterParts.join(' AND ')\n\n  const searchResult = await meilisearchService.search(searchQuery, {\n    filter,\n    page,\n    hitsPerPage,\n    attributesToRetrieve: ['id'],\n  })\n\n  const productIds = searchResult.hits.map((hit) => hit.id)\n\n  if (!productIds.length) {\n    return res.json({\n      products: [],\n      totalHits: searchResult.totalHits,\n      page: searchResult.page,\n      totalPages: searchResult.totalPages,\n      hitsPerPage: searchResult.hitsPerPage,\n      processingTimeMs: searchResult.processingTimeMs,\n      query: searchResult.query,\n    })\n  }\n\n  const hasPricingContext =\n    currency_code || region_id || customer_id || customer_group_id\n\n  const contextParams: Record<string, unknown> = {}\n  if (hasPricingContext) {\n    contextParams.variants = {\n      calculated_price: QueryContext({\n        ...(currency_code && { currency_code }),\n        ...(region_id && { region_id }),\n        ...(customer_id && { customer_id }),\n        ...(customer_group_id && { customer_group_id }),\n      }),\n    }\n  }\n\n  const { data: products } = await query.graph({\n    entity: 'product',\n    fields: [\n      '*',\n      'images.*',\n      'options.*',\n      'options.values.*',\n      'variants.*',\n      'variants.options.*',\n      'variants.prices.*',\n      ...(hasPricingContext ? ['variants.calculated_price.*'] : []),\n      'categories.*',\n      'collection.*',\n      'type.*',\n      'tags.*',\n      'seller.*',\n    ],\n    filters: { id: productIds },\n    ...(Object.keys(contextParams).length > 0 && { context: contextParams }),\n  })\n\n  const productMap = new Map(products.map((p) => [p.id, p]))\n  const orderedProducts = productIds.map((id) => productMap.get(id)).filter(Boolean)\n\n  return res.json({\n    products: orderedProducts,\n    totalHits: searchResult.totalHits,\n    page: searchResult.page,\n    totalPages: searchResult.totalPages,\n    hitsPerPage: searchResult.hitsPerPage,\n    processingTimeMs: searchResult.processingTimeMs,\n    query: searchResult.query,\n    facetDistribution: searchResult.facetDistribution,\n  })\n}\n",
      "type": "registry:api"
    }
  ]
}