@aklinker1/zeta 1.0.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/LICENSE +21 -0
- package/README.md +655 -0
- package/package.json +60 -0
- package/src/adapters/zod-schema-adapter.ts +46 -0
- package/src/app.ts +453 -0
- package/src/client.ts +183 -0
- package/src/custom-responses.ts +166 -0
- package/src/errors.ts +543 -0
- package/src/index.ts +5 -0
- package/src/internal/call-handler.ts +132 -0
- package/src/internal/serialization.ts +72 -0
- package/src/internal/utils.ts +131 -0
- package/src/open-api.ts +234 -0
- package/src/status.ts +143 -0
- package/src/testing.ts +62 -0
- package/src/types.ts +1111 -0
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aklinker1/zeta",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"packageManager": "bun@1.2.17",
|
|
7
|
+
"module": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./types": "./src/types.ts",
|
|
12
|
+
"./client": "./src/client.ts",
|
|
13
|
+
"./testing": "./src/testing.ts",
|
|
14
|
+
"./adapters/zod-schema-adapter": "./src/adapters/zod-schema-adapter.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src/*.ts",
|
|
18
|
+
"src/adapters/*.ts",
|
|
19
|
+
"src/internal/*.ts"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"url": "https://github.com/aklinker1/zeta"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "bun test --watch",
|
|
29
|
+
"bench": "bun run src/__tests__/bench.ts",
|
|
30
|
+
"example": "bun --watch run example.ts",
|
|
31
|
+
"example:prod": "NODE_ENV=production bun run example",
|
|
32
|
+
"publish": "bun run scripts/publish.ts",
|
|
33
|
+
"docs:dev": "vitepress dev docs",
|
|
34
|
+
"docs:build": "vitepress build docs",
|
|
35
|
+
"docs:preview": "vitepress preview docs"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@standard-schema/spec": "^1.0.0",
|
|
39
|
+
"rou3": "^0.7.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@aklinker1/check": "^2.1.0",
|
|
43
|
+
"@types/bun": "latest",
|
|
44
|
+
"changelogen": "^0.6.2",
|
|
45
|
+
"elysia": "^1.3.5",
|
|
46
|
+
"expect-type": "^1.2.1",
|
|
47
|
+
"hono": "^4.8.3",
|
|
48
|
+
"jsr": "^0.13.5",
|
|
49
|
+
"mermaid": "^11.12.0",
|
|
50
|
+
"openapi-types": "^12.1.3",
|
|
51
|
+
"oxlint": "^1.2.0",
|
|
52
|
+
"prettier": "^3.5.3",
|
|
53
|
+
"publint": "^0.3.12",
|
|
54
|
+
"tinybench": "^4.0.1",
|
|
55
|
+
"typescript": "^5.8.3",
|
|
56
|
+
"vitepress": "^2.0.0-alpha.12",
|
|
57
|
+
"vitepress-plugin-mermaid": "^2.0.17",
|
|
58
|
+
"zod": "^4.1.11"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contains a schema adapter for app's using [`zod`](https://npmjs.com/package/zod).
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import type { SchemaAdapter } from "../types";
|
|
6
|
+
|
|
7
|
+
const zod = "zod";
|
|
8
|
+
const { z } = await import(zod);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Usage:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { zodSchemaAdapter } from "@aklinker1/zeta/adapters/zod-schema-adapter";
|
|
15
|
+
*
|
|
16
|
+
* const app = createApp({
|
|
17
|
+
* schemaAdapter: zodSchemaAdapter,
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const zodSchemaAdapter: SchemaAdapter = {
|
|
22
|
+
toJsonSchema: (schema) => {
|
|
23
|
+
if (!("_zod" in schema)) throw Error("input schema is not a Zod schema");
|
|
24
|
+
const res = z.toJSONSchema(schema, { target: "draft-7" });
|
|
25
|
+
delete res.$schema;
|
|
26
|
+
return res;
|
|
27
|
+
},
|
|
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
|
+
getMeta: (schema: any) => {
|
|
44
|
+
return z.globalRegistry.get(schema);
|
|
45
|
+
},
|
|
46
|
+
};
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { callHandler } from "./internal/call-handler";
|
|
2
|
+
import { HttpError } from "./errors";
|
|
3
|
+
import { HttpStatus } from "./status";
|
|
4
|
+
import type {
|
|
5
|
+
App,
|
|
6
|
+
RouterData,
|
|
7
|
+
RouteDef,
|
|
8
|
+
BasePath,
|
|
9
|
+
ServerSideFetch,
|
|
10
|
+
OnGlobalRequestContext,
|
|
11
|
+
LifeCycleHooks,
|
|
12
|
+
LifeCycleHook,
|
|
13
|
+
DefaultAppData,
|
|
14
|
+
BasePrefix,
|
|
15
|
+
SchemaAdapter,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import { addRoute, createRouter, findRoute } from "rou3";
|
|
18
|
+
import { callCtxModifierHooks, serializeErrorResponse } from "./internal/utils";
|
|
19
|
+
import type { OpenAPIV3_1 } from "openapi-types";
|
|
20
|
+
import { buildOpenApiDocs, buildScalarHtml } from "./open-api";
|
|
21
|
+
|
|
22
|
+
let appIdInc = 0;
|
|
23
|
+
const nextAppId = () => `app-${appIdInc++}`;
|
|
24
|
+
|
|
25
|
+
let _hookIdInc = 0;
|
|
26
|
+
const nextHookId = (appId: string) => `${appId}/hook-${_hookIdInc++}`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a server-side, Zeta application.
|
|
30
|
+
*
|
|
31
|
+
* Zeta provides simple support for serving applications using `Bun.serve` and
|
|
32
|
+
* `Deno.serve` by calling `app.listen(3000)`.
|
|
33
|
+
*
|
|
34
|
+
* If you need more customization, you can use the `build` method to create a
|
|
35
|
+
* `fetch` function and serve it however you like.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { createApp } from "@aklinker1/zeta";
|
|
40
|
+
*
|
|
41
|
+
* const app = createApp({ prefix: "/api" })
|
|
42
|
+
* .get("/health", () => "OK")
|
|
43
|
+
* .get("/users", () => ["user1", "user2"]);
|
|
44
|
+
*
|
|
45
|
+
* app.listen(3000);
|
|
46
|
+
*
|
|
47
|
+
* // Or serve the app yourself
|
|
48
|
+
* const fetch = app.build();
|
|
49
|
+
* Bun.serve({ fetch, ... });
|
|
50
|
+
* Deno.serve({ fetch, ... });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @param options Configure application behavior.
|
|
54
|
+
*/
|
|
55
|
+
export function createApp<TPrefix extends BasePrefix = "">(
|
|
56
|
+
options?: CreateAppOptions<TPrefix>,
|
|
57
|
+
): App<{
|
|
58
|
+
ctx: {};
|
|
59
|
+
exported: false;
|
|
60
|
+
prefix: TPrefix;
|
|
61
|
+
routes: {};
|
|
62
|
+
}> {
|
|
63
|
+
const appId = nextAppId();
|
|
64
|
+
|
|
65
|
+
const { origin = "http://localhost", prefix = "" } = options ?? {};
|
|
66
|
+
const hooks: App["~zeta"]["hooks"] = {
|
|
67
|
+
onAfterHandle: [],
|
|
68
|
+
onGlobalAfterResponse: [],
|
|
69
|
+
onBeforeHandle: [],
|
|
70
|
+
onMapResponse: [],
|
|
71
|
+
onGlobalError: [],
|
|
72
|
+
onGlobalRequest: [],
|
|
73
|
+
onTransform: [],
|
|
74
|
+
};
|
|
75
|
+
const routes: App["~zeta"]["routes"] = {};
|
|
76
|
+
|
|
77
|
+
const addRoutesEntry = (method: string, route: string, data: RouterData) => {
|
|
78
|
+
routes[method] ??= {};
|
|
79
|
+
if (routes[method][route]) {
|
|
80
|
+
console.warn(`Route ${route} already exists`);
|
|
81
|
+
}
|
|
82
|
+
routes[method][route] = data;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const cloneHooks = () => ({
|
|
86
|
+
onAfterHandle: [...hooks.onAfterHandle],
|
|
87
|
+
onGlobalAfterResponse: [...hooks.onGlobalAfterResponse],
|
|
88
|
+
onBeforeHandle: [...hooks.onBeforeHandle],
|
|
89
|
+
onMapResponse: [...hooks.onMapResponse],
|
|
90
|
+
onGlobalError: [...hooks.onGlobalError],
|
|
91
|
+
onGlobalRequest: [...hooks.onGlobalRequest],
|
|
92
|
+
onTransform: [...hooks.onTransform],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const app: App<DefaultAppData> = {
|
|
96
|
+
// @ts-expect-error
|
|
97
|
+
[Symbol.toStringTag]: "ZetaApp",
|
|
98
|
+
|
|
99
|
+
"~zeta": {
|
|
100
|
+
id: appId,
|
|
101
|
+
prefix,
|
|
102
|
+
routes,
|
|
103
|
+
hooks,
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
build: () => {
|
|
107
|
+
const jsonRoute = options?.openApiRoute ?? "/openapi.json";
|
|
108
|
+
const scalarRoute = options?.scalarRoute ?? "/scalar";
|
|
109
|
+
const docs = buildOpenApiDocs(options, app);
|
|
110
|
+
if (docs.type === "error") {
|
|
111
|
+
console.error("Failed to build OpenAPI docs:", docs.error);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
app.get(jsonRoute, () => {
|
|
115
|
+
if (docs.type === "error") {
|
|
116
|
+
console.error("Failed to build OpenAPI docs:", docs.error);
|
|
117
|
+
throw docs.error;
|
|
118
|
+
}
|
|
119
|
+
return docs.spec;
|
|
120
|
+
});
|
|
121
|
+
if (docs.type === "success") {
|
|
122
|
+
const scalarHtml = buildScalarHtml(jsonRoute, options);
|
|
123
|
+
app.get(
|
|
124
|
+
scalarRoute,
|
|
125
|
+
() =>
|
|
126
|
+
new Response(scalarHtml, {
|
|
127
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const router = createRouter<RouterData>();
|
|
133
|
+
for (const [method, methodValue] of Object.entries(routes)) {
|
|
134
|
+
for (const [path, data] of Object.entries(methodValue)) {
|
|
135
|
+
addRoute(
|
|
136
|
+
router,
|
|
137
|
+
method === Method.Any ? undefined : method,
|
|
138
|
+
path,
|
|
139
|
+
data,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// const getRoute = compileRouter(router);
|
|
145
|
+
const getRoute = (method: string, path: string) =>
|
|
146
|
+
findRoute(router, method, path);
|
|
147
|
+
|
|
148
|
+
return async (request) => {
|
|
149
|
+
const url = new URL(request.url, origin);
|
|
150
|
+
const ctx: any = {
|
|
151
|
+
path: url.pathname,
|
|
152
|
+
url,
|
|
153
|
+
request,
|
|
154
|
+
method: request.method,
|
|
155
|
+
set: {
|
|
156
|
+
status: HttpStatus.Ok,
|
|
157
|
+
headers: {},
|
|
158
|
+
},
|
|
159
|
+
} satisfies OnGlobalRequestContext;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const onGlobalRequestResponse = await callCtxModifierHooks(
|
|
163
|
+
ctx,
|
|
164
|
+
hooks.onGlobalRequest,
|
|
165
|
+
);
|
|
166
|
+
if (onGlobalRequestResponse) {
|
|
167
|
+
ctx.response = onGlobalRequestResponse;
|
|
168
|
+
return onGlobalRequestResponse;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const response = await callHandler(ctx, getRoute);
|
|
172
|
+
ctx.response = response;
|
|
173
|
+
|
|
174
|
+
return response;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
ctx.error = err;
|
|
177
|
+
|
|
178
|
+
for (const hook of hooks.onGlobalError) {
|
|
179
|
+
const res = hook.callback(ctx);
|
|
180
|
+
res instanceof Promise ? await res : res;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const status =
|
|
184
|
+
err instanceof HttpError
|
|
185
|
+
? err.status
|
|
186
|
+
: HttpStatus.InternalServerError;
|
|
187
|
+
const res = Response.json(serializeErrorResponse(err), { status });
|
|
188
|
+
ctx.response = res;
|
|
189
|
+
return res;
|
|
190
|
+
} finally {
|
|
191
|
+
// Defer calls to the `onGlobalAfterResponse` hooks until after the response is sent
|
|
192
|
+
if (hooks.onGlobalAfterResponse.length > 0) {
|
|
193
|
+
setTimeout(async () => {
|
|
194
|
+
try {
|
|
195
|
+
for (const hook of hooks.onGlobalAfterResponse) {
|
|
196
|
+
let res = hook.callback(ctx);
|
|
197
|
+
if (res instanceof Promise) await res;
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
ctx.error = err;
|
|
201
|
+
|
|
202
|
+
for (const hook of hooks.onGlobalError) {
|
|
203
|
+
const res = hook.callback(ctx);
|
|
204
|
+
res instanceof Promise ? await res : res;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}, 0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
getOpenApiSpec: () => {
|
|
214
|
+
const res = buildOpenApiDocs(options, app);
|
|
215
|
+
if (res.type === "error") throw res.error;
|
|
216
|
+
|
|
217
|
+
return res.spec;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
export: () => {
|
|
221
|
+
app["~zeta"].exported = true;
|
|
222
|
+
return app as any;
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
listen: (port, cb) => {
|
|
226
|
+
if (typeof Bun !== "undefined") {
|
|
227
|
+
Bun.serve({ port, fetch: app.build() });
|
|
228
|
+
if (cb) setTimeout(cb, 0);
|
|
229
|
+
} else if (
|
|
230
|
+
// @ts-expect-error: Deno types not installed.
|
|
231
|
+
typeof Deno !== "undefined"
|
|
232
|
+
) {
|
|
233
|
+
// @ts-expect-error: Deno types not installed.
|
|
234
|
+
Deno.serve({ port, fetch: app.build() });
|
|
235
|
+
if (cb) setTimeout(cb, 0);
|
|
236
|
+
}
|
|
237
|
+
return app;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
decorate: (...args: any[]) => {
|
|
241
|
+
const obj: Record<string, any> =
|
|
242
|
+
args.length === 2 ? { [args[0]]: args[1] } : args[0];
|
|
243
|
+
|
|
244
|
+
hooks.onTransform.push({
|
|
245
|
+
id: nextHookId(appId),
|
|
246
|
+
applyTo: "local",
|
|
247
|
+
callback: () => obj,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return app;
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
onGlobalRequest(callback: any) {
|
|
254
|
+
hooks.onGlobalRequest.push({
|
|
255
|
+
id: nextHookId(appId),
|
|
256
|
+
applyTo: "global",
|
|
257
|
+
callback,
|
|
258
|
+
});
|
|
259
|
+
return app;
|
|
260
|
+
},
|
|
261
|
+
onTransform(callback: any) {
|
|
262
|
+
hooks.onTransform.push({
|
|
263
|
+
id: nextHookId(appId),
|
|
264
|
+
applyTo: "local",
|
|
265
|
+
callback,
|
|
266
|
+
});
|
|
267
|
+
return app;
|
|
268
|
+
},
|
|
269
|
+
onBeforeHandle(callback: any) {
|
|
270
|
+
hooks.onBeforeHandle.push({
|
|
271
|
+
id: nextHookId(appId),
|
|
272
|
+
applyTo: "local",
|
|
273
|
+
callback,
|
|
274
|
+
});
|
|
275
|
+
return app;
|
|
276
|
+
},
|
|
277
|
+
onAfterHandle(callback: any) {
|
|
278
|
+
hooks.onAfterHandle.push({
|
|
279
|
+
id: nextHookId(appId),
|
|
280
|
+
applyTo: "local",
|
|
281
|
+
callback,
|
|
282
|
+
});
|
|
283
|
+
return app;
|
|
284
|
+
},
|
|
285
|
+
onMapResponse(callback: any) {
|
|
286
|
+
hooks.onMapResponse.push({
|
|
287
|
+
id: nextHookId(appId),
|
|
288
|
+
applyTo: "local",
|
|
289
|
+
callback,
|
|
290
|
+
});
|
|
291
|
+
return app;
|
|
292
|
+
},
|
|
293
|
+
onGlobalError(callback: any) {
|
|
294
|
+
hooks.onGlobalError.push({
|
|
295
|
+
id: nextHookId(appId),
|
|
296
|
+
applyTo: "global",
|
|
297
|
+
callback,
|
|
298
|
+
});
|
|
299
|
+
return app;
|
|
300
|
+
},
|
|
301
|
+
onGlobalAfterResponse(callback: any) {
|
|
302
|
+
hooks.onGlobalAfterResponse.push({
|
|
303
|
+
id: nextHookId(appId),
|
|
304
|
+
applyTo: "global",
|
|
305
|
+
callback,
|
|
306
|
+
});
|
|
307
|
+
return app;
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
get: (...args: any[]) =>
|
|
311
|
+
app.method.apply(app, [Method.Get, ...args] as any) as any,
|
|
312
|
+
post: (...args: any[]) =>
|
|
313
|
+
app.method.apply(app, [Method.Post, ...args] as any) as any,
|
|
314
|
+
put: (...args: any[]) =>
|
|
315
|
+
app.method.apply(app, [Method.Put, ...args] as any) as any,
|
|
316
|
+
delete: (...args: any[]) =>
|
|
317
|
+
app.method.apply(app, [Method.Delete, ...args] as any) as any,
|
|
318
|
+
any: (...args: any[]) =>
|
|
319
|
+
app.method.apply(app, [Method.Any, ...args] as any) as any,
|
|
320
|
+
|
|
321
|
+
method(method: string, path: BasePath, ...args: any[]) {
|
|
322
|
+
const def: RouteDef = args.length === 2 ? args[0] : undefined;
|
|
323
|
+
const handler = args[1] ?? args[0];
|
|
324
|
+
const route = `${prefix}${path}`;
|
|
325
|
+
addRoutesEntry(method, route, {
|
|
326
|
+
def,
|
|
327
|
+
handler,
|
|
328
|
+
route,
|
|
329
|
+
hooks: cloneHooks(),
|
|
330
|
+
});
|
|
331
|
+
return app;
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
mount(...args: any[]) {
|
|
335
|
+
let path = "";
|
|
336
|
+
let def = {};
|
|
337
|
+
let fetch: ServerSideFetch;
|
|
338
|
+
|
|
339
|
+
if (args.length === 1) {
|
|
340
|
+
fetch = args[0];
|
|
341
|
+
} else if (args.length === 2) {
|
|
342
|
+
path = args[0];
|
|
343
|
+
fetch = args[1];
|
|
344
|
+
} else {
|
|
345
|
+
path = args[0];
|
|
346
|
+
def = args[1];
|
|
347
|
+
fetch = args[2];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const route = `${prefix}${path}/**`;
|
|
351
|
+
addRoutesEntry(Method.Any, route, {
|
|
352
|
+
def,
|
|
353
|
+
fetch,
|
|
354
|
+
route,
|
|
355
|
+
hooks: cloneHooks(),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return app as any;
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
use: (childApp) => {
|
|
362
|
+
// Bring in routes
|
|
363
|
+
for (const [method, methodValue] of Object.entries(
|
|
364
|
+
childApp["~zeta"].routes,
|
|
365
|
+
)) {
|
|
366
|
+
for (const [subRoute, routeValue] of Object.entries(methodValue)) {
|
|
367
|
+
const route = `${prefix}${subRoute}`;
|
|
368
|
+
addRoutesEntry(method, route, { ...routeValue, route });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Add global hooks to parent app's hooks
|
|
373
|
+
for (const _name of Object.keys(hooks)) {
|
|
374
|
+
const name = _name as keyof LifeCycleHooks;
|
|
375
|
+
for (const hook of childApp["~zeta"].hooks[name]) {
|
|
376
|
+
if (hook.applyTo === "global" || childApp["~zeta"].exported) {
|
|
377
|
+
hooks[name].push(hook as LifeCycleHook<any>);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
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
|
+
}
|
|
387
|
+
|
|
388
|
+
return app as any;
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return app as any;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Configure how the app is created.
|
|
397
|
+
*/
|
|
398
|
+
export type CreateAppOptions<TPrefix extends BasePrefix = ""> = {
|
|
399
|
+
/**
|
|
400
|
+
* The origin to use when constructing URLs.
|
|
401
|
+
* @default "http://localhost"
|
|
402
|
+
*/
|
|
403
|
+
origin?: string;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Add a prefix to the beginning of all routes in the app.
|
|
407
|
+
*/
|
|
408
|
+
prefix?: TPrefix;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Tell Zeta which library you're using for validation. OpenAPI docs cannot
|
|
412
|
+
* be served without a schema adapter.
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```ts
|
|
416
|
+
* import { zodSchemaAdapter } from "@aklinker1/zeta/adapters/zod-schema-adapter"
|
|
417
|
+
*
|
|
418
|
+
* const app = createApp({
|
|
419
|
+
* openApi: {
|
|
420
|
+
* schemaAdapter: zodSchemaAdapter,
|
|
421
|
+
* },
|
|
422
|
+
* });
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
schemaAdapter?: SchemaAdapter;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Where the OpenAPI JSON docs is hosted.
|
|
429
|
+
* @default "/openapi.json"
|
|
430
|
+
*/
|
|
431
|
+
openApiRoute?: BasePath;
|
|
432
|
+
/**
|
|
433
|
+
* Where the Scalar UI is hosted.
|
|
434
|
+
* @default "/scalar"
|
|
435
|
+
*/
|
|
436
|
+
scalarRoute?: BasePath;
|
|
437
|
+
|
|
438
|
+
/** Configure how your application's OpenAPI docs are generated. */
|
|
439
|
+
openApi?: Partial<OpenAPIV3_1.Document> & {};
|
|
440
|
+
/**
|
|
441
|
+
* Configure [Scalar](https://scalar.com/) UI docs.
|
|
442
|
+
* @see https://github.com/scalar/scalar/blob/main/documentation/configuration.md#list-of-all-attributes
|
|
443
|
+
*/
|
|
444
|
+
scalar?: any;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
enum Method {
|
|
448
|
+
Get = "GET",
|
|
449
|
+
Post = "POST",
|
|
450
|
+
Put = "PUT",
|
|
451
|
+
Delete = "DELETE",
|
|
452
|
+
Any = "ANY",
|
|
453
|
+
}
|