270 lines
7.4 KiB
JavaScript
270 lines
7.4 KiB
JavaScript
#!/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);
|
|
});
|