@mcp-web/core 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.
- package/LICENSE +201 -0
- package/README.md +253 -0
- package/dist/addTool.typetest.d.ts +11 -0
- package/dist/addTool.typetest.d.ts.map +1 -0
- package/dist/addTool.typetest.js +248 -0
- package/dist/create-state-tools.d.ts +77 -0
- package/dist/create-state-tools.d.ts.map +1 -0
- package/dist/create-state-tools.js +181 -0
- package/dist/create-tool.d.ts +90 -0
- package/dist/create-tool.d.ts.map +1 -0
- package/dist/create-tool.js +82 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts +8 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts.map +1 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.js +53 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts +2 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.js +331 -0
- package/dist/expanded-schema-tools/index.d.ts +4 -0
- package/dist/expanded-schema-tools/index.d.ts.map +1 -0
- package/dist/expanded-schema-tools/index.js +2 -0
- package/dist/expanded-schema-tools/integration.test.d.ts +2 -0
- package/dist/expanded-schema-tools/integration.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/integration.test.js +599 -0
- package/dist/expanded-schema-tools/schema-analysis.d.ts +18 -0
- package/dist/expanded-schema-tools/schema-analysis.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-analysis.js +142 -0
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts +2 -0
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-analysis.test.js +314 -0
- package/dist/expanded-schema-tools/schema-helpers.d.ts +69 -0
- package/dist/expanded-schema-tools/schema-helpers.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-helpers.js +139 -0
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts +2 -0
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-helpers.test.js +223 -0
- package/dist/expanded-schema-tools/tool-generator.d.ts +10 -0
- package/dist/expanded-schema-tools/tool-generator.d.ts.map +1 -0
- package/dist/expanded-schema-tools/tool-generator.js +430 -0
- package/dist/expanded-schema-tools/tool-generator.test.d.ts +2 -0
- package/dist/expanded-schema-tools/tool-generator.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/tool-generator.test.js +689 -0
- package/dist/expanded-schema-tools/types.d.ts +26 -0
- package/dist/expanded-schema-tools/types.d.ts.map +1 -0
- package/dist/expanded-schema-tools/types.js +1 -0
- package/dist/expanded-schema-tools/utils.d.ts +16 -0
- package/dist/expanded-schema-tools/utils.d.ts.map +1 -0
- package/dist/expanded-schema-tools/utils.js +35 -0
- package/dist/expanded-schema-tools/utils.test.d.ts +2 -0
- package/dist/expanded-schema-tools/utils.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/utils.test.js +169 -0
- package/dist/group-state.d.ts +60 -0
- package/dist/group-state.d.ts.map +1 -0
- package/dist/group-state.js +54 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/query.d.ts +104 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +128 -0
- package/dist/schema-helpers.d.ts +69 -0
- package/dist/schema-helpers.d.ts.map +1 -0
- package/dist/schema-helpers.js +139 -0
- package/dist/schemas.d.ts +140 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +70 -0
- package/dist/tool-generators/generate-basic-state-tools.d.ts +23 -0
- package/dist/tool-generators/generate-basic-state-tools.d.ts.map +1 -0
- package/dist/tool-generators/generate-basic-state-tools.js +95 -0
- package/dist/tool-generators/generate-fixed-shape-tools.d.ts +8 -0
- package/dist/tool-generators/generate-fixed-shape-tools.d.ts.map +1 -0
- package/dist/tool-generators/generate-fixed-shape-tools.js +53 -0
- package/dist/tool-generators/index.d.ts +6 -0
- package/dist/tool-generators/index.d.ts.map +1 -0
- package/dist/tool-generators/index.js +3 -0
- package/dist/tool-generators/schema-analysis.d.ts +18 -0
- package/dist/tool-generators/schema-analysis.d.ts.map +1 -0
- package/dist/tool-generators/schema-analysis.js +142 -0
- package/dist/tool-generators/schema-helpers.d.ts +87 -0
- package/dist/tool-generators/schema-helpers.d.ts.map +1 -0
- package/dist/tool-generators/schema-helpers.js +157 -0
- package/dist/tool-generators/tool-generator.d.ts +11 -0
- package/dist/tool-generators/tool-generator.d.ts.map +1 -0
- package/dist/tool-generators/tool-generator.js +437 -0
- package/dist/tool-generators/types.d.ts +26 -0
- package/dist/tool-generators/types.d.ts.map +1 -0
- package/dist/tool-generators/types.js +1 -0
- package/dist/tool-generators/utils.d.ts +16 -0
- package/dist/tool-generators/utils.d.ts.map +1 -0
- package/dist/tool-generators/utils.js +35 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +108 -0
- package/dist/web.d.ts +680 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +1312 -0
- package/dist/zod-to-tools.d.ts +49 -0
- package/dist/zod-to-tools.d.ts.map +1 -0
- package/dist/zod-to-tools.js +623 -0
- package/package.json +58 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { isKeyField, unwrapSchema } from './schema-helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Analyzes a schema to determine its shape characteristics.
|
|
5
|
+
*/
|
|
6
|
+
export function analyzeSchemaShape(schema) {
|
|
7
|
+
const unwrapped = unwrapSchema(schema);
|
|
8
|
+
// Default result
|
|
9
|
+
const result = {
|
|
10
|
+
type: 'unsupported',
|
|
11
|
+
subtype: 'unknown',
|
|
12
|
+
hasOptionalFields: false,
|
|
13
|
+
optionalPaths: [],
|
|
14
|
+
fixedPaths: [],
|
|
15
|
+
dynamicPaths: [],
|
|
16
|
+
};
|
|
17
|
+
if (unwrapped instanceof z.ZodString ||
|
|
18
|
+
unwrapped instanceof z.ZodNumber ||
|
|
19
|
+
unwrapped instanceof z.ZodBoolean ||
|
|
20
|
+
unwrapped instanceof z.ZodLiteral ||
|
|
21
|
+
unwrapped instanceof z.ZodEnum) {
|
|
22
|
+
result.type = 'fixed';
|
|
23
|
+
result.subtype = 'primitive';
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
if (unwrapped instanceof z.ZodTuple) {
|
|
27
|
+
result.type = 'fixed';
|
|
28
|
+
result.subtype = 'tuple';
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
if (unwrapped instanceof z.ZodArray) {
|
|
32
|
+
result.type = 'dynamic';
|
|
33
|
+
result.subtype = 'array';
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
if (unwrapped instanceof z.ZodRecord) {
|
|
37
|
+
result.type = 'dynamic';
|
|
38
|
+
result.subtype = 'record';
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
// Objects - analyze further
|
|
42
|
+
if (unwrapped instanceof z.ZodObject) {
|
|
43
|
+
result.subtype = 'object';
|
|
44
|
+
const shape = unwrapped.shape;
|
|
45
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
46
|
+
const zodField = field;
|
|
47
|
+
const unwrappedField = unwrapSchema(zodField);
|
|
48
|
+
// Check for optional fields
|
|
49
|
+
if (zodField instanceof z.ZodOptional) {
|
|
50
|
+
result.hasOptionalFields = true;
|
|
51
|
+
result.optionalPaths.push(key);
|
|
52
|
+
}
|
|
53
|
+
// Check if field is dynamic (array or record)
|
|
54
|
+
if (unwrappedField instanceof z.ZodArray || unwrappedField instanceof z.ZodRecord) {
|
|
55
|
+
result.dynamicPaths.push(key);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
result.fixedPaths.push(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Determine object type
|
|
62
|
+
if (result.dynamicPaths.length > 0 && result.fixedPaths.length > 0) {
|
|
63
|
+
result.type = 'mixed';
|
|
64
|
+
}
|
|
65
|
+
else if (result.dynamicPaths.length > 0) {
|
|
66
|
+
result.type = 'dynamic';
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result.type = 'fixed';
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Detects the ID field in an object schema.
|
|
77
|
+
* Returns information about explicit id() markers.
|
|
78
|
+
* Throws an error if multiple id() markers are found.
|
|
79
|
+
*/
|
|
80
|
+
export function findIdField(schema) {
|
|
81
|
+
const shape = schema.shape;
|
|
82
|
+
let explicitKey = null;
|
|
83
|
+
for (const [name, field] of Object.entries(shape)) {
|
|
84
|
+
if (isKeyField(field)) {
|
|
85
|
+
// Error if multiple id() markers
|
|
86
|
+
if (explicitKey) {
|
|
87
|
+
throw new Error(`Multiple fields marked with id(): '${explicitKey}' and '${name}'.\n` +
|
|
88
|
+
`Only one field can be the ID. For compound keys, use index-based addressing.`);
|
|
89
|
+
}
|
|
90
|
+
explicitKey = name;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (explicitKey) {
|
|
94
|
+
return { type: 'explicit', field: explicitKey };
|
|
95
|
+
}
|
|
96
|
+
// No id() marker → use index-based addressing
|
|
97
|
+
return { type: 'none' };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Validates that a schema only uses supported types.
|
|
101
|
+
* Throws an error if unsupported types are found.
|
|
102
|
+
*/
|
|
103
|
+
export function validateSupportedTypes(schema, path = 'root') {
|
|
104
|
+
const errors = [];
|
|
105
|
+
const unwrapped = unwrapSchema(schema);
|
|
106
|
+
const supported = [
|
|
107
|
+
z.ZodObject,
|
|
108
|
+
z.ZodArray,
|
|
109
|
+
z.ZodRecord,
|
|
110
|
+
z.ZodString,
|
|
111
|
+
z.ZodNumber,
|
|
112
|
+
z.ZodBoolean,
|
|
113
|
+
z.ZodLiteral,
|
|
114
|
+
z.ZodEnum,
|
|
115
|
+
z.ZodTuple,
|
|
116
|
+
z.ZodDate,
|
|
117
|
+
z.ZodBigInt,
|
|
118
|
+
z.ZodNull,
|
|
119
|
+
z.ZodUndefined,
|
|
120
|
+
];
|
|
121
|
+
const isSupported = supported.some((type) => unwrapped instanceof type);
|
|
122
|
+
if (!isSupported) {
|
|
123
|
+
errors.push(`Unsupported type at '${path}': ${unwrapped.constructor.name}`);
|
|
124
|
+
}
|
|
125
|
+
// Recurse for nested schemas
|
|
126
|
+
if (unwrapped instanceof z.ZodObject) {
|
|
127
|
+
for (const [key, field] of Object.entries(unwrapped.shape)) {
|
|
128
|
+
errors.push(...validateSupportedTypes(field, `${path}.${key}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (unwrapped instanceof z.ZodArray) {
|
|
132
|
+
errors.push(...validateSupportedTypes(unwrapped.element, `${path}[]`));
|
|
133
|
+
}
|
|
134
|
+
else if (unwrapped instanceof z.ZodRecord) {
|
|
135
|
+
const def = unwrapped._def;
|
|
136
|
+
const valueType = def.valueType;
|
|
137
|
+
if (valueType) {
|
|
138
|
+
errors.push(...validateSupportedTypes(valueType, `${path}{}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return errors;
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-analysis.test.d.ts","sourceRoot":"","sources":["../../src/expanded-schema-tools/schema-analysis.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { analyzeSchemaShape, findIdField, validateSupportedTypes, } from './schema-analysis.js';
|
|
4
|
+
import { id } from './schema-helpers.js';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Shape Detection - Primitives
|
|
7
|
+
// ============================================================================
|
|
8
|
+
test('analyzeSchemaShape() - string is fixed primitive', () => {
|
|
9
|
+
const schema = z.string();
|
|
10
|
+
const shape = analyzeSchemaShape(schema);
|
|
11
|
+
expect(shape.type).toBe('fixed');
|
|
12
|
+
expect(shape.subtype).toBe('primitive');
|
|
13
|
+
});
|
|
14
|
+
test('analyzeSchemaShape() - number is fixed primitive', () => {
|
|
15
|
+
const schema = z.number();
|
|
16
|
+
const shape = analyzeSchemaShape(schema);
|
|
17
|
+
expect(shape.type).toBe('fixed');
|
|
18
|
+
expect(shape.subtype).toBe('primitive');
|
|
19
|
+
});
|
|
20
|
+
test('analyzeSchemaShape() - boolean is fixed primitive', () => {
|
|
21
|
+
const schema = z.boolean();
|
|
22
|
+
const shape = analyzeSchemaShape(schema);
|
|
23
|
+
expect(shape.type).toBe('fixed');
|
|
24
|
+
expect(shape.subtype).toBe('primitive');
|
|
25
|
+
});
|
|
26
|
+
test('analyzeSchemaShape() - enum is fixed primitive', () => {
|
|
27
|
+
const schema = z.enum(['light', 'dark', 'system']);
|
|
28
|
+
const shape = analyzeSchemaShape(schema);
|
|
29
|
+
expect(shape.type).toBe('fixed');
|
|
30
|
+
expect(shape.subtype).toBe('primitive');
|
|
31
|
+
});
|
|
32
|
+
test('analyzeSchemaShape() - literal is fixed primitive', () => {
|
|
33
|
+
const schema = z.literal('constant');
|
|
34
|
+
const shape = analyzeSchemaShape(schema);
|
|
35
|
+
expect(shape.type).toBe('fixed');
|
|
36
|
+
expect(shape.subtype).toBe('primitive');
|
|
37
|
+
});
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Shape Detection - Collections
|
|
40
|
+
// ============================================================================
|
|
41
|
+
test('analyzeSchemaShape() - array is dynamic', () => {
|
|
42
|
+
const schema = z.array(z.string());
|
|
43
|
+
const shape = analyzeSchemaShape(schema);
|
|
44
|
+
expect(shape.type).toBe('dynamic');
|
|
45
|
+
expect(shape.subtype).toBe('array');
|
|
46
|
+
});
|
|
47
|
+
test('analyzeSchemaShape() - record is dynamic', () => {
|
|
48
|
+
const schema = z.record(z.string(), z.number());
|
|
49
|
+
const shape = analyzeSchemaShape(schema);
|
|
50
|
+
expect(shape.type).toBe('dynamic');
|
|
51
|
+
expect(shape.subtype).toBe('record');
|
|
52
|
+
});
|
|
53
|
+
test('analyzeSchemaShape() - tuple is fixed', () => {
|
|
54
|
+
const schema = z.tuple([z.string(), z.number(), z.boolean()]);
|
|
55
|
+
const shape = analyzeSchemaShape(schema);
|
|
56
|
+
expect(shape.type).toBe('fixed');
|
|
57
|
+
expect(shape.subtype).toBe('tuple');
|
|
58
|
+
});
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Shape Detection - Objects
|
|
61
|
+
// ============================================================================
|
|
62
|
+
test('analyzeSchemaShape() - object with only fixed props is fixed', () => {
|
|
63
|
+
const schema = z.object({
|
|
64
|
+
name: z.string(),
|
|
65
|
+
age: z.number(),
|
|
66
|
+
active: z.boolean(),
|
|
67
|
+
});
|
|
68
|
+
const shape = analyzeSchemaShape(schema);
|
|
69
|
+
expect(shape.type).toBe('fixed');
|
|
70
|
+
expect(shape.subtype).toBe('object');
|
|
71
|
+
expect(shape.fixedPaths).toEqual(['name', 'age', 'active']);
|
|
72
|
+
expect(shape.dynamicPaths).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
test('analyzeSchemaShape() - object with only dynamic props is dynamic', () => {
|
|
75
|
+
const schema = z.object({
|
|
76
|
+
todos: z.array(z.string()),
|
|
77
|
+
projects: z.record(z.string(), z.object({ name: z.string() })),
|
|
78
|
+
});
|
|
79
|
+
const shape = analyzeSchemaShape(schema);
|
|
80
|
+
expect(shape.type).toBe('dynamic');
|
|
81
|
+
expect(shape.subtype).toBe('object');
|
|
82
|
+
expect(shape.fixedPaths).toEqual([]);
|
|
83
|
+
expect(shape.dynamicPaths).toEqual(['todos', 'projects']);
|
|
84
|
+
});
|
|
85
|
+
test('analyzeSchemaShape() - object with mixed props is mixed', () => {
|
|
86
|
+
const schema = z.object({
|
|
87
|
+
name: z.string(),
|
|
88
|
+
todos: z.array(z.string()),
|
|
89
|
+
settings: z.object({ theme: z.string() }),
|
|
90
|
+
projects: z.record(z.string(), z.object({ title: z.string() })),
|
|
91
|
+
});
|
|
92
|
+
const shape = analyzeSchemaShape(schema);
|
|
93
|
+
expect(shape.type).toBe('mixed');
|
|
94
|
+
expect(shape.subtype).toBe('object');
|
|
95
|
+
expect(shape.fixedPaths).toContain('name');
|
|
96
|
+
expect(shape.fixedPaths).toContain('settings');
|
|
97
|
+
expect(shape.dynamicPaths).toContain('todos');
|
|
98
|
+
expect(shape.dynamicPaths).toContain('projects');
|
|
99
|
+
});
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Optional Field Detection
|
|
102
|
+
// ============================================================================
|
|
103
|
+
test('analyzeSchemaShape() - detects optional fields', () => {
|
|
104
|
+
const schema = z.object({
|
|
105
|
+
name: z.string(),
|
|
106
|
+
email: z.string().optional(),
|
|
107
|
+
age: z.number().optional(),
|
|
108
|
+
});
|
|
109
|
+
const shape = analyzeSchemaShape(schema);
|
|
110
|
+
expect(shape.hasOptionalFields).toBe(true);
|
|
111
|
+
expect(shape.optionalPaths).toContain('email');
|
|
112
|
+
expect(shape.optionalPaths).toContain('age');
|
|
113
|
+
expect(shape.optionalPaths).not.toContain('name');
|
|
114
|
+
});
|
|
115
|
+
test('analyzeSchemaShape() - no optional fields when none present', () => {
|
|
116
|
+
const schema = z.object({
|
|
117
|
+
name: z.string(),
|
|
118
|
+
age: z.number(),
|
|
119
|
+
});
|
|
120
|
+
const shape = analyzeSchemaShape(schema);
|
|
121
|
+
expect(shape.hasOptionalFields).toBe(false);
|
|
122
|
+
expect(shape.optionalPaths).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
test('analyzeSchemaShape() - wrapped optional fields (optional before default not detected)', () => {
|
|
125
|
+
// When optional() comes before default(), the field is wrapped by ZodDefault
|
|
126
|
+
// So the optional detection doesn't see it (checks the outer ZodDefault, not inner ZodOptional)
|
|
127
|
+
const schema = z.object({
|
|
128
|
+
value: z.string().optional().default('hello'),
|
|
129
|
+
});
|
|
130
|
+
const shape = analyzeSchemaShape(schema);
|
|
131
|
+
// This is a limitation - wrapped optionals are not detected
|
|
132
|
+
expect(shape.hasOptionalFields).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Fixed vs Dynamic Path Tracking
|
|
136
|
+
// ============================================================================
|
|
137
|
+
test('analyzeSchemaShape() - fixedPaths contains non-collection fields', () => {
|
|
138
|
+
const schema = z.object({
|
|
139
|
+
title: z.string(),
|
|
140
|
+
count: z.number(),
|
|
141
|
+
nested: z.object({ value: z.string() }),
|
|
142
|
+
items: z.array(z.string()),
|
|
143
|
+
});
|
|
144
|
+
const shape = analyzeSchemaShape(schema);
|
|
145
|
+
expect(shape.fixedPaths).toContain('title');
|
|
146
|
+
expect(shape.fixedPaths).toContain('count');
|
|
147
|
+
expect(shape.fixedPaths).toContain('nested');
|
|
148
|
+
expect(shape.fixedPaths).not.toContain('items');
|
|
149
|
+
});
|
|
150
|
+
test('analyzeSchemaShape() - dynamicPaths contains array/record fields', () => {
|
|
151
|
+
const schema = z.object({
|
|
152
|
+
name: z.string(),
|
|
153
|
+
todos: z.array(z.string()),
|
|
154
|
+
projects: z.record(z.string(), z.object({ title: z.string() })),
|
|
155
|
+
});
|
|
156
|
+
const shape = analyzeSchemaShape(schema);
|
|
157
|
+
expect(shape.dynamicPaths).toContain('todos');
|
|
158
|
+
expect(shape.dynamicPaths).toContain('projects');
|
|
159
|
+
expect(shape.dynamicPaths).not.toContain('name');
|
|
160
|
+
});
|
|
161
|
+
test('analyzeSchemaShape() - correctly categorizes nested objects', () => {
|
|
162
|
+
const schema = z.object({
|
|
163
|
+
settings: z.object({
|
|
164
|
+
theme: z.string(),
|
|
165
|
+
language: z.string(),
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
const shape = analyzeSchemaShape(schema);
|
|
169
|
+
expect(shape.fixedPaths).toContain('settings');
|
|
170
|
+
expect(shape.dynamicPaths).not.toContain('settings');
|
|
171
|
+
});
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// ID Field Detection
|
|
174
|
+
// ============================================================================
|
|
175
|
+
test('findIdField() - detects explicit id() marker', () => {
|
|
176
|
+
const schema = z.object({
|
|
177
|
+
id: id(z.string()),
|
|
178
|
+
name: z.string(),
|
|
179
|
+
});
|
|
180
|
+
const result = findIdField(schema);
|
|
181
|
+
expect(result.type).toBe('explicit');
|
|
182
|
+
expect(result.field).toBe('id');
|
|
183
|
+
});
|
|
184
|
+
test('findIdField() - returns none when no marker', () => {
|
|
185
|
+
const schema = z.object({
|
|
186
|
+
id: z.string(),
|
|
187
|
+
name: z.string(),
|
|
188
|
+
});
|
|
189
|
+
const result = findIdField(schema);
|
|
190
|
+
expect(result.type).toBe('none');
|
|
191
|
+
expect(result.field).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
test('findIdField() - throws error on multiple id() markers', () => {
|
|
194
|
+
const schema = z.object({
|
|
195
|
+
id: id(z.string()),
|
|
196
|
+
userId: id(z.string()),
|
|
197
|
+
name: z.string(),
|
|
198
|
+
});
|
|
199
|
+
expect(() => findIdField(schema)).toThrow();
|
|
200
|
+
});
|
|
201
|
+
test('findIdField() - error message mentions both fields', () => {
|
|
202
|
+
const schema = z.object({
|
|
203
|
+
primaryId: id(z.string()),
|
|
204
|
+
secondaryId: id(z.string()),
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
findIdField(schema);
|
|
208
|
+
expect(true).toBe(false); // Should not reach here
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const message = error.message;
|
|
212
|
+
expect(message).toContain('primaryId');
|
|
213
|
+
expect(message).toContain('secondaryId');
|
|
214
|
+
expect(message).toContain('Multiple');
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
test('findIdField() - error suggests using index-based for compound keys', () => {
|
|
218
|
+
const schema = z.object({
|
|
219
|
+
id1: id(z.string()),
|
|
220
|
+
id2: id(z.string()),
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
findIdField(schema);
|
|
224
|
+
expect(true).toBe(false); // Should not reach here
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
const message = error.message;
|
|
228
|
+
expect(message).toContain('index-based');
|
|
229
|
+
expect(message).toContain('compound');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Type Validation
|
|
234
|
+
// ============================================================================
|
|
235
|
+
test('validateSupportedTypes() - allows all JSON-compatible types', () => {
|
|
236
|
+
const schema = z.object({
|
|
237
|
+
str: z.string(),
|
|
238
|
+
num: z.number(),
|
|
239
|
+
bool: z.boolean(),
|
|
240
|
+
date: z.date(),
|
|
241
|
+
bigint: z.bigint(),
|
|
242
|
+
nullValue: z.null(),
|
|
243
|
+
undefinedValue: z.undefined(),
|
|
244
|
+
literal: z.literal('constant'),
|
|
245
|
+
enum: z.enum(['a', 'b']),
|
|
246
|
+
});
|
|
247
|
+
const errors = validateSupportedTypes(schema);
|
|
248
|
+
expect(errors).toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
test('validateSupportedTypes() - detects unsupported types', () => {
|
|
251
|
+
const schema = z.function();
|
|
252
|
+
const errors = validateSupportedTypes(schema);
|
|
253
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
254
|
+
expect(errors[0]).toContain('Unsupported');
|
|
255
|
+
});
|
|
256
|
+
test('validateSupportedTypes() - recurses into nested objects', () => {
|
|
257
|
+
const schema = z.object({
|
|
258
|
+
valid: z.string(),
|
|
259
|
+
nested: z.object({
|
|
260
|
+
alsoValid: z.number(),
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
const errors = validateSupportedTypes(schema);
|
|
264
|
+
expect(errors).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
test('validateSupportedTypes() - checks array elements', () => {
|
|
267
|
+
const schema = z.array(z.string());
|
|
268
|
+
const errors = validateSupportedTypes(schema);
|
|
269
|
+
expect(errors).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
test('validateSupportedTypes() - checks record values', () => {
|
|
272
|
+
const schema = z.record(z.string(), z.object({
|
|
273
|
+
name: z.string(),
|
|
274
|
+
count: z.number(),
|
|
275
|
+
}));
|
|
276
|
+
const errors = validateSupportedTypes(schema);
|
|
277
|
+
expect(errors).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
test('validateSupportedTypes() - detects unsupported nested types', () => {
|
|
280
|
+
const schema = z.object({
|
|
281
|
+
valid: z.string(),
|
|
282
|
+
nested: z.object({
|
|
283
|
+
invalid: z.function(),
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
const errors = validateSupportedTypes(schema);
|
|
287
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
288
|
+
expect(errors[0]).toContain('nested.invalid');
|
|
289
|
+
});
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Wrapped Schema Handling
|
|
292
|
+
// ============================================================================
|
|
293
|
+
test('analyzeSchemaShape() - handles wrapped schemas (default, optional, nullable)', () => {
|
|
294
|
+
const schema = z.object({
|
|
295
|
+
withDefault: z.string().default('hello'),
|
|
296
|
+
withOptional: z.number().optional(),
|
|
297
|
+
withNullable: z.boolean().nullable(),
|
|
298
|
+
});
|
|
299
|
+
const shape = analyzeSchemaShape(schema);
|
|
300
|
+
expect(shape.type).toBe('fixed');
|
|
301
|
+
expect(shape.subtype).toBe('object');
|
|
302
|
+
});
|
|
303
|
+
test('analyzeSchemaShape() - unwraps arrays with defaults', () => {
|
|
304
|
+
const schema = z.array(z.string()).default([]);
|
|
305
|
+
const shape = analyzeSchemaShape(schema);
|
|
306
|
+
expect(shape.type).toBe('dynamic');
|
|
307
|
+
expect(shape.subtype).toBe('array');
|
|
308
|
+
});
|
|
309
|
+
test('analyzeSchemaShape() - unwraps records with defaults', () => {
|
|
310
|
+
const schema = z.record(z.string(), z.number()).default({});
|
|
311
|
+
const shape = analyzeSchemaShape(schema);
|
|
312
|
+
expect(shape.type).toBe('dynamic');
|
|
313
|
+
expect(shape.subtype).toBe('record');
|
|
314
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Marks a field as the unique identifier for array elements.
|
|
4
|
+
* Enables ID-based tools instead of index-based.
|
|
5
|
+
* Only one field per schema can be marked with id().
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const TodoSchema = z.object({
|
|
10
|
+
* id: id(z.string()),
|
|
11
|
+
* value: z.string()
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function id<T extends z.ZodTypeAny>(schema: T): T;
|
|
16
|
+
/**
|
|
17
|
+
* Marks a field as system-generated.
|
|
18
|
+
* Field is excluded from input schemas (add/set).
|
|
19
|
+
* MUST have a default() — error thrown otherwise.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const TodoSchema = z.object({
|
|
24
|
+
* id: id(system(z.string().default(() => crypto.randomUUID()))),
|
|
25
|
+
* created_at: system(z.number().default(() => Date.now()))
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function system<T extends z.ZodTypeAny>(schema: T): T;
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a field is marked with id().
|
|
32
|
+
*/
|
|
33
|
+
export declare function isKeyField(field: z.ZodTypeAny): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Checks if a field is marked with system().
|
|
36
|
+
*/
|
|
37
|
+
export declare function isSystemField(field: z.ZodTypeAny): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a schema has a default value.
|
|
40
|
+
*/
|
|
41
|
+
export declare function hasDefault(schema: z.ZodTypeAny): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Unwraps a schema from ZodDefault, ZodOptional, ZodNullable wrappers.
|
|
44
|
+
* Returns the innermost schema.
|
|
45
|
+
*/
|
|
46
|
+
export declare function unwrapSchema(schema: z.ZodTypeAny): z.ZodTypeAny;
|
|
47
|
+
/**
|
|
48
|
+
* Unwraps ZodDefault to get the inner schema.
|
|
49
|
+
*/
|
|
50
|
+
export declare function unwrapDefault(schema: z.ZodTypeAny): z.ZodTypeAny;
|
|
51
|
+
/**
|
|
52
|
+
* Derives an input schema for add operations.
|
|
53
|
+
* - system() fields are excluded
|
|
54
|
+
* - default() fields become optional
|
|
55
|
+
* - other fields remain required
|
|
56
|
+
*/
|
|
57
|
+
export declare function deriveAddInputSchema(schema: z.ZodObject<z.ZodRawShape>): z.ZodObject<z.ZodRawShape>;
|
|
58
|
+
/**
|
|
59
|
+
* Derives an input schema for set operations (partial updates).
|
|
60
|
+
* - system() fields are excluded
|
|
61
|
+
* - all other fields become optional
|
|
62
|
+
*/
|
|
63
|
+
export declare function deriveSetInputSchema(schema: z.ZodObject<z.ZodRawShape>): z.ZodObject<z.ZodRawShape>;
|
|
64
|
+
/**
|
|
65
|
+
* Validates that all system() fields have default values.
|
|
66
|
+
* Throws an error if a system field lacks a default.
|
|
67
|
+
*/
|
|
68
|
+
export declare function validateSystemFields(schema: z.ZodObject<z.ZodRawShape>): void;
|
|
69
|
+
//# sourceMappingURL=schema-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-helpers.d.ts","sourceRoot":"","sources":["../../src/expanded-schema-tools/schema-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;GAYG;AACH,wBAAgB,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAGvD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAG3D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,UAAU,GAAG,OAAO,CAEvD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,UAAU,GAAG,OAAO,CAE1D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,OAAO,CASxD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAa/D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAMhE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAiBnG;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAanG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,IAAI,CAa7E"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Marks a field as the unique identifier for array elements.
|
|
4
|
+
* Enables ID-based tools instead of index-based.
|
|
5
|
+
* Only one field per schema can be marked with id().
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const TodoSchema = z.object({
|
|
10
|
+
* id: id(z.string()),
|
|
11
|
+
* value: z.string()
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function id(schema) {
|
|
16
|
+
schema.def.__mcpWebKey = true;
|
|
17
|
+
return schema;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Marks a field as system-generated.
|
|
21
|
+
* Field is excluded from input schemas (add/set).
|
|
22
|
+
* MUST have a default() — error thrown otherwise.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const TodoSchema = z.object({
|
|
27
|
+
* id: id(system(z.string().default(() => crypto.randomUUID()))),
|
|
28
|
+
* created_at: system(z.number().default(() => Date.now()))
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function system(schema) {
|
|
33
|
+
schema.def.__mcpWebSystem = true;
|
|
34
|
+
return schema;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Checks if a field is marked with id().
|
|
38
|
+
*/
|
|
39
|
+
export function isKeyField(field) {
|
|
40
|
+
return field.def.__mcpWebKey === true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Checks if a field is marked with system().
|
|
44
|
+
*/
|
|
45
|
+
export function isSystemField(field) {
|
|
46
|
+
return field.def.__mcpWebSystem === true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Checks if a schema has a default value.
|
|
50
|
+
*/
|
|
51
|
+
export function hasDefault(schema) {
|
|
52
|
+
// Check for ZodDefault wrapper
|
|
53
|
+
if (schema instanceof z.ZodDefault) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// Check _def for default value
|
|
57
|
+
const def = schema.def;
|
|
58
|
+
return def.defaultValue !== undefined || typeof def.defaultValue === 'function';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Unwraps a schema from ZodDefault, ZodOptional, ZodNullable wrappers.
|
|
62
|
+
* Returns the innermost schema.
|
|
63
|
+
*/
|
|
64
|
+
export function unwrapSchema(schema) {
|
|
65
|
+
let current = schema;
|
|
66
|
+
while (current instanceof z.ZodDefault ||
|
|
67
|
+
current instanceof z.ZodOptional ||
|
|
68
|
+
current instanceof z.ZodNullable) {
|
|
69
|
+
const def = current.def;
|
|
70
|
+
current = def.innerType || current;
|
|
71
|
+
}
|
|
72
|
+
return current;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Unwraps ZodDefault to get the inner schema.
|
|
76
|
+
*/
|
|
77
|
+
export function unwrapDefault(schema) {
|
|
78
|
+
if (schema instanceof z.ZodDefault) {
|
|
79
|
+
const def = schema.def;
|
|
80
|
+
return def.innerType;
|
|
81
|
+
}
|
|
82
|
+
return schema;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Derives an input schema for add operations.
|
|
86
|
+
* - system() fields are excluded
|
|
87
|
+
* - default() fields become optional
|
|
88
|
+
* - other fields remain required
|
|
89
|
+
*/
|
|
90
|
+
export function deriveAddInputSchema(schema) {
|
|
91
|
+
const inputShape = {};
|
|
92
|
+
for (const [key, field] of Object.entries(schema.shape)) {
|
|
93
|
+
const zodField = field;
|
|
94
|
+
// Skip system fields entirely
|
|
95
|
+
if (isSystemField(zodField))
|
|
96
|
+
continue;
|
|
97
|
+
// Fields with default() become optional in add
|
|
98
|
+
if (hasDefault(zodField)) {
|
|
99
|
+
inputShape[key] = unwrapDefault(zodField).optional();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
inputShape[key] = zodField;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return z.object(inputShape);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Derives an input schema for set operations (partial updates).
|
|
109
|
+
* - system() fields are excluded
|
|
110
|
+
* - all other fields become optional
|
|
111
|
+
*/
|
|
112
|
+
export function deriveSetInputSchema(schema) {
|
|
113
|
+
const inputShape = {};
|
|
114
|
+
for (const [key, field] of Object.entries(schema.shape)) {
|
|
115
|
+
const zodField = field;
|
|
116
|
+
// Skip system fields entirely
|
|
117
|
+
if (isSystemField(zodField))
|
|
118
|
+
continue;
|
|
119
|
+
// All fields optional for partial updates
|
|
120
|
+
inputShape[key] = unwrapDefault(zodField).optional();
|
|
121
|
+
}
|
|
122
|
+
return z.object(inputShape);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validates that all system() fields have default values.
|
|
126
|
+
* Throws an error if a system field lacks a default.
|
|
127
|
+
*/
|
|
128
|
+
export function validateSystemFields(schema) {
|
|
129
|
+
for (const [key, field] of Object.entries(schema.shape)) {
|
|
130
|
+
const zodField = field;
|
|
131
|
+
if (isSystemField(zodField) && !hasDefault(zodField)) {
|
|
132
|
+
throw new Error(`Error: Field '${key}' is marked as system() but has no default value.\n\n` +
|
|
133
|
+
`System fields are excluded from input schemas, so a default is required.\n` +
|
|
134
|
+
`The handler uses this default to fill in the value when creating new items.\n\n` +
|
|
135
|
+
`Fix: Add a default value or generator:\n` +
|
|
136
|
+
` ${key}: system(z.number().default(() => Date.now()))`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-helpers.test.d.ts","sourceRoot":"","sources":["../../src/expanded-schema-tools/schema-helpers.test.ts"],"names":[],"mappings":""}
|