@doswiftly/storefront-sdk 22.4.0 → 22.5.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 +38 -0
- package/README.md +22 -0
- package/dist/core/client/cache-eligibility.d.ts +61 -0
- package/dist/core/client/cache-eligibility.d.ts.map +1 -0
- package/dist/core/client/cache-eligibility.js +80 -0
- package/dist/core/client/create-client.d.ts.map +1 -1
- package/dist/core/client/create-client.js +16 -3
- package/dist/core/client/execute.d.ts.map +1 -1
- package/dist/core/client/execute.js +47 -35
- package/dist/core/client/types.d.ts +31 -4
- package/dist/core/client/types.d.ts.map +1 -1
- package/dist/core/client/types.js +3 -3
- package/dist/core/generated/operation-types.d.ts +1 -1
- package/dist/core/generated/operation-types.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 22.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6ca2a5f: Corrected the GraphQL schema descriptions for the `CartStatus` enum and the `Cart.status` field so they match the cart's actual behaviour: an `ABANDONED` cart is revived in place by any deliberate buyer edit (it is not locked), editing an `EXPIRED` cart returns `CART_NOT_FOUND`, and only `CONVERTED` / completed carts return `ALREADY_COMPLETED`. Documentation only — no type or API changes.
|
|
8
|
+
|
|
9
|
+
## 22.5.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 3d5f1d3: Public reads now use the cacheable `GET` transport **by default** — you no longer opt in per query with `cacheLong()`. A non-mutation query that carries a persisted-document id and no signed-in identity is sent as a shared, credential-less `GET` so a CDN can cache it; a request carrying an `Authorization` bearer or a cart secret automatically stays on `POST` with credentials, so personalised reads are never shared. The server decides whether a result is actually cacheable via its `Cache-Control` response — the client only chooses the transport. Opt a specific persisted read out of the shared cache with `cachePrivate()` or `cacheNone()` (forces `POST`). Mutations and any request without a document id keep the existing `POST` behaviour, so current behaviour is unchanged until document ids are present.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// Cacheable GET automatically (anonymous visitor) — no cacheLong() needed:
|
|
17
|
+
const products = await client.query(ProductsQuery, { first: 20 });
|
|
18
|
+
|
|
19
|
+
// Keep a persisted read per-user (forces POST):
|
|
20
|
+
const account = await client.query(MyAccountQuery, {}, cachePrivate());
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- 7664b81: Added a ready GraphQL codegen recipe for edge-cacheable reads. Import `createCodegenConfig` from `@doswiftly/storefront-operations/codegen` and `export default` it from your `codegen.ts` — it points the client preset at the bundled schema and emits persisted documents whose `documentId` matches the Storefront API's `sha256:<hex>` contract, so public reads resolve server-side and can be cached at the edge.
|
|
24
|
+
|
|
25
|
+
You write operations with the generated `gql(...)` tag. They compile to lightweight typed strings (no `graphql` package at runtime) that the SDK's `query` / `mutate` accept directly, custom scalars are mapped to precision-safe TypeScript types (money and 64-bit integers as strings), and fragment fields are read directly with no unmasking helper. If your project already imports a `gql` from another GraphQL client, override the tag name: `createCodegenConfig({ gqlTagName: 'graphql' })`. Requires `@graphql-codegen/cli`, `@graphql-codegen/client-preset@^4` and `graphql@^16` as devDependencies (the preset doesn't support `graphql` v17 yet — fragment operations fail to generate). See the README "Edge-cacheable reads" section. `@doswiftly/storefront-sdk` accepts these generated documents directly in `query` / `mutate`, and its cacheable `GET` transport degrades to a `POST` on any non-success response.
|
|
26
|
+
|
|
27
|
+
Author operations with the generated tag — codegen emits each as a typed document carrying its `documentId`:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { gql } from "./gql";
|
|
31
|
+
|
|
32
|
+
export const ProductsQuery = gql(`
|
|
33
|
+
query Products($first: Int) {
|
|
34
|
+
products(first: $first) {
|
|
35
|
+
nodes { id handle title }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`);
|
|
39
|
+
```
|
|
40
|
+
|
|
3
41
|
## 22.4.0
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1122,6 +1122,28 @@ cacheCustom({ maxAge: 300, swr: 600 }) // 5min + 10min swr
|
|
|
1122
1122
|
const data = await client.query(ProductQuery, { handle }, cacheLong());
|
|
1123
1123
|
```
|
|
1124
1124
|
|
|
1125
|
+
### Edge caching is the default for public reads
|
|
1126
|
+
|
|
1127
|
+
When an operation is generated with a persisted-document id (the recipe in
|
|
1128
|
+
`@doswiftly/storefront-operations` does this), a non-mutation read with no
|
|
1129
|
+
signed-in identity is sent as a **cacheable `GET`** automatically — you do not
|
|
1130
|
+
need to pass `cacheLong()`. A shared CDN can then serve it, and the server
|
|
1131
|
+
decides whether the result is actually cacheable via its `Cache-Control`
|
|
1132
|
+
response. Requests carrying an identity (an `Authorization` bearer or a cart
|
|
1133
|
+
secret) stay on `POST` with credentials, so personalised reads are never shared.
|
|
1134
|
+
|
|
1135
|
+
The cache strategies above are now **tuning / opt-out** rather than the on
|
|
1136
|
+
switch: pass `cachePrivate()` or `cacheNone()` to keep a persisted read per-user
|
|
1137
|
+
(forces `POST`), or `cacheLong({ tags })` to attach Next.js revalidation tags.
|
|
1138
|
+
|
|
1139
|
+
```typescript
|
|
1140
|
+
// Cacheable GET automatically — no strategy argument needed:
|
|
1141
|
+
const products = await client.query(ProductsQuery, { first: 20 });
|
|
1142
|
+
|
|
1143
|
+
// Keep a persisted read per-user (forces POST):
|
|
1144
|
+
const account = await client.query(MyAccountQuery, {}, cachePrivate());
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1125
1147
|
## GraphQL schema for codegen
|
|
1126
1148
|
|
|
1127
1149
|
The GraphQL SDL ships in the linked `@doswiftly/storefront-operations` package
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cacheable-public-read eligibility — the platform-default rule deciding whether
|
|
3
|
+
* a query may travel over the shared, credential-less cacheable GET transport
|
|
4
|
+
* instead of POST. No per-query `cacheLong()` opt-in is required: a non-mutation
|
|
5
|
+
* persisted read with no identity is eligible by default.
|
|
6
|
+
*
|
|
7
|
+
* Mirror of the backend storefront-cache header SSOT
|
|
8
|
+
* (`commerce/storefront-graphql/cache/cache-headers.ts`). The published SDK
|
|
9
|
+
* cannot import the private backend, so the contract is duplicated here
|
|
10
|
+
* intentionally — the same rationale as `LANGUAGE_HEADER_NAME`. A drift test
|
|
11
|
+
* (`cache-eligibility.drift.test.ts`, run against the backend source) keeps the
|
|
12
|
+
* two in sync:
|
|
13
|
+
* - {@link IDENTITY_HEADERS} must be a **superset** of the backend's (missing one
|
|
14
|
+
* → the SDK sends a credential-less GET that strips the identity → wrong data).
|
|
15
|
+
* - {@link VARIANCE_AXIS_HEADERS} must **equal** the backend's (a missing axis
|
|
16
|
+
* collapses two shops / currencies / languages onto one shared edge entry →
|
|
17
|
+
* cross-tenant / wrong-variant leak; an extra axis fragments the cache).
|
|
18
|
+
*
|
|
19
|
+
* The SDK does NOT classify operations as cacheable (PUBLIC_READ vs USER_SCOPED) —
|
|
20
|
+
* that stays the backend's sole authority. It resolves the documentId, classifies
|
|
21
|
+
* the operation fail-closed and emits `Cache-Control: public|private`; the SDK
|
|
22
|
+
* only chooses the *transport*. Do NOT add a PUBLIC_READ allow-list here — it
|
|
23
|
+
* would duplicate the backend registry and drift.
|
|
24
|
+
*
|
|
25
|
+
* Identity manifests as a header by the time a request reaches the transport:
|
|
26
|
+
* `authMiddleware` adds `Authorization` whenever a customer is signed in (the
|
|
27
|
+
* token lives in memory, seeded server-side or rehydrated client-side), and
|
|
28
|
+
* `cartSecretMiddleware` adds `x-cart-secret`. The backend's additional
|
|
29
|
+
* cookie / `@inContext` identity checks are upstream signals the SDK has already
|
|
30
|
+
* converted to one of these headers — so the header gate is sufficient.
|
|
31
|
+
*/
|
|
32
|
+
import type { GraphQLRequest } from './types';
|
|
33
|
+
/** Shop-routing header the client always sends; also a cache variance axis. */
|
|
34
|
+
export declare const SHOP_SLUG_HEADER = "X-Shop-Slug";
|
|
35
|
+
/**
|
|
36
|
+
* Header-level identity signals. Their presence keeps a request on POST (with
|
|
37
|
+
* credentials) instead of the shared cacheable GET. Mirror of backend
|
|
38
|
+
* `IDENTITY_HEADERS`; compared case-insensitively.
|
|
39
|
+
*/
|
|
40
|
+
export declare const IDENTITY_HEADERS: readonly ["authorization", "x-cart-secret"];
|
|
41
|
+
/**
|
|
42
|
+
* Cache variance axes as `[requestHeaderName, getUrlParam]`. The backend keys its
|
|
43
|
+
* L1 cache on the resolved `{tenant, currency, language}`; the SDK copies the same
|
|
44
|
+
* axes into the GET URL so a shared edge cache keys correctly (it keys on the URL,
|
|
45
|
+
* not on request headers). Consumed by `buildTrustedGetRequest`.
|
|
46
|
+
*/
|
|
47
|
+
export declare const VARIANCE_AXIS_PARAMS: readonly [readonly ["X-Shop-Slug", "__shop"], readonly ["X-Preferred-Currency", "__currency"], readonly ["X-Language", "__language"]];
|
|
48
|
+
/**
|
|
49
|
+
* Lowercase variance header names — the contract compared against the backend
|
|
50
|
+
* `VARIANCE_HEADERS` SSOT by the drift test. Derived from {@link VARIANCE_AXIS_PARAMS}
|
|
51
|
+
* so there is one list in the SDK.
|
|
52
|
+
*/
|
|
53
|
+
export declare const VARIANCE_AXIS_HEADERS: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Whether a request may use the cacheable GET transport (transport safety only —
|
|
56
|
+
* the backend decides actual cacheability via `Cache-Control`). Eligible when it
|
|
57
|
+
* is a non-mutation persisted read carrying no identity. The explicit cache
|
|
58
|
+
* opt-outs (`cachePrivate()` / `cacheNone()`) are applied by the caller.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isPublicReadEligible(request: GraphQLRequest): boolean;
|
|
61
|
+
//# sourceMappingURL=cache-eligibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-eligibility.d.ts","sourceRoot":"","sources":["../../../src/core/client/cache-eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAI9C,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,6CAA8C,CAAC;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,uIAIvB,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAElD,CAAC;AAYF;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAIrE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cacheable-public-read eligibility — the platform-default rule deciding whether
|
|
3
|
+
* a query may travel over the shared, credential-less cacheable GET transport
|
|
4
|
+
* instead of POST. No per-query `cacheLong()` opt-in is required: a non-mutation
|
|
5
|
+
* persisted read with no identity is eligible by default.
|
|
6
|
+
*
|
|
7
|
+
* Mirror of the backend storefront-cache header SSOT
|
|
8
|
+
* (`commerce/storefront-graphql/cache/cache-headers.ts`). The published SDK
|
|
9
|
+
* cannot import the private backend, so the contract is duplicated here
|
|
10
|
+
* intentionally — the same rationale as `LANGUAGE_HEADER_NAME`. A drift test
|
|
11
|
+
* (`cache-eligibility.drift.test.ts`, run against the backend source) keeps the
|
|
12
|
+
* two in sync:
|
|
13
|
+
* - {@link IDENTITY_HEADERS} must be a **superset** of the backend's (missing one
|
|
14
|
+
* → the SDK sends a credential-less GET that strips the identity → wrong data).
|
|
15
|
+
* - {@link VARIANCE_AXIS_HEADERS} must **equal** the backend's (a missing axis
|
|
16
|
+
* collapses two shops / currencies / languages onto one shared edge entry →
|
|
17
|
+
* cross-tenant / wrong-variant leak; an extra axis fragments the cache).
|
|
18
|
+
*
|
|
19
|
+
* The SDK does NOT classify operations as cacheable (PUBLIC_READ vs USER_SCOPED) —
|
|
20
|
+
* that stays the backend's sole authority. It resolves the documentId, classifies
|
|
21
|
+
* the operation fail-closed and emits `Cache-Control: public|private`; the SDK
|
|
22
|
+
* only chooses the *transport*. Do NOT add a PUBLIC_READ allow-list here — it
|
|
23
|
+
* would duplicate the backend registry and drift.
|
|
24
|
+
*
|
|
25
|
+
* Identity manifests as a header by the time a request reaches the transport:
|
|
26
|
+
* `authMiddleware` adds `Authorization` whenever a customer is signed in (the
|
|
27
|
+
* token lives in memory, seeded server-side or rehydrated client-side), and
|
|
28
|
+
* `cartSecretMiddleware` adds `x-cart-secret`. The backend's additional
|
|
29
|
+
* cookie / `@inContext` identity checks are upstream signals the SDK has already
|
|
30
|
+
* converted to one of these headers — so the header gate is sufficient.
|
|
31
|
+
*/
|
|
32
|
+
import { CURRENCY_HEADER_NAME } from '../currency/cookie-config';
|
|
33
|
+
import { LANGUAGE_HEADER_NAME } from '../language/cookie-config';
|
|
34
|
+
/** Shop-routing header the client always sends; also a cache variance axis. */
|
|
35
|
+
export const SHOP_SLUG_HEADER = 'X-Shop-Slug';
|
|
36
|
+
/**
|
|
37
|
+
* Header-level identity signals. Their presence keeps a request on POST (with
|
|
38
|
+
* credentials) instead of the shared cacheable GET. Mirror of backend
|
|
39
|
+
* `IDENTITY_HEADERS`; compared case-insensitively.
|
|
40
|
+
*/
|
|
41
|
+
export const IDENTITY_HEADERS = ['authorization', 'x-cart-secret'];
|
|
42
|
+
/**
|
|
43
|
+
* Cache variance axes as `[requestHeaderName, getUrlParam]`. The backend keys its
|
|
44
|
+
* L1 cache on the resolved `{tenant, currency, language}`; the SDK copies the same
|
|
45
|
+
* axes into the GET URL so a shared edge cache keys correctly (it keys on the URL,
|
|
46
|
+
* not on request headers). Consumed by `buildTrustedGetRequest`.
|
|
47
|
+
*/
|
|
48
|
+
export const VARIANCE_AXIS_PARAMS = [
|
|
49
|
+
[SHOP_SLUG_HEADER, '__shop'],
|
|
50
|
+
[CURRENCY_HEADER_NAME, '__currency'],
|
|
51
|
+
[LANGUAGE_HEADER_NAME, '__language'],
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Lowercase variance header names — the contract compared against the backend
|
|
55
|
+
* `VARIANCE_HEADERS` SSOT by the drift test. Derived from {@link VARIANCE_AXIS_PARAMS}
|
|
56
|
+
* so there is one list in the SDK.
|
|
57
|
+
*/
|
|
58
|
+
export const VARIANCE_AXIS_HEADERS = VARIANCE_AXIS_PARAMS.map(([header]) => header.toLowerCase());
|
|
59
|
+
const IDENTITY_HEADER_SET = new Set(IDENTITY_HEADERS);
|
|
60
|
+
/** True when any identity header is present (case-insensitive). */
|
|
61
|
+
function hasIdentityHeader(headers) {
|
|
62
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
63
|
+
if (value && IDENTITY_HEADER_SET.has(name.toLowerCase()))
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a request may use the cacheable GET transport (transport safety only —
|
|
70
|
+
* the backend decides actual cacheability via `Cache-Control`). Eligible when it
|
|
71
|
+
* is a non-mutation persisted read carrying no identity. The explicit cache
|
|
72
|
+
* opt-outs (`cachePrivate()` / `cacheNone()`) are applied by the caller.
|
|
73
|
+
*/
|
|
74
|
+
export function isPublicReadEligible(request) {
|
|
75
|
+
if (request.isMutation)
|
|
76
|
+
return false;
|
|
77
|
+
if (typeof request.documentId !== 'string' || request.documentId.length === 0)
|
|
78
|
+
return false;
|
|
79
|
+
return !hasIdentityHeader(request.headers);
|
|
80
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-client.d.ts","sourceRoot":"","sources":["../../../src/core/client/create-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAKjB,MAAM,SAAS,CAAC;AAQjB,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"create-client.d.ts","sourceRoot":"","sources":["../../../src/core/client/create-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAKjB,MAAM,SAAS,CAAC;AAQjB,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,gBAAgB,CAmIvF"}
|
|
@@ -47,17 +47,30 @@ export function createStorefrontClient(config) {
|
|
|
47
47
|
return compiledPipeline;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Resolve query string from
|
|
50
|
+
* Resolve query string from a typed document or plain string.
|
|
51
51
|
*/
|
|
52
52
|
function resolveQuery(document) {
|
|
53
53
|
return typeof document === 'string' ? document : document.toString();
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Persisted-document id carried by codegen on `
|
|
56
|
+
* Persisted-document id carried by codegen on `__meta__.hash`.
|
|
57
57
|
* Plain strings (and documents without a populated hash) have none → POST transport.
|
|
58
|
+
*
|
|
59
|
+
* `__meta__` is optional codegen metadata, not part of the public document
|
|
60
|
+
* contract (`TypedDocumentLike`), so it is read defensively via narrowing — no
|
|
61
|
+
* cast, no assumption that any given document carries it.
|
|
58
62
|
*/
|
|
59
63
|
function resolveDocumentId(document) {
|
|
60
|
-
|
|
64
|
+
if (typeof document === 'string')
|
|
65
|
+
return undefined;
|
|
66
|
+
if (!('__meta__' in document))
|
|
67
|
+
return undefined;
|
|
68
|
+
const meta = document.__meta__;
|
|
69
|
+
if (typeof meta !== 'object' || meta === null)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (!('hash' in meta) || typeof meta.hash !== 'string')
|
|
72
|
+
return undefined;
|
|
73
|
+
return meta.hash;
|
|
61
74
|
}
|
|
62
75
|
/**
|
|
63
76
|
* Core request execution — shared by query() and mutate().
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../../src/core/client/execute.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EAEZ,cAAc,EACd,eAAe,EAChB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../../src/core/client/execute.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EAEZ,cAAc,EACd,eAAe,EAChB,MAAM,SAAS,CAAC;AAGjB,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B;;;;OAIG;IACH,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CAClC;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,GACpD,oBAAoB,GAAG,IAAI,CAiB7B;AAqKD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,IAGnB,SAAS,cAAc,KAAG,OAAO,CAAC,eAAe,CAAC,CAqFjF"}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* Sends GraphQL POST request, parses response, returns typed GraphQLResponse.
|
|
5
5
|
* Error normalization is handled by error middleware, NOT here.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { LANGUAGE_HEADER_NAME } from '../language/cookie-config';
|
|
7
|
+
import { isPublicReadEligible, VARIANCE_AXIS_PARAMS } from './cache-eligibility';
|
|
9
8
|
const DEFAULT_LOG = (event) => {
|
|
10
9
|
// Header on a separate line so the data dump below renders untruncated in
|
|
11
10
|
// both terminals and DevTools.
|
|
@@ -153,21 +152,6 @@ function extractUserErrors(data) {
|
|
|
153
152
|
}
|
|
154
153
|
return flat;
|
|
155
154
|
}
|
|
156
|
-
/** Backend signal that a `documentId` is not in the allowlist → replay as POST. */
|
|
157
|
-
const TRUSTED_DOC_NOT_FOUND = 'TRUSTED_DOCUMENT_NOT_FOUND';
|
|
158
|
-
/** Shop-routing header the client always sends; lifted into the GET cache key below. */
|
|
159
|
-
const SHOP_SLUG_HEADER = 'X-Shop-Slug';
|
|
160
|
-
/**
|
|
161
|
-
* Variance axes mirrored from request headers into the GET query string. A shared
|
|
162
|
-
* edge cache keys on the URL, not on request headers — without these in the URL two
|
|
163
|
-
* shops / currencies / languages would collide on one cache entry. The backend still
|
|
164
|
-
* reads the values from the headers; the URL copy exists only to split the cache key.
|
|
165
|
-
*/
|
|
166
|
-
const AXIS_URL_PARAMS = [
|
|
167
|
-
[SHOP_SLUG_HEADER, '__shop'],
|
|
168
|
-
[CURRENCY_HEADER_NAME, '__currency'],
|
|
169
|
-
[LANGUAGE_HEADER_NAME, '__language'],
|
|
170
|
-
];
|
|
171
155
|
/**
|
|
172
156
|
* Default POST transport — the full query travels in the body and cookies are sent.
|
|
173
157
|
* Every request that is not a cacheable public read uses this (unchanged contract).
|
|
@@ -215,16 +199,28 @@ function buildTrustedGetRequest(endpoint, request) {
|
|
|
215
199
|
if (request.variables && Object.keys(request.variables).length > 0) {
|
|
216
200
|
url.searchParams.set('variables', JSON.stringify(request.variables));
|
|
217
201
|
}
|
|
218
|
-
for
|
|
219
|
-
|
|
202
|
+
// Case-insensitive lookup for the variance axes: request header casing is not guaranteed
|
|
203
|
+
// (middleware/proxies may normalise), and the identity gate is already case-insensitive.
|
|
204
|
+
// A casing mismatch here would drop an axis from the URL — and the shared edge keys on the
|
|
205
|
+
// URL only, so two shops/currencies/languages would collapse onto one entry (cross-tenant /
|
|
206
|
+
// wrong-variant leak). Build one lowercased view, then look up each axis by lowercase name.
|
|
207
|
+
const lowerHeaders = {};
|
|
208
|
+
for (const [name, value] of Object.entries(request.headers))
|
|
209
|
+
lowerHeaders[name.toLowerCase()] = value;
|
|
210
|
+
for (const [headerName, param] of VARIANCE_AXIS_PARAMS) {
|
|
211
|
+
const value = lowerHeaders[headerName.toLowerCase()];
|
|
220
212
|
if (value)
|
|
221
213
|
url.searchParams.set(param, value);
|
|
222
214
|
}
|
|
223
215
|
const headers = { ...request.headers };
|
|
224
|
-
// Public read = customer-agnostic →
|
|
225
|
-
//
|
|
216
|
+
// Public read = customer-agnostic → the GET must carry NO credentials, so the cached
|
|
217
|
+
// response can be served to any visitor. `credentials: 'omit'` drops the browser cookie jar,
|
|
218
|
+
// but NOT an explicitly-set Cookie header (e.g. forwarded on SSR) — strip it (and the bearer
|
|
219
|
+
// token) by hand so the request is truly credential-less and can never personalise the body.
|
|
226
220
|
delete headers.Authorization;
|
|
227
221
|
delete headers.authorization;
|
|
222
|
+
delete headers.Cookie;
|
|
223
|
+
delete headers.cookie;
|
|
228
224
|
headers.Accept = 'application/json';
|
|
229
225
|
// Force a CORS preflight so the backend CSRF prevention admits the GET.
|
|
230
226
|
headers['apollo-require-preflight'] = 'true';
|
|
@@ -236,11 +232,16 @@ function buildTrustedGetRequest(endpoint, request) {
|
|
|
236
232
|
export function createExecute(config) {
|
|
237
233
|
const { endpoint, fetch: fetchFn, debug } = config;
|
|
238
234
|
return async function execute(request) {
|
|
239
|
-
const { query, variables, headers, operationName,
|
|
235
|
+
const { query, variables, headers, operationName, cache, documentId } = request;
|
|
240
236
|
const startedAt = debug?.timing ? Date.now() : 0;
|
|
241
|
-
//
|
|
242
|
-
// shared edge cache can serve
|
|
243
|
-
|
|
237
|
+
// Platform default: a non-mutation persisted read with no identity goes over the
|
|
238
|
+
// shared cacheable GET so an edge cache can serve it — no per-query `cacheLong()`
|
|
239
|
+
// opt-in needed. The caller can still opt OUT with `cachePrivate()` / `cacheNone()`
|
|
240
|
+
// for a persisted read it wants kept per-user. Whether the result is ACTUALLY
|
|
241
|
+
// cached is the backend's call (it classifies the operation and emits
|
|
242
|
+
// `Cache-Control`); the SDK never classifies operations here — adding a PUBLIC_READ
|
|
243
|
+
// list would duplicate the backend registry and drift.
|
|
244
|
+
const useTrustedGet = isPublicReadEligible(request) && cache?.mode !== 'private' && cache?.mode !== 'no-store';
|
|
244
245
|
if (debug) {
|
|
245
246
|
const requestData = { variables, method: useTrustedGet ? 'GET' : 'POST' };
|
|
246
247
|
if (useTrustedGet)
|
|
@@ -253,16 +254,27 @@ export function createExecute(config) {
|
|
|
253
254
|
}
|
|
254
255
|
let response;
|
|
255
256
|
if (useTrustedGet) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
257
|
+
// The cacheable GET is only an optimization for a shared edge cache — it must NEVER
|
|
258
|
+
// break a read. Use it only on a clean 2xx; degrade to a full-query POST on ANY other
|
|
259
|
+
// outcome: the GET transport throwing (network / CORS / aborted), an opaque or blocked
|
|
260
|
+
// response (`status` 0, `ok` false), an unknown documentId surfaced as 400, a 5xx, etc.
|
|
261
|
+
// The full query travels in the POST body, so the backend always resolves it.
|
|
262
|
+
let getResponse = null;
|
|
263
|
+
try {
|
|
264
|
+
const [getUrl, getInit] = buildTrustedGetRequest(endpoint, request);
|
|
265
|
+
const candidate = await fetchFn(getUrl, getInit);
|
|
266
|
+
if (candidate.ok)
|
|
267
|
+
getResponse = candidate;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// network / CORS / aborted GET — fall through to the safe POST below
|
|
271
|
+
}
|
|
272
|
+
if (getResponse) {
|
|
273
|
+
response = getResponse;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const [postUrl, postInit] = buildPostRequest(endpoint, request);
|
|
277
|
+
response = await fetchFn(postUrl, postInit);
|
|
266
278
|
}
|
|
267
279
|
}
|
|
268
280
|
else {
|
|
@@ -3,13 +3,39 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Framework-agnostic — no React, no Zustand, 0 runtime dependencies.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Structural contract for a typed GraphQL document accepted by `query` / `mutate`.
|
|
8
|
+
*
|
|
9
|
+
* A document carries its result type (`TResult`) and variables type (`TVariables`)
|
|
10
|
+
* at the type level via the phantom `__apiType` brand (never called at runtime) and
|
|
11
|
+
* stringifies to the GraphQL operation text. This is the parameter type the client
|
|
12
|
+
* exposes — deliberately a *structural interface*, not a concrete class, so it is
|
|
13
|
+
* satisfied by:
|
|
14
|
+
* - the SDK's own {@link TypedDocumentString} class, and
|
|
15
|
+
* - the class graphql-codegen's client-preset emits in a consumer project
|
|
16
|
+
* (which declares a `private` field and would otherwise be nominally
|
|
17
|
+
* incompatible with a class-typed parameter — private members only block
|
|
18
|
+
* class-to-class assignment, never class-to-interface).
|
|
19
|
+
*
|
|
20
|
+
* Inference is preserved either way: `client.query(doc, vars)` infers both the
|
|
21
|
+
* result and the variables shape from the document.
|
|
22
|
+
*/
|
|
23
|
+
export interface TypedDocumentLike<TResult = unknown, TVariables = unknown> {
|
|
24
|
+
/** Type-level brand carrying result & variable types — never called at runtime. */
|
|
25
|
+
__apiType?: (variables: TVariables) => TResult;
|
|
26
|
+
/** Stringifies to the GraphQL operation text. */
|
|
27
|
+
toString(): string;
|
|
28
|
+
}
|
|
6
29
|
/**
|
|
7
30
|
* A branded string carrying result & variable types.
|
|
8
31
|
* Produced by graphql-codegen client-preset with `documentMode: 'string'`.
|
|
9
32
|
*
|
|
10
33
|
* SDK also accepts plain strings — TypedDocumentString is purely for DX.
|
|
34
|
+
*
|
|
35
|
+
* Structurally satisfies {@link TypedDocumentLike}, the type accepted by
|
|
36
|
+
* `query` / `mutate`.
|
|
11
37
|
*/
|
|
12
|
-
export declare class TypedDocumentString<TResult = unknown, TVariables = unknown> extends String {
|
|
38
|
+
export declare class TypedDocumentString<TResult = unknown, TVariables = unknown> extends String implements TypedDocumentLike<TResult, TVariables> {
|
|
13
39
|
__meta__?: {
|
|
14
40
|
hash: string;
|
|
15
41
|
} | undefined;
|
|
@@ -211,15 +237,16 @@ export interface StorefrontClient {
|
|
|
211
237
|
/**
|
|
212
238
|
* Execute a typed GraphQL query.
|
|
213
239
|
*
|
|
214
|
-
* Accepts
|
|
240
|
+
* Accepts any typed document (the SDK's `TypedDocumentString` or a
|
|
241
|
+
* graphql-codegen client-preset document) or a plain string.
|
|
215
242
|
*/
|
|
216
|
-
query<T = unknown, V = Record<string, unknown>>(document:
|
|
243
|
+
query<T = unknown, V = Record<string, unknown>>(document: TypedDocumentLike<T, V> | string, variables?: V, cache?: CacheStrategy): Promise<T>;
|
|
217
244
|
/**
|
|
218
245
|
* Execute a typed GraphQL mutation.
|
|
219
246
|
*
|
|
220
247
|
* Mutations are never cached and never retried by retry middleware.
|
|
221
248
|
*/
|
|
222
|
-
mutate<T = unknown, V = Record<string, unknown>>(document:
|
|
249
|
+
mutate<T = unknown, V = Record<string, unknown>>(document: TypedDocumentLike<T, V> | string, variables?: V): Promise<T>;
|
|
223
250
|
/**
|
|
224
251
|
* Add middleware to the pipeline (imperative API).
|
|
225
252
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/client/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/client/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,iBAAiB,CAAC,OAAO,GAAG,OAAO,EAAE,UAAU,GAAG,OAAO;IACxE,mFAAmF;IACnF,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC;IAC/C,iDAAiD;IACjD,QAAQ,IAAI,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,mBAAmB,CAAC,OAAO,GAAG,OAAO,EAAE,UAAU,GAAG,OAAO,CACtE,SAAQ,MACR,YAAW,iBAAiB,CAAC,OAAO,EAAE,UAAU,CAAC;IAKf,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;IAH7D,+CAA+C;IAC/C,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC;gBAEnC,KAAK,EAAE,MAAM,EAAS,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,YAAA;IAIpD,QAAQ,IAAI,MAAM;CAG5B;AAMD,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,wDAAwD;IACxD,UAAU,EAAE,OAAO,CAAC;IACpB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,2BAA2B;IAC3B,IAAI,EAAE,CAAC,CAAC;IACR,8BAA8B;IAC9B,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC5B,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAMD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAE9E;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAMhG,MAAM,WAAW,YAAY;IAC3B,yBAAyB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iBAAiB;IACjB,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;IACxC,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC;AAMzC;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,2GAA2G;IAC3G,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wGAAwG;IACxG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gKAAgK;IAChK,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sIAAsI;IACtI,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2JAA2J;IAC3J,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,kBAAkB,GAAG,eAAe,CAAC;CACzD;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wGAAwG;IACxG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;IACzC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,0BAA0B;IAC1B,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;IAC1B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,YAAY,CAAC;CAC5C;AAMD,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5C,QAAQ,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,EAC1C,SAAS,CAAC,EAAE,CAAC,EACb,KAAK,CAAC,EAAE,aAAa,GACpB,OAAO,CAAC,CAAC,CAAC,CAAC;IAEd;;;;OAIG;IACH,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7C,QAAQ,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,EAC1C,SAAS,CAAC,EAAE,CAAC,GACZ,OAAO,CAAC,CAAC,CAAC,CAAC;IAEd;;;;OAIG;IACH,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;CACnC"}
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Framework-agnostic — no React, no Zustand, 0 runtime dependencies.
|
|
5
5
|
*/
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// TypedDocumentString (graphql-codegen client-preset pattern)
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
6
|
/**
|
|
10
7
|
* A branded string carrying result & variable types.
|
|
11
8
|
* Produced by graphql-codegen client-preset with `documentMode: 'string'`.
|
|
12
9
|
*
|
|
13
10
|
* SDK also accepts plain strings — TypedDocumentString is purely for DX.
|
|
11
|
+
*
|
|
12
|
+
* Structurally satisfies {@link TypedDocumentLike}, the type accepted by
|
|
13
|
+
* `query` / `mutate`.
|
|
14
14
|
*/
|
|
15
15
|
export class TypedDocumentString extends String {
|
|
16
16
|
__meta__;
|
|
@@ -551,7 +551,7 @@ export type Cart = Node & {
|
|
|
551
551
|
selectedShippingMethod?: Maybe<CartShippingMethod>;
|
|
552
552
|
/** Shipping address attached to the cart via `cartSetShippingAddress`. Null until set. */
|
|
553
553
|
shippingAddress?: Maybe<MailingAddress>;
|
|
554
|
-
/** Lifecycle status — `ACTIVE`
|
|
554
|
+
/** Lifecycle status — `ACTIVE` / `RECOVERED` are editable; `ABANDONED` is a recovery flag a deliberate buyer action revives in place; `CONVERTED` / `EXPIRED` are terminal. Check this on SSR before rendering the checkout form: a `CONVERTED` cart should redirect (typically to the order confirmation when `completedOrder` is populated) instead of presenting a form whose first mutation fails with `CartErrorCode.ALREADY_COMPLETED`. */
|
|
555
555
|
status: CartStatus;
|
|
556
556
|
/** Sum of `quantity` across all lines — the badge number for the cart icon. */
|
|
557
557
|
totalQuantity: Scalars['Int']['output'];
|