@asstio-dev/storefront 0.3.0 → 0.6.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 CHANGED
@@ -75,6 +75,60 @@ for (const file of product?.files ?? []) console.log(file.filename, fileUrl(site
75
75
  // A variant with hasOwnImages: false inherits: variant.hasOwnImages ? variant.images : product.images
76
76
  ```
77
77
 
78
+ `videoSources` returns `[]` for anything that is not a video, while `mediaUrl`/`pictureSources`/`fileUrl`/`posterUrl`
79
+ throw `MediaUrlError` when the descriptor cannot produce that URL — branch on `kind` before calling.
80
+
81
+ ### Product content
82
+
83
+ `description` is **already sanitized** on the server: render `description.html` as-is — do not sanitize, escape or
84
+ re-parse it — and use `description.text` wherever you need plain text (a meta description, a snippet, an
85
+ `aria-label`). `shortDescription` is plain text. `brand.facet` is set when the brand shares a facet's source, so it
86
+ can link straight to a filtered listing; when it is `null`, render `brand.label` as plain text and never guess a
87
+ filter.
88
+
89
+ `attributes[]` arrives ordered and labelled for display. Branch on `type` (`text` | `html` | `number` | `bool` |
90
+ `list`): a list fills `values`, everything else fills `value`, and `html` fills `html` too — sanitized exactly like
91
+ `description.html`.
92
+
93
+ ```ts
94
+ if (product.description) render(product.description.html); // as-is: sanitized server-side
95
+ for (const a of product.attributes) {
96
+ if (a.type === "html") render(a.html!); // sanitized like description.html
97
+ else if (a.values) render(a.values.join(", ")); // type: "list"
98
+ else render(a.value!); // text | number | bool
99
+ }
100
+ ```
101
+
102
+ `facetKey` and `valueKeys` turn an attribute's values into filter links — `filter[facetKey]=valueKeys[i]`, aligned
103
+ index-for-index with `values` (or carrying the single `value`'s key). `valueKeys` is absent entirely when any one
104
+ value has no key of its own: a list is either fully linkable or not linkable at all.
105
+
106
+ `related[]` holds groups of cards already resolved for this market, and `bundle.components[]` the cards a bundle is
107
+ made of; both leave out anything that no longer exists or is not sold here.
108
+
109
+ ### Search-as-you-type and search-in-facet
110
+
111
+ `/v1/search/suggest` answers three short lists for what the shopper has typed so far: up to 5 product cards
112
+ (matched on the product name), 5 categories (each with the `path` to show above it) and 10 values from the facets
113
+ whose panel is searchable. `q` is required and 1–100 characters — blank, missing or longer is a 400
114
+ (`INVALID_QUERY`), not a truncation the way a listing's `q` is.
115
+
116
+ `/v1/products/facets/{key}` is the search box inside one facet panel: the facet's values with the counts they have
117
+ in the same `filter[…]` context the listing is showing, so a count is what selecting that value would leave. Send
118
+ the panel's own filters with it, and omit `q` for the values as they are.
119
+
120
+ ```ts
121
+ const { data: suggestions } = await storefront.GET("/v1/search/suggest", { params: { query: { q: typed } } });
122
+ const { data: brands } = await storefront.GET("/v1/products/facets/{key}", {
123
+ params: { path: { key: "varumarke" }, query: { q: typed, filter: filterQuery({ kon: ["dam"] }) } },
124
+ });
125
+ ```
126
+
127
+ The endpoint reads the facet's **1000 most common values** and matches `q` against those — label contains,
128
+ case-insensitively, or key prefix — returning at most 50. A facet with more distinct values than that is searched
129
+ over its top 1000; no regular expression ever reaches the search index. `key` has to be a `list` or `swatch` facet:
130
+ an unknown key is `FACET_NOT_FOUND` (404), and a range or toggle facet is `FACET_NOT_SEARCHABLE` (400).
131
+
78
132
  ## Headers
79
133
 
80
134
  Every call the client makes carries these; you never set them by hand.
@@ -115,7 +169,8 @@ Every operation documents `400`, `401`, `403`, `404`, `429` and `503`; the entit
115
169
  Codes you will meet: `SITE_KEY_MISSING`, `SITE_KEY_INVALID`, `SITE_INACTIVE`, `SECRET_KEY_FROM_BROWSER`,
116
170
  `SITE_REGISTRY_WARMING`, `MARKET_UNKNOWN`, `RATE_LIMITED` (with `retryAfterSeconds`, plus a `Retry-After`
117
171
  header), `PRODUCT_NOT_FOUND`, `COLLECTION_NOT_FOUND`, `INVALID_PAGE`, `INVALID_SINCE`, `INVALID_TYPE`,
118
- `INDEX_UNAVAILABLE` (retry — a preview index is being rebuilt), `NOT_FOUND`.
172
+ `INDEX_UNAVAILABLE` (retry — a preview index is being rebuilt), `NOT_FOUND`, `INVALID_QUERY`,
173
+ `FACET_NOT_FOUND`, `FACET_NOT_SEARCHABLE`.
119
174
 
120
175
  `SITE_REGISTRY_WARMING` and `INDEX_UNAVAILABLE` are retries, not bugs in your call.
121
176
 
@@ -135,11 +190,21 @@ goes blank because of a bad query string:
135
190
 
136
191
  `q`, `page`, `pageSize`, `sort` and `filter` are all in the contract, so they are typed and autocompleted.
137
192
  `filter` is a `deepObject`: `{ brand: "dior,chanel" }` goes out as `filter[brand]=dior,chanel`, which is the
138
- form the API reads. `filterQuery()` is a thin helper that joins multi-select values and drops `undefined` ones.
193
+ form the API reads. In a list, swatch or hierarchy filter `,` separates values, `\,` is a comma inside a value and
194
+ `\\` a backslash — value keys are raw values, and an option such as `14,2` is the key `14,2`. Never join or split a
195
+ filter parameter yourself: `encodeFilterValues(values)` builds one and `decodeFilterValues(param)` reads one (for
196
+ the selected state of a URL you parse), for every facet alike. `filterQuery()` uses `encodeFilterValues` for
197
+ multi-select arrays, passes a single string (a range `100-500`, a toggle `true`) as given and drops `undefined` ones.
198
+ A range is `min-max`: either bound may be left out and either may be negative — `-10--2`, `-10-2`, `-10-` (at least
199
+ −10), `--2` (at most −2) — and a leading dash with nothing before it keeps its meaning, `-500` is "at most 500". A
200
+ range whose lower bound is above its upper bound is ignored with a warning.
139
201
 
140
202
  `/v1/sitemap` takes `type` (`product` | `collection`), `since` (ISO-8601, inclusive), `page` and `pageSize`
141
203
  (max 1000, default 500).
142
204
 
205
+ `/v1/search/suggest` takes `q` only, and `/v1/products/facets/{key}` takes `q` plus the same `filter` object as a
206
+ listing — both are described under [Search-as-you-type and search-in-facet](#search-as-you-type-and-search-in-facet).
207
+
143
208
  ## Caching
144
209
 
145
210
  Read responses carry an `ETag` and `Vary: X-Site-Key, X-Market, Accept-Language` — the answer depends on
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The list grammar of a `filter[<key>]` parameter (list, swatch and hierarchy facets): `,` separates values, `\,` is a
3
+ * comma inside a value and `\\` a backslash. A value key is the raw value — an option such as "14,2" is the key "14,2" —
4
+ * so a frontend never joins or splits a filter parameter by hand: it builds and toggles filter URLs with these two, for
5
+ * every facet alike. Range (`min-max`) and toggle (`true`) parameters are single strings and need neither.
6
+ */
7
+ export declare function encodeFilterValues(values: readonly string[]): string;
8
+ /**
9
+ * The values a `filter[<key>]` parameter selects, read the way the API reads it: `\,` and `\\` unescaped, a backslash
10
+ * before anything else (or at the end) kept as it is, values trimmed, empty ones and repeats dropped.
11
+ */
12
+ export declare function decodeFilterValues(param: string): string[];
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The list grammar of a `filter[<key>]` parameter (list, swatch and hierarchy facets): `,` separates values, `\,` is a
3
+ * comma inside a value and `\\` a backslash. A value key is the raw value — an option such as "14,2" is the key "14,2" —
4
+ * so a frontend never joins or splits a filter parameter by hand: it builds and toggles filter URLs with these two, for
5
+ * every facet alike. Range (`min-max`) and toggle (`true`) parameters are single strings and need neither.
6
+ */
7
+ export function encodeFilterValues(values) {
8
+ return values.map((v) => v.replace(/\\/g, "\\\\").replace(/,/g, "\\,")).join(",");
9
+ }
10
+ /**
11
+ * The values a `filter[<key>]` parameter selects, read the way the API reads it: `\,` and `\\` unescaped, a backslash
12
+ * before anything else (or at the end) kept as it is, values trimmed, empty ones and repeats dropped.
13
+ */
14
+ export function decodeFilterValues(param) {
15
+ const values = [];
16
+ let current = "";
17
+ const push = () => {
18
+ const value = current.trim();
19
+ current = "";
20
+ if (value.length > 0 && !values.includes(value))
21
+ values.push(value);
22
+ };
23
+ for (let i = 0; i < param.length; i++) {
24
+ const c = param[i];
25
+ const next = param[i + 1];
26
+ if (c === "\\" && (next === "," || next === "\\")) {
27
+ current += next;
28
+ i++;
29
+ }
30
+ else if (c === ",") {
31
+ push();
32
+ }
33
+ else {
34
+ current += c;
35
+ }
36
+ }
37
+ push();
38
+ return values;
39
+ }
package/dist/index.d.ts CHANGED
@@ -38,10 +38,12 @@ export declare function createStorefrontClient(opts: StorefrontClientOptions): C
38
38
  /** The `filter` query parameter of a listing call: one facet key to one value, as the generated types spell it. */
39
39
  export type FilterSelection = NonNullable<NonNullable<paths["/v1/products"]["get"]["parameters"]["query"]>["filter"]>;
40
40
  /**
41
- * Collapse a facet selection into the typed `filter` object: several values for one facet are comma-separated,
42
- * and `undefined` facets drop out — so an unchecked checkbox needs no special case at the call site.
41
+ * Collapse a facet selection into the typed `filter` object: several values for one facet are encoded with
42
+ * `encodeFilterValues` (so a value containing a comma or a backslash survives), a single string is sent as given (a
43
+ * range "100-500", a toggle "true"), and `undefined` facets drop out — an unchecked checkbox needs no special case.
43
44
  * `{ brand: ["dior", "chanel"], price: "100-500", size: undefined }` → `{ brand: "dior,chanel", price: "100-500" }`.
44
45
  * The client sends it as `filter[brand]=dior,chanel` (deepObject), which is what the API reads.
45
46
  */
46
47
  export declare function filterQuery(selection: Record<string, string | string[] | undefined>): FilterSelection;
48
+ export { decodeFilterValues, encodeFilterValues } from "./filters.js";
47
49
  export { fileUrl, mediaHash, mediaUrl, MediaUrlError, pickWidth, pictureSources, posterUrl, srcSet, videoSources, type MediaConfig, type MediaDescriptor, type PictureSources, type VideoSource, } from "./media.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import createClient from "openapi-fetch";
2
+ import { encodeFilterValues } from "./filters.js";
2
3
  /** Thin typed client: every call carries X-Site-Key / X-Market / Accept-Language. Works in browsers and on the server. */
3
4
  export function createStorefrontClient(opts) {
4
5
  const client = createClient({ baseUrl: opts.baseUrl.replace(/\/$/, ""), fetch: opts.fetch });
@@ -18,8 +19,9 @@ export function createStorefrontClient(opts) {
18
19
  return client;
19
20
  }
20
21
  /**
21
- * Collapse a facet selection into the typed `filter` object: several values for one facet are comma-separated,
22
- * and `undefined` facets drop out — so an unchecked checkbox needs no special case at the call site.
22
+ * Collapse a facet selection into the typed `filter` object: several values for one facet are encoded with
23
+ * `encodeFilterValues` (so a value containing a comma or a backslash survives), a single string is sent as given (a
24
+ * range "100-500", a toggle "true"), and `undefined` facets drop out — an unchecked checkbox needs no special case.
23
25
  * `{ brand: ["dior", "chanel"], price: "100-500", size: undefined }` → `{ brand: "dior,chanel", price: "100-500" }`.
24
26
  * The client sends it as `filter[brand]=dior,chanel` (deepObject), which is what the API reads.
25
27
  */
@@ -28,8 +30,9 @@ export function filterQuery(selection) {
28
30
  for (const [key, value] of Object.entries(selection)) {
29
31
  if (value === undefined)
30
32
  continue;
31
- out[key] = Array.isArray(value) ? value.join(",") : value;
33
+ out[key] = Array.isArray(value) ? encodeFilterValues(value) : value;
32
34
  }
33
35
  return out;
34
36
  }
37
+ export { decodeFilterValues, encodeFilterValues } from "./filters.js";
35
38
  export { fileUrl, mediaHash, mediaUrl, MediaUrlError, pickWidth, pictureSources, posterUrl, srcSet, videoSources, } from "./media.js";
package/dist/schema.d.ts CHANGED
@@ -153,6 +153,23 @@ export interface paths {
153
153
  patch?: never;
154
154
  trace?: never;
155
155
  };
156
+ "/v1/products/facets/{key}": {
157
+ parameters: {
158
+ query?: never;
159
+ header?: never;
160
+ path?: never;
161
+ cookie?: never;
162
+ };
163
+ /** Search within a facet: values whose label matches q, counted in this request's filter[...] context (spelled as on /products). Free text and collections are not applied. */
164
+ get: operations["SearchFacetValues"];
165
+ put?: never;
166
+ post?: never;
167
+ delete?: never;
168
+ options?: never;
169
+ head?: never;
170
+ patch?: never;
171
+ trace?: never;
172
+ };
156
173
  "/v1/products/{slugOrId}": {
157
174
  parameters: {
158
175
  query?: never;
@@ -187,6 +204,23 @@ export interface paths {
187
204
  patch?: never;
188
205
  trace?: never;
189
206
  };
207
+ "/v1/search/suggest": {
208
+ parameters: {
209
+ query?: never;
210
+ header?: never;
211
+ path?: never;
212
+ cookie?: never;
213
+ };
214
+ /** Search-as-you-type: products by name prefix, categories and facet values whose label starts with q. Market, assortment and language as /products. */
215
+ get: operations["Suggest"];
216
+ put?: never;
217
+ post?: never;
218
+ delete?: never;
219
+ options?: never;
220
+ head?: never;
221
+ patch?: never;
222
+ trace?: never;
223
+ };
190
224
  "/v1/site": {
191
225
  parameters: {
192
226
  query?: never;
@@ -235,6 +269,27 @@ export interface components {
235
269
  label: string;
236
270
  value: string;
237
271
  };
272
+ AttributeDto: {
273
+ facetKey: null | string;
274
+ html: null | string;
275
+ key: string;
276
+ label: string;
277
+ type: string;
278
+ value: null | string;
279
+ valueKeys: string[] | null;
280
+ values: string[] | null;
281
+ };
282
+ BrandDto: {
283
+ facet: null | components["schemas"]["FacetLinkDto"];
284
+ label: string;
285
+ };
286
+ BundleComponentDto: {
287
+ product: components["schemas"]["ProductCardDto"];
288
+ quantity: string;
289
+ };
290
+ BundleDto: {
291
+ components: components["schemas"]["BundleComponentDto"][];
292
+ };
238
293
  CartDto: {
239
294
  currency: string;
240
295
  grandTotalInc: string;
@@ -251,10 +306,22 @@ export interface components {
251
306
  id: string;
252
307
  image: null | components["schemas"]["MediaDescriptorDto"];
253
308
  price: components["schemas"]["PriceDto"];
254
- productId: string;
309
+ product: components["schemas"]["CartLineProductDto"];
255
310
  /** Format: int32 */
256
311
  quantity: number | string;
257
312
  sku: string;
313
+ variant: components["schemas"]["CartLineVariantDto"];
314
+ };
315
+ CartLineProductDto: {
316
+ id: string;
317
+ name: string;
318
+ slug: string;
319
+ };
320
+ CartLineVariantDto: {
321
+ id: string;
322
+ selectedOptions: {
323
+ [key: string]: string;
324
+ };
258
325
  };
259
326
  CategoryNodeDto: {
260
327
  children: components["schemas"]["CategoryNodeDto"][];
@@ -298,7 +365,13 @@ export interface components {
298
365
  slug: string;
299
366
  url: string;
300
367
  };
368
+ DescriptionDto: {
369
+ html: string;
370
+ text: string;
371
+ };
301
372
  FacetDto: {
373
+ collapsed: boolean;
374
+ help: null | string;
302
375
  key: string;
303
376
  label: string;
304
377
  match: string;
@@ -308,16 +381,25 @@ export interface components {
308
381
  type: string;
309
382
  values: components["schemas"]["FacetValueDto"][];
310
383
  };
384
+ FacetLinkDto: {
385
+ key: string;
386
+ valueKey: string;
387
+ };
311
388
  FacetValueDto: {
312
389
  children: components["schemas"]["FacetValueDto"][] | null;
313
390
  /** Format: int64 */
314
391
  count: number | string;
392
+ help: null | string;
315
393
  key: string;
316
394
  label: string;
317
395
  pinned: boolean;
318
396
  selected: boolean;
319
397
  swatch: null | components["schemas"]["SwatchDto"];
320
398
  };
399
+ FacetValuesResponse: {
400
+ facet: string;
401
+ values: components["schemas"]["FacetValueDto"][];
402
+ };
321
403
  ListingResponse: {
322
404
  appliedFilters: components["schemas"]["AppliedFilterDto"][];
323
405
  facets: components["schemas"]["FacetDto"][];
@@ -414,6 +496,7 @@ export interface components {
414
496
  vatRate: string;
415
497
  };
416
498
  ProductCardDto: {
499
+ brand: null | components["schemas"]["BrandDto"];
417
500
  collections: string[];
418
501
  id: string;
419
502
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -421,6 +504,7 @@ export interface components {
421
504
  name: string;
422
505
  price: null | components["schemas"]["PriceDto"];
423
506
  priceVaries: boolean;
507
+ shortDescription: null | string;
424
508
  sku: string;
425
509
  slug: string;
426
510
  /** Format: date-time */
@@ -431,8 +515,12 @@ export interface components {
431
515
  alternateUrls: {
432
516
  [key: string]: string;
433
517
  };
518
+ attributes: components["schemas"]["AttributeDto"][];
519
+ brand: null | components["schemas"]["BrandDto"];
520
+ bundle: null | components["schemas"]["BundleDto"];
434
521
  categories: components["schemas"]["CategoryRefDto"][];
435
522
  collections: components["schemas"]["CollectionRefDto"][];
523
+ description: null | components["schemas"]["DescriptionDto"];
436
524
  files: components["schemas"]["MediaDescriptorDto"][];
437
525
  id: string;
438
526
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -441,7 +529,9 @@ export interface components {
441
529
  name: string;
442
530
  options: components["schemas"]["OptionDto"][];
443
531
  redirectTo: null | string;
532
+ related: components["schemas"]["RelatedGroupDto"][];
444
533
  seo: components["schemas"]["SeoDto"];
534
+ shortDescription: null | string;
445
535
  sku: string;
446
536
  slug: string;
447
537
  /** Format: date-time */
@@ -457,12 +547,18 @@ export interface components {
457
547
  min: string;
458
548
  mode: string;
459
549
  selected: null | components["schemas"]["RangeSelectionDto"];
550
+ step: null | string;
460
551
  unit: null | string;
461
552
  };
462
553
  RangeSelectionDto: {
463
554
  max: null | string;
464
555
  min: null | string;
465
556
  };
557
+ RelatedGroupDto: {
558
+ key: string;
559
+ label: string;
560
+ products: components["schemas"]["ProductCardDto"][];
561
+ };
466
562
  SeoDto: {
467
563
  description: null | string;
468
564
  title: string;
@@ -532,6 +628,22 @@ export interface components {
532
628
  /** @description Link to this code's documentation. */
533
629
  type?: string;
534
630
  };
631
+ SuggestCategoryDto: {
632
+ id: string;
633
+ name: string;
634
+ path: string[];
635
+ };
636
+ SuggestFacetValueDto: {
637
+ facet: string;
638
+ key: string;
639
+ label: string;
640
+ };
641
+ SuggestResponse: {
642
+ categories: components["schemas"]["SuggestCategoryDto"][];
643
+ facetValues: components["schemas"]["SuggestFacetValueDto"][];
644
+ products: components["schemas"]["ProductCardDto"][];
645
+ query: string;
646
+ };
535
647
  SwatchDto: {
536
648
  color: null | string;
537
649
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -1630,6 +1742,96 @@ export interface operations {
1630
1742
  };
1631
1743
  };
1632
1744
  };
1745
+ SearchFacetValues: {
1746
+ parameters: {
1747
+ query?: {
1748
+ /** @description Narrows to the values whose label contains q (case-insensitively) or whose key starts with it. Omit for the facet's values as they are. Matched against the 1000 most common values of the facet, in this API — at most 50 are returned. Present but blank is a 400 (INVALID_QUERY). */
1749
+ q?: string;
1750
+ /** @description The filter context the values are counted in, sent as filter[key]=value exactly as on /products: a count is what selecting that value would leave. This facet's own selection does not narrow it (the counts are disjunctive), and a key this site has no facet for is dropped silently. */
1751
+ filter?: {
1752
+ [key: string]: string;
1753
+ };
1754
+ };
1755
+ header: {
1756
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
1757
+ "X-Site-Key": string;
1758
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
1759
+ "X-Market"?: string;
1760
+ /** @description BCP-47 language tag. Omit to use the market's language. */
1761
+ "Accept-Language"?: string;
1762
+ };
1763
+ path: {
1764
+ key: string;
1765
+ };
1766
+ cookie?: never;
1767
+ };
1768
+ requestBody?: never;
1769
+ responses: {
1770
+ /** @description OK */
1771
+ 200: {
1772
+ headers: {
1773
+ [name: string]: unknown;
1774
+ };
1775
+ content: {
1776
+ "application/json": components["schemas"]["FacetValuesResponse"];
1777
+ };
1778
+ };
1779
+ /** @description problem+json. MARKET_UNKNOWN (no such market on this site), INVALID_PAGE, or INVALID_QUERY (q present but blank, or longer than 100 characters), or FACET_NOT_SEARCHABLE (the facet exists but has no list of values to search in: a range, a toggle or a tree). */
1780
+ 400: {
1781
+ headers: {
1782
+ [name: string]: unknown;
1783
+ };
1784
+ content: {
1785
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1786
+ };
1787
+ };
1788
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
1789
+ 401: {
1790
+ headers: {
1791
+ [name: string]: unknown;
1792
+ };
1793
+ content: {
1794
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1795
+ };
1796
+ };
1797
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
1798
+ 403: {
1799
+ headers: {
1800
+ [name: string]: unknown;
1801
+ };
1802
+ content: {
1803
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1804
+ };
1805
+ };
1806
+ /** @description problem+json. FACET_NOT_FOUND, or SITE_INACTIVE when the site itself is not live. */
1807
+ 404: {
1808
+ headers: {
1809
+ [name: string]: unknown;
1810
+ };
1811
+ content: {
1812
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1813
+ };
1814
+ };
1815
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
1816
+ 429: {
1817
+ headers: {
1818
+ [name: string]: unknown;
1819
+ };
1820
+ content: {
1821
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1822
+ };
1823
+ };
1824
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
1825
+ 503: {
1826
+ headers: {
1827
+ [name: string]: unknown;
1828
+ };
1829
+ content: {
1830
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1831
+ };
1832
+ };
1833
+ };
1834
+ };
1633
1835
  GetProduct: {
1634
1836
  parameters: {
1635
1837
  query?: never;
@@ -1805,6 +2007,90 @@ export interface operations {
1805
2007
  };
1806
2008
  };
1807
2009
  };
2010
+ Suggest: {
2011
+ parameters: {
2012
+ query: {
2013
+ /** @description What the shopper has typed so far, 1–100 characters. Blank, missing or longer is a 400 (INVALID_QUERY) — unlike a listing's q, which is truncated. */
2014
+ q: string;
2015
+ };
2016
+ header: {
2017
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
2018
+ "X-Site-Key": string;
2019
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
2020
+ "X-Market"?: string;
2021
+ /** @description BCP-47 language tag. Omit to use the market's language. */
2022
+ "Accept-Language"?: string;
2023
+ };
2024
+ path?: never;
2025
+ cookie?: never;
2026
+ };
2027
+ requestBody?: never;
2028
+ responses: {
2029
+ /** @description OK */
2030
+ 200: {
2031
+ headers: {
2032
+ [name: string]: unknown;
2033
+ };
2034
+ content: {
2035
+ "application/json": components["schemas"]["SuggestResponse"];
2036
+ };
2037
+ };
2038
+ /** @description problem+json. MARKET_UNKNOWN (no such market on this site), INVALID_PAGE, or INVALID_QUERY (q missing, blank or longer than 100 characters). */
2039
+ 400: {
2040
+ headers: {
2041
+ [name: string]: unknown;
2042
+ };
2043
+ content: {
2044
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2045
+ };
2046
+ };
2047
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
2048
+ 401: {
2049
+ headers: {
2050
+ [name: string]: unknown;
2051
+ };
2052
+ content: {
2053
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2054
+ };
2055
+ };
2056
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
2057
+ 403: {
2058
+ headers: {
2059
+ [name: string]: unknown;
2060
+ };
2061
+ content: {
2062
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2063
+ };
2064
+ };
2065
+ /** @description problem+json. SITE_INACTIVE, or NOT_FOUND for an unrouted path. */
2066
+ 404: {
2067
+ headers: {
2068
+ [name: string]: unknown;
2069
+ };
2070
+ content: {
2071
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2072
+ };
2073
+ };
2074
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
2075
+ 429: {
2076
+ headers: {
2077
+ [name: string]: unknown;
2078
+ };
2079
+ content: {
2080
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2081
+ };
2082
+ };
2083
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
2084
+ 503: {
2085
+ headers: {
2086
+ [name: string]: unknown;
2087
+ };
2088
+ content: {
2089
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2090
+ };
2091
+ };
2092
+ };
2093
+ };
1808
2094
  GetSite: {
1809
2095
  parameters: {
1810
2096
  query?: never;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asstio-dev/storefront",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "description": "Typed client for the Asstio Storefront API (generated from OpenAPI).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  "build": "npm run generate && tsc -p tsconfig.json && node -e \"require('node:fs').copyFileSync('src/schema.d.ts','dist/schema.d.ts')\" && npm run typecheck-examples && npm run smoke",
21
21
  "test": "vitest run",
22
22
  "check": "npm run generate && npm run typecheck-examples && npm test && git diff --exit-code -- src/schema.d.ts && test -z \"$(git status --porcelain -- src/schema.d.ts)\"",
23
- "smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
23
+ "smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"encodeFilterValues\\\", \\\"decodeFilterValues\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
24
24
  },
25
25
  "dependencies": {
26
26
  "openapi-fetch": "0.17.0"
package/src/schema.d.ts CHANGED
@@ -153,6 +153,23 @@ export interface paths {
153
153
  patch?: never;
154
154
  trace?: never;
155
155
  };
156
+ "/v1/products/facets/{key}": {
157
+ parameters: {
158
+ query?: never;
159
+ header?: never;
160
+ path?: never;
161
+ cookie?: never;
162
+ };
163
+ /** Search within a facet: values whose label matches q, counted in this request's filter[...] context (spelled as on /products). Free text and collections are not applied. */
164
+ get: operations["SearchFacetValues"];
165
+ put?: never;
166
+ post?: never;
167
+ delete?: never;
168
+ options?: never;
169
+ head?: never;
170
+ patch?: never;
171
+ trace?: never;
172
+ };
156
173
  "/v1/products/{slugOrId}": {
157
174
  parameters: {
158
175
  query?: never;
@@ -187,6 +204,23 @@ export interface paths {
187
204
  patch?: never;
188
205
  trace?: never;
189
206
  };
207
+ "/v1/search/suggest": {
208
+ parameters: {
209
+ query?: never;
210
+ header?: never;
211
+ path?: never;
212
+ cookie?: never;
213
+ };
214
+ /** Search-as-you-type: products by name prefix, categories and facet values whose label starts with q. Market, assortment and language as /products. */
215
+ get: operations["Suggest"];
216
+ put?: never;
217
+ post?: never;
218
+ delete?: never;
219
+ options?: never;
220
+ head?: never;
221
+ patch?: never;
222
+ trace?: never;
223
+ };
190
224
  "/v1/site": {
191
225
  parameters: {
192
226
  query?: never;
@@ -235,6 +269,27 @@ export interface components {
235
269
  label: string;
236
270
  value: string;
237
271
  };
272
+ AttributeDto: {
273
+ facetKey: null | string;
274
+ html: null | string;
275
+ key: string;
276
+ label: string;
277
+ type: string;
278
+ value: null | string;
279
+ valueKeys: string[] | null;
280
+ values: string[] | null;
281
+ };
282
+ BrandDto: {
283
+ facet: null | components["schemas"]["FacetLinkDto"];
284
+ label: string;
285
+ };
286
+ BundleComponentDto: {
287
+ product: components["schemas"]["ProductCardDto"];
288
+ quantity: string;
289
+ };
290
+ BundleDto: {
291
+ components: components["schemas"]["BundleComponentDto"][];
292
+ };
238
293
  CartDto: {
239
294
  currency: string;
240
295
  grandTotalInc: string;
@@ -251,10 +306,22 @@ export interface components {
251
306
  id: string;
252
307
  image: null | components["schemas"]["MediaDescriptorDto"];
253
308
  price: components["schemas"]["PriceDto"];
254
- productId: string;
309
+ product: components["schemas"]["CartLineProductDto"];
255
310
  /** Format: int32 */
256
311
  quantity: number | string;
257
312
  sku: string;
313
+ variant: components["schemas"]["CartLineVariantDto"];
314
+ };
315
+ CartLineProductDto: {
316
+ id: string;
317
+ name: string;
318
+ slug: string;
319
+ };
320
+ CartLineVariantDto: {
321
+ id: string;
322
+ selectedOptions: {
323
+ [key: string]: string;
324
+ };
258
325
  };
259
326
  CategoryNodeDto: {
260
327
  children: components["schemas"]["CategoryNodeDto"][];
@@ -298,7 +365,13 @@ export interface components {
298
365
  slug: string;
299
366
  url: string;
300
367
  };
368
+ DescriptionDto: {
369
+ html: string;
370
+ text: string;
371
+ };
301
372
  FacetDto: {
373
+ collapsed: boolean;
374
+ help: null | string;
302
375
  key: string;
303
376
  label: string;
304
377
  match: string;
@@ -308,16 +381,25 @@ export interface components {
308
381
  type: string;
309
382
  values: components["schemas"]["FacetValueDto"][];
310
383
  };
384
+ FacetLinkDto: {
385
+ key: string;
386
+ valueKey: string;
387
+ };
311
388
  FacetValueDto: {
312
389
  children: components["schemas"]["FacetValueDto"][] | null;
313
390
  /** Format: int64 */
314
391
  count: number | string;
392
+ help: null | string;
315
393
  key: string;
316
394
  label: string;
317
395
  pinned: boolean;
318
396
  selected: boolean;
319
397
  swatch: null | components["schemas"]["SwatchDto"];
320
398
  };
399
+ FacetValuesResponse: {
400
+ facet: string;
401
+ values: components["schemas"]["FacetValueDto"][];
402
+ };
321
403
  ListingResponse: {
322
404
  appliedFilters: components["schemas"]["AppliedFilterDto"][];
323
405
  facets: components["schemas"]["FacetDto"][];
@@ -414,6 +496,7 @@ export interface components {
414
496
  vatRate: string;
415
497
  };
416
498
  ProductCardDto: {
499
+ brand: null | components["schemas"]["BrandDto"];
417
500
  collections: string[];
418
501
  id: string;
419
502
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -421,6 +504,7 @@ export interface components {
421
504
  name: string;
422
505
  price: null | components["schemas"]["PriceDto"];
423
506
  priceVaries: boolean;
507
+ shortDescription: null | string;
424
508
  sku: string;
425
509
  slug: string;
426
510
  /** Format: date-time */
@@ -431,8 +515,12 @@ export interface components {
431
515
  alternateUrls: {
432
516
  [key: string]: string;
433
517
  };
518
+ attributes: components["schemas"]["AttributeDto"][];
519
+ brand: null | components["schemas"]["BrandDto"];
520
+ bundle: null | components["schemas"]["BundleDto"];
434
521
  categories: components["schemas"]["CategoryRefDto"][];
435
522
  collections: components["schemas"]["CollectionRefDto"][];
523
+ description: null | components["schemas"]["DescriptionDto"];
436
524
  files: components["schemas"]["MediaDescriptorDto"][];
437
525
  id: string;
438
526
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -441,7 +529,9 @@ export interface components {
441
529
  name: string;
442
530
  options: components["schemas"]["OptionDto"][];
443
531
  redirectTo: null | string;
532
+ related: components["schemas"]["RelatedGroupDto"][];
444
533
  seo: components["schemas"]["SeoDto"];
534
+ shortDescription: null | string;
445
535
  sku: string;
446
536
  slug: string;
447
537
  /** Format: date-time */
@@ -457,12 +547,18 @@ export interface components {
457
547
  min: string;
458
548
  mode: string;
459
549
  selected: null | components["schemas"]["RangeSelectionDto"];
550
+ step: null | string;
460
551
  unit: null | string;
461
552
  };
462
553
  RangeSelectionDto: {
463
554
  max: null | string;
464
555
  min: null | string;
465
556
  };
557
+ RelatedGroupDto: {
558
+ key: string;
559
+ label: string;
560
+ products: components["schemas"]["ProductCardDto"][];
561
+ };
466
562
  SeoDto: {
467
563
  description: null | string;
468
564
  title: string;
@@ -532,6 +628,22 @@ export interface components {
532
628
  /** @description Link to this code's documentation. */
533
629
  type?: string;
534
630
  };
631
+ SuggestCategoryDto: {
632
+ id: string;
633
+ name: string;
634
+ path: string[];
635
+ };
636
+ SuggestFacetValueDto: {
637
+ facet: string;
638
+ key: string;
639
+ label: string;
640
+ };
641
+ SuggestResponse: {
642
+ categories: components["schemas"]["SuggestCategoryDto"][];
643
+ facetValues: components["schemas"]["SuggestFacetValueDto"][];
644
+ products: components["schemas"]["ProductCardDto"][];
645
+ query: string;
646
+ };
535
647
  SwatchDto: {
536
648
  color: null | string;
537
649
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -1630,6 +1742,96 @@ export interface operations {
1630
1742
  };
1631
1743
  };
1632
1744
  };
1745
+ SearchFacetValues: {
1746
+ parameters: {
1747
+ query?: {
1748
+ /** @description Narrows to the values whose label contains q (case-insensitively) or whose key starts with it. Omit for the facet's values as they are. Matched against the 1000 most common values of the facet, in this API — at most 50 are returned. Present but blank is a 400 (INVALID_QUERY). */
1749
+ q?: string;
1750
+ /** @description The filter context the values are counted in, sent as filter[key]=value exactly as on /products: a count is what selecting that value would leave. This facet's own selection does not narrow it (the counts are disjunctive), and a key this site has no facet for is dropped silently. */
1751
+ filter?: {
1752
+ [key: string]: string;
1753
+ };
1754
+ };
1755
+ header: {
1756
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
1757
+ "X-Site-Key": string;
1758
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
1759
+ "X-Market"?: string;
1760
+ /** @description BCP-47 language tag. Omit to use the market's language. */
1761
+ "Accept-Language"?: string;
1762
+ };
1763
+ path: {
1764
+ key: string;
1765
+ };
1766
+ cookie?: never;
1767
+ };
1768
+ requestBody?: never;
1769
+ responses: {
1770
+ /** @description OK */
1771
+ 200: {
1772
+ headers: {
1773
+ [name: string]: unknown;
1774
+ };
1775
+ content: {
1776
+ "application/json": components["schemas"]["FacetValuesResponse"];
1777
+ };
1778
+ };
1779
+ /** @description problem+json. MARKET_UNKNOWN (no such market on this site), INVALID_PAGE, or INVALID_QUERY (q present but blank, or longer than 100 characters), or FACET_NOT_SEARCHABLE (the facet exists but has no list of values to search in: a range, a toggle or a tree). */
1780
+ 400: {
1781
+ headers: {
1782
+ [name: string]: unknown;
1783
+ };
1784
+ content: {
1785
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1786
+ };
1787
+ };
1788
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
1789
+ 401: {
1790
+ headers: {
1791
+ [name: string]: unknown;
1792
+ };
1793
+ content: {
1794
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1795
+ };
1796
+ };
1797
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
1798
+ 403: {
1799
+ headers: {
1800
+ [name: string]: unknown;
1801
+ };
1802
+ content: {
1803
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1804
+ };
1805
+ };
1806
+ /** @description problem+json. FACET_NOT_FOUND, or SITE_INACTIVE when the site itself is not live. */
1807
+ 404: {
1808
+ headers: {
1809
+ [name: string]: unknown;
1810
+ };
1811
+ content: {
1812
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1813
+ };
1814
+ };
1815
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
1816
+ 429: {
1817
+ headers: {
1818
+ [name: string]: unknown;
1819
+ };
1820
+ content: {
1821
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1822
+ };
1823
+ };
1824
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
1825
+ 503: {
1826
+ headers: {
1827
+ [name: string]: unknown;
1828
+ };
1829
+ content: {
1830
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1831
+ };
1832
+ };
1833
+ };
1834
+ };
1633
1835
  GetProduct: {
1634
1836
  parameters: {
1635
1837
  query?: never;
@@ -1805,6 +2007,90 @@ export interface operations {
1805
2007
  };
1806
2008
  };
1807
2009
  };
2010
+ Suggest: {
2011
+ parameters: {
2012
+ query: {
2013
+ /** @description What the shopper has typed so far, 1–100 characters. Blank, missing or longer is a 400 (INVALID_QUERY) — unlike a listing's q, which is truncated. */
2014
+ q: string;
2015
+ };
2016
+ header: {
2017
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
2018
+ "X-Site-Key": string;
2019
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
2020
+ "X-Market"?: string;
2021
+ /** @description BCP-47 language tag. Omit to use the market's language. */
2022
+ "Accept-Language"?: string;
2023
+ };
2024
+ path?: never;
2025
+ cookie?: never;
2026
+ };
2027
+ requestBody?: never;
2028
+ responses: {
2029
+ /** @description OK */
2030
+ 200: {
2031
+ headers: {
2032
+ [name: string]: unknown;
2033
+ };
2034
+ content: {
2035
+ "application/json": components["schemas"]["SuggestResponse"];
2036
+ };
2037
+ };
2038
+ /** @description problem+json. MARKET_UNKNOWN (no such market on this site), INVALID_PAGE, or INVALID_QUERY (q missing, blank or longer than 100 characters). */
2039
+ 400: {
2040
+ headers: {
2041
+ [name: string]: unknown;
2042
+ };
2043
+ content: {
2044
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2045
+ };
2046
+ };
2047
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
2048
+ 401: {
2049
+ headers: {
2050
+ [name: string]: unknown;
2051
+ };
2052
+ content: {
2053
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2054
+ };
2055
+ };
2056
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
2057
+ 403: {
2058
+ headers: {
2059
+ [name: string]: unknown;
2060
+ };
2061
+ content: {
2062
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2063
+ };
2064
+ };
2065
+ /** @description problem+json. SITE_INACTIVE, or NOT_FOUND for an unrouted path. */
2066
+ 404: {
2067
+ headers: {
2068
+ [name: string]: unknown;
2069
+ };
2070
+ content: {
2071
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2072
+ };
2073
+ };
2074
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
2075
+ 429: {
2076
+ headers: {
2077
+ [name: string]: unknown;
2078
+ };
2079
+ content: {
2080
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2081
+ };
2082
+ };
2083
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
2084
+ 503: {
2085
+ headers: {
2086
+ [name: string]: unknown;
2087
+ };
2088
+ content: {
2089
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2090
+ };
2091
+ };
2092
+ };
2093
+ };
1808
2094
  GetSite: {
1809
2095
  parameters: {
1810
2096
  query?: never;