Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
225 changes: 224 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ import {
getResponseSchemaValidator,
getCookieValidator,
ElysiaTypeCheck,
queryCoercions
queryCoercions,
mergeObjectSchemas
} from './schema'
import {
composeHandler,
Expand Down Expand Up @@ -333,6 +334,228 @@ export default class Elysia<
return this.router.history
}

/**
* Get routes with standaloneValidator schemas merged into direct hook properties.
* This is useful for plugins that need to access guard() schemas.
*
* @returns Routes with flattened schema structure
*/
protected getFlattenedRoutes(): InternalRoute[] {
return this.router.history.map((route) => {
if (!route.hooks?.standaloneValidator?.length) {
return route
}

return {
...route,
hooks: this.mergeStandaloneValidators(route.hooks)
}
})
}

/**
* Merge standaloneValidator array into direct hook properties
*/
private mergeStandaloneValidators(hooks: AnyLocalHook): AnyLocalHook {
const merged = { ...hooks }

if (!hooks.standaloneValidator?.length) return merged

for (const validator of hooks.standaloneValidator) {
// Merge each schema property
if (validator.body) {
merged.body = this.mergeSchemaProperty(
merged.body,
validator.body
)
}
if (validator.headers) {
merged.headers = this.mergeSchemaProperty(
merged.headers,
validator.headers
)
}
if (validator.query) {
merged.query = this.mergeSchemaProperty(
merged.query,
validator.query
)
}
if (validator.params) {
merged.params = this.mergeSchemaProperty(
merged.params,
validator.params
)
}
if (validator.cookie) {
merged.cookie = this.mergeSchemaProperty(
merged.cookie,
validator.cookie
)
}
if (validator.response) {
merged.response = this.mergeResponseSchema(
merged.response,
validator.response
)
}
}

// Normalize any remaining string references in the final result
if (typeof merged.body === 'string') {
merged.body = this.normalizeSchemaReference(merged.body)
}
if (typeof merged.headers === 'string') {
merged.headers = this.normalizeSchemaReference(merged.headers)
}
if (typeof merged.query === 'string') {
merged.query = this.normalizeSchemaReference(merged.query)
}
if (typeof merged.params === 'string') {
merged.params = this.normalizeSchemaReference(merged.params)
}
if (typeof merged.cookie === 'string') {
merged.cookie = this.normalizeSchemaReference(merged.cookie)
}
if (merged.response && typeof merged.response !== 'string') {
// Normalize string references in status code objects
const response = merged.response as any
if ('type' in response || '$ref' in response) {
// It's a schema, not a status code object
if (typeof response === 'string') {
merged.response = this.normalizeSchemaReference(response)
}
} else {
// It's a status code object, normalize each value
for (const [status, schema] of Object.entries(response)) {
if (typeof schema === 'string') {
response[status] = this.normalizeSchemaReference(schema)
}
}
}
}

return merged
}

/**
* Normalize string schema references to TRef nodes for proper merging
*/
private normalizeSchemaReference(
schema: TSchema | string | undefined
): TSchema | undefined {
if (!schema) return undefined
if (typeof schema !== 'string') return schema

// Convert string reference to t.Ref node
// This allows string aliases to participate in schema composition
return t.Ref(schema)
}

/**
* Merge two schema properties (body, query, headers, params, cookie)
*/
private mergeSchemaProperty(
existing: TSchema | string | undefined,
incoming: TSchema | string | undefined
): TSchema | string | undefined {
if (!existing) return incoming
if (!incoming) return existing

// Normalize string references to TRef nodes so they can be merged
const existingSchema = this.normalizeSchemaReference(existing)
const incomingSchema = this.normalizeSchemaReference(incoming)

if (!existingSchema) return incoming
if (!incomingSchema) return existing

// If both are object schemas, merge them
const { schema: mergedSchema, notObjects } = mergeObjectSchemas([
existingSchema,
incomingSchema
])

// If we have non-object schemas, create an Intersect
if (notObjects.length > 0) {
if (mergedSchema) {
return t.Intersect([mergedSchema, ...notObjects])
}
return notObjects.length === 1
? notObjects[0]
: t.Intersect(notObjects)
}

return mergedSchema
}

/**
* Merge response schemas (handles status code objects)
*/
private mergeResponseSchema(
existing:
| TSchema
| { [status: number]: TSchema }
| string
| { [status: number]: string | TSchema }
| undefined,
incoming:
| TSchema
| { [status: number]: TSchema }
| string
| { [status: number]: string | TSchema }
| undefined
): TSchema | { [status: number]: TSchema | string } | string | undefined {
if (!existing) return incoming
if (!incoming) return existing

// Normalize string references to TRef nodes
const normalizedExisting = typeof existing === 'string'
? this.normalizeSchemaReference(existing)
: existing
const normalizedIncoming = typeof incoming === 'string'
? this.normalizeSchemaReference(incoming)
: incoming

if (!normalizedExisting) return incoming
if (!normalizedIncoming) return existing

// Check if either is a TSchema (has 'type' or '$ref' property) vs status code object
const existingIsSchema = 'type' in normalizedExisting || '$ref' in normalizedExisting
const incomingIsSchema = 'type' in normalizedIncoming || '$ref' in normalizedIncoming

// If both are plain schemas, preserve existing (route-specific schema takes precedence)
if (existingIsSchema && incomingIsSchema) {
return normalizedExisting
}

// If existing is status code object and incoming is plain schema,
// merge incoming as status 200 to preserve other status codes
if (!existingIsSchema && incomingIsSchema) {
return (normalizedExisting as Record<number, TSchema | string>)[200] ===
undefined
? {
...normalizedExisting,
200: normalizedIncoming
}
: normalizedExisting
}

// If existing is plain schema and incoming is status code object,
// merge existing as status 200 into incoming (spread incoming first to preserve all status codes)
if (existingIsSchema && !incomingIsSchema) {
return {
...normalizedIncoming,
200: normalizedExisting
}
}

// Both are status code objects, merge them
return {
...normalizedIncoming,
...normalizedExisting
}
}

protected getGlobalDefinitions() {
return this.definitions
}
Expand Down
Loading
Loading