@doswiftly/storefront-sdk 4.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.
- package/README.md +430 -0
- package/dist/__tests__/unit/test-helpers.d.ts +46 -0
- package/dist/__tests__/unit/test-helpers.d.ts.map +1 -0
- package/dist/__tests__/unit/test-helpers.js +72 -0
- package/dist/core/auth/auth-client.d.ts +46 -0
- package/dist/core/auth/auth-client.d.ts.map +1 -0
- package/dist/core/auth/auth-client.js +82 -0
- package/dist/core/auth/cookie-config.d.ts +18 -0
- package/dist/core/auth/cookie-config.d.ts.map +1 -0
- package/dist/core/auth/cookie-config.js +18 -0
- package/dist/core/auth/handlers.d.ts +32 -0
- package/dist/core/auth/handlers.d.ts.map +1 -0
- package/dist/core/auth/handlers.js +127 -0
- package/dist/core/auth/routes.d.ts +21 -0
- package/dist/core/auth/routes.d.ts.map +1 -0
- package/dist/core/auth/routes.js +14 -0
- package/dist/core/auth/token-client.d.ts +26 -0
- package/dist/core/auth/token-client.d.ts.map +1 -0
- package/dist/core/auth/token-client.js +42 -0
- package/dist/core/auth/types.d.ts +53 -0
- package/dist/core/auth/types.d.ts.map +1 -0
- package/dist/core/auth/types.js +4 -0
- package/dist/core/cache.d.ts +54 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +82 -0
- package/dist/core/cart/cart-client.d.ts +57 -0
- package/dist/core/cart/cart-client.d.ts.map +1 -0
- package/dist/core/cart/cart-client.js +89 -0
- package/dist/core/cart/types.d.ts +110 -0
- package/dist/core/cart/types.d.ts.map +1 -0
- package/dist/core/cart/types.js +6 -0
- package/dist/core/client/compose.d.ts +9 -0
- package/dist/core/client/compose.d.ts.map +1 -0
- package/dist/core/client/compose.js +9 -0
- package/dist/core/client/create-client.d.ts +15 -0
- package/dist/core/client/create-client.d.ts.map +1 -0
- package/dist/core/client/create-client.js +85 -0
- package/dist/core/client/dedupe.d.ts +7 -0
- package/dist/core/client/dedupe.d.ts.map +1 -0
- package/dist/core/client/dedupe.js +16 -0
- package/dist/core/client/execute.d.ts +20 -0
- package/dist/core/client/execute.d.ts.map +1 -0
- package/dist/core/client/execute.js +48 -0
- package/dist/core/client/hash.d.ts +7 -0
- package/dist/core/client/hash.d.ts.map +1 -0
- package/dist/core/client/hash.js +21 -0
- package/dist/core/client/operation-name.d.ts +7 -0
- package/dist/core/client/operation-name.d.ts.map +1 -0
- package/dist/core/client/operation-name.js +10 -0
- package/dist/core/client/types.d.ts +126 -0
- package/dist/core/client/types.d.ts.map +1 -0
- package/dist/core/client/types.js +26 -0
- package/dist/core/errors.d.ts +43 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +43 -0
- package/dist/core/format.d.ts +92 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +216 -0
- package/dist/core/helpers/assert-no-user-errors.d.ts +10 -0
- package/dist/core/helpers/assert-no-user-errors.d.ts.map +1 -0
- package/dist/core/helpers/assert-no-user-errors.js +16 -0
- package/dist/core/helpers/normalize-connection.d.ts +36 -0
- package/dist/core/helpers/normalize-connection.d.ts.map +1 -0
- package/dist/core/helpers/normalize-connection.js +21 -0
- package/dist/core/helpers/sanitize-html.d.ts +8 -0
- package/dist/core/helpers/sanitize-html.d.ts.map +1 -0
- package/dist/core/helpers/sanitize-html.js +35 -0
- package/dist/core/index.d.ts +59 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +68 -0
- package/dist/core/middleware/auth.d.ts +16 -0
- package/dist/core/middleware/auth.d.ts.map +1 -0
- package/dist/core/middleware/auth.js +22 -0
- package/dist/core/middleware/currency.d.ts +15 -0
- package/dist/core/middleware/currency.d.ts.map +1 -0
- package/dist/core/middleware/currency.js +21 -0
- package/dist/core/middleware/errors.d.ts +24 -0
- package/dist/core/middleware/errors.d.ts.map +1 -0
- package/dist/core/middleware/errors.js +77 -0
- package/dist/core/middleware/retry.d.ts +22 -0
- package/dist/core/middleware/retry.d.ts.map +1 -0
- package/dist/core/middleware/retry.js +58 -0
- package/dist/core/middleware/timeout.d.ts +19 -0
- package/dist/core/middleware/timeout.d.ts.map +1 -0
- package/dist/core/middleware/timeout.js +51 -0
- package/dist/core/operations/auth.d.ts +11 -0
- package/dist/core/operations/auth.d.ts.map +1 -0
- package/dist/core/operations/auth.js +112 -0
- package/dist/core/operations/cart.d.ts +15 -0
- package/dist/core/operations/cart.d.ts.map +1 -0
- package/dist/core/operations/cart.js +169 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/react/cookies.d.ts +28 -0
- package/dist/react/cookies.d.ts.map +1 -0
- package/dist/react/cookies.js +49 -0
- package/dist/react/helpers/create-store-context.d.ts +37 -0
- package/dist/react/helpers/create-store-context.d.ts.map +1 -0
- package/dist/react/helpers/create-store-context.js +47 -0
- package/dist/react/hooks/use-auth.d.ts +65 -0
- package/dist/react/hooks/use-auth.d.ts.map +1 -0
- package/dist/react/hooks/use-auth.js +168 -0
- package/dist/react/hooks/use-cart-manager.d.ts +30 -0
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -0
- package/dist/react/hooks/use-cart-manager.js +223 -0
- package/dist/react/hooks/use-currency.d.ts +11 -0
- package/dist/react/hooks/use-currency.d.ts.map +1 -0
- package/dist/react/hooks/use-currency.js +19 -0
- package/dist/react/hooks/use-debounced-value.d.ts +15 -0
- package/dist/react/hooks/use-debounced-value.d.ts.map +1 -0
- package/dist/react/hooks/use-debounced-value.js +25 -0
- package/dist/react/hooks/use-hydrated.d.ts +9 -0
- package/dist/react/hooks/use-hydrated.d.ts.map +1 -0
- package/dist/react/hooks/use-hydrated.js +14 -0
- package/dist/react/hooks/use-storefront-client.d.ts +6 -0
- package/dist/react/hooks/use-storefront-client.d.ts.map +1 -0
- package/dist/react/hooks/use-storefront-client.js +8 -0
- package/dist/react/index.d.ts +30 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +34 -0
- package/dist/react/providers/currency-provider.d.ts +14 -0
- package/dist/react/providers/currency-provider.d.ts.map +1 -0
- package/dist/react/providers/currency-provider.js +20 -0
- package/dist/react/providers/storefront-client-provider.d.ts +33 -0
- package/dist/react/providers/storefront-client-provider.d.ts.map +1 -0
- package/dist/react/providers/storefront-client-provider.js +57 -0
- package/dist/react/providers/storefront-provider.d.ts +42 -0
- package/dist/react/providers/storefront-provider.d.ts.map +1 -0
- package/dist/react/providers/storefront-provider.js +40 -0
- package/dist/react/server/get-storefront-client.d.ts +42 -0
- package/dist/react/server/get-storefront-client.d.ts.map +1 -0
- package/dist/react/server/get-storefront-client.js +44 -0
- package/dist/react/server/index.d.ts +2 -0
- package/dist/react/server/index.d.ts.map +1 -0
- package/dist/react/server/index.js +1 -0
- package/dist/react/stores/auth.store.d.ts +48 -0
- package/dist/react/stores/auth.store.d.ts.map +1 -0
- package/dist/react/stores/auth.store.js +67 -0
- package/dist/react/stores/currency.store.d.ts +29 -0
- package/dist/react/stores/currency.store.d.ts.map +1 -0
- package/dist/react/stores/currency.store.js +76 -0
- package/dist/react/stores/index.d.ts +8 -0
- package/dist/react/stores/index.d.ts.map +1 -0
- package/dist/react/stores/index.js +10 -0
- package/dist/react/stores/store-context.d.ts +27 -0
- package/dist/react/stores/store-context.d.ts.map +1 -0
- package/dist/react/stores/store-context.js +62 -0
- package/package.json +71 -0
- package/src/__tests__/contract/storefront-api.contract.test.ts +450 -0
- package/src/__tests__/unit/auth-client.test.ts +210 -0
- package/src/__tests__/unit/cart-client.test.ts +233 -0
- package/src/__tests__/unit/create-client.test.ts +356 -0
- package/src/__tests__/unit/helpers.test.ts +377 -0
- package/src/__tests__/unit/middleware.test.ts +374 -0
- package/src/__tests__/unit/test-helpers.ts +103 -0
- package/src/core/auth/auth-client.ts +123 -0
- package/src/core/auth/cookie-config.ts +23 -0
- package/src/core/auth/handlers.ts +168 -0
- package/src/core/auth/routes.ts +26 -0
- package/src/core/auth/token-client.ts +51 -0
- package/src/core/auth/types.ts +54 -0
- package/src/core/cache.ts +102 -0
- package/src/core/cart/cart-client.ts +150 -0
- package/src/core/cart/types.ts +104 -0
- package/src/core/client/compose.ts +15 -0
- package/src/core/client/create-client.ts +129 -0
- package/src/core/client/dedupe.ts +19 -0
- package/src/core/client/execute.ts +70 -0
- package/src/core/client/hash.ts +21 -0
- package/src/core/client/operation-name.ts +12 -0
- package/src/core/client/types.ts +171 -0
- package/src/core/errors.ts +67 -0
- package/src/core/format.ts +254 -0
- package/src/core/helpers/assert-no-user-errors.ts +21 -0
- package/src/core/helpers/normalize-connection.ts +48 -0
- package/src/core/helpers/sanitize-html.ts +42 -0
- package/src/core/index.ts +148 -0
- package/src/core/middleware/auth.ts +27 -0
- package/src/core/middleware/currency.ts +26 -0
- package/src/core/middleware/errors.ts +86 -0
- package/src/core/middleware/retry.ts +75 -0
- package/src/core/middleware/timeout.ts +61 -0
- package/src/core/operations/auth.ts +123 -0
- package/src/core/operations/cart.ts +185 -0
- package/src/index.ts +25 -0
- package/src/react/cookies.ts +54 -0
- package/src/react/helpers/create-store-context.ts +56 -0
- package/src/react/hooks/use-auth.ts +218 -0
- package/src/react/hooks/use-cart-manager.ts +236 -0
- package/src/react/hooks/use-currency.ts +23 -0
- package/src/react/hooks/use-debounced-value.ts +30 -0
- package/src/react/hooks/use-hydrated.ts +20 -0
- package/src/react/hooks/use-storefront-client.ts +12 -0
- package/src/react/index.ts +45 -0
- package/src/react/providers/currency-provider.tsx +30 -0
- package/src/react/providers/storefront-client-provider.tsx +90 -0
- package/src/react/providers/storefront-provider.tsx +71 -0
- package/src/react/server/get-storefront-client.ts +60 -0
- package/src/react/server/index.ts +1 -0
- package/src/react/stores/auth.store.ts +112 -0
- package/src/react/stores/currency.store.ts +113 -0
- package/src/react/stores/index.ts +17 -0
- package/src/react/stores/store-context.tsx +82 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createStorefrontClient — transport factory (Hydrogen pattern).
|
|
3
|
+
*
|
|
4
|
+
* Creates a framework-agnostic GraphQL client with:
|
|
5
|
+
* - Middleware pipeline (composable, lazy)
|
|
6
|
+
* - Request deduplication (same-tick queries)
|
|
7
|
+
* - TypedDocumentString support (Saleor/client-preset)
|
|
8
|
+
* - Plain string queries (for custom operations)
|
|
9
|
+
* - Debug mode
|
|
10
|
+
*
|
|
11
|
+
* 0 runtime dependencies.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
StorefrontClientConfig,
|
|
16
|
+
StorefrontClient,
|
|
17
|
+
Middleware,
|
|
18
|
+
CacheStrategy,
|
|
19
|
+
TypedDocumentString,
|
|
20
|
+
ExecuteFn,
|
|
21
|
+
} from './types';
|
|
22
|
+
import { createExecute } from './execute';
|
|
23
|
+
import { compose } from './compose';
|
|
24
|
+
import { dedupe } from './dedupe';
|
|
25
|
+
import { hashQuery } from './hash';
|
|
26
|
+
import { getOperationName } from './operation-name';
|
|
27
|
+
|
|
28
|
+
export function createStorefrontClient(config: StorefrontClientConfig): StorefrontClient {
|
|
29
|
+
const {
|
|
30
|
+
apiUrl,
|
|
31
|
+
shopSlug,
|
|
32
|
+
defaultHeaders = {},
|
|
33
|
+
middleware: initialMiddleware = [],
|
|
34
|
+
fetch: customFetch = globalThis.fetch,
|
|
35
|
+
debug = false,
|
|
36
|
+
} = config;
|
|
37
|
+
|
|
38
|
+
const endpoint = `${apiUrl.replace(/\/$/, '')}/storefront/graphql`;
|
|
39
|
+
|
|
40
|
+
// Mutable middleware list — use() adds to it, invalidates compiled pipeline
|
|
41
|
+
const middlewares: Middleware[] = [...initialMiddleware];
|
|
42
|
+
let compiledPipeline: ExecuteFn | null = null;
|
|
43
|
+
|
|
44
|
+
// Create the innermost execute function (native fetch)
|
|
45
|
+
const innerExecute = createExecute({ endpoint, fetch: customFetch, debug });
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get or build the compiled middleware pipeline (lazy).
|
|
49
|
+
* Rebuilt on first call and after use() is called.
|
|
50
|
+
*/
|
|
51
|
+
function getPipeline(): ExecuteFn {
|
|
52
|
+
if (!compiledPipeline) {
|
|
53
|
+
compiledPipeline = middlewares.length > 0
|
|
54
|
+
? compose(middlewares, innerExecute)
|
|
55
|
+
: innerExecute;
|
|
56
|
+
}
|
|
57
|
+
return compiledPipeline;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve query string from TypedDocumentString or plain string.
|
|
62
|
+
*/
|
|
63
|
+
function resolveQuery(document: TypedDocumentString | string): string {
|
|
64
|
+
return typeof document === 'string' ? document : document.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Core request execution — shared by query() and mutate().
|
|
69
|
+
*/
|
|
70
|
+
async function request<T>(
|
|
71
|
+
document: TypedDocumentString<T, unknown> | string,
|
|
72
|
+
variables?: Record<string, unknown>,
|
|
73
|
+
isMutation: boolean = false,
|
|
74
|
+
cache?: CacheStrategy,
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
const query = resolveQuery(document);
|
|
77
|
+
const operationName = getOperationName(query);
|
|
78
|
+
const pipeline = getPipeline();
|
|
79
|
+
|
|
80
|
+
const headers: Record<string, string> = {
|
|
81
|
+
'X-Shop-Slug': shopSlug,
|
|
82
|
+
...defaultHeaders,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (operationName !== 'anonymous') {
|
|
86
|
+
headers['X-Operation-Name'] = operationName;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const graphqlRequest = {
|
|
90
|
+
query,
|
|
91
|
+
variables,
|
|
92
|
+
operationName,
|
|
93
|
+
headers,
|
|
94
|
+
isMutation,
|
|
95
|
+
cache,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Dedupe queries (not mutations) in the same tick
|
|
99
|
+
const executeFn = () => pipeline(graphqlRequest);
|
|
100
|
+
|
|
101
|
+
const response = isMutation
|
|
102
|
+
? await executeFn()
|
|
103
|
+
: await dedupe(hashQuery(query, variables), executeFn);
|
|
104
|
+
|
|
105
|
+
return response.data as T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
query<T, V>(
|
|
110
|
+
document: TypedDocumentString<T, V> | string,
|
|
111
|
+
variables?: V,
|
|
112
|
+
cache?: CacheStrategy,
|
|
113
|
+
): Promise<T> {
|
|
114
|
+
return request<T>(document as TypedDocumentString<T, unknown>, variables as Record<string, unknown>, false, cache);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
mutate<T, V>(
|
|
118
|
+
document: TypedDocumentString<T, V> | string,
|
|
119
|
+
variables?: V,
|
|
120
|
+
): Promise<T> {
|
|
121
|
+
return request<T>(document as TypedDocumentString<T, unknown>, variables as Record<string, unknown>, true);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
use(middleware: Middleware): void {
|
|
125
|
+
middlewares.push(middleware);
|
|
126
|
+
compiledPipeline = null; // Invalidate — rebuild on next request
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request deduplication — identical queries in the same tick share one fetch.
|
|
3
|
+
*
|
|
4
|
+
* Key = hashQuery(query, variables). Only applies to queries, not mutations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const pending = new Map<string, Promise<unknown>>();
|
|
8
|
+
|
|
9
|
+
export function dedupe<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
10
|
+
const existing = pending.get(key);
|
|
11
|
+
if (existing) return existing as Promise<T>;
|
|
12
|
+
|
|
13
|
+
const promise = fn().finally(() => {
|
|
14
|
+
pending.delete(key);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
pending.set(key, promise);
|
|
18
|
+
return promise;
|
|
19
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native fetch transport — 0 runtime dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Sends GraphQL POST request, parses response, returns typed GraphQLResponse.
|
|
5
|
+
* Error normalization is handled by error middleware, NOT here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GraphQLRequest, GraphQLResponse, GraphQLErrorInfo } from './types';
|
|
9
|
+
|
|
10
|
+
export interface ExecuteConfig {
|
|
11
|
+
/** GraphQL endpoint URL */
|
|
12
|
+
endpoint: string;
|
|
13
|
+
/** Custom fetch implementation */
|
|
14
|
+
fetch: typeof globalThis.fetch;
|
|
15
|
+
/** Enable debug logging */
|
|
16
|
+
debug: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create the innermost execute function for the middleware pipeline.
|
|
21
|
+
*/
|
|
22
|
+
export function createExecute(config: ExecuteConfig) {
|
|
23
|
+
const { endpoint, fetch: fetchFn, debug } = config;
|
|
24
|
+
|
|
25
|
+
return async function execute(request: GraphQLRequest): Promise<GraphQLResponse> {
|
|
26
|
+
const { query, variables, headers, signal, operationName } = request;
|
|
27
|
+
|
|
28
|
+
if (debug) {
|
|
29
|
+
console.log('[StorefrontSDK]', operationName ?? 'request', { variables });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const body: Record<string, unknown> = { query };
|
|
33
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
34
|
+
body.variables = variables;
|
|
35
|
+
}
|
|
36
|
+
if (operationName) {
|
|
37
|
+
body.operationName = operationName;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = await fetchFn(endpoint, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
Accept: 'application/json',
|
|
45
|
+
...headers,
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
signal,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const json = await response.json() as {
|
|
52
|
+
data?: unknown;
|
|
53
|
+
errors?: GraphQLErrorInfo[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (debug) {
|
|
57
|
+
console.log('[StorefrontSDK]', operationName ?? 'response', {
|
|
58
|
+
status: response.status,
|
|
59
|
+
hasErrors: !!json.errors?.length,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
data: json.data as never,
|
|
65
|
+
errors: json.errors,
|
|
66
|
+
status: response.status,
|
|
67
|
+
headers: response.headers,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable query hashing for request deduplication.
|
|
3
|
+
*
|
|
4
|
+
* Sorts object keys so `{a:1,b:2}` and `{b:2,a:1}` produce the same hash.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function stableStringify(value: unknown): string {
|
|
8
|
+
if (value === null || value === undefined) return '';
|
|
9
|
+
if (typeof value !== 'object') return JSON.stringify(value);
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return '[' + value.map(stableStringify).join(',') + ']';
|
|
12
|
+
}
|
|
13
|
+
const keys = Object.keys(value as Record<string, unknown>).sort();
|
|
14
|
+
return '{' + keys.map(k => JSON.stringify(k) + ':' + stableStringify((value as Record<string, unknown>)[k])).join(',') + '}';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hashQuery(query: string, variables?: Record<string, unknown>): string {
|
|
18
|
+
const queryPart = query.trim();
|
|
19
|
+
const varsPart = variables ? stableStringify(variables) : '';
|
|
20
|
+
return queryPart + '::' + varsPart;
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-extract operation name from a GraphQL query string.
|
|
3
|
+
*
|
|
4
|
+
* Used for: X-Operation-Name header (debugging), cache tags, logging.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const OPERATION_RE = /(?:query|mutation|subscription)\s+(\w+)/;
|
|
8
|
+
|
|
9
|
+
export function getOperationName(query: string): string {
|
|
10
|
+
const match = query.match(OPERATION_RE);
|
|
11
|
+
return match?.[1] ?? 'anonymous';
|
|
12
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for Storefront SDK transport layer.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic — no React, no Zustand, 0 runtime dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// TypedDocumentString (Saleor / client-preset pattern)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A branded string carrying result & variable types.
|
|
13
|
+
* Produced by graphql-codegen client-preset with `documentMode: 'string'`.
|
|
14
|
+
*
|
|
15
|
+
* SDK also accepts plain strings — TypedDocumentString is purely for DX.
|
|
16
|
+
*/
|
|
17
|
+
export class TypedDocumentString<TResult = unknown, TVariables = unknown> extends String {
|
|
18
|
+
/** Type-level brand — never used at runtime */
|
|
19
|
+
__apiType?: (variables: TVariables) => TResult;
|
|
20
|
+
|
|
21
|
+
constructor(value: string, public __meta__?: { hash: string }) {
|
|
22
|
+
super(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override toString(): string {
|
|
26
|
+
return this.valueOf();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// GraphQL request / response
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface GraphQLRequest {
|
|
35
|
+
/** GraphQL query or mutation string */
|
|
36
|
+
query: string;
|
|
37
|
+
/** Operation variables */
|
|
38
|
+
variables?: Record<string, unknown>;
|
|
39
|
+
/** Operation name (auto-extracted if omitted) */
|
|
40
|
+
operationName?: string;
|
|
41
|
+
/** Request headers (merged: default < middleware < request) */
|
|
42
|
+
headers: Record<string, string>;
|
|
43
|
+
/** AbortSignal for cancellation */
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
/** Whether this is a mutation (affects retry, cache) */
|
|
46
|
+
isMutation: boolean;
|
|
47
|
+
/** Cache strategy override */
|
|
48
|
+
cache?: CacheStrategy;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GraphQLResponse<T = unknown> {
|
|
52
|
+
/** Parsed response data */
|
|
53
|
+
data: T;
|
|
54
|
+
/** GraphQL errors (if any) */
|
|
55
|
+
errors?: GraphQLErrorInfo[];
|
|
56
|
+
/** HTTP status code */
|
|
57
|
+
status: number;
|
|
58
|
+
/** Raw response headers */
|
|
59
|
+
headers: Headers;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GraphQLErrorInfo {
|
|
63
|
+
message: string;
|
|
64
|
+
locations?: Array<{ line: number; column: number }>;
|
|
65
|
+
path?: Array<string | number>;
|
|
66
|
+
extensions?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// User errors (cart mutations, auth mutations, etc.)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export interface UserError {
|
|
74
|
+
message: string;
|
|
75
|
+
field?: string[];
|
|
76
|
+
code?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Middleware
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Execute function — the innermost (or next) handler in the pipeline.
|
|
85
|
+
*/
|
|
86
|
+
export type ExecuteFn = (request: GraphQLRequest) => Promise<GraphQLResponse>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Middleware function — receives the request and the next handler.
|
|
90
|
+
*
|
|
91
|
+
* Can modify request, modify response, short-circuit, retry, etc.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const authMiddleware: Middleware = (req, next) => {
|
|
96
|
+
* req.headers['Authorization'] = `Bearer ${getToken()}`;
|
|
97
|
+
* return next(req);
|
|
98
|
+
* };
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export type Middleware = (request: GraphQLRequest, next: ExecuteFn) => Promise<GraphQLResponse>;
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Cache strategy
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export interface CacheOptions {
|
|
108
|
+
/** Max age in seconds */
|
|
109
|
+
maxAge: number;
|
|
110
|
+
/** Stale-while-revalidate in seconds */
|
|
111
|
+
staleWhileRevalidate?: number;
|
|
112
|
+
/** Cache mode */
|
|
113
|
+
mode: 'public' | 'private' | 'no-store';
|
|
114
|
+
/** Cache tags (for Next.js revalidateTag) */
|
|
115
|
+
tags?: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type CacheStrategy = CacheOptions;
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Client config
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export interface StorefrontClientConfig {
|
|
125
|
+
/** GraphQL API URL (e.g. 'https://api.doswiftly.pl') */
|
|
126
|
+
apiUrl: string;
|
|
127
|
+
/** Shop slug for multi-tenant routing */
|
|
128
|
+
shopSlug: string;
|
|
129
|
+
/** Default headers for all requests */
|
|
130
|
+
defaultHeaders?: Record<string, string>;
|
|
131
|
+
/** Middleware pipeline */
|
|
132
|
+
middleware?: Middleware[];
|
|
133
|
+
/** Custom fetch implementation (polyfill, test mocks, edge) */
|
|
134
|
+
fetch?: typeof globalThis.fetch;
|
|
135
|
+
/** Enable debug logging (request/response in dev) */
|
|
136
|
+
debug?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Client interface
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
export interface StorefrontClient {
|
|
144
|
+
/**
|
|
145
|
+
* Execute a typed GraphQL query.
|
|
146
|
+
*
|
|
147
|
+
* Accepts TypedDocumentString (from codegen) or plain string.
|
|
148
|
+
*/
|
|
149
|
+
query<T = unknown, V = Record<string, unknown>>(
|
|
150
|
+
document: TypedDocumentString<T, V> | string,
|
|
151
|
+
variables?: V,
|
|
152
|
+
cache?: CacheStrategy,
|
|
153
|
+
): Promise<T>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute a typed GraphQL mutation.
|
|
157
|
+
*
|
|
158
|
+
* Mutations are never cached and never retried by retry middleware.
|
|
159
|
+
*/
|
|
160
|
+
mutate<T = unknown, V = Record<string, unknown>>(
|
|
161
|
+
document: TypedDocumentString<T, V> | string,
|
|
162
|
+
variables?: V,
|
|
163
|
+
): Promise<T>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Add middleware to the pipeline (imperative API).
|
|
167
|
+
*
|
|
168
|
+
* Invalidates the compiled pipeline — next request re-composes.
|
|
169
|
+
*/
|
|
170
|
+
use(middleware: Middleware): void;
|
|
171
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StorefrontError — unified error class for all SDK errors.
|
|
3
|
+
*
|
|
4
|
+
* Normalizes GraphQL errors, network errors, user errors, and timeouts
|
|
5
|
+
* into a single throwable type with structured data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GraphQLErrorInfo, UserError } from './client/types';
|
|
9
|
+
|
|
10
|
+
export interface StorefrontErrorOptions {
|
|
11
|
+
/** Error code for programmatic handling */
|
|
12
|
+
code: string;
|
|
13
|
+
/** Human-readable message */
|
|
14
|
+
message: string;
|
|
15
|
+
/** HTTP status code (0 for network errors) */
|
|
16
|
+
status?: number;
|
|
17
|
+
/** GraphQL-level errors from the response */
|
|
18
|
+
graphqlErrors?: GraphQLErrorInfo[];
|
|
19
|
+
/** User errors from mutations (field-level validation) */
|
|
20
|
+
userErrors?: UserError[];
|
|
21
|
+
/** Original error (if wrapping) */
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class StorefrontError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly graphqlErrors: GraphQLErrorInfo[];
|
|
29
|
+
readonly userErrors: UserError[];
|
|
30
|
+
|
|
31
|
+
constructor(options: StorefrontErrorOptions) {
|
|
32
|
+
super(options.message, { cause: options.cause });
|
|
33
|
+
this.name = 'StorefrontError';
|
|
34
|
+
this.code = options.code;
|
|
35
|
+
this.status = options.status ?? 0;
|
|
36
|
+
this.graphqlErrors = options.graphqlErrors ?? [];
|
|
37
|
+
this.userErrors = options.userErrors ?? [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True if this error contains user errors (field-level validation) */
|
|
41
|
+
get hasUserErrors(): boolean {
|
|
42
|
+
return this.userErrors.length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True if this is a network error (no HTTP response) */
|
|
46
|
+
get isNetworkError(): boolean {
|
|
47
|
+
return this.code === 'NETWORK_ERROR';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True if the request timed out */
|
|
51
|
+
get isTimeout(): boolean {
|
|
52
|
+
return this.code === 'TIMEOUT';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Error codes
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export const ErrorCodes = {
|
|
61
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
62
|
+
TIMEOUT: 'TIMEOUT',
|
|
63
|
+
HTTP_ERROR: 'HTTP_ERROR',
|
|
64
|
+
GRAPHQL_ERROR: 'GRAPHQL_ERROR',
|
|
65
|
+
USER_ERROR: 'USER_ERROR',
|
|
66
|
+
NO_DATA: 'NO_DATA',
|
|
67
|
+
} as const;
|