@porulle/adapter-shopify 0.57.1 → 0.58.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/adapter-shopify",
3
- "version": "0.57.1",
3
+ "version": "0.58.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,15 +10,15 @@
10
10
  }
11
11
  },
12
12
  "dependencies": {
13
- "@porulle/core": "0.57.1"
13
+ "@porulle/core": "0.58.0"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^24.5.2",
17
17
  "eslint": "^9.39.1",
18
18
  "typescript": "5.9.2",
19
19
  "vitest": "^3.2.4",
20
- "@porulle/typescript-config": "0.1.0",
21
- "@porulle/eslint-config": "0.1.0"
20
+ "@porulle/eslint-config": "0.1.0",
21
+ "@porulle/typescript-config": "0.1.0"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"
package/src/index.ts CHANGED
@@ -326,6 +326,17 @@ export function shopifyReauthorizeUrl(
326
326
 
327
327
  export function shopifyConnector(options: ShopifyConnectorOptions = {}): ChannelConnector {
328
328
  const fetchImpl = options.fetchImpl ?? fetch;
329
+ /** One `products.json` page of variant stock (`inventory_quantity`, all locations summed), keyed by variant id. */
330
+ const inventoryPage = async (store: ChannelStore, cursor: string | null): Promise<Result<{ levels: ChannelInventoryLevel[]; nextCursor: string | null }>> => {
331
+ const token = credentials(store);
332
+ if (!token) return Err({ code: "SHOPIFY_CREDENTIALS_REQUIRED", message: "Shopify accessToken is required." });
333
+ const url = cursor ?? `${apiBase(store, version, options.baseUrl)}/products.json?limit=250&fields=id,variants`;
334
+ const result = await request<{ products: ShopifyProduct[] }>(fetchImpl, url, token);
335
+ if (!result.ok) return result;
336
+ const levels = result.value.data.products.flatMap((product) => (product.variants ?? [])
337
+ .map((variant): ChannelInventoryLevel => ({ externalId: String(variant.id), available: Math.max(0, variant.inventory_quantity ?? 0) })));
338
+ return Ok({ levels, nextCursor: nextPageUrl(result.value.response) });
339
+ };
329
340
  const version = options.apiVersion ?? "2024-10";
330
341
  const currencyCache = new Map<string, Promise<string | undefined>>();
331
342
  return defineChannelConnector({
@@ -496,22 +507,20 @@ export function shopifyConnector(options: ShopifyConnectorOptions = {}): Channel
496
507
 
497
508
  const wanted = ids === undefined ? undefined : new Set(ids);
498
509
  const levels: ChannelInventoryLevel[] = [];
499
- let url: string | null = `${base}/products.json?limit=250&fields=id,variants`;
510
+ let cursor: string | null = null;
500
511
  const seen = new Set<string>();
501
- while (url !== null) {
502
- if (seen.has(url)) return Err({ code: "SHOPIFY_API_FAILED", message: "Shopify pagination repeated a page." });
503
- seen.add(url);
504
- const result = await request<{ products: ShopifyProduct[] }>(fetchImpl, url, token);
505
- if (!result.ok) return result;
506
- for (const product of result.value.data.products) {
507
- for (const variant of product.variants ?? []) {
508
- if (wanted === undefined || wanted.has(String(variant.id))) levels.push(level(variant));
509
- }
510
- }
511
- url = nextPageUrl(result.value.response);
512
- }
512
+ do {
513
+ const page = await inventoryPage(store, cursor);
514
+ if (!page.ok) return page;
515
+ levels.push(...page.value.levels.filter((entry) => wanted === undefined || wanted.has(entry.externalId)));
516
+ cursor = page.value.nextCursor;
517
+ if (cursor !== null && seen.has(cursor)) return Err({ code: "SHOPIFY_API_FAILED", message: "Shopify pagination repeated a page." });
518
+ if (cursor !== null) seen.add(cursor);
519
+ } while (cursor !== null);
513
520
  return Ok(levels);
514
521
  },
522
+ /** One `products.json` page (250 products) of variant stock; the cursor is Shopify's next-page URL. */
523
+ fetchInventoryPage: (store, cursor) => inventoryPage(store, cursor),
515
524
  async pushCatalog(store: ChannelStore, items: ChannelPushCatalogItem[], opts?: { dryRun?: boolean }): Promise<Result<ChannelPushCatalogResult, ChannelConnectorError>> {
516
525
  const oauthStartUrl = options.appUrl ? shopifyOAuthStartUrl(options.appUrl, store.storeDomain) : undefined;
517
526
  return executePushCatalog({