@aklinker1/zeta 2.0.0 → 2.1.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aklinker1/zeta",
3
3
  "description": "Composable, testable, OpenAPI-first backend framework with validation built-in",
4
- "version": "2.0.0",
4
+ "version": "2.1.1",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "packageManager": "bun@1.3.5",
@@ -53,9 +53,16 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@aklinker1/check": "^2.2.0",
56
+ "@opentelemetry/api": "^1.9.0",
57
+ "@opentelemetry/resources": "^2.5.0",
58
+ "@opentelemetry/sdk-node": "^0.211.0",
59
+ "@opentelemetry/sdk-trace-node": "^2.5.0",
60
+ "@opentelemetry/semantic-conventions": "^1.39.0",
56
61
  "@types/bun": "latest",
57
62
  "@typescript/native-preview": "^7.0.0-dev.20251114.1",
58
63
  "changelogen": "^0.6.2",
64
+ "cookie": "^1.1.1",
65
+ "dedent": "^1.7.1",
59
66
  "elysia": "^1.4.19",
60
67
  "expect-type": "^1.2.1",
61
68
  "hono": "^4.11.2",
package/src/app.ts CHANGED
@@ -1,23 +1,23 @@
1
+ import type { OpenAPI, OpenAPIV3_1 } from "openapi-types";
2
+ import { addRoute, createRouter } from "rou3";
3
+ import { compileRouter } from "rou3/compiler";
4
+ import { compileFetchFunction } from "./internal/compile-fetch-function";
5
+ import { compileRouteHandler } from "./internal/compile-route-handler";
6
+ import { detectTransport } from "./internal/utils";
7
+ import { buildOpenApiDocs, buildScalarHtml } from "./open-api";
1
8
  import type {
2
9
  App,
3
- RouterData,
4
- RouteDef,
5
10
  BasePath,
6
- ServerSideFetch,
7
- LifeCycleHook,
8
- DefaultAppData,
9
11
  BasePrefix,
12
+ DefaultAppData,
13
+ LifeCycleHook,
14
+ LifeCycleHookName,
15
+ RouteDef,
16
+ RouterData,
10
17
  SchemaAdapter,
18
+ ServerSideFetch,
11
19
  Transport,
12
- LifeCycleHookName,
13
20
  } from "./types";
14
- import { addRoute, createRouter } from "rou3";
15
- import { compileRouter } from "rou3/compiler";
16
- import { detectTransport } from "./internal/utils";
17
- import type { OpenAPIV3_1 } from "openapi-types";
18
- import { buildOpenApiDocs, buildScalarHtml } from "./open-api";
19
- import { compileRouteHandler } from "./internal/compile-route-handler";
20
- import { compileFetchFunction } from "./internal/compile-fetch-function";
21
21
 
22
22
  let appIdInc = 0;
23
23
  const nextAppId = () => `app-${appIdInc++}`;
@@ -243,10 +243,15 @@ export function createApp<TPrefix extends BasePrefix = "">(
243
243
  app.method.apply(app, [Method.Any, ...args] as any) as any,
244
244
 
245
245
  method(method: string, path: BasePath, ...args: any[]) {
246
- const def: RouteDef = args.length === 2 ? args[0] : undefined;
246
+ const routeDef: RouteDef | undefined =
247
+ args.length === 2 ? args[0] : undefined;
247
248
  const handler = args[1] ?? args[0];
248
249
  const route = `${prefix}${path}`;
249
250
  const hooks = cloneHooks();
251
+
252
+ // Merge app-level tags and security into route definition
253
+ const def: RouteDef | undefined = mergeAppDefaults(routeDef, options);
254
+
250
255
  const compiledHandler = compileRouteHandler({
251
256
  schemaAdapter: options?.schemaAdapter,
252
257
  def,
@@ -267,7 +272,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
267
272
 
268
273
  mount(...args: any[]) {
269
274
  let path = "";
270
- let def = {};
275
+ let routeDef: RouteDef = {};
271
276
  let fetch: ServerSideFetch;
272
277
 
273
278
  if (args.length === 1) {
@@ -277,12 +282,16 @@ export function createApp<TPrefix extends BasePrefix = "">(
277
282
  fetch = args[1];
278
283
  } else {
279
284
  path = args[0];
280
- def = args[1];
285
+ routeDef = args[1];
281
286
  fetch = args[2];
282
287
  }
283
288
 
284
289
  const route = `${prefix}${path}/**`;
285
290
  const hooks = cloneHooks();
291
+
292
+ // Merge app-level tags and security into route definition
293
+ const def = mergeAppDefaults(routeDef, options);
294
+
286
295
  const compiledHandler = compileRouteHandler({
287
296
  schemaAdapter: options?.schemaAdapter,
288
297
  hooks,
@@ -403,6 +412,38 @@ export type CreateAppOptions<TPrefix extends BasePrefix = ""> = {
403
412
 
404
413
  /** Configure how your application's OpenAPI docs are generated. */
405
414
  openApi?: Partial<OpenAPIV3_1.Document> & {};
415
+
416
+ /**
417
+ * OpenAPI tags to apply to all routes in this app. Route-level tags will
418
+ * override these app-level tags.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * const usersApp = createApp({
423
+ * prefix: "/users",
424
+ * tags: ["Users"],
425
+ * })
426
+ * .get("/", {}, () => [...]) // Will have ["Users"] tag
427
+ * .get("/:id", { tags: ["Admin"] }, () => {...}) // Will have ["Admin"] tag (overrides app-level)
428
+ * ```
429
+ */
430
+ tags?: string[];
431
+
432
+ /**
433
+ * OpenAPI security requirements to apply to all routes in this app.
434
+ * Route-level security will override these app-level security requirements.
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * const authApp = createApp({
439
+ * prefix: "/auth",
440
+ * security: [{ bearerAuth: [] }],
441
+ * })
442
+ * .get("/profile", {}, () => {...}) // Will require bearerAuth
443
+ * .get("/admin", { security: [{ adminKey: [] }] }, () => {...}) // Will require adminKey (overrides app-level)
444
+ * ```
445
+ */
446
+ security?: OpenAPI.Document["security"];
406
447
  /**
407
448
  * Configure [Scalar](https://scalar.com/) UI docs.
408
449
  * @see https://github.com/scalar/scalar/blob/main/documentation/configuration.md#list-of-all-attributes
@@ -410,6 +451,25 @@ export type CreateAppOptions<TPrefix extends BasePrefix = ""> = {
410
451
  scalar?: any;
411
452
  };
412
453
 
454
+ /**
455
+ * Apply app-level defaults (tags, security) to a route definition.
456
+ * Route-level values override app-level defaults.
457
+ */
458
+ function mergeAppDefaults(
459
+ routeDef: RouteDef | undefined,
460
+ options: CreateAppOptions<any> | undefined,
461
+ ): RouteDef | undefined {
462
+ if (!options?.tags?.length && !options?.security?.length) {
463
+ return routeDef;
464
+ }
465
+
466
+ return {
467
+ tags: options.tags,
468
+ security: options.security,
469
+ ...routeDef,
470
+ };
471
+ }
472
+
413
473
  enum Method {
414
474
  Get = "GET",
415
475
  Post = "POST",
package/src/errors.ts CHANGED
@@ -411,7 +411,7 @@ export class NotImplementedHttpError extends HttpError {
411
411
  options?: ErrorOptions,
412
412
  ) {
413
413
  super(HttpStatus.NotImplemented, message, additionalInfo, options);
414
- this.name = "NotImplementedError";
414
+ this.name = "NotImplementedHttpError";
415
415
  }
416
416
  }
417
417
 
@@ -1,13 +1,13 @@
1
1
  import type { MatchedRoute } from "rou3";
2
+ import { HttpError, NotFoundHttpError } from "../errors";
3
+ import { HttpStatus } from "../status";
2
4
  import type { LifeCycleHooks, RouterData, ServerSideFetch } from "../types";
5
+ import { Context } from "./context";
3
6
  import {
4
7
  cleanupCompiledWhitespace,
5
8
  getRawPathname,
6
9
  serializeErrorResponse,
7
10
  } from "./utils";
8
- import { Context } from "./context";
9
- import { HttpError, NotFoundHttpError } from "../errors";
10
- import { HttpStatus } from "../status";
11
11
 
12
12
  export function compileFetchFunction(options: CompileOptions): ServerSideFetch {
13
13
  const onGlobalRequestCount = options.hooks.onGlobalRequest?.length;
@@ -19,6 +19,7 @@ export function compileFetchFunction(options: CompileOptions): ServerSideFetch {
19
19
  return (request) => {
20
20
  const path = utils.getRawPathname(request);
21
21
  const ctx = new utils.Context(request, path, utils.origin);
22
+ ${onGlobalAfterResponseCount ? "let handlerReturnedPromise = false;" : ""}
22
23
 
23
24
  try {
24
25
  ${onGlobalRequestCount ? compileOnGlobalRequestHook(onGlobalRequestCount) : ""}
@@ -36,6 +37,7 @@ ${onGlobalRequestCount ? compileOnGlobalRequestHook(onGlobalRequestCount) : ""}
36
37
  ctx.response = matchedRoute.data.compiledHandler(request, ctx);
37
38
  if (typeof ctx.response.then !== utils.FUNCTION) return ctx.response;
38
39
 
40
+ ${onGlobalAfterResponseCount ? "handlerReturnedPromise = true;" : ""}
39
41
  return ctx.response.catch(error => {
40
42
  ${onGlobalErrorCount ? compileOnGlobalErrorHook(onGlobalErrorCount, 3) : ""}
41
43
 
@@ -105,7 +107,9 @@ function compileOnGlobalAfterResponseFinally(
105
107
  ): string {
106
108
  const indent = " ".repeat(tabs);
107
109
  return `finally {
108
- ${compileOnGlobalAfterResponseHook(hookCount, tabs + 1)}
110
+ ${indent} if (!handlerReturnedPromise) {
111
+ ${compileOnGlobalAfterResponseHook(hookCount, tabs + 2)}
112
+ ${indent} }
109
113
  ${indent}}
110
114
  `;
111
115
  }
@@ -144,7 +148,12 @@ function compileErrorResponse(tabs: number): string {
144
148
  ${indent} error instanceof utils.HttpError
145
149
  ${indent} ? error.status
146
150
  ${indent} : utils.HttpStatus.InternalServerError;
147
- ${indent}return (ctx.response = Response.json(utils.serializeErrorResponse(error), { status }));`;
151
+ ${indent}return (
152
+ ${indent} ctx.response = Response.json(
153
+ ${indent} utils.serializeErrorResponse(error),
154
+ ${indent} { status, headers: ctx.set.headers },
155
+ ${indent} )
156
+ ${indent});`;
148
157
  }
149
158
 
150
159
  type CompileOptions = {
@@ -1,8 +1,9 @@
1
- import type { MaybePromise } from "elysia";
1
+ import { getMeta } from "../meta";
2
2
  import type {
3
3
  CompiledRouteHandler,
4
4
  LifeCycleHookName,
5
5
  LifeCycleHooks,
6
+ MaybePromise,
6
7
  OnBeforeHandleContext,
7
8
  RouteDef,
8
9
  SchemaAdapter,
@@ -15,7 +16,6 @@ import {
15
16
  validateInputSchema,
16
17
  validateOutputSchema,
17
18
  } from "./utils";
18
- import { getMeta } from "../meta";
19
19
 
20
20
  export function compileRouteHandler(
21
21
  options: CompileOptions,
@@ -57,18 +57,22 @@ ${options.hooks.onAfterHandle?.length ? compileResponseModifierHookCall("onAfter
57
57
  ${options.hooks.onMapResponse?.length ? compileResponseModifierHookCall("onMapResponse", options.hooks.onMapResponse.length) : ""}
58
58
 
59
59
  if (ctx.response == null) {
60
- return new Response(undefined, {
61
- status: ctx.set.status,
62
- headers: ctx.set.headers,
63
- })
60
+ return (
61
+ ctx.response = new Response(undefined, {
62
+ status: ctx.set.status,
63
+ headers: ctx.set.headers,
64
+ })
65
+ )
64
66
  }
65
67
 
66
68
  const serialized = utils.smartSerialize(ctx.response);
67
69
  if (!ctx.set.headers["Content-Type"]) ctx.set.headers["Content-Type"] = ${responseContentTypeMap ? "responseContentTypeMap[ctx.set.status] ??" : ""} serialized.contentType
68
- return new Response(serialized.value, {
69
- status: ctx.set.status,
70
- headers: ctx.set.headers,
71
- })
70
+ return (
71
+ ctx.response = new Response(serialized.value, {
72
+ status: ctx.set.status,
73
+ headers: ctx.set.headers,
74
+ })
75
+ )
72
76
  }
73
77
  //#sourceURL=${getSourceUrl(options)}
74
78
  `;
@@ -11,6 +11,10 @@ export class Context {
11
11
 
12
12
  matchedRoute: MatchedRoute<RouterData> | undefined;
13
13
 
14
+ // Private storage for overwritten values
15
+ #params: Record<string, any> | undefined;
16
+ #query: Record<string, any> | undefined;
17
+
14
18
  constructor(
15
19
  public request: Request,
16
20
  public path: string,
@@ -21,14 +25,28 @@ export class Context {
21
25
  return new URL(this.request.url, this.origin);
22
26
  }
23
27
 
24
- get params(): Record<string, string> {
28
+ get params(): Record<string, any> {
29
+ if (this.#params !== undefined) {
30
+ return this.#params;
31
+ }
25
32
  return this.matchedRoute?.params ? getRawParams(this.matchedRoute) : {};
26
33
  }
27
34
 
28
- get query(): Record<string, string> {
35
+ set params(value: Record<string, any>) {
36
+ this.#params = value;
37
+ }
38
+
39
+ get query(): Record<string, any> {
40
+ if (this.#query !== undefined) {
41
+ return this.#query;
42
+ }
29
43
  return this.request.url.includes("?") ? getRawQuery(this.request) : {};
30
44
  }
31
45
 
46
+ set query(value: Record<string, any>) {
47
+ this.#query = value;
48
+ }
49
+
32
50
  get route(): string | undefined {
33
51
  return this.matchedRoute?.data.route;
34
52
  }
package/src/types.ts CHANGED
@@ -130,12 +130,7 @@ export interface App<TAppData extends AppData = AppData> {
130
130
  onGlobalRequest(
131
131
  callback: (
132
132
  ctx: OnGlobalRequestContext<GetAppDataCtx<TAppData>>,
133
- ) => MaybePromise<void>,
134
- ): this;
135
- onGlobalRequest(
136
- callback: (
137
- ctx: OnGlobalRequestContext<GetAppDataCtx<TAppData>>,
138
- ) => MaybePromise<Response>,
133
+ ) => MaybePromise<Response | void>,
139
134
  ): this;
140
135
  onGlobalRequest<TNewCtx extends Record<string, any>>(
141
136
  callback: (
@@ -154,12 +149,7 @@ export interface App<TAppData extends AppData = AppData> {
154
149
  onTransform(
155
150
  callback: (
156
151
  ctx: OnTransformContext<GetAppDataCtx<TAppData>>,
157
- ) => MaybePromise<void>,
158
- ): this;
159
- onTransform(
160
- callback: (
161
- ctx: OnTransformContext<GetAppDataCtx<TAppData>>,
162
- ) => MaybePromise<Response>,
152
+ ) => MaybePromise<Response | void>,
163
153
  ): this;
164
154
  onTransform<TNewCtx extends Record<string, any>>(
165
155
  callback: (
@@ -178,12 +168,7 @@ export interface App<TAppData extends AppData = AppData> {
178
168
  onBeforeHandle(
179
169
  callback: (
180
170
  ctx: OnBeforeHandleContext<GetAppDataCtx<TAppData>>,
181
- ) => MaybePromise<void>,
182
- ): this;
183
- onBeforeHandle(
184
- callback: (
185
- ctx: OnBeforeHandleContext<GetAppDataCtx<TAppData>>,
186
- ) => MaybePromise<Response>,
171
+ ) => MaybePromise<Response | void>,
187
172
  ): this;
188
173
  onBeforeHandle<TNewCtx extends Record<string, any>>(
189
174
  callback: (