← back to Designerwallcoverings

scripts/c34-osborne-gaston/APPLY-blank-reverify-guard.sh

109 lines

#!/usr/bin/env bash
# ============================================================================
# APPLY the drain-time live-blank re-verify guard to the Kamatera worker.
# RUN ON KAMATERA (operates on the local /root path). Steve-gated deploy.
#
#   scp ~/Projects/designerwallcoverings/scripts/c34-osborne-gaston/APPLY-blank-reverify-guard.sh my-server:/tmp/
#   ssh my-server "bash /tmp/APPLY-blank-reverify-guard.sh"
#
# Idempotent: if the guard is already present it exits 0 without touching the
# file. Anchored: refuses to edit if either insertion anchor isn't found
# (so a future worker.js refactor can't cause a silent mis-splice).
# Backs up to worker.js.pre-blankreverify.<ts>, syntax-checks (node -c),
# and restarts via pm2. On syntax failure it auto-restores the backup.
# DTD verdict A (2026-06-18, 2/3) — vp-dw-commerce + vp-engineering APPROVE.
# ============================================================================
set -euo pipefail

W=/root/DW-Agents/shopify-queue-worker/worker.js
TS=$(date +%Y%m%d-%H%M%S)
BAK="${W}.pre-blankreverify.${TS}"

[ -f "$W" ] || { echo "FATAL: $W not found"; exit 1; }

if grep -q 'parseMetafieldCreateJob' "$W"; then
  echo "Already applied (parseMetafieldCreateJob present) — no-op."
  exit 0
fi

cp -p "$W" "$BAK"
echo "Backup → $BAK"

node <<'NODE'
const fs = require('fs');
const W = '/root/DW-Agents/shopify-queue-worker/worker.js';
let s = fs.readFileSync(W, 'utf8');

const HELPERS = `
// --- DRAIN-TIME LIVE BLANK RE-VERIFY (officer-required for c34 specs push) ---
function parseMetafieldCreateJob(job) {
    if (job.method !== 'POST') return null;
    const m = /^\\/products\\/(\\d+)\\/metafields\\.json$/.exec(job.endpoint || '');
    if (!m) return null;
    const payload = typeof job.payload === 'string' ? JSON.parse(job.payload) : job.payload;
    const mf = payload && payload.metafield;
    if (!mf || mf.namespace !== 'custom' || !mf.key) return null;
    return { productId: m[1], namespace: mf.namespace, key: mf.key };
}
async function liveMetafieldAlreadyFilled(productId, key, token) {
    const res = await shopifyRequest('GET', \`/products/\${productId}/metafields.json?namespace=custom&key=\${encodeURIComponent(key)}\`, null, token);
    const list = (res.body && res.body.metafields) || [];
    const hit = list.find(x => x.namespace === 'custom' && x.key === key);
    const val = hit && hit.value;
    return typeof val === 'string' ? val.trim().length > 0 : (val != null);
}

`;

const GUARD = `
    // --- DRAIN-TIME LIVE BLANK RE-VERIFY (blank-fill-only invariant at the drain) ---
    const mfJob = (() => { try { return parseMetafieldCreateJob(job); } catch { return null; } })();
    if (mfJob) {
        try {
            const filled = await liveMetafieldAlreadyFilled(mfJob.productId, mfJob.key, token);
            if (filled) {
                await pool.query(
                    \`UPDATE shopify_api_queue SET status = 'done', response_body = $2, error = $3, completed_at = NOW() WHERE id = $1\`,
                    [job.id,
                     JSON.stringify({ blank_reverify_skip: true, product_id: mfJob.productId, key: mfJob.key }),
                     \`BLANK-REVERIFY SKIP: custom.\${mfJob.key} already non-null live\`]
                );
                await pool.query(\`SELECT pg_notify('shopify_queue_done', $1)\`, [String(job.id)]);
                console.log(\`[blank-reverify] #\${job.id} custom.\${mfJob.key} already filled on product \${mfJob.productId} — skipped write\`);
                return;
            }
        } catch (rvErr) {
            console.error(\`[blank-reverify] #\${job.id} guard error (proceeding with write): \${rvErr.message}\`);
        }
    }

`;

// Anchor 1: top-level helpers go immediately before processJob's declaration.
const A1 = 'async function processJob(job) {';
if (s.indexOf(A1) === -1) { console.error('FATAL: anchor A1 (processJob declaration) not found'); process.exit(2); }
if (s.indexOf(A1) !== s.lastIndexOf(A1)) { console.error('FATAL: anchor A1 not unique'); process.exit(2); }
s = s.replace(A1, HELPERS + A1);

// Anchor 2: the guard block goes immediately before the main shopifyRequest write.
const A2 = '    try {\n        const res = await shopifyRequest(job.method, job.endpoint, job.payload, token);';
if (s.indexOf(A2) === -1) { console.error('FATAL: anchor A2 (main shopifyRequest write) not found'); process.exit(2); }
if (s.indexOf(A2) !== s.lastIndexOf(A2)) { console.error('FATAL: anchor A2 not unique'); process.exit(2); }
s = s.replace(A2, GUARD + A2);

fs.writeFileSync(W, s);
console.log('Inserted helpers + guard block.');
NODE

if node -c "$W"; then
  echo "Syntax OK."
else
  echo "SYNTAX FAILED — restoring backup."
  cp -p "$BAK" "$W"
  echo "Restored $W from $BAK. No restart performed."
  exit 3
fi

pm2 restart shopify-queue-worker --update-env
echo "Done. Guard live. Rollback if needed:  cp -p $BAK $W && pm2 restart shopify-queue-worker"