{
  "$schema": "https://registry.mercurjs.com/registry-item.json",
  "name": "algolia",
  "description": "Algolia search integration with product indexing, event-driven sync, and store search API.",
  "dependencies": [
    "algoliasearch"
  ],
  "registryDependencies": [],
  "docs": "## Configuration\n\nAdd the algolia module to your `medusa-config.ts`:\n\n```ts\nmodules: [\n  {\n    resolve: './modules/algolia',\n    options: {\n      appId: process.env.ALGOLIA_APP_ID,\n      apiKey: process.env.ALGOLIA_API_KEY,\n    },\n  },\n]\n```\n\n## Environment Variables\n\n```\nALGOLIA_APP_ID=your_algolia_app_id\nALGOLIA_API_KEY=your_algolia_admin_api_key\n```\n\n## Middlewares\n\nAdd algolia middlewares to your `api/middlewares.ts`:\n\n```ts\nimport { defineMiddlewares } from \"@medusajs/medusa\";\nimport { algoliaMiddlewares } from \"./store/products/search/middlewares\";\n\nexport default defineMiddlewares({\n  routes: [...algoliaMiddlewares],\n});\n```\n\nIf you already have a `middlewares.ts` file, merge the algolia middleware imports and spread them into your existing `routes` array.\n\n## Database Migrations\n\nAfter installing the block, run migrations for the new links:\n\n```bash\nnpx medusa db:migrate\n```\n\n## Run codegen\n\nAfter installing, regenerate SDK types:\n\n```bash\nnpx @mercurjs/cli@latest codegen\n```",
  "categories": [
    "module",
    "workflow",
    "api",
    "link"
  ],
  "files": [
    {
      "path": "algolia/modules/algolia/index.ts",
      "content": "import { Module } from '@medusajs/framework/utils'\n\nimport AlgoliaModuleService from './service'\n\nexport const ALGOLIA_MODULE = 'algolia'\nexport { AlgoliaModuleService }\nexport { defaultProductSettings } from './service'\n\nexport default Module(ALGOLIA_MODULE, {\n  service: AlgoliaModuleService,\n})\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/modules/algolia/service.ts",
      "content": "import {\n  Action,\n  Algoliasearch,\n  BatchRequest,\n  IndexSettings,\n  SearchParams,\n  SearchResponse,\n  algoliasearch,\n} from \"algoliasearch\";\n\nimport { AlgoliaEntity, IndexType } from \"./types\";\n\ntype ModuleOptions = {\n  appId: string;\n  apiKey: string;\n};\n\nexport const defaultProductSettings: IndexSettings = {\n  searchableAttributes: [\n    \"title\",\n    \"subtitle\",\n    \"tags.value\",\n    \"type.value\",\n    \"categories.name\",\n    \"collection.title\",\n    \"variants.title\",\n  ],\n};\n\nclass AlgoliaModuleService {\n  private options_: ModuleOptions;\n  private algolia_: Algoliasearch;\n\n  constructor(_: unknown, options: ModuleOptions) {\n    this.options_ = options;\n    this.algolia_ = algoliasearch(this.options_.appId, this.options_.apiKey);\n  }\n\n  getAppId() {\n    return this.options_.appId;\n  }\n\n  checkIndex(index: IndexType) {\n    return this.algolia_.indexExists({\n      indexName: index,\n    });\n  }\n\n  updateSettings(index: IndexType, settings: IndexSettings) {\n    return this.algolia_.setSettings({\n      indexName: index,\n      indexSettings: settings,\n    });\n  }\n\n  batch(type: IndexType, toAdd: AlgoliaEntity[], toDelete: string[]) {\n    const addRequests: BatchRequest[] = toAdd.map((entity) => {\n      return {\n        action: \"addObject\" as Action,\n        objectID: entity.id,\n        body: entity,\n      };\n    });\n\n    const deleteRequests: BatchRequest[] = toDelete.map((id) => {\n      return {\n        action: \"deleteObject\" as Action,\n        objectID: id,\n        body: {},\n      };\n    });\n\n    const requests = [...addRequests, ...deleteRequests];\n\n    return this.algolia_.batch({\n      indexName: type,\n      batchWriteParams: {\n        requests,\n      },\n    });\n  }\n\n  batchUpsert(type: IndexType, entities: AlgoliaEntity[]) {\n    return this.algolia_.batch({\n      indexName: type,\n      batchWriteParams: {\n        requests: entities.map((entity) => {\n          return {\n            action: \"addObject\",\n            objectID: entity.id,\n            body: entity,\n          };\n        }),\n      },\n    });\n  }\n\n  batchDelete(type: IndexType, ids: string[]) {\n    return this.algolia_.batch({\n      indexName: type,\n      batchWriteParams: {\n        requests: ids.map((id) => {\n          return {\n            action: \"deleteObject\",\n            objectID: id,\n            body: {},\n          };\n        }),\n      },\n    });\n  }\n\n  upsert(type: IndexType, entity: AlgoliaEntity) {\n    return this.algolia_.addOrUpdateObject({\n      indexName: type,\n      objectID: entity.id,\n      body: entity,\n    });\n  }\n\n  delete(type: IndexType, id: string) {\n    return this.algolia_.deleteObject({\n      indexName: type,\n      objectID: id,\n    });\n  }\n\n  partialUpdate(\n    type: IndexType,\n    entity: Partial<AlgoliaEntity> & { id: string },\n  ) {\n    return this.algolia_.partialUpdateObject({\n      indexName: type,\n      objectID: entity.id,\n      attributesToUpdate: { ...entity },\n    });\n  }\n\n  search<T = Record<string, unknown>>(\n    indexName: IndexType,\n    params: SearchParams,\n  ): Promise<SearchResponse<T>> {\n    return this.algolia_.searchSingleIndex<T>({\n      indexName,\n      searchParams: params,\n    });\n  }\n}\n\nexport default AlgoliaModuleService;\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/modules/algolia/types.ts",
      "content": "import { z } from 'zod'\n\nexport enum IndexType {\n  PRODUCT = 'products'\n}\n\nexport enum AlgoliaEvents {\n  PRODUCTS_CHANGED = 'algolia.products.changed',\n  PRODUCTS_DELETED = 'algolia.products.deleted'\n}\n\nexport enum IntermediateEvents {\n  FULFILLMENT_SET_CHANGED = 'algolia.intermediate.fulfillment_set.changed',\n  SERVICE_ZONE_CHANGED = 'algolia.intermediate.service_zone.changed',\n  SHIPPING_OPTION_CHANGED = 'algolia.intermediate.shipping_option.changed',\n  STOCK_LOCATION_CHANGED = 'algolia.intermediate.stock_location.changed',\n  INVENTORY_ITEM_CHANGED = 'algolia.intermediate.inventory_item.changed'\n}\n\nexport const AlgoliaVariantValidator = 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  ups: z.string().nullish(),\n  allow_backorder: z.boolean(),\n  manage_inventory: z.boolean(),\n  hs_code: z.string().nullish(),\n  origin_country: z.string().nullish(),\n  mid_code: z.string().nullish(),\n  material: z.string().nullish(),\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      title: z.string().nullish(),\n      currency_code: z.string(),\n      min_quantity: z.number().nullish(),\n      max_quantity: z.number().nullish(),\n      rules_count: z.number(),\n      amount: z.number(),\n    })\n  ),\n})\n\nexport type AlgoliaProduct = z.infer<typeof AlgoliaProductValidator>\nexport const AlgoliaProductValidator = 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  average_rating: z.coerce.number().nullable().default(null),\n  supported_countries: z.array(z.string()).nullable().default([]),\n  options: z.array(z.record(z.string())).nullable().default(null),\n  images: z\n    .array(\n      z.object({\n        id: z.string(),\n        url: z.string(),\n        rank: z.number(),\n      })\n    )\n    .nullable()\n    .optional(),\n  collection: z\n    .object({\n      title: z.string(),\n    })\n    .nullable()\n    .optional(),\n  type: z\n    .object({\n      value: z.string(),\n    })\n    .nullable()\n    .optional(),\n  tags: z\n    .array(\n      z.object({\n        value: z.string(),\n      })\n    )\n    .optional(),\n  categories: z\n    .array(\n      z.object({\n        name: z.string(),\n        id: z.string(),\n      })\n    )\n    .optional(),\n  variants: z.any().nullable().default(null),\n  sku: z.string().nullable().optional(),\n  ean: z.string().nullable().optional(),\n  upc: z.string().nullable().optional(),\n  barcode: z.string().nullable().optional(),\n  hs_code: z.string().nullable().optional(),\n  mid_code: z.string().nullable().optional(),\n  weight: z.coerce.number().nullable().optional(),\n  length: z.coerce.number().nullable().optional(),\n  height: z.coerce.number().nullable().optional(),\n  width: z.coerce.number().nullable().optional(),\n  origin_country: z.string().nullable().optional(),\n  material: z.string().nullable().optional(),\n  seller: z\n    .object({\n      id: z.string(),\n      handle: z.string().nullish(),\n      status: z.string().nullish(),\n    })\n    .nullable(),\n})\n\nexport type AlgoliaEntity = AlgoliaProduct\n\nexport type AlgoliaSearchResult<T> = {\n  hits: T[]\n  nbHits: number\n  page: number\n  nbPages: number\n  hitsPerPage: number\n  facets?: Record<string, Record<string, number>>\n  facets_stats?: Record<\n    string,\n    { min: number; max: number; avg: number; sum: number }\n  >\n  processingTimeMS: number\n  query: string\n}\n\nexport interface IAlgoliaModuleService {\n  search<T = Record<string, unknown>>(\n    indexName: IndexType,\n    params: Record<string, unknown>\n  ): Promise<AlgoliaSearchResult<T>>\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/workflows/algolia/steps/sync-algolia-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 { ALGOLIA_MODULE, AlgoliaModuleService } from '../../../modules/algolia'\nimport { AlgoliaEvents, IndexType } from '../../../modules/algolia/types'\n\nconst CHUNK_SIZE = 100\n\nexport const syncAlgoliaProductsStep = createStep(\n  'sync-algolia-products',\n  async (_: void, { container }) => {\n    const query = container.resolve<RemoteQueryFunction>(ContainerRegistrationKeys.QUERY)\n    const algolia = container.resolve<AlgoliaModuleService>(ALGOLIA_MODULE)\n\n    const { data: productsToDelete } = await query.graph({\n      entity: 'product',\n      filters: {\n        $or: [\n          {\n            deleted_at: {\n              $ne: null,\n            },\n          },\n          {\n            status: {\n              $ne: 'published',\n            },\n          },\n        ],\n      },\n      fields: ['id'],\n    })\n\n    await algolia.batchDelete(\n      IndexType.PRODUCT,\n      productsToDelete.map((p) => p.id)\n    )\n\n    const { data: publishedProducts } = await query.graph({\n      entity: 'product',\n      filters: {\n        status: 'published',\n      },\n      fields: ['id'],\n    })\n\n    const productsToInsert = publishedProducts.map((p) => p.id)\n    const productChunks: string[][] = []\n    for (let i = 0; i < productsToInsert.length; i += CHUNK_SIZE) {\n      productChunks.push(productsToInsert.slice(i, i + CHUNK_SIZE))\n    }\n\n    const eventBus = container.resolve<IEventBusModuleService>(Modules.EVENT_BUS)\n    for (const chunk of productChunks) {\n      await eventBus.emit({\n        name: AlgoliaEvents.PRODUCTS_CHANGED,\n        data: {\n          ids: chunk,\n        },\n      })\n    }\n\n    return new StepResponse()\n  }\n)\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/workflows/algolia/workflows/sync-algolia.ts",
      "content": "\nimport { createWorkflow, WorkflowResponse } from '@medusajs/framework/workflows-sdk'\nimport { syncAlgoliaProductsStep } from '../steps/sync-algolia-products'\n\nexport const syncAlgoliaWorkflow = createWorkflow(\n  'sync-algolia-workflow',\n  function () {\n    return new WorkflowResponse(syncAlgoliaProductsStep())\n  }\n)\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-product-events-bridge.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { IEventBusModuleService } from '@medusajs/framework/types'\nimport {\n  ContainerRegistrationKeys,\n  Modules,\n} from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents } from '../modules/algolia/types'\n\nexport default async function algoliaProductEventsBridgeHandler({\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  logger.info(`Algolia bridge: received event ${event.name} with data ${JSON.stringify(event.data)}`)\n\n  const isDelete = event.name === 'product.product.deleted' || event.name === 'product.deleted'\n\n  await eventBus.emit({\n    name: isDelete\n      ? AlgoliaEvents.PRODUCTS_DELETED\n      : AlgoliaEvents.PRODUCTS_CHANGED,\n    data: { ids: [event.data.id] },\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: 'algolia-product-events-bridge'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-products-changed.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys } from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents, IndexType } from '../modules/algolia/types'\nimport { ALGOLIA_MODULE, AlgoliaModuleService } from '../modules/algolia'\nimport {\n  filterProductsByStatus,\n  findAndTransformAlgoliaProducts\n} from './utils/algolia-product'\n\nexport default async function algoliaProductsChangedHandler({\n  event,\n  container\n}: SubscriberArgs<{ ids: string[] }>) {\n  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)\n\n  try {\n    const algolia = container.resolve<AlgoliaModuleService>(ALGOLIA_MODULE)\n\n    const { published, other } = await filterProductsByStatus(\n      container,\n      event.data.ids\n    )\n\n    logger.debug(\n      `Algolia sync: Processing ${event.data.ids.length} products - ${published.length} to upsert, ${other.length} to delete`\n    )\n\n    const productsToInsert = published.length\n      ? await findAndTransformAlgoliaProducts(container, published)\n      : []\n\n    await algolia.batch(IndexType.PRODUCT, productsToInsert, other)\n\n    logger.debug(\n      `Algolia sync: Successfully synced ${productsToInsert.length} products`\n    )\n  } catch (error: unknown) {\n    logger.error(\n      `Algolia 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: AlgoliaEvents.PRODUCTS_CHANGED,\n  context: {\n    subscriberId: 'algolia-products-changed-handler'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-products-deleted.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys } from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents, IndexType } from '../modules/algolia/types'\nimport { ALGOLIA_MODULE, AlgoliaModuleService } from '../modules/algolia'\n\nexport default async function algoliaProductsDeletedHandler({\n  event,\n  container\n}: SubscriberArgs<{ ids: string[] }>) {\n  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)\n\n  try {\n    const algolia = container.resolve<AlgoliaModuleService>(ALGOLIA_MODULE)\n\n    logger.debug(\n      `Algolia sync: Deleting ${event.data.ids.length} products from index`\n    )\n\n    await algolia.batchDelete(IndexType.PRODUCT, event.data.ids)\n\n    logger.debug(\n      `Algolia sync: Successfully deleted products ${event.data.ids.join(', ')}`\n    )\n  } catch (error: unknown) {\n    logger.error(\n      `Algolia 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: AlgoliaEvents.PRODUCTS_DELETED,\n  context: {\n    subscriberId: 'algolia-products-deleted-handler'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/utils/algolia-product.ts",
      "content": "import { z } from 'zod'\n\nimport { MedusaContainer } from '@medusajs/framework'\nimport {\n  ContainerRegistrationKeys,\n  arrayDifference,\n} from '@medusajs/framework/utils'\n\nimport {\n  AlgoliaProductValidator,\n  AlgoliaVariantValidator\n} from '../../modules/algolia/types'\n\nasync function selectProductVariantsSupportedCountries(\n  container: MedusaContainer,\n  product_id: string\n) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n  const { data: variants } = await query.graph({\n    entity: 'product_variant',\n    fields: ['inventory_items.inventory.location_levels.location_id'],\n    filters: {\n      product_id\n    }\n  })\n\n  let location_ids: string[] = []\n\n  for (const variant of variants) {\n    const inventory_items =\n      (variant as any).inventory_items?.map((item: any) => item.inventory) || []\n    const locations = inventory_items\n      .flatMap((inventory_item: any) => inventory_item.location_levels || [])\n      .map((level: any) => level.location_id)\n\n    location_ids = location_ids.concat(locations)\n  }\n\n  const { data: stock_locations } = await query.graph({\n    entity: 'stock_location',\n    fields: ['fulfillment_sets.service_zones.geo_zones.country_code'],\n    filters: {\n      id: location_ids\n    }\n  })\n\n  let country_codes: string[] = []\n\n  for (const location of stock_locations) {\n    const fulfillmentSets =\n      (location as any).fulfillment_sets?.flatMap((set: any) => set.service_zones || []) || []\n    const codes = fulfillmentSets\n      .flatMap((sz: any) => sz.geo_zones || [])\n      .map((gz: any) => gz.country_code)\n\n    country_codes = country_codes.concat(codes)\n  }\n\n  return [...new Set(country_codes)]\n}\n\nasync function selectProductSeller(\n  container: MedusaContainer,\n  product_id: string\n) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n\n  const {\n    data: [product]\n  } = await query.graph({\n    entity: 'product',\n    fields: ['seller.id', 'seller.handle', 'seller.status'],\n    filters: {\n      id: product_id\n    }\n  })\n\n\n\n  const p = product as any\n  return p?.seller?.id\n    ? {\n        id: p.seller.id as string,\n        handle: p.seller.handle as string | null,\n        status: p.seller.status as string | null\n      }\n    : null\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: {\n      id: ids\n    }\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\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\nexport async function findAndTransformAlgoliaProducts(\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.options.prices.*',\n      'variants.prices.*',\n      'options.*',\n      'options.values.*',\n      'images.*'\n    ],\n    filters: ids.length\n      ? {\n          id: ids,\n          status: 'published'\n        }\n      : { status: 'published' }\n  })\n\n  for (const product of products as any[]) {\n    product.average_rating = 0\n    product.supported_countries = await selectProductVariantsSupportedCountries(\n      container,\n      product.id\n    )\n    product.seller = await selectProductSeller(container, product.id)\n\n    product.options = (product.options ?? [])\n      .filter((option: any) => option?.title && option?.values)\n      .map((option: any) => {\n        return option.values.map((value: any) => {\n          const entry: Record<string, string> = {}\n          entry[option.title.toLowerCase()] = value.value\n          return entry\n        })\n      })\n      .flat()\n\n    product.variants = z\n      .array(AlgoliaVariantValidator)\n      .parse(product.variants ?? [])\n    product.variants = (product.variants ?? [])\n      .map((variant: any) => {\n        return (variant.options ?? []).reduce((entry: any, item: any) => {\n          if (item?.option?.title) {\n            entry[item.option.title.toLowerCase()] = item.value\n          }\n          return entry\n        }, variant)\n      })\n      .flat()\n\n  }\n\n  return z.array(AlgoliaProductValidator).parse(products)\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-fulfillment-set-changed.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents, IntermediateEvents } from '../modules/algolia/types'\nimport sellerFulfillmentSet from '@mercurjs/core/links/fulfillment-set-seller-link'\nimport productSellerLink from '@mercurjs/core/links/product-seller-link'\n\nexport default async function fulfillmentSetChangedHandler({\n  event,\n  container\n}: SubscriberArgs<{ id: string }>) {\n  const fulfillment_set_id = event.data.id\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n  const eventBus = container.resolve(Modules.EVENT_BUS)\n\n  const {\n    data: [seller]\n  } = await query.graph({\n    entity: sellerFulfillmentSet.entryPoint,\n    fields: ['seller_id'],\n    filters: {\n      fulfillment_set_id\n    },\n    withDeleted: true\n  })\n\n  if (!seller) {\n    return\n  }\n\n  const { data: products } = await query.graph({\n    entity: productSellerLink.entryPoint,\n    fields: ['product_id'],\n    filters: {\n      seller_id: seller.seller_id\n    }\n  })\n\n  await eventBus.emit({\n    name: AlgoliaEvents.PRODUCTS_CHANGED,\n    data: { ids: products.map((p) => p.product_id) }\n  })\n}\n\nexport const config: SubscriberConfig = {\n  event: IntermediateEvents.FULFILLMENT_SET_CHANGED,\n  context: {\n    subscriberId: 'fulfillment-set-changed-handler'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-inventory-item-changed.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents, IntermediateEvents } from '../modules/algolia/types'\n\nexport default async function inventoryItemChangedHandler({\n  event,\n  container\n}: SubscriberArgs<{ id: string | string[] }>) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n  const eventBus = container.resolve(Modules.EVENT_BUS)\n\n  const { data: items } = await query.graph({\n    entity: 'inventory_item',\n    fields: ['variants.product_id'],\n    filters: {\n      id: event.data.id\n    }\n  })\n\n  const products = items\n    .flatMap((items) => items.variants)\n    .map((variant) => variant.product_id)\n\n  await eventBus.emit({\n    name: AlgoliaEvents.PRODUCTS_CHANGED,\n    data: { ids: [...new Set(products)] }\n  })\n}\n\nexport const config: SubscriberConfig = {\n  event: IntermediateEvents.INVENTORY_ITEM_CHANGED,\n  context: {\n    subscriberId: 'inventory-item-changed-handler'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-service-zone-changed.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents, IntermediateEvents } from '../modules/algolia/types'\nimport productSellerLink from '@mercurjs/core/links/product-seller-link'\nimport sellerServiceZone from '@mercurjs/core/links/service-zone-seller-link'\n\nexport default async function serviceZoneChangedHandler({\n  event,\n  container\n}: SubscriberArgs<{ id: string }>) {\n  const service_zone_id = event.data.id\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n  const eventBus = container.resolve(Modules.EVENT_BUS)\n\n  const {\n    data: [seller]\n  } = await query.graph({\n    entity: sellerServiceZone.entryPoint,\n    fields: ['seller_id'],\n    filters: {\n      service_zone_id\n    },\n    withDeleted: true\n  })\n\n  if (!seller) {\n    return\n  }\n\n  const { data: products } = await query.graph({\n    entity: productSellerLink.entryPoint,\n    fields: ['product_id'],\n    filters: {\n      seller_id: seller.seller_id\n    }\n  })\n\n  await eventBus.emit({\n    name: AlgoliaEvents.PRODUCTS_CHANGED,\n    data: { ids: products.map((p) => p.product_id) }\n  })\n}\n\nexport const config: SubscriberConfig = {\n  event: IntermediateEvents.SERVICE_ZONE_CHANGED,\n  context: {\n    subscriberId: 'service-zone-changed-handler'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-shipping-option-changed.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents, IntermediateEvents } from '../modules/algolia/types'\nimport productSellerLink from '@mercurjs/core/links/product-seller-link'\nimport shippingOptionSellerLink from '@mercurjs/core/links/shipping-option-seller-link'\n\nexport default async function shippingOptionChangedHandler({\n  event,\n  container\n}: SubscriberArgs<{ id: string }>) {\n  const shipping_option_id = event.data.id\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n  const eventBus = container.resolve(Modules.EVENT_BUS)\n\n  const {\n    data: [seller]\n  } = await query.graph({\n    entity: shippingOptionSellerLink.entryPoint,\n    fields: ['seller_id'],\n    filters: {\n      shipping_option_id\n    },\n    withDeleted: true\n  })\n\n  if (!seller) {\n    return\n  }\n\n  const { data: products } = await query.graph({\n    entity: productSellerLink.entryPoint,\n    fields: ['product_id'],\n    filters: {\n      seller_id: seller.seller_id\n    }\n  })\n\n  await eventBus.emit({\n    name: AlgoliaEvents.PRODUCTS_CHANGED,\n    data: { ids: products.map((p) => p.product_id) }\n  })\n}\n\nexport const config: SubscriberConfig = {\n  event: IntermediateEvents.SHIPPING_OPTION_CHANGED,\n  context: {\n    subscriberId: 'shipping-option-changed-handler'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/subscribers/algolia-stock-location-changed.ts",
      "content": "import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'\n\nimport { AlgoliaEvents, IntermediateEvents } from '../modules/algolia/types'\nimport productSellerLink from '@mercurjs/core/links/product-seller-link'\nimport stockLocationSellerLink from '@mercurjs/core/links/stock-location-seller-link'\n\nexport default async function stockLocationChangedHandler({\n  event,\n  container\n}: SubscriberArgs<{ id: string }>) {\n  const stock_location_id = event.data.id\n  const query = container.resolve(ContainerRegistrationKeys.QUERY)\n  const eventBus = container.resolve(Modules.EVENT_BUS)\n\n  const {\n    data: [seller]\n  } = await query.graph({\n    entity: stockLocationSellerLink.entryPoint,\n    fields: ['seller_id'],\n    filters: {\n      stock_location_id\n    },\n    withDeleted: true\n  })\n\n  if (!seller) {\n    return\n  }\n\n  const { data: products } = await query.graph({\n    entity: productSellerLink.entryPoint,\n    fields: ['product_id'],\n    filters: {\n      seller_id: seller.seller_id\n    }\n  })\n\n  await eventBus.emit({\n    name: AlgoliaEvents.PRODUCTS_CHANGED,\n    data: { ids: products.map((p) => p.product_id) }\n  })\n}\n\nexport const config: SubscriberConfig = {\n  event: IntermediateEvents.STOCK_LOCATION_CHANGED,\n  context: {\n    subscriberId: 'stock-location-changed-handler'\n  }\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/api/middlewares.ts",
      "content": "import { algoliaMiddlewares } from './store/products/search/middlewares'\n\nexport const allAlgoliaMiddlewares = [...algoliaMiddlewares]\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/api/admin/algolia/route.ts",
      "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework'\n\nimport { ALGOLIA_MODULE, AlgoliaModuleService } from '../../../modules/algolia'\nimport { IndexType } from '../../../modules/algolia/types'\nimport { syncAlgoliaWorkflow } from '../../../workflows/algolia/workflows/sync-algolia'\n\nexport const POST = async (req: MedusaRequest, res: MedusaResponse) => {\n  await syncAlgoliaWorkflow.run({\n    container: req.scope,\n  })\n\n  res.status(200).json({ message: 'Sync in progress' })\n}\n\nexport const GET = async (req: MedusaRequest, res: MedusaResponse) => {\n  const algoliaService =\n    req.scope.resolve<AlgoliaModuleService>(ALGOLIA_MODULE)\n\n  const appId = algoliaService.getAppId()\n  const productIndex = await algoliaService.checkIndex(IndexType.PRODUCT)\n  res.status(200).json({ appId, productIndex })\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/api/store/products/search/route.ts",
      "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework'\nimport {\n  ContainerRegistrationKeys,\n  QueryContext\n} from '@medusajs/framework/utils'\n\nimport { ALGOLIA_MODULE } from '../../../../modules/algolia'\nimport { IAlgoliaModuleService, IndexType } from '../../../../modules/algolia/types'\nimport { StoreSearchProductsType } from './validators'\n\ntype AlgoliaHit = {\n  id: string\n}\n\nexport const POST = async (\n  req: MedusaRequest<StoreSearchProductsType>,\n  res: MedusaResponse\n) => {\n  const algoliaService =\n    req.scope.resolve<IAlgoliaModuleService>(ALGOLIA_MODULE)\n  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)\n\n  const {\n    query: searchQuery,\n    page,\n    hitsPerPage,\n    filters,\n    facets,\n    maxValuesPerFacet,\n    currency_code,\n    region_id,\n    customer_id,\n    customer_group_id\n  } = req.validatedBody\n\n  const searchParams: Record<string, unknown> = {\n    query: searchQuery,\n    page,\n    hitsPerPage,\n    attributesToRetrieve: ['id'],\n    attributesToHighlight: []\n  }\n\n  if (filters) {\n    searchParams.filters = filters\n  }\n  if (facets) {\n    searchParams.facets = facets\n  }\n  if (maxValuesPerFacet) {\n    searchParams.maxValuesPerFacet = maxValuesPerFacet\n  }\n\n  const algoliaResult = await algoliaService.search<AlgoliaHit>(\n    IndexType.PRODUCT,\n    searchParams\n  )\n\n  const productIds = algoliaResult.hits.map((hit) => hit.id)\n\n  if (productIds.length === 0) {\n    return res.json({\n      products: [],\n      nbHits: algoliaResult.nbHits,\n      page: algoliaResult.page,\n      nbPages: algoliaResult.nbPages,\n      hitsPerPage: algoliaResult.hitsPerPage,\n      facets: algoliaResult.facets,\n      facets_stats: algoliaResult.facets_stats,\n      processingTimeMS: algoliaResult.processingTimeMS,\n      query: algoliaResult.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: {\n      id: productIds\n    },\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\n    .map((id) => productMap.get(id))\n    .filter(Boolean)\n\n  res.json({\n    products: orderedProducts,\n    nbHits: algoliaResult.nbHits,\n    page: algoliaResult.page,\n    nbPages: algoliaResult.nbPages,\n    hitsPerPage: algoliaResult.hitsPerPage,\n    facets: algoliaResult.facets,\n    facets_stats: algoliaResult.facets_stats,\n    processingTimeMS: algoliaResult.processingTimeMS,\n    query: algoliaResult.query\n  })\n}\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/api/store/products/search/middlewares.ts",
      "content": "import {\n  MiddlewareRoute,\n  validateAndTransformBody\n} from '@medusajs/framework'\n\nimport { StoreSearchProductsSchema } from './validators'\n\nexport const algoliaMiddlewares: MiddlewareRoute[] = [\n  {\n    methods: ['POST'],\n    matcher: '/store/products/search',\n    middlewares: [validateAndTransformBody(StoreSearchProductsSchema)]\n  }\n]\n",
      "type": "registry:api"
    },
    {
      "path": "algolia/api/store/products/search/validators.ts",
      "content": "import { z } from 'zod'\n\nexport type StoreSearchProductsType = z.infer<typeof StoreSearchProductsSchema>\n\nexport const StoreSearchProductsSchema = z.object({\n  query: z.string().default(''),\n  page: z.coerce.number().int().min(0).default(0),\n  hitsPerPage: z.coerce.number().int().min(1).max(100).default(12),\n  filters: z.string().optional(),\n  facets: z.array(z.string()).optional(),\n  maxValuesPerFacet: z.coerce.number().int().min(1).max(1000).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",
      "type": "registry:api"
    }
  ]
}