diff --git a/src/schema.ts b/src/schema.ts index cec272ef..3051fd30 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -80,6 +80,39 @@ export const isOptional = ( return !!schema && OptionalKind in schema } +export const hasPatternProperties = ( + _schema: TAnySchema | TypeCheck | ElysiaTypeCheck +): boolean => { + if (!_schema) return false + + // @ts-expect-error private property + const schema: TAnySchema = (_schema as TypeCheck)?.schema ?? _schema + + if ('patternProperties' in schema) return true + + if (schema[Kind] === 'Import' && _schema.References) + return _schema.References().some(hasPatternProperties) + + if (schema.anyOf) return schema.anyOf.some(hasPatternProperties) + if (schema.oneOf) return schema.oneOf.some(hasPatternProperties) + if (schema.someOf) return schema.someOf.some(hasPatternProperties) + if (schema.allOf) return schema.allOf.some(hasPatternProperties) + if (schema.not) return hasPatternProperties(schema.not) + + if (schema.type === 'object' && schema.properties) { + const properties = schema.properties as Record + + for (const key of Object.keys(properties)) { + if (hasPatternProperties(properties[key])) return true + } + } + + if (schema.type === 'array' && schema.items && !Array.isArray(schema.items)) + return hasPatternProperties(schema.items) + + return false +} + export const hasAdditionalProperties = ( _schema: TAnySchema | TypeCheck | ElysiaTypeCheck ): boolean => { @@ -507,7 +540,7 @@ export const getSchemaValidator = < let mirror: Function if (normalize === true || normalize === 'exactMirror') try { - mirror = createMirror(schema as TSchema, { + mirror = createMirror(schema as TAnySchema, { TypeCompiler, sanitize: sanitize?.(), modules @@ -684,14 +717,15 @@ export const getSchemaValidator = < }) ]) - if (schema.type === 'object' && hasAdditional) + if (schema.type === 'object' && hasAdditional && !('patternProperties' in schema)) schema.additionalProperties = false } } else { if ( schema.type === 'object' && ('additionalProperties' in schema === false || - forceAdditionalProperties) + forceAdditionalProperties) && + !('patternProperties' in schema) ) schema.additionalProperties = additionalProperties else @@ -701,6 +735,7 @@ export const getSchemaValidator = < to(schema) { if (!schema.properties) return schema; if ("additionalProperties" in schema) return schema; + if ("patternProperties" in schema) return schema; return t.Object(schema.properties, { ...schema, @@ -764,10 +799,10 @@ export const getSchemaValidator = < if (validator?.schema?.config) delete validator.schema.config } - if (normalize && schema.additionalProperties === false) { + if (normalize && !hasPatternProperties(schema)) { if (normalize === true || normalize === 'exactMirror') { try { - validator.Clean = createMirror(schema, { + validator.Clean = createMirror(schema as TAnySchema, { TypeCompiler, sanitize: sanitize?.(), modules @@ -903,25 +938,27 @@ export const getSchemaValidator = < if (compiled?.schema?.config) delete compiled.schema.config } - if (normalize === true || normalize === 'exactMirror') { - try { - compiled.Clean = createMirror(schema, { - TypeCompiler, - sanitize: sanitize?.(), - modules - }) - } catch (error) { - console.warn( - 'Failed to create exactMirror. Please report the following code to https://github.com/elysiajs/elysia/issues' - ) - console.dir(schema, { - depth: null - }) + if (normalize && !hasPatternProperties(schema)) { + if (normalize === true || normalize === 'exactMirror') { + try { + compiled.Clean = createMirror(schema as TAnySchema, { + TypeCompiler, + sanitize: sanitize?.(), + modules + }) + } catch (error) { + console.warn( + 'Failed to create exactMirror. Please report the following code to https://github.com/elysiajs/elysia/issues' + ) + console.dir(schema, { + depth: null + }) + compiled.Clean = createCleaner(schema) + } + } else if (normalize === 'typebox') compiled.Clean = createCleaner(schema) - } - } else if (normalize === 'typebox') - compiled.Clean = createCleaner(schema) + } } else { compiled = { provider: 'standard', diff --git a/test/validator/response.test.ts b/test/validator/response.test.ts index e1bb36b3..58d1f9fb 100644 --- a/test/validator/response.test.ts +++ b/test/validator/response.test.ts @@ -562,4 +562,33 @@ describe('Response Validator', () => { expect(result.join('')).toContain('data: {"name":"Name"}') }) + + it('validate Record with nested objects', async () => { + const app = new Elysia().get('/', () => ({ + list: [{ + toto: { bar: 1 }, + foo: { link: 'first' } + }], + one: { + toto: { bar: 0 }, + foo: { link: 'second' } + } + }), { + response: { + 200: t.Object({ + list: t.Array(t.Object({ + toto: t.Object({ bar: t.Integer() }), + foo: t.Record(t.String(), t.String()) + })), + one: t.Object({ + toto: t.Object({ bar: t.Integer() }), + foo: t.Record(t.String(), t.String()) + }) + }) + } + }) + + const res = await app.handle(req('/')) + expect(res.status).toBe(200) + }) })