@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 +79 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/media.d.ts +51 -0
- package/dist/media.js +114 -0
- package/dist/schema.d.ts +356 -2
- package/package.json +7 -4
- package/src/schema.d.ts +356 -2
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
package/dist/media.d.ts
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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;
|