shopify_storefront_sdk 1.0.4 → 2.0.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.
Files changed (89) hide show
  1. package/README.md +136 -70
  2. package/dist/cjs/client/client.js +110 -22
  3. package/dist/cjs/generated/shopify.js +25 -49
  4. package/dist/cjs/helpers/CartManager.js +55 -0
  5. package/dist/cjs/helpers/CustomerSession.js +41 -0
  6. package/dist/cjs/index.js +210 -27
  7. package/dist/cjs/tests/integration/sdk.test.js +266 -0
  8. package/dist/cjs/tests/unit/cache.test.js +86 -0
  9. package/dist/cjs/tests/unit/cartManager.test.js +43 -0
  10. package/dist/cjs/tests/unit/client.test.js +134 -0
  11. package/dist/cjs/tests/unit/customerSession.test.js +50 -0
  12. package/dist/cjs/tests/unit/pagination.test.js +41 -0
  13. package/dist/cjs/tests/unit/webhooks.test.js +41 -0
  14. package/dist/cjs/utils/cache.js +29 -0
  15. package/dist/cjs/utils/errors.js +53 -0
  16. package/dist/cjs/utils/pagination.js +30 -0
  17. package/dist/cjs/utils/webhooks.js +33 -0
  18. package/dist/client/client.d.ts +12 -1
  19. package/dist/client/client.d.ts.map +1 -1
  20. package/dist/client/client.js +111 -21
  21. package/dist/client/client.js.map +1 -1
  22. package/dist/generated/shopify.d.ts +4048 -2764
  23. package/dist/generated/shopify.d.ts.map +1 -1
  24. package/dist/generated/shopify.js +26 -50
  25. package/dist/generated/shopify.js.map +1 -1
  26. package/dist/helpers/CartManager.d.ts +32 -0
  27. package/dist/helpers/CartManager.d.ts.map +1 -0
  28. package/dist/helpers/CartManager.js +53 -0
  29. package/dist/helpers/CartManager.js.map +1 -0
  30. package/dist/helpers/CustomerSession.d.ts +27 -0
  31. package/dist/helpers/CustomerSession.d.ts.map +1 -0
  32. package/dist/helpers/CustomerSession.js +40 -0
  33. package/dist/helpers/CustomerSession.js.map +1 -0
  34. package/dist/index.d.ts +179 -16
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +204 -28
  37. package/dist/index.js.map +1 -1
  38. package/dist/tests/integration/sdk.test.d.ts +2 -0
  39. package/dist/tests/integration/sdk.test.d.ts.map +1 -0
  40. package/dist/tests/integration/sdk.test.js +245 -0
  41. package/dist/tests/integration/sdk.test.js.map +1 -0
  42. package/dist/tests/unit/cache.test.d.ts +2 -0
  43. package/dist/tests/unit/cache.test.d.ts.map +1 -0
  44. package/dist/tests/unit/cache.test.js +85 -0
  45. package/dist/tests/unit/cache.test.js.map +1 -0
  46. package/dist/tests/unit/cartManager.test.d.ts +2 -0
  47. package/dist/tests/unit/cartManager.test.d.ts.map +1 -0
  48. package/dist/tests/unit/cartManager.test.js +43 -0
  49. package/dist/tests/unit/cartManager.test.js.map +1 -0
  50. package/dist/tests/unit/client.test.d.ts +2 -0
  51. package/dist/tests/unit/client.test.d.ts.map +1 -0
  52. package/dist/tests/unit/client.test.js +133 -0
  53. package/dist/tests/unit/client.test.js.map +1 -0
  54. package/dist/tests/unit/customerSession.test.d.ts +2 -0
  55. package/dist/tests/unit/customerSession.test.d.ts.map +1 -0
  56. package/dist/tests/unit/customerSession.test.js +49 -0
  57. package/dist/tests/unit/customerSession.test.js.map +1 -0
  58. package/dist/tests/unit/pagination.test.d.ts +2 -0
  59. package/dist/tests/unit/pagination.test.d.ts.map +1 -0
  60. package/dist/tests/unit/pagination.test.js +40 -0
  61. package/dist/tests/unit/pagination.test.js.map +1 -0
  62. package/dist/tests/unit/webhooks.test.d.ts +2 -0
  63. package/dist/tests/unit/webhooks.test.d.ts.map +1 -0
  64. package/dist/tests/unit/webhooks.test.js +40 -0
  65. package/dist/tests/unit/webhooks.test.js.map +1 -0
  66. package/dist/utils/cache.d.ts +14 -0
  67. package/dist/utils/cache.d.ts.map +1 -0
  68. package/dist/utils/cache.js +24 -0
  69. package/dist/utils/cache.js.map +1 -0
  70. package/dist/utils/config.d.ts +6 -0
  71. package/dist/utils/config.d.ts.map +1 -1
  72. package/dist/utils/errors.d.ts +30 -0
  73. package/dist/utils/errors.d.ts.map +1 -0
  74. package/dist/utils/errors.js +48 -0
  75. package/dist/utils/errors.js.map +1 -0
  76. package/dist/utils/pagination.d.ts +20 -0
  77. package/dist/utils/pagination.d.ts.map +1 -0
  78. package/dist/utils/pagination.js +28 -0
  79. package/dist/utils/pagination.js.map +1 -0
  80. package/dist/utils/webhooks.d.ts +10 -0
  81. package/dist/utils/webhooks.d.ts.map +1 -0
  82. package/dist/utils/webhooks.js +31 -0
  83. package/dist/utils/webhooks.js.map +1 -0
  84. package/package.json +11 -7
  85. package/dist/cjs/tests/test.js +0 -201
  86. package/dist/tests/test.d.ts +0 -34
  87. package/dist/tests/test.d.ts.map +0 -1
  88. package/dist/tests/test.js +0 -202
  89. package/dist/tests/test.js.map +0 -1
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_js_1 = require("../../index.js");
5
+ const cache_js_1 = require("../../utils/cache.js");
6
+ const fetchMock = vitest_1.vi.fn();
7
+ vitest_1.vi.stubGlobal("fetch", fetchMock);
8
+ (0, vitest_1.describe)("Caching System", () => {
9
+ const MOCK_DOMAIN = "test-store.myshopify.com";
10
+ const MOCK_TOKEN = "mock-token-123";
11
+ let cache;
12
+ (0, vitest_1.beforeEach)(() => {
13
+ vitest_1.vi.resetAllMocks();
14
+ cache = new cache_js_1.InMemoryCache();
15
+ });
16
+ (0, vitest_1.it)("should cache successful queries and prevent subsequent network requests", async () => {
17
+ const client = new index_js_1.default({
18
+ storeUrl: MOCK_DOMAIN,
19
+ storefrontToken: MOCK_TOKEN,
20
+ cache,
21
+ defaultCacheTtl: 60,
22
+ });
23
+ fetchMock.mockResolvedValueOnce({
24
+ ok: true,
25
+ json: async () => ({
26
+ data: { shop: { name: "First Fetch" } },
27
+ }),
28
+ });
29
+ // 1st call: Should hit the network (fetchMock called)
30
+ const result1 = await client.customQuery("query { shop { name } }", {});
31
+ (0, vitest_1.expect)(result1.shop.name).toBe("First Fetch");
32
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(1);
33
+ // 2nd call: Should return from cache (fetchMock NOT called again)
34
+ const result2 = await client.customQuery("query { shop { name } }", {});
35
+ (0, vitest_1.expect)(result2.shop.name).toBe("First Fetch");
36
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(1);
37
+ });
38
+ (0, vitest_1.it)("should NOT cache mutations", async () => {
39
+ const client = new index_js_1.default({
40
+ storeUrl: MOCK_DOMAIN,
41
+ storefrontToken: MOCK_TOKEN,
42
+ cache,
43
+ defaultCacheTtl: 60,
44
+ });
45
+ fetchMock.mockResolvedValue({
46
+ ok: true,
47
+ json: async () => ({
48
+ data: { customerCreate: { customer: { id: "123" } } },
49
+ }),
50
+ });
51
+ // 1st call
52
+ await client.customQuery("mutation { customerCreate(input: {}) { customer { id } } }", {});
53
+ // 2nd call with exactly the same mutation
54
+ await client.customQuery("mutation { customerCreate(input: {}) { customer { id } } }", {});
55
+ // Mutations should bypass the cache entirely
56
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(2);
57
+ });
58
+ (0, vitest_1.it)("should expire cache entries after the TTL", async () => {
59
+ // Override Date.now for time travel
60
+ const originalDateNow = Date.now;
61
+ let mockedTime = 1000000;
62
+ vitest_1.vi.spyOn(Date, "now").mockImplementation(() => mockedTime);
63
+ const client = new index_js_1.default({
64
+ storeUrl: MOCK_DOMAIN,
65
+ storefrontToken: MOCK_TOKEN,
66
+ cache,
67
+ defaultCacheTtl: 1, // 1 second TTL
68
+ });
69
+ fetchMock.mockResolvedValue({
70
+ ok: true,
71
+ json: async () => ({
72
+ data: { data: "mocked" },
73
+ }),
74
+ });
75
+ // Initial fetch
76
+ await client.customQuery("query { something }", {});
77
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(1);
78
+ // Fast forward time by 2 seconds (exceeding 1s TTL)
79
+ mockedTime += 2000;
80
+ // Subsequent fetch should hit network again because cache expired
81
+ await client.customQuery("query { something }", {});
82
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(2);
83
+ // Restore Date.now
84
+ vitest_1.vi.restoreAllMocks();
85
+ });
86
+ });
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_js_1 = require("../../index.js");
5
+ (0, vitest_1.describe)("CartManager", () => {
6
+ const MOCK_DOMAIN = "test-store.myshopify.com";
7
+ const MOCK_TOKEN = "mock-token-123";
8
+ let shopify;
9
+ (0, vitest_1.beforeEach)(() => {
10
+ vitest_1.vi.resetAllMocks();
11
+ shopify = new index_js_1.default({ storeUrl: MOCK_DOMAIN, storefrontToken: MOCK_TOKEN });
12
+ });
13
+ (0, vitest_1.it)("should initialize with an existing cart ID", () => {
14
+ const cart = shopify.cart("existing-cart-123");
15
+ (0, vitest_1.expect)(cart.id).toBe("existing-cart-123");
16
+ });
17
+ (0, vitest_1.it)("should lazily create a cart if no ID is provided", async () => {
18
+ const cart = shopify.cart();
19
+ (0, vitest_1.expect)(cart.id).toBeNull();
20
+ // Mock createCart on the SDK instance
21
+ const createCartSpy = vitest_1.vi.spyOn(shopify, "createCart").mockResolvedValueOnce({
22
+ cartCreate: { cart: { id: "new-cart-456" } }
23
+ });
24
+ // Mock addCartLines
25
+ const addCartLinesSpy = vitest_1.vi.spyOn(shopify, "addCartLines").mockResolvedValueOnce({});
26
+ await cart.addLines([{ merchandiseId: "variant-1", quantity: 1 }]);
27
+ (0, vitest_1.expect)(createCartSpy).toHaveBeenCalledTimes(1);
28
+ (0, vitest_1.expect)(addCartLinesSpy).toHaveBeenCalledWith({
29
+ cartId: "new-cart-456",
30
+ lines: [{ merchandiseId: "variant-1", quantity: 1 }]
31
+ });
32
+ (0, vitest_1.expect)(cart.id).toBe("new-cart-456");
33
+ });
34
+ (0, vitest_1.it)("should reuse the cart ID for subsequent operations", async () => {
35
+ const cart = shopify.cart("existing-cart-123");
36
+ const createCartSpy = vitest_1.vi.spyOn(shopify, "createCart");
37
+ const getCartSpy = vitest_1.vi.spyOn(shopify, "getCart").mockResolvedValueOnce({});
38
+ await cart.get();
39
+ // Should NOT have called createCart since an ID was provided
40
+ (0, vitest_1.expect)(createCartSpy).not.toHaveBeenCalled();
41
+ (0, vitest_1.expect)(getCartSpy).toHaveBeenCalledWith({ cartId: "existing-cart-123", first: 250 });
42
+ });
43
+ });
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_js_1 = require("../../index.js");
5
+ const errors_js_1 = require("../../utils/errors.js");
6
+ const fetchMock = vitest_1.vi.fn();
7
+ vitest_1.vi.stubGlobal("fetch", fetchMock);
8
+ (0, vitest_1.describe)("Shopify SDK Unit Tests", () => {
9
+ const MOCK_DOMAIN = "test-store.myshopify.com";
10
+ const MOCK_TOKEN = "mock-token-123";
11
+ (0, vitest_1.beforeEach)(() => {
12
+ vitest_1.vi.resetAllMocks();
13
+ });
14
+ (0, vitest_1.describe)("Initialization", () => {
15
+ (0, vitest_1.it)("should throw ShopifyInvalidRequestError if domain is missing", () => {
16
+ (0, vitest_1.expect)(() => {
17
+ new index_js_1.default({ storeUrl: "", storefrontToken: MOCK_TOKEN });
18
+ }).toThrow(errors_js_1.ShopifyInvalidRequestError);
19
+ });
20
+ (0, vitest_1.it)("should throw ShopifyInvalidRequestError if domain is invalid", () => {
21
+ (0, vitest_1.expect)(() => {
22
+ new index_js_1.default({ storeUrl: "invalid-url.com", storefrontToken: MOCK_TOKEN });
23
+ }).toThrow(errors_js_1.ShopifyInvalidRequestError);
24
+ });
25
+ (0, vitest_1.it)("should throw ShopifyInvalidRequestError if token is missing", () => {
26
+ (0, vitest_1.expect)(() => {
27
+ new index_js_1.default({ storeUrl: MOCK_DOMAIN, storefrontToken: "" });
28
+ }).toThrow(errors_js_1.ShopifyInvalidRequestError);
29
+ });
30
+ (0, vitest_1.it)("should initialize successfully with valid config", () => {
31
+ const client = new index_js_1.default({ storeUrl: MOCK_DOMAIN, storefrontToken: MOCK_TOKEN });
32
+ (0, vitest_1.expect)(client).toBeDefined();
33
+ });
34
+ });
35
+ (0, vitest_1.describe)("API Versioning", () => {
36
+ (0, vitest_1.it)("should default to the 2025-01 API version if none is provided", async () => {
37
+ const client = new index_js_1.default({ storeUrl: MOCK_DOMAIN, storefrontToken: MOCK_TOKEN });
38
+ fetchMock.mockResolvedValueOnce({
39
+ ok: true,
40
+ json: async () => ({ data: {} }),
41
+ });
42
+ await client.customQuery("query { shop { name } }", {});
43
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledWith(`https://${MOCK_DOMAIN}/api/2025-01/graphql.json`, vitest_1.expect.objectContaining({ method: "POST" }));
44
+ });
45
+ (0, vitest_1.it)("should use a custom API version if provided", async () => {
46
+ const client = new index_js_1.default({
47
+ storeUrl: MOCK_DOMAIN,
48
+ storefrontToken: MOCK_TOKEN,
49
+ apiVersion: "unstable"
50
+ });
51
+ fetchMock.mockResolvedValueOnce({
52
+ ok: true,
53
+ json: async () => ({ data: {} }),
54
+ });
55
+ await client.customQuery("query { shop { name } }", {});
56
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledWith(`https://${MOCK_DOMAIN}/api/unstable/graphql.json`, vitest_1.expect.objectContaining({ method: "POST" }));
57
+ });
58
+ });
59
+ (0, vitest_1.describe)("Error Handling", () => {
60
+ let client;
61
+ (0, vitest_1.beforeEach)(() => {
62
+ // Reduce retries in tests to avoid slowing them down
63
+ client = new index_js_1.default({ storeUrl: MOCK_DOMAIN, storefrontToken: MOCK_TOKEN, maxRetries: 0 });
64
+ });
65
+ (0, vitest_1.it)("should throw ShopifyNetworkError when fetch fails (e.g. network down)", async () => {
66
+ fetchMock.mockRejectedValueOnce(new Error("Network connection lost"));
67
+ await (0, vitest_1.expect)(client.customQuery("query { shop { name } }", {})).rejects.toThrow(errors_js_1.ShopifyNetworkError);
68
+ });
69
+ (0, vitest_1.it)("should throw ShopifyInvalidRequestError for 400 responses", async () => {
70
+ fetchMock.mockResolvedValueOnce({
71
+ ok: false,
72
+ status: 400,
73
+ statusText: "Bad Request"
74
+ });
75
+ await (0, vitest_1.expect)(client.customQuery("query { shop { name } }", {})).rejects.toThrow(errors_js_1.ShopifyInvalidRequestError);
76
+ });
77
+ (0, vitest_1.it)("should throw ShopifyGraphQLError when Shopify returns top-level errors", async () => {
78
+ fetchMock.mockResolvedValueOnce({
79
+ ok: true,
80
+ json: async () => ({
81
+ errors: [{ message: "Unauthorized Request" }],
82
+ })
83
+ });
84
+ await (0, vitest_1.expect)(client.customQuery("query { shop { name } }", {})).rejects.toThrow(errors_js_1.ShopifyGraphQLError);
85
+ });
86
+ (0, vitest_1.it)("should throw ShopifyUserError when a mutation returns nested userErrors", async () => {
87
+ fetchMock.mockResolvedValueOnce({
88
+ ok: true,
89
+ json: async () => ({
90
+ data: {
91
+ customerCreate: {
92
+ customer: null,
93
+ userErrors: [{ field: ["email"], message: "Email has already been taken" }],
94
+ },
95
+ },
96
+ })
97
+ });
98
+ await (0, vitest_1.expect)(client.customQuery("mutation { customerCreate { userErrors { message } } }", {})).rejects.toThrow(errors_js_1.ShopifyUserError);
99
+ });
100
+ (0, vitest_1.it)("should return the correct data on a successful response", async () => {
101
+ fetchMock.mockResolvedValueOnce({
102
+ ok: true,
103
+ json: async () => ({
104
+ data: {
105
+ shop: { name: "Test Store" },
106
+ },
107
+ })
108
+ });
109
+ const response = await client.customQuery("query { shop { name } }", {});
110
+ (0, vitest_1.expect)(response.shop.name).toBe("Test Store");
111
+ });
112
+ });
113
+ (0, vitest_1.describe)("Auto-Retries", () => {
114
+ (0, vitest_1.it)("should successfully retry on a 429 rate limit response", async () => {
115
+ const client = new index_js_1.default({ storeUrl: MOCK_DOMAIN, storefrontToken: MOCK_TOKEN, maxRetries: 2 });
116
+ // First attempt: Rate Limited
117
+ fetchMock.mockResolvedValueOnce({
118
+ ok: false,
119
+ status: 429,
120
+ statusText: "Too Many Requests"
121
+ });
122
+ // Second attempt: Success
123
+ fetchMock.mockResolvedValueOnce({
124
+ ok: true,
125
+ json: async () => ({
126
+ data: { shop: { name: "Success After Retry" } },
127
+ })
128
+ });
129
+ const response = await client.customQuery("query { shop { name } }", {});
130
+ (0, vitest_1.expect)(response.shop.name).toBe("Success After Retry");
131
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(2);
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_js_1 = require("../../index.js");
5
+ (0, vitest_1.describe)("CustomerSession", () => {
6
+ const MOCK_DOMAIN = "test-store.myshopify.com";
7
+ const MOCK_TOKEN = "mock-token-123";
8
+ let shopify;
9
+ (0, vitest_1.beforeEach)(() => {
10
+ vitest_1.vi.resetAllMocks();
11
+ shopify = new index_js_1.default({ storeUrl: MOCK_DOMAIN, storefrontToken: MOCK_TOKEN });
12
+ });
13
+ (0, vitest_1.it)("should initialize with the provided token", () => {
14
+ const session = shopify.customer("customer-token-123");
15
+ (0, vitest_1.expect)(session.token).toBe("customer-token-123");
16
+ });
17
+ (0, vitest_1.it)("should forward the token when verifying the customer", async () => {
18
+ const session = shopify.customer("customer-token-123");
19
+ const verifySpy = vitest_1.vi.spyOn(shopify, "verifyCustomerToken").mockResolvedValueOnce({});
20
+ await session.verify();
21
+ (0, vitest_1.expect)(verifySpy).toHaveBeenCalledWith({ token: "customer-token-123" });
22
+ });
23
+ (0, vitest_1.it)("should forward the token and customer payload when updating the customer", async () => {
24
+ const session = shopify.customer("customer-token-123");
25
+ const updateSpy = vitest_1.vi.spyOn(shopify, "updateCustomer").mockResolvedValueOnce({});
26
+ await session.update({ firstName: "John", lastName: "Doe" });
27
+ (0, vitest_1.expect)(updateSpy).toHaveBeenCalledWith({
28
+ customerAccessToken: "customer-token-123",
29
+ customer: { firstName: "John", lastName: "Doe" }
30
+ });
31
+ });
32
+ (0, vitest_1.it)("should forward the token and first argument when fetching orders", async () => {
33
+ const session = shopify.customer("customer-token-123");
34
+ const getOrdersSpy = vitest_1.vi.spyOn(shopify, "getCustomerOrders").mockResolvedValueOnce({});
35
+ await session.getOrders(50);
36
+ (0, vitest_1.expect)(getOrdersSpy).toHaveBeenCalledWith({
37
+ customerAccessToken: "customer-token-123",
38
+ first: 50
39
+ });
40
+ });
41
+ (0, vitest_1.it)("should default to first: 250 when fetching orders if not specified", async () => {
42
+ const session = shopify.customer("customer-token-123");
43
+ const getOrdersSpy = vitest_1.vi.spyOn(shopify, "getCustomerOrders").mockResolvedValueOnce({});
44
+ await session.getOrders();
45
+ (0, vitest_1.expect)(getOrdersSpy).toHaveBeenCalledWith({
46
+ customerAccessToken: "customer-token-123",
47
+ first: 250
48
+ });
49
+ });
50
+ });
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const pagination_js_1 = require("../../utils/pagination.js");
5
+ (0, vitest_1.describe)("Pagination Utility", () => {
6
+ (0, vitest_1.it)("should extract nodes and stop when hasNextPage is false", async () => {
7
+ // Mock a paginated API response
8
+ const mockResponses = [
9
+ {
10
+ data: {
11
+ edges: [{ node: { id: 1 } }, { node: { id: 2 } }],
12
+ pageInfo: { hasNextPage: true, endCursor: "cursor_1" },
13
+ },
14
+ },
15
+ {
16
+ data: {
17
+ edges: [{ node: { id: 3 } }, { node: { id: 4 } }],
18
+ pageInfo: { hasNextPage: false, endCursor: null },
19
+ },
20
+ },
21
+ ];
22
+ let requestCount = 0;
23
+ const fetchPage = async (cursor) => {
24
+ if (cursor === "cursor_1") {
25
+ requestCount++;
26
+ return mockResponses[1];
27
+ }
28
+ requestCount++;
29
+ return mockResponses[0];
30
+ };
31
+ const results = await (0, pagination_js_1.autoPaginate)(fetchPage, (res) => res === null || res === void 0 ? void 0 : res.data);
32
+ (0, vitest_1.expect)(requestCount).toBe(2);
33
+ (0, vitest_1.expect)(results).toHaveLength(4);
34
+ (0, vitest_1.expect)(results).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]);
35
+ });
36
+ (0, vitest_1.it)("should return early if the connection is null or missing", async () => {
37
+ const fetchPage = async () => ({ data: null });
38
+ const results = await (0, pagination_js_1.autoPaginate)(fetchPage, (res) => res === null || res === void 0 ? void 0 : res.data);
39
+ (0, vitest_1.expect)(results).toHaveLength(0);
40
+ });
41
+ });
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const webhooks_js_1 = require("../../utils/webhooks.js");
5
+ const index_js_1 = require("../../index.js");
6
+ const crypto = require("node:crypto");
7
+ (0, vitest_1.describe)("Webhook Verification", () => {
8
+ const SECRET = "my_super_secret_key";
9
+ const PAYLOAD = JSON.stringify({ id: 12345, status: "created" });
10
+ (0, vitest_1.it)("should return true for a valid signature", () => {
11
+ // Generate valid HMAC
12
+ const validHmac = crypto
13
+ .createHmac("sha256", SECRET)
14
+ .update(PAYLOAD)
15
+ .digest("base64");
16
+ // Test standalone utility
17
+ (0, vitest_1.expect)((0, webhooks_js_1.verifyWebhook)(PAYLOAD, validHmac, SECRET)).toBe(true);
18
+ // Test SDK static method wrapper
19
+ (0, vitest_1.expect)(index_js_1.default.verifyWebhook(PAYLOAD, validHmac, SECRET)).toBe(true);
20
+ });
21
+ (0, vitest_1.it)("should return false for an invalid signature", () => {
22
+ const invalidHmac = crypto
23
+ .createHmac("sha256", "wrong_secret")
24
+ .update(PAYLOAD)
25
+ .digest("base64");
26
+ (0, vitest_1.expect)((0, webhooks_js_1.verifyWebhook)(PAYLOAD, invalidHmac, SECRET)).toBe(false);
27
+ });
28
+ (0, vitest_1.it)("should return false if payload has been tampered with", () => {
29
+ const validHmac = crypto
30
+ .createHmac("sha256", SECRET)
31
+ .update(PAYLOAD)
32
+ .digest("base64");
33
+ const tamperedPayload = JSON.stringify({ id: 12345, status: "deleted" });
34
+ (0, vitest_1.expect)((0, webhooks_js_1.verifyWebhook)(tamperedPayload, validHmac, SECRET)).toBe(false);
35
+ });
36
+ (0, vitest_1.it)("should return false if missing parameters", () => {
37
+ (0, vitest_1.expect)((0, webhooks_js_1.verifyWebhook)("", "hmac", SECRET)).toBe(false);
38
+ (0, vitest_1.expect)((0, webhooks_js_1.verifyWebhook)(PAYLOAD, "", SECRET)).toBe(false);
39
+ (0, vitest_1.expect)((0, webhooks_js_1.verifyWebhook)(PAYLOAD, "hmac", "")).toBe(false);
40
+ });
41
+ });
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryCache = void 0;
4
+ class InMemoryCache {
5
+ constructor() {
6
+ this.cache = new Map();
7
+ }
8
+ get(key) {
9
+ const item = this.cache.get(key);
10
+ if (!item)
11
+ return null;
12
+ if (Date.now() > item.expiresAt) {
13
+ this.cache.delete(key);
14
+ return null;
15
+ }
16
+ return item.value;
17
+ }
18
+ set(key, value, ttlSeconds) {
19
+ const expiresAt = Date.now() + ttlSeconds * 1000;
20
+ this.cache.set(key, { value, expiresAt });
21
+ }
22
+ /**
23
+ * Utility to manually clear the entire memory cache.
24
+ */
25
+ clear() {
26
+ this.cache.clear();
27
+ }
28
+ }
29
+ exports.InMemoryCache = InMemoryCache;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShopifyInvalidRequestError = exports.ShopifyUserError = exports.ShopifyGraphQLError = exports.ShopifyNetworkError = exports.ShopifyError = void 0;
4
+ class ShopifyError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "ShopifyError";
8
+ Object.setPrototypeOf(this, new.target.prototype);
9
+ }
10
+ }
11
+ exports.ShopifyError = ShopifyError;
12
+ /**
13
+ * Thrown when there is an issue with the network or the Shopify API is unreachable (e.g. timeouts, 5xx responses).
14
+ */
15
+ class ShopifyNetworkError extends ShopifyError {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "ShopifyNetworkError";
19
+ }
20
+ }
21
+ exports.ShopifyNetworkError = ShopifyNetworkError;
22
+ /**
23
+ * Thrown when the GraphQL request is successfully made but Shopify returns top-level GraphQL errors.
24
+ */
25
+ class ShopifyGraphQLError extends ShopifyError {
26
+ constructor(message, graphqlErrors) {
27
+ super(message);
28
+ this.name = "ShopifyGraphQLError";
29
+ this.graphqlErrors = graphqlErrors;
30
+ }
31
+ }
32
+ exports.ShopifyGraphQLError = ShopifyGraphQLError;
33
+ /**
34
+ * Thrown when a mutation returns user errors (e.g. invalid form data, missing fields)
35
+ */
36
+ class ShopifyUserError extends ShopifyError {
37
+ constructor(message, userErrors) {
38
+ super(message);
39
+ this.name = "ShopifyUserError";
40
+ this.userErrors = userErrors;
41
+ }
42
+ }
43
+ exports.ShopifyUserError = ShopifyUserError;
44
+ /**
45
+ * Thrown when the SDK configuration is invalid or the request is malformed (e.g. 400 Bad Request, 401 Unauthorized).
46
+ */
47
+ class ShopifyInvalidRequestError extends ShopifyError {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = "ShopifyInvalidRequestError";
51
+ }
52
+ }
53
+ exports.ShopifyInvalidRequestError = ShopifyInvalidRequestError;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.autoPaginate = autoPaginate;
4
+ /**
5
+ * Automatically fetches all pages of a Shopify GraphQL Connection and flattens the nodes into an array.
6
+ *
7
+ * @param fetchPage A function that takes an optional cursor and returns a promise of the API response.
8
+ * @param getConnection A selector function that extracts the Connection object from the API response.
9
+ * @returns A flat array of all nodes across all pages.
10
+ */
11
+ async function autoPaginate(fetchPage, getConnection) {
12
+ const results = [];
13
+ let hasNextPage = true;
14
+ let cursor = undefined;
15
+ while (hasNextPage) {
16
+ const response = await fetchPage(cursor);
17
+ const connection = getConnection(response);
18
+ if (!connection || !connection.edges || !connection.pageInfo) {
19
+ break;
20
+ }
21
+ for (const edge of connection.edges) {
22
+ if (edge && edge.node) {
23
+ results.push(edge.node);
24
+ }
25
+ }
26
+ hasNextPage = connection.pageInfo.hasNextPage;
27
+ cursor = connection.pageInfo.endCursor || undefined;
28
+ }
29
+ return results;
30
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyWebhook = verifyWebhook;
4
+ const crypto = require("node:crypto");
5
+ /**
6
+ * Verifies a Shopify webhook's HMAC signature.
7
+ *
8
+ * @param rawBody The raw, unparsed request body string or buffer.
9
+ * @param hmacHeader The `x-shopify-hmac-sha256` header provided by Shopify.
10
+ * @param secret Your Shopify app or storefront webhook API secret.
11
+ * @returns A boolean indicating whether the webhook signature is valid.
12
+ */
13
+ function verifyWebhook(rawBody, hmacHeader, secret) {
14
+ if (!rawBody || !hmacHeader || !secret) {
15
+ return false;
16
+ }
17
+ try {
18
+ const generatedHash = crypto
19
+ .createHmac("sha256", secret)
20
+ .update(rawBody)
21
+ .digest("base64");
22
+ const generatedBuffer = Buffer.from(generatedHash);
23
+ const hmacBuffer = Buffer.from(hmacHeader);
24
+ // Use timingSafeEqual to prevent timing attacks
25
+ if (generatedBuffer.length !== hmacBuffer.length) {
26
+ return false;
27
+ }
28
+ return crypto.timingSafeEqual(generatedBuffer, hmacBuffer);
29
+ }
30
+ catch (err) {
31
+ return false;
32
+ }
33
+ }
@@ -2,8 +2,19 @@ import type { StoreConfig } from "../utils/config.js";
2
2
  declare class ShopifyClient {
3
3
  private endpoint;
4
4
  private token;
5
- constructor({ storeUrl, storefrontToken }: StoreConfig);
5
+ private maxRetries;
6
+ private cache;
7
+ private cacheTtl;
8
+ constructor({ storeUrl, storefrontToken, apiVersion, maxRetries, cache, defaultCacheTtl }: StoreConfig);
9
+ /**
10
+ * Generates a deterministic hash for a GraphQL query and its variables.
11
+ */
12
+ private generateCacheKey;
6
13
  query<T = any>(query: string, variables?: {}): Promise<T>;
14
+ /**
15
+ * Helper function to recursively check for `userErrors` inside the GraphQL response data.
16
+ */
17
+ private checkForUserErrors;
7
18
  }
8
19
  export default ShopifyClient;
9
20
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,cAAM,aAAa;IACjB,OAAO,CAAC,QAAQ,CAAC;IACjB,OAAO,CAAC,KAAK,CAAC;gBACF,EAAE,QAAQ,EAAE,eAAe,EAAE,EAAE,WAAW;IAQhD,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC;CAsBhE;AAED,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAUtD,cAAM,aAAa;IACjB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,QAAQ,CAAS;gBAEb,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAsB,EAAE,UAAc,EAAE,KAAK,EAAE,eAAoB,EAAE,EAAE,WAAW;IAc3H;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAIlB,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC;IA0F/D;;OAEG;IACH,OAAO,CAAC,kBAAkB;CAsB3B;AAED,eAAe,aAAa,CAAC"}