@instruments/materialgraph 3.1.0 → 3.2.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/association-types.d.ts +20 -20
- package/brand-types.d.ts +6 -6
- package/by-spec-types.d.ts +6 -6
- package/catalog-discovery-types.d.ts +4 -4
- package/categories-client.d.ts +3 -3
- package/category-types.d.ts +18 -18
- package/client-config.d.ts +2 -2
- package/client-response-schema.d.ts +2 -2
- package/client-response-schema.js +4 -4
- package/client-vocabulary.js +1 -1
- package/client.js +8 -10
- package/color-profiles-types.d.ts +8 -8
- package/color-wire-types.d.ts +6 -6
- package/colors-client.d.ts +3 -3
- package/discovery-context-types.d.ts +8 -8
- package/errors.d.ts +12 -13
- package/errors.js +84 -74
- package/find-sampled-together-types.d.ts +6 -6
- package/find-variants-by-color-types.d.ts +30 -30
- package/find-variants-by-vector-types.d.ts +13 -13
- package/get-variant-types.d.ts +6 -6
- package/http-transport.d.ts +14 -10
- package/http-transport.js +14 -12
- package/image-reference-types.d.ts +2 -2
- package/json.d.ts +18 -0
- package/json.js +36 -0
- package/list-enum-values-types.d.ts +8 -8
- package/list-sampled-together-pairs-types.d.ts +6 -6
- package/list-variant-facets-types.d.ts +8 -8
- package/list-variants-types.d.ts +40 -31
- package/material-asset-types.d.ts +26 -26
- package/material-block-types.d.ts +6 -6
- package/material-types.d.ts +6 -6
- package/nearest-paints-types.d.ts +8 -8
- package/operation-responses.d.ts +2 -2
- package/operations.d.ts +1 -1
- package/operations.js +1 -1
- package/package.json +1 -1
- package/parse-operation-response.d.ts +4 -3
- package/parse-operation-response.js +112 -135
- package/resolve-color-types.d.ts +6 -6
- package/resolve-variant-types.d.ts +10 -10
- package/response-schema-catalog.d.ts +8 -0
- package/response-schema-catalog.js +82 -0
- package/response-schemas.d.ts +32 -3
- package/response-schemas.generated.js +26 -26
- package/response-schemas.js +5 -77
- package/schema-resource-types.d.ts +12 -12
- package/search-catalog-subset-types.d.ts +2 -2
- package/search-variants-types.d.ts +12 -12
- package/source-material-types.d.ts +6 -6
- package/source-material.js +2 -32
- package/types.d.ts +38 -38
- package/webmcp.d.ts +10 -4
- package/webmcp.generated.js +5 -4
package/operations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MaterialGraphError } from "./errors.js";
|
|
2
|
-
import { SDK_OPERATIONS
|
|
2
|
+
import { SDK_OPERATIONS } from "./operations.generated.js";
|
|
3
3
|
export { MATERIALGRAPH_API_VERSION, SDK_OPERATIONS, } from "./operations.generated.js";
|
|
4
4
|
/** Look up a registered operation by its OpenAPI `operationId`. */
|
|
5
5
|
export const findSdkOperation = (operationId) => SDK_OPERATIONS.find((candidate) => candidate.operationId === operationId);
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { JsonValue } from "./json-value-types.js";
|
|
2
|
+
import type { JsonObject } from "./json.js";
|
|
1
3
|
import type { JsonOperationId, OperationResponses } from "./operation-responses.js";
|
|
2
|
-
import type { JsonSchema } from "./response-schemas.js";
|
|
3
4
|
/** Evaluate the JSON Schema keywords used by the published response contracts. */
|
|
4
|
-
export declare const matchesResponseSchema: (value:
|
|
5
|
-
export declare const parseOperationResponse: <K extends JsonOperationId>(operationId: K, value:
|
|
5
|
+
export declare const matchesResponseSchema: (value: JsonValue | undefined, schema: JsonValue | undefined, root: JsonObject) => boolean;
|
|
6
|
+
export declare const parseOperationResponse: <K extends JsonOperationId>(operationId: K, value: JsonValue | undefined) => Promise<OperationResponses[K]>;
|
|
@@ -1,185 +1,162 @@
|
|
|
1
1
|
import { openClientResponseEnums } from "./client-response-schema.js";
|
|
2
2
|
import { MaterialGraphError } from "./errors.js";
|
|
3
|
-
|
|
3
|
+
import { isJsonNumber, isJsonObject, isJsonString, jsonKind } from "./json.js";
|
|
4
4
|
const sameJson = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
5
|
+
const SIMPLE_TYPES = new Set([
|
|
6
|
+
"array",
|
|
7
|
+
"boolean",
|
|
8
|
+
"null",
|
|
9
|
+
"object",
|
|
10
|
+
"string",
|
|
11
|
+
]);
|
|
5
12
|
const matchesType = (value, type) => {
|
|
6
13
|
if (Array.isArray(type)) {
|
|
7
14
|
return type.some((candidate) => matchesType(value, candidate));
|
|
8
15
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return Array.isArray(value);
|
|
18
|
-
}
|
|
19
|
-
case "object": {
|
|
20
|
-
return isRecord(value);
|
|
21
|
-
}
|
|
22
|
-
case "integer": {
|
|
23
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
24
|
-
}
|
|
25
|
-
case "number": {
|
|
26
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
27
|
-
}
|
|
28
|
-
case "string": {
|
|
29
|
-
return typeof value === "string";
|
|
30
|
-
}
|
|
31
|
-
case "boolean": {
|
|
32
|
-
return typeof value === "boolean";
|
|
33
|
-
}
|
|
34
|
-
default: {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
16
|
+
if (type === undefined) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (type === "integer") {
|
|
20
|
+
return isJsonNumber(value) && Number.isInteger(value);
|
|
21
|
+
}
|
|
22
|
+
if (type === "number") {
|
|
23
|
+
return isJsonNumber(value) && Number.isFinite(value);
|
|
37
24
|
}
|
|
25
|
+
return (isJsonString(type) && SIMPLE_TYPES.has(type) && jsonKind(value) === type);
|
|
38
26
|
};
|
|
39
27
|
const matchesNumberKeywords = (value, schema) => {
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
if (!isJsonNumber(value)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = schema;
|
|
32
|
+
if (isJsonNumber(minimum) && value < minimum) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (isJsonNumber(maximum) && value > maximum) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (isJsonNumber(exclusiveMinimum) && value <= exclusiveMinimum) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return !(isJsonNumber(exclusiveMaximum) && value >= exclusiveMaximum);
|
|
42
|
+
};
|
|
43
|
+
const matchesStringKeywords = (value, schema) => {
|
|
44
|
+
if (!isJsonString(value)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const { maxLength, minLength, pattern } = schema;
|
|
48
|
+
let length = 0;
|
|
49
|
+
for (const _codePoint of value) {
|
|
50
|
+
length += 1;
|
|
51
|
+
}
|
|
52
|
+
if (isJsonNumber(minLength) && length < minLength) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (isJsonNumber(maxLength) && length > maxLength) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return !(isJsonString(pattern) && !new RegExp(pattern, "u").test(value));
|
|
59
|
+
};
|
|
60
|
+
const matchesValueKeywords = (value, schema) => {
|
|
61
|
+
if ("const" in schema && !sameJson(value, schema.const)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const { enum: allowed } = schema;
|
|
65
|
+
return !(Array.isArray(allowed) &&
|
|
66
|
+
!allowed.some((candidate) => sameJson(candidate, value)));
|
|
67
|
+
};
|
|
68
|
+
/** Evaluates the JSON Schema keywords the published response contracts use. */
|
|
69
|
+
class ResponseSchemaEvaluator {
|
|
70
|
+
#root;
|
|
71
|
+
constructor(root) {
|
|
72
|
+
this.#root = root;
|
|
73
|
+
}
|
|
74
|
+
matches(value, schema) {
|
|
75
|
+
if (schema === true || schema === false) {
|
|
76
|
+
return schema;
|
|
43
77
|
}
|
|
44
|
-
if (
|
|
78
|
+
if (!isJsonObject(schema)) {
|
|
45
79
|
return false;
|
|
46
80
|
}
|
|
47
|
-
if (
|
|
48
|
-
value <= schema.exclusiveMinimum) {
|
|
81
|
+
if (!this.#matchesReference(value, schema)) {
|
|
49
82
|
return false;
|
|
50
83
|
}
|
|
51
|
-
if (
|
|
52
|
-
value >= schema.exclusiveMaximum) {
|
|
84
|
+
if (!matchesType(value, schema.type)) {
|
|
53
85
|
return false;
|
|
54
86
|
}
|
|
55
|
-
|
|
56
|
-
return true;
|
|
57
|
-
};
|
|
58
|
-
const matchesStringKeywords = (value, schema) => {
|
|
59
|
-
if (typeof value === "string") {
|
|
60
|
-
let length = 0;
|
|
61
|
-
for (const _codePoint of value) {
|
|
62
|
-
length += 1;
|
|
63
|
-
}
|
|
64
|
-
if (typeof schema.minLength === "number" && length < schema.minLength) {
|
|
87
|
+
if (!matchesValueKeywords(value, schema)) {
|
|
65
88
|
return false;
|
|
66
89
|
}
|
|
67
|
-
if (
|
|
90
|
+
if (Array.isArray(schema.anyOf) &&
|
|
91
|
+
!schema.anyOf.some((candidate) => this.matches(value, candidate))) {
|
|
68
92
|
return false;
|
|
69
93
|
}
|
|
70
|
-
if (
|
|
71
|
-
!new RegExp(schema.pattern, "u").test(value)) {
|
|
94
|
+
if ("not" in schema && this.matches(value, schema.not)) {
|
|
72
95
|
return false;
|
|
73
96
|
}
|
|
97
|
+
return (matchesNumberKeywords(value, schema) &&
|
|
98
|
+
matchesStringKeywords(value, schema) &&
|
|
99
|
+
this.#matchesArrayKeywords(value, schema) &&
|
|
100
|
+
this.#matchesObjectKeywords(value, schema));
|
|
74
101
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
#matchesArrayKeywords(value, schema) {
|
|
103
|
+
if (!Array.isArray(value)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
const { items, maxItems, minItems, prefixItems } = schema;
|
|
107
|
+
if (isJsonNumber(minItems) && value.length < minItems) {
|
|
80
108
|
return false;
|
|
81
109
|
}
|
|
82
|
-
if (
|
|
110
|
+
if (isJsonNumber(maxItems) && value.length > maxItems) {
|
|
83
111
|
return false;
|
|
84
112
|
}
|
|
85
|
-
const prefix = Array.isArray(
|
|
113
|
+
const prefix = Array.isArray(prefixItems) ? prefixItems : [];
|
|
86
114
|
for (const [index, item] of value.entries()) {
|
|
87
|
-
const itemSchema = index < prefix.length ? prefix[index] :
|
|
88
|
-
if (itemSchema !== undefined &&
|
|
89
|
-
!matchesResponseSchema(item, itemSchema, root)) {
|
|
115
|
+
const itemSchema = index < prefix.length ? prefix[index] : items;
|
|
116
|
+
if (itemSchema !== undefined && !this.matches(item, itemSchema)) {
|
|
90
117
|
return false;
|
|
91
118
|
}
|
|
92
119
|
}
|
|
120
|
+
return true;
|
|
93
121
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
122
|
+
#matchesObjectKeywords(value, schema) {
|
|
123
|
+
if (!isJsonObject(value)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
const { additionalProperties, propertyNames, required } = schema;
|
|
127
|
+
if (Array.isArray(required) &&
|
|
128
|
+
!required.every((key) => isJsonString(key) && Object.hasOwn(value, key))) {
|
|
100
129
|
return false;
|
|
101
130
|
}
|
|
102
|
-
const properties =
|
|
131
|
+
const properties = isJsonObject(schema.properties) ? schema.properties : {};
|
|
103
132
|
for (const [key, item] of Object.entries(value)) {
|
|
104
|
-
if (
|
|
105
|
-
!matchesResponseSchema(key, schema.propertyNames, root)) {
|
|
133
|
+
if (propertyNames !== undefined && !this.matches(key, propertyNames)) {
|
|
106
134
|
return false;
|
|
107
135
|
}
|
|
108
136
|
const property = Object.hasOwn(properties, key)
|
|
109
137
|
? properties[key]
|
|
110
|
-
:
|
|
111
|
-
if (property !== undefined &&
|
|
112
|
-
!matchesResponseSchema(item, property, root)) {
|
|
138
|
+
: additionalProperties;
|
|
139
|
+
if (property !== undefined && !this.matches(item, property)) {
|
|
113
140
|
return false;
|
|
114
141
|
}
|
|
115
142
|
}
|
|
143
|
+
return true;
|
|
116
144
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!schema.$ref.startsWith("#/$defs/")) {
|
|
122
|
-
return false;
|
|
145
|
+
#matchesReference(value, schema) {
|
|
146
|
+
const reference = schema.$ref;
|
|
147
|
+
if (!isJsonString(reference)) {
|
|
148
|
+
return true;
|
|
123
149
|
}
|
|
124
|
-
|
|
125
|
-
.slice(8)
|
|
126
|
-
.replaceAll("~1", "/")
|
|
127
|
-
.replaceAll("~0", "~");
|
|
128
|
-
if (!isRecord(root.$defs) ||
|
|
129
|
-
!matchesResponseSchema(value, root.$defs[name], root)) {
|
|
150
|
+
if (!reference.startsWith("#/$defs/")) {
|
|
130
151
|
return false;
|
|
131
152
|
}
|
|
153
|
+
const name = reference.slice(8).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
154
|
+
const definitions = this.#root.$defs;
|
|
155
|
+
return isJsonObject(definitions) && this.matches(value, definitions[name]);
|
|
132
156
|
}
|
|
133
|
-
|
|
134
|
-
};
|
|
135
|
-
const matchesResponseValue = (value, schema) => {
|
|
136
|
-
if ("const" in schema && !sameJson(value, schema.const)) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
if (Array.isArray(schema.enum) &&
|
|
140
|
-
!schema.enum.some((candidate) => sameJson(candidate, value))) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
return true;
|
|
144
|
-
};
|
|
157
|
+
}
|
|
145
158
|
/** Evaluate the JSON Schema keywords used by the published response contracts. */
|
|
146
|
-
export const matchesResponseSchema = (value, schema, root) =>
|
|
147
|
-
if (typeof schema === "boolean") {
|
|
148
|
-
return schema;
|
|
149
|
-
}
|
|
150
|
-
if (!isRecord(schema)) {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
if (!matchesResponseReference(value, schema, root)) {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
if (!matchesType(value, schema.type)) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
if (!matchesResponseValue(value, schema)) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
if (Array.isArray(schema.anyOf) &&
|
|
163
|
-
!schema.anyOf.some((candidate) => matchesResponseSchema(value, candidate, root))) {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
if ("not" in schema && matchesResponseSchema(value, schema.not, root)) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
if (!matchesNumberKeywords(value, schema)) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
if (!matchesStringKeywords(value, schema)) {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
if (!matchesArrayKeywords(value, schema, root)) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
if (!matchesObjectKeywords(value, schema, root)) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
return true;
|
|
182
|
-
};
|
|
159
|
+
export const matchesResponseSchema = (value, schema, root) => new ResponseSchemaEvaluator(root).matches(value, schema);
|
|
183
160
|
const schemas = new Map();
|
|
184
161
|
/** The operation's published schema, rather than a caller's type argument, proves its response. */
|
|
185
162
|
const isOperationResponse = (operationId, value) => {
|
|
@@ -190,8 +167,8 @@ export const parseOperationResponse = async (operationId, value) => {
|
|
|
190
167
|
let schema = schemas.get(operationId);
|
|
191
168
|
if (!schema) {
|
|
192
169
|
// oxlint-disable-next-line howells/no-runtime-dynamic-imports -- Load the large response schema bundle only when a response needs validation.
|
|
193
|
-
const
|
|
194
|
-
schema =
|
|
170
|
+
const catalog = await import("./response-schema-catalog.js");
|
|
171
|
+
schema = catalog.buildResponseSchema(operationId);
|
|
195
172
|
openClientResponseEnums(schema);
|
|
196
173
|
schemas.set(operationId, schema);
|
|
197
174
|
}
|
package/resolve-color-types.d.ts
CHANGED
|
@@ -7,15 +7,15 @@ import type { VariantId } from "./identifiers.js";
|
|
|
7
7
|
import type { SearchCatalogSubset } from "./search-catalog-subset-types.js";
|
|
8
8
|
import type { OpenEnum } from "./types.js";
|
|
9
9
|
/** Options for `resolveColor`. */
|
|
10
|
-
export
|
|
10
|
+
export interface ResolveColorOptions {
|
|
11
11
|
/**
|
|
12
12
|
* Sets `inScope` on each catalog variant the lookup reports. It never
|
|
13
13
|
* narrows the lookup. Absent leaves every `inScope` null.
|
|
14
14
|
*/
|
|
15
15
|
searchCatalogSubset?: SearchCatalogSubset;
|
|
16
|
-
}
|
|
16
|
+
}
|
|
17
17
|
/** One catalog variant carrying the named paint. */
|
|
18
|
-
export
|
|
18
|
+
export interface ResolveColorCatalogVariant {
|
|
19
19
|
variantId: VariantId;
|
|
20
20
|
/** The brand exactly as stored, suspect or not. */
|
|
21
21
|
brand: {
|
|
@@ -25,7 +25,7 @@ export type ResolveColorCatalogVariant = {
|
|
|
25
25
|
};
|
|
26
26
|
/** Whether the variant is in the requested scope; null when none was asked for. */
|
|
27
27
|
inScope: boolean | null;
|
|
28
|
-
}
|
|
28
|
+
}
|
|
29
29
|
/**
|
|
30
30
|
* Whether the named paint is a variant in the catalog. Exact, global, and
|
|
31
31
|
* never narrowed by `searchCatalogSubset`, and run only for a `brand` library hit.
|
|
@@ -43,7 +43,7 @@ export type ResolveColorVariant = ({
|
|
|
43
43
|
status: "not-looked-up";
|
|
44
44
|
reason: OpenEnum<"reference-color" | "descriptive-color" | "no-paint-identity" | "generic-color" | "several-paints">;
|
|
45
45
|
};
|
|
46
|
-
export
|
|
46
|
+
export interface ResolveColorResponse {
|
|
47
47
|
/** Examples within the governed range. They do not replace its representative or bounds. */
|
|
48
48
|
rangeSamples?: string[];
|
|
49
49
|
/** Explicit brand choices for an ambiguous paint name. Never selected automatically. */
|
|
@@ -73,4 +73,4 @@ export type ResolveColorResponse = {
|
|
|
73
73
|
/** The catalog lookup for the name. Absent from servers that predate it. */
|
|
74
74
|
variant?: ResolveColorVariant;
|
|
75
75
|
} | null;
|
|
76
|
-
}
|
|
76
|
+
}
|
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
* product names visible on the page, sent only to tell several URL candidates
|
|
14
14
|
* apart - they never widen a search or invent an identity on their own.
|
|
15
15
|
*/
|
|
16
|
-
export
|
|
16
|
+
export interface ResolveVariantInput {
|
|
17
17
|
url?: string;
|
|
18
18
|
materialBankId?: string;
|
|
19
19
|
code?: string;
|
|
20
20
|
brandId?: string;
|
|
21
21
|
labels?: string[];
|
|
22
|
-
}
|
|
22
|
+
}
|
|
23
23
|
/** One identified, placed or bounded-candidate subject. */
|
|
24
|
-
export
|
|
24
|
+
export interface ResolveVariantSubject {
|
|
25
25
|
kind: "variant" | "brand";
|
|
26
26
|
id: string;
|
|
27
27
|
name: string;
|
|
@@ -30,23 +30,23 @@ export type ResolveVariantSubject = {
|
|
|
30
30
|
id: string;
|
|
31
31
|
name: string;
|
|
32
32
|
};
|
|
33
|
-
}
|
|
33
|
+
}
|
|
34
34
|
/** What stood behind an outcome. Exact and scoreless: never a similarity guess. */
|
|
35
|
-
export
|
|
35
|
+
export interface ResolveVariantEvidence {
|
|
36
36
|
kind: "brand-url" | "brand-homepage" | "brand-code" | "material-bank-id" | "visible-label";
|
|
37
37
|
url?: string;
|
|
38
38
|
detail: string;
|
|
39
|
-
}
|
|
39
|
+
}
|
|
40
40
|
/** The submitted URL, normalized, and the steps that normalization took. Absent for a code-only call. */
|
|
41
|
-
export
|
|
41
|
+
export interface ResolveVariantReceipt {
|
|
42
42
|
submittedUrl: string;
|
|
43
43
|
normalizedUrl: string;
|
|
44
44
|
normalizationSteps: string[];
|
|
45
|
-
}
|
|
46
|
-
|
|
45
|
+
}
|
|
46
|
+
interface ResolveVariantOutputBase {
|
|
47
47
|
evidence: ResolveVariantEvidence[];
|
|
48
48
|
receipt?: ResolveVariantReceipt;
|
|
49
|
-
}
|
|
49
|
+
}
|
|
50
50
|
/**
|
|
51
51
|
* Why nothing was resolved. Absent and empty differ: this names what was
|
|
52
52
|
* looked for and not found, never a similarity guess.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonObject } from "./json.js";
|
|
2
|
+
export declare const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
3
|
+
/**
|
|
4
|
+
* The success-response schema for one operation, self-contained and carrying
|
|
5
|
+
* only the definitions it reaches. Each call returns a fresh copy, so a caller
|
|
6
|
+
* that annotates or mutates it cannot change what the next caller gets.
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildResponseSchema: (operationId: string) => JsonObject;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated response-schema catalog, parsed once on first use. It is its
|
|
3
|
+
* own module because the catalog is large: importing the client does not load
|
|
4
|
+
* it.
|
|
5
|
+
*/
|
|
6
|
+
import { MaterialGraphError } from "./errors.js";
|
|
7
|
+
import { isJsonObject, isJsonString, parseJson } from "./json.js";
|
|
8
|
+
import { RESPONSE_SCHEMA_CATALOG_JSON } from "./response-schemas.generated.js";
|
|
9
|
+
export const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
10
|
+
const DEFINITION_PREFIX = "#/$defs/";
|
|
11
|
+
const isJsonSchemaMap = (value) => isJsonObject(value) && Object.values(value).every(isJsonObject);
|
|
12
|
+
const isCatalog = (value) => isJsonObject(value) &&
|
|
13
|
+
isJsonSchemaMap(value.definitions) &&
|
|
14
|
+
isJsonSchemaMap(value.operations);
|
|
15
|
+
let parsedCatalog;
|
|
16
|
+
const loadCatalog = () => {
|
|
17
|
+
if (parsedCatalog === undefined) {
|
|
18
|
+
const parsed = parseJson(RESPONSE_SCHEMA_CATALOG_JSON);
|
|
19
|
+
if (!isCatalog(parsed)) {
|
|
20
|
+
throw MaterialGraphError.unexpected("The response schema catalog is malformed");
|
|
21
|
+
}
|
|
22
|
+
parsedCatalog = parsed;
|
|
23
|
+
}
|
|
24
|
+
return parsedCatalog;
|
|
25
|
+
};
|
|
26
|
+
/** Add every definition `value` reaches, transitively, to `into`. */
|
|
27
|
+
const collectReferences = (catalog, value, into) => {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
for (const item of value) {
|
|
30
|
+
collectReferences(catalog, item, into);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!isJsonObject(value)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const [key, child] of Object.entries(value)) {
|
|
38
|
+
if (key === "$ref" &&
|
|
39
|
+
isJsonString(child) &&
|
|
40
|
+
child.startsWith(DEFINITION_PREFIX)) {
|
|
41
|
+
const name = child
|
|
42
|
+
.slice(DEFINITION_PREFIX.length)
|
|
43
|
+
.replaceAll("~1", "/")
|
|
44
|
+
.replaceAll("~0", "~");
|
|
45
|
+
if (!into.has(name)) {
|
|
46
|
+
into.add(name);
|
|
47
|
+
collectReferences(catalog, catalog.definitions[name], into);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
collectReferences(catalog, child, into);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The success-response schema for one operation, self-contained and carrying
|
|
57
|
+
* only the definitions it reaches. Each call returns a fresh copy, so a caller
|
|
58
|
+
* that annotates or mutates it cannot change what the next caller gets.
|
|
59
|
+
*/
|
|
60
|
+
export const buildResponseSchema = (operationId) => {
|
|
61
|
+
const catalog = loadCatalog();
|
|
62
|
+
const root = Object.hasOwn(catalog.operations, operationId)
|
|
63
|
+
? catalog.operations[operationId]
|
|
64
|
+
: undefined;
|
|
65
|
+
if (root === undefined || !isJsonObject(root)) {
|
|
66
|
+
throw MaterialGraphError.unexpected(`No response schema for operation: ${operationId}`);
|
|
67
|
+
}
|
|
68
|
+
const names = new Set();
|
|
69
|
+
collectReferences(catalog, root, names);
|
|
70
|
+
const definitions = {};
|
|
71
|
+
for (const name of [...names].toSorted()) {
|
|
72
|
+
const definition = catalog.definitions[name];
|
|
73
|
+
if (definition !== undefined) {
|
|
74
|
+
definitions[name] = definition;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const schema = { $schema: JSON_SCHEMA_DIALECT, ...root };
|
|
78
|
+
if (names.size > 0) {
|
|
79
|
+
schema.$defs = definitions;
|
|
80
|
+
}
|
|
81
|
+
return structuredClone(schema);
|
|
82
|
+
};
|
package/response-schemas.d.ts
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime response contracts: the JSON Schema of each operation's success body.
|
|
3
|
+
*
|
|
4
|
+
* The schemas are the producer's own. They are projected from the served
|
|
5
|
+
* OpenAPI document, whose response bodies are generated from the Zod contracts
|
|
6
|
+
* the routes validate against, so a consumer never has to re-describe a large,
|
|
7
|
+
* partly open payload and watch it drift. Hand one to anything that takes a
|
|
8
|
+
* JSON Schema, such as a Mastra tool's `outputSchema`:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { responseSchema } from "@instruments/materialgraph/response-schemas";
|
|
12
|
+
*
|
|
13
|
+
* createTool({ outputSchema: responseSchema("search_variants"), … });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Keyed by OpenAPI `operationId`, the same id `SDK_OPERATIONS` pairs with each
|
|
17
|
+
* client method. Schemas are JSON Schema 2020-12 and describe the parsed body
|
|
18
|
+
* the client method returns.
|
|
19
|
+
*
|
|
20
|
+
* Objects accept properties the schema does not declare, so a field the API
|
|
21
|
+
* adds later never makes a pinned consumer reject a live response; enums and
|
|
22
|
+
* constants stay closed. That is why these are more lenient than the WebMCP
|
|
23
|
+
* `outputSchema`s, which describe what the API sends rather than what a
|
|
24
|
+
* consumer should tolerate.
|
|
25
|
+
*
|
|
26
|
+
* This is its own entry point because the catalog is large; importing the
|
|
27
|
+
* client does not load it, and it is parsed on the first `responseSchema()`
|
|
28
|
+
* call rather than on import.
|
|
29
|
+
*/
|
|
30
|
+
import { JSON_SCHEMA_DIALECT } from "./response-schema-catalog.js";
|
|
1
31
|
import type { RESPONSE_SCHEMA_OPERATION_IDS } from "./response-schemas.generated.js";
|
|
2
32
|
export { RESPONSE_SCHEMA_API_VERSION, RESPONSE_SCHEMA_OPERATION_IDS, } from "./response-schemas.generated.js";
|
|
3
33
|
/** Every operation {@link responseSchema} answers for. */
|
|
4
34
|
export type ResponseSchemaOperationId = (typeof RESPONSE_SCHEMA_OPERATION_IDS)[number];
|
|
5
35
|
/** A JSON Schema document or subschema, as plain JSON. */
|
|
6
|
-
export
|
|
36
|
+
export interface JsonSchema {
|
|
7
37
|
[keyword: string]: unknown;
|
|
8
|
-
}
|
|
38
|
+
}
|
|
9
39
|
/** A self-contained schema: every `$ref` resolves inside its own `$defs`. */
|
|
10
40
|
export type ResponseJsonSchema = JsonSchema & {
|
|
11
41
|
$schema: typeof JSON_SCHEMA_DIALECT;
|
|
12
42
|
$defs?: Record<string, JsonSchema>;
|
|
13
43
|
};
|
|
14
|
-
declare const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
15
44
|
/**
|
|
16
45
|
* The success-response schema for one operation, self-contained and carrying
|
|
17
46
|
* only the definitions it reaches. Each call returns a fresh copy, so a caller
|