@supacloud/elysia 0.24.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -630,6 +630,16 @@ transaction and persist with a row lock or expected-version check.
630
630
  Lightweight error class carrying HTTP `status`, machine-readable `code`, and
631
631
  optional structured `details`.
632
632
 
633
+ The adapter registers `ApplicationError` and `SchemaContractError` in Elysia
634
+ 2.0's native error dictionary (`app.error(ErrorClass, handler)`). Typed entries
635
+ keep these classes out of the generic catch-all, so route response inference
636
+ stays precise while Elysia (and Eden Treaty) knows the errors are handled. The
637
+ dictionary is exported as `applicationErrorDictionary`, and
638
+ `frameworkErrorCode(error)` exposes the same code normalization (`ParseError` →
639
+ `PARSE`, `ValidationError` → `VALIDATION`, otherwise the error's own `code`)
640
+ used by the generic lane. A configured `errorMapper` remains authoritative for
641
+ every lane and still receives the request context.
642
+
633
643
  ### `createMemorySandbox(options): MemorySandbox`
634
644
 
635
645
  Creates an in-process application harness with `request()`, `db`, `storage`,
@@ -693,9 +703,18 @@ const app = registerElysiaRoute(new Elysia(), route);
693
703
 
694
704
  The callback is typed from the contract (including decoded transforms and
695
705
  declared response statuses). Cookie values retain Elysia's native shape, so a
696
- declared `session: t.String()` is read as `cookie.session.value`. This helper
697
- does not add Eden-style client inference to an existing Elysia instance; the
698
- compiler-generated client remains the source of transport types.
706
+ declared `session: t.String()` is read as `cookie.session.value`.
707
+ `registerElysiaRoute` returns the same instance and preserves its existing type.
708
+ Chaining registers routes at runtime but does not add those routes to Eden's
709
+ static route tree. Use the contract's typed client or the compiler-generated
710
+ client for transport types.
711
+
712
+ When no schema declares `query` or `headers`, the handler context falls back to
713
+ Elysia's native `Record<string, string | undefined>` (not a narrower `string`),
714
+ and `params` resolves from the path exactly as Elysia does. TypeBox transforms
715
+ (`t.Numeric`, `t.Transform`, `Type.Codec`) are decoded before the handler runs,
716
+ so `query.page` from `t.Object({ page: t.Numeric() })` is a `number`, not a
717
+ string.
699
718
 
700
719
  Response maps may use concrete statuses, `1XX`-`5XX` families, and `default`.
701
720
  The adapter expands family/default entries to concrete validators before
@@ -92,6 +92,10 @@ export interface CompiledController {
92
92
  export interface CompiledModule {
93
93
  name: string;
94
94
  createServices(deps: Record<string, unknown>, imported: Record<string, Record<string, unknown>>): Record<string, unknown>;
95
+ /** Initializes application-scoped instances owned by this module. */
96
+ initializeServices?(services: Record<string, unknown>): Promise<void>;
97
+ /** Destroys application-scoped instances owned by this module. */
98
+ destroyServices?(services: Record<string, unknown>): Promise<void>;
95
99
  createRequestScope?(services: Record<string, unknown>, ctx: unknown, imported?: Record<string, Record<string, unknown>>): Promise<Record<string, unknown>>;
96
100
  destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
97
101
  createJobScope?(services: Record<string, unknown>, ctx: unknown, imported?: Record<string, Record<string, unknown>>): Promise<Record<string, unknown>>;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export type { HttpPolicyDatabase } from "./http-policy-postgres";
14
14
  export { createHttpTelemetry } from "./http-telemetry";
15
15
  export type { HttpTelemetryEvent, HttpTelemetryObserver } from "./http-telemetry";
16
16
  import { type ExecutionObserver } from "./execution";
17
+ import { SchemaContractError } from "./schema_contract";
17
18
  import { type ApplicationDocumentationOptions } from "./documentation";
18
19
  export type { ExecutionEvent, ExecutionObserver } from "./execution";
19
20
  export { bindCompiledCommand } from "./command-binding";
@@ -242,6 +243,28 @@ export interface ErrorContext {
242
243
  requestContext: unknown;
243
244
  frameworkCode: string | number | undefined;
244
245
  }
246
+ /**
247
+ * Elysia 2.0 native error dictionary registered by the adapter.
248
+ *
249
+ * Declaring the runtime classes here lets Elysia's type system (and therefore
250
+ * Eden Treaty / generated clients) know these errors are *handled*: the generic
251
+ * `.error(handler)` catch-all no longer widens every route's response schema
252
+ * with an untyped fallback. Each entry also documents the public `code` served
253
+ * by {@link defaultErrorResponse} when a caller does not install an
254
+ * `errorMapper`.
255
+ */
256
+ export declare const applicationErrorDictionary: {
257
+ readonly ApplicationError: typeof ApplicationError;
258
+ readonly SchemaContractError: typeof SchemaContractError;
259
+ };
260
+ export type ApplicationErrorDictionary = typeof applicationErrorDictionary;
261
+ export type ApplicationErrorClass = ApplicationErrorDictionary[keyof ApplicationErrorDictionary];
262
+ /**
263
+ * Normalize the framework's runtime error code the same way the generic error
264
+ * hook always has: Elysia's built-in `ParseError`/`ValidationError` become
265
+ * `PARSE`/`VALIDATION`, while domain errors keep their own `code`.
266
+ */
267
+ export declare function frameworkErrorCode(error: unknown): string;
245
268
  export type ErrorMapper = (error: unknown, context: ErrorContext) => Response | undefined | Promise<Response | undefined>;
246
269
  export type ApplicationHttpContext<Http extends AnyElysia = Elysia> = Omit<Context<{}, {
247
270
  decorator: Http["~Singleton"]["decorator"];
package/dist/index.js CHANGED
@@ -1978,6 +1978,13 @@ function validatedJsonResponse(validate, value, init) {
1978
1978
  headers.set("content-type", "application/json");
1979
1979
  return new Response(body, { ...init, headers });
1980
1980
  }
1981
+ var applicationErrorDictionary = {
1982
+ ApplicationError,
1983
+ SchemaContractError
1984
+ };
1985
+ function frameworkErrorCode(error) {
1986
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code.toUpperCase().replaceAll("-", "_") : "UNKNOWN";
1987
+ }
1981
1988
  var httpPluginIds = new WeakMap;
1982
1989
  var nextHttpPluginId = 0;
1983
1990
  function contextPlugin(http) {
@@ -2409,16 +2416,18 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
2409
2416
  requestCompletions.delete(request);
2410
2417
  }
2411
2418
  });
2412
- plugin.error(async ({ error, request }) => {
2413
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code.toUpperCase().replaceAll("-", "_") : "UNKNOWN";
2419
+ const mapError = async (error, frameworkCode, request) => {
2414
2420
  const context = {
2415
2421
  request,
2416
2422
  requestContext: requestContexts.get(request),
2417
- frameworkCode: code
2423
+ frameworkCode
2418
2424
  };
2419
2425
  const mapped = await options.errorMapper?.(error, context);
2420
- return mapped ?? defaultErrorResponse(error, code);
2421
- });
2426
+ return mapped ?? defaultErrorResponse(error, frameworkCode);
2427
+ };
2428
+ plugin.error(ApplicationError, ({ error, request }) => mapError(error, frameworkErrorCode(error), request));
2429
+ plugin.error(SchemaContractError, ({ error, request }) => mapError(error, frameworkErrorCode(error), request));
2430
+ plugin.error(({ error, request }) => mapError(error, frameworkErrorCode(error), request));
2422
2431
  const routeRegistrar = plugin;
2423
2432
  for (const controller of compiled.controllers) {
2424
2433
  for (const route of controller.routes) {
@@ -2828,6 +2837,7 @@ export {
2828
2837
  VERIFIED_JWT_SUBJECT_HEADER,
2829
2838
  WorkerReceiptUnconfirmedError,
2830
2839
  WorkerRegistrationError,
2840
+ applicationErrorDictionary,
2831
2841
  assertFeatureTransition,
2832
2842
  assertResponseStatusDeclared,
2833
2843
  bindCompiledCommand,
@@ -2859,6 +2869,7 @@ export {
2859
2869
  defineRouteContract,
2860
2870
  executeCompiledCommand,
2861
2871
  executeJob,
2872
+ frameworkErrorCode,
2862
2873
  previewCompiledCommand,
2863
2874
  registerElysiaRoute,
2864
2875
  requireIdempotencyKey,
@@ -70,9 +70,29 @@ type PathParameterNames<Path extends string> = Path extends `${string}:${infer N
70
70
  type PathParameters<Path extends string> = [PathParameterNames<Path>] extends [never] ? Record<string, string> : {
71
71
  [Name in PathParameterNames<Path>]: string;
72
72
  };
73
+ /**
74
+ * Resolve one declared schema field to the value Elysia hands to the handler.
75
+ *
76
+ * Elysia registers the route with the same schema instance, so the handler sees
77
+ * the *decoded* output of TypeBox transforms (`t.Numeric`, `t.Transform`,
78
+ * `Type.Codec`). `StaticDecode` mirrors that pipeline exactly, which is why the
79
+ * decoded type is used here instead of the raw `Static` input type.
80
+ */
73
81
  type DecodedField<Schemas extends RouteContractSchemas, Key extends keyof RouteContractSchemas> = [
74
82
  Extract<Schemas[Key], TSchema>
75
83
  ] extends [never] ? unknown : UnwrapSchema<Extract<Schemas[Key], TSchema>>;
84
+ /** True when the contract declared a schema for `Key`. */
85
+ type HasField<Schemas extends RouteContractSchemas, Key extends keyof RouteContractSchemas> = [
86
+ Extract<Schemas[Key], TSchema>
87
+ ] extends [never] ? false : true;
88
+ /**
89
+ * Elysia's native fallback for an undeclared route field.
90
+ *
91
+ * The values are copied from Elysia's own `Context` so a contract without a
92
+ * schema behaves exactly like a hand-written Elysia route (query and headers
93
+ * stay `string | undefined`, params resolve from the path).
94
+ */
95
+ type FieldFallback<Schemas extends RouteContractSchemas, Key extends keyof RouteContractSchemas, Path extends string> = HasField<Schemas, Key> extends true ? DecodedField<Schemas, Key> : Key extends "params" ? PathParameters<Path> : Key extends "query" | "headers" ? Record<string, string | undefined> : Record<string, unknown>;
76
96
  type DecodedCookie<Schemas extends RouteContractSchemas> = DecodedField<Schemas, "cookie"> extends infer Value ? Value extends Record<string, unknown> ? Record<string, Cookie<unknown>> & {
77
97
  [Key in keyof Value]-?: Cookie<Value[Key]>;
78
98
  } : Record<string, Cookie<unknown>> : Record<string, Cookie<unknown>>;
@@ -84,9 +104,9 @@ type RouteStatus<Schemas extends RouteContractSchemas> = [StatusCode<Schemas>] e
84
104
  /** Elysia-compatible decoded context inferred from one route contract and path. */
85
105
  export type ElysiaRouteContext<Schemas extends RouteContractSchemas, Path extends string = ""> = {
86
106
  body: DecodedField<Schemas, "body">;
87
- params: Schemas["params"] extends TSchema ? DecodedField<Schemas, "params"> : PathParameters<Path>;
88
- query: Schemas["query"] extends TSchema ? DecodedField<Schemas, "query"> : Record<string, string>;
89
- headers: Schemas["headers"] extends TSchema ? DecodedField<Schemas, "headers"> : Record<string, string | undefined>;
107
+ params: FieldFallback<Schemas, "params", Path>;
108
+ query: FieldFallback<Schemas, "query", Path>;
109
+ headers: FieldFallback<Schemas, "headers", Path>;
90
110
  cookie: DecodedCookie<Schemas>;
91
111
  request: Request;
92
112
  path: string;
@@ -94,7 +114,7 @@ export type ElysiaRouteContext<Schemas extends RouteContractSchemas, Path extend
94
114
  server: unknown;
95
115
  store: Record<string, unknown>;
96
116
  set: {
97
- headers: Record<string, string>;
117
+ headers: Record<string, string | number>;
98
118
  status?: number | keyof StatusMap;
99
119
  redirect?: string;
100
120
  cookie?: Record<string, unknown>;
@@ -127,7 +147,12 @@ export declare function toElysiaRouteSchema<const Schemas extends RouteContractS
127
147
  * contextually typed from the same schemas that will be registered at runtime.
128
148
  */
129
149
  export declare function defineElysiaRoute<const Method extends HTTPMethod, const Path extends string, const Schemas extends RouteContractSchemas>(method: Method, path: Path, contract: Schemas, handler: ElysiaRouteHandler<NoInfer<Schemas>, Path>): ElysiaRouteDefinition<Method, Path, Schemas>;
130
- /** Register a contract-bound route while preserving Elysia's fluent app API. */
150
+ /**
151
+ * Register a contract-bound route while preserving Elysia's fluent app API.
152
+ *
153
+ * Runtime registration preserves the caller's existing instance type. It does
154
+ * not add the contract to Eden's static route tree.
155
+ */
131
156
  export declare function registerElysiaRoute<const App extends AnyElysia, const Method extends HTTPMethod, const Path extends string, const Schemas extends RouteContractSchemas>(app: App, route: ElysiaRouteDefinition<Method, Path, Schemas>): App;
132
157
  export declare class SchemaContractError extends Error {
133
158
  readonly code = "SCHEMA_CONTRACT_INVALID";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/elysia",
3
- "version": "0.24.1",
3
+ "version": "0.26.0",
4
4
  "description": "Elysia runtime adapter for SupaCloud compiled modules: application/request scopes, route registration and validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -65,7 +65,7 @@
65
65
  "elysia": "2.0.0-beta.21"
66
66
  },
67
67
  "devDependencies": {
68
- "@supacloud/compiler": "0.38.1",
68
+ "@supacloud/compiler": "0.39.0",
69
69
  "@supacloud/delivery": "0.8.0",
70
70
  "@supacloud/commands": "0.9.1",
71
71
  "@supacloud/db": "0.13.1",