← back to Vendor Discount Agent
create-vendor-pages.js
208 lines
#!/usr/bin/env node
/**
* Create Shopify inquiry pages for vendor brands with < 5 products.
* Skips vendors that already have a page (checks by handle).
* Respects Shopify rate limits (2 req/s bucket, with 500ms delay between calls).
*/
const { Client } = require('pg');
const https = require('https');
const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const SHOPIFY_TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');
const API_VERSION = '2024-01';
const DB_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:15432/dw_unified');
// Vendors that already have pages (will also be checked dynamically)
const SKIP_HANDLES = new Set([
'vendor-anthology',
'vendor-carnegie',
'brand-mckenzie',
]);
function buildBodyHtml(vendorName) {
return `<div style="max-width:700px;margin:0 auto;padding:30px 20px;font-family:Georgia,serif;">
<h2 style="text-align:center;color:#2c2420;">${vendorName}</h2>
<p style="text-align:center;font-size:16px;line-height:1.7;color:#5a4a3a;">
We carry ${vendorName} products and can source any item from their current collections.
Contact us for pricing, samples, and availability.
</p>
<div style="text-align:center;margin:30px 0;">
<a href="/pages/contact-us" style="display:inline-block;background:#2c2420;color:white;padding:14px 36px;border-radius:4px;text-decoration:none;font-size:15px;">Contact Us for ${vendorName}</a>
</div>
</div>`;
}
function vendorCodeToHandle(vendorCode) {
// Convert underscores to hyphens, lowercase
return 'vendor-' + vendorCode.toLowerCase().replace(/_/g, '-');
}
function shopifyRequest(method, path, body = null) {
return new Promise((resolve, reject) => {
const options = {
hostname: SHOPIFY_DOMAIN,
path: `/admin/api/${API_VERSION}${path}`,
method,
headers: {
'X-Shopify-Access-Token': SHOPIFY_TOKEN,
'Content-Type': 'application/json',
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
const remaining = res.headers['x-shopify-shop-api-call-limit'];
try {
const parsed = JSON.parse(data);
resolve({ status: res.statusCode, data: parsed, rateLimit: remaining });
} catch (e) {
resolve({ status: res.statusCode, data: data, rateLimit: remaining });
}
});
});
req.on('error', reject);
req.setTimeout(15000, () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function getExistingVendorPages() {
// Fetch all pages and check for vendor- handles
const existing = new Set();
let sinceId = 0;
let hasMore = true;
while (hasMore) {
const url = `/pages.json?limit=250&fields=id,handle&since_id=${sinceId}`;
const res = await shopifyRequest('GET', url);
if (res.status !== 200) {
console.error('Failed to fetch pages:', res.status, res.data);
break;
}
const pages = res.data.pages || [];
for (const p of pages) {
if (p.handle && (p.handle.startsWith('vendor-') || p.handle.startsWith('brand-'))) {
existing.add(p.handle);
}
sinceId = Math.max(sinceId, p.id);
}
hasMore = pages.length === 250;
if (hasMore) await sleep(500);
}
return existing;
}
async function main() {
console.log('=== Vendor Inquiry Page Creator ===');
console.log(`Started: ${new Date().toISOString()}\n`);
// 1. Get existing Shopify pages
console.log('Fetching existing Shopify pages...');
const existingHandles = await getExistingVendorPages();
console.log(`Found ${existingHandles.size} existing vendor/brand pages:`);
for (const h of existingHandles) {
console.log(` - ${h}`);
SKIP_HANDLES.add(h);
}
console.log('');
// 2. Query DB for vendor list
const client = new Client({ connectionString: DB_URL });
await client.connect();
const result = await client.query(
`SELECT vendor_name, vendor_code
FROM vendor_registry
WHERE show_on_brands_page = true AND shopify_count < 5
ORDER BY vendor_name`
);
await client.end();
const vendors = result.rows;
console.log(`Found ${vendors.length} vendors in DB with < 5 products.\n`);
// 3. Create pages
let created = 0;
let skipped = 0;
let errors = 0;
for (let i = 0; i < vendors.length; i++) {
const { vendor_name, vendor_code } = vendors[i];
const handle = vendorCodeToHandle(vendor_code);
// Skip if already exists
if (SKIP_HANDLES.has(handle)) {
console.log(`[${i + 1}/${vendors.length}] SKIP ${vendor_name} (${handle}) - already exists`);
skipped++;
continue;
}
// Create the page
const pageData = {
page: {
title: vendor_name,
handle: handle,
body_html: buildBodyHtml(vendor_name),
published: true,
},
};
try {
const res = await shopifyRequest('POST', '/pages.json', pageData);
if (res.status === 201 || res.status === 200) {
const pageId = res.data.page?.id || 'unknown';
console.log(`[${i + 1}/${vendors.length}] CREATED ${vendor_name} (${handle}) -> ID: ${pageId} [${res.rateLimit}]`);
created++;
SKIP_HANDLES.add(handle);
} else if (res.status === 422 && JSON.stringify(res.data).includes('handle')) {
// Handle already taken
console.log(`[${i + 1}/${vendors.length}] SKIP ${vendor_name} (${handle}) - handle conflict (422)`);
skipped++;
} else if (res.status === 429) {
console.log(`[${i + 1}/${vendors.length}] RATE LIMITED - waiting 2s and retrying...`);
await sleep(2000);
i--; // Retry this vendor
continue;
} else {
console.error(`[${i + 1}/${vendors.length}] ERROR ${vendor_name} (${handle}): ${res.status}`, JSON.stringify(res.data).substring(0, 200));
errors++;
}
} catch (err) {
console.error(`[${i + 1}/${vendors.length}] EXCEPTION ${vendor_name}: ${err.message}`);
errors++;
}
// Rate limit: wait 500ms between requests (2 req/s bucket)
await sleep(500);
}
console.log('\n=== SUMMARY ===');
console.log(`Total vendors: ${vendors.length}`);
console.log(`Created: ${created}`);
console.log(`Skipped: ${skipped}`);
console.log(`Errors: ${errors}`);
console.log(`Finished: ${new Date().toISOString()}`);
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});