@instruments/materialgraph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ /** Query options and wire types for the canonical brands collection. */
2
+ export interface BrandsOptions {
3
+ q?: string;
4
+ limit?: number;
5
+ offset?: number;
6
+ }
7
+ export interface BrandCatalogListItem {
8
+ id: string;
9
+ slug: string;
10
+ name: string;
11
+ primaryMarketSegment: string;
12
+ homepageUrl: string | null;
13
+ productCount: number;
14
+ variantCount: number;
15
+ }
16
+ export interface BrandCatalogListPage {
17
+ items: BrandCatalogListItem[];
18
+ meta: {
19
+ count: number;
20
+ total: number;
21
+ limit: number;
22
+ offset: number;
23
+ hasMore: boolean;
24
+ };
25
+ }
package/brand-types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ /** Catalog value-discovery contracts mirrored from the canonical HTTP routes. */
2
+ import type { OpenEnum } from "./types.js";
3
+ /** Options for resolving a free-text value against one attribute's dictionary. */
4
+ export interface ResolveAttributeValueOptions {
5
+ limit?: number;
6
+ }
7
+ /** One ranked canonical value candidate. */
8
+ export interface ResolveValueCandidate {
9
+ id: string;
10
+ label: string;
11
+ dictionaryId?: string;
12
+ score: number;
13
+ tier: OpenEnum<"exact_id" | "exact_alias" | "lexical" | "semantic">;
14
+ /** Present only for a colour_family candidate resolved via the colorscope fallback. */
15
+ via?: OpenEnum<"colorscope">;
16
+ }
17
+ /** Response from resolving a free-text value against one attribute's dictionary. */
18
+ export interface ResolveAttributeValueResponse {
19
+ attribute: {
20
+ id: string;
21
+ name: string;
22
+ dictionaryId: string;
23
+ };
24
+ resolution: {
25
+ status: OpenEnum<"resolved" | "ambiguous" | "unresolved">;
26
+ query: string;
27
+ best: ResolveValueCandidate | null;
28
+ candidates: ResolveValueCandidate[];
29
+ recommendedAction: OpenEnum<"use_best" | "choose_from_candidates" | "ask_user" | "none_found">;
30
+ };
31
+ }
32
+ /** Options for searching registry attributes and value-dictionary terms together. */
33
+ export interface VocabularySearchOptions {
34
+ limit?: number;
35
+ kind?: "attribute" | "term";
36
+ }
37
+ /** One ranked cross-vocabulary candidate. */
38
+ export interface VocabularySearchCandidate extends ResolveValueCandidate {
39
+ kind: OpenEnum<"attribute" | "term">;
40
+ /** Present for terms: attributes whose value dictionary owns this term. */
41
+ attributeIds?: string[];
42
+ }
43
+ /** Response from the cross-vocabulary catalog search. */
44
+ export interface VocabularySearchResponse {
45
+ query: string;
46
+ results: VocabularySearchCandidate[];
47
+ }
@@ -0,0 +1 @@
1
+ export {};
package/client.d.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { type RateLimit } from "./errors.js";
2
+ import type { BrandCatalogListPage, BrandsOptions } from "./brand-types.js";
3
+ import type { MaterialAssetProjection } from "./material-asset-types.js";
4
+ import type { MaterialBlockProjection } from "./material-block-types.js";
5
+ import type { SchemaResource, SchemaResourceResponse } from "./schema-resource-types.js";
6
+ import type { ApplicableSpecsResponse, BrandOptions, BrandProfileResponse, ByColourParams, ByColourResponse, BrowseParams, BrowseResponse, BySpecOptions, BySpecResponse, ColourFacetsResponse, CoordinateInput, CoordinateResponse, DemandParams, DemandResponse, ExtractPaletteInput, ExtractPaletteResponse, GlossaryTermResponse, MaterialResponse, MaterialsBatchDetailResponse, MaterialsBatchGetOptions, MaterialsBatchOptions, MaterialsBatchTileResponse, MatchInput, MatchResponse, PairingsOptions, PairingsResponse, ResolveAttributeOptions, ResolveAttributeResponse, ResolveAttributeValueOptions, ResolveAttributeValueResponse, ResolveColourResponse, SearchGetParams, SearchInput, SearchResponse, SiblingsResponse, SpecFacetsDistributionResponse, SpecFacetsListResponse, SuggestColourResponse, TaxonomyResponse, VocabularySearchOptions, VocabularySearchResponse, WhoamiResponse } from "./types.js";
7
+ export declare const DEFAULT_BASE_URL = "https://beta.materialgraph.com";
8
+ export declare const MAX_BATCH_IDS = 100;
9
+ export interface MaterialGraphClientOptions {
10
+ /** A `mg_read_*` API key, presented as `Authorization: Bearer <key>`. */
11
+ apiKey: string;
12
+ /** Override the API origin. Defaults to {@link DEFAULT_BASE_URL}. */
13
+ baseUrl?: string;
14
+ /** Inject a custom `fetch` (tests, proxies). Defaults to the global `fetch`. */
15
+ fetch?: typeof fetch;
16
+ }
17
+ export interface BrowseNamespace {
18
+ (params: BrowseParams, signal?: AbortSignal): Promise<BrowseResponse>;
19
+ pages(params: BrowseParams, signal?: AbortSignal): AsyncGenerator<BrowseResponse, void, unknown>;
20
+ }
21
+ export declare class MaterialGraphClient {
22
+ #private;
23
+ readonly baseUrl: string;
24
+ readonly browse: BrowseNamespace;
25
+ constructor(options: MaterialGraphClientOptions);
26
+ get lastRateLimit(): RateLimit | null;
27
+ search(input: SearchInput, signal?: AbortSignal): Promise<SearchResponse>;
28
+ searchGet(params: SearchGetParams, signal?: AbortSignal): Promise<SearchResponse>;
29
+ match(input: MatchInput, signal?: AbortSignal): Promise<MatchResponse>;
30
+ byColour(params: ByColourParams, signal?: AbortSignal): Promise<ByColourResponse>;
31
+ /** Get a single material's detail record (GET `/api/v1/materials/{variantId}`). */
32
+ material(variantId: string, options?: {
33
+ includeSiblings?: boolean;
34
+ }, signal?: AbortSignal): Promise<MaterialResponse>;
35
+ /** The other variants of the same product (GET `/api/v1/materials/{variantId}/siblings`). */
36
+ siblings(variantId: string, signal?: AbortSignal): Promise<SiblingsResponse>;
37
+ /** Canonical block completeness for a material (GET `/api/v1/materials/{variantId}/blocks`). */
38
+ materialBlocks(variantId: string, signal?: AbortSignal): Promise<MaterialBlockProjection>;
39
+ /** Safe images and downloads for a material (GET `/api/v1/materials/{variantId}/assets`). */
40
+ materialAssets(variantId: string, signal?: AbortSignal): Promise<MaterialAssetProjection>;
41
+ /** Read one static governed schema resource. */
42
+ schema<K extends SchemaResource>(resource: K, signal?: AbortSignal): Promise<SchemaResourceResponse<K>>;
43
+ /** The "what pairs with this material" rail (GET `/api/v1/materials/{variantId}/pairings`). */
44
+ pairings(variantId: string, options?: PairingsOptions, signal?: AbortSignal): Promise<PairingsResponse>;
45
+ /**
46
+ * Batch-read materials by id (POST `/api/v1/materials/batch`, ≤ 100 ids).
47
+ * Throws client-side before any request when more than {@link MAX_BATCH_IDS}
48
+ * ids are supplied.
49
+ */
50
+ materials(ids: string[], options: {
51
+ view: "detail";
52
+ idKind?: MaterialsBatchOptions["idKind"];
53
+ }, signal?: AbortSignal): Promise<MaterialsBatchDetailResponse>;
54
+ materials(ids: string[], options?: {
55
+ view?: "tile";
56
+ idKind?: MaterialsBatchOptions["idKind"];
57
+ }, signal?: AbortSignal): Promise<MaterialsBatchTileResponse>;
58
+ /**
59
+ * Cacheable GET form of the batch read (GET `/api/v1/materials/batch`,
60
+ * `Cache-Control: public, max-age=300`). Same response as {@link materials};
61
+ * prefer this for repeatable, cache-friendly id sets. Also ≤ 100 ids.
62
+ */
63
+ materialsCached(ids: string[], options: {
64
+ view: "detail";
65
+ idKind?: MaterialsBatchGetOptions["idKind"];
66
+ }, signal?: AbortSignal): Promise<MaterialsBatchDetailResponse>;
67
+ materialsCached(ids: string[], options?: {
68
+ view?: "tile";
69
+ idKind?: MaterialsBatchGetOptions["idKind"];
70
+ }, signal?: AbortSignal): Promise<MaterialsBatchTileResponse>;
71
+ /** The three canonical colour vocabularies with live counts (GET `/api/v1/colours/facets`). */
72
+ colourFacets(signal?: AbortSignal): Promise<ColourFacetsResponse>;
73
+ /** Resolve a colour name/phrase to a hex (GET `/api/v1/search/colours/resolve`). */
74
+ resolveColour(name: string, signal?: AbortSignal): Promise<ResolveColourResponse>;
75
+ /** Ranked colour-name typeahead (GET `/api/v1/search/colours/suggest`). */
76
+ suggestColour(q: string, options?: {
77
+ limit?: number;
78
+ }, signal?: AbortSignal): Promise<SuggestColourResponse>;
79
+ /** Resolve a free-text spec phrase to canonical attribute ids (GET `/api/v1/catalog/attributes/resolve`). */
80
+ resolveAttribute(q: string, options?: ResolveAttributeOptions, signal?: AbortSignal): Promise<ResolveAttributeResponse>;
81
+ /** Resolve a value phrase against one canonical attribute's dictionary. */
82
+ resolveAttributeValue(attributeId: string, query: string, options?: ResolveAttributeValueOptions, signal?: AbortSignal): Promise<ResolveAttributeValueResponse>;
83
+ /** Search canonical attributes and value-dictionary terms together. */
84
+ searchVocabulary(query: string, options?: VocabularySearchOptions, signal?: AbortSignal): Promise<VocabularySearchResponse>;
85
+ /**
86
+ * Find materials by canonical specification (GET `/api/v1/search/specifications`).
87
+ * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL.
88
+ */
89
+ bySpec(spec: string | string[], options?: BySpecOptions, signal?: AbortSignal): Promise<BySpecResponse>;
90
+ /** The product-category tree, flattened (GET `/api/v1/taxonomy/categories`). */
91
+ taxonomy(signal?: AbortSignal): Promise<TaxonomyResponse>;
92
+ /** Echo the authenticated principal and scopes (GET `/api/v1/whoami`). */
93
+ whoami(signal?: AbortSignal): Promise<WhoamiResponse>;
94
+ /**
95
+ * Coordinate a palette of already-chosen materials (POST
96
+ * `/api/v1/pairings/coordinate`). Returns demand-lift-ranked companions with
97
+ * colour swatches and an annotate-only `colourFit` (never reorders in v1).
98
+ */
99
+ coordinate(input: CoordinateInput, signal?: AbortSignal): Promise<CoordinateResponse>;
100
+ /**
101
+ * How much a material is specified, plus what's specified alongside it (GET
102
+ * `/api/v1/demand`). Provide EXACTLY ONE of `variantId` or `material`.
103
+ */
104
+ demand(params: DemandParams, signal?: AbortSignal): Promise<DemandResponse>;
105
+ /** Extract a named colour palette from an uploaded image (POST `/api/v1/search/palettes/extract`). */
106
+ extractPalette(input: ExtractPaletteInput, signal?: AbortSignal): Promise<ExtractPaletteResponse>;
107
+ /** Discover the live specification-facet collection. */
108
+ specificationFacets(signal?: AbortSignal): Promise<SpecFacetsListResponse>;
109
+ /** List the live values for one canonical attribute. */
110
+ attributeValues(attributeId: string, signal?: AbortSignal): Promise<SpecFacetsDistributionResponse>;
111
+ /** The specs a product type COULD carry (GET `/api/v1/taxonomy/categories/{key}/specs`). */
112
+ applicableSpecs(key: string, signal?: AbortSignal): Promise<ApplicableSpecsResponse>;
113
+ /** Resolve a term to its meaning in the schema (GET `/api/v1/glossary/{term}`). */
114
+ glossary(term: string, signal?: AbortSignal): Promise<GlossaryTermResponse>;
115
+ /** List registered brands (GET `/api/v1/brands`). */
116
+ brands(options?: BrandsOptions, signal?: AbortSignal): Promise<BrandCatalogListPage>;
117
+ /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{slug}`). */
118
+ brand(slug: string, options?: BrandOptions, signal?: AbortSignal): Promise<BrandProfileResponse>;
119
+ }
package/client.js ADDED
@@ -0,0 +1,333 @@
1
+ import { MaterialGraphError } from "./errors.js";
2
+ import { SDK_OPERATIONS } from "./operations.js";
3
+ export const DEFAULT_BASE_URL = "https://beta.materialgraph.com";
4
+ export const MAX_BATCH_IDS = 100;
5
+ const SCHEMA_RESOURCE_OPERATION_IDS = {
6
+ blocks: "list_schema_blocks",
7
+ attributes: "list_schema_attributes",
8
+ verticals: "list_schema_verticals",
9
+ productTypes: "list_schema_product_types",
10
+ units: "list_schema_units",
11
+ valueDictionaries: "list_schema_value_dictionaries",
12
+ sourceRequirements: "list_schema_source_requirements",
13
+ productTypeTemplates: "list_schema_product_type_templates",
14
+ mappings: "list_schema_mappings",
15
+ };
16
+ function operation(operationId) {
17
+ const match = SDK_OPERATIONS.find((candidate) => candidate.operationId === operationId);
18
+ if (!match) {
19
+ throw MaterialGraphError.unexpected(`Unknown SDK operation: ${operationId}`);
20
+ }
21
+ return match;
22
+ }
23
+ function operationPath(sdkOperation, pathParams = {}) {
24
+ return Object.entries(pathParams).reduce((path, [name, value]) => path.replace(`{${name}}`, encodeURIComponent(value)), sdkOperation.path);
25
+ }
26
+ function parseIntOrNull(value) {
27
+ if (value === null)
28
+ return null;
29
+ const n = Number.parseInt(value, 10);
30
+ return Number.isNaN(n) ? null : n;
31
+ }
32
+ function parseRateLimit(headers) {
33
+ const limit = parseIntOrNull(headers.get("x-ratelimit-limit"));
34
+ const remaining = parseIntOrNull(headers.get("x-ratelimit-remaining"));
35
+ const reset = parseIntOrNull(headers.get("x-ratelimit-reset"));
36
+ const retryAfter = parseIntOrNull(headers.get("retry-after"));
37
+ if (limit === null && remaining === null && reset === null && retryAfter === null) {
38
+ return null;
39
+ }
40
+ return { limit, remaining, reset, retryAfter };
41
+ }
42
+ export class MaterialGraphClient {
43
+ baseUrl;
44
+ browse;
45
+ #apiKey;
46
+ #fetch;
47
+ #lastRateLimit = null;
48
+ constructor(options) {
49
+ if (!options.apiKey) {
50
+ throw MaterialGraphError.unexpected("MaterialGraphClient: `apiKey` is required.");
51
+ }
52
+ this.#apiKey = options.apiKey;
53
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
54
+ this.#fetch = options.fetch ?? globalThis.fetch;
55
+ const browse = (params, signal) => this.#browsePage(params, signal);
56
+ browse.pages = (params, signal) => this.#browsePages(params, signal);
57
+ this.browse = browse;
58
+ }
59
+ get lastRateLimit() {
60
+ return this.#lastRateLimit;
61
+ }
62
+ #buildUrl(path, query) {
63
+ const url = new URL(`${this.baseUrl}${path}`);
64
+ if (query) {
65
+ for (const [key, value] of Object.entries(query)) {
66
+ if (value === undefined || value === null)
67
+ continue;
68
+ if (Array.isArray(value)) {
69
+ for (const item of value) {
70
+ if (item !== undefined && item !== null) {
71
+ url.searchParams.append(key, String(item));
72
+ }
73
+ }
74
+ }
75
+ else {
76
+ url.searchParams.append(key, String(value));
77
+ }
78
+ }
79
+ }
80
+ return url.toString();
81
+ }
82
+ async #request(sdkOperation, options = {}) {
83
+ const path = operationPath(sdkOperation, options.pathParams);
84
+ const url = this.#buildUrl(path, options.query);
85
+ const requestUrl = new URL(url);
86
+ const requestMetadata = {
87
+ method: sdkOperation.method,
88
+ origin: requestUrl.origin,
89
+ path: requestUrl.pathname,
90
+ };
91
+ const headers = {
92
+ Authorization: `Bearer ${this.#apiKey}`,
93
+ Accept: "application/json",
94
+ };
95
+ const hasBody = options.body !== undefined;
96
+ if (hasBody)
97
+ headers["Content-Type"] = "application/json";
98
+ let response;
99
+ try {
100
+ response = await this.#fetch(url, {
101
+ method: sdkOperation.method,
102
+ headers,
103
+ body: hasBody ? JSON.stringify(options.body) : undefined,
104
+ signal: options.signal,
105
+ });
106
+ }
107
+ catch (cause) {
108
+ throw MaterialGraphError.fromTransport({
109
+ ...requestMetadata,
110
+ cause,
111
+ callerSignal: options.signal,
112
+ });
113
+ }
114
+ const rateLimit = parseRateLimit(response.headers);
115
+ this.#lastRateLimit = rateLimit;
116
+ let text;
117
+ try {
118
+ text = await response.text();
119
+ }
120
+ catch (cause) {
121
+ throw MaterialGraphError.fromTransport({
122
+ ...requestMetadata,
123
+ cause,
124
+ callerSignal: options.signal,
125
+ });
126
+ }
127
+ let parsed;
128
+ if (text.length > 0) {
129
+ try {
130
+ parsed = JSON.parse(text);
131
+ }
132
+ catch {
133
+ parsed = text;
134
+ }
135
+ }
136
+ if (!response.ok) {
137
+ const querySecrets = [...requestUrl.searchParams.values()];
138
+ throw MaterialGraphError.fromResponse({
139
+ ...requestMetadata,
140
+ status: response.status,
141
+ body: parsed,
142
+ responseText: text,
143
+ rateLimit,
144
+ apiKey: this.#apiKey,
145
+ queryValues: querySecrets,
146
+ });
147
+ }
148
+ return parsed;
149
+ }
150
+ #get(operationId, query, signal, pathParams) {
151
+ return this.#request(operation(operationId), { query, signal, pathParams });
152
+ }
153
+ #post(operationId, body, signal) {
154
+ return this.#request(operation(operationId), { body, signal });
155
+ }
156
+ search(input, signal) {
157
+ return this.#post("search_materials_post", input, signal);
158
+ }
159
+ searchGet(params, signal) {
160
+ return this.#get("search_materials_get", { ...params }, signal);
161
+ }
162
+ match(input, signal) {
163
+ return this.#post("match_material", input, signal);
164
+ }
165
+ byColour(params, signal) {
166
+ return this.#get("search_by_colour", {
167
+ hex: params.hex,
168
+ colour: params.colour,
169
+ l: params.l,
170
+ a: params.a,
171
+ b: params.b,
172
+ category: params.category,
173
+ limit: params.limit,
174
+ }, signal);
175
+ }
176
+ // ── Materials ──────────────────────────────────────────────────────────────
177
+ /** Get a single material's detail record (GET `/api/v1/materials/{variantId}`). */
178
+ material(variantId, options, signal) {
179
+ return this.#get("get_material", options?.includeSiblings ? { include: "siblings" } : undefined, signal, { variantId });
180
+ }
181
+ /** The other variants of the same product (GET `/api/v1/materials/{variantId}/siblings`). */
182
+ siblings(variantId, signal) {
183
+ return this.#get("get_material_siblings", undefined, signal, { variantId });
184
+ }
185
+ /** Canonical block completeness for a material (GET `/api/v1/materials/{variantId}/blocks`). */
186
+ materialBlocks(variantId, signal) {
187
+ return this.#get("list_material_blocks", undefined, signal, {
188
+ variantId,
189
+ });
190
+ }
191
+ /** Safe images and downloads for a material (GET `/api/v1/materials/{variantId}/assets`). */
192
+ materialAssets(variantId, signal) {
193
+ return this.#get("list_material_assets", undefined, signal, {
194
+ variantId,
195
+ });
196
+ }
197
+ /** Read one static governed schema resource. */
198
+ schema(resource, signal) {
199
+ return this.#get(SCHEMA_RESOURCE_OPERATION_IDS[resource], undefined, signal);
200
+ }
201
+ /** The "what pairs with this material" rail (GET `/api/v1/materials/{variantId}/pairings`). */
202
+ pairings(variantId, options, signal) {
203
+ return this.#get("get_material_pairings", { categories: options?.categories, limit: options?.limit }, signal, { variantId });
204
+ }
205
+ materials(ids, options, signal) {
206
+ if (ids.length > MAX_BATCH_IDS) {
207
+ throw MaterialGraphError.unexpected(`MaterialGraphClient.materials(): at most ${MAX_BATCH_IDS} ids per call, received ${ids.length}. Split into multiple batches.`);
208
+ }
209
+ return this.#post("get_materials_batch_post", { ids, view: options?.view ?? "tile", idKind: options?.idKind ?? "variantId" }, signal);
210
+ }
211
+ materialsCached(ids, options, signal) {
212
+ if (ids.length > MAX_BATCH_IDS) {
213
+ throw MaterialGraphError.unexpected(`MaterialGraphClient.materialsCached(): at most ${MAX_BATCH_IDS} ids per call, received ${ids.length}. Split into multiple batches.`);
214
+ }
215
+ return this.#get("get_materials_batch_get", {
216
+ ids: ids.join(","),
217
+ view: options?.view ?? "tile",
218
+ idKind: options?.idKind ?? "variantId",
219
+ }, signal);
220
+ }
221
+ // ── Browse (private impls behind the `browse` namespace) ─────────────────────
222
+ #browsePage(params, signal) {
223
+ return this.#get("search_browse_by_colour", {
224
+ sort: params.sort,
225
+ cursor: params.cursor,
226
+ limit: params.limit,
227
+ types: params.types,
228
+ }, signal);
229
+ }
230
+ async *#browsePages(params, signal) {
231
+ let cursor = params.cursor;
232
+ for (;;) {
233
+ const page = await this.#browsePage({ ...params, cursor }, signal);
234
+ yield page;
235
+ if (page.cursor === undefined || page.results.length === 0)
236
+ return;
237
+ cursor = page.cursor;
238
+ }
239
+ }
240
+ // ── Colour helpers ───────────────────────────────────────────────────────────
241
+ /** The three canonical colour vocabularies with live counts (GET `/api/v1/colours/facets`). */
242
+ colourFacets(signal) {
243
+ return this.#get("get_colour_facets", undefined, signal);
244
+ }
245
+ /** Resolve a colour name/phrase to a hex (GET `/api/v1/search/colours/resolve`). */
246
+ resolveColour(name, signal) {
247
+ return this.#get("resolve_colour_name", { name }, signal);
248
+ }
249
+ /** Ranked colour-name typeahead (GET `/api/v1/search/colours/suggest`). */
250
+ suggestColour(q, options, signal) {
251
+ return this.#get("suggest_colour_names", { q, limit: options?.limit }, signal);
252
+ }
253
+ // ── Catalog ──────────────────────────────────────────────────────────────────
254
+ /** Resolve a free-text spec phrase to canonical attribute ids (GET `/api/v1/catalog/attributes/resolve`). */
255
+ resolveAttribute(q, options, signal) {
256
+ return this.#get("catalog_resolve_attribute", { q, limit: options?.limit, facetableOnly: options?.facetableOnly }, signal);
257
+ }
258
+ /** Resolve a value phrase against one canonical attribute's dictionary. */
259
+ resolveAttributeValue(attributeId, query, options, signal) {
260
+ return this.#get("catalog_resolve_value", { q: query, limit: options?.limit }, signal, { attributeId });
261
+ }
262
+ /** Search canonical attributes and value-dictionary terms together. */
263
+ searchVocabulary(query, options, signal) {
264
+ return this.#get("catalog_vocabulary_search", { q: query, limit: options?.limit, kind: options?.kind }, signal);
265
+ }
266
+ /**
267
+ * Find materials by canonical specification (GET `/api/v1/search/specifications`).
268
+ * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL.
269
+ */
270
+ bySpec(spec, options, signal) {
271
+ return this.#get("catalog_by_spec", { spec: Array.isArray(spec) ? spec : [spec], limit: options?.limit }, signal);
272
+ }
273
+ // ── Taxonomy + meta ──────────────────────────────────────────────────────────
274
+ /** The product-category tree, flattened (GET `/api/v1/taxonomy/categories`). */
275
+ taxonomy(signal) {
276
+ return this.#get("get_category_taxonomy", undefined, signal);
277
+ }
278
+ /** Echo the authenticated principal and scopes (GET `/api/v1/whoami`). */
279
+ whoami(signal) {
280
+ return this.#get("whoami", undefined, signal);
281
+ }
282
+ // ── Pairings / demand ────────────────────────────────────────────────────────
283
+ /**
284
+ * Coordinate a palette of already-chosen materials (POST
285
+ * `/api/v1/pairings/coordinate`). Returns demand-lift-ranked companions with
286
+ * colour swatches and an annotate-only `colourFit` (never reorders in v1).
287
+ */
288
+ coordinate(input, signal) {
289
+ return this.#post("coordinate_materials", input, signal);
290
+ }
291
+ /**
292
+ * How much a material is specified, plus what's specified alongside it (GET
293
+ * `/api/v1/demand`). Provide EXACTLY ONE of `variantId` or `material`.
294
+ */
295
+ demand(params, signal) {
296
+ return this.#get("get_material_demand", { variantId: params.variantId, material: params.material }, signal);
297
+ }
298
+ // ── Colour helpers (extract) ─────────────────────────────────────────────────
299
+ /** Extract a named colour palette from an uploaded image (POST `/api/v1/search/palettes/extract`). */
300
+ extractPalette(input, signal) {
301
+ return this.#post("extract_image_palette", input, signal);
302
+ }
303
+ // ── Catalog (facets / applicable specs / glossary) ───────────────────────────
304
+ /** Discover the live specification-facet collection. */
305
+ specificationFacets(signal) {
306
+ return this.#get("list_spec_facets", undefined, signal);
307
+ }
308
+ /** List the live values for one canonical attribute. */
309
+ attributeValues(attributeId, signal) {
310
+ return this.#get("list_attribute_values", undefined, signal, {
311
+ attributeId,
312
+ });
313
+ }
314
+ /** The specs a product type COULD carry (GET `/api/v1/taxonomy/categories/{key}/specs`). */
315
+ applicableSpecs(key, signal) {
316
+ return this.#get("get_applicable_specs", undefined, signal, { key });
317
+ }
318
+ /** Resolve a term to its meaning in the schema (GET `/api/v1/glossary/{term}`). */
319
+ glossary(term, signal) {
320
+ return this.#get("explain_material_term", undefined, signal, { term });
321
+ }
322
+ // ── Brands ───────────────────────────────────────────────────────────────────
323
+ /** List registered brands (GET `/api/v1/brands`). */
324
+ brands(options = {}, signal) {
325
+ return this.#get("list_brands", { q: options.q, limit: options.limit, offset: options.offset }, signal);
326
+ }
327
+ /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{slug}`). */
328
+ brand(slug, options, signal) {
329
+ return this.#get("get_brand_profile", { q: options?.q }, signal, {
330
+ slug,
331
+ });
332
+ }
333
+ }
package/errors.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ /** Error and rate-limit contracts for the retry-free MaterialGraph SDK. */
2
+ export type MaterialGraphErrorKind = "problem" | "simple" | "network" | "timeout" | "aborted" | "rate_limited" | "unexpected";
3
+ export type SanitizedErrorValue = null | boolean | number | string | SanitizedErrorValue[] | SanitizedIssue;
4
+ /** A bounded, recursively sanitized validation-issue record. */
5
+ export interface SanitizedIssue {
6
+ [key: string]: SanitizedErrorValue;
7
+ }
8
+ /** Normalized RFC 7807 body. Unknown response fields are intentionally discarded. */
9
+ export interface ProblemDetail {
10
+ type: string;
11
+ title: string;
12
+ status: number;
13
+ detail: string;
14
+ instance?: string;
15
+ is_retriable: boolean;
16
+ trace_id: string;
17
+ suggestions?: string[];
18
+ doc_uri?: string;
19
+ issues?: SanitizedIssue[];
20
+ }
21
+ /** Normalized residual `{ error }` body. Unknown response fields are discarded. */
22
+ export interface SimpleErrorBody {
23
+ error: string;
24
+ issues?: SanitizedIssue[];
25
+ }
26
+ /** Parsed `X-RateLimit-*` / `Retry-After` headers. `null` means absent. */
27
+ export interface RateLimit {
28
+ limit: number | null;
29
+ remaining: number | null;
30
+ reset: number | null;
31
+ retryAfter: number | null;
32
+ }
33
+ export interface MaterialGraphRequestMetadata {
34
+ method: string;
35
+ origin: string;
36
+ path: string;
37
+ }
38
+ export interface MaterialGraphErrorInit {
39
+ kind: MaterialGraphErrorKind;
40
+ status?: number;
41
+ retriable: boolean;
42
+ traceId?: string;
43
+ problem?: ProblemDetail;
44
+ simple?: SimpleErrorBody;
45
+ rateLimit?: RateLimit | null;
46
+ method?: string;
47
+ origin?: string;
48
+ path?: string;
49
+ /** Accepted for compatibility but deliberately never retained on the public error. */
50
+ cause?: unknown;
51
+ }
52
+ interface ResponseErrorInput extends MaterialGraphRequestMetadata {
53
+ status: number;
54
+ body: unknown;
55
+ responseText: string;
56
+ rateLimit: RateLimit | null;
57
+ apiKey: string;
58
+ queryValues: readonly string[];
59
+ }
60
+ interface TransportErrorInput extends MaterialGraphRequestMetadata {
61
+ cause: unknown;
62
+ callerSignal?: AbortSignal;
63
+ }
64
+ /** The one error class thrown by all SDK transport and HTTP failures. */
65
+ export declare class MaterialGraphError extends Error {
66
+ readonly kind: MaterialGraphErrorKind;
67
+ readonly status: number | undefined;
68
+ readonly retriable: boolean;
69
+ readonly traceId: string | undefined;
70
+ readonly problem: ProblemDetail | undefined;
71
+ readonly simple: SimpleErrorBody | undefined;
72
+ readonly rateLimit: RateLimit | null;
73
+ readonly method: string | undefined;
74
+ readonly origin: string | undefined;
75
+ readonly path: string | undefined;
76
+ constructor(message: string, init: MaterialGraphErrorInit);
77
+ /** Compatibility alias for the former SDK property. */
78
+ get isRetriable(): boolean;
79
+ static unexpected(message: string): MaterialGraphError;
80
+ static fromTransport(input: TransportErrorInput): MaterialGraphError;
81
+ static fromResponse(input: ResponseErrorInput): MaterialGraphError;
82
+ }
83
+ export {};