@curless/shopify-adapter 0.4.0 → 0.6.0
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/README.md +13 -4
- package/dist/checkout.d.ts +21 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +29 -0
- package/dist/checkout.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/order-lookup.d.ts +33 -0
- package/dist/order-lookup.d.ts.map +1 -0
- package/dist/order-lookup.js +83 -0
- package/dist/order-lookup.js.map +1 -0
- package/dist/products.d.ts +54 -0
- package/dist/products.d.ts.map +1 -0
- package/dist/products.js +213 -0
- package/dist/products.js.map +1 -0
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -13
package/README.md
CHANGED
|
@@ -77,10 +77,19 @@ here would let you record a reversal that doesn't match the money that actually
|
|
|
77
77
|
|
|
78
78
|
## Notes
|
|
79
79
|
|
|
80
|
-
- **Auth
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
- **Auth — pick by whose store it is.**
|
|
81
|
+
- **A store you do NOT own** (a real merchant): they create a custom app in
|
|
82
|
+
their own admin and give you its Admin API token. Set `SHOPIFY_ADMIN_TOKEN`
|
|
83
|
+
+ `SHOPIFY_SHOP_DOMAIN`. **This is the only option** — the client-credentials
|
|
84
|
+
grant requires the app and shop to sit in the same Shopify organization, so
|
|
85
|
+
it cannot reach a third party's store at all.
|
|
86
|
+
- **Your own store**: Shopify's 2026 **client-credentials** grant. A Dev
|
|
87
|
+
Dashboard app no longer shows a token in the UI, so you exchange
|
|
88
|
+
`SHOPIFY_CLIENT_ID` + `SHOPIFY_CLIENT_SECRET` for a short-lived Admin token
|
|
89
|
+
programmatically.
|
|
90
|
+
|
|
91
|
+
A token, when present, wins — no exchange, one fewer call and one fewer way to
|
|
92
|
+
fail. The app needs `read_products` (catalog) and
|
|
84
93
|
`write_orders` (writeback) — required scopes go in `shopify.app.toml`, then
|
|
85
94
|
`deploy` + **reinstall**. Secrets come from the environment; never hardcode them.
|
|
86
95
|
- **Amounts are currency-aware.** Prices convert to integer minor units using the
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ShopifyConfig } from './types.js';
|
|
2
|
+
export type CartLine = {
|
|
3
|
+
/** Shopify variant NUMERIC id (CatalogItem.variantId), not the gid://. */
|
|
4
|
+
variantId: string;
|
|
5
|
+
quantity: number;
|
|
6
|
+
};
|
|
7
|
+
export declare const cartPermalink: (config: Pick<ShopifyConfig, "shopDomain">, lines: CartLine[], opts?: {
|
|
8
|
+
discountCode?: string;
|
|
9
|
+
note?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Cart attributes — `attributes[key]=value`, which Shopify records on the
|
|
12
|
+
* order and shows the merchant under its additional details.
|
|
13
|
+
*
|
|
14
|
+
* This is the ONLY place an order can carry where it came from. The order's
|
|
15
|
+
* `Channel` says "Online Store" because that is literally the channel that
|
|
16
|
+
* created it, and changing that would mean being a registered sales-channel
|
|
17
|
+
* app rather than a link.
|
|
18
|
+
*/
|
|
19
|
+
attributes?: Record<string, string>;
|
|
20
|
+
}) => string;
|
|
21
|
+
//# sourceMappingURL=checkout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG;IACrB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAQF,eAAO,MAAM,aAAa,GACxB,QAAQ,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,EACzC,OAAO,QAAQ,EAAE,EACjB,OAAM;IACJ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,KACL,MAoBF,CAAC"}
|
package/dist/checkout.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// `https://shop.myshopify.com/cart/{variantId}:{qty},{variantId}:{qty}?…`
|
|
2
|
+
//
|
|
3
|
+
// The permalink is a documented Shopify entry point, and it is the reason this
|
|
4
|
+
// path needs no app permissions at all — anyone holding a variant id can build
|
|
5
|
+
// one. That also means it must never carry anything private: no buyer email, no
|
|
6
|
+
// internal ids, nothing but what the buyer is buying.
|
|
7
|
+
export const cartPermalink = (config, lines, opts = {}) => {
|
|
8
|
+
const usable = lines.filter((l) => l.variantId && Number.isInteger(l.quantity) && l.quantity > 0);
|
|
9
|
+
if (usable.length === 0) {
|
|
10
|
+
throw new Error('cartPermalink: need at least one line with a variantId and a positive quantity');
|
|
11
|
+
}
|
|
12
|
+
// A gid:// slipped in here would 404 the buyer at the checkout with no hint
|
|
13
|
+
// why, so take the numeric tail rather than trusting the caller's shape.
|
|
14
|
+
const path = usable
|
|
15
|
+
.map((l) => `${String(l.variantId).replace(/^.*\//, '')}:${l.quantity}`)
|
|
16
|
+
.join(',');
|
|
17
|
+
const qs = new URLSearchParams();
|
|
18
|
+
if (opts.discountCode)
|
|
19
|
+
qs.set('discount', opts.discountCode);
|
|
20
|
+
if (opts.note)
|
|
21
|
+
qs.set('note', opts.note);
|
|
22
|
+
for (const [k, v] of Object.entries(opts.attributes ?? {})) {
|
|
23
|
+
if (k && v)
|
|
24
|
+
qs.set(`attributes[${k}]`, v);
|
|
25
|
+
}
|
|
26
|
+
const query = qs.toString();
|
|
27
|
+
return `https://${config.shopDomain}/cart/${path}${query ? `?${query}` : ''}`;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=checkout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAeA,0EAA0E;AAC1E,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,sDAAsD;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,MAAyC,EACzC,KAAiB,EACjB,OAaI,EAAE,EACE,EAAE;IACV,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;IAClG,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,gFAAgF,CACjF,CAAC;IACJ,CAAC;IACD,4EAA4E;IAC5E,yEAAyE;IACzE,MAAM,IAAI,GAAG,MAAM;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;SACvE,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;IACjC,IAAI,IAAI,CAAC,YAAY;QAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7D,IAAI,IAAI,CAAC,IAAI;QAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACzC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC,IAAI,CAAC;YAAE,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAC5B,OAAO,WAAW,MAAM,CAAC,UAAU,SAAS,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAChF,CAAC,CAAC"}
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAKhD,eAAO,MAAM,oBAAoB,GAC/B,MAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,KACpD,aAAa,GAAG,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAKhD,eAAO,MAAM,oBAAoB,GAC/B,MAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,KACpD,aAAa,GAAG,IAYlB,CAAC;AAKF,eAAO,MAAM,aAAa,GACxB,QAAQ,aAAa,EACrB,YAAW,OAAO,KAAa,KAC9B,OAAO,CAAC,MAAM,CA0BhB,CAAC;AASF,eAAO,MAAM,cAAc,GAAU,CAAC,EACpC,QAAQ,aAAa,EACrB,OAAO,MAAM,EACb,OAAO,MAAM,EACb,WAAW,OAAO,EAClB,YAAW,OAAO,KAAa,KAC9B,OAAO,CAAC,CAAC,CAgBX,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -3,16 +3,31 @@
|
|
|
3
3
|
// fall back rather than fail.
|
|
4
4
|
export const shopifyConfigFromEnv = (env = process.env) => {
|
|
5
5
|
const shopDomain = env.SHOPIFY_SHOP_DOMAIN;
|
|
6
|
+
if (!shopDomain)
|
|
7
|
+
return null;
|
|
8
|
+
const apiVersion = env.SHOPIFY_API_VERSION ?? '2025-01';
|
|
9
|
+
// A direct Admin API token first — it is the ONLY thing that works for a store
|
|
10
|
+
// you do not own, and it skips the exchange entirely.
|
|
11
|
+
const adminToken = env.SHOPIFY_ADMIN_TOKEN;
|
|
12
|
+
if (adminToken)
|
|
13
|
+
return { shopDomain, adminToken, apiVersion };
|
|
6
14
|
const clientId = env.SHOPIFY_CLIENT_ID;
|
|
7
15
|
const clientSecret = env.SHOPIFY_CLIENT_SECRET;
|
|
8
|
-
if (!
|
|
16
|
+
if (!clientId || !clientSecret)
|
|
9
17
|
return null;
|
|
10
|
-
return { shopDomain, clientId, clientSecret, apiVersion
|
|
18
|
+
return { shopDomain, clientId, clientSecret, apiVersion };
|
|
11
19
|
};
|
|
12
20
|
// Exchange client id + secret for a short-lived Admin API token
|
|
13
21
|
// (grant_type=client_credentials). Form-encoded, NOT JSON. Only works when the
|
|
14
22
|
// app and store are in the same Shopify org, which a self-installed dev app is.
|
|
15
23
|
export const exchangeToken = async (config, fetchImpl = fetch) => {
|
|
24
|
+
// Already holding a token — nothing to exchange. This is the third-party
|
|
25
|
+
// merchant path, where the client-credentials grant would fail anyway.
|
|
26
|
+
if (config.adminToken)
|
|
27
|
+
return config.adminToken;
|
|
28
|
+
if (!config.clientId || !config.clientSecret) {
|
|
29
|
+
throw new Error("Shopify auth is not configured: set SHOPIFY_ADMIN_TOKEN (a custom app in the store's own admin), or SHOPIFY_CLIENT_ID + SHOPIFY_CLIENT_SECRET for a same-org app");
|
|
30
|
+
}
|
|
16
31
|
const res = await fetchImpl(`https://${config.shopDomain}/admin/oauth/access_token`, {
|
|
17
32
|
method: 'POST',
|
|
18
33
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AASA,kFAAkF;AAClF,gFAAgF;AAChF,8BAA8B;AAC9B,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,MAA0C,OAAO,CAAC,GAAG,EAC/B,EAAE;IACxB,MAAM,UAAU,GAAG,GAAG,CAAC,mBAAmB,CAAC;IAC3C,MAAM,QAAQ,GAAG,GAAG,CAAC,iBAAiB,CAAC;IACvC,MAAM,YAAY,GAAG,GAAG,CAAC,qBAAqB,CAAC;IAC/C,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export { shopifyConfigFromEnv, exchangeToken, shopifyGraphql } from './client.js';
|
|
2
2
|
export { fetchShopifyCatalog, productsToItems } from './catalog.js';
|
|
3
3
|
export { writebackShopifyOrder, refundShopifyOrder } from './orders.js';
|
|
4
|
+
export { fetchShopifyProducts, fetchProductVariants, resolveVariant, snapChoices, } from './products.js';
|
|
5
|
+
export { lookupShopifyOrder } from './order-lookup.js';
|
|
6
|
+
export { cartPermalink } from './checkout.js';
|
|
4
7
|
export { decimalsFor, toMinor, toMajorString } from './money.js';
|
|
8
|
+
export type { ShopifyProduct, ProductVariant, ProductOption } from './products.js';
|
|
9
|
+
export type { OrderLookupResult, OrderLookupLine } from './order-lookup.js';
|
|
10
|
+
export type { CartLine } from './checkout.js';
|
|
5
11
|
export type { CatalogItem, ShopifyConfig, OrderWriteback, WritebackResult, OrderRefund, RefundResult, } from './types.js';
|
|
6
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACjE,YAAY,EACV,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACjE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC5E,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EACV,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,8 @@
|
|
|
15
15
|
export { shopifyConfigFromEnv, exchangeToken, shopifyGraphql } from './client.js';
|
|
16
16
|
export { fetchShopifyCatalog, productsToItems } from './catalog.js';
|
|
17
17
|
export { writebackShopifyOrder, refundShopifyOrder } from './orders.js';
|
|
18
|
+
export { fetchShopifyProducts, fetchProductVariants, resolveVariant, snapChoices, } from './products.js';
|
|
19
|
+
export { lookupShopifyOrder } from './order-lookup.js';
|
|
20
|
+
export { cartPermalink } from './checkout.js';
|
|
18
21
|
export { decimalsFor, toMinor, toMajorString } from './money.js';
|
|
19
22
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,8DAA8D;AAC9D,EAAE;AACF,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,4EAA4E;AAC5E,gDAAgD;AAChD,4FAA4F;AAC5F,wEAAwE;AACxE,8EAA8E;AAC9E,qEAAqE;AACrE,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,8DAA8D;AAC9D,EAAE;AACF,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,4EAA4E;AAC5E,gDAAgD;AAChD,4FAA4F;AAC5F,wEAAwE;AACxE,8EAA8E;AAC9E,qEAAqE;AACrE,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ShopifyConfig } from './types.js';
|
|
2
|
+
export type OrderLookupLine = {
|
|
3
|
+
title: string;
|
|
4
|
+
variantTitle?: string;
|
|
5
|
+
quantity: number;
|
|
6
|
+
priceMinor: number;
|
|
7
|
+
imageUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
export type OrderLookupResult = {
|
|
10
|
+
found: true;
|
|
11
|
+
/** The merchant-facing number, e.g. "#1052". */
|
|
12
|
+
name: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
currency: string;
|
|
15
|
+
totalMinor: number;
|
|
16
|
+
/** Shopify's own vocabulary: PAID / REFUNDED / PARTIALLY_REFUNDED / … */
|
|
17
|
+
financialStatus: string;
|
|
18
|
+
/** UNFULFILLED / FULFILLED / …, or undefined when Shopify reports none. */
|
|
19
|
+
fulfillmentStatus?: string;
|
|
20
|
+
lines: OrderLookupLine[];
|
|
21
|
+
/** Carrier tracking URLs, when the merchant has shipped and entered them. */
|
|
22
|
+
trackingUrls: string[];
|
|
23
|
+
/** Shopify's own status page for this order — the buyer's canonical view. */
|
|
24
|
+
statusUrl?: string;
|
|
25
|
+
} | {
|
|
26
|
+
found: false;
|
|
27
|
+
reason: 'not-found';
|
|
28
|
+
};
|
|
29
|
+
export declare const lookupShopifyOrder: (config: ShopifyConfig, input: {
|
|
30
|
+
orderNumber: string;
|
|
31
|
+
email: string;
|
|
32
|
+
}, fetchImpl?: typeof fetch) => Promise<OrderLookupResult>;
|
|
33
|
+
//# sourceMappingURL=order-lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-lookup.d.ts","sourceRoot":"","sources":["../src/order-lookup.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,eAAe,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC;AAiD1C,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,aAAa,EACrB,OAAO;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC7C,YAAW,OAAO,KAAa,KAC9B,OAAO,CAAC,iBAAiB,CAoD3B,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// "Where is my order?" — for a storefront where the buyer has no account.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ THE WHOLE DESIGN IS THE TWO-FACTOR CHECK. Checkout happens on Shopify's own
|
|
4
|
+
// page, so nothing here knows who is asking: an agent typing an order number is
|
|
5
|
+
// not proof of anything. Requiring the order number AND the email it was placed
|
|
6
|
+
// with is the same bar Shopify's own guest order lookup sets — one alone is
|
|
7
|
+
// guessable (order numbers are sequential), both together are not.
|
|
8
|
+
//
|
|
9
|
+
// So: never return an order because the number matched. The email must match
|
|
10
|
+
// too, and a mismatch is reported exactly like a miss — telling the caller
|
|
11
|
+
// "right order, wrong email" would confirm that the order exists, which is half
|
|
12
|
+
// the answer they were not entitled to.
|
|
13
|
+
import { exchangeToken, shopifyGraphql } from './client.js';
|
|
14
|
+
import { toMinor } from './money.js';
|
|
15
|
+
const QUERY = `
|
|
16
|
+
query FindOrder($q: String!) {
|
|
17
|
+
orders(first: 5, query: $q) {
|
|
18
|
+
nodes {
|
|
19
|
+
name createdAt email
|
|
20
|
+
displayFinancialStatus displayFulfillmentStatus statusPageUrl
|
|
21
|
+
currentTotalPriceSet { shopMoney { amount currencyCode } }
|
|
22
|
+
lineItems(first: 20) {
|
|
23
|
+
nodes {
|
|
24
|
+
title variantTitle quantity
|
|
25
|
+
originalUnitPriceSet { shopMoney { amount } }
|
|
26
|
+
image { url }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
fulfillments(first: 10) { trackingInfo { url } }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
// Shopify's search syntax is space-separated terms; a stray quote or colon in
|
|
35
|
+
// caller input would change the query's meaning rather than fail it. Keep only
|
|
36
|
+
// what an order number can legitimately contain.
|
|
37
|
+
const safeOrderNumber = (raw) => raw.trim().replace(/[^0-9A-Za-z#_-]/g, '');
|
|
38
|
+
export const lookupShopifyOrder = async (config, input, fetchImpl = fetch) => {
|
|
39
|
+
const number = safeOrderNumber(input.orderNumber);
|
|
40
|
+
const email = input.email.trim().toLowerCase();
|
|
41
|
+
// Both are required. An empty one must not degrade into "match on the other".
|
|
42
|
+
if (!number || !email.includes('@'))
|
|
43
|
+
return { found: false, reason: 'not-found' };
|
|
44
|
+
const token = await exchangeToken(config, fetchImpl);
|
|
45
|
+
const withHash = number.startsWith('#') ? number : `#${number}`;
|
|
46
|
+
const data = await shopifyGraphql(config, token, QUERY, { q: `name:${JSON.stringify(withHash)}` }, fetchImpl);
|
|
47
|
+
const order = data.orders.nodes.find((o) => o.name === withHash && (o.email ?? '').trim().toLowerCase() === email);
|
|
48
|
+
// Deliberately the same answer as "no such order": see the header.
|
|
49
|
+
if (!order)
|
|
50
|
+
return { found: false, reason: 'not-found' };
|
|
51
|
+
const currency = order.currentTotalPriceSet.shopMoney.currencyCode;
|
|
52
|
+
const minor = (amount) => {
|
|
53
|
+
try {
|
|
54
|
+
return toMinor(amount, currency);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
found: true,
|
|
62
|
+
name: order.name,
|
|
63
|
+
createdAt: order.createdAt,
|
|
64
|
+
currency,
|
|
65
|
+
totalMinor: minor(order.currentTotalPriceSet.shopMoney.amount),
|
|
66
|
+
financialStatus: order.displayFinancialStatus ?? 'UNKNOWN',
|
|
67
|
+
...(order.displayFulfillmentStatus
|
|
68
|
+
? { fulfillmentStatus: order.displayFulfillmentStatus }
|
|
69
|
+
: {}),
|
|
70
|
+
lines: order.lineItems.nodes.map((l) => ({
|
|
71
|
+
title: l.title,
|
|
72
|
+
...(l.variantTitle ? { variantTitle: l.variantTitle } : {}),
|
|
73
|
+
quantity: l.quantity,
|
|
74
|
+
priceMinor: minor(l.originalUnitPriceSet.shopMoney.amount),
|
|
75
|
+
...(l.image?.url ? { imageUrl: l.image.url } : {}),
|
|
76
|
+
})),
|
|
77
|
+
trackingUrls: order.fulfillments
|
|
78
|
+
.flatMap((f) => f.trackingInfo.map((t) => t.url))
|
|
79
|
+
.filter((u) => Boolean(u)),
|
|
80
|
+
...(order.statusPageUrl ? { statusUrl: order.statusPageUrl } : {}),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=order-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-lookup.js","sourceRoot":"","sources":["../src/order-lookup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ShopifyConfig } from './types.js';
|
|
2
|
+
export type ProductOption = {
|
|
3
|
+
/** The merchant's own label — "Color", "度数", "Size". Never normalized. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** In the merchant's own order, which is usually meaningful (-0.00 → -5.00). */
|
|
6
|
+
values: string[];
|
|
7
|
+
};
|
|
8
|
+
export type ProductVariant = {
|
|
9
|
+
/** Numeric variant id — what a cart permalink takes. */
|
|
10
|
+
variantId: string;
|
|
11
|
+
sku: string | null;
|
|
12
|
+
/** Shopify's own joined title, e.g. "Teal / -2.00". */
|
|
13
|
+
title: string;
|
|
14
|
+
priceMinor: number;
|
|
15
|
+
/** Struck-through price when the merchant set one, else undefined. */
|
|
16
|
+
compareAtMinor?: number;
|
|
17
|
+
available: boolean;
|
|
18
|
+
/** Which option values select this variant — the picker matches on these. */
|
|
19
|
+
options: {
|
|
20
|
+
name: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}[];
|
|
23
|
+
imageUrl?: string;
|
|
24
|
+
};
|
|
25
|
+
export type ShopifyProduct = {
|
|
26
|
+
/** Shopify numeric product id. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** URL slug — the storefront link a buyer can be sent to. */
|
|
29
|
+
handle: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
productType: string;
|
|
33
|
+
tags: string[];
|
|
34
|
+
currency: string;
|
|
35
|
+
imageUrl?: string;
|
|
36
|
+
images: string[];
|
|
37
|
+
options: ProductOption[];
|
|
38
|
+
/**
|
|
39
|
+
* EMPTY from the bulk read. A pair of lenses is Left × Right, 676 variants,
|
|
40
|
+
* and 382 of those products is a quarter-million objects held in memory to
|
|
41
|
+
* answer "what colours do you have" — while still not being complete enough to
|
|
42
|
+
* resolve a purchase. Load them per product, when someone is buying one.
|
|
43
|
+
*/
|
|
44
|
+
variants: ProductVariant[];
|
|
45
|
+
/** False until `fetchProductVariants` has filled them. Guards the resolver. */
|
|
46
|
+
variantsLoaded: boolean;
|
|
47
|
+
/** Cheapest available variant, for a list that shows one price per product. */
|
|
48
|
+
fromPriceMinor?: number;
|
|
49
|
+
};
|
|
50
|
+
export declare const fetchShopifyProducts: (config: ShopifyConfig, fetchImpl?: typeof fetch, warn?: (msg: string) => void) => Promise<ShopifyProduct[]>;
|
|
51
|
+
export declare const resolveVariant: (variants: ProductVariant[], choices: Record<string, string>) => ProductVariant | null;
|
|
52
|
+
export declare const snapChoices: (options: ProductOption[], choices: Record<string, string>) => Record<string, string>;
|
|
53
|
+
export declare const fetchProductVariants: (config: ShopifyConfig, handle: string, fetchImpl?: typeof fetch, warn?: (msg: string) => void) => Promise<ProductVariant[]>;
|
|
54
|
+
//# sourceMappingURL=products.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"products.d.ts","sourceRoot":"","sources":["../src/products.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAC;IACxB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AA2DF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,aAAa,EACrB,YAAW,OAAO,KAAa,EAC/B,OAAM,CAAC,GAAG,EAAE,MAAM,KAAK,IAA4C,KAClE,OAAO,CAAC,cAAc,EAAE,CAmD1B,CAAC;AAiCF,eAAO,MAAM,cAAc,GACzB,UAAU,cAAc,EAAE,EAC1B,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,cAAc,GAAG,IASnB,CAAC;AAeF,eAAO,MAAM,WAAW,GACtB,SAAS,aAAa,EAAE,EACxB,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CASvB,CAAC;AAyBF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,YAAW,OAAO,KAAa,EAC/B,OAAM,CAAC,GAAG,EAAE,MAAM,KAAK,IAA4C,KAClE,OAAO,CAAC,cAAc,EAAE,CAuD1B,CAAC"}
|
package/dist/products.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Shopify store → PRODUCTS with their option matrix intact.
|
|
2
|
+
//
|
|
3
|
+
// `fetchShopifyCatalog` flattens every variant into a standalone CatalogItem,
|
|
4
|
+
// which is right for a gateway quoting one sku. It is wrong for a storefront:
|
|
5
|
+
// a buyer picks a product, then picks its options, and the two choices together
|
|
6
|
+
// name the variant. Contact lenses are the clearest case — one product is
|
|
7
|
+
// Colour × Prescription, and "Jade Flare" alone cannot be bought.
|
|
8
|
+
//
|
|
9
|
+
// So this returns the shape a picker needs: the option names in the merchant's
|
|
10
|
+
// own words, their values in the merchant's own order, and every variant with
|
|
11
|
+
// the option values that select it.
|
|
12
|
+
import { exchangeToken, shopifyGraphql } from './client.js';
|
|
13
|
+
import { toMinor } from './money.js';
|
|
14
|
+
const PAGE = 50;
|
|
15
|
+
// Shopify's own ceiling for a connection page. A pair of contact lenses is
|
|
16
|
+
// Left × Right — 26 × 26 = 676 variants — so even the maximum does not fetch one
|
|
17
|
+
// product whole, which is exactly why the bulk read no longer tries.
|
|
18
|
+
const VARIANT_PAGE = 250;
|
|
19
|
+
const QUERY = `
|
|
20
|
+
query Products($first: Int!, $after: String) {
|
|
21
|
+
shop { currencyCode }
|
|
22
|
+
products(first: $first, after: $after, query: "status:active") {
|
|
23
|
+
pageInfo { hasNextPage endCursor }
|
|
24
|
+
nodes {
|
|
25
|
+
id handle title description productType tags status
|
|
26
|
+
featuredImage { url }
|
|
27
|
+
media(first: 8) { nodes { preview { image { url } } } }
|
|
28
|
+
options { name optionValues { name } }
|
|
29
|
+
priceRangeV2 { minVariantPrice { amount currencyCode } }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
// `gid://shopify/ProductVariant/123` → `123`. Cart permalinks and the Admin URL
|
|
35
|
+
// both want the bare number, and a gid slipped into either fails in a way that
|
|
36
|
+
// says nothing about why.
|
|
37
|
+
const numericId = (gid) => gid.replace(/^.*\//, '');
|
|
38
|
+
export const fetchShopifyProducts = async (config, fetchImpl = fetch, warn = (m) => console.warn(`[shopify] ${m}`)) => {
|
|
39
|
+
const token = await exchangeToken(config, fetchImpl);
|
|
40
|
+
const out = [];
|
|
41
|
+
let cursor = null;
|
|
42
|
+
let currency = 'USD';
|
|
43
|
+
do {
|
|
44
|
+
const page = await shopifyGraphql(config, token, QUERY, { first: PAGE, after: cursor }, fetchImpl);
|
|
45
|
+
currency = page.shop?.currencyCode ?? currency;
|
|
46
|
+
for (const p of page.products.nodes) {
|
|
47
|
+
let fromPriceMinor;
|
|
48
|
+
const from = p.priceRangeV2?.minVariantPrice;
|
|
49
|
+
if (from) {
|
|
50
|
+
try {
|
|
51
|
+
fromPriceMinor = toMinor(from.amount, from.currencyCode || currency);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
warn(`"${p.title}" has an unparseable price ${JSON.stringify(from.amount)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const images = p.media.nodes
|
|
58
|
+
.map((n) => n.preview?.image?.url)
|
|
59
|
+
.filter((u) => Boolean(u));
|
|
60
|
+
out.push({
|
|
61
|
+
id: numericId(p.id),
|
|
62
|
+
handle: p.handle,
|
|
63
|
+
title: p.title,
|
|
64
|
+
description: p.description ?? '',
|
|
65
|
+
productType: p.productType ?? '',
|
|
66
|
+
tags: p.tags ?? [],
|
|
67
|
+
currency,
|
|
68
|
+
...(p.featuredImage?.url ? { imageUrl: p.featuredImage.url } : {}),
|
|
69
|
+
images,
|
|
70
|
+
options: p.options.map((o) => ({
|
|
71
|
+
name: o.name,
|
|
72
|
+
values: o.optionValues.map((v) => v.name),
|
|
73
|
+
})),
|
|
74
|
+
// EMPTY by design — see the type. Fetch them for the one product a buyer
|
|
75
|
+
// is actually looking at.
|
|
76
|
+
variants: [],
|
|
77
|
+
variantsLoaded: false,
|
|
78
|
+
...(fromPriceMinor !== undefined ? { fromPriceMinor } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
cursor = page.products.pageInfo.hasNextPage ? page.products.pageInfo.endCursor : null;
|
|
82
|
+
} while (cursor);
|
|
83
|
+
return out;
|
|
84
|
+
};
|
|
85
|
+
// Resolve a buyer's choices to ONE variant. Takes the VARIANTS, not the product:
|
|
86
|
+
// a product from the bulk read carries none, and passing it here would resolve
|
|
87
|
+
// to "nothing matches" — indistinguishable from a prescription the store does
|
|
88
|
+
// not stock, and the caller would ask the buyer a question they already answered.
|
|
89
|
+
//
|
|
90
|
+
// Option names and values are matched case- and space-insensitively because the
|
|
91
|
+
// buyer is typing, not clicking: they say "left" for "Left" and "-2.0" for
|
|
92
|
+
// "-2.00".
|
|
93
|
+
//
|
|
94
|
+
// Returns null when the choices do not name exactly one variant — the caller
|
|
95
|
+
// must then ask rather than pick, because guessing here means shipping someone
|
|
96
|
+
// the wrong prescription.
|
|
97
|
+
const norm = (s) => s.trim().toLowerCase().replace(/\s+/g, '');
|
|
98
|
+
// A numeric choice compares numerically: "-2.0", "-2.00" and "−2.00" (with a
|
|
99
|
+
// unicode minus, which is what a phone keyboard produces) are one value. The
|
|
100
|
+
// empty string is NOT a number here — Number('') is 0, and a blank answer that
|
|
101
|
+
// silently meant "0.00 dioptres" would be a plano lens nobody asked for.
|
|
102
|
+
const asNumber = (s) => {
|
|
103
|
+
const t = s.trim().replace(/[−–—]/g, '-');
|
|
104
|
+
if (t === '')
|
|
105
|
+
return null;
|
|
106
|
+
const n = Number(t);
|
|
107
|
+
return Number.isFinite(n) ? n : null;
|
|
108
|
+
};
|
|
109
|
+
/** Whether two option names — or two option values — mean the same thing. */
|
|
110
|
+
const same = (a, b) => {
|
|
111
|
+
const [na, nb] = [asNumber(a), asNumber(b)];
|
|
112
|
+
return na !== null && nb !== null ? na === nb : norm(a) === norm(b);
|
|
113
|
+
};
|
|
114
|
+
export const resolveVariant = (variants, choices) => {
|
|
115
|
+
const wanted = Object.entries(choices).filter(([, v]) => v !== undefined && v !== '');
|
|
116
|
+
if (wanted.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const matches = variants.filter((v) => wanted.every(([name, value]) => v.options.some((o) => same(o.name, name) && same(o.value, value))));
|
|
119
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
120
|
+
};
|
|
121
|
+
// The buyer's choices, rewritten in the STORE's own words — `{left: '-2.0'}`
|
|
122
|
+
// against a `Left: -2.00` option becomes `{Left: '-2.00'}`.
|
|
123
|
+
//
|
|
124
|
+
// This exists because those choices get handed onwards. A picker that shows the
|
|
125
|
+
// buyer what was understood so far has to compare them against the option values
|
|
126
|
+
// it draws, and comparing `-2.0` to `-2.00` as strings highlights nothing while
|
|
127
|
+
// still counting as answered — so the buyer sees an unanswered question with the
|
|
128
|
+
// Buy button live, presses it, and gets the same screen back. Snapping here, in
|
|
129
|
+
// the one place that already knows how these compare, keeps that logic from
|
|
130
|
+
// being reimplemented (differently) further out.
|
|
131
|
+
//
|
|
132
|
+
// Anything that does not name exactly one value is DROPPED, not guessed: an
|
|
133
|
+
// unrecognised prescription has to come back as a question.
|
|
134
|
+
export const snapChoices = (options, choices) => {
|
|
135
|
+
const out = {};
|
|
136
|
+
for (const [name, value] of Object.entries(choices)) {
|
|
137
|
+
if (value === undefined || value === '')
|
|
138
|
+
continue;
|
|
139
|
+
const opt = options.find((o) => same(o.name, name));
|
|
140
|
+
const hits = opt?.values.filter((v) => same(v, value)) ?? [];
|
|
141
|
+
if (opt && hits.length === 1 && hits[0] !== undefined)
|
|
142
|
+
out[opt.name] = hits[0];
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
};
|
|
146
|
+
const VARIANTS_QUERY = `
|
|
147
|
+
query ProductVariants($handle: String!, $first: Int!, $after: String) {
|
|
148
|
+
shop { currencyCode }
|
|
149
|
+
productByHandle(handle: $handle) {
|
|
150
|
+
variants(first: $first, after: $after) {
|
|
151
|
+
pageInfo { hasNextPage endCursor }
|
|
152
|
+
nodes {
|
|
153
|
+
id sku title price compareAtPrice availableForSale
|
|
154
|
+
selectedOptions { name value }
|
|
155
|
+
image { url }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
// Every variant of ONE product, paginated to completion.
|
|
162
|
+
//
|
|
163
|
+
// Completeness is the point. The bulk read used to take the first 100 of 676 and
|
|
164
|
+
// warn about the rest — the warning was accurate and the behaviour was still
|
|
165
|
+
// wrong, because a buyer asking for -6.50 in the right eye was told it does not
|
|
166
|
+
// exist when it merely sat past a page boundary. Most of the store was
|
|
167
|
+
// unbuyable and every refusal looked like a legitimate "we don't stock that".
|
|
168
|
+
export const fetchProductVariants = async (config, handle, fetchImpl = fetch, warn = (m) => console.warn(`[shopify] ${m}`)) => {
|
|
169
|
+
const token = await exchangeToken(config, fetchImpl);
|
|
170
|
+
const out = [];
|
|
171
|
+
let cursor = null;
|
|
172
|
+
let currency = 'USD';
|
|
173
|
+
do {
|
|
174
|
+
const page = await shopifyGraphql(config, token, VARIANTS_QUERY, { handle, first: VARIANT_PAGE, after: cursor }, fetchImpl);
|
|
175
|
+
currency = page.shop?.currencyCode ?? currency;
|
|
176
|
+
const conn = page.productByHandle?.variants;
|
|
177
|
+
if (!conn)
|
|
178
|
+
return out;
|
|
179
|
+
for (const v of conn.nodes) {
|
|
180
|
+
let priceMinor;
|
|
181
|
+
try {
|
|
182
|
+
priceMinor = toMinor(v.price, currency);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
warn(`skipping ${handle} / ${v.title} — unparseable price ${JSON.stringify(v.price)}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let compareAtMinor;
|
|
189
|
+
if (v.compareAtPrice) {
|
|
190
|
+
try {
|
|
191
|
+
compareAtMinor = toMinor(v.compareAtPrice, currency);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Cosmetic only — drop the strike-through, keep the variant buyable.
|
|
195
|
+
compareAtMinor = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
out.push({
|
|
199
|
+
variantId: numericId(v.id),
|
|
200
|
+
sku: v.sku,
|
|
201
|
+
title: v.title,
|
|
202
|
+
priceMinor,
|
|
203
|
+
...(compareAtMinor !== undefined ? { compareAtMinor } : {}),
|
|
204
|
+
available: v.availableForSale,
|
|
205
|
+
options: v.selectedOptions ?? [],
|
|
206
|
+
...(v.image?.url ? { imageUrl: v.image.url } : {}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
|
|
210
|
+
} while (cursor);
|
|
211
|
+
return out;
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=products.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"products.js","sourceRoot":"","sources":["../src/products.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,gFAAgF;AAChF,0EAA0E;AAC1E,kEAAkE;AAClE,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,oCAAoC;AACpC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAmDrC,MAAM,IAAI,GAAG,EAAE,CAAC;AAChB,2EAA2E;AAC3E,iFAAiF;AACjF,qEAAqE;AACrE,MAAM,YAAY,GAAG,GAAG,CAAC;AAgCzB,MAAM,KAAK,GAAG;;;;;;;;;;;;;;CAcb,CAAC;AAEF,gFAAgF;AAChF,+EAA+E;AAC/E,0BAA0B;AAC1B,MAAM,SAAS,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAEpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,MAAqB,EACrB,YAA0B,KAAK,EAC/B,OAA8B,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,EACxC,EAAE;IAC7B,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAqB,EAAE,CAAC;IACjC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,GAAG,CAAC;QACF,MAAM,IAAI,GAAS,MAAM,cAAc,CACrC,MAAM,EACN,KAAK,EACL,KAAK,EACL,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAC9B,SAAS,CACV,CAAC;QACF,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,IAAI,QAAQ,CAAC;QAC/C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,cAAkC,CAAC;YACvC,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,eAAe,CAAC;YAC7C,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC;oBACH,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,IAAI,QAAQ,CAAC,CAAC;gBACvE,CAAC;gBAAC,MAAM,CAAC;oBACP,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,8BAA8B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBAC/E,CAAC;YACH,CAAC;YACD,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK;iBACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC;iBACjC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,GAAG,CAAC,IAAI,CAAC;gBACP,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;gBACnB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;gBAChC,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;gBAChC,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;gBAClB,QAAQ;gBACR,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClE,MAAM;gBACN,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;oBAC7B,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,MAAM,EAAE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;iBAC1C,CAAC,CAAC;gBACH,yEAAyE;gBACzE,0BAA0B;gBAC1B,QAAQ,EAAE,EAAE;gBACZ,cAAc,EAAE,KAAK;gBACrB,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CAAC,CAAC;QACL,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IACxF,CAAC,QAAQ,MAAM,EAAE;IACjB,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,iFAAiF;AACjF,+EAA+E;AAC/E,8EAA8E;AAC9E,kFAAkF;AAClF,EAAE;AACF,gFAAgF;AAChF,2EAA2E;AAC3E,WAAW;AACX,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,0BAA0B;AAC1B,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAEvE,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,yEAAyE;AACzE,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAiB,EAAE;IAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvC,CAAC,CAAC;AAEF,6EAA6E;AAC7E,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE;IAC7C,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,QAA0B,EAC1B,OAA+B,EACR,EAAE;IACzB,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACtF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACpC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAC7B,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAClE,CACF,CAAC;IACF,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC,CAAC;AAEF,6EAA6E;AAC7E,4DAA4D;AAC5D,EAAE;AACF,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,iDAAiD;AACjD,EAAE;AACF,4EAA4E;AAC5E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,OAAwB,EACxB,OAA+B,EACP,EAAE;IAC1B,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;YAAE,SAAS;QAClD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7D,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;;;;;;CActB,CAAC;AAEF,yDAAyD;AACzD,EAAE;AACF,iFAAiF;AACjF,6EAA6E;AAC7E,gFAAgF;AAChF,uEAAuE;AACvE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,MAAqB,EACrB,MAAc,EACd,YAA0B,KAAK,EAC/B,OAA8B,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,EACxC,EAAE;IAC7B,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAqB,EAAE,CAAC;IACjC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,GAAG,CAAC;QACF,MAAM,IAAI,GAQN,MAAM,cAAc,CACtB,MAAM,EACN,KAAK,EACL,cAAc,EACd,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,EAC9C,SAAS,CACV,CAAC;QACF,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,IAAI,QAAQ,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC;QAC5C,IAAI,CAAC,IAAI;YAAE,OAAO,GAAG,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC3B,IAAI,UAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,YAAY,MAAM,MAAM,CAAC,CAAC,KAAK,wBAAwB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACvF,SAAS;YACX,CAAC;YACD,IAAI,cAAkC,CAAC;YACvC,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;gBACrB,IAAI,CAAC;oBACH,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;gBACvD,CAAC;gBAAC,MAAM,CAAC;oBACP,qEAAqE;oBACrE,cAAc,GAAG,SAAS,CAAC;gBAC7B,CAAC;YACH,CAAC;YACD,GAAG,CAAC,IAAI,CAAC;gBACP,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1B,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,UAAU;gBACV,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3D,SAAS,EAAE,CAAC,CAAC,gBAAgB;gBAC7B,OAAO,EAAE,CAAC,CAAC,eAAe,IAAI,EAAE;gBAChC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnD,CAAC,CAAC;QACL,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,CAAC,QAAQ,MAAM,EAAE;IACjB,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -17,10 +17,12 @@ export type CatalogItem = {
|
|
|
17
17
|
export type ShopifyConfig = {
|
|
18
18
|
/** e.g. "your-store.myshopify.com" */
|
|
19
19
|
shopDomain: string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/** Admin API access token (`shpat_…`) from a custom app in the store's admin. Secret. */
|
|
21
|
+
adminToken?: string;
|
|
22
|
+
/** Dev Dashboard app client id (API key). Same-org stores only. */
|
|
23
|
+
clientId?: string;
|
|
22
24
|
/** Dev Dashboard app client secret. Secret — from env only, never hardcode. */
|
|
23
|
-
clientSecret
|
|
25
|
+
clientSecret?: string;
|
|
24
26
|
/** Admin API version, e.g. "2025-01". Pinned so an API change can't silently shift behavior. */
|
|
25
27
|
apiVersion?: string;
|
|
26
28
|
};
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,oFAAoF;IACpF,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,oFAAoF;IACpF,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAcF,MAAM,MAAM,aAAa,GAAG;IAC1B,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAKF,MAAM,MAAM,cAAc,GAAG;IAC3B,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAO1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAavC,MAAM,MAAM,WAAW,GAAG;IACxB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAOF,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curless/shopify-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Use a Shopify store as an agentbank catalog source, and record agent-settled sales back as paid Shopify orders. Maps Shopify Admin products → a generic CatalogItem your merchant backend prices from, writes an order back after an agent pays through agentbank, and records the reversal when that sale is refunded. Zero runtime dependencies (uses the platform fetch).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
15
|
+
"files": ["dist", "README.md"],
|
|
19
16
|
"publishConfig": {
|
|
20
17
|
"access": "public"
|
|
21
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
25
|
+
},
|
|
22
26
|
"devDependencies": {
|
|
23
27
|
"@types/node": "^22.10.1",
|
|
24
28
|
"typescript": "^5.6.3",
|
|
@@ -41,11 +45,5 @@
|
|
|
41
45
|
"url": "git+https://gitlab.com/robin-ruan/agentbank.git",
|
|
42
46
|
"directory": "packages/sdks/shopify-adapter"
|
|
43
47
|
},
|
|
44
|
-
"author": "robin"
|
|
45
|
-
|
|
46
|
-
"build": "tsc",
|
|
47
|
-
"typecheck": "tsc --noEmit",
|
|
48
|
-
"test": "vitest run",
|
|
49
|
-
"clean": "rm -rf dist"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
48
|
+
"author": "robin"
|
|
49
|
+
}
|