@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.
Files changed (79) hide show
  1. package/dist/CustomersPanel.d.ts +11 -0
  2. package/dist/CustomersPanel.d.ts.map +1 -0
  3. package/dist/CustomersPanel.js +86 -0
  4. package/dist/CustomersPanel.js.map +1 -0
  5. package/dist/InventoryLevelsPanel.d.ts +10 -0
  6. package/dist/InventoryLevelsPanel.d.ts.map +1 -0
  7. package/dist/InventoryLevelsPanel.js +190 -0
  8. package/dist/InventoryLevelsPanel.js.map +1 -0
  9. package/dist/OrdersPanel.d.ts +12 -0
  10. package/dist/OrdersPanel.d.ts.map +1 -0
  11. package/dist/OrdersPanel.js +170 -0
  12. package/dist/OrdersPanel.js.map +1 -0
  13. package/dist/ProductsPanel.d.ts +13 -0
  14. package/dist/ProductsPanel.d.ts.map +1 -0
  15. package/dist/ProductsPanel.js +419 -0
  16. package/dist/ProductsPanel.js.map +1 -0
  17. package/dist/ShopifyAppView.d.ts +3 -0
  18. package/dist/ShopifyAppView.d.ts.map +1 -0
  19. package/dist/ShopifyAppView.helpers.d.ts +11 -0
  20. package/dist/ShopifyAppView.helpers.d.ts.map +1 -0
  21. package/dist/ShopifyAppView.helpers.js +59 -0
  22. package/dist/ShopifyAppView.helpers.js.map +1 -0
  23. package/dist/ShopifyAppView.interact.d.ts +2 -0
  24. package/dist/ShopifyAppView.interact.d.ts.map +1 -0
  25. package/dist/ShopifyAppView.interact.js +93 -0
  26. package/dist/ShopifyAppView.interact.js.map +1 -0
  27. package/dist/ShopifyAppView.js +667 -0
  28. package/dist/ShopifyAppView.js.map +1 -0
  29. package/dist/ShopifyView.d.ts +18 -0
  30. package/dist/ShopifyView.d.ts.map +1 -0
  31. package/dist/ShopifyView.js +143 -0
  32. package/dist/ShopifyView.js.map +1 -0
  33. package/dist/StoreOverviewCard.d.ts +13 -0
  34. package/dist/StoreOverviewCard.d.ts.map +1 -0
  35. package/dist/StoreOverviewCard.js +23 -0
  36. package/dist/StoreOverviewCard.js.map +1 -0
  37. package/dist/components/ShopifySpatialView.d.ts +57 -0
  38. package/dist/components/ShopifySpatialView.d.ts.map +1 -0
  39. package/dist/components/ShopifySpatialView.js +419 -0
  40. package/dist/components/ShopifySpatialView.js.map +1 -0
  41. package/dist/index.d.ts +14 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +20 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/plugin.d.ts +14 -0
  46. package/dist/plugin.d.ts.map +1 -0
  47. package/dist/plugin.js +94 -0
  48. package/dist/plugin.js.map +1 -0
  49. package/dist/register-routes.d.ts +2 -0
  50. package/dist/register-routes.d.ts.map +1 -0
  51. package/dist/register-routes.js +6 -0
  52. package/dist/register-routes.js.map +1 -0
  53. package/dist/register-terminal-view.d.ts +15 -0
  54. package/dist/register-terminal-view.d.ts.map +1 -0
  55. package/dist/register-terminal-view.js +37 -0
  56. package/dist/register-terminal-view.js.map +1 -0
  57. package/dist/register.d.ts +2 -0
  58. package/dist/register.d.ts.map +1 -0
  59. package/dist/register.js +17 -0
  60. package/dist/register.js.map +1 -0
  61. package/dist/routes.d.ts +18 -0
  62. package/dist/routes.d.ts.map +1 -0
  63. package/dist/routes.js +518 -0
  64. package/dist/routes.js.map +1 -0
  65. package/dist/shopify-app.d.ts +11 -0
  66. package/dist/shopify-app.d.ts.map +1 -0
  67. package/dist/shopify-app.js +16 -0
  68. package/dist/shopify-app.js.map +1 -0
  69. package/dist/shopify-view-bundle.d.ts +3 -0
  70. package/dist/shopify-view-bundle.d.ts.map +1 -0
  71. package/dist/shopify-view-bundle.js +7 -0
  72. package/dist/shopify-view-bundle.js.map +1 -0
  73. package/dist/useShopifyDashboard.d.ts +118 -0
  74. package/dist/useShopifyDashboard.d.ts.map +1 -0
  75. package/dist/useShopifyDashboard.js +212 -0
  76. package/dist/useShopifyDashboard.js.map +1 -0
  77. package/dist/views/bundle.js +948 -0
  78. package/dist/views/bundle.js.map +1 -0
  79. package/package.json +5 -5
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=register-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-routes.d.ts","sourceRoot":"","sources":["../src/register-routes.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ import { registerAppRoutePluginLoader } from "@elizaos/core";
2
+ registerAppRoutePluginLoader("@elizaos/plugin-shopify-ui", async () => {
3
+ const { shopifyPlugin } = await import("./plugin.js");
4
+ return shopifyPlugin;
5
+ });
6
+ //# sourceMappingURL=register-routes.js.map
@@ -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,2 @@
1
+ import "./shopify-app";
2
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AACA,OAAO,eAAe,CAAC"}
@@ -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":[]}
@@ -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