@elizaos/plugin-shopify-ui 2.0.3-beta.6 → 2.0.3-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CustomersPanel.d.ts +11 -0
- package/dist/CustomersPanel.d.ts.map +1 -0
- package/dist/CustomersPanel.js +86 -0
- package/dist/CustomersPanel.js.map +1 -0
- package/dist/InventoryLevelsPanel.d.ts +10 -0
- package/dist/InventoryLevelsPanel.d.ts.map +1 -0
- package/dist/InventoryLevelsPanel.js +190 -0
- package/dist/InventoryLevelsPanel.js.map +1 -0
- package/dist/OrdersPanel.d.ts +12 -0
- package/dist/OrdersPanel.d.ts.map +1 -0
- package/dist/OrdersPanel.js +170 -0
- package/dist/OrdersPanel.js.map +1 -0
- package/dist/ProductsPanel.d.ts +13 -0
- package/dist/ProductsPanel.d.ts.map +1 -0
- package/dist/ProductsPanel.js +419 -0
- package/dist/ProductsPanel.js.map +1 -0
- package/dist/ShopifyAppView.d.ts +3 -0
- package/dist/ShopifyAppView.d.ts.map +1 -0
- package/dist/ShopifyAppView.helpers.d.ts +11 -0
- package/dist/ShopifyAppView.helpers.d.ts.map +1 -0
- package/dist/ShopifyAppView.helpers.js +59 -0
- package/dist/ShopifyAppView.helpers.js.map +1 -0
- package/dist/ShopifyAppView.interact.d.ts +2 -0
- package/dist/ShopifyAppView.interact.d.ts.map +1 -0
- package/dist/ShopifyAppView.interact.js +93 -0
- package/dist/ShopifyAppView.interact.js.map +1 -0
- package/dist/ShopifyAppView.js +667 -0
- package/dist/ShopifyAppView.js.map +1 -0
- package/dist/ShopifyView.d.ts +18 -0
- package/dist/ShopifyView.d.ts.map +1 -0
- package/dist/ShopifyView.js +143 -0
- package/dist/ShopifyView.js.map +1 -0
- package/dist/StoreOverviewCard.d.ts +13 -0
- package/dist/StoreOverviewCard.d.ts.map +1 -0
- package/dist/StoreOverviewCard.js +23 -0
- package/dist/StoreOverviewCard.js.map +1 -0
- package/dist/components/ShopifySpatialView.d.ts +57 -0
- package/dist/components/ShopifySpatialView.d.ts.map +1 -0
- package/dist/components/ShopifySpatialView.js +419 -0
- package/dist/components/ShopifySpatialView.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +14 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +94 -0
- package/dist/plugin.js.map +1 -0
- package/dist/register-routes.d.ts +2 -0
- package/dist/register-routes.d.ts.map +1 -0
- package/dist/register-routes.js +6 -0
- package/dist/register-routes.js.map +1 -0
- package/dist/register-terminal-view.d.ts +15 -0
- package/dist/register-terminal-view.d.ts.map +1 -0
- package/dist/register-terminal-view.js +37 -0
- package/dist/register-terminal-view.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +17 -0
- package/dist/register.js.map +1 -0
- package/dist/routes.d.ts +18 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +518 -0
- package/dist/routes.js.map +1 -0
- package/dist/shopify-app.d.ts +11 -0
- package/dist/shopify-app.d.ts.map +1 -0
- package/dist/shopify-app.js +16 -0
- package/dist/shopify-app.js.map +1 -0
- package/dist/shopify-view-bundle.d.ts +3 -0
- package/dist/shopify-view-bundle.d.ts.map +1 -0
- package/dist/shopify-view-bundle.js +7 -0
- package/dist/shopify-view-bundle.js.map +1 -0
- package/dist/useShopifyDashboard.d.ts +118 -0
- package/dist/useShopifyDashboard.d.ts.map +1 -0
- package/dist/useShopifyDashboard.js +212 -0
- package/dist/useShopifyDashboard.js.map +1 -0
- package/dist/views/bundle.js +948 -0
- package/dist/views/bundle.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-routes.d.ts","sourceRoot":"","sources":["../src/register-routes.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/register-routes.ts"],"sourcesContent":["import { registerAppRoutePluginLoader } from \"@elizaos/core\";\n\nregisterAppRoutePluginLoader(\"@elizaos/plugin-shopify-ui\", async () => {\n const { shopifyPlugin } = await import(\"./plugin.js\");\n return shopifyPlugin;\n});\n"],"mappings":"AAAA,SAAS,oCAAoC;AAE7C,6BAA6B,8BAA8B,YAAY;AACrE,QAAM,EAAE,cAAc,IAAI,MAAM,OAAO,aAAa;AACpD,SAAO;AACT,CAAC;","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the Shopify view for terminal rendering.
|
|
3
|
+
*
|
|
4
|
+
* The agent terminal mounts plugin views by id from the `@elizaos/tui` terminal
|
|
5
|
+
* registry. This makes the Shopify `viewType: "tui"` declaration render for real
|
|
6
|
+
* in the terminal (the unified {@link ShopifySpatialView}) rather than only
|
|
7
|
+
* navigating a GUI shell. A module-level snapshot lets a host push live store
|
|
8
|
+
* data; with no live store it defaults to the offline overview.
|
|
9
|
+
*/
|
|
10
|
+
import { type ShopifySnapshot } from "./components/ShopifySpatialView.tsx";
|
|
11
|
+
/** Update the snapshot the registered terminal view renders from. */
|
|
12
|
+
export declare function setShopifyTerminalSnapshot(next: ShopifySnapshot): void;
|
|
13
|
+
/** Register the Shopify terminal view; returns an unregister function. */
|
|
14
|
+
export declare function registerShopifyTerminalView(): () => void;
|
|
15
|
+
//# sourceMappingURL=register-terminal-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-terminal-view.d.ts","sourceRoot":"","sources":["../src/register-terminal-view.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EACL,KAAK,eAAe,EAErB,MAAM,qCAAqC,CAAC;AAsB7C,qEAAqE;AACrE,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAEtE;AAED,0EAA0E;AAC1E,wBAAgB,2BAA2B,IAAI,MAAM,IAAI,CAIxD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerSpatialTerminalView } from "@elizaos/ui/spatial/tui";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import {
|
|
4
|
+
ShopifySpatialView
|
|
5
|
+
} from "./components/ShopifySpatialView.js";
|
|
6
|
+
const EMPTY = {
|
|
7
|
+
status: { connected: false, shop: null },
|
|
8
|
+
tab: "overview",
|
|
9
|
+
counts: { productCount: 0, orderCount: 0, customerCount: 0 },
|
|
10
|
+
products: [],
|
|
11
|
+
productsTotal: 0,
|
|
12
|
+
productsPage: 1,
|
|
13
|
+
productSearch: "",
|
|
14
|
+
orders: [],
|
|
15
|
+
ordersTotal: 0,
|
|
16
|
+
orderStatusFilter: "any",
|
|
17
|
+
inventoryItems: [],
|
|
18
|
+
inventoryLocations: [],
|
|
19
|
+
customers: [],
|
|
20
|
+
customersTotal: 0,
|
|
21
|
+
customerSearch: ""
|
|
22
|
+
};
|
|
23
|
+
let current = EMPTY;
|
|
24
|
+
function setShopifyTerminalSnapshot(next) {
|
|
25
|
+
current = next;
|
|
26
|
+
}
|
|
27
|
+
function registerShopifyTerminalView() {
|
|
28
|
+
return registerSpatialTerminalView(
|
|
29
|
+
"shopify",
|
|
30
|
+
() => createElement(ShopifySpatialView, { snapshot: current })
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
registerShopifyTerminalView,
|
|
35
|
+
setShopifyTerminalSnapshot
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=register-terminal-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/register-terminal-view.tsx"],"sourcesContent":["/**\n * Register the Shopify view for terminal rendering.\n *\n * The agent terminal mounts plugin views by id from the `@elizaos/tui` terminal\n * registry. This makes the Shopify `viewType: \"tui\"` declaration render for real\n * in the terminal (the unified {@link ShopifySpatialView}) rather than only\n * navigating a GUI shell. A module-level snapshot lets a host push live store\n * data; with no live store it defaults to the offline overview.\n */\n\nimport { registerSpatialTerminalView } from \"@elizaos/ui/spatial/tui\";\nimport { createElement } from \"react\";\nimport {\n type ShopifySnapshot,\n ShopifySpatialView,\n} from \"./components/ShopifySpatialView.js\";\n\nconst EMPTY: ShopifySnapshot = {\n status: { connected: false, shop: null },\n tab: \"overview\",\n counts: { productCount: 0, orderCount: 0, customerCount: 0 },\n products: [],\n productsTotal: 0,\n productsPage: 1,\n productSearch: \"\",\n orders: [],\n ordersTotal: 0,\n orderStatusFilter: \"any\",\n inventoryItems: [],\n inventoryLocations: [],\n customers: [],\n customersTotal: 0,\n customerSearch: \"\",\n};\n\nlet current: ShopifySnapshot = EMPTY;\n\n/** Update the snapshot the registered terminal view renders from. */\nexport function setShopifyTerminalSnapshot(next: ShopifySnapshot): void {\n current = next;\n}\n\n/** Register the Shopify terminal view; returns an unregister function. */\nexport function registerShopifyTerminalView(): () => void {\n return registerSpatialTerminalView(\"shopify\", () =>\n createElement(ShopifySpatialView, { snapshot: current }),\n );\n}\n"],"mappings":"AAUA,SAAS,mCAAmC;AAC5C,SAAS,qBAAqB;AAC9B;AAAA,EAEE;AAAA,OACK;AAEP,MAAM,QAAyB;AAAA,EAC7B,QAAQ,EAAE,WAAW,OAAO,MAAM,KAAK;AAAA,EACvC,KAAK;AAAA,EACL,QAAQ,EAAE,cAAc,GAAG,YAAY,GAAG,eAAe,EAAE;AAAA,EAC3D,UAAU,CAAC;AAAA,EACX,eAAe;AAAA,EACf,cAAc;AAAA,EACd,eAAe;AAAA,EACf,QAAQ,CAAC;AAAA,EACT,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,gBAAgB,CAAC;AAAA,EACjB,oBAAoB,CAAC;AAAA,EACrB,WAAW,CAAC;AAAA,EACZ,gBAAgB;AAAA,EAChB,gBAAgB;AAClB;AAEA,IAAI,UAA2B;AAGxB,SAAS,2BAA2B,MAA6B;AACtE,YAAU;AACZ;AAGO,SAAS,8BAA0C;AACxD,SAAO;AAAA,IAA4B;AAAA,IAAW,MAC5C,cAAc,oBAAoB,EAAE,UAAU,QAAQ,CAAC;AAAA,EACzD;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AACA,OAAO,eAAe,CAAC"}
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "./shopify-app.js";
|
|
2
|
+
import { registerAppShellPage } from "@elizaos/ui/app-shell-registry";
|
|
3
|
+
if (typeof window === "undefined") {
|
|
4
|
+
void import("./register-terminal-view.js").then((m) => m.registerShopifyTerminalView()).catch(() => {
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
registerAppShellPage({
|
|
8
|
+
id: "shopify",
|
|
9
|
+
pluginId: "@elizaos/plugin-shopify-ui",
|
|
10
|
+
label: "Shopify",
|
|
11
|
+
icon: "ShoppingBag",
|
|
12
|
+
path: "/shopify",
|
|
13
|
+
loader: () => import("./shopify-view-bundle.js").then((m) => ({
|
|
14
|
+
default: m.ShopifyView
|
|
15
|
+
}))
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/register.ts"],"sourcesContent":["// Self-register Shopify overlay app at import time.\nimport \"./shopify-app.js\";\nimport { registerAppShellPage } from \"@elizaos/ui/app-shell-registry\";\n\n// In a terminal host (the Node agent, no DOM), register the Shopify view so it\n// renders inline in the terminal. Lazy + DOM-guarded so the terminal engine\n// stays out of browser/mobile bundles.\nif (typeof window === \"undefined\") {\n void import(\"./register-terminal-view.js\")\n .then((m) => m.registerShopifyTerminalView())\n .catch(() => {\n // Terminal rendering is best-effort; never block plugin load.\n });\n}\n\n// iOS/Android disable DynamicViewLoader, so register this view's already-bundled\n// component as an in-process app-shell page. Web/desktop dedupe it against the\n// agent-served bundle entry (network wins -> DynamicViewLoader), so it only adds\n// the render path on native. See packages/app/src/mobile-plugin-views.ts.\nregisterAppShellPage({\n id: \"shopify\",\n pluginId: \"@elizaos/plugin-shopify-ui\",\n label: \"Shopify\",\n icon: \"ShoppingBag\",\n path: \"/shopify\",\n loader: () =>\n import(\"./shopify-view-bundle.js\").then((m) => ({\n default: m.ShopifyView,\n })),\n});\n"],"mappings":"AACA,OAAO;AACP,SAAS,4BAA4B;AAKrC,IAAI,OAAO,WAAW,aAAa;AACjC,OAAK,OAAO,6BAA6B,EACtC,KAAK,CAAC,MAAM,EAAE,4BAA4B,CAAC,EAC3C,MAAM,MAAM;AAAA,EAEb,CAAC;AACL;AAMA,qBAAqB;AAAA,EACnB,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ,MACN,OAAO,0BAA0B,EAAE,KAAK,CAAC,OAAO;AAAA,IAC9C,SAAS,EAAE;AAAA,EACb,EAAE;AACN,CAAC;","names":[]}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shopify dashboard API routes.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/shopify/status
|
|
5
|
+
* GET /api/shopify/products?page=N&limit=N&q=Q
|
|
6
|
+
* POST /api/shopify/products body: { title, vendor?, productType?, price? }
|
|
7
|
+
* GET /api/shopify/orders?status=S&limit=N
|
|
8
|
+
* GET /api/shopify/inventory
|
|
9
|
+
* POST /api/shopify/inventory/:itemId/adjust body: { delta, locationId? }
|
|
10
|
+
* GET /api/shopify/customers?q=Q&limit=N
|
|
11
|
+
*
|
|
12
|
+
* Credentials are read from process.env:
|
|
13
|
+
* SHOPIFY_STORE_DOMAIN — e.g. mystore.myshopify.com
|
|
14
|
+
* SHOPIFY_ACCESS_TOKEN — Shopify Admin API access token
|
|
15
|
+
*/
|
|
16
|
+
import type http from "node:http";
|
|
17
|
+
export declare function handleShopifyRoute(req: http.IncomingMessage, res: http.ServerResponse, pathname: string, method: string): Promise<boolean>;
|
|
18
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAqElC,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAqsBlB"}
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { sendJson, sendJsonError } from "@elizaos/app-core/api/response";
|
|
2
|
+
import { logger } from "@elizaos/core";
|
|
3
|
+
const API_VERSION = "2025-04";
|
|
4
|
+
const ORDER_STATUS_FILTER_VALUES = [
|
|
5
|
+
"any",
|
|
6
|
+
"paid",
|
|
7
|
+
"pending",
|
|
8
|
+
"refunded",
|
|
9
|
+
"partially_refunded"
|
|
10
|
+
];
|
|
11
|
+
const ORDER_STATUS_FILTERS = new Set(ORDER_STATUS_FILTER_VALUES);
|
|
12
|
+
function resolveShopifyConfig() {
|
|
13
|
+
const storeDomain = process.env.SHOPIFY_STORE_DOMAIN?.trim() ?? null;
|
|
14
|
+
const accessToken = process.env.SHOPIFY_ACCESS_TOKEN?.trim() ?? null;
|
|
15
|
+
if (!storeDomain || !accessToken) return null;
|
|
16
|
+
return { storeDomain, accessToken };
|
|
17
|
+
}
|
|
18
|
+
async function shopifyGql(config, query, variables) {
|
|
19
|
+
const domain = config.storeDomain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
20
|
+
const url = `https://${domain}/admin/api/${API_VERSION}/graphql.json`;
|
|
21
|
+
const resp = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"X-Shopify-Access-Token": config.accessToken
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ query, variables }),
|
|
28
|
+
signal: AbortSignal.timeout(15e3)
|
|
29
|
+
});
|
|
30
|
+
if (!resp.ok) {
|
|
31
|
+
const text = await resp.text().catch(() => "");
|
|
32
|
+
throw new Error(`Shopify API ${resp.status}: ${text.slice(0, 200)}`);
|
|
33
|
+
}
|
|
34
|
+
const json = await resp.json();
|
|
35
|
+
if (json.errors?.length) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Shopify GraphQL: ${json.errors.map((e) => e.message).join(", ")}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return json.data;
|
|
41
|
+
}
|
|
42
|
+
async function handleShopifyRoute(req, res, pathname, method) {
|
|
43
|
+
if (!pathname.startsWith("/api/shopify")) return false;
|
|
44
|
+
const config = resolveShopifyConfig();
|
|
45
|
+
if (method === "GET" && pathname === "/api/shopify/status") {
|
|
46
|
+
if (!config) {
|
|
47
|
+
sendJson(res, 200, { connected: false, shop: null });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const data = await shopifyGql(
|
|
52
|
+
config,
|
|
53
|
+
`{ shop { name myshopifyDomain plan { displayName } email currencyCode } }`
|
|
54
|
+
);
|
|
55
|
+
sendJson(res, 200, {
|
|
56
|
+
connected: true,
|
|
57
|
+
shop: {
|
|
58
|
+
name: data.shop.name,
|
|
59
|
+
domain: data.shop.myshopifyDomain,
|
|
60
|
+
plan: data.shop.plan.displayName,
|
|
61
|
+
email: data.shop.email,
|
|
62
|
+
currencyCode: data.shop.currencyCode,
|
|
63
|
+
...typeof data.shop.productsCount?.count === "number" ? { productCount: data.shop.productsCount.count } : {}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.error(
|
|
68
|
+
`[shopify/status] ${err instanceof Error ? err.message : String(err)}`
|
|
69
|
+
);
|
|
70
|
+
sendJson(res, 200, {
|
|
71
|
+
connected: false,
|
|
72
|
+
shop: null,
|
|
73
|
+
error: err instanceof Error ? err.message : "Connection failed"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (!config) {
|
|
79
|
+
sendJsonError(
|
|
80
|
+
res,
|
|
81
|
+
404,
|
|
82
|
+
"Shopify not configured (SHOPIFY_STORE_DOMAIN / SHOPIFY_ACCESS_TOKEN not set)"
|
|
83
|
+
);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (method === "GET" && pathname === "/api/shopify/products") {
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
89
|
+
const page = Math.max(1, Number(url.searchParams.get("page") ?? "1"));
|
|
90
|
+
const limit = Math.min(
|
|
91
|
+
50,
|
|
92
|
+
Math.max(1, Number(url.searchParams.get("limit") ?? "20"))
|
|
93
|
+
);
|
|
94
|
+
const search = url.searchParams.get("q")?.trim() || null;
|
|
95
|
+
const countData = await shopifyGql(
|
|
96
|
+
config,
|
|
97
|
+
`query CountProducts($query: String) {
|
|
98
|
+
productsCount(query: $query) { count }
|
|
99
|
+
}`,
|
|
100
|
+
{ query: search }
|
|
101
|
+
);
|
|
102
|
+
let after = null;
|
|
103
|
+
let pageProducts = [];
|
|
104
|
+
for (let currentPage = 1; currentPage <= page; currentPage++) {
|
|
105
|
+
const data = await shopifyGql(
|
|
106
|
+
config,
|
|
107
|
+
`query ListProductsPage($first: Int!, $after: String, $query: String) {
|
|
108
|
+
products(first: $first, after: $after, query: $query, sortKey: TITLE) {
|
|
109
|
+
edges {
|
|
110
|
+
cursor
|
|
111
|
+
node {
|
|
112
|
+
id title status productType vendor totalInventory updatedAt
|
|
113
|
+
featuredImage { url }
|
|
114
|
+
priceRangeV2 { minVariantPrice { amount } maxVariantPrice { amount } }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
pageInfo { hasNextPage endCursor }
|
|
118
|
+
}
|
|
119
|
+
}`,
|
|
120
|
+
{
|
|
121
|
+
first: limit,
|
|
122
|
+
after,
|
|
123
|
+
query: search
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
if (currentPage === page) {
|
|
127
|
+
pageProducts = data.products.edges;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
if (!data.products.pageInfo.hasNextPage || !data.products.pageInfo.endCursor) {
|
|
131
|
+
pageProducts = [];
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
after = data.products.pageInfo.endCursor;
|
|
135
|
+
}
|
|
136
|
+
const products = pageProducts.map((edge) => ({
|
|
137
|
+
id: edge.node.id,
|
|
138
|
+
title: edge.node.title,
|
|
139
|
+
status: edge.node.status,
|
|
140
|
+
productType: edge.node.productType,
|
|
141
|
+
vendor: edge.node.vendor,
|
|
142
|
+
totalInventory: edge.node.totalInventory,
|
|
143
|
+
priceRange: {
|
|
144
|
+
min: edge.node.priceRangeV2.minVariantPrice.amount,
|
|
145
|
+
max: edge.node.priceRangeV2.maxVariantPrice.amount
|
|
146
|
+
},
|
|
147
|
+
imageUrl: edge.node.featuredImage?.url ?? null,
|
|
148
|
+
updatedAt: edge.node.updatedAt
|
|
149
|
+
}));
|
|
150
|
+
sendJson(res, 200, {
|
|
151
|
+
products,
|
|
152
|
+
total: countData.productsCount.count,
|
|
153
|
+
page,
|
|
154
|
+
pageSize: limit
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : "Failed to fetch products";
|
|
158
|
+
logger.error(
|
|
159
|
+
`[shopify/products] ${err instanceof Error ? err.message : String(err)}`
|
|
160
|
+
);
|
|
161
|
+
sendJsonError(res, 500, message);
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (method === "POST" && pathname === "/api/shopify/products") {
|
|
166
|
+
try {
|
|
167
|
+
const raw = await readBody(req);
|
|
168
|
+
const input = JSON.parse(raw);
|
|
169
|
+
if (!input.title?.trim()) {
|
|
170
|
+
sendJsonError(res, 400, "title is required");
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
const data = await shopifyGql(
|
|
174
|
+
config,
|
|
175
|
+
`mutation CreateProduct($input: ProductInput!) {
|
|
176
|
+
productCreate(input: $input) {
|
|
177
|
+
product {
|
|
178
|
+
id
|
|
179
|
+
title
|
|
180
|
+
status
|
|
181
|
+
productType
|
|
182
|
+
vendor
|
|
183
|
+
totalInventory
|
|
184
|
+
updatedAt
|
|
185
|
+
featuredImage { url }
|
|
186
|
+
}
|
|
187
|
+
userErrors { field message }
|
|
188
|
+
}
|
|
189
|
+
}`,
|
|
190
|
+
{
|
|
191
|
+
input: {
|
|
192
|
+
title: input.title.trim(),
|
|
193
|
+
productType: input.productType?.trim() ?? "",
|
|
194
|
+
vendor: input.vendor?.trim() ?? "",
|
|
195
|
+
status: "DRAFT"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
if (data.productCreate.userErrors.length) {
|
|
200
|
+
sendJsonError(
|
|
201
|
+
res,
|
|
202
|
+
422,
|
|
203
|
+
data.productCreate.userErrors.map((e) => `${e.field.join(".")}: ${e.message}`).join("; ")
|
|
204
|
+
);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
const product = data.productCreate.product;
|
|
208
|
+
if (!product) {
|
|
209
|
+
sendJsonError(res, 500, "Product create returned no product");
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
sendJson(res, 201, {
|
|
213
|
+
id: product.id,
|
|
214
|
+
title: product.title,
|
|
215
|
+
status: product.status,
|
|
216
|
+
productType: product.productType,
|
|
217
|
+
vendor: product.vendor,
|
|
218
|
+
totalInventory: product.totalInventory,
|
|
219
|
+
updatedAt: product.updatedAt,
|
|
220
|
+
imageUrl: product.featuredImage?.url ?? null,
|
|
221
|
+
priceRange: {
|
|
222
|
+
min: input.price ?? "0.00",
|
|
223
|
+
max: input.price ?? "0.00"
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const message = err instanceof Error ? err.message : "Failed to create product";
|
|
228
|
+
logger.error(
|
|
229
|
+
`[shopify/products/create] ${err instanceof Error ? err.message : String(err)}`
|
|
230
|
+
);
|
|
231
|
+
sendJsonError(res, 500, message);
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (method === "GET" && pathname === "/api/shopify/orders") {
|
|
236
|
+
try {
|
|
237
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
238
|
+
const limit = Math.min(
|
|
239
|
+
50,
|
|
240
|
+
Math.max(1, Number(url.searchParams.get("limit") ?? "20"))
|
|
241
|
+
);
|
|
242
|
+
const status = (url.searchParams.get("status") ?? "any").trim().toLowerCase();
|
|
243
|
+
if (!ORDER_STATUS_FILTERS.has(status)) {
|
|
244
|
+
sendJsonError(res, 400, `Unsupported order status filter: ${status}`);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
const queryFilter = status !== "any" ? `financial_status:${status}` : null;
|
|
248
|
+
const data = await shopifyGql(
|
|
249
|
+
config,
|
|
250
|
+
`query ListOrders($first: Int!, $query: String) {
|
|
251
|
+
orders(first: $first, query: $query, sortKey: CREATED_AT, reverse: true) {
|
|
252
|
+
edges {
|
|
253
|
+
node {
|
|
254
|
+
id name email createdAt
|
|
255
|
+
displayFinancialStatus displayFulfillmentStatus
|
|
256
|
+
totalPriceSet { shopMoney { amount currencyCode } }
|
|
257
|
+
lineItems(first: 1) { edges { node { id } } }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
ordersCount { count }
|
|
262
|
+
}`,
|
|
263
|
+
{ first: limit, query: queryFilter }
|
|
264
|
+
);
|
|
265
|
+
const orders = data.orders.edges.map((edge) => ({
|
|
266
|
+
id: edge.node.id,
|
|
267
|
+
name: edge.node.name,
|
|
268
|
+
email: edge.node.email ?? "",
|
|
269
|
+
totalPrice: edge.node.totalPriceSet.shopMoney.amount,
|
|
270
|
+
currencyCode: edge.node.totalPriceSet.shopMoney.currencyCode,
|
|
271
|
+
fulfillmentStatus: edge.node.displayFulfillmentStatus,
|
|
272
|
+
financialStatus: edge.node.displayFinancialStatus,
|
|
273
|
+
createdAt: edge.node.createdAt,
|
|
274
|
+
lineItemCount: edge.node.lineItems.edges.length
|
|
275
|
+
}));
|
|
276
|
+
sendJson(res, 200, { orders, total: data.ordersCount.count });
|
|
277
|
+
} catch (err) {
|
|
278
|
+
const message = err instanceof Error ? err.message : "Failed to fetch orders";
|
|
279
|
+
logger.error(
|
|
280
|
+
`[shopify/orders] ${err instanceof Error ? err.message : String(err)}`
|
|
281
|
+
);
|
|
282
|
+
sendJsonError(res, 500, message);
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (method === "GET" && pathname === "/api/shopify/inventory") {
|
|
287
|
+
try {
|
|
288
|
+
const data = await shopifyGql(
|
|
289
|
+
config,
|
|
290
|
+
`{
|
|
291
|
+
products(first: 50) {
|
|
292
|
+
edges {
|
|
293
|
+
node {
|
|
294
|
+
title
|
|
295
|
+
variants(first: 10) {
|
|
296
|
+
edges {
|
|
297
|
+
node {
|
|
298
|
+
id title sku
|
|
299
|
+
inventoryItem {
|
|
300
|
+
id
|
|
301
|
+
inventoryLevels(first: 10) {
|
|
302
|
+
edges { node { available location { id name } } }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
locations(first: 20) {
|
|
312
|
+
edges { node { name isActive } }
|
|
313
|
+
}
|
|
314
|
+
}`
|
|
315
|
+
);
|
|
316
|
+
const items = [];
|
|
317
|
+
for (const productEdge of data.products.edges) {
|
|
318
|
+
for (const variantEdge of productEdge.node.variants.edges) {
|
|
319
|
+
const variant = variantEdge.node;
|
|
320
|
+
const levels = variant.inventoryItem.inventoryLevels.edges;
|
|
321
|
+
if (levels.length === 0) {
|
|
322
|
+
items.push({
|
|
323
|
+
id: variant.inventoryItem.id,
|
|
324
|
+
sku: variant.sku ?? "",
|
|
325
|
+
productTitle: productEdge.node.title,
|
|
326
|
+
variantTitle: variant.title === "Default Title" ? "" : variant.title,
|
|
327
|
+
locationId: null,
|
|
328
|
+
locationName: "",
|
|
329
|
+
available: 0,
|
|
330
|
+
incoming: 0
|
|
331
|
+
});
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
for (const levelEdge of levels) {
|
|
335
|
+
items.push({
|
|
336
|
+
id: variant.inventoryItem.id,
|
|
337
|
+
sku: variant.sku ?? "",
|
|
338
|
+
productTitle: productEdge.node.title,
|
|
339
|
+
variantTitle: variant.title === "Default Title" ? "" : variant.title,
|
|
340
|
+
locationId: levelEdge.node.location.id,
|
|
341
|
+
locationName: levelEdge.node.location.name,
|
|
342
|
+
available: levelEdge.node.available,
|
|
343
|
+
incoming: 0
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const locations = data.locations.edges.filter((edge) => edge.node.isActive).map((edge) => edge.node.name);
|
|
349
|
+
sendJson(res, 200, { items, locations });
|
|
350
|
+
} catch (err) {
|
|
351
|
+
const message = err instanceof Error ? err.message : "Failed to fetch inventory";
|
|
352
|
+
logger.error(
|
|
353
|
+
`[shopify/inventory] ${err instanceof Error ? err.message : String(err)}`
|
|
354
|
+
);
|
|
355
|
+
sendJsonError(res, 500, message);
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
const adjustMatch = pathname.match(
|
|
360
|
+
/^\/api\/shopify\/inventory\/(.+)\/adjust$/
|
|
361
|
+
);
|
|
362
|
+
if (adjustMatch && method === "POST") {
|
|
363
|
+
try {
|
|
364
|
+
const raw = await readBody(req);
|
|
365
|
+
const body = JSON.parse(raw);
|
|
366
|
+
const delta = Number(body.delta);
|
|
367
|
+
if (!Number.isInteger(delta) || delta === 0) {
|
|
368
|
+
sendJsonError(res, 400, "delta must be a non-zero integer");
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
const inventoryItemId = adjustMatch[1];
|
|
372
|
+
const requestedLocationId = typeof body.locationId === "string" && body.locationId.trim() ? body.locationId.trim() : null;
|
|
373
|
+
const itemData = await shopifyGql(
|
|
374
|
+
config,
|
|
375
|
+
`query GetInventoryItem($id: ID!) {
|
|
376
|
+
inventoryItem(id: $id) {
|
|
377
|
+
id
|
|
378
|
+
inventoryLevels(first: 5) {
|
|
379
|
+
edges { node { id location { id name } } }
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}`,
|
|
383
|
+
{ id: inventoryItemId }
|
|
384
|
+
);
|
|
385
|
+
if (!itemData.inventoryItem) {
|
|
386
|
+
sendJsonError(res, 404, `Inventory item not found: ${inventoryItemId}`);
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
const levels = itemData.inventoryItem.inventoryLevels.edges;
|
|
390
|
+
if (levels.length === 0) {
|
|
391
|
+
sendJsonError(
|
|
392
|
+
res,
|
|
393
|
+
422,
|
|
394
|
+
"No inventory levels found for this item \u2014 item may not be tracked"
|
|
395
|
+
);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
let locationId = requestedLocationId;
|
|
399
|
+
if (locationId) {
|
|
400
|
+
const matchingLevel = levels.find(
|
|
401
|
+
(level) => level.node.location.id === locationId
|
|
402
|
+
);
|
|
403
|
+
if (!matchingLevel) {
|
|
404
|
+
sendJsonError(
|
|
405
|
+
res,
|
|
406
|
+
400,
|
|
407
|
+
`Location ${locationId} is not valid for inventory item ${inventoryItemId}`
|
|
408
|
+
);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
} else if (levels.length === 1) {
|
|
412
|
+
locationId = levels[0].node.location.id;
|
|
413
|
+
} else {
|
|
414
|
+
sendJsonError(
|
|
415
|
+
res,
|
|
416
|
+
400,
|
|
417
|
+
"locationId is required when an inventory item exists in multiple locations"
|
|
418
|
+
);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
const adjustData = await shopifyGql(
|
|
422
|
+
config,
|
|
423
|
+
`mutation AdjustInventory($input: InventoryAdjustQuantitiesInput!) {
|
|
424
|
+
inventoryAdjustQuantities(input: $input) {
|
|
425
|
+
inventoryAdjustmentGroup { reason }
|
|
426
|
+
userErrors { field message }
|
|
427
|
+
}
|
|
428
|
+
}`,
|
|
429
|
+
{
|
|
430
|
+
input: {
|
|
431
|
+
reason: "correction",
|
|
432
|
+
name: "available",
|
|
433
|
+
changes: [
|
|
434
|
+
{
|
|
435
|
+
inventoryItemId,
|
|
436
|
+
locationId,
|
|
437
|
+
delta
|
|
438
|
+
}
|
|
439
|
+
]
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
if (adjustData.inventoryAdjustQuantities.userErrors.length) {
|
|
444
|
+
sendJsonError(
|
|
445
|
+
res,
|
|
446
|
+
422,
|
|
447
|
+
adjustData.inventoryAdjustQuantities.userErrors.map((error) => error.message).join("; ")
|
|
448
|
+
);
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
sendJson(res, 200, { ok: true, locationId });
|
|
452
|
+
} catch (err) {
|
|
453
|
+
const message = err instanceof Error ? err.message : "Failed to adjust inventory";
|
|
454
|
+
logger.error(
|
|
455
|
+
`[shopify/inventory/adjust] ${err instanceof Error ? err.message : String(err)}`
|
|
456
|
+
);
|
|
457
|
+
sendJsonError(res, 500, message);
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
if (method === "GET" && pathname === "/api/shopify/customers") {
|
|
462
|
+
try {
|
|
463
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
464
|
+
const limit = Math.min(
|
|
465
|
+
50,
|
|
466
|
+
Math.max(1, Number(url.searchParams.get("limit") ?? "20"))
|
|
467
|
+
);
|
|
468
|
+
const search = url.searchParams.get("q")?.trim() || null;
|
|
469
|
+
const data = await shopifyGql(
|
|
470
|
+
config,
|
|
471
|
+
`query ListCustomers($first: Int!, $query: String) {
|
|
472
|
+
customers(first: $first, query: $query) {
|
|
473
|
+
edges {
|
|
474
|
+
node {
|
|
475
|
+
id firstName lastName email numberOfOrders
|
|
476
|
+
amountSpent { amount currencyCode }
|
|
477
|
+
createdAt
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
customersCount { count }
|
|
482
|
+
}`,
|
|
483
|
+
{ first: limit, query: search }
|
|
484
|
+
);
|
|
485
|
+
const customers = data.customers.edges.map((edge) => ({
|
|
486
|
+
id: edge.node.id,
|
|
487
|
+
firstName: edge.node.firstName ?? "",
|
|
488
|
+
lastName: edge.node.lastName ?? "",
|
|
489
|
+
email: edge.node.email ?? "",
|
|
490
|
+
ordersCount: Number(edge.node.numberOfOrders ?? 0),
|
|
491
|
+
totalSpent: edge.node.amountSpent.amount,
|
|
492
|
+
currencyCode: edge.node.amountSpent.currencyCode,
|
|
493
|
+
createdAt: edge.node.createdAt
|
|
494
|
+
}));
|
|
495
|
+
sendJson(res, 200, { customers, total: data.customersCount.count });
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const message = err instanceof Error ? err.message : "Failed to fetch customers";
|
|
498
|
+
logger.error(
|
|
499
|
+
`[shopify/customers] ${err instanceof Error ? err.message : String(err)}`
|
|
500
|
+
);
|
|
501
|
+
sendJsonError(res, 500, message);
|
|
502
|
+
}
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
function readBody(req) {
|
|
508
|
+
return new Promise((resolve, reject) => {
|
|
509
|
+
const chunks = [];
|
|
510
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
511
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
512
|
+
req.on("error", reject);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
export {
|
|
516
|
+
handleShopifyRoute
|
|
517
|
+
};
|
|
518
|
+
//# sourceMappingURL=routes.js.map
|