diff --git a/src/index.ts b/src/index.ts index 1db3637d..adf92f9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,170 +1,156 @@ -import { Memoirist } from 'memoirist' import { Kind, - type TObject, - type TSchema, + type TAnySchema, type TModule, + type TObject, type TRef, - type TAnySchema + type TSchema } from '@sinclair/typebox' - import fastDecodeURIComponent from 'fast-decode-uri-component' -import type { Context, PreContext } from './context' - -import { t } from './type-system' -import { mergeInference, sucrose, type Sucrose } from './sucrose' - -import type { WSLocalHook } from './ws/types' - +import { Memoirist } from 'memoirist' import { BunAdapter } from './adapter/bun/index' -import { WebStandardAdapter } from './adapter/web-standard/index' import type { ElysiaAdapter } from './adapter/types' - -import { env } from './universal/env' -import type { ListenCallback, Serve, Server } from './universal/server' - -import { - cloneInference, - deduplicateChecksum, - fnToContainer, - getLoosePath, - localHookToLifeCycleStore, - mergeDeep, - mergeSchemaValidator, - PromiseGroup, - promoteEvent, - isNotEmpty, - encodePath, - lifeCycleToArray, - supportPerMethodInlineHandler, - redirect, - emptySchema, - insertStandaloneValidator -} from './utils' - -import { - coercePrimitiveRoot, - stringToStructureCoercions, - getSchemaValidator, - getResponseSchemaValidator, - getCookieValidator, - ElysiaTypeCheck, - queryCoercions -} from './schema' +import { WebStandardAdapter } from './adapter/web-standard/index' import { - composeHandler, + composeErrorHandler, composeGeneralHandler, - composeErrorHandler + composeHandler } from './compose' - -import { createTracer } from './trace' - -import { - mergeHook, - checksum, - mergeLifeCycle, - filterGlobalHook, - asHookType, - replaceUrlPath -} from './utils' - +import type { Context, PreContext } from './context' import { createDynamicErrorHandler, createDynamicHandler, type DynamicHandler } from './dynamic-handle' - import { - status, + type ElysiaCustomStatusResponse, ERROR_CODE, - ValidationError, - type ParseError, - type NotFoundError, type InternalServerError, - type ElysiaCustomStatusResponse + type NotFoundError, + type ParseError, + status, + ValidationError } from './error' - +import { + coercePrimitiveRoot, + type ElysiaTypeCheck, + getCookieValidator, + getResponseSchemaValidator, + getSchemaValidator, + mergeObjectSchemas, + queryCoercions, + stringToStructureCoercions +} from './schema' +import { mergeInference, type Sucrose, sucrose } from './sucrose' import type { TraceHandler } from './trace' - +import { createTracer } from './trace' +import { t } from './type-system' import type { - ElysiaConfig, - SingletonBase, - DefinitionBase, - Handler, - ComposedHandler, - InputSchema, - LocalHook, - AnyLocalHook, - MergeSchema, - RouteSchema, - UnwrapRoute, - InternalRoute, - HTTPMethod, - SchemaValidator, - PreHandler, - BodyHandler, - OptionalHandler, - ErrorHandler, - LifeCycleStore, - MaybePromise, - Prettify, AddPrefix, - AddSuffix, AddPrefixCapitalize, + AddSuffix, AddSuffixCapitalize, - MaybeArray, - GracefulHandler, - MapResponse, + AfterHandler, + AfterResponseHandler, + AnyLocalHook, + BodyHandler, Checksum, - MacroManager, - MacroToProperty, - TransformHandler, - MetadataBase, - RouteBase, - CreateEden, + ComposedHandler, ComposeElysiaResponse, - InlineHandler, - HookContainer, - LifeCycleType, + ContextAppendType, + CreateEden, + CreateEdenResponse, + DefinitionBase, + DocumentDecoration, + ElysiaConfig, + ElysiaHandlerToResponseSchema, + ElysiaHandlerToResponseSchemaAmbiguous, + ElysiaHandlerToResponseSchemas, + EmptyRouteSchema, EphemeralType, + ErrorHandler, ExcludeElysiaResponse, - ModelValidator, - ContextAppendType, - Reconcile, - AfterResponseHandler, + ExtractErrorFromHandle, + GracefulHandler, + GuardLocalHook, + GuardSchemaType, + Handler, HigherOrderFunction, - ResolvePath, + HookContainer, + HTTPMethod, + InlineHandler, + InlineHandlerNonMacro, + InputSchema, + InternalRoute, + IntersectIfObject, + IntersectIfObjectSchema, JoinPath, - ValidatorLayer, - MergeElysiaInstances, + LifeCycleStore, + LifeCycleType, + LocalHook, Macro, + MacroManager, + MacroProperty, MacroToContext, - StandaloneValidator, - GuardSchemaType, - Or, - DocumentDecoration, - AfterHandler, + MacroToProperty, + MapResponse, + MaybeArray, + MaybeFunction, + MaybePromise, + MaybeValueOrVoidFunction, + MergeElysiaInstances, + MergeSchema, + MetadataBase, + ModelValidator, NonResolvableMacroKey, - StandardSchemaV1Like, - ElysiaHandlerToResponseSchema, - ElysiaHandlerToResponseSchemas, - ExtractErrorFromHandle, - ElysiaHandlerToResponseSchemaAmbiguous, - GuardLocalHook, + OptionalHandler, + Or, PickIfExists, + PreHandler, + Prettify, + Reconcile, + ResolvePath, + RouteBase, + Router, + RouteSchema, + SchemaValidator, SimplifyToSchema, + SingletonBase, + StandaloneValidator, + StandardSchemaV1Like, + TransformHandler, UnionResponseStatus, - CreateEdenResponse, - MacroProperty, - MaybeValueOrVoidFunction, - IntersectIfObject, - IntersectIfObjectSchema, - EmptyRouteSchema, UnknownRouteSchema, - MaybeFunction, - InlineHandlerNonMacro, - Router + UnwrapRoute, + ValidatorLayer } from './types' +import { env } from './universal/env' +import type { ListenCallback, Serve, Server } from './universal/server' +import { + asHookType, + checksum, + cloneInference, + deduplicateChecksum, + emptySchema, + encodePath, + filterGlobalHook, + fnToContainer, + getLoosePath, + insertStandaloneValidator, + isNotEmpty, + lifeCycleToArray, + localHookToLifeCycleStore, + mergeDeep, + mergeHook, + mergeLifeCycle, + mergeSchemaValidator, + PromiseGroup, + promoteEvent, + redirect, + replaceUrlPath, + supportPerMethodInlineHandler +} from './utils' +import type { WSLocalHook } from './ws/types' export type AnyElysia = Elysia @@ -333,6 +319,172 @@ 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 + ) + } + } + + return merged + } + + /** + * 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 + + // If either is a string reference, we can't merge - use incoming + if (typeof existing === 'string' || typeof incoming === 'string') { + return incoming + } + + // If both are object schemas, merge them + const { schema: mergedSchema, notObjects } = mergeObjectSchemas([ + existing, + incoming + ]) + + // 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 + + // If either is a string, we can't merge - use incoming + if (typeof existing === 'string' || typeof incoming === 'string') { + return incoming + } + + // Check if either is a TSchema (has 'type' property) vs status code object + const existingIsSchema = 'type' in existing + const incomingIsSchema = 'type' in incoming + + // If both are plain schemas, preserve existing (route-specific schema takes precedence) + if (existingIsSchema && incomingIsSchema) { + return existing + } + + // 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 (existing as Record)[200] === + undefined + ? { + ...existing, + 200: incoming + } + : existing + } + + // 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 { + ...incoming, + 200: existing + } + } + + // Both are status code objects, merge them + return { + ...incoming, + ...existing + } + } + protected getGlobalDefinitions() { return this.definitions } @@ -856,12 +1008,12 @@ export default class Elysia< if (typeof x.fn === 'function') return x.fn(context) - // @ts-ignore just in case + // @ts-expect-error just in case if (typeof x === 'function') return x(context) }) } catch (error) { let res - // @ts-ignore + // @ts-expect-error context.error = error this.event.error?.some((x) => { @@ -869,7 +1021,7 @@ export default class Elysia< return (res = x.fn(context)) if (typeof x === 'function') - // @ts-ignore just in case + // @ts-expect-error just in case return (res = x(context)) }) @@ -3059,10 +3211,10 @@ export default class Elysia< ): AnyElysia { switch (typeof name) { case 'string': - // @ts-ignore + // @ts-expect-error error.prototype[ERROR_CODE] = name - // @ts-ignore + // @ts-expect-error this.definitions.error[name] = error return this @@ -3074,7 +3226,7 @@ export default class Elysia< } for (const [code, error] of Object.entries(name)) { - // @ts-ignore + // @ts-expect-error error.prototype[ERROR_CODE] = code as any this.definitions.error[code] = error as any @@ -3881,13 +4033,13 @@ export default class Elysia< prefix: Prefix, schema: GuardLocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] resolve: Ephemeral['resolve'] & Volatile['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['response'] }, keyof Metadata['parser'], @@ -3903,7 +4055,7 @@ export default class Elysia< store: Singleton['store'] derive: Singleton['derive'] resolve: Singleton['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] }, Definitions, @@ -3916,7 +4068,7 @@ export default class Elysia< macroFn: Metadata['macroFn'] parser: Metadata['parser'] response: Metadata['response'] & - // @ts-ignore + // @ts-expect-error MacroContext['response'] & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & @@ -4106,13 +4258,13 @@ export default class Elysia< >( hook: GuardLocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] resolve: Ephemeral['resolve'] & Volatile['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['response'] }, keyof Metadata['parser'], @@ -4140,7 +4292,7 @@ export default class Elysia< { derive: Volatile['derive'] resolve: Volatile['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] schema: {} extends PickIfExists< Input, @@ -4157,7 +4309,7 @@ export default class Elysia< ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & - // @ts-ignore + // @ts-expect-error MacroContext['return'] } > @@ -4169,7 +4321,7 @@ export default class Elysia< store: Singleton['store'] derive: Singleton['derive'] resolve: Singleton['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] }, Definitions, @@ -4196,7 +4348,7 @@ export default class Elysia< ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & - // @ts-ignore + // @ts-expect-error MacroContext['return'] }, Routes, @@ -4212,7 +4364,7 @@ export default class Elysia< { derive: Ephemeral['derive'] resolve: Ephemeral['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] schema: {} extends PickIfExists< Input, @@ -4232,7 +4384,7 @@ export default class Elysia< ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & - // @ts-ignore + // @ts-expect-error MacroContext['return'] }, Volatile @@ -4251,7 +4403,7 @@ export default class Elysia< { derive: Volatile['derive'] resolve: Volatile['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] schema: Volatile['schema'] standaloneSchema: SimplifyToSchema & @@ -4266,7 +4418,7 @@ export default class Elysia< ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & - // @ts-ignore + // @ts-expect-error MacroContext['return'] } > @@ -4278,7 +4430,7 @@ export default class Elysia< store: Singleton['store'] derive: Singleton['derive'] resolve: Singleton['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] }, Definitions, @@ -4303,7 +4455,7 @@ export default class Elysia< ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & - // @ts-ignore + // @ts-expect-error MacroContext['return'] }, Routes, @@ -4319,7 +4471,7 @@ export default class Elysia< { derive: Ephemeral['derive'] resolve: Ephemeral['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] schema: Ephemeral['schema'] standaloneSchema: SimplifyToSchema & @@ -4337,7 +4489,7 @@ export default class Elysia< ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & - // @ts-ignore + // @ts-expect-error MacroContext['return'] }, Volatile @@ -4374,7 +4526,7 @@ export default class Elysia< >( schema: GuardLocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] @@ -4393,7 +4545,7 @@ export default class Elysia< store: Singleton['store'] derive: Singleton['derive'] resolve: Singleton['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] }, Definitions, @@ -4406,7 +4558,7 @@ export default class Elysia< macroFn: Metadata['macroFn'] parser: Metadata['parser'] response: Metadata['response'] & - // @ts-ignore + // @ts-expect-error MacroContext['response'] & ElysiaHandlerToResponseSchemaAmbiguous & ElysiaHandlerToResponseSchemaAmbiguous & @@ -4427,12 +4579,12 @@ export default class Elysia< { derive: Volatile['derive'] resolve: Volatile['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] response: Volatile['response'] & - // @ts-ignore + // @ts-expect-error MacroContext['response'] } > @@ -5198,13 +5350,13 @@ export default class Elysia< } if (plugin.validator.global) - // @ts-ignore + // @ts-expect-error this.validator.global = mergeHook(this.validator.global, { ...plugin.validator.global }) as any if (plugin.validator.scoped) - // @ts-ignore + // @ts-expect-error this.validator.local = mergeHook(this.validator.local, { ...plugin.validator.scoped }) @@ -5241,7 +5393,7 @@ export default class Elysia< resolve: Partial< Ephemeral['resolve'] & Volatile['resolve'] > & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] }, Definitions['error'] @@ -5385,7 +5537,7 @@ export default class Elysia< if (iteration >= 16) return const macro = this.extender.macro - for (let [key, value] of Object.entries(appliable)) { + for (const [key, value] of Object.entries(appliable)) { if (key in macro === false) continue const macroHook = @@ -5442,7 +5594,7 @@ export default class Elysia< (k === 'derive' || k === 'resolve') && typeof value === 'function' ) - // @ts-ignore + // @ts-expect-error value = { fn: value, subType: k @@ -5656,7 +5808,7 @@ export default class Elysia< : InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -5664,7 +5816,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -5696,7 +5848,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -5765,7 +5917,7 @@ export default class Elysia< : InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -5773,7 +5925,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -5805,7 +5957,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -5874,7 +6026,7 @@ export default class Elysia< : InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -5882,7 +6034,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -5914,7 +6066,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -5981,7 +6133,7 @@ export default class Elysia< const Handle extends InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -5989,7 +6141,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -6021,7 +6173,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -6088,7 +6240,7 @@ export default class Elysia< const Handle extends InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -6096,7 +6248,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -6128,7 +6280,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -6195,7 +6347,7 @@ export default class Elysia< const Handle extends InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -6203,7 +6355,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -6235,7 +6387,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -6302,7 +6454,7 @@ export default class Elysia< const Handle extends InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -6310,7 +6462,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -6342,7 +6494,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -6409,7 +6561,7 @@ export default class Elysia< const Handle extends InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -6417,7 +6569,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -6449,7 +6601,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -6516,7 +6668,7 @@ export default class Elysia< const Handle extends InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -6524,7 +6676,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -6556,7 +6708,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -6624,7 +6776,7 @@ export default class Elysia< const Handle extends InlineHandler< NoInfer, NoInfer, - // @ts-ignore + // @ts-expect-error MacroContext > >( @@ -6633,7 +6785,7 @@ export default class Elysia< handler: Handle, hook?: LocalHook< Input, - // @ts-ignore + // @ts-expect-error Schema & MacroContext, Decorator, Definitions['error'], @@ -6670,7 +6822,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -6737,7 +6889,7 @@ export default class Elysia< derive: Ephemeral['derive'] & Volatile['derive'] resolve: Ephemeral['resolve'] & Volatile['resolve'] & - // @ts-ignore + // @ts-expect-error MacroContext['resolve'] } > @@ -6771,7 +6923,7 @@ export default class Elysia< Ephemeral['response'], UnionResponseStatus< Volatile['response'], - // @ts-ignore + // @ts-expect-error MacroContext['return'] & {} > > @@ -7600,7 +7752,7 @@ export default class Elysia< } switch (typeof name) { - case 'object': + case 'object': { const parsedTypebox = {} as Record< string, TSchema | StandardSchemaV1Like @@ -7628,15 +7780,17 @@ export default class Elysia< } as any) return this + } - case 'function': + case 'function': { const result = name(this.definitions.type) this.definitions.type = result this.definitions.typebox = t.Module(onlyTypebox(result)) return this + } - case 'string': + case 'string': { if (!model) break this.definitions.type[name] = model @@ -7654,6 +7808,7 @@ export default class Elysia< } as any) return this + } } if (!model) return this @@ -8105,123 +8260,114 @@ export default class Elysia< export { Elysia } -export { t } from './type-system' -export { validationDetail, fileType } from './type-system/utils' -export type { - ElysiaTypeCustomError, - ElysiaTypeCustomErrorCallback -} from './type-system/types' +export type { Static, TSchema } from '@sinclair/typebox' +export { TypeSystemPolicy } from '@sinclair/typebox/system' +export type { ElysiaAdapter } from './adapter' +export type { Context, ErrorContext, PreContext } from './context' +export { Cookie, type CookieOptions, serializeCookie } from './cookies' +export { + ElysiaCustomStatusResponse, + ERROR_CODE, + InternalServerError, + InvalidCookieSignature, + InvalidFileType, + mapValueError, + NotFoundError, + ParseError, + type SelectiveStatus, + status, + ValidationError +} from './error' -export { serializeCookie, Cookie, type CookieOptions } from './cookies' -export type { Context, PreContext, ErrorContext } from './context' +export { + getResponseSchemaValidator, + getSchemaValidator, + replaceSchemaType +} from './schema' export { ELYSIA_TRACE, type TraceEvent, - type TraceListener, type TraceHandler, + type TraceListener, type TraceProcess, type TraceStream } from './trace' - -export { - getSchemaValidator, - getResponseSchemaValidator, - replaceSchemaType -} from './schema' - -export { - mergeHook, - mergeObjectArray, - redirect, - StatusMap, - InvertedStatusMap, - form, - replaceUrlPath, - checksum, - cloneInference, - deduplicateChecksum, - ELYSIA_FORM_DATA, - ELYSIA_REQUEST_ID, - sse -} from './utils' - -export { - status, - mapValueError, - ParseError, - NotFoundError, - ValidationError, - InvalidFileType, - InternalServerError, - InvalidCookieSignature, - ERROR_CODE, - ElysiaCustomStatusResponse, - type SelectiveStatus -} from './error' - +export { t } from './type-system' export type { - EphemeralType, - CreateEden, + ElysiaTypeCustomError, + ElysiaTypeCustomErrorCallback +} from './type-system/types' +export { fileType, validationDetail } from './type-system/utils' +export type { + AfterHandler, + AfterResponseHandler, + BaseMacro, + BodyHandler, + Checksum, + ComposedHandler, ComposeElysiaResponse, - ElysiaConfig, - SingletonBase, + CreateEden, DefinitionBase, - RouteBase, + DocumentDecoration, + ElysiaConfig, + EmptyRouteSchema, + EphemeralType, + ErrorHandler, + ExcludeElysiaResponse, + GracefulHandler, Handler, - ComposedHandler, + HTTPHeaders, + HTTPMethod, + InferContext, + InferHandler, + InlineHandler, InputSchema, - LocalHook, - MergeSchema, - RouteSchema, - UnwrapRoute, InternalRoute, - HTTPMethod, - SchemaValidator, - VoidHandler, - PreHandler, - BodyHandler, - OptionalHandler, - AfterResponseHandler, - ErrorHandler, LifeCycleEvent, LifeCycleStore, LifeCycleType, - MaybePromise, - UnwrapSchema, - Checksum, - DocumentDecoration, - InferContext, - InferHandler, - ResolvePath, - MapResponse, - BaseMacro, + LocalHook, MacroManager, MacroToProperty, - MergeElysiaInstances, + MapResponse, MaybeArray, - ModelValidator, + MaybePromise, + MergeElysiaInstances, + MergeSchema, + MergeStandaloneSchema, + MergeTypeModule, MetadataBase, - UnwrapBodySchema, - UnwrapGroupGuardRoute, + ModelValidator, ModelValidatorError, - ExcludeElysiaResponse, + OptionalHandler, + PreHandler, + ResolveHandler, + ResolvePath, + RouteBase, + RouteSchema, + SchemaValidator, + SingletonBase, SSEPayload, StandaloneInputSchema, - MergeStandaloneSchema, - MergeTypeModule, - GracefulHandler, - AfterHandler, - InlineHandler, - ResolveHandler, TransformHandler, HTTPHeaders, EmptyRouteSchema, ExtractErrorFromHandle } from './types' - export { env } from './universal/env' -export { file, ElysiaFile } from './universal/file' -export type { ElysiaAdapter } from './adapter' - -export { TypeSystemPolicy } from '@sinclair/typebox/system' -export type { Static, TSchema } from '@sinclair/typebox' +export { ElysiaFile, file } from './universal/file' +export { + checksum, + cloneInference, + deduplicateChecksum, + ELYSIA_FORM_DATA, + ELYSIA_REQUEST_ID, + form, + InvertedStatusMap, + mergeHook, + mergeObjectArray, + redirect, + replaceUrlPath, + StatusMap, + sse +} from './utils' diff --git a/test/core/flattened-routes.test.ts b/test/core/flattened-routes.test.ts new file mode 100644 index 00000000..a6b98fc1 --- /dev/null +++ b/test/core/flattened-routes.test.ts @@ -0,0 +1,208 @@ +import { Elysia, t } from '../../src' + +import { describe, expect, it } from 'bun:test' + +describe('getFlattenedRoutes', () => { + it('merges guard standaloneValidator into direct properties', () => { + const app = new Elysia().guard( + { + body: t.Object({ + username: t.String(), + password: t.String() + }) + }, + (app) => + app + .post('/sign-up', ({ body }) => body) + .post('/sign-in', ({ body }) => body) + ) + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + + const signUpRoute = flatRoutes.find((r) => r.path === '/sign-up') + const signInRoute = flatRoutes.find((r) => r.path === '/sign-in') + + expect(signUpRoute).toBeDefined() + expect(signInRoute).toBeDefined() + + // Check that body schema exists in hooks + expect(signUpRoute?.hooks.body).toBeDefined() + expect(signInRoute?.hooks.body).toBeDefined() + + // Verify it's an object schema with the expected properties + expect(signUpRoute?.hooks.body.type).toBe('object') + expect(signUpRoute?.hooks.body.properties).toHaveProperty('username') + expect(signUpRoute?.hooks.body.properties).toHaveProperty('password') + }) + + it('returns original route when no standaloneValidator', () => { + const app = new Elysia().get('/', () => 'hi') + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + // @ts-expect-error - accessing protected method for testing + const normalRoutes = app.getGlobalRoutes() + + expect(flatRoutes.length).toBe(normalRoutes.length) + expect(flatRoutes[0]).toBe(normalRoutes[0]) + }) + + it('merges nested guard schemas', () => { + const app = new Elysia().guard( + { + headers: t.Object({ + authorization: t.String() + }) + }, + (app) => + app.guard( + { + body: t.Object({ + data: t.String() + }) + }, + (app) => app.post('/nested', ({ body }) => body) + ) + ) + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + + const nestedRoute = flatRoutes.find((r) => r.path === '/nested') + + expect(nestedRoute).toBeDefined() + expect(nestedRoute?.hooks.headers).toBeDefined() + expect(nestedRoute?.hooks.body).toBeDefined() + }) + + it('merges guard schema with direct route schema', () => { + const app = new Elysia().guard( + { + headers: t.Object({ + 'x-api-key': t.String() + }) + }, + (app) => + app.post('/mixed', ({ body }) => body, { + body: t.Object({ + name: t.String() + }) + }) + ) + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + + const mixedRoute = flatRoutes.find((r) => r.path === '/mixed') + + expect(mixedRoute).toBeDefined() + expect(mixedRoute?.hooks.headers).toBeDefined() + expect(mixedRoute?.hooks.body).toBeDefined() + + // Both guard and direct schemas should be present + expect(mixedRoute?.hooks.headers.type).toBe('object') + expect(mixedRoute?.hooks.body.type).toBe('object') + }) + + it('handles query and params schemas from guard', () => { + const app = new Elysia().guard( + { + query: t.Object({ + page: t.String() + }), + params: t.Object({ + id: t.String() + }) + }, + (app) => app.get('/items/:id', ({ params, query }) => ({ params, query })) + ) + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + + const itemRoute = flatRoutes.find((r) => r.path === '/items/:id') + + expect(itemRoute).toBeDefined() + expect(itemRoute?.hooks.query).toBeDefined() + expect(itemRoute?.hooks.params).toBeDefined() + }) + + it('handles cookie schemas from guard', () => { + const app = new Elysia().guard( + { + cookie: t.Object({ + session: t.String() + }) + }, + (app) => app.get('/profile', ({ cookie }) => cookie) + ) + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + + const profileRoute = flatRoutes.find((r) => r.path === '/profile') + + expect(profileRoute).toBeDefined() + expect(profileRoute?.hooks.cookie).toBeDefined() + }) + + it('handles response schemas from guard', () => { + const app = new Elysia().guard( + { + response: { + 200: t.Object({ + success: t.Boolean() + }) + } + }, + (app) => app.get('/status', () => ({ success: true })) + ) + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + + const statusRoute = flatRoutes.find((r) => r.path === '/status') + + expect(statusRoute).toBeDefined() + expect(statusRoute?.hooks.response).toBeDefined() + expect(statusRoute?.hooks.response[200]).toBeDefined() + }) + + it('merges status-code map response with plain schema without data loss', () => { + // Test case for the coderabbitai feedback - ensure we don't lose status-code schemas + const app = new Elysia().guard( + { + response: { + 200: t.Object({ data: t.String() }), + 404: t.Object({ error: t.String() }), + 500: t.Object({ message: t.String() }) + } + }, + (app) => + app.get('/data', () => ({ data: 'test' }), { + response: t.String() // Plain schema should be merged as 200, not replace entire map + }) + ) + + // @ts-expect-error - accessing protected method for testing + const flatRoutes = app.getFlattenedRoutes() + + const dataRoute = flatRoutes.find((r) => r.path === '/data') + + expect(dataRoute).toBeDefined() + expect(dataRoute?.hooks.response).toBeDefined() + + // The plain schema should override 200 but preserve 404 and 500 + expect(dataRoute?.hooks.response[200]).toBeDefined() + expect(dataRoute?.hooks.response[404]).toBeDefined() + expect(dataRoute?.hooks.response[500]).toBeDefined() + + // The 200 response should be the plain schema from the route (more specific) + expect(dataRoute?.hooks.response[200].type).toBe('string') + + // Other status codes should be preserved from guard + expect(dataRoute?.hooks.response[404].type).toBe('object') + expect(dataRoute?.hooks.response[500].type).toBe('object') + }) +})