@mcp-web/decompose-zod-schema 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 +358 -0
- package/dist/apply.d.ts +2 -0
- package/dist/apply.d.ts.map +1 -0
- package/dist/apply.js +65 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +164 -0
- package/dist/decompose.d.ts +41 -0
- package/dist/decompose.d.ts.map +1 -0
- package/dist/decompose.js +383 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/plan-builder.d.ts +81 -0
- package/dist/plan-builder.d.ts.map +1 -0
- package/dist/plan-builder.js +109 -0
- package/dist/schemas.d.ts +14 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +22 -0
- package/dist/split-suggestions.d.ts +42 -0
- package/dist/split-suggestions.d.ts.map +1 -0
- package/dist/split-suggestions.js +179 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +161 -0
- package/dist/validate.d.ts +9 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +134 -0
- package/package.json +46 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { decomposeSchema } from './decompose.js';
|
|
6
|
+
import { estimateTokensByJsonSchema } from './utils.js';
|
|
7
|
+
function log(message, verbose) {
|
|
8
|
+
if (verbose) {
|
|
9
|
+
console.log(`[INFO] ${message}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function extractSchemaFromFile(filePath, schemaName) {
|
|
13
|
+
try {
|
|
14
|
+
// Read the TypeScript file
|
|
15
|
+
await readFile(filePath, 'utf-8');
|
|
16
|
+
// This is a simplified approach - in a real implementation, you might want to:
|
|
17
|
+
// 1. Use a TypeScript parser/AST
|
|
18
|
+
// 2. Dynamically import and evaluate the module
|
|
19
|
+
// 3. Support more complex schema definitions
|
|
20
|
+
// For now, we'll create a simple example schema that users can modify
|
|
21
|
+
const exampleSchema = z.object({
|
|
22
|
+
user: z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
email: z.string().email(),
|
|
26
|
+
age: z.number(),
|
|
27
|
+
profile: z.object({
|
|
28
|
+
bio: z.string(),
|
|
29
|
+
avatar: z.string().url().optional(),
|
|
30
|
+
preferences: z.object({
|
|
31
|
+
theme: z.enum(['light', 'dark', 'auto']),
|
|
32
|
+
language: z.enum([
|
|
33
|
+
'en',
|
|
34
|
+
'es',
|
|
35
|
+
'fr',
|
|
36
|
+
'de',
|
|
37
|
+
'it',
|
|
38
|
+
'pt',
|
|
39
|
+
'ru',
|
|
40
|
+
'ja',
|
|
41
|
+
'ko',
|
|
42
|
+
'zh',
|
|
43
|
+
]),
|
|
44
|
+
notifications: z.boolean(),
|
|
45
|
+
newsletter: z.boolean(),
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
settings: z.object({
|
|
50
|
+
privacy: z.enum(['public', 'private', 'friends']),
|
|
51
|
+
twoFactor: z.boolean(),
|
|
52
|
+
apiKeys: z.array(z.string()),
|
|
53
|
+
}),
|
|
54
|
+
metadata: z.object({
|
|
55
|
+
createdAt: z.date(),
|
|
56
|
+
updatedAt: z.date(),
|
|
57
|
+
version: z.number(),
|
|
58
|
+
tags: z.array(z.string()),
|
|
59
|
+
categories: z.enum(Array.from({ length: 150 }, (_, i) => `category-${i}`)),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
// Log warning about simplified extraction
|
|
63
|
+
console.warn(`Warning: Using example schema. In a real implementation, this would parse '${schemaName}' from '${filePath}'.`);
|
|
64
|
+
console.warn('To use your own schema, modify the CLI to properly import and extract your schema definition.');
|
|
65
|
+
return exampleSchema;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`Failed to extract schema from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function main() {
|
|
72
|
+
const program = new Command();
|
|
73
|
+
program
|
|
74
|
+
.name('decompose-zod-schema')
|
|
75
|
+
.description('Utility for decomposing large Zod schemas into smaller, manageable sub-schemas')
|
|
76
|
+
.version('0.1.0')
|
|
77
|
+
.requiredOption('-i, --input <file>', 'Input TypeScript file containing schema definition')
|
|
78
|
+
.option('-o, --output <file>', 'Output JSON file for decomposed schemas')
|
|
79
|
+
.requiredOption('-s, --schema <name>', 'Name of the schema variable to decompose')
|
|
80
|
+
.option('-t, --tokens <number>', 'Maximum tokens per decomposed schema', '2000')
|
|
81
|
+
.option('-e, --enum-size <number>', 'Maximum enum size before splitting', '200')
|
|
82
|
+
.option('-v, --verbose', 'Verbose output', false)
|
|
83
|
+
.addHelpText('after', `
|
|
84
|
+
Examples:
|
|
85
|
+
$ decompose-zod-schema -i schema.ts -s userSchema -o decomposed.json
|
|
86
|
+
$ decompose-zod-schema -i schema.ts -s configSchema -t 1000 -e 100 -v`);
|
|
87
|
+
program.parse();
|
|
88
|
+
const options = program.opts();
|
|
89
|
+
// Convert string options to numbers
|
|
90
|
+
options.tokens = parseInt(options.tokens, 10);
|
|
91
|
+
options.enumSize = parseInt(options.enumSize, 10);
|
|
92
|
+
try {
|
|
93
|
+
log(`Extracting schema '${options.schema}' from '${options.input}'`, options.verbose);
|
|
94
|
+
const schema = await extractSchemaFromFile(options.input, options.schema);
|
|
95
|
+
log('Estimating token count for original schema', options.verbose);
|
|
96
|
+
const originalTokens = estimateTokensByJsonSchema(schema);
|
|
97
|
+
log(`Original schema estimated tokens: ${originalTokens}`, options.verbose);
|
|
98
|
+
if (originalTokens <= options.tokens) {
|
|
99
|
+
log('Schema is already within token limits, no decomposition needed', options.verbose);
|
|
100
|
+
const result = {
|
|
101
|
+
originalTokens,
|
|
102
|
+
maxTokensPerSchema: options.tokens,
|
|
103
|
+
needsDecomposition: false,
|
|
104
|
+
decomposedSchemas: [
|
|
105
|
+
{
|
|
106
|
+
name: 'complete',
|
|
107
|
+
schema: JSON.parse(JSON.stringify(schema._def)),
|
|
108
|
+
targetPaths: ['*'],
|
|
109
|
+
estimatedTokens: originalTokens,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
if (options.output) {
|
|
114
|
+
await writeFile(options.output, JSON.stringify(result, null, 2), 'utf-8');
|
|
115
|
+
log(`Results written to ${options.output}`, options.verbose);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(JSON.stringify(result, null, 2));
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
log('Decomposing schema', options.verbose);
|
|
123
|
+
const decomposed = decomposeSchema(schema, {
|
|
124
|
+
maxTokensPerSchema: options.tokens,
|
|
125
|
+
maxOptionsPerEnum: options.enumSize,
|
|
126
|
+
});
|
|
127
|
+
log(`Created ${decomposed.length} decomposed schemas`, options.verbose);
|
|
128
|
+
const result = {
|
|
129
|
+
originalTokens,
|
|
130
|
+
maxTokensPerSchema: options.tokens,
|
|
131
|
+
maxOptionsPerEnum: options.enumSize,
|
|
132
|
+
needsDecomposition: true,
|
|
133
|
+
decomposedSchemas: decomposed.map((item) => ({
|
|
134
|
+
name: item.name,
|
|
135
|
+
schema: JSON.parse(JSON.stringify(item.schema._def)),
|
|
136
|
+
targetPaths: item.targetPaths,
|
|
137
|
+
estimatedTokens: estimateTokensByJsonSchema(item.schema),
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
// Summary
|
|
141
|
+
log('\\nDecomposition Summary:', options.verbose);
|
|
142
|
+
log(` Original tokens: ${originalTokens}`, options.verbose);
|
|
143
|
+
log(` Decomposed into: ${decomposed.length} schemas`, options.verbose);
|
|
144
|
+
decomposed.forEach((item, index) => {
|
|
145
|
+
const tokens = estimateTokensByJsonSchema(item.schema);
|
|
146
|
+
log(` Schema ${index + 1} (${item.name}): ${tokens} tokens, ${item.targetPaths.length} paths`, options.verbose);
|
|
147
|
+
});
|
|
148
|
+
if (options.output) {
|
|
149
|
+
await writeFile(options.output, JSON.stringify(result, null, 2), 'utf-8');
|
|
150
|
+
log(`Results written to ${options.output}`, options.verbose);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(JSON.stringify(result, null, 2));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Run the CLI if this file is being executed directly
|
|
162
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
163
|
+
main();
|
|
164
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ZodObject, type ZodType } from 'zod';
|
|
2
|
+
import type { DecomposedSchema, DecompositionOptions, SplitPlan } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Decomposes a Zod schema into smaller schemas based on a split plan or options.
|
|
5
|
+
*
|
|
6
|
+
* This function supports two modes:
|
|
7
|
+
* 1. **Manual decomposition**: Pass a SplitPlan array to control exactly how the schema is split
|
|
8
|
+
* 2. **Automatic decomposition**: Pass options to automatically suggest splits based on size
|
|
9
|
+
*
|
|
10
|
+
* @param schema - The Zod object schema to decompose
|
|
11
|
+
* @param planOrOptions - Either a SplitPlan for manual decomposition or DecompositionOptions for automatic
|
|
12
|
+
* @returns Array of decomposed schemas with their target paths
|
|
13
|
+
*
|
|
14
|
+
* @example Manual decomposition with SplitPlan
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const decomposed = decomposeSchema(GameStateSchema, [
|
|
17
|
+
* 'board',
|
|
18
|
+
* ['currentPlayer', 'turn'],
|
|
19
|
+
* 'settings',
|
|
20
|
+
* ]);
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example Automatic decomposition with options
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const decomposed = decomposeSchema(LargeSchema, {
|
|
26
|
+
* maxTokensPerSchema: 2000,
|
|
27
|
+
* maxOptionsPerEnum: 200,
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function decomposeSchema(schema: ZodObject<Record<string, ZodType>>, planOrOptions: SplitPlan | DecompositionOptions): DecomposedSchema[];
|
|
32
|
+
/**
|
|
33
|
+
* Manually decompose a schema using a predefined split plan.
|
|
34
|
+
* This function takes a schema and a split plan that defines exactly how to split it.
|
|
35
|
+
*
|
|
36
|
+
* @param schema - The Zod schema to decompose
|
|
37
|
+
* @param plan - Array of path specifications defining how to split the schema
|
|
38
|
+
* @returns Array of decomposed schemas with their target paths
|
|
39
|
+
*/
|
|
40
|
+
export declare function decomposeSchemaWithPlan(schema: ZodObject<Record<string, ZodType>>, plan: SplitPlan): DecomposedSchema[];
|
|
41
|
+
//# sourceMappingURL=decompose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decompose.d.ts","sourceRoot":"","sources":["../src/decompose.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAA6B,KAAK,OAAO,EAAK,MAAM,KAAK,CAAC;AAE/F,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAoB,SAAS,EAAE,MAAM,YAAY,CAAC;AAStG;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC1C,aAAa,EAAE,SAAS,GAAG,oBAAoB,GAC9C,gBAAgB,EAAE,CAepB;AAGD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC1C,IAAI,EAAE,SAAS,GACd,gBAAgB,EAAE,CAuRpB"}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { ZodArray, ZodEnum, ZodObject, ZodRecord, z } from 'zod';
|
|
2
|
+
import { suggestDecompositionPlan } from './split-suggestions.js';
|
|
3
|
+
import { evenChunk, extractSchemaForPaths, getSchemaAtPath, parseEnumSplit, } from './utils.js';
|
|
4
|
+
import { validatePlan } from './validate.js';
|
|
5
|
+
/**
|
|
6
|
+
* Decomposes a Zod schema into smaller schemas based on a split plan or options.
|
|
7
|
+
*
|
|
8
|
+
* This function supports two modes:
|
|
9
|
+
* 1. **Manual decomposition**: Pass a SplitPlan array to control exactly how the schema is split
|
|
10
|
+
* 2. **Automatic decomposition**: Pass options to automatically suggest splits based on size
|
|
11
|
+
*
|
|
12
|
+
* @param schema - The Zod object schema to decompose
|
|
13
|
+
* @param planOrOptions - Either a SplitPlan for manual decomposition or DecompositionOptions for automatic
|
|
14
|
+
* @returns Array of decomposed schemas with their target paths
|
|
15
|
+
*
|
|
16
|
+
* @example Manual decomposition with SplitPlan
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const decomposed = decomposeSchema(GameStateSchema, [
|
|
19
|
+
* 'board',
|
|
20
|
+
* ['currentPlayer', 'turn'],
|
|
21
|
+
* 'settings',
|
|
22
|
+
* ]);
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example Automatic decomposition with options
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const decomposed = decomposeSchema(LargeSchema, {
|
|
28
|
+
* maxTokensPerSchema: 2000,
|
|
29
|
+
* maxOptionsPerEnum: 200,
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function decomposeSchema(schema, planOrOptions) {
|
|
34
|
+
// Check if it's a split plan (array) or options (object with specific properties)
|
|
35
|
+
if (Array.isArray(planOrOptions)) {
|
|
36
|
+
// Manual decomposition with split plan
|
|
37
|
+
return decomposeSchemaWithPlan(schema, planOrOptions);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Automatic decomposition with size-based suggestions
|
|
41
|
+
// Provide defaults for missing options
|
|
42
|
+
const sizeBasedOptions = {
|
|
43
|
+
maxTokensPerSchema: planOrOptions.maxTokensPerSchema ?? 2000,
|
|
44
|
+
maxOptionsPerEnum: planOrOptions.maxOptionsPerEnum ?? 200,
|
|
45
|
+
};
|
|
46
|
+
const suggestedPlan = suggestDecompositionPlan(schema, sizeBasedOptions);
|
|
47
|
+
return decomposeSchemaWithPlan(schema, suggestedPlan);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Manually decompose a schema using a predefined split plan.
|
|
52
|
+
* This function takes a schema and a split plan that defines exactly how to split it.
|
|
53
|
+
*
|
|
54
|
+
* @param schema - The Zod schema to decompose
|
|
55
|
+
* @param plan - Array of path specifications defining how to split the schema
|
|
56
|
+
* @returns Array of decomposed schemas with their target paths
|
|
57
|
+
*/
|
|
58
|
+
export function decomposeSchemaWithPlan(schema, plan) {
|
|
59
|
+
// Validate the split plan first
|
|
60
|
+
const validationErrors = validatePlan(plan, schema);
|
|
61
|
+
if (validationErrors.length > 0) {
|
|
62
|
+
throw new Error(`Invalid split plan: ${validationErrors.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
const result = [];
|
|
65
|
+
const processedPaths = new Set();
|
|
66
|
+
const processedItems = new Set();
|
|
67
|
+
const parsedSplits = plan.map(split => ({
|
|
68
|
+
original: split,
|
|
69
|
+
...parseArrayNotation(split)
|
|
70
|
+
}));
|
|
71
|
+
// Group splits by type
|
|
72
|
+
const arrayGroups = new Map();
|
|
73
|
+
const recordGroups = new Map();
|
|
74
|
+
const regularSplits = [];
|
|
75
|
+
parsedSplits.forEach(({ original, path, isArrayElement }) => {
|
|
76
|
+
const parsed = parseEnumSplit(original);
|
|
77
|
+
if (parsed.isArraySplit) {
|
|
78
|
+
// Handle nested array splits like 'views[].tracks.top[]'
|
|
79
|
+
if (original.includes('[].') && original.match(/^(.+)\[\]\.(.+)\[\]$/)) {
|
|
80
|
+
// This is a nested array split - handle it specially
|
|
81
|
+
if (!arrayGroups.has('nested'))
|
|
82
|
+
arrayGroups.set('nested', []);
|
|
83
|
+
arrayGroups.get('nested')?.push(original);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Regular array split like 'views[]'
|
|
87
|
+
if (!arrayGroups.has(parsed.path))
|
|
88
|
+
arrayGroups.set(parsed.path, []);
|
|
89
|
+
arrayGroups.get(parsed.path)?.push(original);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (parsed.isRecordSplit) {
|
|
93
|
+
if (!recordGroups.has(parsed.path))
|
|
94
|
+
recordGroups.set(parsed.path, []);
|
|
95
|
+
recordGroups.get(parsed.path)?.push(original);
|
|
96
|
+
}
|
|
97
|
+
else if (isArrayElement) {
|
|
98
|
+
if (!arrayGroups.has(path))
|
|
99
|
+
arrayGroups.set(path, []);
|
|
100
|
+
arrayGroups.get(path)?.push(original);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
regularSplits.push(original);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// Handle array splits
|
|
107
|
+
arrayGroups.forEach((splits, arrayPath) => {
|
|
108
|
+
if (arrayPath === 'nested') {
|
|
109
|
+
// Handle nested array splits like 'views[].tracks.top[]'
|
|
110
|
+
splits.forEach(splitPath => {
|
|
111
|
+
const nestedMatch = splitPath.match(/^(.+)\[\]\.(.+)\[\]$/);
|
|
112
|
+
if (nestedMatch) {
|
|
113
|
+
const [, basePath, subPath] = nestedMatch;
|
|
114
|
+
// Get the base array schema (e.g., 'views')
|
|
115
|
+
const baseArraySchema = getSchemaAtPath(schema, basePath);
|
|
116
|
+
if (!(baseArraySchema instanceof ZodArray)) {
|
|
117
|
+
console.warn(`Expected array at path: ${basePath}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Get the element schema and navigate to the sub-array
|
|
121
|
+
const elementSchema = baseArraySchema.element;
|
|
122
|
+
if (!(elementSchema instanceof ZodObject)) {
|
|
123
|
+
console.warn(`Expected object element in array at path: ${basePath}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Get the sub-array schema (e.g., 'tracks.top')
|
|
127
|
+
let subArraySchema = getSchemaAtPath(elementSchema, subPath);
|
|
128
|
+
// Unwrap optional schemas
|
|
129
|
+
if (subArraySchema && typeof subArraySchema === 'object' && 'unwrap' in subArraySchema) {
|
|
130
|
+
subArraySchema = subArraySchema.unwrap();
|
|
131
|
+
}
|
|
132
|
+
if (!(subArraySchema instanceof ZodArray)) {
|
|
133
|
+
console.warn(`Expected array at nested path: ${basePath}[].${subPath}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Create the schema for the nested array element
|
|
137
|
+
const nestedElementSchema = createArrayElementSchema(subArraySchema);
|
|
138
|
+
result.push({
|
|
139
|
+
name: `${basePath.replace(/\./g, '-')}-${subPath.replace(/\./g, '-')}-item`,
|
|
140
|
+
schema: nestedElementSchema,
|
|
141
|
+
targetPaths: [splitPath]
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Handle regular array splits
|
|
148
|
+
const arraySchema = getSchemaAtPath(schema, arrayPath);
|
|
149
|
+
if (!(arraySchema instanceof ZodArray)) {
|
|
150
|
+
console.warn(`Expected array at path: ${arrayPath}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
splits.forEach(splitPath => {
|
|
154
|
+
const parsed = parseEnumSplit(splitPath);
|
|
155
|
+
if (parsed.isArraySplit) {
|
|
156
|
+
const elementSchema = createArrayElementSchema(arraySchema);
|
|
157
|
+
result.push({
|
|
158
|
+
name: `${arrayPath.replace(/\./g, '-')}-item`,
|
|
159
|
+
schema: elementSchema,
|
|
160
|
+
targetPaths: [splitPath]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Handle legacy array notation with exclusions
|
|
165
|
+
const { excludedSubArrays } = parseArrayNotation(splitPath);
|
|
166
|
+
const elementSchema = createArrayElementSchema(arraySchema, excludedSubArrays);
|
|
167
|
+
result.push({
|
|
168
|
+
name: `${arrayPath.replace(/\./g, '-')}-item`,
|
|
169
|
+
schema: elementSchema,
|
|
170
|
+
targetPaths: [splitPath]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// Handle record splits
|
|
177
|
+
recordGroups.forEach((splits, recordPath) => {
|
|
178
|
+
const recordSchema = getSchemaAtPath(schema, recordPath);
|
|
179
|
+
if (!(recordSchema instanceof ZodRecord)) {
|
|
180
|
+
console.warn(`Expected record at path: ${recordPath}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
splits.forEach(splitPath => {
|
|
184
|
+
const elementSchema = createRecordElementSchema(recordSchema);
|
|
185
|
+
result.push({
|
|
186
|
+
name: `${recordPath.replace(/\./g, '-')}-entry`,
|
|
187
|
+
schema: elementSchema,
|
|
188
|
+
targetPaths: [splitPath]
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
for (const item of plan) {
|
|
193
|
+
const parsed = parseEnumSplit(item);
|
|
194
|
+
// Skip if we've already processed this exact item
|
|
195
|
+
if (processedItems.has(item)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Skip array and record splits as they're handled above
|
|
199
|
+
if (parsed.isArraySplit || parsed.isRecordSplit) {
|
|
200
|
+
processedItems.add(item);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const schemaAtPath = getSchemaAtPath(schema, parsed.path);
|
|
204
|
+
if (!schemaAtPath) {
|
|
205
|
+
continue; // Skip invalid paths (already validated above)
|
|
206
|
+
}
|
|
207
|
+
// Handle enum splitting
|
|
208
|
+
if (schemaAtPath instanceof ZodEnum && parsed.chunkSize) {
|
|
209
|
+
const enumOptions = schemaAtPath.options;
|
|
210
|
+
const chunks = evenChunk(enumOptions, parsed.chunkSize);
|
|
211
|
+
chunks.forEach((chunk, index) => {
|
|
212
|
+
const chunkName = chunks.length > 1 ? `${parsed.path}-${index + 1}` : parsed.path;
|
|
213
|
+
const startIndex = index * Math.ceil(enumOptions.length / chunks.length);
|
|
214
|
+
const endIndex = Math.min(startIndex + chunk.length, enumOptions.length);
|
|
215
|
+
const slicePath = `${parsed.path}[${startIndex}:${endIndex}]`;
|
|
216
|
+
// Create schema with just this enum chunk
|
|
217
|
+
const chunkSchema = z.object({
|
|
218
|
+
[parsed.path.split('.').pop() || parsed.path]: z.enum(chunk),
|
|
219
|
+
});
|
|
220
|
+
result.push({
|
|
221
|
+
name: chunkName,
|
|
222
|
+
schema: chunkSchema,
|
|
223
|
+
targetPaths: [slicePath],
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
processedItems.add(item);
|
|
227
|
+
}
|
|
228
|
+
// Handle slice notation (including shorthand)
|
|
229
|
+
else if (schemaAtPath instanceof ZodEnum && parsed.start !== undefined) {
|
|
230
|
+
const enumOptions = schemaAtPath.options;
|
|
231
|
+
const end = parsed.end !== undefined ? parsed.end : enumOptions.length;
|
|
232
|
+
const slicedOptions = enumOptions.slice(parsed.start, end);
|
|
233
|
+
if (slicedOptions.length > 0) {
|
|
234
|
+
const sliceSchema = z.object({
|
|
235
|
+
[parsed.path.split('.').pop() || parsed.path]: z.enum(slicedOptions),
|
|
236
|
+
});
|
|
237
|
+
// Generate appropriate name for the slice
|
|
238
|
+
const sliceName = parsed.end !== undefined
|
|
239
|
+
? `${parsed.path}-${parsed.start}-${parsed.end}`
|
|
240
|
+
: `${parsed.path}-${parsed.start}-end`;
|
|
241
|
+
result.push({
|
|
242
|
+
name: sliceName,
|
|
243
|
+
schema: sliceSchema,
|
|
244
|
+
targetPaths: [item],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
processedItems.add(item);
|
|
248
|
+
}
|
|
249
|
+
// Handle regular path extraction
|
|
250
|
+
else {
|
|
251
|
+
// Group paths that share the same root for better organization
|
|
252
|
+
const pathsToInclude = plan
|
|
253
|
+
.filter((planItem) => {
|
|
254
|
+
const planParsed = parseEnumSplit(planItem);
|
|
255
|
+
return (planParsed.path === parsed.path ||
|
|
256
|
+
planParsed.path.startsWith(`${parsed.path}.`));
|
|
257
|
+
})
|
|
258
|
+
.map((planItem) => parseEnumSplit(planItem).path)
|
|
259
|
+
.filter((path) => !processedPaths.has(path));
|
|
260
|
+
if (pathsToInclude.length > 0) {
|
|
261
|
+
try {
|
|
262
|
+
const extractedSchema = extractSchemaForPaths(schema, pathsToInclude);
|
|
263
|
+
result.push({
|
|
264
|
+
name: parsed.path.replace(/\./g, '-'),
|
|
265
|
+
schema: extractedSchema,
|
|
266
|
+
targetPaths: pathsToInclude,
|
|
267
|
+
});
|
|
268
|
+
// Mark all included paths as processed
|
|
269
|
+
pathsToInclude.forEach((path) => processedPaths.add(path));
|
|
270
|
+
}
|
|
271
|
+
catch (_error) {
|
|
272
|
+
// If extraction fails, create a simple schema with just this path
|
|
273
|
+
const shape = {};
|
|
274
|
+
const parts = parsed.path.split('.');
|
|
275
|
+
const leafKey = parts[parts.length - 1];
|
|
276
|
+
if (parts.length === 1) {
|
|
277
|
+
shape[leafKey] = schemaAtPath;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// For nested paths, create the nested structure
|
|
281
|
+
let current = shape;
|
|
282
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
283
|
+
const part = parts[i];
|
|
284
|
+
current[part] = z.object({});
|
|
285
|
+
current = current[part]
|
|
286
|
+
.shape;
|
|
287
|
+
}
|
|
288
|
+
current[leafKey] = schemaAtPath;
|
|
289
|
+
}
|
|
290
|
+
result.push({
|
|
291
|
+
name: parsed.path.replace(/\./g, '-'),
|
|
292
|
+
schema: z.object(shape),
|
|
293
|
+
targetPaths: [parsed.path],
|
|
294
|
+
});
|
|
295
|
+
processedPaths.add(parsed.path);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
const parseArrayNotation = (splitPath) => {
|
|
303
|
+
const arrayMatch = splitPath.match(/^(.+)\[\](.*)$/);
|
|
304
|
+
if (arrayMatch) {
|
|
305
|
+
const [, basePath, subPath] = arrayMatch;
|
|
306
|
+
return {
|
|
307
|
+
path: basePath,
|
|
308
|
+
isArrayElement: true,
|
|
309
|
+
excludedSubArrays: subPath ? [subPath.slice(1)] : [] // Remove leading dot
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return { path: splitPath, isArrayElement: false, excludedSubArrays: [] };
|
|
313
|
+
};
|
|
314
|
+
const createArrayElementSchema = (arraySchema, excludedPaths = []) => {
|
|
315
|
+
let elementSchema = arraySchema.element;
|
|
316
|
+
// Unwrap ZodLazy schemas
|
|
317
|
+
if (elementSchema && typeof elementSchema === 'object' && '_def' in elementSchema &&
|
|
318
|
+
elementSchema._def.type === 'lazy') {
|
|
319
|
+
const lazySchema = elementSchema;
|
|
320
|
+
elementSchema = lazySchema._def.getter();
|
|
321
|
+
}
|
|
322
|
+
// For array splits, we need either ZodObject or ZodUnion (for different track types)
|
|
323
|
+
if (!(elementSchema instanceof ZodObject) && elementSchema?.constructor.name !== 'ZodUnion') {
|
|
324
|
+
throw new Error(`Array element must be an object or union for array splitting, got ${elementSchema?.constructor.name}`);
|
|
325
|
+
}
|
|
326
|
+
// Create element schema with exclusions
|
|
327
|
+
let finalElementSchema = elementSchema;
|
|
328
|
+
if (elementSchema instanceof ZodObject) {
|
|
329
|
+
const elementWithExclusions = createSchemaWithExclusions(elementSchema, '', excludedPaths);
|
|
330
|
+
finalElementSchema = elementWithExclusions || elementSchema;
|
|
331
|
+
}
|
|
332
|
+
// For union schemas, we keep them as-is since they represent valid track types
|
|
333
|
+
// Wrap with index
|
|
334
|
+
return z.object({
|
|
335
|
+
index: z.number().min(0),
|
|
336
|
+
value: finalElementSchema
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
const createRecordElementSchema = (recordSchema) => {
|
|
340
|
+
// Access internal properties of ZodRecord
|
|
341
|
+
const keySchema = recordSchema._def.keyType || z.string();
|
|
342
|
+
const valueSchema = recordSchema._def.valueType;
|
|
343
|
+
// Transform z.record(K, V) to z.object({ key: K, value: V })
|
|
344
|
+
return z.object({
|
|
345
|
+
key: keySchema,
|
|
346
|
+
value: valueSchema
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
const createSchemaWithExclusions = (rootSchema, basePath, exclusions) => {
|
|
350
|
+
const baseSchema = getSchemaAtPath(rootSchema, basePath);
|
|
351
|
+
if (!(baseSchema instanceof ZodObject)) {
|
|
352
|
+
return baseSchema || null;
|
|
353
|
+
}
|
|
354
|
+
// Filter exclusions to only those that are direct children of basePath
|
|
355
|
+
const relevantExclusions = exclusions
|
|
356
|
+
.filter(exc => {
|
|
357
|
+
if (!basePath) {
|
|
358
|
+
// For root level, exclude direct children
|
|
359
|
+
return !exc.includes('.');
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// For nested paths, exclude direct children of this path
|
|
363
|
+
return exc.startsWith(`${basePath}.`) &&
|
|
364
|
+
!exc.slice(basePath.length + 1).includes('.');
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
.map(exc => basePath ? exc.slice(basePath.length + 1) : exc);
|
|
368
|
+
if (relevantExclusions.length === 0) {
|
|
369
|
+
return baseSchema;
|
|
370
|
+
}
|
|
371
|
+
// Create new shape excluding the specified properties
|
|
372
|
+
const newShape = {};
|
|
373
|
+
Object.entries(baseSchema.shape).forEach(([key, schema]) => {
|
|
374
|
+
if (!relevantExclusions.includes(key)) {
|
|
375
|
+
newShape[key] = schema;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
// Return null if all properties were excluded
|
|
379
|
+
if (Object.keys(newShape).length === 0) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return z.object(newShape);
|
|
383
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { applyPartialUpdate } from './apply.js';
|
|
2
|
+
export { decomposeSchema } from './decompose.js';
|
|
3
|
+
export { PlanBuilder } from './plan-builder.js';
|
|
4
|
+
export { defaultStrategyRegistry, SemanticSuggestionStrategy, SizeBasedSuggestionStrategy, SuggestionStrategyRegistry, suggestDecompositionPlan, suggestWithStrategy, } from './split-suggestions.js';
|
|
5
|
+
export type { DecomposedSchema, DecompositionOptions, SizeBasedOptions, Split, SplitPlan, SuggestionStrategy, } from './types.js';
|
|
6
|
+
export { conditionalEnumSplit } from './utils.js';
|
|
7
|
+
export { validatePlan } from './validate.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,EAC1B,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,EACL,SAAS,EACT,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { applyPartialUpdate } from './apply.js';
|
|
2
|
+
export { decomposeSchema } from './decompose.js';
|
|
3
|
+
export { PlanBuilder } from './plan-builder.js';
|
|
4
|
+
export { defaultStrategyRegistry, SemanticSuggestionStrategy, SizeBasedSuggestionStrategy, SuggestionStrategyRegistry, suggestDecompositionPlan, suggestWithStrategy, } from './split-suggestions.js';
|
|
5
|
+
export { conditionalEnumSplit } from './utils.js';
|
|
6
|
+
export { validatePlan } from './validate.js';
|