@spindlecommerce/storefront-client 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +27 -2
- package/dist/builder.d.ts +6 -0
- package/dist/builder.js +5 -0
- package/dist/cart.d.ts +105 -0
- package/dist/cart.js +230 -0
- package/dist/client.d.ts +102 -0
- package/dist/client.js +108 -0
- package/dist/documents.d.ts +39 -0
- package/dist/documents.js +217 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +20 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/media.d.ts +2 -0
- package/dist/media.js +2 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +25 -0
- package/dist/prices.d.ts +4 -0
- package/dist/prices.js +3 -0
- package/dist/types.d.ts +455 -0
- package/dist/types.js +2 -0
- package/dist/variants.d.ts +29 -0
- package/dist/variants.js +26 -0
- package/dist/visual-editing.d.ts +47 -0
- package/dist/visual-editing.js +23 -0
- package/dist/webhooks.d.ts +52 -0
- package/dist/webhooks.js +80 -0
- package/package.json +37 -3
- package/schema.graphql +1453 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/** What the Storefront API returns, as this package's documents select it. */
|
|
2
|
+
export type Money = {
|
|
3
|
+
amount: string;
|
|
4
|
+
currencyCode: string;
|
|
5
|
+
formatted: string;
|
|
6
|
+
};
|
|
7
|
+
export type PriceRange = {
|
|
8
|
+
minVariantPrice: Money | null;
|
|
9
|
+
maxVariantPrice: Money | null;
|
|
10
|
+
compareAtMinVariantPrice: Money | null;
|
|
11
|
+
};
|
|
12
|
+
export type ProductOptionValue = {
|
|
13
|
+
code: string;
|
|
14
|
+
label: string;
|
|
15
|
+
swatch: string | null;
|
|
16
|
+
};
|
|
17
|
+
export type ProductOption = {
|
|
18
|
+
attribute: {
|
|
19
|
+
code: string;
|
|
20
|
+
label: string | null;
|
|
21
|
+
};
|
|
22
|
+
values: ProductOptionValue[];
|
|
23
|
+
};
|
|
24
|
+
/** A variant as cards need it: what to add, and whether it can be bought. */
|
|
25
|
+
export type VariantSummary = {
|
|
26
|
+
id: string;
|
|
27
|
+
isAvailable: boolean;
|
|
28
|
+
selectedOptions: {
|
|
29
|
+
attribute: string;
|
|
30
|
+
value: string;
|
|
31
|
+
}[];
|
|
32
|
+
};
|
|
33
|
+
/** What product cards and grids show. */
|
|
34
|
+
export type ProductSummary = {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
isAvailable: boolean;
|
|
39
|
+
priceRange: PriceRange | null;
|
|
40
|
+
/** In position order. A photo of one variant names it. */
|
|
41
|
+
media: {
|
|
42
|
+
url: string;
|
|
43
|
+
alt: string | null;
|
|
44
|
+
kind: "IMAGE" | "VIDEO";
|
|
45
|
+
variant: {
|
|
46
|
+
id: string;
|
|
47
|
+
} | null;
|
|
48
|
+
}[];
|
|
49
|
+
options: ProductOption[];
|
|
50
|
+
variants: VariantSummary[];
|
|
51
|
+
};
|
|
52
|
+
export type SelectedOption = {
|
|
53
|
+
attribute: string;
|
|
54
|
+
value: string;
|
|
55
|
+
label: string;
|
|
56
|
+
name: string;
|
|
57
|
+
swatch: string | null;
|
|
58
|
+
};
|
|
59
|
+
export type ProductVariant = {
|
|
60
|
+
id: string;
|
|
61
|
+
sku: string;
|
|
62
|
+
name: string;
|
|
63
|
+
isAvailable: boolean;
|
|
64
|
+
quantityAvailable: number | null;
|
|
65
|
+
price: Money | null;
|
|
66
|
+
compareAtPrice: Money | null;
|
|
67
|
+
selectedOptions: SelectedOption[];
|
|
68
|
+
media: {
|
|
69
|
+
url: string;
|
|
70
|
+
alt: string | null;
|
|
71
|
+
}[];
|
|
72
|
+
};
|
|
73
|
+
export type CategoryLink = {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
path: string;
|
|
77
|
+
};
|
|
78
|
+
export type Product = Omit<ProductSummary, "media" | "variants"> & {
|
|
79
|
+
description: string | null;
|
|
80
|
+
seo: {
|
|
81
|
+
title: string | null;
|
|
82
|
+
description: string | null;
|
|
83
|
+
} | null;
|
|
84
|
+
categories: CategoryLink[];
|
|
85
|
+
variants: ProductVariant[];
|
|
86
|
+
media: {
|
|
87
|
+
id: string;
|
|
88
|
+
url: string;
|
|
89
|
+
alt: string | null;
|
|
90
|
+
kind: "IMAGE" | "VIDEO";
|
|
91
|
+
position: number;
|
|
92
|
+
variant: {
|
|
93
|
+
id: string;
|
|
94
|
+
} | null;
|
|
95
|
+
}[];
|
|
96
|
+
attributes: {
|
|
97
|
+
attribute: {
|
|
98
|
+
code: string;
|
|
99
|
+
label: string;
|
|
100
|
+
};
|
|
101
|
+
value: string | null;
|
|
102
|
+
}[];
|
|
103
|
+
};
|
|
104
|
+
export type Category = CategoryLink & {
|
|
105
|
+
slug: string;
|
|
106
|
+
description: string | null;
|
|
107
|
+
ancestors: CategoryLink[];
|
|
108
|
+
children: CategoryLink[];
|
|
109
|
+
};
|
|
110
|
+
export type Connection<N> = {
|
|
111
|
+
pageInfo: {
|
|
112
|
+
hasNextPage: boolean;
|
|
113
|
+
endCursor: string | null;
|
|
114
|
+
};
|
|
115
|
+
edges: {
|
|
116
|
+
node: N | null;
|
|
117
|
+
}[];
|
|
118
|
+
};
|
|
119
|
+
export type ProductConnection = Connection<ProductSummary> & {
|
|
120
|
+
totalCount: number;
|
|
121
|
+
};
|
|
122
|
+
export type ProductSort = {
|
|
123
|
+
field: "NEWEST" | "PRICE" | "NAME";
|
|
124
|
+
direction: "ASC" | "DESC";
|
|
125
|
+
};
|
|
126
|
+
export type CategoryTreeNode = {
|
|
127
|
+
path: string;
|
|
128
|
+
children?: CategoryTreeNode[];
|
|
129
|
+
};
|
|
130
|
+
export type PageLink = {
|
|
131
|
+
id: string;
|
|
132
|
+
path: string | null;
|
|
133
|
+
title: string | null;
|
|
134
|
+
};
|
|
135
|
+
export type PageEntry = {
|
|
136
|
+
path: string | null;
|
|
137
|
+
updatedAt: string | null;
|
|
138
|
+
seo: {
|
|
139
|
+
noIndex: boolean;
|
|
140
|
+
} | null;
|
|
141
|
+
};
|
|
142
|
+
export type PageImage = {
|
|
143
|
+
url: string;
|
|
144
|
+
alt: string | null;
|
|
145
|
+
width: number | null;
|
|
146
|
+
height: number | null;
|
|
147
|
+
};
|
|
148
|
+
/** A component instance as the API delivers it. */
|
|
149
|
+
export type ContentNode = {
|
|
150
|
+
id: string;
|
|
151
|
+
component: string;
|
|
152
|
+
variation: string | null;
|
|
153
|
+
fields: Record<string, unknown>;
|
|
154
|
+
};
|
|
155
|
+
/** A component tree in the request locale, and what it refers to. */
|
|
156
|
+
export type BuilderPage<Root extends {
|
|
157
|
+
id: string;
|
|
158
|
+
component: string;
|
|
159
|
+
} = ContentNode> = {
|
|
160
|
+
id: string;
|
|
161
|
+
type: string;
|
|
162
|
+
path: string | null;
|
|
163
|
+
title: string | null;
|
|
164
|
+
isPreview: boolean;
|
|
165
|
+
publishedAt: string | null;
|
|
166
|
+
/** Changes with every draft save (in preview) or publish. */
|
|
167
|
+
updatedAt: string;
|
|
168
|
+
seo: {
|
|
169
|
+
title: string | null;
|
|
170
|
+
description: string | null;
|
|
171
|
+
noIndex: boolean;
|
|
172
|
+
image: PageImage | null;
|
|
173
|
+
};
|
|
174
|
+
content: Root;
|
|
175
|
+
references: {
|
|
176
|
+
products: ProductSummary[];
|
|
177
|
+
categories: CategoryLink[];
|
|
178
|
+
pages: PageLink[];
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
export type TemplateKind = "product" | "category";
|
|
182
|
+
/**
|
|
183
|
+
* A pathless builder page that product or category pages render with. The product or category
|
|
184
|
+
* supplies its components' data.
|
|
185
|
+
*/
|
|
186
|
+
export type TemplatePage = BuilderPage & {
|
|
187
|
+
templateFor: "PRODUCT" | "CATEGORY";
|
|
188
|
+
};
|
|
189
|
+
/** A null template renders the storefront's built-in layout. The template places the sections. */
|
|
190
|
+
export type WithTemplate = {
|
|
191
|
+
template: TemplatePage | null;
|
|
192
|
+
sections: BuilderPage | null;
|
|
193
|
+
};
|
|
194
|
+
export type Redirect = {
|
|
195
|
+
location: string;
|
|
196
|
+
statusCode: number;
|
|
197
|
+
};
|
|
198
|
+
export type RouteResult = ({
|
|
199
|
+
__typename: "Page";
|
|
200
|
+
} & BuilderPage) | ({
|
|
201
|
+
__typename: "Product";
|
|
202
|
+
} & Product & WithTemplate) | ({
|
|
203
|
+
__typename: "Category";
|
|
204
|
+
} & Category & WithTemplate) | ({
|
|
205
|
+
__typename: "Redirect";
|
|
206
|
+
} & Redirect);
|
|
207
|
+
/** route() without a product's or category's template and sections, which load separately. */
|
|
208
|
+
export type RouteData = Exclude<RouteResult, {
|
|
209
|
+
__typename: "Product" | "Category";
|
|
210
|
+
}> | ({
|
|
211
|
+
__typename: "Product";
|
|
212
|
+
} & Product) | ({
|
|
213
|
+
__typename: "Category";
|
|
214
|
+
} & Category);
|
|
215
|
+
export type Localization = {
|
|
216
|
+
locale: string;
|
|
217
|
+
currencyCode: string;
|
|
218
|
+
channel: {
|
|
219
|
+
code: string;
|
|
220
|
+
name: string;
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
export type SiteData<Header extends {
|
|
224
|
+
id: string;
|
|
225
|
+
component: string;
|
|
226
|
+
} = ContentNode, Footer extends {
|
|
227
|
+
id: string;
|
|
228
|
+
component: string;
|
|
229
|
+
} = ContentNode> = {
|
|
230
|
+
header: BuilderPage<Header> | null;
|
|
231
|
+
footer: BuilderPage<Footer> | null;
|
|
232
|
+
localization: Localization | null;
|
|
233
|
+
};
|
|
234
|
+
export type PreviewSession = {
|
|
235
|
+
expiresAt: string;
|
|
236
|
+
adminOrigin: string | null;
|
|
237
|
+
at: string | null;
|
|
238
|
+
};
|
|
239
|
+
export type CartLine = {
|
|
240
|
+
id: string;
|
|
241
|
+
name: string;
|
|
242
|
+
quantity: number;
|
|
243
|
+
quantityAvailable: number | null;
|
|
244
|
+
unitPrice: Money | null;
|
|
245
|
+
compareAtUnitPrice: Money | null;
|
|
246
|
+
total: Money | null;
|
|
247
|
+
issue: {
|
|
248
|
+
code: string;
|
|
249
|
+
message: string;
|
|
250
|
+
} | null;
|
|
251
|
+
product: {
|
|
252
|
+
name: string;
|
|
253
|
+
slug: string;
|
|
254
|
+
media: {
|
|
255
|
+
url: string;
|
|
256
|
+
alt: string | null;
|
|
257
|
+
kind: string;
|
|
258
|
+
}[];
|
|
259
|
+
} | null;
|
|
260
|
+
variant: {
|
|
261
|
+
id: string;
|
|
262
|
+
name: string;
|
|
263
|
+
selectedOptions: {
|
|
264
|
+
name: string;
|
|
265
|
+
label: string;
|
|
266
|
+
}[];
|
|
267
|
+
media: {
|
|
268
|
+
url: string;
|
|
269
|
+
alt: string | null;
|
|
270
|
+
}[];
|
|
271
|
+
} | null;
|
|
272
|
+
};
|
|
273
|
+
export type Cart = {
|
|
274
|
+
id: string;
|
|
275
|
+
isOpen: boolean;
|
|
276
|
+
totalQuantity: number;
|
|
277
|
+
currencyCode: string;
|
|
278
|
+
cost: {
|
|
279
|
+
subtotal: Money | null;
|
|
280
|
+
total: Money | null;
|
|
281
|
+
tax: Money | null;
|
|
282
|
+
shipping: Money | null;
|
|
283
|
+
discountTotal: Money | null;
|
|
284
|
+
pricesIncludeTax: boolean;
|
|
285
|
+
} | null;
|
|
286
|
+
lines: CartLine[];
|
|
287
|
+
};
|
|
288
|
+
export type Address = {
|
|
289
|
+
firstName: string | null;
|
|
290
|
+
lastName: string | null;
|
|
291
|
+
company: string | null;
|
|
292
|
+
line1: string | null;
|
|
293
|
+
line2: string | null;
|
|
294
|
+
city: string | null;
|
|
295
|
+
region: string | null;
|
|
296
|
+
regionCode: string | null;
|
|
297
|
+
postalCode: string | null;
|
|
298
|
+
countryCode: string | null;
|
|
299
|
+
phone: string | null;
|
|
300
|
+
};
|
|
301
|
+
/** An address as cartUpdate takes it. Incomplete addresses are saved as drafts. */
|
|
302
|
+
export type AddressInput = {
|
|
303
|
+
[Field in Exclude<keyof Address, "countryCode">]?: string | null;
|
|
304
|
+
} & {
|
|
305
|
+
countryCode: string;
|
|
306
|
+
};
|
|
307
|
+
/** cartUpdate's input. Fields left out stay as they are. */
|
|
308
|
+
export type CartUpdateInput = {
|
|
309
|
+
email?: string;
|
|
310
|
+
locale?: string;
|
|
311
|
+
currencyCode?: string;
|
|
312
|
+
shippingAddress?: {
|
|
313
|
+
address: AddressInput;
|
|
314
|
+
};
|
|
315
|
+
billingAddress?: {
|
|
316
|
+
sameAsShipping: true;
|
|
317
|
+
} | {
|
|
318
|
+
address: AddressInput;
|
|
319
|
+
};
|
|
320
|
+
/** Null goes back to the cheapest rate. */
|
|
321
|
+
shippingMethodCode?: string | null;
|
|
322
|
+
paymentMethodCode?: string;
|
|
323
|
+
};
|
|
324
|
+
/** What a cart still needs before it can become an order (Cart.missing). */
|
|
325
|
+
export type CartRequirement = "EMAIL" | "LINES" | "LINE_ISSUES" | "SHIPPING_ADDRESS" | "SHIPPING_METHOD" | "BILLING_ADDRESS" | "PAYMENT_METHOD";
|
|
326
|
+
export type ShippingRate = {
|
|
327
|
+
code: string;
|
|
328
|
+
name: string;
|
|
329
|
+
description: string | null;
|
|
330
|
+
price: Money | null;
|
|
331
|
+
minDeliveryDays: number | null;
|
|
332
|
+
maxDeliveryDays: number | null;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* `provider`: offline, test_gateway, stripe, … `sessionRequired`: needs a payment session before
|
|
336
|
+
* the order is placed. An older API gives null and false.
|
|
337
|
+
*/
|
|
338
|
+
export type PaymentMethod = {
|
|
339
|
+
code: string;
|
|
340
|
+
name: string;
|
|
341
|
+
instructions: string | null;
|
|
342
|
+
provider: string | null;
|
|
343
|
+
sessionRequired: boolean;
|
|
344
|
+
};
|
|
345
|
+
export type Country = {
|
|
346
|
+
code: string;
|
|
347
|
+
name: string;
|
|
348
|
+
};
|
|
349
|
+
export type Discount = {
|
|
350
|
+
name: string;
|
|
351
|
+
couponCode: string | null;
|
|
352
|
+
amount: Money | null;
|
|
353
|
+
};
|
|
354
|
+
/** `subtotal`: quantity × unit price, before discounts and any tax added on top. */
|
|
355
|
+
export type CheckoutLine = CartLine & {
|
|
356
|
+
subtotal: Money | null;
|
|
357
|
+
};
|
|
358
|
+
export type CheckoutCart = {
|
|
359
|
+
id: string;
|
|
360
|
+
/** Increments on every change to the cart. */
|
|
361
|
+
version: number;
|
|
362
|
+
email: string | null;
|
|
363
|
+
requiresShipping: boolean;
|
|
364
|
+
missing: CartRequirement[];
|
|
365
|
+
/** As far as the shopper filled them in. */
|
|
366
|
+
shippingAddress: Address | null;
|
|
367
|
+
/** The cart's own, or else the shipping address. */
|
|
368
|
+
billingAddress: Address | null;
|
|
369
|
+
billingSameAsShipping: boolean;
|
|
370
|
+
shippingMethods: ShippingRate[];
|
|
371
|
+
/** Before shipping discounts (see `discounts`). The cheapest until the shopper chooses. */
|
|
372
|
+
selectedShipping: {
|
|
373
|
+
code: string;
|
|
374
|
+
name: string;
|
|
375
|
+
price: Money | null;
|
|
376
|
+
} | null;
|
|
377
|
+
paymentMethods: PaymentMethod[];
|
|
378
|
+
selectedPayment: PaymentMethod | null;
|
|
379
|
+
coupons: {
|
|
380
|
+
code: string;
|
|
381
|
+
applied: boolean;
|
|
382
|
+
reason: string | null;
|
|
383
|
+
}[];
|
|
384
|
+
discounts: Discount[];
|
|
385
|
+
cost: {
|
|
386
|
+
subtotal: Money | null;
|
|
387
|
+
total: Money | null;
|
|
388
|
+
tax: Money | null;
|
|
389
|
+
pricesIncludeTax: boolean;
|
|
390
|
+
} | null;
|
|
391
|
+
lines: CheckoutLine[];
|
|
392
|
+
};
|
|
393
|
+
/** `path` runs from the argument root, e.g. ["input", "shippingAddress", "address", "postalCode"]. */
|
|
394
|
+
export type FieldError = {
|
|
395
|
+
path: string[];
|
|
396
|
+
message: string;
|
|
397
|
+
};
|
|
398
|
+
/** What guestOrder needs to look the order up: its number, email and billing postal code. */
|
|
399
|
+
export type PlacedOrderRef = {
|
|
400
|
+
number: string;
|
|
401
|
+
email: string;
|
|
402
|
+
billingAddress: {
|
|
403
|
+
postalCode: string | null;
|
|
404
|
+
} | null;
|
|
405
|
+
};
|
|
406
|
+
export type PlacedOrder = {
|
|
407
|
+
number: string;
|
|
408
|
+
email: string;
|
|
409
|
+
locale: string | null;
|
|
410
|
+
status: "OPEN" | "COMPLETED" | "CANCELED";
|
|
411
|
+
paymentStatus: "PENDING" | "AUTHORIZED" | "PAID" | "PARTIALLY_PAID" | "REFUNDED" | "PARTIALLY_REFUNDED" | "VOIDED" | null;
|
|
412
|
+
placedAt: string | null;
|
|
413
|
+
currencyCode: string;
|
|
414
|
+
lines: {
|
|
415
|
+
id: string;
|
|
416
|
+
name: string;
|
|
417
|
+
sku: string;
|
|
418
|
+
quantity: number;
|
|
419
|
+
unitPrice: Money | null;
|
|
420
|
+
subtotal: Money | null;
|
|
421
|
+
total: Money | null;
|
|
422
|
+
product: {
|
|
423
|
+
name: string;
|
|
424
|
+
slug: string;
|
|
425
|
+
media: {
|
|
426
|
+
url: string;
|
|
427
|
+
alt: string | null;
|
|
428
|
+
kind: string;
|
|
429
|
+
}[];
|
|
430
|
+
} | null;
|
|
431
|
+
variant: {
|
|
432
|
+
selectedOptions: {
|
|
433
|
+
name: string;
|
|
434
|
+
label: string;
|
|
435
|
+
}[];
|
|
436
|
+
media: {
|
|
437
|
+
url: string;
|
|
438
|
+
alt: string | null;
|
|
439
|
+
}[];
|
|
440
|
+
} | null;
|
|
441
|
+
}[];
|
|
442
|
+
totals: {
|
|
443
|
+
subtotal: Money | null;
|
|
444
|
+
shipping: Money | null;
|
|
445
|
+
tax: Money | null;
|
|
446
|
+
total: Money | null;
|
|
447
|
+
pricesIncludeTax: boolean;
|
|
448
|
+
discounts: Discount[] | null;
|
|
449
|
+
} | null;
|
|
450
|
+
shippingAddress: Address | null;
|
|
451
|
+
billingAddress: Address | null;
|
|
452
|
+
shippingMethod: {
|
|
453
|
+
name: string;
|
|
454
|
+
} | null;
|
|
455
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ProductOption, VariantSummary } from "./types.ts";
|
|
2
|
+
/** Option values by attribute code, e.g. { color: "blue", size: "m" }. */
|
|
3
|
+
export type Selection = Record<string, string>;
|
|
4
|
+
type WithOptions = Pick<VariantSummary, "selectedOptions">;
|
|
5
|
+
type Stocked = WithOptions & Pick<VariantSummary, "isAvailable">;
|
|
6
|
+
export declare const selectionOf: (variant: WithOptions | undefined) => Selection;
|
|
7
|
+
/** The selection names every option of the variant. */
|
|
8
|
+
export declare const matches: (variant: WithOptions, selection: Selection) => boolean;
|
|
9
|
+
/** The variant has the selection's values, and maybe other options. */
|
|
10
|
+
export declare const hasValues: (variant: WithOptions, selection: Selection) => boolean;
|
|
11
|
+
export declare const variantFor: <V extends WithOptions>(variants: V[], selection: Selection) => V | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Choosing `value` keeps the other choices when that combination exists; otherwise it moves to one
|
|
14
|
+
* that does, in stock if there is one.
|
|
15
|
+
*/
|
|
16
|
+
export declare const chooseValue: <V extends Stocked>(variants: V[], selection: Selection, attribute: string, value: string) => {
|
|
17
|
+
selection: Selection;
|
|
18
|
+
variant: V | undefined;
|
|
19
|
+
};
|
|
20
|
+
/** Whether choosing `value`, keeping the other choices, gives a variant in stock. */
|
|
21
|
+
export declare const inStock: (variants: Stocked[], selection: Selection, attribute: string, value: string) => boolean;
|
|
22
|
+
/** The attribute's value on the first variant in stock, else on the first variant. */
|
|
23
|
+
export declare const firstValue: (variants: Stocked[], attribute: string) => string | null;
|
|
24
|
+
/** The option's values some variant has along with the selection, each with that variant. */
|
|
25
|
+
export declare const valuesWith: <V extends WithOptions>(variants: V[], option: ProductOption, selection: Selection) => {
|
|
26
|
+
value: import("./types.ts").ProductOptionValue;
|
|
27
|
+
variant: V;
|
|
28
|
+
}[];
|
|
29
|
+
export {};
|
package/dist/variants.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const selectionOf = (variant) => Object.fromEntries((variant?.selectedOptions ?? []).map((o) => [o.attribute, o.value]));
|
|
2
|
+
/** The selection names every option of the variant. */
|
|
3
|
+
export const matches = (variant, selection) => variant.selectedOptions.every((o) => selection[o.attribute] === o.value);
|
|
4
|
+
/** The variant has the selection's values, and maybe other options. */
|
|
5
|
+
export const hasValues = (variant, selection) => Object.entries(selection).every(([attribute, value]) => variant.selectedOptions.some((o) => o.attribute === attribute && o.value === value));
|
|
6
|
+
export const variantFor = (variants, selection) => variants.find((v) => matches(v, selection));
|
|
7
|
+
/**
|
|
8
|
+
* Choosing `value` keeps the other choices when that combination exists; otherwise it moves to one
|
|
9
|
+
* that does, in stock if there is one.
|
|
10
|
+
*/
|
|
11
|
+
export const chooseValue = (variants, selection, attribute, value) => {
|
|
12
|
+
const wanted = { ...selection, [attribute]: value };
|
|
13
|
+
const variant = variants.find((v) => matches(v, wanted)) ??
|
|
14
|
+
variants.find((v) => v.isAvailable && hasValues(v, { [attribute]: value })) ??
|
|
15
|
+
variants.find((v) => hasValues(v, { [attribute]: value }));
|
|
16
|
+
return { selection: variant ? selectionOf(variant) : wanted, variant };
|
|
17
|
+
};
|
|
18
|
+
/** Whether choosing `value`, keeping the other choices, gives a variant in stock. */
|
|
19
|
+
export const inStock = (variants, selection, attribute, value) => variants.some((v) => v.isAvailable && matches(v, { ...selection, [attribute]: value }));
|
|
20
|
+
/** The attribute's value on the first variant in stock, else on the first variant. */
|
|
21
|
+
export const firstValue = (variants, attribute) => selectionOf(variants.find((v) => v.isAvailable) ?? variants[0])[attribute] ?? null;
|
|
22
|
+
/** The option's values some variant has along with the selection, each with that variant. */
|
|
23
|
+
export const valuesWith = (variants, option, selection) => option.values.flatMap((value) => {
|
|
24
|
+
const variant = variantFor(variants, { ...selection, [option.attribute.code]: value.code });
|
|
25
|
+
return variant ? [{ value, variant }] : [];
|
|
26
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markup for the visual editing bridge (see the header of the API's /visual-editing/bridge.js).
|
|
3
|
+
* Outside draft mode every helper returns no attributes.
|
|
4
|
+
*
|
|
5
|
+
* data-ecom-document around each document: the page, the header, the footer
|
|
6
|
+
* data-ecom-node on a component's root element (with data-ecom-component)
|
|
7
|
+
* data-ecom-field on an element showing a field: "<nodeId>.<field>", or for a list item
|
|
8
|
+
* "<nodeId>.<field>.<itemId>[.<subField>]"
|
|
9
|
+
* data-ecom-patch "text" when the element shows the field's text as is, so the bridge can
|
|
10
|
+
* update it as the editor types
|
|
11
|
+
*/
|
|
12
|
+
export type EditAttributes = {
|
|
13
|
+
[attribute: `data-ecom-${string}`]: string;
|
|
14
|
+
};
|
|
15
|
+
export type Editable = {
|
|
16
|
+
draft: boolean;
|
|
17
|
+
document: (id: string) => EditAttributes;
|
|
18
|
+
node: (node: {
|
|
19
|
+
id: string;
|
|
20
|
+
component: string;
|
|
21
|
+
}) => EditAttributes;
|
|
22
|
+
field: (nodeId: string, ...path: string[]) => EditAttributes;
|
|
23
|
+
/** A field whose element shows exactly the field's text. */
|
|
24
|
+
text: (nodeId: string, ...path: string[]) => EditAttributes;
|
|
25
|
+
};
|
|
26
|
+
export declare const editable: (draft: boolean) => Editable;
|
|
27
|
+
/** The bridge, protocol version 1. */
|
|
28
|
+
export type VisualEditingBridge = {
|
|
29
|
+
version: number;
|
|
30
|
+
readonly connected: boolean;
|
|
31
|
+
init(options: {
|
|
32
|
+
adminOrigin: string;
|
|
33
|
+
onRefresh?: () => void | Promise<void>;
|
|
34
|
+
onPatch?: (patch: {
|
|
35
|
+
nodeId: string;
|
|
36
|
+
path: string[];
|
|
37
|
+
value: unknown;
|
|
38
|
+
}) => void;
|
|
39
|
+
navigate?: (path: string) => void;
|
|
40
|
+
}): VisualEditingBridge;
|
|
41
|
+
/** Tells the editor a refresh has rendered. */
|
|
42
|
+
rendered(): void;
|
|
43
|
+
};
|
|
44
|
+
/** Where the platform serves the bridge's script. */
|
|
45
|
+
export declare const visualEditingBridgeUrl: (apiUrl: string) => string;
|
|
46
|
+
/** The bridge, once its script has run in this window. */
|
|
47
|
+
export declare const visualEditingBridge: () => VisualEditingBridge | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markup for the visual editing bridge (see the header of the API's /visual-editing/bridge.js).
|
|
3
|
+
* Outside draft mode every helper returns no attributes.
|
|
4
|
+
*
|
|
5
|
+
* data-ecom-document around each document: the page, the header, the footer
|
|
6
|
+
* data-ecom-node on a component's root element (with data-ecom-component)
|
|
7
|
+
* data-ecom-field on an element showing a field: "<nodeId>.<field>", or for a list item
|
|
8
|
+
* "<nodeId>.<field>.<itemId>[.<subField>]"
|
|
9
|
+
* data-ecom-patch "text" when the element shows the field's text as is, so the bridge can
|
|
10
|
+
* update it as the editor types
|
|
11
|
+
*/
|
|
12
|
+
const NONE = {};
|
|
13
|
+
export const editable = (draft) => ({
|
|
14
|
+
draft,
|
|
15
|
+
document: (id) => (draft ? { "data-ecom-document": id } : NONE),
|
|
16
|
+
node: (node) => (draft ? { "data-ecom-node": node.id, "data-ecom-component": node.component } : NONE),
|
|
17
|
+
field: (nodeId, ...path) => (draft ? { "data-ecom-field": [nodeId, ...path].join(".") } : NONE),
|
|
18
|
+
text: (nodeId, ...path) => (draft ? { "data-ecom-field": [nodeId, ...path].join("."), "data-ecom-patch": "text" } : NONE),
|
|
19
|
+
});
|
|
20
|
+
/** Where the platform serves the bridge's script. */
|
|
21
|
+
export const visualEditingBridgeUrl = (apiUrl) => `${apiUrl.replace(/\/+$/, "")}/visual-editing/bridge.js`;
|
|
22
|
+
/** The bridge, once its script has run in this window. */
|
|
23
|
+
export const visualEditingBridge = () => globalThis.EcomVisualEditing;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TemplateKind } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Platform webhooks: `{ id, type, occurredAt, data }`, signed with
|
|
4
|
+
* `x-spindle-signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">`.
|
|
5
|
+
*/
|
|
6
|
+
export type WebhookEvent = {
|
|
7
|
+
id: string;
|
|
8
|
+
type: string;
|
|
9
|
+
occurredAt: string;
|
|
10
|
+
data: {
|
|
11
|
+
aggregateType?: string;
|
|
12
|
+
aggregateId?: string;
|
|
13
|
+
type?: unknown;
|
|
14
|
+
path?: unknown;
|
|
15
|
+
previousPath?: unknown;
|
|
16
|
+
templateFor?: unknown;
|
|
17
|
+
sectionsFor?: unknown;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/** Older or newer signatures are refused, so a captured request cannot be replayed. */
|
|
22
|
+
export declare const SIGNATURE_TOLERANCE_SECONDS: number;
|
|
23
|
+
/** Checks the signature over the exact text received. Verify before parsing. */
|
|
24
|
+
export declare const verifyWebhookSignature: (secret: string, body: string, header: string | null, nowSeconds?: number) => Promise<boolean>;
|
|
25
|
+
/** A verified body as an event; null when it is not one. */
|
|
26
|
+
export declare const parseWebhookEvent: (body: string) => WebhookEvent | null;
|
|
27
|
+
/** Content an event changed, for a storefront to expire what shows it. */
|
|
28
|
+
export type ContentChange = {
|
|
29
|
+
kind: "everything";
|
|
30
|
+
} | {
|
|
31
|
+
kind: "pages";
|
|
32
|
+
} | {
|
|
33
|
+
kind: "catalog";
|
|
34
|
+
} | {
|
|
35
|
+
kind: "path";
|
|
36
|
+
path: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "templates";
|
|
39
|
+
for: TemplateKind;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "sections";
|
|
42
|
+
for: TemplateKind;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "singleton";
|
|
45
|
+
type: string;
|
|
46
|
+
};
|
|
47
|
+
/** `immediate`: expire now (the next visit waits for fresh data) rather than serve stale while refetching. */
|
|
48
|
+
export type ContentChanges = {
|
|
49
|
+
changes: ContentChange[];
|
|
50
|
+
immediate: boolean;
|
|
51
|
+
};
|
|
52
|
+
export declare const changesFor: (event: WebhookEvent) => ContentChanges;
|