← back to Ventura Claw
server/connectors/shopify.js
79 lines
// Shopify (DW prod store) — REAL.
// Reads SHOPIFY_ADMIN_TOKEN + SHOPIFY_STORE from env. Routed via secrets-manager.
// Docs: https://shopify.dev/docs/api/admin-rest
function token(c) {
const t = c?.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN;
if (!t) throw new Error("SHOPIFY_ADMIN_TOKEN not set");
return t;
}
function store(c) {
return c?.SHOPIFY_STORE || process.env.SHOPIFY_STORE || "designer-laboratory-sandbox.myshopify.com";
}
async function call(method, path, body, c) {
const url = `https://${store(c)}/admin/api/2024-10${path}`;
const r = await fetch(url, {
method,
headers: { "X-Shopify-Access-Token": token(c), "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(20_000)
});
const text = await r.text();
if (!r.ok) throw new Error(`shopify ${method} ${path} ${r.status}: ${text.slice(0, 200)}`);
return text ? JSON.parse(text) : {};
}
module.exports = {
meta: { id: "shopify", name: "Shopify", category: "commerce", docsUrl: "https://shopify.dev/docs/api/admin-rest", auth: "api_key", realImpl: true },
fields: [
{ key: "SHOPIFY_ADMIN_TOKEN", label: "Admin Access Token (shpat_...)", type: "password", required: true, hint: "Settings → Apps → Develop apps → reveal Admin API access token" },
{ key: "SHOPIFY_STORE", label: "Store domain", type: "text", required: false, hint: "e.g. mystore.myshopify.com (optional, defaults to env)" },
],
configured(c) { return !!(c?.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN); },
async health(c) {
if (!this.configured(c)) return { ok: false, reason: "no_token" };
try {
const d = await call("GET", "/shop.json", null, c);
return { ok: true, store: d.shop?.name, domain: d.shop?.myshopify_domain, plan: d.shop?.plan_name };
} catch (e) { return { ok: false, reason: e.message }; }
},
actions: {
async "shop.info"(_, c) { return call("GET", "/shop.json", null, c); },
async "products.list"({ limit }, c) { return call("GET", `/products.json?limit=${Math.min(limit||10, 250)}`, null, c); },
async "products.count"(_, c) { return call("GET", "/products/count.json", null, c); },
async "orders.list"({ status, limit }, c) {
const s = status || "any";
return call("GET", `/orders.json?status=${s}&limit=${Math.min(limit||10, 250)}`, null, c);
},
async "orders.recent"(_, c) {
return call("GET", `/orders.json?status=any&limit=20&order=created_at desc`, null, c);
},
async "order.fulfill"({ order_id, tracking_number, tracking_company }, c) {
if (!order_id) throw new Error("order_id required");
return call("POST", `/orders/${order_id}/fulfillments.json`, {
fulfillment: { tracking_number, tracking_company, notify_customer: true }
}, c);
},
async "order.refund"({ order_id, amount, reason }, c) {
if (!order_id) throw new Error("order_id required");
return call("POST", `/orders/${order_id}/refunds.json`, {
refund: { note: reason || "automated refund", shipping: { full_refund: true }, transactions: amount ? [{ amount, kind: "refund" }] : undefined }
}, c);
},
async "product.update"({ product_id, title, tags, body_html, status }, c) {
if (!product_id) throw new Error("product_id required");
const fields = { id: product_id };
if (title !== undefined) fields.title = title;
if (tags !== undefined) fields.tags = tags;
if (body_html !== undefined) fields.body_html = body_html;
if (status !== undefined) fields.status = status;
return call("PUT", `/products/${product_id}.json`, { product: fields }, c);
},
async "collections.list"(_, c) {
const cust = await call("GET", "/custom_collections.json?limit=250", null, c);
const smart = await call("GET", "/smart_collections.json?limit=250", null, c);
return { custom: cust.custom_collections || [], smart: smart.smart_collections || [] };
},
}
};