@asstio-dev/storefront 0.2.0 → 0.5.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
@@ -55,6 +55,80 @@ const { data: page } = await storefront.GET("/v1/products/{slugOrId}/variants",
55
55
  Every example in this file is compiled as part of `npm run check` — the source of them is
56
56
  [`examples/readme.ts`](examples/readme.ts), which also covers product lookup, ETag revalidation and crawling.
57
57
 
58
+ ### Images
59
+
60
+ Products, cards, variants and cart lines carry **media descriptors**, never URLs. Combine a descriptor with the
61
+ `media` block of `/v1/site` to build `src`, `srcset` and `<picture>` sources. The helpers only ever produce a URL
62
+ the CDN actually serves: a width and a format the descriptor announces, and the descriptor's own `recipe`.
63
+
64
+ ```ts
65
+ import { pictureSources, fileUrl } from "@asstio-dev/storefront";
66
+
67
+ const { data: site } = await storefront.GET("/v1/site");
68
+ const { data: product } = await storefront.GET("/v1/products/{slugOrId}", { params: { path: { slugOrId: "acuvue-oasys" } } });
69
+
70
+ if (site && product?.image) {
71
+ const { sources, img } = pictureSources(site.media, product.image, "(min-width: 900px) 33vw, 100vw");
72
+ // <picture>{sources.map(s => <source {...s} />)}<img {...img} alt={product.image.alt["sv"] ?? product.name} /></picture>
73
+ }
74
+ for (const file of product?.files ?? []) console.log(file.filename, fileUrl(site!.media, file));
75
+ // A variant with hasOwnImages: false inherits: variant.hasOwnImages ? variant.images : product.images
76
+ ```
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
+
58
132
  ## Headers
59
133
 
60
134
  Every call the client makes carries these; you never set them by hand.
@@ -95,7 +169,8 @@ Every operation documents `400`, `401`, `403`, `404`, `429` and `503`; the entit
95
169
  Codes you will meet: `SITE_KEY_MISSING`, `SITE_KEY_INVALID`, `SITE_INACTIVE`, `SECRET_KEY_FROM_BROWSER`,
96
170
  `SITE_REGISTRY_WARMING`, `MARKET_UNKNOWN`, `RATE_LIMITED` (with `retryAfterSeconds`, plus a `Retry-After`
97
171
  header), `PRODUCT_NOT_FOUND`, `COLLECTION_NOT_FOUND`, `INVALID_PAGE`, `INVALID_SINCE`, `INVALID_TYPE`,
98
- `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`.
99
174
 
100
175
  `SITE_REGISTRY_WARMING` and `INDEX_UNAVAILABLE` are retries, not bugs in your call.
101
176
 
@@ -120,6 +195,9 @@ form the API reads. `filterQuery()` is a thin helper that joins multi-select val
120
195
  `/v1/sitemap` takes `type` (`product` | `collection`), `since` (ISO-8601, inclusive), `page` and `pageSize`
121
196
  (max 1000, default 500).
122
197
 
198
+ `/v1/search/suggest` takes `q` only, and `/v1/products/facets/{key}` takes `q` plus the same `filter` object as a
199
+ listing — both are described under [Search-as-you-type and search-in-facet](#search-as-you-type-and-search-in-facet).
200
+
123
201
  ## Caching
124
202
 
125
203
  Read responses carry an `ETag` and `Vary: X-Site-Key, X-Market, Accept-Language` — the answer depends on
package/dist/index.d.ts CHANGED
@@ -44,3 +44,4 @@ export type FilterSelection = NonNullable<NonNullable<paths["/v1/products"]["get
44
44
  * The client sends it as `filter[brand]=dior,chanel` (deepObject), which is what the API reads.
45
45
  */
46
46
  export declare function filterQuery(selection: Record<string, string | string[] | undefined>): FilterSelection;
47
+ 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
@@ -32,3 +32,4 @@ export function filterQuery(selection) {
32
32
  }
33
33
  return out;
34
34
  }
35
+ export { fileUrl, mediaHash, mediaUrl, MediaUrlError, pickWidth, pictureSources, posterUrl, srcSet, videoSources, } from "./media.js";
@@ -0,0 +1,51 @@
1
+ import type { components } from "./schema";
2
+ export type MediaDescriptor = components["schemas"]["MediaDescriptorDto"];
3
+ export type MediaConfig = components["schemas"]["MediaDto"];
4
+ export type PictureSources = {
5
+ sources: {
6
+ type: string;
7
+ srcSet: string;
8
+ sizes: string;
9
+ }[];
10
+ img: {
11
+ src: string;
12
+ srcSet: string;
13
+ sizes: string;
14
+ width?: number;
15
+ height?: number;
16
+ };
17
+ };
18
+ export type VideoSource = {
19
+ src: string;
20
+ type: string;
21
+ width: number;
22
+ height: number;
23
+ name: string;
24
+ };
25
+ /** A descriptor that cannot produce the URL asked for: wrong kind, a format or width it does not carry. */
26
+ export declare class MediaUrlError extends Error {
27
+ constructor(message: string);
28
+ }
29
+ /** The descriptor id without its `sha256:` prefix — the `{hash}` of every template. */
30
+ export declare function mediaHash(d: MediaDescriptor): string;
31
+ /** The smallest rendition at or above `width`, else the largest — only ever a width the descriptor announces. */
32
+ export declare function pickWidth(d: MediaDescriptor, width: number): number;
33
+ /** One image URL. `format` defaults to the last one announced (jpg, or gif for an animated GIF). */
34
+ export declare function mediaUrl(site: MediaConfig, d: MediaDescriptor, opts: {
35
+ width: number;
36
+ format?: string;
37
+ }): string;
38
+ /** `srcset` over every actual width of one format. */
39
+ export declare function srcSet(site: MediaConfig, d: MediaDescriptor, format: string): string;
40
+ /**
41
+ * Everything a `<picture>` needs: one `<source>` per format except the last (avif first, as the feed lists it),
42
+ * and the `<img>` in the last format with the middle width as `src`. One format ⇒ no sources, only the img (T15).
43
+ * `sizes` is mandatory — without it the browser picks the largest candidate.
44
+ */
45
+ export declare function pictureSources(site: MediaConfig, d: MediaDescriptor, sizes: string): PictureSources;
46
+ /** Poster (video, PDF, animated GIF): the poster template over `poster.widths`; always avif. */
47
+ export declare function posterUrl(site: MediaConfig, d: MediaDescriptor, width: number): string;
48
+ /** The download itself: the file template plus the sanitized filename the descriptor carries. */
49
+ export declare function fileUrl(site: MediaConfig, d: MediaDescriptor): string;
50
+ /** Every encoded rendition of a video, for `<source>` elements. Empty for anything that is not a video. */
51
+ export declare function videoSources(site: MediaConfig, d: MediaDescriptor): VideoSource[];
package/dist/media.js ADDED
@@ -0,0 +1,114 @@
1
+ /** A descriptor that cannot produce the URL asked for: wrong kind, a format or width it does not carry. */
2
+ export class MediaUrlError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "MediaUrlError";
6
+ }
7
+ }
8
+ const MIME = {
9
+ avif: "image/avif", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", png: "image/png",
10
+ mp4: "video/mp4", webm: "video/webm",
11
+ };
12
+ const mime = (format) => MIME[format] ?? `application/octet-stream`;
13
+ // `w`/`h`/`widths`/etc. are typed `number | string` in schema.d.ts (OpenAPI `format: int32` round-trips as
14
+ // either on the wire); every arithmetic/comparison use below normalizes through this first.
15
+ const toNumber = (v) => (typeof v === "number" ? v : Number(v));
16
+ /** The descriptor id without its `sha256:` prefix — the `{hash}` of every template. */
17
+ export function mediaHash(d) {
18
+ if (!d.id.startsWith("sha256:") || d.id.length !== 71)
19
+ throw new MediaUrlError(`media id '${d.id}' is not a sha256: descriptor id`);
20
+ return d.id.slice(7);
21
+ }
22
+ function fill(site, template, vars) {
23
+ const base = site.baseUrl.replace(/\/$/, "");
24
+ const path = template.replace(/\{(\w+)\}/g, (_, key) => {
25
+ const v = vars[key];
26
+ if (v === undefined)
27
+ throw new MediaUrlError(`template '${template}' needs '${key}'`);
28
+ return String(v);
29
+ });
30
+ return base + path;
31
+ }
32
+ function requireImage(d, what) {
33
+ // Branch on kind, never on emptiness: a file descriptor has widths: [] and formats: [] by contract.
34
+ if (d.kind !== "image" && d.kind !== "video")
35
+ throw new MediaUrlError(`${what}: kind '${d.kind}' has no image renditions; use fileUrl or posterUrl`);
36
+ if (d.widths.length === 0 || d.formats.length === 0)
37
+ throw new MediaUrlError(`${what}: descriptor ${d.id} announces no renditions`);
38
+ }
39
+ function pick(widths, width) {
40
+ const sorted = widths.map(toNumber).sort((a, b) => a - b);
41
+ return sorted.find((w) => w >= width) ?? sorted[sorted.length - 1];
42
+ }
43
+ /** The smallest rendition at or above `width`, else the largest — only ever a width the descriptor announces. */
44
+ export function pickWidth(d, width) {
45
+ requireImage(d, "pickWidth");
46
+ return pick(d.widths, width);
47
+ }
48
+ function requireFormat(d, format) {
49
+ const f = format ?? d.formats[d.formats.length - 1];
50
+ if (!d.formats.includes(f))
51
+ throw new MediaUrlError(`format '${f}' is not one of ${d.formats.join(", ")} for ${d.id}`);
52
+ return f;
53
+ }
54
+ /** One image URL. `format` defaults to the last one announced (jpg, or gif for an animated GIF). */
55
+ export function mediaUrl(site, d, opts) {
56
+ requireImage(d, "mediaUrl");
57
+ const format = requireFormat(d, opts.format);
58
+ return fill(site, site.templates.image, { hash: mediaHash(d), recipe: d.recipe, width: pick(d.widths, opts.width), format });
59
+ }
60
+ /** `srcset` over every actual width of one format. */
61
+ export function srcSet(site, d, format) {
62
+ requireImage(d, "srcSet");
63
+ const f = requireFormat(d, format);
64
+ return d.widths
65
+ .map(toNumber)
66
+ .sort((a, b) => a - b)
67
+ .map((w) => `${fill(site, site.templates.image, { hash: mediaHash(d), recipe: d.recipe, width: w, format: f })} ${w}w`)
68
+ .join(", ");
69
+ }
70
+ /**
71
+ * Everything a `<picture>` needs: one `<source>` per format except the last (avif first, as the feed lists it),
72
+ * and the `<img>` in the last format with the middle width as `src`. One format ⇒ no sources, only the img (T15).
73
+ * `sizes` is mandatory — without it the browser picks the largest candidate.
74
+ */
75
+ export function pictureSources(site, d, sizes) {
76
+ requireImage(d, "pictureSources");
77
+ const formats = d.formats;
78
+ const last = formats[formats.length - 1];
79
+ const widths = d.widths.map(toNumber).sort((a, b) => a - b);
80
+ return {
81
+ sources: formats.slice(0, -1).map((f) => ({ type: mime(f), srcSet: srcSet(site, d, f), sizes })),
82
+ img: {
83
+ src: mediaUrl(site, d, { width: widths[Math.floor(widths.length / 2)], format: last }),
84
+ srcSet: srcSet(site, d, last),
85
+ sizes,
86
+ width: d.w == null ? undefined : toNumber(d.w),
87
+ height: d.h == null ? undefined : toNumber(d.h),
88
+ },
89
+ };
90
+ }
91
+ /** Poster (video, PDF, animated GIF): the poster template over `poster.widths`; always avif. */
92
+ export function posterUrl(site, d, width) {
93
+ if (!d.poster || d.poster.widths.length === 0)
94
+ throw new MediaUrlError(`posterUrl: descriptor ${d.id} has no poster`);
95
+ return fill(site, site.templates.poster, { hash: mediaHash(d), recipe: d.recipe, width: pick(d.poster.widths, width) });
96
+ }
97
+ /** The download itself: the file template plus the sanitized filename the descriptor carries. */
98
+ export function fileUrl(site, d) {
99
+ if (d.kind !== "file" || !d.filename)
100
+ throw new MediaUrlError(`fileUrl: descriptor ${d.id} is not a file`);
101
+ return fill(site, site.templates.file, { hash: mediaHash(d), recipe: d.recipe, filename: d.filename });
102
+ }
103
+ /** Every encoded rendition of a video, for `<source>` elements. Empty for anything that is not a video. */
104
+ export function videoSources(site, d) {
105
+ if (d.kind !== "video" || !d.variants)
106
+ return [];
107
+ return d.variants.map((v) => ({
108
+ src: fill(site, site.templates.video, { hash: mediaHash(d), recipe: d.recipe, variant: v.name, format: v.format }),
109
+ type: mime(v.format),
110
+ width: toNumber(v.w),
111
+ height: toNumber(v.h),
112
+ name: v.name,
113
+ }));
114
+ }
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;
@@ -249,11 +304,24 @@ export interface components {
249
304
  };
250
305
  CartLineDto: {
251
306
  id: string;
307
+ image: null | components["schemas"]["MediaDescriptorDto"];
252
308
  price: components["schemas"]["PriceDto"];
253
- productId: string;
309
+ product: components["schemas"]["CartLineProductDto"];
254
310
  /** Format: int32 */
255
311
  quantity: number | string;
256
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
+ };
257
325
  };
258
326
  CategoryNodeDto: {
259
327
  children: components["schemas"]["CategoryNodeDto"][];
@@ -297,6 +365,10 @@ export interface components {
297
365
  slug: string;
298
366
  url: string;
299
367
  };
368
+ DescriptionDto: {
369
+ html: string;
370
+ text: string;
371
+ };
300
372
  FacetDto: {
301
373
  key: string;
302
374
  label: string;
@@ -307,6 +379,10 @@ export interface components {
307
379
  type: string;
308
380
  values: components["schemas"]["FacetValueDto"][];
309
381
  };
382
+ FacetLinkDto: {
383
+ key: string;
384
+ valueKey: string;
385
+ };
310
386
  FacetValueDto: {
311
387
  children: components["schemas"]["FacetValueDto"][] | null;
312
388
  /** Format: int64 */
@@ -317,6 +393,10 @@ export interface components {
317
393
  selected: boolean;
318
394
  swatch: null | components["schemas"]["SwatchDto"];
319
395
  };
396
+ FacetValuesResponse: {
397
+ facet: string;
398
+ values: components["schemas"]["FacetValueDto"][];
399
+ };
320
400
  ListingResponse: {
321
401
  appliedFilters: components["schemas"]["AppliedFilterDto"][];
322
402
  facets: components["schemas"]["FacetDto"][];
@@ -339,6 +419,68 @@ export interface components {
339
419
  language: string;
340
420
  urlTemplate: string;
341
421
  };
422
+ MediaDescriptorDto: {
423
+ alt: {
424
+ [key: string]: string;
425
+ };
426
+ /** @description Passed through opaque, exactly as Purchase wrote it (spec 2026-09-18 §3); no fixed shape. */
427
+ attributes: null | {
428
+ [key: string]: unknown;
429
+ };
430
+ bg: null | string;
431
+ blurhash: null | string;
432
+ /** Format: int64 */
433
+ bytes: number | null | string;
434
+ caption: null | {
435
+ [key: string]: string;
436
+ };
437
+ color: null | string;
438
+ /** Format: int32 */
439
+ duration: number | null | string;
440
+ filename: null | string;
441
+ formats: string[];
442
+ /** Format: int32 */
443
+ h: number | null | string;
444
+ id: string;
445
+ kind: string;
446
+ /** Format: int32 */
447
+ order: number | string;
448
+ /** Format: int32 */
449
+ pages: number | null | string;
450
+ poster: null | components["schemas"]["MediaPosterDto"];
451
+ /** Format: int32 */
452
+ recipe: number | string;
453
+ role: string;
454
+ variants: components["schemas"]["MediaVideoVariantDto"][] | null;
455
+ view: null | string;
456
+ /** Format: int32 */
457
+ w: number | null | string;
458
+ widths: (number | string)[];
459
+ };
460
+ MediaDto: {
461
+ baseUrl: string;
462
+ templates: components["schemas"]["MediaTemplatesDto"];
463
+ };
464
+ MediaPosterDto: {
465
+ formats: string[];
466
+ widths: (number | string)[];
467
+ };
468
+ MediaTemplatesDto: {
469
+ file: string;
470
+ image: string;
471
+ poster: string;
472
+ video: string;
473
+ };
474
+ MediaVideoVariantDto: {
475
+ /** Format: int64 */
476
+ bytes: number | string;
477
+ format: string;
478
+ /** Format: int32 */
479
+ h: number | string;
480
+ name: string;
481
+ /** Format: int32 */
482
+ w: number | string;
483
+ };
342
484
  OptionDto: {
343
485
  name: string;
344
486
  values: string[];
@@ -351,12 +493,15 @@ export interface components {
351
493
  vatRate: string;
352
494
  };
353
495
  ProductCardDto: {
496
+ brand: null | components["schemas"]["BrandDto"];
354
497
  collections: string[];
355
498
  id: string;
499
+ image: null | components["schemas"]["MediaDescriptorDto"];
356
500
  inStock: boolean;
357
501
  name: string;
358
502
  price: null | components["schemas"]["PriceDto"];
359
503
  priceVaries: boolean;
504
+ shortDescription: null | string;
360
505
  sku: string;
361
506
  slug: string;
362
507
  /** Format: date-time */
@@ -367,14 +512,23 @@ export interface components {
367
512
  alternateUrls: {
368
513
  [key: string]: string;
369
514
  };
515
+ attributes: components["schemas"]["AttributeDto"][];
516
+ brand: null | components["schemas"]["BrandDto"];
517
+ bundle: null | components["schemas"]["BundleDto"];
370
518
  categories: components["schemas"]["CategoryRefDto"][];
371
519
  collections: components["schemas"]["CollectionRefDto"][];
520
+ description: null | components["schemas"]["DescriptionDto"];
521
+ files: components["schemas"]["MediaDescriptorDto"][];
372
522
  id: string;
523
+ image: null | components["schemas"]["MediaDescriptorDto"];
524
+ images: components["schemas"]["MediaDescriptorDto"][];
373
525
  matchedVariantId: null | string;
374
526
  name: string;
375
527
  options: components["schemas"]["OptionDto"][];
376
528
  redirectTo: null | string;
529
+ related: components["schemas"]["RelatedGroupDto"][];
377
530
  seo: components["schemas"]["SeoDto"];
531
+ shortDescription: null | string;
378
532
  sku: string;
379
533
  slug: string;
380
534
  /** Format: date-time */
@@ -390,12 +544,18 @@ export interface components {
390
544
  min: string;
391
545
  mode: string;
392
546
  selected: null | components["schemas"]["RangeSelectionDto"];
547
+ step: null | string;
393
548
  unit: null | string;
394
549
  };
395
550
  RangeSelectionDto: {
396
551
  max: null | string;
397
552
  min: null | string;
398
553
  };
554
+ RelatedGroupDto: {
555
+ key: string;
556
+ label: string;
557
+ products: components["schemas"]["ProductCardDto"][];
558
+ };
399
559
  SeoDto: {
400
560
  description: null | string;
401
561
  title: string;
@@ -408,6 +568,7 @@ export interface components {
408
568
  configVersion: string;
409
569
  defaultMarket: string;
410
570
  markets: components["schemas"]["MarketDto"][];
571
+ media: components["schemas"]["MediaDto"];
411
572
  name: string;
412
573
  preview: boolean;
413
574
  urlPatterns: components["schemas"]["UrlPatternsDto"];
@@ -464,9 +625,25 @@ export interface components {
464
625
  /** @description Link to this code's documentation. */
465
626
  type?: string;
466
627
  };
628
+ SuggestCategoryDto: {
629
+ id: string;
630
+ name: string;
631
+ path: string[];
632
+ };
633
+ SuggestFacetValueDto: {
634
+ facet: string;
635
+ key: string;
636
+ label: string;
637
+ };
638
+ SuggestResponse: {
639
+ categories: components["schemas"]["SuggestCategoryDto"][];
640
+ facetValues: components["schemas"]["SuggestFacetValueDto"][];
641
+ products: components["schemas"]["ProductCardDto"][];
642
+ query: string;
643
+ };
467
644
  SwatchDto: {
468
645
  color: null | string;
469
- imageUrl: null | string;
646
+ image: null | components["schemas"]["MediaDescriptorDto"];
470
647
  };
471
648
  UpdateCartLineRequest: {
472
649
  /** Format: int32 */
@@ -477,7 +654,10 @@ export interface components {
477
654
  product: string;
478
655
  };
479
656
  VariantDto: {
657
+ hasOwnImages: boolean;
480
658
  id: string;
659
+ image: null | components["schemas"]["MediaDescriptorDto"];
660
+ images: components["schemas"]["MediaDescriptorDto"][];
481
661
  inStock: boolean;
482
662
  price: null | components["schemas"]["PriceDto"];
483
663
  selectedOptions: {
@@ -1559,6 +1739,96 @@ export interface operations {
1559
1739
  };
1560
1740
  };
1561
1741
  };
1742
+ SearchFacetValues: {
1743
+ parameters: {
1744
+ query?: {
1745
+ /** @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). */
1746
+ q?: string;
1747
+ /** @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. */
1748
+ filter?: {
1749
+ [key: string]: string;
1750
+ };
1751
+ };
1752
+ header: {
1753
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
1754
+ "X-Site-Key": string;
1755
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
1756
+ "X-Market"?: string;
1757
+ /** @description BCP-47 language tag. Omit to use the market's language. */
1758
+ "Accept-Language"?: string;
1759
+ };
1760
+ path: {
1761
+ key: string;
1762
+ };
1763
+ cookie?: never;
1764
+ };
1765
+ requestBody?: never;
1766
+ responses: {
1767
+ /** @description OK */
1768
+ 200: {
1769
+ headers: {
1770
+ [name: string]: unknown;
1771
+ };
1772
+ content: {
1773
+ "application/json": components["schemas"]["FacetValuesResponse"];
1774
+ };
1775
+ };
1776
+ /** @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). */
1777
+ 400: {
1778
+ headers: {
1779
+ [name: string]: unknown;
1780
+ };
1781
+ content: {
1782
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1783
+ };
1784
+ };
1785
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
1786
+ 401: {
1787
+ headers: {
1788
+ [name: string]: unknown;
1789
+ };
1790
+ content: {
1791
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1792
+ };
1793
+ };
1794
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
1795
+ 403: {
1796
+ headers: {
1797
+ [name: string]: unknown;
1798
+ };
1799
+ content: {
1800
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1801
+ };
1802
+ };
1803
+ /** @description problem+json. FACET_NOT_FOUND, or SITE_INACTIVE when the site itself is not live. */
1804
+ 404: {
1805
+ headers: {
1806
+ [name: string]: unknown;
1807
+ };
1808
+ content: {
1809
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1810
+ };
1811
+ };
1812
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
1813
+ 429: {
1814
+ headers: {
1815
+ [name: string]: unknown;
1816
+ };
1817
+ content: {
1818
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1819
+ };
1820
+ };
1821
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
1822
+ 503: {
1823
+ headers: {
1824
+ [name: string]: unknown;
1825
+ };
1826
+ content: {
1827
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1828
+ };
1829
+ };
1830
+ };
1831
+ };
1562
1832
  GetProduct: {
1563
1833
  parameters: {
1564
1834
  query?: never;
@@ -1734,6 +2004,90 @@ export interface operations {
1734
2004
  };
1735
2005
  };
1736
2006
  };
2007
+ Suggest: {
2008
+ parameters: {
2009
+ query: {
2010
+ /** @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. */
2011
+ q: string;
2012
+ };
2013
+ header: {
2014
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
2015
+ "X-Site-Key": string;
2016
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
2017
+ "X-Market"?: string;
2018
+ /** @description BCP-47 language tag. Omit to use the market's language. */
2019
+ "Accept-Language"?: string;
2020
+ };
2021
+ path?: never;
2022
+ cookie?: never;
2023
+ };
2024
+ requestBody?: never;
2025
+ responses: {
2026
+ /** @description OK */
2027
+ 200: {
2028
+ headers: {
2029
+ [name: string]: unknown;
2030
+ };
2031
+ content: {
2032
+ "application/json": components["schemas"]["SuggestResponse"];
2033
+ };
2034
+ };
2035
+ /** @description problem+json. MARKET_UNKNOWN (no such market on this site), INVALID_PAGE, or INVALID_QUERY (q missing, blank or longer than 100 characters). */
2036
+ 400: {
2037
+ headers: {
2038
+ [name: string]: unknown;
2039
+ };
2040
+ content: {
2041
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2042
+ };
2043
+ };
2044
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
2045
+ 401: {
2046
+ headers: {
2047
+ [name: string]: unknown;
2048
+ };
2049
+ content: {
2050
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2051
+ };
2052
+ };
2053
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
2054
+ 403: {
2055
+ headers: {
2056
+ [name: string]: unknown;
2057
+ };
2058
+ content: {
2059
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2060
+ };
2061
+ };
2062
+ /** @description problem+json. SITE_INACTIVE, or NOT_FOUND for an unrouted path. */
2063
+ 404: {
2064
+ headers: {
2065
+ [name: string]: unknown;
2066
+ };
2067
+ content: {
2068
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2069
+ };
2070
+ };
2071
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
2072
+ 429: {
2073
+ headers: {
2074
+ [name: string]: unknown;
2075
+ };
2076
+ content: {
2077
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2078
+ };
2079
+ };
2080
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
2081
+ 503: {
2082
+ headers: {
2083
+ [name: string]: unknown;
2084
+ };
2085
+ content: {
2086
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2087
+ };
2088
+ };
2089
+ };
2090
+ };
1737
2091
  GetSite: {
1738
2092
  parameters: {
1739
2093
  query?: never;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asstio-dev/storefront",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "Typed client for the Asstio Storefront API (generated from OpenAPI).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,14 +17,17 @@
17
17
  "scripts": {
18
18
  "generate": "openapi-typescript ../../tests/Storefront.Api.Tests/Snapshots/openapi-v1.json -o src/schema.d.ts",
19
19
  "typecheck-examples": "tsc -p tsconfig.examples.json",
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",
21
- "check": "npm run generate && npm run typecheck-examples && git diff --exit-code -- src/schema.d.ts && test -z \"$(git status --porcelain -- src/schema.d.ts)\""
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
+ "test": "vitest run",
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) })\""
22
24
  },
23
25
  "dependencies": {
24
26
  "openapi-fetch": "0.17.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "openapi-typescript": "7.13.0",
28
- "typescript": "5.9.3"
30
+ "typescript": "5.9.3",
31
+ "vitest": "3.2.7"
29
32
  }
30
33
  }
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;
@@ -249,11 +304,24 @@ export interface components {
249
304
  };
250
305
  CartLineDto: {
251
306
  id: string;
307
+ image: null | components["schemas"]["MediaDescriptorDto"];
252
308
  price: components["schemas"]["PriceDto"];
253
- productId: string;
309
+ product: components["schemas"]["CartLineProductDto"];
254
310
  /** Format: int32 */
255
311
  quantity: number | string;
256
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
+ };
257
325
  };
258
326
  CategoryNodeDto: {
259
327
  children: components["schemas"]["CategoryNodeDto"][];
@@ -297,6 +365,10 @@ export interface components {
297
365
  slug: string;
298
366
  url: string;
299
367
  };
368
+ DescriptionDto: {
369
+ html: string;
370
+ text: string;
371
+ };
300
372
  FacetDto: {
301
373
  key: string;
302
374
  label: string;
@@ -307,6 +379,10 @@ export interface components {
307
379
  type: string;
308
380
  values: components["schemas"]["FacetValueDto"][];
309
381
  };
382
+ FacetLinkDto: {
383
+ key: string;
384
+ valueKey: string;
385
+ };
310
386
  FacetValueDto: {
311
387
  children: components["schemas"]["FacetValueDto"][] | null;
312
388
  /** Format: int64 */
@@ -317,6 +393,10 @@ export interface components {
317
393
  selected: boolean;
318
394
  swatch: null | components["schemas"]["SwatchDto"];
319
395
  };
396
+ FacetValuesResponse: {
397
+ facet: string;
398
+ values: components["schemas"]["FacetValueDto"][];
399
+ };
320
400
  ListingResponse: {
321
401
  appliedFilters: components["schemas"]["AppliedFilterDto"][];
322
402
  facets: components["schemas"]["FacetDto"][];
@@ -339,6 +419,68 @@ export interface components {
339
419
  language: string;
340
420
  urlTemplate: string;
341
421
  };
422
+ MediaDescriptorDto: {
423
+ alt: {
424
+ [key: string]: string;
425
+ };
426
+ /** @description Passed through opaque, exactly as Purchase wrote it (spec 2026-09-18 §3); no fixed shape. */
427
+ attributes: null | {
428
+ [key: string]: unknown;
429
+ };
430
+ bg: null | string;
431
+ blurhash: null | string;
432
+ /** Format: int64 */
433
+ bytes: number | null | string;
434
+ caption: null | {
435
+ [key: string]: string;
436
+ };
437
+ color: null | string;
438
+ /** Format: int32 */
439
+ duration: number | null | string;
440
+ filename: null | string;
441
+ formats: string[];
442
+ /** Format: int32 */
443
+ h: number | null | string;
444
+ id: string;
445
+ kind: string;
446
+ /** Format: int32 */
447
+ order: number | string;
448
+ /** Format: int32 */
449
+ pages: number | null | string;
450
+ poster: null | components["schemas"]["MediaPosterDto"];
451
+ /** Format: int32 */
452
+ recipe: number | string;
453
+ role: string;
454
+ variants: components["schemas"]["MediaVideoVariantDto"][] | null;
455
+ view: null | string;
456
+ /** Format: int32 */
457
+ w: number | null | string;
458
+ widths: (number | string)[];
459
+ };
460
+ MediaDto: {
461
+ baseUrl: string;
462
+ templates: components["schemas"]["MediaTemplatesDto"];
463
+ };
464
+ MediaPosterDto: {
465
+ formats: string[];
466
+ widths: (number | string)[];
467
+ };
468
+ MediaTemplatesDto: {
469
+ file: string;
470
+ image: string;
471
+ poster: string;
472
+ video: string;
473
+ };
474
+ MediaVideoVariantDto: {
475
+ /** Format: int64 */
476
+ bytes: number | string;
477
+ format: string;
478
+ /** Format: int32 */
479
+ h: number | string;
480
+ name: string;
481
+ /** Format: int32 */
482
+ w: number | string;
483
+ };
342
484
  OptionDto: {
343
485
  name: string;
344
486
  values: string[];
@@ -351,12 +493,15 @@ export interface components {
351
493
  vatRate: string;
352
494
  };
353
495
  ProductCardDto: {
496
+ brand: null | components["schemas"]["BrandDto"];
354
497
  collections: string[];
355
498
  id: string;
499
+ image: null | components["schemas"]["MediaDescriptorDto"];
356
500
  inStock: boolean;
357
501
  name: string;
358
502
  price: null | components["schemas"]["PriceDto"];
359
503
  priceVaries: boolean;
504
+ shortDescription: null | string;
360
505
  sku: string;
361
506
  slug: string;
362
507
  /** Format: date-time */
@@ -367,14 +512,23 @@ export interface components {
367
512
  alternateUrls: {
368
513
  [key: string]: string;
369
514
  };
515
+ attributes: components["schemas"]["AttributeDto"][];
516
+ brand: null | components["schemas"]["BrandDto"];
517
+ bundle: null | components["schemas"]["BundleDto"];
370
518
  categories: components["schemas"]["CategoryRefDto"][];
371
519
  collections: components["schemas"]["CollectionRefDto"][];
520
+ description: null | components["schemas"]["DescriptionDto"];
521
+ files: components["schemas"]["MediaDescriptorDto"][];
372
522
  id: string;
523
+ image: null | components["schemas"]["MediaDescriptorDto"];
524
+ images: components["schemas"]["MediaDescriptorDto"][];
373
525
  matchedVariantId: null | string;
374
526
  name: string;
375
527
  options: components["schemas"]["OptionDto"][];
376
528
  redirectTo: null | string;
529
+ related: components["schemas"]["RelatedGroupDto"][];
377
530
  seo: components["schemas"]["SeoDto"];
531
+ shortDescription: null | string;
378
532
  sku: string;
379
533
  slug: string;
380
534
  /** Format: date-time */
@@ -390,12 +544,18 @@ export interface components {
390
544
  min: string;
391
545
  mode: string;
392
546
  selected: null | components["schemas"]["RangeSelectionDto"];
547
+ step: null | string;
393
548
  unit: null | string;
394
549
  };
395
550
  RangeSelectionDto: {
396
551
  max: null | string;
397
552
  min: null | string;
398
553
  };
554
+ RelatedGroupDto: {
555
+ key: string;
556
+ label: string;
557
+ products: components["schemas"]["ProductCardDto"][];
558
+ };
399
559
  SeoDto: {
400
560
  description: null | string;
401
561
  title: string;
@@ -408,6 +568,7 @@ export interface components {
408
568
  configVersion: string;
409
569
  defaultMarket: string;
410
570
  markets: components["schemas"]["MarketDto"][];
571
+ media: components["schemas"]["MediaDto"];
411
572
  name: string;
412
573
  preview: boolean;
413
574
  urlPatterns: components["schemas"]["UrlPatternsDto"];
@@ -464,9 +625,25 @@ export interface components {
464
625
  /** @description Link to this code's documentation. */
465
626
  type?: string;
466
627
  };
628
+ SuggestCategoryDto: {
629
+ id: string;
630
+ name: string;
631
+ path: string[];
632
+ };
633
+ SuggestFacetValueDto: {
634
+ facet: string;
635
+ key: string;
636
+ label: string;
637
+ };
638
+ SuggestResponse: {
639
+ categories: components["schemas"]["SuggestCategoryDto"][];
640
+ facetValues: components["schemas"]["SuggestFacetValueDto"][];
641
+ products: components["schemas"]["ProductCardDto"][];
642
+ query: string;
643
+ };
467
644
  SwatchDto: {
468
645
  color: null | string;
469
- imageUrl: null | string;
646
+ image: null | components["schemas"]["MediaDescriptorDto"];
470
647
  };
471
648
  UpdateCartLineRequest: {
472
649
  /** Format: int32 */
@@ -477,7 +654,10 @@ export interface components {
477
654
  product: string;
478
655
  };
479
656
  VariantDto: {
657
+ hasOwnImages: boolean;
480
658
  id: string;
659
+ image: null | components["schemas"]["MediaDescriptorDto"];
660
+ images: components["schemas"]["MediaDescriptorDto"][];
481
661
  inStock: boolean;
482
662
  price: null | components["schemas"]["PriceDto"];
483
663
  selectedOptions: {
@@ -1559,6 +1739,96 @@ export interface operations {
1559
1739
  };
1560
1740
  };
1561
1741
  };
1742
+ SearchFacetValues: {
1743
+ parameters: {
1744
+ query?: {
1745
+ /** @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). */
1746
+ q?: string;
1747
+ /** @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. */
1748
+ filter?: {
1749
+ [key: string]: string;
1750
+ };
1751
+ };
1752
+ header: {
1753
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
1754
+ "X-Site-Key": string;
1755
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
1756
+ "X-Market"?: string;
1757
+ /** @description BCP-47 language tag. Omit to use the market's language. */
1758
+ "Accept-Language"?: string;
1759
+ };
1760
+ path: {
1761
+ key: string;
1762
+ };
1763
+ cookie?: never;
1764
+ };
1765
+ requestBody?: never;
1766
+ responses: {
1767
+ /** @description OK */
1768
+ 200: {
1769
+ headers: {
1770
+ [name: string]: unknown;
1771
+ };
1772
+ content: {
1773
+ "application/json": components["schemas"]["FacetValuesResponse"];
1774
+ };
1775
+ };
1776
+ /** @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). */
1777
+ 400: {
1778
+ headers: {
1779
+ [name: string]: unknown;
1780
+ };
1781
+ content: {
1782
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1783
+ };
1784
+ };
1785
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
1786
+ 401: {
1787
+ headers: {
1788
+ [name: string]: unknown;
1789
+ };
1790
+ content: {
1791
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1792
+ };
1793
+ };
1794
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
1795
+ 403: {
1796
+ headers: {
1797
+ [name: string]: unknown;
1798
+ };
1799
+ content: {
1800
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1801
+ };
1802
+ };
1803
+ /** @description problem+json. FACET_NOT_FOUND, or SITE_INACTIVE when the site itself is not live. */
1804
+ 404: {
1805
+ headers: {
1806
+ [name: string]: unknown;
1807
+ };
1808
+ content: {
1809
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1810
+ };
1811
+ };
1812
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
1813
+ 429: {
1814
+ headers: {
1815
+ [name: string]: unknown;
1816
+ };
1817
+ content: {
1818
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1819
+ };
1820
+ };
1821
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
1822
+ 503: {
1823
+ headers: {
1824
+ [name: string]: unknown;
1825
+ };
1826
+ content: {
1827
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
1828
+ };
1829
+ };
1830
+ };
1831
+ };
1562
1832
  GetProduct: {
1563
1833
  parameters: {
1564
1834
  query?: never;
@@ -1734,6 +2004,90 @@ export interface operations {
1734
2004
  };
1735
2005
  };
1736
2006
  };
2007
+ Suggest: {
2008
+ parameters: {
2009
+ query: {
2010
+ /** @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. */
2011
+ q: string;
2012
+ };
2013
+ header: {
2014
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
2015
+ "X-Site-Key": string;
2016
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
2017
+ "X-Market"?: string;
2018
+ /** @description BCP-47 language tag. Omit to use the market's language. */
2019
+ "Accept-Language"?: string;
2020
+ };
2021
+ path?: never;
2022
+ cookie?: never;
2023
+ };
2024
+ requestBody?: never;
2025
+ responses: {
2026
+ /** @description OK */
2027
+ 200: {
2028
+ headers: {
2029
+ [name: string]: unknown;
2030
+ };
2031
+ content: {
2032
+ "application/json": components["schemas"]["SuggestResponse"];
2033
+ };
2034
+ };
2035
+ /** @description problem+json. MARKET_UNKNOWN (no such market on this site), INVALID_PAGE, or INVALID_QUERY (q missing, blank or longer than 100 characters). */
2036
+ 400: {
2037
+ headers: {
2038
+ [name: string]: unknown;
2039
+ };
2040
+ content: {
2041
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2042
+ };
2043
+ };
2044
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
2045
+ 401: {
2046
+ headers: {
2047
+ [name: string]: unknown;
2048
+ };
2049
+ content: {
2050
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2051
+ };
2052
+ };
2053
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
2054
+ 403: {
2055
+ headers: {
2056
+ [name: string]: unknown;
2057
+ };
2058
+ content: {
2059
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2060
+ };
2061
+ };
2062
+ /** @description problem+json. SITE_INACTIVE, or NOT_FOUND for an unrouted path. */
2063
+ 404: {
2064
+ headers: {
2065
+ [name: string]: unknown;
2066
+ };
2067
+ content: {
2068
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2069
+ };
2070
+ };
2071
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
2072
+ 429: {
2073
+ headers: {
2074
+ [name: string]: unknown;
2075
+ };
2076
+ content: {
2077
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2078
+ };
2079
+ };
2080
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
2081
+ 503: {
2082
+ headers: {
2083
+ [name: string]: unknown;
2084
+ };
2085
+ content: {
2086
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2087
+ };
2088
+ };
2089
+ };
2090
+ };
1737
2091
  GetSite: {
1738
2092
  parameters: {
1739
2093
  query?: never;