@propeller-commerce/propeller-sdk-v2 0.18.0 → 0.18.1
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@propeller-commerce/propeller-sdk-v2` are documented here.
|
|
4
4
|
|
|
5
|
+
---
|
|
6
|
+
## [0.18.1] - 2026-11-03
|
|
7
|
+
|
|
8
|
+
A host can now hand the client its own `fetch()`, and a query no longer fails
|
|
9
|
+
the page when the connection to the API drops once.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`fetch` on `GraphQLClientConfig`.** Supply the `fetch()` the client sends
|
|
14
|
+
requests with; it defaults to the global `fetch`. Pass a request-scoped fetch
|
|
15
|
+
during SSR (Nitro's `event.fetch`, in Nuxt `useRequestEvent()!.fetch`) so a
|
|
16
|
+
relative proxy `endpoint` such as `/api/graphql` is served in-process instead
|
|
17
|
+
of over the network, which fails with `fetch failed` wherever a server can't
|
|
18
|
+
reach its own public hostname. A request-scoped fetch belongs to one request,
|
|
19
|
+
so create the client per request when you pass one. The type is exported as
|
|
20
|
+
`FetchImplementation`. Contributed by Mark Wienk (TRIMM) in
|
|
21
|
+
[#1](https://github.com/propeller-commerce/propeller-sdk-v2/pull/1). (PWP-1091)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A query is sent once more when the connection drops.** `GraphQL request
|
|
26
|
+
failed: fetch failed` used to reach the page straight away, so a
|
|
27
|
+
server-rendered page answered 500 where a reload worked. A query now gets one
|
|
28
|
+
more attempt, within the same `timeout`, before the error is thrown.
|
|
29
|
+
Mutations are never resent, since the server may have applied one before the
|
|
30
|
+
connection dropped; timeouts, HTTP error statuses and GraphQL errors are not
|
|
31
|
+
retried. (PWP-1062)
|
|
32
|
+
|
|
5
33
|
---
|
|
6
34
|
## [0.18.0] - 2026-09-24
|
|
7
35
|
|
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ See [MIGRATION-0.10.0.md](./MIGRATION-0.10.0.md) for the full upgrade guide.
|
|
|
87
87
|
| `getAccessToken` | `() => string \| undefined \| Promise<string \| undefined>` | reads `localStorage['access_token']` in browser | Use this for SSR (`getServerSession`, HTTP-only cookies) or in-memory token stores. |
|
|
88
88
|
| `defaultLanguage` | `string` | — | ISO 639-1 tag (e.g. `'NL'`). Service methods that take an optional top-level `language` and don't receive one fall back to this value before sending the request. An explicit `language` on the call always wins. |
|
|
89
89
|
| `throwOnPartialErrors` | `boolean` | `false` | On a partial response (`data` *and* `errors`), services return the data and debug-log the errors. Set `true` to throw `GraphQLOperationError` instead. `client.execute()` never throws regardless. |
|
|
90
|
+
| `fetch` | `(url: string, init: RequestInit) => Promise<Response>` | global `fetch` | Request transport. Pass a request-scoped fetch during SSR (e.g. Nitro's `event.fetch`) so a relative proxy `endpoint` is handled in-process instead of over the network. A request-scoped fetch belongs to one request, so create the client per request when you pass one. |
|
|
90
91
|
|
|
91
92
|
### Proxy contract
|
|
92
93
|
|
|
@@ -66,6 +66,13 @@ export interface GraphQLResponse<T = any> {
|
|
|
66
66
|
* May return a string, undefined (no token), or a Promise of either.
|
|
67
67
|
*/
|
|
68
68
|
export type AccessTokenProvider = () => string | undefined | Promise<string | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* The subset of `fetch()` the client calls: a URL string and a `RequestInit`,
|
|
71
|
+
* resolving to a standard `Response`. Narrower than `typeof fetch` so
|
|
72
|
+
* host-runtime implementations (undici, Nitro's `event.fetch`, test doubles)
|
|
73
|
+
* fit without matching every overload.
|
|
74
|
+
*/
|
|
75
|
+
export type FetchImplementation = (url: string, init: RequestInit) => Promise<Response>;
|
|
69
76
|
/**
|
|
70
77
|
* Configuration for the GraphQL client.
|
|
71
78
|
*/
|
|
@@ -118,6 +125,18 @@ export interface GraphQLClientConfig {
|
|
|
118
125
|
* compatible). The low-level `client.execute()` never throws regardless.
|
|
119
126
|
*/
|
|
120
127
|
throwOnPartialErrors?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* `fetch()` implementation used for every request. Defaults to the global
|
|
130
|
+
* `fetch`, resolved per request (so later polyfills and spies apply).
|
|
131
|
+
*
|
|
132
|
+
* Supply a request-scoped fetch during SSR to keep the call in-process:
|
|
133
|
+
* Nitro's `event.fetch` (Nuxt: `useRequestEvent()!.fetch`) routes a
|
|
134
|
+
* relative `endpoint` like `/api/graphql` straight to the app's own proxy
|
|
135
|
+
* handler. With the global `fetch`, SSR needs an absolute URL and makes a
|
|
136
|
+
* real network round trip to its own public hostname — which fails with
|
|
137
|
+
* `fetch failed` wherever the server can't reach its own ingress.
|
|
138
|
+
*/
|
|
139
|
+
fetch?: FetchImplementation;
|
|
121
140
|
}
|
|
122
141
|
/**
|
|
123
142
|
* A GraphQL client for the Propeller eCommerce Platform.
|
|
@@ -195,6 +214,9 @@ export declare class GraphQLClient {
|
|
|
195
214
|
* who want to inspect partial responses can do so. The higher-level
|
|
196
215
|
* `query()` / `mutate()` helpers and the `runOperation` helper used by
|
|
197
216
|
* services throw `GraphQLOperationError` when `errors` is non-empty.
|
|
217
|
+
*
|
|
218
|
+
* A query whose connection drops (`fetch failed`) is sent once more, within
|
|
219
|
+
* the same `timeout`, before the error is thrown. Mutations are never resent.
|
|
198
220
|
*/
|
|
199
221
|
execute<T = any>(operation: GraphQLOperation): Promise<GraphQLResponse<T>>;
|
|
200
222
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Note: This client uses the built-in fetch API available in Node.js 18
|
|
3
|
-
// For older runtimes,
|
|
2
|
+
// Note: This client uses the built-in fetch API available in Node.js 18+
|
|
3
|
+
// unless `config.fetch` supplies another implementation. For older runtimes,
|
|
4
|
+
// install a fetch polyfill.
|
|
4
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
6
|
exports.GraphQLClient = void 0;
|
|
6
7
|
exports.createClient = createClient;
|
|
@@ -15,6 +16,10 @@ const DEFAULT_ORDER_EDITOR_MUTATIONS = [
|
|
|
15
16
|
'triggerQuoteSendRequest',
|
|
16
17
|
'triggerOrderSendConfirm',
|
|
17
18
|
];
|
|
19
|
+
// The `mutation` keyword at the start of any line. Generated documents put
|
|
20
|
+
// their fragments first, so "the document starts with `mutation`" would miss
|
|
21
|
+
// most mutations — and a missed mutation is one that gets sent twice.
|
|
22
|
+
const MUTATION_DOCUMENT = /^\s*mutation\b/m;
|
|
18
23
|
function defaultAccessTokenProvider() {
|
|
19
24
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
20
25
|
return window.localStorage.getItem(DEFAULT_TOKEN_STORAGE_KEY) || undefined;
|
|
@@ -183,6 +188,9 @@ class GraphQLClient {
|
|
|
183
188
|
* who want to inspect partial responses can do so. The higher-level
|
|
184
189
|
* `query()` / `mutate()` helpers and the `runOperation` helper used by
|
|
185
190
|
* services throw `GraphQLOperationError` when `errors` is non-empty.
|
|
191
|
+
*
|
|
192
|
+
* A query whose connection drops (`fetch failed`) is sent once more, within
|
|
193
|
+
* the same `timeout`, before the error is thrown. Mutations are never resent.
|
|
186
194
|
*/
|
|
187
195
|
async execute(operation) {
|
|
188
196
|
const { query, variables = {}, operationName, fetchOptions } = operation;
|
|
@@ -200,7 +208,8 @@ class GraphQLClient {
|
|
|
200
208
|
const controller = new AbortController();
|
|
201
209
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
202
210
|
try {
|
|
203
|
-
const
|
|
211
|
+
const fetchImpl = this.config.fetch ?? fetch;
|
|
212
|
+
const send = () => fetchImpl(this.getEndpoint(), {
|
|
204
213
|
method: 'POST',
|
|
205
214
|
headers,
|
|
206
215
|
body,
|
|
@@ -214,6 +223,18 @@ class GraphQLClient {
|
|
|
214
223
|
next: fetchOptions?.next,
|
|
215
224
|
signal: controller.signal,
|
|
216
225
|
});
|
|
226
|
+
// A dropped connection rejects fetch() with a TypeError (the fetch spec's
|
|
227
|
+
// network error). A timeout rejects with an AbortError and an HTTP error
|
|
228
|
+
// status resolves, so neither is retried. Matched by name, not
|
|
229
|
+
// `instanceof`, so a TypeError from another realm (Jest, a vm sandbox)
|
|
230
|
+
// still counts. A mutation is never sent twice: the server may have
|
|
231
|
+
// applied it before the connection dropped. The retry shares the timeout.
|
|
232
|
+
const response = await send().catch((error) => {
|
|
233
|
+
if (error?.name !== 'TypeError' || MUTATION_DOCUMENT.test(query))
|
|
234
|
+
throw error;
|
|
235
|
+
this.debugLog(`Connection dropped (${error.message}), retrying ${actualOperationName ?? 'query'} once`);
|
|
236
|
+
return send();
|
|
237
|
+
});
|
|
217
238
|
clearTimeout(timeoutId);
|
|
218
239
|
if (!response.ok) {
|
|
219
240
|
const bodyText = await response.text().catch(() => '');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* compatibility with code written against 0.9.x.
|
|
8
8
|
*/
|
|
9
9
|
export { GraphQLClient, createClient, createGraphQLClient, initializeClient, getClient, } from './GraphQLClient';
|
|
10
|
-
export type { GraphQLOperation, GraphQLResponse, GraphQLClientConfig, GraphQLFetchOptions, AccessTokenProvider, } from './GraphQLClient';
|
|
10
|
+
export type { GraphQLOperation, GraphQLResponse, GraphQLClientConfig, GraphQLFetchOptions, AccessTokenProvider, FetchImplementation, } from './GraphQLClient';
|
|
11
11
|
export { GraphQLOperationError } from './GraphQLOperationError';
|
|
12
12
|
export type { GraphQLErrorEntry } from './GraphQLOperationError';
|
|
13
13
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Note: This client uses the built-in fetch API available in Node.js 18
|
|
2
|
-
// For older runtimes,
|
|
1
|
+
// Note: This client uses the built-in fetch API available in Node.js 18+
|
|
2
|
+
// unless `config.fetch` supplies another implementation. For older runtimes,
|
|
3
|
+
// install a fetch polyfill.
|
|
3
4
|
import { GraphQLOperationError } from './GraphQLOperationError.js';
|
|
4
5
|
const DEFAULT_TOKEN_STORAGE_KEY = 'access_token';
|
|
5
6
|
const DEFAULT_ORDER_EDITOR_MUTATIONS = [
|
|
@@ -8,6 +9,10 @@ const DEFAULT_ORDER_EDITOR_MUTATIONS = [
|
|
|
8
9
|
'triggerQuoteSendRequest',
|
|
9
10
|
'triggerOrderSendConfirm',
|
|
10
11
|
];
|
|
12
|
+
// The `mutation` keyword at the start of any line. Generated documents put
|
|
13
|
+
// their fragments first, so "the document starts with `mutation`" would miss
|
|
14
|
+
// most mutations — and a missed mutation is one that gets sent twice.
|
|
15
|
+
const MUTATION_DOCUMENT = /^\s*mutation\b/m;
|
|
11
16
|
function defaultAccessTokenProvider() {
|
|
12
17
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
13
18
|
return window.localStorage.getItem(DEFAULT_TOKEN_STORAGE_KEY) || undefined;
|
|
@@ -176,6 +181,9 @@ export class GraphQLClient {
|
|
|
176
181
|
* who want to inspect partial responses can do so. The higher-level
|
|
177
182
|
* `query()` / `mutate()` helpers and the `runOperation` helper used by
|
|
178
183
|
* services throw `GraphQLOperationError` when `errors` is non-empty.
|
|
184
|
+
*
|
|
185
|
+
* A query whose connection drops (`fetch failed`) is sent once more, within
|
|
186
|
+
* the same `timeout`, before the error is thrown. Mutations are never resent.
|
|
179
187
|
*/
|
|
180
188
|
async execute(operation) {
|
|
181
189
|
const { query, variables = {}, operationName, fetchOptions } = operation;
|
|
@@ -193,7 +201,8 @@ export class GraphQLClient {
|
|
|
193
201
|
const controller = new AbortController();
|
|
194
202
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
195
203
|
try {
|
|
196
|
-
const
|
|
204
|
+
const fetchImpl = this.config.fetch ?? fetch;
|
|
205
|
+
const send = () => fetchImpl(this.getEndpoint(), {
|
|
197
206
|
method: 'POST',
|
|
198
207
|
headers,
|
|
199
208
|
body,
|
|
@@ -207,6 +216,18 @@ export class GraphQLClient {
|
|
|
207
216
|
next: fetchOptions?.next,
|
|
208
217
|
signal: controller.signal,
|
|
209
218
|
});
|
|
219
|
+
// A dropped connection rejects fetch() with a TypeError (the fetch spec's
|
|
220
|
+
// network error). A timeout rejects with an AbortError and an HTTP error
|
|
221
|
+
// status resolves, so neither is retried. Matched by name, not
|
|
222
|
+
// `instanceof`, so a TypeError from another realm (Jest, a vm sandbox)
|
|
223
|
+
// still counts. A mutation is never sent twice: the server may have
|
|
224
|
+
// applied it before the connection dropped. The retry shares the timeout.
|
|
225
|
+
const response = await send().catch((error) => {
|
|
226
|
+
if (error?.name !== 'TypeError' || MUTATION_DOCUMENT.test(query))
|
|
227
|
+
throw error;
|
|
228
|
+
this.debugLog(`Connection dropped (${error.message}), retrying ${actualOperationName ?? 'query'} once`);
|
|
229
|
+
return send();
|
|
230
|
+
});
|
|
210
231
|
clearTimeout(timeoutId);
|
|
211
232
|
if (!response.ok) {
|
|
212
233
|
const bodyText = await response.text().catch(() => '');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@propeller-commerce/propeller-sdk-v2",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "TypeScript GraphQL client for Propeller eCommerce V2 Platform - works with React, Vue, Angular and all JavaScript frameworks",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|