← back to Dw Activation Debug TK11314
scripts/capture-readiness.js
57 lines
#!/usr/bin/env node
'use strict';
// READ-ONLY: this command contains a single GraphQL query, no mutations or vision calls.
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const args = process.argv.slice(2);
const value = name => args[args.indexOf(name) + 1];
for (const flag of ['--audit', '--since', '--until', '--output'])
if (!args.includes(flag) || !value(flag)) throw new Error(`Required: ${flag}`);
const env = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8');
const token = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1]?.trim().replace(/^['"]|['"]$/g, '');
if (!token) throw new Error('Missing Shopify credential');
const query = `query($ids:[ID!]!){nodes(ids:$ids){... on Product{
id title vendor status descriptionHtml tags updatedAt
images(first:1){nodes{url}}
variants(first:100){nodes{sku title price} pageInfo{hasNextPage}}
metafields(first:100){nodes{namespace key value} pageInfo{hasNextPage}}
}}}`;
async function main() {
const rows = fs.readFileSync(value('--audit'), 'utf8').trim().split('\n').map(line => JSON.parse(line))
.filter(r => r.ts >= value('--since') && r.ts < value('--until'));
const ids = [...new Set(rows.map(r => r.shopify_id).filter(Boolean))];
if (!ids.length) throw new Error('No product IDs in explicit interval');
const products = []; const costs = []; let batchSize = 10;
for (let index = 0; index < ids.length;) {
const batch = ids.slice(index, index + batchSize);
const response = await fetch('https://designer-laboratory-sandbox.myshopify.com/admin/api/2026-07/graphql.json', {
method: 'POST', headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { ids: batch } }), signal: AbortSignal.timeout(30000),
});
const data = await response.json();
if (data.errors?.some(e => e.extensions?.code === 'MAX_COST_EXCEEDED') && batchSize > 1) {
batchSize = Math.max(1, Math.floor(batchSize / 2)); continue;
}
if (!response.ok || data.errors || !Array.isArray(data.data?.nodes))
throw new Error(`Shopify read failed HTTP${response.status}: ${JSON.stringify(data.errors || []).slice(0, 300)}`);
costs.push(data.extensions?.cost);
products.push(...data.data.nodes);
index += batch.length;
if (index % 100 === 0 || index === ids.length) console.log(`Captured ${index}/${ids.length}`);
const budget = data.extensions?.cost?.throttleStatus;
const cost = data.extensions?.cost?.requestedQueryCost || 0;
if (budget && budget.currentlyAvailable < cost && budget.restoreRate > 0)
await new Promise(resolve => setTimeout(resolve, Math.min(30000, 1000 * (cost - budget.currentlyAvailable) / budget.restoreRate + 100)));
}
const result = { captured_at: new Date().toISOString(), source: 'Shopify Admin GraphQL GET-equivalent query',
mutations: 0, vision_calls: 0, source_audit: value('--audit'), since: value('--since'), until: value('--until'),
products, cost_summary: { requests: costs.length } };
fs.mkdirSync(path.dirname(value('--output')), { recursive: true });
fs.writeFileSync(value('--output'), JSON.stringify(result, null, 2) + '\n');
console.log(JSON.stringify({ output: value('--output'), products: products.filter(Boolean).length,
missing: products.filter(p => !p).length,
incomplete: products.filter(p => p?.variants?.pageInfo?.hasNextPage || p?.metafields?.pageInfo?.hasNextPage).length }));
}
main().catch(error => { console.error(error.message); process.exitCode = 1; });