#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, ".."); const categoriesDir = path.join(root, "src", "content", "categories"); const defaultEndpoint = "http://localhost:1337/graphql"; const endpoint = process.env.STRAPI_GRAPHQL_URL ?? process.env.PUBLIC_GRAPHQL_URL ?? defaultEndpoint; const token = process.env.STRAPI_TOKEN ?? process.env.STRAPI_API_TOKEN ?? process.env.AUTH_TOKEN; const isDryRun = process.argv.includes("--dry-run"); function readCategorySource(slug) { const filePath = path.join(categoriesDir, `${slug}.json`); if (!fs.existsSync(filePath)) { throw new Error(`Missing source file: ${path.relative(root, filePath)}`); } return JSON.parse(fs.readFileSync(filePath, "utf8")); } function discoverCategorySlugs() { if (!fs.existsSync(categoriesDir)) { throw new Error(`Categories directory missing: ${path.relative(root, categoriesDir)}`); } const slugs = []; for (const dirent of fs.readdirSync(categoriesDir, { withFileTypes: true })) { if (!dirent.isFile() || !dirent.name.endsWith(".json")) continue; slugs.push(path.basename(dirent.name, ".json")); } if (slugs.length === 0) { throw new Error(`No *.json categories in ${path.relative(root, categoriesDir)}`); } return slugs; } function normalizeProducts(products) { if (!Array.isArray(products)) return []; const unique = new Set(); for (const product of products) { if (typeof product !== "string") continue; const trimmed = product.trim(); if (!trimmed) continue; unique.add(trimmed); } return [...unique].map((value) => ({ value })); } function collectVendorsBySlug(categorySlugs) { const vendors = new Map(); for (const categorySlug of categorySlugs) { const source = readCategorySource(categorySlug); const categoryVendors = Array.isArray(source.vendors) ? source.vendors : []; for (const rawVendor of categoryVendors) { const name = typeof rawVendor?.name === "string" ? rawVendor.name.trim() : ""; if (!name) continue; const vendorSlug = typeof rawVendor?.slug === "string" ? rawVendor.slug.trim() : ""; if (!vendorSlug) { throw new Error(`Missing vendor slug for "${name}" in ${categorySlug}.json`); } const description = typeof rawVendor?.description === "string" ? rawVendor.description.trim() : ""; const products = normalizeProducts(rawVendor?.products); if (!vendors.has(vendorSlug)) { vendors.set(vendorSlug, { slug: vendorSlug, title: name, description, products: new Map(products.map((item) => [item.value, item])), categories: new Set([categorySlug]), }); continue; } const existing = vendors.get(vendorSlug); existing.categories.add(categorySlug); for (const product of products) { existing.products.set(product.value, product); } if (!existing.description && description) { existing.description = description; } } } return [...vendors.values()].map((vendor) => ({ slug: vendor.slug, title: vendor.title, description: vendor.description, products: [...vendor.products.values()], categorySlugs: [...vendor.categories], })); } async function gql(query, variables = {}) { const headers = { "Content-Type": "application/json", Accept: "application/graphql-response+json, application/json", }; if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify({ query, variables }), }); if (!response.ok) { throw new Error(`GraphQL HTTP ${response.status}: ${response.statusText}`); } const payload = await response.json(); if (payload.errors?.length) { const messages = payload.errors.map((err) => err.message).join("; "); throw new Error(`GraphQL error: ${messages}`); } return payload.data; } async function fetchCategoryDocumentIds() { const query = ` query CategoryIds { categories(pagination: { limit: 500 }) { documentId slug } } `; const data = await gql(query); const map = new Map(); for (const category of data?.categories ?? []) { if (!category?.slug || !category?.documentId) continue; map.set(category.slug, category.documentId); } return map; } async function findVendorBySlug(slug) { const query = ` query VendorBySlug($slug: String!) { vendors(filters: { slug: { eq: $slug } }, pagination: { limit: 1 }) { documentId slug } } `; const data = await gql(query, { slug }); return data?.vendors?.[0] ?? null; } async function createVendor(input) { const mutation = ` mutation CreateVendor($data: VendorInput!) { createVendor(data: $data) { documentId slug title } } `; const data = await gql(mutation, { data: input }); return data?.createVendor; } async function updateVendor(documentId, input) { const mutation = ` mutation UpdateVendor($documentId: ID!, $data: VendorInput!) { updateVendor(documentId: $documentId, data: $data) { documentId slug title } } `; const data = await gql(mutation, { documentId, data: input }); return data?.updateVendor; } async function seedVendors(vendors, categoryIdBySlug, dryRun = false) { let createdCount = 0; let updatedCount = 0; for (const vendor of vendors) { const categoryDocumentIds = vendor.categorySlugs.map((slug) => { const docId = categoryIdBySlug.get(slug); if (!docId) { throw new Error( `Category "${slug}" is missing in Strapi. Seed categories before seeding vendors.` ); } return docId; }); const input = { slug: vendor.slug, title: vendor.title, description: vendor.description, products: vendor.products, categories: categoryDocumentIds, }; const existing = await findVendorBySlug(vendor.slug); if (existing?.documentId) { if (dryRun) { updatedCount += 1; console.log(`DRY-RUN UPDATE ${vendor.slug} -> ${vendor.title}`); continue; } const updated = await updateVendor(existing.documentId, input); updatedCount += 1; console.log(`UPDATE ${updated.slug} -> ${updated.title}`); continue; } if (dryRun) { createdCount += 1; console.log(`DRY-RUN CREATE ${vendor.slug} -> ${vendor.title}`); continue; } const created = await createVendor(input); createdCount += 1; console.log(`CREATE ${created.slug} -> ${created.title}`); } return { createdCount, updatedCount }; } async function main() { console.log(`Seeding vendors to ${endpoint}${isDryRun ? " (dry-run)" : ""}`); if (!token) { console.warn("No STRAPI token provided. If endpoint is protected, set STRAPI_TOKEN."); } const categorySlugs = discoverCategorySlugs(); const vendors = collectVendorsBySlug(categorySlugs); const categoryIdBySlug = await fetchCategoryDocumentIds(); const { createdCount, updatedCount } = await seedVendors(vendors, categoryIdBySlug, isDryRun); console.log(`Done. created=${createdCount}, updated=${updatedCount}, total=${vendors.length}`); } main().catch((error) => { console.error(error); process.exit(1); });