Add support for .default() on nested objects and arrays in SecureZodSchemaParser (#5269)
feat(secureZodParser): add support for .default() on nested objects and arrays
This commit is contained in:
parent
0065e8f1a0
commit
6890ced939
|
|
@ -16,7 +16,8 @@ export class SecureZodSchemaParser {
|
||||||
'optional',
|
'optional',
|
||||||
'max',
|
'max',
|
||||||
'min',
|
'min',
|
||||||
'describe'
|
'describe',
|
||||||
|
'default'
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -137,7 +138,25 @@ export class SecureZodSchemaParser {
|
||||||
private static parseZodType(typeStr: string): any {
|
private static parseZodType(typeStr: string): any {
|
||||||
// Check if this is a nested object (not in an array)
|
// Check if this is a nested object (not in an array)
|
||||||
if (typeStr.startsWith('z.object(') && !typeStr.startsWith('z.array(')) {
|
if (typeStr.startsWith('z.object(') && !typeStr.startsWith('z.array(')) {
|
||||||
// Extract object content
|
// Check if there are modifiers after the object
|
||||||
|
const objectWithModifiers = this.extractObjectWithModifiers(typeStr)
|
||||||
|
if (objectWithModifiers.hasModifiers) {
|
||||||
|
const objectMatch = objectWithModifiers.objectPart.match(/z\.object\(\s*\{([\s\S]*)\}\s*\)/)
|
||||||
|
if (!objectMatch) {
|
||||||
|
throw new Error('Invalid object syntax')
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectContent = objectMatch[1]
|
||||||
|
const objectProperties = this.parseObjectProperties(objectContent)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isNestedObject: true,
|
||||||
|
objectSchema: objectProperties,
|
||||||
|
modifiers: objectWithModifiers.modifiers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original code for objects without modifiers
|
||||||
const objectMatch = typeStr.match(/z\.object\(\s*\{([\s\S]*)\}\s*\)/)
|
const objectMatch = typeStr.match(/z\.object\(\s*\{([\s\S]*)\}\s*\)/)
|
||||||
if (!objectMatch) {
|
if (!objectMatch) {
|
||||||
throw new Error('Invalid object syntax')
|
throw new Error('Invalid object syntax')
|
||||||
|
|
@ -154,6 +173,16 @@ export class SecureZodSchemaParser {
|
||||||
|
|
||||||
// Check if this is any kind of array
|
// Check if this is any kind of array
|
||||||
if (typeStr.startsWith('z.array(')) {
|
if (typeStr.startsWith('z.array(')) {
|
||||||
|
// Check if there are modifiers after the array
|
||||||
|
const arrayWithModifiers = this.extractArrayWithModifiers(typeStr)
|
||||||
|
if (arrayWithModifiers.hasModifiers) {
|
||||||
|
const arrayResult = this.parseArray(arrayWithModifiers.arrayPart)
|
||||||
|
// Convert array result to have modifiers
|
||||||
|
return {
|
||||||
|
...arrayResult,
|
||||||
|
modifiers: arrayWithModifiers.modifiers
|
||||||
|
}
|
||||||
|
}
|
||||||
return this.parseArray(typeStr)
|
return this.parseArray(typeStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,6 +358,191 @@ export class SecureZodSchemaParser {
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static extractArrayWithModifiers(typeStr: string): { arrayPart: string; modifiers: any[]; hasModifiers: boolean } {
|
||||||
|
// Find the matching closing parenthesis for z.array(
|
||||||
|
let depth = 0
|
||||||
|
let arrayEndIndex = -1
|
||||||
|
let startIndex = typeStr.indexOf('z.array(') + 7 // Position after "z.array"
|
||||||
|
|
||||||
|
for (let i = startIndex; i < typeStr.length; i++) {
|
||||||
|
if (typeStr[i] === '(') depth++
|
||||||
|
else if (typeStr[i] === ')') {
|
||||||
|
depth--
|
||||||
|
if (depth === 0) {
|
||||||
|
arrayEndIndex = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrayEndIndex === -1) {
|
||||||
|
return { arrayPart: typeStr, modifiers: [], hasModifiers: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayPart = typeStr.substring(0, arrayEndIndex)
|
||||||
|
const remainingPart = typeStr.substring(arrayEndIndex)
|
||||||
|
|
||||||
|
if (!remainingPart.startsWith('.')) {
|
||||||
|
return { arrayPart: typeStr, modifiers: [], hasModifiers: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse modifiers
|
||||||
|
const modifiers: any[] = []
|
||||||
|
const modifierParts = remainingPart.substring(1).split('.')
|
||||||
|
|
||||||
|
for (const part of modifierParts) {
|
||||||
|
const modMatch = part.match(/^(\w+)(\(.*\))?$/)
|
||||||
|
if (!modMatch) {
|
||||||
|
throw new Error(`Invalid modifier: ${part}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modName = modMatch[1]
|
||||||
|
const modArgs = modMatch[2] ? this.parseArguments(modMatch[2]) : []
|
||||||
|
|
||||||
|
if (!this.ALLOWED_TYPES.includes(modName)) {
|
||||||
|
throw new Error(`Unsupported modifier: ${modName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiers.push({ name: modName, args: modArgs })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { arrayPart, modifiers, hasModifiers: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractObjectWithModifiers(typeStr: string): { objectPart: string; modifiers: any[]; hasModifiers: boolean } {
|
||||||
|
// Find the matching closing brace and parenthesis for z.object({...})
|
||||||
|
let braceDepth = 0
|
||||||
|
let parenDepth = 0
|
||||||
|
let objectEndIndex = -1
|
||||||
|
let startIndex = typeStr.indexOf('z.object(') + 8 // Position after "z.object"
|
||||||
|
let foundOpenBrace = false
|
||||||
|
|
||||||
|
for (let i = startIndex; i < typeStr.length; i++) {
|
||||||
|
if (typeStr[i] === '{') {
|
||||||
|
braceDepth++
|
||||||
|
foundOpenBrace = true
|
||||||
|
} else if (typeStr[i] === '}') {
|
||||||
|
braceDepth--
|
||||||
|
} else if (typeStr[i] === '(' && foundOpenBrace) {
|
||||||
|
parenDepth++
|
||||||
|
} else if (typeStr[i] === ')' && foundOpenBrace) {
|
||||||
|
if (braceDepth === 0 && parenDepth === 0) {
|
||||||
|
objectEndIndex = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parenDepth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectEndIndex === -1) {
|
||||||
|
return { objectPart: typeStr, modifiers: [], hasModifiers: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectPart = typeStr.substring(0, objectEndIndex)
|
||||||
|
const remainingPart = typeStr.substring(objectEndIndex)
|
||||||
|
|
||||||
|
if (!remainingPart.startsWith('.')) {
|
||||||
|
return { objectPart: typeStr, modifiers: [], hasModifiers: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse modifiers (need special handling for .default() with object argument)
|
||||||
|
const modifiers: any[] = []
|
||||||
|
let i = 1 // Skip the initial dot
|
||||||
|
|
||||||
|
while (i < remainingPart.length) {
|
||||||
|
// Find modifier name
|
||||||
|
const modNameMatch = remainingPart.substring(i).match(/^(\w+)/)
|
||||||
|
if (!modNameMatch) break
|
||||||
|
|
||||||
|
const modName = modNameMatch[1]
|
||||||
|
i += modName.length
|
||||||
|
|
||||||
|
// Check for arguments
|
||||||
|
let modArgs: any[] = []
|
||||||
|
if (i < remainingPart.length && remainingPart[i] === '(') {
|
||||||
|
// Find matching closing paren, handling nested structures
|
||||||
|
let depth = 0
|
||||||
|
let argStart = i
|
||||||
|
for (let j = i; j < remainingPart.length; j++) {
|
||||||
|
if (remainingPart[j] === '(') depth++
|
||||||
|
else if (remainingPart[j] === ')') {
|
||||||
|
depth--
|
||||||
|
if (depth === 0) {
|
||||||
|
const argsStr = remainingPart.substring(argStart, j + 1)
|
||||||
|
modArgs = this.parseComplexArguments(argsStr)
|
||||||
|
i = j + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.ALLOWED_TYPES.includes(modName)) {
|
||||||
|
throw new Error(`Unsupported modifier: ${modName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiers.push({ name: modName, args: modArgs })
|
||||||
|
|
||||||
|
// Skip dot if present
|
||||||
|
if (i < remainingPart.length && remainingPart[i] === '.') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { objectPart, modifiers, hasModifiers: modifiers.length > 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseComplexArguments(argsStr: string): any[] {
|
||||||
|
// Remove outer parentheses
|
||||||
|
const inner = argsStr.slice(1, -1).trim()
|
||||||
|
if (!inner) return []
|
||||||
|
|
||||||
|
// Check if it's an object literal
|
||||||
|
if (inner.startsWith('{') && inner.endsWith('}')) {
|
||||||
|
// Parse object literal for .default()
|
||||||
|
return [this.parseObjectLiteral(inner)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing parseArguments for simple cases
|
||||||
|
return this.parseArguments(argsStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseObjectLiteral(objStr: string): any {
|
||||||
|
// Simple object literal parser for default values
|
||||||
|
const obj: any = {}
|
||||||
|
const content = objStr.slice(1, -1).trim() // Remove { }
|
||||||
|
|
||||||
|
if (!content) return obj
|
||||||
|
|
||||||
|
// Split by comma at depth 0
|
||||||
|
const props = this.splitProperties(content)
|
||||||
|
|
||||||
|
for (const prop of props) {
|
||||||
|
const colonIndex = prop.indexOf(':')
|
||||||
|
if (colonIndex === -1) continue
|
||||||
|
|
||||||
|
const key = prop.substring(0, colonIndex).trim().replace(/['"]/g, '')
|
||||||
|
const valueStr = prop.substring(colonIndex + 1).trim()
|
||||||
|
|
||||||
|
// Parse the value
|
||||||
|
if (valueStr.startsWith('[') && valueStr.endsWith(']')) {
|
||||||
|
// Array value
|
||||||
|
const arrayContent = valueStr.slice(1, -1)
|
||||||
|
obj[key] = this.parseArrayContent(arrayContent)
|
||||||
|
} else if (valueStr.startsWith('"') && valueStr.endsWith('"')) {
|
||||||
|
// String value
|
||||||
|
obj[key] = valueStr.slice(1, -1)
|
||||||
|
} else if (valueStr.match(/^\d+$/)) {
|
||||||
|
// Number value
|
||||||
|
obj[key] = parseInt(valueStr, 10)
|
||||||
|
} else {
|
||||||
|
obj[key] = valueStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
private static buildZodSchema(parsed: Record<string, any>): z.ZodObject<any> {
|
private static buildZodSchema(parsed: Record<string, any>): z.ZodObject<any> {
|
||||||
const schemaObj: Record<string, z.ZodTypeAny> = {}
|
const schemaObj: Record<string, z.ZodTypeAny> = {}
|
||||||
|
|
||||||
|
|
@ -342,19 +556,40 @@ export class SecureZodSchemaParser {
|
||||||
private static buildZodType(typeInfo: any): z.ZodTypeAny {
|
private static buildZodType(typeInfo: any): z.ZodTypeAny {
|
||||||
// Special case for nested objects
|
// Special case for nested objects
|
||||||
if (typeInfo.isNestedObject) {
|
if (typeInfo.isNestedObject) {
|
||||||
return this.buildZodSchema(typeInfo.objectSchema)
|
let zodType: z.ZodTypeAny = this.buildZodSchema(typeInfo.objectSchema)
|
||||||
|
|
||||||
|
// Apply modifiers if present
|
||||||
|
if (typeInfo.modifiers) {
|
||||||
|
zodType = this.applyModifiers(zodType, typeInfo.modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case for array of objects
|
// Special case for array of objects
|
||||||
if (typeInfo.isArrayOfObjects) {
|
if (typeInfo.isArrayOfObjects) {
|
||||||
const objectSchema = this.buildZodSchema(typeInfo.objectSchema)
|
const objectSchema = this.buildZodSchema(typeInfo.objectSchema)
|
||||||
return z.array(objectSchema)
|
let zodType: z.ZodTypeAny = z.array(objectSchema)
|
||||||
|
|
||||||
|
// Apply modifiers if present
|
||||||
|
if (typeInfo.modifiers) {
|
||||||
|
zodType = this.applyModifiers(zodType, typeInfo.modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case for simple arrays
|
// Special case for simple arrays
|
||||||
if (typeInfo.isSimpleArray) {
|
if (typeInfo.isSimpleArray) {
|
||||||
const innerZodType = this.buildZodType(typeInfo.innerType)
|
const innerZodType = this.buildZodType(typeInfo.innerType)
|
||||||
return z.array(innerZodType)
|
let zodType: z.ZodTypeAny = z.array(innerZodType)
|
||||||
|
|
||||||
|
// Apply modifiers if present
|
||||||
|
if (typeInfo.modifiers) {
|
||||||
|
zodType = this.applyModifiers(zodType, typeInfo.modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodType
|
||||||
}
|
}
|
||||||
|
|
||||||
let zodType: z.ZodTypeAny
|
let zodType: z.ZodTypeAny
|
||||||
|
|
@ -386,7 +621,13 @@ export class SecureZodSchemaParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply modifiers
|
// Apply modifiers
|
||||||
for (const modifier of typeInfo.modifiers || []) {
|
zodType = this.applyModifiers(zodType, typeInfo.modifiers || [])
|
||||||
|
|
||||||
|
return zodType
|
||||||
|
}
|
||||||
|
|
||||||
|
private static applyModifiers(zodType: z.ZodTypeAny, modifiers: any[]): z.ZodTypeAny {
|
||||||
|
for (const modifier of modifiers) {
|
||||||
switch (modifier.name) {
|
switch (modifier.name) {
|
||||||
case 'int':
|
case 'int':
|
||||||
if (zodType._def?.typeName === 'ZodNumber') {
|
if (zodType._def?.typeName === 'ZodNumber') {
|
||||||
|
|
@ -422,12 +663,16 @@ export class SecureZodSchemaParser {
|
||||||
zodType = zodType.describe(modifier.args[0])
|
zodType = zodType.describe(modifier.args[0])
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case 'default':
|
||||||
|
if (modifier.args[0] !== undefined) {
|
||||||
|
zodType = zodType.default(modifier.args[0])
|
||||||
|
}
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
// Ignore unknown modifiers for compatibility
|
// Ignore unknown modifiers for compatibility
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodType
|
return zodType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue