@aklinker1/zeta 2.0.0 → 2.1.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/package.json +8 -1
- package/src/app.ts +76 -16
- package/src/errors.ts +1 -1
- package/src/internal/compile-fetch-function.ts +14 -5
- package/src/internal/compile-route-handler.ts +13 -9
- package/src/types.ts +3 -18
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.
|
|
4
|
+
"version": "2.1.0",
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
${
|
|
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 (
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import type { MaybePromise } from "elysia";
|
|
2
|
+
import { getMeta } from "../meta";
|
|
2
3
|
import type {
|
|
3
4
|
CompiledRouteHandler,
|
|
4
5
|
LifeCycleHookName,
|
|
@@ -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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
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
|
`;
|
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: (
|