import "server-only"; import { catalogProductSchema, filterCatalogProducts, inventoryStateSchema, productSlugSchema, resolveInventoryAvailability, type CatalogProduct, } from "@salixcommerce/core"; import { PostgrestClient } from "@supabase/postgrest-js"; import type { CatalogListInput, CatalogRepository } from "./repository"; interface SupabaseCatalogCredentials { readonly currency: string; readonly serviceRoleKey: string; readonly url: string; } type DatabaseRow = Record; export function createSupabaseCatalogRepository( credentials: SupabaseCatalogCredentials, ): CatalogRepository { const client = new PostgrestClient(`${credentials.url.replace(/\/$/u, "")}/rest/v1`, { headers: { apikey: credentials.serviceRoleKey, authorization: `Bearer ${credentials.serviceRoleKey}`, }, timeout: 10_000, }); async function loadProducts(slug?: string): Promise { const now = new Date(); let productQuery = client .from("products") .select("id,slug,name,description,status,attributes,tags,published_at,created_at,updated_at") .eq("status", "published") .lte("published_at", now.toISOString()) .order("name") .order("id") .limit(1000); if (slug !== undefined) productQuery = productQuery.eq("slug", slug); const productResult = await productQuery; const productRows = readRows(productResult.data, productResult.error, "products"); if (productRows.length === 0) return []; const productIds = productRows.map((row) => readIdentifier(row, "id")); const [variantResult, imageResult, assignmentResult] = await Promise.all([ client .from("product_variants") .select( "id,product_id,sku,name,options,price_minor,currency,active,position,allow_backorder", ) .in("product_id", productIds) .eq("active", true) .order("position") .order("id"), client .from("product_images") .select("id,product_id,path,alt_text,position") .in("product_id", productIds) .order("position") .order("id"), client .from("product_category_assignments") .select("product_id,category_id") .in("product_id", productIds), ]); const variantRows = readRows(variantResult.data, variantResult.error, "product variants"); const imageRows = readRows(imageResult.data, imageResult.error, "product images"); const assignmentRows = readRows( assignmentResult.data, assignmentResult.error, "product category assignments", ); const variantIds = variantRows.map((row) => readIdentifier(row, "id")); const categoryIds = [ ...new Set(assignmentRows.map((row) => readIdentifier(row, "category_id"))), ]; const [inventoryRows, categoryRows] = await Promise.all([ loadRowsByIdentifiers( variantIds, () => client .from("inventory_items") .select("variant_id,mode,on_hand,reserved") .in("variant_id", variantIds), "inventory items", ), loadRowsByIdentifiers( categoryIds, () => client.from("product_categories").select("id,slug").in("id", categoryIds), "product categories", ), ]); const inventoryByVariant = new Map( inventoryRows.map((row) => [readIdentifier(row, "variant_id"), row]), ); const categorySlugById = new Map( categoryRows.map((row) => [readIdentifier(row, "id"), readString(row, "slug")]), ); return productRows.map((productRow) => { const productId = readIdentifier(productRow, "id"); return catalogProductSchema.parse({ attributes: readObject(productRow, "attributes"), categorySlugs: assignmentRows .filter((row) => readIdentifier(row, "product_id") === productId) .map((row) => { const categoryId = readIdentifier(row, "category_id"); const categorySlug = categorySlugById.get(categoryId); if (categorySlug === undefined) { throw new Error(`Catalog data is missing category ${categoryId}.`); } return categorySlug; }), createdAt: readString(productRow, "created_at"), description: readNullableString(productRow, "description"), id: productId, images: imageRows .filter((row) => readIdentifier(row, "product_id") === productId) .map((row) => ({ altText: readString(row, "alt_text"), id: readIdentifier(row, "id"), path: readString(row, "path"), position: readSafeNumber(row, "position"), })), name: readString(productRow, "name"), publishedAt: readNullableString(productRow, "published_at"), slug: readString(productRow, "slug"), status: readString(productRow, "status"), tags: readStringArray(productRow, "tags"), updatedAt: readString(productRow, "updated_at"), variants: variantRows .filter((row) => readIdentifier(row, "product_id") === productId) .map((row) => { const variantId = readIdentifier(row, "id"); const inventory = inventoryByVariant.get(variantId); if (inventory === undefined) { throw new Error(`Catalog data is missing inventory for variant ${variantId}.`); } return { active: readBoolean(row, "active"), availability: resolveInventoryAvailability( inventoryStateSchema.parse({ mode: readString(inventory, "mode"), onHand: readSafeNumber(inventory, "on_hand"), reserved: readSafeNumber(inventory, "reserved"), }), readBoolean(row, "allow_backorder"), ), id: variantId, name: readString(row, "name"), options: readObject(row, "options"), position: readSafeNumber(row, "position"), price: { amountMinor: readSafeNumber(row, "price_minor"), currency: readCatalogCurrency(row, credentials.currency), }, productId, sku: readNullableString(row, "sku"), }; }), }); }); } return Object.freeze({ async findBySlug(slug: string): Promise { const parsedSlug = productSlugSchema.safeParse(slug); if (!parsedSlug.success) return null; return (await loadProducts(parsedSlug.data))[0] ?? null; }, async list(input: CatalogListInput = {}): Promise { return filterCatalogProducts(await loadProducts(), input); }, }); } async function loadRowsByIdentifiers( identifiers: readonly string[], query: () => PromiseLike<{ data: unknown; error: unknown }>, label: string, ): Promise { if (identifiers.length === 0) return []; const result = await query(); return readRows(result.data, result.error, label); } function readRows(data: unknown, error: unknown, label: string): DatabaseRow[] { if (error !== null) throw new Error(`Unable to load ${label} from the catalog provider.`); if (!Array.isArray(data) || !data.every(isDatabaseRow)) { throw new TypeError(`Catalog provider returned invalid ${label}.`); } return data; } function isDatabaseRow(value: unknown): value is DatabaseRow { return typeof value === "object" && value !== null && !Array.isArray(value); } function readString(row: DatabaseRow, key: string): string { const value = row[key]; if (typeof value !== "string") throw new TypeError(`Catalog field ${key} must be a string.`); return value; } function readNullableString(row: DatabaseRow, key: string): string | null { const value = row[key]; if (value === null) return null; if (typeof value !== "string") { throw new TypeError(`Catalog field ${key} must be a string or null.`); } return value; } function readBoolean(row: DatabaseRow, key: string): boolean { const value = row[key]; if (typeof value !== "boolean") throw new TypeError(`Catalog field ${key} must be boolean.`); return value; } function readIdentifier(row: DatabaseRow, key: string): string { const value = row[key]; if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return String(value); if (typeof value === "string" && /^[1-9][0-9]*$/u.test(value)) return value; throw new TypeError(`Catalog field ${key} must be a positive safe identifier.`); } function readSafeNumber(row: DatabaseRow, key: string): number { const value = row[key]; const parsed = typeof value === "string" && /^-?[0-9]+$/u.test(value) ? Number(value) : value; if (typeof parsed !== "number" || !Number.isSafeInteger(parsed)) { throw new TypeError(`Catalog field ${key} must be a safe integer.`); } return parsed; } function readObject(row: DatabaseRow, key: string): Record { const value = row[key]; if (!isDatabaseRow(value)) throw new TypeError(`Catalog field ${key} must be an object.`); return value; } function readStringArray(row: DatabaseRow, key: string): string[] { const value = row[key]; if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { throw new TypeError(`Catalog field ${key} must be a string array.`); } return value; } function readCatalogCurrency(row: DatabaseRow, expectedCurrency: string): string { const currency = readString(row, "currency"); if (currency !== expectedCurrency) { throw new Error(`Catalog variant currency must be ${expectedCurrency}.`); } return currency; }