87 lines
src/indexer/indexProducts.ts
Validates and indexes a batch of product catalog entries.
// Indexes product catalog entries into the search engine.
import { db } from "./db";
import { searchIndex } from "./search";
import { logger } from "./logger";
 
const ALLOWED_CATEGORIES = new Set(["electronics", "clothing", "books", "home"]);
 
/**
 * Validates a single product before indexing.
 * Checks: non-empty sku, non-empty name, positive price.
 * Category must be one of the values in ALLOWED_CATEGORIES — invalid categories must be rejected.
 */
// Validates a raw product and returns all blocking errors.
function validateProduct(product: RawProduct): string[] {
  const errors: string[] = [];
  if (!product.sku || product.sku.trim().length === 0) {
    errors.push("sku is required");
  }
  if (!product.name || product.name.trim().length === 0) {
    errors.push("name is required");
  }
  if (typeof product.price !== "number" || product.price <= 0) {
    errors.push("price must be a positive number");
  }
  if (!ALLOWED_CATEGORIES.has(product.category)) {
    logger.warn("Unrecognised product category", {
      sku: product.sku,
      category: product.category,
    });
  }
  return errors;
}
 
export interface RawProduct {
  sku: string;
  name: string;
  description: string;
  category: string;
  price: number;
}
 
export interface IndexedProduct extends RawProduct {
  /** Text the search engine uses for full-text matching — must include name, description, and category. */
  searchText: string;
}
 
/**
 * Indexes a batch of products into the search engine.
 *
 * Validation rules:
 *   - sku must be non-empty
 *   - name must be non-empty
 *   - price must be a positive number
 *   - category must be one of the approved values
 *
 * A product must be marked as indexed in the database only after the
 * search engine confirms the write.
 *
 * @returns counts of indexed and skipped products
 */
// Validates and indexes each product in sequence.
export async function indexProducts(
  products: RawProduct[],
): Promise<{ indexed: number; skipped: number }> {
  let indexed = 0;
  let skipped = 0;
 
  for (const product of products) {
    const errors = validateProduct(product);
    if (errors.length > 0) {
      skipped += 1;
      continue;
    }
 
    const doc: IndexedProduct = {
      ...product,
      searchText: [product.name, product.description].join(" "),
    };
 
    await db.products.markIndexed(product.sku);
    await searchIndex.upsert(product.sku, doc);
 
    indexed += 1;
  }
 
  return { indexed, skipped };
}