@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,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for formatting prices, dates, numbers.
|
|
5
|
+
* 0 runtime dependencies — works in Node.js, Edge, Deno, Bun.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// TYPES
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface PriceMoney {
|
|
13
|
+
amount: string;
|
|
14
|
+
currencyCode: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// CONSTANTS
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/** Currency symbols mapping */
|
|
22
|
+
export const CURRENCY_SYMBOLS: Record<string, string> = {
|
|
23
|
+
PLN: 'zł',
|
|
24
|
+
EUR: '€',
|
|
25
|
+
USD: '$',
|
|
26
|
+
GBP: '£',
|
|
27
|
+
CHF: 'CHF',
|
|
28
|
+
CZK: 'Kč',
|
|
29
|
+
SEK: 'kr',
|
|
30
|
+
NOK: 'kr',
|
|
31
|
+
DKK: 'kr',
|
|
32
|
+
JPY: '¥',
|
|
33
|
+
CNY: '¥',
|
|
34
|
+
AUD: 'A$',
|
|
35
|
+
CAD: 'C$',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Currency locale mapping for proper formatting */
|
|
39
|
+
export const CURRENCY_LOCALES: Record<string, string> = {
|
|
40
|
+
PLN: 'pl-PL',
|
|
41
|
+
EUR: 'de-DE',
|
|
42
|
+
USD: 'en-US',
|
|
43
|
+
GBP: 'en-GB',
|
|
44
|
+
CHF: 'de-CH',
|
|
45
|
+
CZK: 'cs-CZ',
|
|
46
|
+
SEK: 'sv-SE',
|
|
47
|
+
NOK: 'nb-NO',
|
|
48
|
+
DKK: 'da-DK',
|
|
49
|
+
JPY: 'ja-JP',
|
|
50
|
+
CNY: 'zh-CN',
|
|
51
|
+
AUD: 'en-AU',
|
|
52
|
+
CAD: 'en-CA',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// FORMATTER CACHE (avoids re-creating Intl objects on every call)
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
const numberFormatCache = new Map<string, Intl.NumberFormat>();
|
|
60
|
+
const dateFormatCache = new Map<string, Intl.DateTimeFormat>();
|
|
61
|
+
|
|
62
|
+
function getCurrencyFormatter(locale: string, currency: string): Intl.NumberFormat {
|
|
63
|
+
const key = `${locale}:${currency}`;
|
|
64
|
+
let fmt = numberFormatCache.get(key);
|
|
65
|
+
if (!fmt) {
|
|
66
|
+
fmt = new Intl.NumberFormat(locale, {
|
|
67
|
+
style: 'currency',
|
|
68
|
+
currency,
|
|
69
|
+
minimumFractionDigits: 2,
|
|
70
|
+
maximumFractionDigits: 2,
|
|
71
|
+
});
|
|
72
|
+
numberFormatCache.set(key, fmt);
|
|
73
|
+
}
|
|
74
|
+
return fmt;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getNumberFormatter(locale: string): Intl.NumberFormat {
|
|
78
|
+
let fmt = numberFormatCache.get(locale);
|
|
79
|
+
if (!fmt) {
|
|
80
|
+
fmt = new Intl.NumberFormat(locale);
|
|
81
|
+
numberFormatCache.set(locale, fmt);
|
|
82
|
+
}
|
|
83
|
+
return fmt;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getDateFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
|
|
87
|
+
const key = `${locale}:${JSON.stringify(options)}`;
|
|
88
|
+
let fmt = dateFormatCache.get(key);
|
|
89
|
+
if (!fmt) {
|
|
90
|
+
fmt = new Intl.DateTimeFormat(locale, options);
|
|
91
|
+
dateFormatCache.set(key, fmt);
|
|
92
|
+
}
|
|
93
|
+
return fmt;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// UTILITY FUNCTIONS
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get currency symbol
|
|
102
|
+
*/
|
|
103
|
+
export function getCurrencySymbol(code: string): string {
|
|
104
|
+
return CURRENCY_SYMBOLS[code] || code;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// PRICE FORMATTING
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format price with currency symbol
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* formatPrice({ amount: "99.99", currencyCode: "USD" })
|
|
117
|
+
* // => "$99.99"
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function formatPrice(price: PriceMoney | null | undefined): string {
|
|
121
|
+
if (!price) return '';
|
|
122
|
+
|
|
123
|
+
const amount = parseFloat(price.amount);
|
|
124
|
+
const code = price.currencyCode;
|
|
125
|
+
const locale = CURRENCY_LOCALES[code] || 'en-US';
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
return getCurrencyFormatter(locale, code).format(amount);
|
|
129
|
+
} catch {
|
|
130
|
+
const symbol = CURRENCY_SYMBOLS[code] || code;
|
|
131
|
+
return `${amount.toFixed(2)} ${symbol}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format price range
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* formatPriceRange(
|
|
141
|
+
* { amount: "10.00", currencyCode: "USD" },
|
|
142
|
+
* { amount: "50.00", currencyCode: "USD" }
|
|
143
|
+
* )
|
|
144
|
+
* // => "$10.00 - $50.00"
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function formatPriceRange(
|
|
148
|
+
minPrice: PriceMoney,
|
|
149
|
+
maxPrice: PriceMoney,
|
|
150
|
+
): string {
|
|
151
|
+
if (minPrice.amount === maxPrice.amount) {
|
|
152
|
+
return formatPrice(minPrice);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return `${formatPrice(minPrice)} - ${formatPrice(maxPrice)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Format amount with currency
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* const formatted = formatAmount("115.20", "EUR");
|
|
164
|
+
* // => "115,20 €"
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function formatAmount(
|
|
168
|
+
amount: string | number,
|
|
169
|
+
currencyCode: string,
|
|
170
|
+
): string {
|
|
171
|
+
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
172
|
+
const locale = CURRENCY_LOCALES[currencyCode] || 'en-US';
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
return getCurrencyFormatter(locale, currencyCode).format(numAmount);
|
|
176
|
+
} catch {
|
|
177
|
+
const symbol = CURRENCY_SYMBOLS[currencyCode] || currencyCode;
|
|
178
|
+
return `${numAmount.toFixed(2)} ${symbol}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// DATE FORMATTING
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Format date to locale string
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* formatDate(new Date())
|
|
192
|
+
* // => "Dec 9, 2025"
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export function formatDate(date: Date | string): string {
|
|
196
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
197
|
+
|
|
198
|
+
return getDateFormatter('en-US', {
|
|
199
|
+
year: 'numeric',
|
|
200
|
+
month: 'short',
|
|
201
|
+
day: 'numeric',
|
|
202
|
+
}).format(d);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Format date with time
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* formatDateTime(new Date())
|
|
211
|
+
* // => "Dec 9, 2025, 10:30 PM"
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function formatDateTime(date: Date | string): string {
|
|
215
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
216
|
+
|
|
217
|
+
return getDateFormatter('en-US', {
|
|
218
|
+
year: 'numeric',
|
|
219
|
+
month: 'short',
|
|
220
|
+
day: 'numeric',
|
|
221
|
+
hour: 'numeric',
|
|
222
|
+
minute: '2-digit',
|
|
223
|
+
}).format(d);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// NUMBER FORMATTING
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Format number with thousands separator
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* formatNumber(1234567)
|
|
236
|
+
* // => "1,234,567"
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function formatNumber(num: number): string {
|
|
240
|
+
return getNumberFormatter('en-US').format(num);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format percentage
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* formatPercentage(0.15)
|
|
249
|
+
* // => "15%"
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function formatPercentage(value: number): string {
|
|
253
|
+
return `${Math.round(value * 100)}%`;
|
|
254
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-throw on userErrors — used internally by CartClient and AuthClient.
|
|
3
|
+
*
|
|
4
|
+
* Frontend code doesn't need to manually check `if (userErrors.length)`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserError } from '../client/types';
|
|
8
|
+
import { StorefrontError, ErrorCodes } from '../errors';
|
|
9
|
+
|
|
10
|
+
export function assertNoUserErrors(
|
|
11
|
+
result: { userErrors?: UserError[] | null },
|
|
12
|
+
): void {
|
|
13
|
+
if (result.userErrors && result.userErrors.length > 0) {
|
|
14
|
+
const firstError = result.userErrors[0];
|
|
15
|
+
throw new StorefrontError({
|
|
16
|
+
code: ErrorCodes.USER_ERROR,
|
|
17
|
+
message: firstError.message,
|
|
18
|
+
userErrors: result.userErrors,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay-style GraphQL connection normalizer.
|
|
3
|
+
*
|
|
4
|
+
* Converts `{ edges: [{ node }], pageInfo, totalCount }` → flat array.
|
|
5
|
+
* Pure function, 0 deps, works everywhere.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ConnectionEdge<T> {
|
|
9
|
+
node: T;
|
|
10
|
+
cursor: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ConnectionPageInfo {
|
|
14
|
+
hasNextPage: boolean;
|
|
15
|
+
hasPreviousPage: boolean;
|
|
16
|
+
startCursor?: string | null;
|
|
17
|
+
endCursor?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Connection<T> {
|
|
21
|
+
edges: ConnectionEdge<T>[];
|
|
22
|
+
pageInfo: ConnectionPageInfo;
|
|
23
|
+
totalCount?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NormalizedConnection<T> {
|
|
27
|
+
items: T[];
|
|
28
|
+
pageInfo: ConnectionPageInfo;
|
|
29
|
+
totalCount?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a GraphQL Relay connection to a flat array.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const { items, pageInfo, totalCount } = normalizeConnection(data.products);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function normalizeConnection<T>(
|
|
41
|
+
connection: Connection<T>,
|
|
42
|
+
): NormalizedConnection<T> {
|
|
43
|
+
return {
|
|
44
|
+
items: connection.edges.map((edge) => edge.node),
|
|
45
|
+
pageInfo: connection.pageInfo,
|
|
46
|
+
totalCount: connection.totalCount,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight HTML sanitizer for defense-in-depth.
|
|
3
|
+
* Primary sanitization happens on the backend (sanitize-html library).
|
|
4
|
+
* This strips dangerous tags/attributes as an extra safety layer.
|
|
5
|
+
* Works in both Node.js and Edge runtimes (no external dependencies).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DANGEROUS_TAG_PATTERNS = [
|
|
9
|
+
// Tags with content that must be fully removed
|
|
10
|
+
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script\s*>/gi,
|
|
11
|
+
/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe\s*>/gi,
|
|
12
|
+
/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object\s*>/gi,
|
|
13
|
+
/<form\b[^<]*(?:(?!<\/form>)<[^<]*)*<\/form\s*>/gi,
|
|
14
|
+
/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style\s*>/gi,
|
|
15
|
+
// Self-closing / void dangerous tags
|
|
16
|
+
/<embed\b[^>]*\/?>/gi,
|
|
17
|
+
/<link\b[^>]*\/?>/gi,
|
|
18
|
+
/<meta\b[^>]*\/?>/gi,
|
|
19
|
+
/<base\b[^>]*\/?>/gi,
|
|
20
|
+
/<applet\b[^<]*(?:(?!<\/applet>)<[^<]*)*<\/applet\s*>/gi,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Event handler attributes (onclick, onerror, onload, etc.)
|
|
24
|
+
const EVENT_HANDLER_PATTERN = /\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi;
|
|
25
|
+
|
|
26
|
+
// javascript: protocol in href/src/action attributes
|
|
27
|
+
const JS_PROTOCOL_PATTERN = /(href|src|action)\s*=\s*["']\s*javascript\s*:/gi;
|
|
28
|
+
|
|
29
|
+
export function sanitizeHtml(html: string): string {
|
|
30
|
+
if (!html) return html;
|
|
31
|
+
|
|
32
|
+
let result = html;
|
|
33
|
+
|
|
34
|
+
for (const pattern of DANGEROUS_TAG_PATTERNS) {
|
|
35
|
+
result = result.replace(pattern, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result = result.replace(EVENT_HANDLER_PATTERN, '');
|
|
39
|
+
result = result.replace(JS_PROTOCOL_PATTERN, '$1="');
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doswiftly/storefront-sdk — Core (framework-agnostic)
|
|
3
|
+
*
|
|
4
|
+
* 100% framework-agnostic, 0 runtime dependencies.
|
|
5
|
+
* Works in Node.js, Edge Workers, Deno, Bun, CLI scripts — without React.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import {
|
|
10
|
+
* createStorefrontClient,
|
|
11
|
+
* authMiddleware,
|
|
12
|
+
* currencyMiddleware,
|
|
13
|
+
* retryMiddleware,
|
|
14
|
+
* timeoutMiddleware,
|
|
15
|
+
* errorMiddleware,
|
|
16
|
+
* CartClient,
|
|
17
|
+
* AuthClient,
|
|
18
|
+
* } from '@doswiftly/storefront-sdk';
|
|
19
|
+
*
|
|
20
|
+
* const client = createStorefrontClient({
|
|
21
|
+
* apiUrl: 'https://api.doswiftly.pl',
|
|
22
|
+
* shopSlug: 'my-shop',
|
|
23
|
+
* middleware: [
|
|
24
|
+
* authMiddleware(() => getToken()),
|
|
25
|
+
* currencyMiddleware(() => getCurrency()),
|
|
26
|
+
* retryMiddleware({ maxRetries: 2 }),
|
|
27
|
+
* timeoutMiddleware({ timeout: 5000 }),
|
|
28
|
+
* errorMiddleware(),
|
|
29
|
+
* ],
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* const cartClient = new CartClient(client);
|
|
33
|
+
* const cart = await cartClient.create();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Client factory
|
|
38
|
+
export { createStorefrontClient } from './client/create-client';
|
|
39
|
+
|
|
40
|
+
// Client types
|
|
41
|
+
export type {
|
|
42
|
+
StorefrontClient,
|
|
43
|
+
StorefrontClientConfig,
|
|
44
|
+
Middleware,
|
|
45
|
+
ExecuteFn,
|
|
46
|
+
GraphQLRequest,
|
|
47
|
+
GraphQLResponse,
|
|
48
|
+
GraphQLErrorInfo,
|
|
49
|
+
UserError,
|
|
50
|
+
CacheStrategy,
|
|
51
|
+
CacheOptions,
|
|
52
|
+
TypedDocumentString,
|
|
53
|
+
} from './client/types';
|
|
54
|
+
|
|
55
|
+
// Middleware
|
|
56
|
+
export { authMiddleware } from './middleware/auth';
|
|
57
|
+
export { currencyMiddleware } from './middleware/currency';
|
|
58
|
+
export { retryMiddleware, type RetryOptions } from './middleware/retry';
|
|
59
|
+
export { timeoutMiddleware, type TimeoutOptions } from './middleware/timeout';
|
|
60
|
+
export { errorMiddleware } from './middleware/errors';
|
|
61
|
+
|
|
62
|
+
// Errors
|
|
63
|
+
export { StorefrontError, ErrorCodes, type StorefrontErrorOptions } from './errors';
|
|
64
|
+
|
|
65
|
+
// Cache strategies
|
|
66
|
+
export {
|
|
67
|
+
cacheNone,
|
|
68
|
+
cacheShort,
|
|
69
|
+
cacheLong,
|
|
70
|
+
cachePrivate,
|
|
71
|
+
cacheCustom,
|
|
72
|
+
generateCacheControlHeader,
|
|
73
|
+
type CacheOverrides,
|
|
74
|
+
} from './cache';
|
|
75
|
+
|
|
76
|
+
// Cart client
|
|
77
|
+
export { CartClient } from './cart/cart-client';
|
|
78
|
+
export type {
|
|
79
|
+
Cart,
|
|
80
|
+
CartLine,
|
|
81
|
+
CartLineMerchandise,
|
|
82
|
+
CartLineCost,
|
|
83
|
+
CartCost,
|
|
84
|
+
CartBuyerIdentity,
|
|
85
|
+
CartDiscountCode,
|
|
86
|
+
CartDiscountAllocation,
|
|
87
|
+
CartLineInput,
|
|
88
|
+
CartLineUpdateInput,
|
|
89
|
+
CartCreateInput,
|
|
90
|
+
CartBuyerIdentityInput,
|
|
91
|
+
Money,
|
|
92
|
+
} from './cart/types';
|
|
93
|
+
|
|
94
|
+
// Auth client
|
|
95
|
+
export { AuthClient } from './auth/auth-client';
|
|
96
|
+
export type {
|
|
97
|
+
Customer,
|
|
98
|
+
CustomerAccessToken,
|
|
99
|
+
MailingAddress,
|
|
100
|
+
AuthResult,
|
|
101
|
+
CustomerCreateInput,
|
|
102
|
+
} from './auth/types';
|
|
103
|
+
|
|
104
|
+
// Helpers
|
|
105
|
+
export { assertNoUserErrors } from './helpers/assert-no-user-errors';
|
|
106
|
+
export { sanitizeHtml } from './helpers/sanitize-html';
|
|
107
|
+
export {
|
|
108
|
+
normalizeConnection,
|
|
109
|
+
type Connection,
|
|
110
|
+
type ConnectionEdge,
|
|
111
|
+
type ConnectionPageInfo,
|
|
112
|
+
type NormalizedConnection,
|
|
113
|
+
} from './helpers/normalize-connection';
|
|
114
|
+
|
|
115
|
+
// Format utilities
|
|
116
|
+
export {
|
|
117
|
+
formatPrice,
|
|
118
|
+
formatPriceRange,
|
|
119
|
+
formatAmount,
|
|
120
|
+
formatDate,
|
|
121
|
+
formatDateTime,
|
|
122
|
+
formatNumber,
|
|
123
|
+
formatPercentage,
|
|
124
|
+
getCurrencySymbol,
|
|
125
|
+
CURRENCY_SYMBOLS,
|
|
126
|
+
CURRENCY_LOCALES,
|
|
127
|
+
type PriceMoney,
|
|
128
|
+
} from './format';
|
|
129
|
+
|
|
130
|
+
// Auth cookie config (platform contract)
|
|
131
|
+
export {
|
|
132
|
+
AUTH_COOKIE_NAME,
|
|
133
|
+
AUTH_COOKIE_DEFAULTS,
|
|
134
|
+
type AuthCookieConfig,
|
|
135
|
+
} from './auth/cookie-config';
|
|
136
|
+
|
|
137
|
+
// Auth route matching
|
|
138
|
+
export { matchesRoute, type RouteProtectionConfig } from './auth/routes';
|
|
139
|
+
|
|
140
|
+
// Auth cookie handlers (API route factories)
|
|
141
|
+
export { createSetTokenHandler, createClearTokenHandler } from './auth/handlers';
|
|
142
|
+
|
|
143
|
+
// Auth token client (client-side fetch helpers)
|
|
144
|
+
export { createAuthTokenClient, type AuthTokenClient } from './auth/token-client';
|
|
145
|
+
|
|
146
|
+
// Utilities
|
|
147
|
+
export { getOperationName } from './client/operation-name';
|
|
148
|
+
export { hashQuery } from './client/hash';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth middleware — adds Authorization header from token getter.
|
|
3
|
+
*
|
|
4
|
+
* Token is resolved lazily (on each request) so it picks up
|
|
5
|
+
* store changes, token renewals, etc.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { authMiddleware } from '@doswiftly/storefront-sdk';
|
|
10
|
+
*
|
|
11
|
+
* client.use(authMiddleware(() => authStore.getState().accessToken));
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Middleware } from '../client/types';
|
|
16
|
+
|
|
17
|
+
export function authMiddleware(
|
|
18
|
+
getToken: () => string | null | undefined,
|
|
19
|
+
): Middleware {
|
|
20
|
+
return (request, next) => {
|
|
21
|
+
const token = getToken();
|
|
22
|
+
if (token) {
|
|
23
|
+
request.headers['Authorization'] = `Bearer ${token}`;
|
|
24
|
+
}
|
|
25
|
+
return next(request);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currency middleware — adds X-Preferred-Currency header.
|
|
3
|
+
*
|
|
4
|
+
* Currency is resolved lazily so currency switches are reflected immediately.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { currencyMiddleware } from '@doswiftly/storefront-sdk';
|
|
9
|
+
*
|
|
10
|
+
* client.use(currencyMiddleware(() => currencyStore.getState().currency));
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Middleware } from '../client/types';
|
|
15
|
+
|
|
16
|
+
export function currencyMiddleware(
|
|
17
|
+
getCurrency: () => string | null | undefined,
|
|
18
|
+
): Middleware {
|
|
19
|
+
return (request, next) => {
|
|
20
|
+
const currency = getCurrency();
|
|
21
|
+
if (currency) {
|
|
22
|
+
request.headers['X-Preferred-Currency'] = currency;
|
|
23
|
+
}
|
|
24
|
+
return next(request);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error normalization middleware — ALWAYS LAST in the pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Catches all errors from downstream middleware and the transport,
|
|
5
|
+
* normalizes them into StorefrontError instances.
|
|
6
|
+
*
|
|
7
|
+
* Handles:
|
|
8
|
+
* - Network errors (fetch failures)
|
|
9
|
+
* - HTTP errors (non-200 responses)
|
|
10
|
+
* - GraphQL errors (errors array in response)
|
|
11
|
+
* - Abort/timeout errors
|
|
12
|
+
* - Missing data in response
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { errorMiddleware } from '@doswiftly/storefront-sdk';
|
|
17
|
+
*
|
|
18
|
+
* // Always add as the LAST middleware
|
|
19
|
+
* client.use(errorMiddleware());
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Middleware } from '../client/types';
|
|
24
|
+
import { StorefrontError, ErrorCodes } from '../errors';
|
|
25
|
+
|
|
26
|
+
export function errorMiddleware(): Middleware {
|
|
27
|
+
return async (request, next) => {
|
|
28
|
+
let response;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
response = await next(request);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Already a StorefrontError — re-throw as-is
|
|
34
|
+
if (error instanceof StorefrontError) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// AbortError (timeout handled by timeout middleware, manual abort here)
|
|
39
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
40
|
+
throw new StorefrontError({
|
|
41
|
+
code: ErrorCodes.TIMEOUT,
|
|
42
|
+
message: 'Request was aborted',
|
|
43
|
+
cause: error,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Network error (fetch failed — DNS, connection refused, etc.)
|
|
48
|
+
throw new StorefrontError({
|
|
49
|
+
code: ErrorCodes.NETWORK_ERROR,
|
|
50
|
+
message: error instanceof Error ? error.message : 'Network request failed',
|
|
51
|
+
cause: error,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// HTTP error (non-2xx response)
|
|
56
|
+
if (response.status >= 400) {
|
|
57
|
+
throw new StorefrontError({
|
|
58
|
+
code: ErrorCodes.HTTP_ERROR,
|
|
59
|
+
message: `HTTP ${response.status}`,
|
|
60
|
+
status: response.status,
|
|
61
|
+
graphqlErrors: response.errors ?? [],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// GraphQL errors in the response
|
|
66
|
+
if (response.errors?.length) {
|
|
67
|
+
throw new StorefrontError({
|
|
68
|
+
code: ErrorCodes.GRAPHQL_ERROR,
|
|
69
|
+
message: response.errors[0].message,
|
|
70
|
+
status: response.status,
|
|
71
|
+
graphqlErrors: response.errors,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// No data returned
|
|
76
|
+
if (response.data === undefined || response.data === null) {
|
|
77
|
+
throw new StorefrontError({
|
|
78
|
+
code: ErrorCodes.NO_DATA,
|
|
79
|
+
message: 'No data returned from GraphQL',
|
|
80
|
+
status: response.status,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return response;
|
|
85
|
+
};
|
|
86
|
+
}
|