@aklinker1/zeta 1.3.3 → 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/README.md +21 -638
- package/package.json +15 -7
- package/src/adapters/zod-schema-adapter.ts +1 -18
- package/src/app.ts +133 -125
- package/src/client.ts +5 -4
- package/src/errors.ts +1 -15
- package/src/internal/compile-fetch-function.ts +166 -0
- package/src/internal/compile-route-handler.ts +194 -0
- package/src/internal/context.ts +47 -0
- package/src/internal/serialization.ts +30 -31
- package/src/internal/utils.ts +77 -46
- package/src/open-api.ts +33 -18
- package/src/types.ts +25 -42
- package/src/internal/call-handler.ts +0 -139
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aklinker1/zeta",
|
|
3
3
|
"description": "Composable, testable, OpenAPI-first backend framework with validation built-in",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"packageManager": "bun@1.3.
|
|
7
|
+
"packageManager": "bun@1.3.5",
|
|
8
8
|
"module": "src/index.ts",
|
|
9
9
|
"types": "src/index.ts",
|
|
10
10
|
"exports": {
|
|
@@ -41,23 +41,31 @@
|
|
|
41
41
|
"bench": "bun run src/__tests__/bench.ts",
|
|
42
42
|
"example": "bun --watch run example.ts",
|
|
43
43
|
"example:prod": "NODE_ENV=production bun run example",
|
|
44
|
-
"docs:dev": "
|
|
44
|
+
"docs:dev": "zola -r docs serve",
|
|
45
45
|
"docs:build": "vitepress build docs",
|
|
46
46
|
"docs:preview": "vitepress preview docs"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@standard-schema/spec": "^1.0.0",
|
|
50
50
|
"openapi-types": "^12.1.3",
|
|
51
|
-
"rou3": "^0.7.
|
|
51
|
+
"rou3": "^0.7.12",
|
|
52
|
+
"scule": "^1.3.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@aklinker1/check": "^2.2.0",
|
|
55
|
-
"@
|
|
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",
|
|
62
|
+
"@typescript/native-preview": "^7.0.0-dev.20251114.1",
|
|
57
63
|
"changelogen": "^0.6.2",
|
|
58
|
-
"
|
|
64
|
+
"cookie": "^1.1.1",
|
|
65
|
+
"dedent": "^1.7.1",
|
|
66
|
+
"elysia": "^1.4.19",
|
|
59
67
|
"expect-type": "^1.2.1",
|
|
60
|
-
"hono": "^4.
|
|
68
|
+
"hono": "^4.11.2",
|
|
61
69
|
"jsr": "^0.13.5",
|
|
62
70
|
"mermaid": "^11.12.0",
|
|
63
71
|
"oxlint": "^1.2.0",
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
* Contains a schema adapter for app's using [`zod`](https://npmjs.com/package/zod).
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import type { SchemaAdapter } from "../types";
|
|
6
7
|
|
|
7
|
-
const zod = "zod";
|
|
8
|
-
const { z } = await import(zod);
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* Usage:
|
|
12
10
|
*
|
|
@@ -25,21 +23,6 @@ export const zodSchemaAdapter: SchemaAdapter = {
|
|
|
25
23
|
delete res.$schema;
|
|
26
24
|
return res;
|
|
27
25
|
},
|
|
28
|
-
parseParamsRecord: (params: any) => {
|
|
29
|
-
if (params?._zod?.def?.type !== "object")
|
|
30
|
-
throw Error(
|
|
31
|
-
"Query, params, and header schemas must be simple Zode objects defined with z.object({ ... })",
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
return Object.entries(params._zod.def.shape).map(
|
|
35
|
-
([key, schema]: [string, any]) => ({
|
|
36
|
-
name: key,
|
|
37
|
-
schema,
|
|
38
|
-
optional: schema.safeParse(undefined).success,
|
|
39
|
-
...z.globalRegistry.get(schema),
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
42
|
-
},
|
|
43
26
|
getMeta: (schema: any) => {
|
|
44
27
|
return z.globalRegistry.get(schema);
|
|
45
28
|
},
|
package/src/app.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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";
|
|
4
8
|
import type {
|
|
5
9
|
App,
|
|
6
|
-
RouterData,
|
|
7
|
-
RouteDef,
|
|
8
10
|
BasePath,
|
|
9
|
-
ServerSideFetch,
|
|
10
|
-
OnGlobalRequestContext,
|
|
11
|
-
LifeCycleHooks,
|
|
12
|
-
LifeCycleHook,
|
|
13
|
-
DefaultAppData,
|
|
14
11
|
BasePrefix,
|
|
12
|
+
DefaultAppData,
|
|
13
|
+
LifeCycleHook,
|
|
14
|
+
LifeCycleHookName,
|
|
15
|
+
RouteDef,
|
|
16
|
+
RouterData,
|
|
15
17
|
SchemaAdapter,
|
|
18
|
+
ServerSideFetch,
|
|
16
19
|
Transport,
|
|
17
20
|
} from "./types";
|
|
18
|
-
import { addRoute, createRouter, findRoute } from "rou3";
|
|
19
|
-
import {
|
|
20
|
-
callCtxModifierHooks,
|
|
21
|
-
detectTransport,
|
|
22
|
-
serializeErrorResponse,
|
|
23
|
-
} from "./internal/utils";
|
|
24
|
-
import type { OpenAPIV3_1 } from "openapi-types";
|
|
25
|
-
import { buildOpenApiDocs, buildScalarHtml } from "./open-api";
|
|
26
21
|
|
|
27
22
|
let appIdInc = 0;
|
|
28
23
|
const nextAppId = () => `app-${appIdInc++}`;
|
|
@@ -68,15 +63,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
68
63
|
const appId = nextAppId();
|
|
69
64
|
|
|
70
65
|
const { origin = "http://localhost", prefix = "" } = options ?? {};
|
|
71
|
-
const hooks: App["~zeta"]["hooks"] = {
|
|
72
|
-
onAfterHandle: [],
|
|
73
|
-
onGlobalAfterResponse: [],
|
|
74
|
-
onBeforeHandle: [],
|
|
75
|
-
onMapResponse: [],
|
|
76
|
-
onGlobalError: [],
|
|
77
|
-
onGlobalRequest: [],
|
|
78
|
-
onTransform: [],
|
|
79
|
-
};
|
|
66
|
+
const hooks: App["~zeta"]["hooks"] = {};
|
|
80
67
|
const routes: App["~zeta"]["routes"] = {};
|
|
81
68
|
|
|
82
69
|
const addRoutesEntry = (method: string, route: string, data: RouterData) => {
|
|
@@ -87,15 +74,13 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
87
74
|
routes[method][route] = data;
|
|
88
75
|
};
|
|
89
76
|
|
|
90
|
-
const cloneHooks = () =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
onTransform: [...hooks.onTransform],
|
|
98
|
-
});
|
|
77
|
+
const cloneHooks = (): App["~zeta"]["hooks"] => {
|
|
78
|
+
const cloned: App["~zeta"]["hooks"] = {};
|
|
79
|
+
for (const key of Object.keys(hooks) as LifeCycleHookName[]) {
|
|
80
|
+
if (hooks[key]) cloned[key] = [...hooks[key]] as any;
|
|
81
|
+
}
|
|
82
|
+
return cloned;
|
|
83
|
+
};
|
|
99
84
|
|
|
100
85
|
const app: App<DefaultAppData> = {
|
|
101
86
|
// @ts-expect-error
|
|
@@ -146,77 +131,8 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
146
131
|
}
|
|
147
132
|
}
|
|
148
133
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
findRoute(router, method, path);
|
|
152
|
-
|
|
153
|
-
return async (request) => {
|
|
154
|
-
const url = new URL(request.url, origin);
|
|
155
|
-
const ctx: any = {
|
|
156
|
-
path: url.pathname,
|
|
157
|
-
url,
|
|
158
|
-
request,
|
|
159
|
-
method: request.method,
|
|
160
|
-
set: {
|
|
161
|
-
status: HttpStatus.Ok,
|
|
162
|
-
headers: {},
|
|
163
|
-
},
|
|
164
|
-
} satisfies OnGlobalRequestContext;
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const onGlobalRequestResponse = await callCtxModifierHooks(
|
|
168
|
-
ctx,
|
|
169
|
-
hooks.onGlobalRequest,
|
|
170
|
-
);
|
|
171
|
-
if (onGlobalRequestResponse) {
|
|
172
|
-
ctx.response = onGlobalRequestResponse;
|
|
173
|
-
return onGlobalRequestResponse;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const response = await callHandler(
|
|
177
|
-
ctx,
|
|
178
|
-
getRoute,
|
|
179
|
-
options?.schemaAdapter,
|
|
180
|
-
);
|
|
181
|
-
ctx.response = response;
|
|
182
|
-
|
|
183
|
-
return response;
|
|
184
|
-
} catch (err) {
|
|
185
|
-
ctx.error = err;
|
|
186
|
-
|
|
187
|
-
for (const hook of hooks.onGlobalError) {
|
|
188
|
-
const res = hook.callback(ctx);
|
|
189
|
-
res instanceof Promise ? await res : res;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const status =
|
|
193
|
-
err instanceof HttpError
|
|
194
|
-
? err.status
|
|
195
|
-
: HttpStatus.InternalServerError;
|
|
196
|
-
const res = Response.json(serializeErrorResponse(err), { status });
|
|
197
|
-
ctx.response = res;
|
|
198
|
-
return res;
|
|
199
|
-
} finally {
|
|
200
|
-
// Defer calls to the `onGlobalAfterResponse` hooks until after the response is sent
|
|
201
|
-
if (hooks.onGlobalAfterResponse.length > 0) {
|
|
202
|
-
setTimeout(async () => {
|
|
203
|
-
try {
|
|
204
|
-
for (const hook of hooks.onGlobalAfterResponse) {
|
|
205
|
-
let res = hook.callback(ctx);
|
|
206
|
-
if (res instanceof Promise) await res;
|
|
207
|
-
}
|
|
208
|
-
} catch (err) {
|
|
209
|
-
ctx.error = err;
|
|
210
|
-
|
|
211
|
-
for (const hook of hooks.onGlobalError) {
|
|
212
|
-
const res = hook.callback(ctx);
|
|
213
|
-
res instanceof Promise ? await res : res;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}, 0);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
};
|
|
134
|
+
const getRoute = compileRouter(router);
|
|
135
|
+
return compileFetchFunction({ getRoute, hooks, origin });
|
|
220
136
|
},
|
|
221
137
|
|
|
222
138
|
getOpenApiSpec: () => {
|
|
@@ -241,6 +157,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
241
157
|
const obj: Record<string, any> =
|
|
242
158
|
args.length === 2 ? { [args[0]]: args[1] } : args[0];
|
|
243
159
|
|
|
160
|
+
hooks.onTransform ??= [];
|
|
244
161
|
hooks.onTransform.push({
|
|
245
162
|
id: nextHookId(appId),
|
|
246
163
|
applyTo: "local",
|
|
@@ -251,6 +168,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
251
168
|
},
|
|
252
169
|
|
|
253
170
|
onGlobalRequest(callback: any) {
|
|
171
|
+
hooks.onGlobalRequest ??= [];
|
|
254
172
|
hooks.onGlobalRequest.push({
|
|
255
173
|
id: nextHookId(appId),
|
|
256
174
|
applyTo: "global",
|
|
@@ -259,6 +177,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
259
177
|
return app;
|
|
260
178
|
},
|
|
261
179
|
onTransform(callback: any) {
|
|
180
|
+
hooks.onTransform ??= [];
|
|
262
181
|
hooks.onTransform.push({
|
|
263
182
|
id: nextHookId(appId),
|
|
264
183
|
applyTo: "local",
|
|
@@ -267,6 +186,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
267
186
|
return app;
|
|
268
187
|
},
|
|
269
188
|
onBeforeHandle(callback: any) {
|
|
189
|
+
hooks.onBeforeHandle ??= [];
|
|
270
190
|
hooks.onBeforeHandle.push({
|
|
271
191
|
id: nextHookId(appId),
|
|
272
192
|
applyTo: "local",
|
|
@@ -275,6 +195,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
275
195
|
return app;
|
|
276
196
|
},
|
|
277
197
|
onAfterHandle(callback: any) {
|
|
198
|
+
hooks.onAfterHandle ??= [];
|
|
278
199
|
hooks.onAfterHandle.push({
|
|
279
200
|
id: nextHookId(appId),
|
|
280
201
|
applyTo: "local",
|
|
@@ -283,6 +204,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
283
204
|
return app;
|
|
284
205
|
},
|
|
285
206
|
onMapResponse(callback: any) {
|
|
207
|
+
hooks.onMapResponse ??= [];
|
|
286
208
|
hooks.onMapResponse.push({
|
|
287
209
|
id: nextHookId(appId),
|
|
288
210
|
applyTo: "local",
|
|
@@ -291,6 +213,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
291
213
|
return app;
|
|
292
214
|
},
|
|
293
215
|
onGlobalError(callback: any) {
|
|
216
|
+
hooks.onGlobalError ??= [];
|
|
294
217
|
hooks.onGlobalError.push({
|
|
295
218
|
id: nextHookId(appId),
|
|
296
219
|
applyTo: "global",
|
|
@@ -299,6 +222,7 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
299
222
|
return app;
|
|
300
223
|
},
|
|
301
224
|
onGlobalAfterResponse(callback: any) {
|
|
225
|
+
hooks.onGlobalAfterResponse ??= [];
|
|
302
226
|
hooks.onGlobalAfterResponse.push({
|
|
303
227
|
id: nextHookId(appId),
|
|
304
228
|
applyTo: "global",
|
|
@@ -319,21 +243,36 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
319
243
|
app.method.apply(app, [Method.Any, ...args] as any) as any,
|
|
320
244
|
|
|
321
245
|
method(method: string, path: BasePath, ...args: any[]) {
|
|
322
|
-
const
|
|
246
|
+
const routeDef: RouteDef | undefined =
|
|
247
|
+
args.length === 2 ? args[0] : undefined;
|
|
323
248
|
const handler = args[1] ?? args[0];
|
|
324
249
|
const route = `${prefix}${path}`;
|
|
325
|
-
|
|
250
|
+
const hooks = cloneHooks();
|
|
251
|
+
|
|
252
|
+
// Merge app-level tags and security into route definition
|
|
253
|
+
const def: RouteDef | undefined = mergeAppDefaults(routeDef, options);
|
|
254
|
+
|
|
255
|
+
const compiledHandler = compileRouteHandler({
|
|
256
|
+
schemaAdapter: options?.schemaAdapter,
|
|
326
257
|
def,
|
|
258
|
+
hooks,
|
|
259
|
+
method,
|
|
260
|
+
route,
|
|
327
261
|
handler,
|
|
262
|
+
});
|
|
263
|
+
addRoutesEntry(method, route, {
|
|
264
|
+
def,
|
|
328
265
|
route,
|
|
329
|
-
hooks
|
|
266
|
+
hooks,
|
|
267
|
+
compiledHandler,
|
|
268
|
+
handler,
|
|
330
269
|
});
|
|
331
270
|
return app;
|
|
332
271
|
},
|
|
333
272
|
|
|
334
273
|
mount(...args: any[]) {
|
|
335
274
|
let path = "";
|
|
336
|
-
let
|
|
275
|
+
let routeDef: RouteDef = {};
|
|
337
276
|
let fetch: ServerSideFetch;
|
|
338
277
|
|
|
339
278
|
if (args.length === 1) {
|
|
@@ -343,16 +282,31 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
343
282
|
fetch = args[1];
|
|
344
283
|
} else {
|
|
345
284
|
path = args[0];
|
|
346
|
-
|
|
285
|
+
routeDef = args[1];
|
|
347
286
|
fetch = args[2];
|
|
348
287
|
}
|
|
349
288
|
|
|
350
289
|
const route = `${prefix}${path}/**`;
|
|
290
|
+
const hooks = cloneHooks();
|
|
291
|
+
|
|
292
|
+
// Merge app-level tags and security into route definition
|
|
293
|
+
const def = mergeAppDefaults(routeDef, options);
|
|
294
|
+
|
|
295
|
+
const compiledHandler = compileRouteHandler({
|
|
296
|
+
schemaAdapter: options?.schemaAdapter,
|
|
297
|
+
hooks,
|
|
298
|
+
method: "ANY",
|
|
299
|
+
route,
|
|
300
|
+
fetch,
|
|
301
|
+
def,
|
|
302
|
+
});
|
|
303
|
+
|
|
351
304
|
addRoutesEntry(Method.Any, route, {
|
|
352
305
|
def,
|
|
353
306
|
fetch,
|
|
354
307
|
route,
|
|
355
|
-
hooks
|
|
308
|
+
hooks,
|
|
309
|
+
compiledHandler,
|
|
356
310
|
});
|
|
357
311
|
|
|
358
312
|
return app as any;
|
|
@@ -369,20 +323,23 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
369
323
|
}
|
|
370
324
|
}
|
|
371
325
|
|
|
372
|
-
// Add global hooks to parent
|
|
373
|
-
for (const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
326
|
+
// Add the child's global hooks to parent
|
|
327
|
+
for (const hookName of Object.keys(
|
|
328
|
+
childApp["~zeta"].hooks,
|
|
329
|
+
) as LifeCycleHookName[]) {
|
|
330
|
+
for (const hook of childApp["~zeta"].hooks[
|
|
331
|
+
hookName
|
|
332
|
+
]! as LifeCycleHook<any>[]) {
|
|
333
|
+
if (hook.applyTo !== "global" && !childApp["~zeta"].exported)
|
|
334
|
+
continue;
|
|
335
|
+
|
|
336
|
+
if (hooks[hookName]) {
|
|
337
|
+
// Don't add a hook if it's already there
|
|
338
|
+
if (!hooks[hookName].includes(hook)) hooks[hookName].push(hook);
|
|
339
|
+
} else {
|
|
340
|
+
hooks[hookName] = [hook];
|
|
378
341
|
}
|
|
379
342
|
}
|
|
380
|
-
let seen = new Set<string>();
|
|
381
|
-
hooks[name] = hooks[name].filter((hook) => {
|
|
382
|
-
if (seen.has(hook.id)) return false;
|
|
383
|
-
seen.add(hook.id);
|
|
384
|
-
return true;
|
|
385
|
-
}) as LifeCycleHook<any>[];
|
|
386
343
|
}
|
|
387
344
|
|
|
388
345
|
return app as any;
|
|
@@ -455,6 +412,38 @@ export type CreateAppOptions<TPrefix extends BasePrefix = ""> = {
|
|
|
455
412
|
|
|
456
413
|
/** Configure how your application's OpenAPI docs are generated. */
|
|
457
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"];
|
|
458
447
|
/**
|
|
459
448
|
* Configure [Scalar](https://scalar.com/) UI docs.
|
|
460
449
|
* @see https://github.com/scalar/scalar/blob/main/documentation/configuration.md#list-of-all-attributes
|
|
@@ -462,6 +451,25 @@ export type CreateAppOptions<TPrefix extends BasePrefix = ""> = {
|
|
|
462
451
|
scalar?: any;
|
|
463
452
|
};
|
|
464
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
|
+
|
|
465
473
|
enum Method {
|
|
466
474
|
Get = "GET",
|
|
467
475
|
Post = "POST",
|
package/src/client.ts
CHANGED
|
@@ -99,11 +99,12 @@ export function createAppClient<TApp extends App>(
|
|
|
99
99
|
headers: { ...headers } as Record<string, string>,
|
|
100
100
|
} satisfies RequestInit;
|
|
101
101
|
|
|
102
|
-
const
|
|
102
|
+
const serializedBody =
|
|
103
103
|
inputs.body == null ? undefined : smartSerialize(inputs.body);
|
|
104
|
-
if (
|
|
105
|
-
init.body =
|
|
106
|
-
if (
|
|
104
|
+
if (serializedBody) {
|
|
105
|
+
init.body = serializedBody.value;
|
|
106
|
+
if (serializedBody.contentType)
|
|
107
|
+
init.headers["Content-Type"] = serializedBody.contentType;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
try {
|
package/src/errors.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
1
|
import { HttpStatus } from "./status";
|
|
3
2
|
|
|
4
|
-
/**
|
|
5
|
-
* Thrown when validation fails. Refer to `.issues` for details on what went wrong.
|
|
6
|
-
*/
|
|
7
|
-
export class ValidationGlobalError extends Error {
|
|
8
|
-
constructor(
|
|
9
|
-
readonly input: any,
|
|
10
|
-
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>,
|
|
11
|
-
) {
|
|
12
|
-
super("Validation error");
|
|
13
|
-
this.name = "ValidationGlobalError";
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
3
|
/**
|
|
18
4
|
* Base class of all HTTP errors. You can throw this error or throw any of the
|
|
19
5
|
* subclasses. Zeta will automatically detect and handle these errors, setting
|
|
@@ -425,7 +411,7 @@ export class NotImplementedHttpError extends HttpError {
|
|
|
425
411
|
options?: ErrorOptions,
|
|
426
412
|
) {
|
|
427
413
|
super(HttpStatus.NotImplemented, message, additionalInfo, options);
|
|
428
|
-
this.name = "
|
|
414
|
+
this.name = "NotImplementedHttpError";
|
|
429
415
|
}
|
|
430
416
|
}
|
|
431
417
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { MatchedRoute } from "rou3";
|
|
2
|
+
import { HttpError, NotFoundHttpError } from "../errors";
|
|
3
|
+
import { HttpStatus } from "../status";
|
|
4
|
+
import type { LifeCycleHooks, RouterData, ServerSideFetch } from "../types";
|
|
5
|
+
import { Context } from "./context";
|
|
6
|
+
import {
|
|
7
|
+
cleanupCompiledWhitespace,
|
|
8
|
+
getRawPathname,
|
|
9
|
+
serializeErrorResponse,
|
|
10
|
+
} from "./utils";
|
|
11
|
+
|
|
12
|
+
export function compileFetchFunction(options: CompileOptions): ServerSideFetch {
|
|
13
|
+
const onGlobalRequestCount = options.hooks.onGlobalRequest?.length;
|
|
14
|
+
const onGlobalAfterResponseCount =
|
|
15
|
+
options.hooks.onGlobalAfterResponse?.length;
|
|
16
|
+
const onGlobalErrorCount = options.hooks.onGlobalError?.length;
|
|
17
|
+
|
|
18
|
+
const js = `
|
|
19
|
+
return (request) => {
|
|
20
|
+
const path = utils.getRawPathname(request);
|
|
21
|
+
const ctx = new utils.Context(request, path, utils.origin);
|
|
22
|
+
${onGlobalAfterResponseCount ? "let handlerReturnedPromise = false;" : ""}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
${onGlobalRequestCount ? compileOnGlobalRequestHook(onGlobalRequestCount) : ""}
|
|
26
|
+
|
|
27
|
+
const matchedRoute = utils.getRoute(request.method, path);
|
|
28
|
+
if (matchedRoute == null) {
|
|
29
|
+
throw new utils.NotFoundHttpError(undefined, {
|
|
30
|
+
method: request.method,
|
|
31
|
+
path,
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
ctx.matchedRoute = matchedRoute;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ctx.response = matchedRoute.data.compiledHandler(request, ctx);
|
|
38
|
+
if (typeof ctx.response.then !== utils.FUNCTION) return ctx.response;
|
|
39
|
+
|
|
40
|
+
${onGlobalAfterResponseCount ? "handlerReturnedPromise = true;" : ""}
|
|
41
|
+
return ctx.response.catch(error => {
|
|
42
|
+
${onGlobalErrorCount ? compileOnGlobalErrorHook(onGlobalErrorCount, 3) : ""}
|
|
43
|
+
|
|
44
|
+
${compileErrorResponse(3)}
|
|
45
|
+
})${onGlobalAfterResponseCount ? compileOnGlobalAfterResponsePromiseFinally(onGlobalAfterResponseCount, 2) : ""};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
${onGlobalErrorCount ? compileOnGlobalErrorHook(onGlobalErrorCount, 2) : ""}
|
|
48
|
+
|
|
49
|
+
${compileErrorResponse(2)}
|
|
50
|
+
} ${onGlobalAfterResponseCount ? compileOnGlobalAfterResponseFinally(onGlobalAfterResponseCount, 1) : ""}
|
|
51
|
+
}
|
|
52
|
+
//#sourceURL=zeta-jit-generated://zeta-fetch-fn.js
|
|
53
|
+
`;
|
|
54
|
+
return new Function("utils", cleanupCompiledWhitespace(js))({
|
|
55
|
+
FUNCTION: "function",
|
|
56
|
+
getRawPathname,
|
|
57
|
+
hooks: options.hooks,
|
|
58
|
+
Context,
|
|
59
|
+
getRoute: options.getRoute,
|
|
60
|
+
NotFoundHttpError,
|
|
61
|
+
origin: options.origin,
|
|
62
|
+
HttpError,
|
|
63
|
+
HttpStatus,
|
|
64
|
+
serializeErrorResponse,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function compileOnGlobalRequestHook(hookCount: number): string {
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < hookCount; i++) {
|
|
72
|
+
const resultVar = `onGlobalRequestRes${i}`;
|
|
73
|
+
lines.push(
|
|
74
|
+
` const ${resultVar} = utils.hooks.onGlobalRequest[${i}].callback(ctx);`,
|
|
75
|
+
...(process.env.NODE_ENV !== "production"
|
|
76
|
+
? [
|
|
77
|
+
` if (${resultVar} instanceof Promise)`,
|
|
78
|
+
` console.warn("Warning: Promise returned from onGlobalRequest hook. Promises returned from onGlobalRequest are not awaited, ignoring the return value.");`,
|
|
79
|
+
]
|
|
80
|
+
: []),
|
|
81
|
+
` if (${resultVar})`,
|
|
82
|
+
` if (typeof ${resultVar}.body?.bytes === utils.FUNCTION)`,
|
|
83
|
+
` return ${resultVar};`,
|
|
84
|
+
` else`,
|
|
85
|
+
` for (const key of Object.keys(${resultVar}))`,
|
|
86
|
+
` ctx[key] = ${resultVar}[key];`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function compileOnGlobalErrorHook(hookCount: number, tabs: number): string {
|
|
94
|
+
const indent = " ".repeat(tabs);
|
|
95
|
+
const lines: string[] = [`${indent}ctx.error = error;`];
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < hookCount; i++) {
|
|
98
|
+
lines.push(`${indent}utils.hooks.onGlobalError[${i}].callback(ctx);`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compileOnGlobalAfterResponseFinally(
|
|
105
|
+
hookCount: number,
|
|
106
|
+
tabs: number,
|
|
107
|
+
): string {
|
|
108
|
+
const indent = " ".repeat(tabs);
|
|
109
|
+
return `finally {
|
|
110
|
+
${indent} if (!handlerReturnedPromise) {
|
|
111
|
+
${compileOnGlobalAfterResponseHook(hookCount, tabs + 2)}
|
|
112
|
+
${indent} }
|
|
113
|
+
${indent}}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compileOnGlobalAfterResponsePromiseFinally(
|
|
118
|
+
hookCount: number,
|
|
119
|
+
tabs: number,
|
|
120
|
+
): string {
|
|
121
|
+
const indent = " ".repeat(tabs);
|
|
122
|
+
return `.finally(() => {
|
|
123
|
+
${compileOnGlobalAfterResponseHook(hookCount, tabs + 1)}
|
|
124
|
+
${indent}})`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compileOnGlobalAfterResponseHook(
|
|
128
|
+
hookCount: number,
|
|
129
|
+
tabs: number,
|
|
130
|
+
): string {
|
|
131
|
+
const indent = " ".repeat(tabs);
|
|
132
|
+
const lines: string[] = [`${indent}setTimeout(() => {`];
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < hookCount; i++) {
|
|
135
|
+
lines.push(
|
|
136
|
+
`${indent} utils.hooks.onGlobalAfterResponse[${i}].callback(ctx);`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lines.push(`${indent}})`);
|
|
141
|
+
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function compileErrorResponse(tabs: number): string {
|
|
146
|
+
const indent = " ".repeat(tabs);
|
|
147
|
+
return `${indent}const status =
|
|
148
|
+
${indent} error instanceof utils.HttpError
|
|
149
|
+
${indent} ? error.status
|
|
150
|
+
${indent} : utils.HttpStatus.InternalServerError;
|
|
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});`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type CompileOptions = {
|
|
160
|
+
hooks: LifeCycleHooks;
|
|
161
|
+
getRoute: (
|
|
162
|
+
method: string,
|
|
163
|
+
path: string,
|
|
164
|
+
) => MatchedRoute<RouterData> | undefined;
|
|
165
|
+
origin: string;
|
|
166
|
+
};
|