Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 26 additions & 29 deletions src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { ELYSIA_TRACE, type TraceHandler } from './trace'
import {
ElysiaTypeCheck,
getCookieValidator,
getSchemaProperties,
getSchemaValidator,
hasElysiaMeta,
hasType,
Expand Down Expand Up @@ -734,9 +735,10 @@ export const composeHandler = ({

if (validator.query?.schema) {
const schema = unwrapImportSchema(validator.query?.schema)
const properties = getSchemaProperties(schema)

if (Kind in schema && schema.properties) {
for (const [key, value] of Object.entries(schema.properties)) {
if (properties) {
for (const [key, value] of Object.entries(properties)) {
if (hasElysiaMeta('ArrayQuery', value as TSchema)) {
arrayProperties[key] = true
hasArrayProperty = true
Expand Down Expand Up @@ -1494,22 +1496,10 @@ export const composeHandler = ({

if (candidate) {
const isFirst = fileUnions.length === 0
// Handle case where schema is wrapped in a Union (e.g., ObjectString coercion)
let properties =
candidate.schema?.properties ?? type.properties

// If no properties but schema is a Union, try to find the Object in anyOf
if (!properties && candidate.schema?.anyOf) {
const objectSchema =
candidate.schema.anyOf.find(
(s: any) =>
s.type === 'object' ||
(Kind in s && s[Kind] === 'Object')
)
if (objectSchema) {
properties = objectSchema.properties
}
}
// Handle case where schema is wrapped in a Union/Intersect (e.g., ObjectString coercion)
const properties =
getSchemaProperties(candidate.schema) ??
getSchemaProperties(type)

if (!properties) continue

Expand Down Expand Up @@ -1566,20 +1556,27 @@ export const composeHandler = ({
) {
let validateFile = ''

const bodyProperties = getSchemaProperties(
unwrapImportSchema(validator.body.schema)
)

let i = 0
for (const [k, v] of Object.entries(
unwrapImportSchema(validator.body.schema).properties
) as [string, TSchema][]) {
if (
!v.extension ||
(v[Kind] !== 'File' && v[Kind] !== 'Files')
)
continue
if (bodyProperties) {
for (const [k, v] of Object.entries(bodyProperties) as [
string,
TSchema
][]) {
if (
!v.extension ||
(v[Kind] !== 'File' && v[Kind] !== 'Files')
)
continue

if (i) validateFile += ','
validateFile += `fileType(c.body.${k},${JSON.stringify(v.extension)},'body.${k}')`
if (i) validateFile += ','
validateFile += `fileType(c.body.${k},${JSON.stringify(v.extension)},'body.${k}')`

i++
i++
}
}

if (i) fnLiteral += '\n'
Expand Down
40 changes: 21 additions & 19 deletions src/dynamic-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from './error'
import type { AnyElysia, CookieOptions } from './index'
import { parseQuery } from './parse-query'
import type { ElysiaTypeCheck } from './schema'
import { getSchemaProperties, type ElysiaTypeCheck } from './schema'
import type { TypeCheck } from './type-system'
import type { Handler, LifeCycleStore, SchemaValidator } from './types'
import { hasSetImmediate, redirect, StatusMap, signCookie } from './utils'
Expand All @@ -34,10 +34,10 @@ const injectDefaultValues = (

if (schema.$defs?.[schema.$ref]) schema = schema.$defs[schema.$ref]

if (!schema?.properties) return
const properties = getSchemaProperties(schema)
if (!properties) return

for (const [key, keySchema] of Object.entries(schema.properties)) {
// @ts-expect-error private
for (const [key, keySchema] of Object.entries(properties)) {
obj[key] ??= keySchema.default
}
}
Expand Down Expand Up @@ -411,19 +411,21 @@ export const createDynamicHandler = (app: AnyElysia) => {
if (schema.$defs?.[schema.$ref])
schema = schema.$defs[schema.$ref]

const properties = schema.properties

for (const property of Object.keys(properties)) {
const value = properties[property]
if (
(value.type === 'array' ||
value.items?.type === 'string') &&
typeof context.query[property] === 'string' &&
context.query[property]
) {
// @ts-expect-error
context.query[property] =
context.query[property].split(',')
const properties = getSchemaProperties(schema)

if (properties) {
for (const property of Object.keys(properties)) {
const value = properties[property]
if (
(value.type === 'array' ||
value.items?.type === 'string') &&
typeof context.query[property] === 'string' &&
context.query[property]
) {
// @ts-expect-error
context.query[property] =
context.query[property].split(',')
}
}
}
}
Expand Down Expand Up @@ -637,11 +639,11 @@ export const createDynamicHandler = (app: AnyElysia) => {
secret
)
} else {
const properties = validator?.cookie?.schema?.properties
const properties = getSchemaProperties(validator?.cookie?.schema)

if (secret)
for (const name of cookieMeta.sign) {
if (!(name in properties)) continue
if (!properties || !(name in properties)) continue

if (context.set.cookie[name]?.value) {
context.set.cookie[name].value =
Expand Down
41 changes: 38 additions & 3 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ export const hasElysiaMeta = (meta: string, _schema: TAnySchema): boolean => {
export const hasProperty = (
expectedProperty: string,
_schema: TAnySchema | TypeCheck<any> | ElysiaTypeCheck<any>
) => {
if (!_schema) return
): boolean => {
if (!_schema) return false

// @ts-expect-error private property
const schema = _schema.schema ?? _schema
Expand All @@ -259,6 +259,19 @@ export const hasProperty = (
.References()
.some((schema: TAnySchema) => hasProperty(expectedProperty, schema))

if (schema.anyOf)
return schema.anyOf.some((s: TSchema) =>
hasProperty(expectedProperty, s)
)
if (schema.allOf)
return schema.allOf.some((s: TSchema) =>
hasProperty(expectedProperty, s)
)
if (schema.oneOf)
return schema.oneOf.some((s: TSchema) =>
hasProperty(expectedProperty, s)
)

if (schema.type === 'object') {
const properties = schema.properties as Record<string, TAnySchema>

Expand Down Expand Up @@ -745,7 +758,7 @@ export const getSchemaValidator = <
hasAdditionalProperties(schema))
},
get hasDefault() {
if ('~hasDefault' in this) return this['~hasDefault']
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix, not sure if it's something in my setup but typescript was complaining about this, the other methods had ! so I added it in

if ('~hasDefault' in this) return this['~hasDefault']!

return (this['~hasDefault'] = hasProperty(
'default',
Expand Down Expand Up @@ -1055,6 +1068,28 @@ export const getSchemaValidator = <
export const isUnion = (schema: TSchema) =>
schema[Kind] === 'Union' || (!schema.schema && !!schema.anyOf)

// Returns all properties as a flat map, handling Union/Intersect
// See: https://github.com/sinclairzx81/typebox/blob/0.34.3/src/type/indexed/indexed.ts#L152-L162
export const getSchemaProperties = (
schema: TAnySchema | undefined
): Record<string, TAnySchema> | undefined => {
if (!schema) return undefined

if (schema.properties) return schema.properties

const members = schema.allOf ?? schema.anyOf ?? schema.oneOf
if (members) {
const result: Record<string, TAnySchema> = {}
for (const member of members) {
const props = getSchemaProperties(member)
if (props) Object.assign(result, props)
}
return Object.keys(result).length > 0 ? result : undefined
}

return undefined
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const mergeObjectSchemas = (
schemas: TSchema[]
): {
Expand Down
51 changes: 51 additions & 0 deletions test/aot/has-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,55 @@ describe('Has Transform', () => {

expect(hasType('Files', schema)).toBe(true)
})

// Intersect schema tests
it('find on direct Intersect', () => {
const schema = t.Intersect([
t.Object({
id: t.Number()
}),
t.Object({
file: t.File()
})
])

expect(hasType('File', schema)).toBe(true)
})

it('do not find on Intersect without File', () => {
const schema = t.Intersect([
t.Object({
id: t.Number()
}),
t.Object({
name: t.String()
})
])

expect(hasType('File', schema)).toBe(false)
})

it('find on nested Union in Intersect', () => {
const schema = t.Intersect([
t.Object({
id: t.Number()
}),
t.Union([t.Object({ file: t.File() }), t.Null()])
])

expect(hasType('File', schema)).toBe(true)
})

it('find File in Intersect referenced via Module.Import()', () => {
const schema = t
.Module({
Data: t.Intersect([
t.Object({ id: t.Number() }),
t.Object({ file: t.File() })
])
})
.Import('Data')

expect(hasType('File', schema)).toBe(true)
})
})
24 changes: 24 additions & 0 deletions test/core/dynamic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,4 +758,28 @@ describe('Dynamic Mode', () => {
// names: ['rapi', 'anis']
// })
// })

// Union schema test - verifies getSchemaProperties handles Union without crashing
it('handle Union query schema', async () => {
const app = new Elysia({ aot: false }).get(
'/',
({ query }) => query,
{
query: t.Union([
t.Object({
search: t.String()
}),
t.Object({
id: t.Numeric()
})
])
}
)

const response = await app
.handle(req('/?search=test'))
.then((x) => x.json())

expect(response.search).toBe('test')
})
})
Loading