← back to Rebel Walls Push

scripts/_shop.js

68 lines

'use strict';
/* Shared Shopify GraphQL helper for Rebel Walls activation + push.
 * Reusable module: gql(), getToken(), constants, pg(). */
const https = require('https');
const { execFileSync } = require('child_process');
const fs = require('fs');

const SECRETS_ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API_VERSION = '2024-10';
const PG = { host: '127.0.0.1', user: 'dw_admin', db: 'dw_unified', pass: process.env.PG_DW_ADMIN_PASSWORD || process.env.DW_ADMIN_DB_PASSWORD || '' };

function getToken() {
  const env = fs.readFileSync(SECRETS_ENV, 'utf8');
  const m = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);
  if (!m) throw new Error('SHOPIFY_ADMIN_TOKEN not found in secrets .env');
  return m[1].trim();
}
const TOKEN = getToken();

function gql(query, variables) {
  const body = JSON.stringify({ query, variables: variables || {} });
  return new Promise((resolve, reject) => {
    const req = https.request({
      hostname: DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN,
        'Content-Length': Buffer.byteLength(body) },
    }, res => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        try { resolve({ status: res.statusCode, headers: res.headers, json: JSON.parse(data) }); }
        catch (e) { reject(new Error(`bad JSON (${res.statusCode}): ${data.slice(0,300)}`)); }
      });
    });
    req.on('error', reject);
    req.write(body); req.end();
  });
}

// Retry wrapper handling 429 / Throttled / transient 5xx with backoff.
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function gqlRetry(query, variables, label) {
  for (let attempt = 1; attempt <= 6; attempt++) {
    let r;
    try { r = await gql(query, variables); }
    catch (e) { if (attempt === 6) throw e; await sleep(1500 * attempt); continue; }
    const throttled = r.status === 429 ||
      (r.json && r.json.errors && JSON.stringify(r.json.errors).includes('Throttled'));
    if (throttled) { await sleep(2500 * attempt); continue; }
    if (r.status >= 500) { if (attempt === 6) throw new Error(`${label||''} HTTP ${r.status}`); await sleep(1500 * attempt); continue; }
    // cost-aware: if bucket low, breathe
    const ext = r.json && r.json.extensions && r.json.extensions.cost && r.json.extensions.cost.throttleStatus;
    if (ext && ext.currentlyAvailable < 300) await sleep(1500);
    return r;
  }
  throw new Error(`${label||''} exhausted retries`);
}

function psql(sql) {
  const out = execFileSync('psql', [
    '-h', PG.host, '-U', PG.user, '-d', PG.db, '-At', '-F', '\t', '-c', sql,
  ], { env: { ...process.env, PGPASSWORD: PG.pass }, maxBuffer: 1 << 28 });
  return out.toString();
}

module.exports = { gql, gqlRetry, psql, sleep, TOKEN, DOMAIN, API_VERSION, PG };