@vertz/core 0.2.1 → 0.2.3
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/dist/index.d.ts +37 -198
- package/dist/index.js +61 -178
- package/dist/internals.d.ts +1 -1
- package/dist/internals.js +1 -1
- package/dist/shared/{chunk-k596zpc6.js → chunk-m3w3ytn5.js} +18 -5
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -39,172 +39,6 @@ declare function createMiddleware<
|
|
|
39
39
|
TRequires extends Record<string, unknown> = Record<string, unknown>,
|
|
40
40
|
TProvides extends Record<string, unknown> = Record<string, unknown>
|
|
41
41
|
>(def: NamedMiddlewareDef<TRequires, TProvides>): NamedMiddlewareDef<TRequires, TProvides>;
|
|
42
|
-
import { Schema as Schema2 } from "@vertz/schema";
|
|
43
|
-
interface ModuleDef<
|
|
44
|
-
TImports extends Record<string, unknown> = Record<string, unknown>,
|
|
45
|
-
TOptions extends Record<string, unknown> = Record<string, unknown>
|
|
46
|
-
> {
|
|
47
|
-
name: string;
|
|
48
|
-
imports?: TImports;
|
|
49
|
-
options?: Schema2<TOptions>;
|
|
50
|
-
}
|
|
51
|
-
interface ServiceDef<
|
|
52
|
-
TDeps = unknown,
|
|
53
|
-
TState = unknown,
|
|
54
|
-
TMethods = unknown,
|
|
55
|
-
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
56
|
-
TEnv extends Record<string, unknown> = Record<string, unknown>
|
|
57
|
-
> {
|
|
58
|
-
inject?: Record<string, unknown>;
|
|
59
|
-
options?: Schema2<TOptions>;
|
|
60
|
-
env?: Schema2<TEnv>;
|
|
61
|
-
onInit?: (deps: TDeps, opts: TOptions, env: TEnv) => Promise<TState> | TState;
|
|
62
|
-
methods: (deps: TDeps, state: TState, opts: TOptions, env: TEnv) => TMethods;
|
|
63
|
-
onDestroy?: (deps: TDeps, state: TState) => Promise<void> | void;
|
|
64
|
-
}
|
|
65
|
-
interface RouterDef<TInject extends Record<string, unknown> = Record<string, unknown>> {
|
|
66
|
-
prefix: string;
|
|
67
|
-
inject?: TInject;
|
|
68
|
-
}
|
|
69
|
-
interface Module<TDef extends ModuleDef = ModuleDef> {
|
|
70
|
-
definition: TDef;
|
|
71
|
-
services: ServiceDef[];
|
|
72
|
-
routers: RouterDef[];
|
|
73
|
-
exports: ServiceDef[];
|
|
74
|
-
}
|
|
75
|
-
type Primitive = string | number | boolean | bigint | symbol | undefined | null;
|
|
76
|
-
type BuiltinObject = Date | RegExp | Error | Map<unknown, unknown> | Set<unknown> | WeakMap<object, unknown> | WeakSet<object> | Promise<unknown> | Request | Response | Headers | ReadableStream | WritableStream;
|
|
77
|
-
type DeepReadonly<T> = unknown extends T ? T : T extends Primitive ? T : T extends BuiltinObject ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : { readonly [K in keyof T] : DeepReadonly<T[K]> };
|
|
78
|
-
interface RawRequest {
|
|
79
|
-
readonly request: Request;
|
|
80
|
-
readonly method: string;
|
|
81
|
-
readonly url: string;
|
|
82
|
-
readonly headers: Headers;
|
|
83
|
-
}
|
|
84
|
-
interface HandlerCtx {
|
|
85
|
-
params: Record<string, unknown>;
|
|
86
|
-
body: unknown;
|
|
87
|
-
query: Record<string, string>;
|
|
88
|
-
headers: Record<string, unknown>;
|
|
89
|
-
raw: RawRequest;
|
|
90
|
-
options: Record<string, unknown>;
|
|
91
|
-
env: Record<string, unknown>;
|
|
92
|
-
[key: string]: unknown;
|
|
93
|
-
}
|
|
94
|
-
type Deps<T extends Record<string, unknown>> = DeepReadonly<T>;
|
|
95
|
-
type Ctx<T extends Record<string, unknown>> = DeepReadonly<T>;
|
|
96
|
-
interface NamedServiceDef<
|
|
97
|
-
TDeps = unknown,
|
|
98
|
-
TState = unknown,
|
|
99
|
-
TMethods = unknown,
|
|
100
|
-
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
101
|
-
TEnv extends Record<string, unknown> = Record<string, unknown>
|
|
102
|
-
> extends ServiceDef<TDeps, TState, TMethods, TOptions, TEnv> {
|
|
103
|
-
moduleName: string;
|
|
104
|
-
}
|
|
105
|
-
type InferOutput<
|
|
106
|
-
T,
|
|
107
|
-
TDefault = unknown
|
|
108
|
-
> = T extends {
|
|
109
|
-
_output: infer O;
|
|
110
|
-
} ? O : T extends {
|
|
111
|
-
parse(v: unknown): infer P;
|
|
112
|
-
} ? P : TDefault;
|
|
113
|
-
type TypedHandlerCtx<
|
|
114
|
-
TParams = unknown,
|
|
115
|
-
TQuery = unknown,
|
|
116
|
-
THeaders = unknown,
|
|
117
|
-
TBody = unknown,
|
|
118
|
-
TMiddleware extends Record<string, unknown> = Record<string, unknown>
|
|
119
|
-
> = Omit<HandlerCtx, "params" | "query" | "headers" | "body"> & {
|
|
120
|
-
params: TParams;
|
|
121
|
-
query: TQuery;
|
|
122
|
-
headers: THeaders;
|
|
123
|
-
body: TBody;
|
|
124
|
-
} & TMiddleware;
|
|
125
|
-
interface RouteConfig<
|
|
126
|
-
TParams = unknown,
|
|
127
|
-
TQuery = unknown,
|
|
128
|
-
THeaders = unknown,
|
|
129
|
-
TBody = unknown,
|
|
130
|
-
TMiddleware extends Record<string, unknown> = Record<string, unknown>
|
|
131
|
-
> {
|
|
132
|
-
params?: TParams;
|
|
133
|
-
body?: TBody;
|
|
134
|
-
query?: TQuery;
|
|
135
|
-
response?: unknown;
|
|
136
|
-
/** Error schemas for errors-as-values pattern. Keys are HTTP status codes. */
|
|
137
|
-
errors?: Record<number, unknown>;
|
|
138
|
-
headers?: THeaders;
|
|
139
|
-
middlewares?: unknown[];
|
|
140
|
-
handler: (ctx: TypedHandlerCtx<InferOutput<TParams>, InferOutput<TQuery, Record<string, string>>, InferOutput<THeaders>, InferOutput<TBody>, TMiddleware>) => unknown;
|
|
141
|
-
}
|
|
142
|
-
interface Route {
|
|
143
|
-
method: string;
|
|
144
|
-
path: string;
|
|
145
|
-
config: RouteConfig<unknown, unknown, unknown, unknown, Record<string, unknown>>;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Extracts the TMethods type from a NamedServiceDef or ServiceDef.
|
|
149
|
-
* Returns `unknown` for non-service types.
|
|
150
|
-
*/
|
|
151
|
-
type ExtractMethods<T> = T extends NamedServiceDef<any, any, infer M> ? M : T extends ServiceDef<any, any, infer M> ? M : unknown;
|
|
152
|
-
/**
|
|
153
|
-
* Resolves an inject map by extracting TMethods from each NamedServiceDef value.
|
|
154
|
-
* Given `{ userService: NamedServiceDef<..., ..., UserMethods> }`,
|
|
155
|
-
* produces `{ userService: UserMethods }`.
|
|
156
|
-
*/
|
|
157
|
-
type ResolveInjectMap<T extends Record<string, unknown>> = { [K in keyof T] : ExtractMethods<T[K]> };
|
|
158
|
-
type HttpMethodFn<
|
|
159
|
-
TMiddleware extends Record<string, unknown> = Record<string, unknown>,
|
|
160
|
-
TInject extends Record<string, unknown> = Record<string, unknown>
|
|
161
|
-
> = <
|
|
162
|
-
TParams,
|
|
163
|
-
TQuery,
|
|
164
|
-
THeaders,
|
|
165
|
-
TBody
|
|
166
|
-
>(path: `/${string}`, config: RouteConfig<TParams, TQuery, THeaders, TBody, TMiddleware & ResolveInjectMap<TInject>>) => NamedRouterDef<TMiddleware, TInject>;
|
|
167
|
-
interface NamedRouterDef<
|
|
168
|
-
TMiddleware extends Record<string, unknown> = Record<string, unknown>,
|
|
169
|
-
TInject extends Record<string, unknown> = Record<string, unknown>
|
|
170
|
-
> extends RouterDef<TInject> {
|
|
171
|
-
moduleName: string;
|
|
172
|
-
routes: Route[];
|
|
173
|
-
get: HttpMethodFn<TMiddleware, TInject>;
|
|
174
|
-
post: HttpMethodFn<TMiddleware, TInject>;
|
|
175
|
-
put: HttpMethodFn<TMiddleware, TInject>;
|
|
176
|
-
patch: HttpMethodFn<TMiddleware, TInject>;
|
|
177
|
-
delete: HttpMethodFn<TMiddleware, TInject>;
|
|
178
|
-
head: HttpMethodFn<TMiddleware, TInject>;
|
|
179
|
-
}
|
|
180
|
-
interface NamedModuleDef<
|
|
181
|
-
TImports extends Record<string, unknown> = Record<string, unknown>,
|
|
182
|
-
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
183
|
-
TMiddleware extends Record<string, unknown> = Record<string, unknown>
|
|
184
|
-
> extends ModuleDef<TImports, TOptions> {
|
|
185
|
-
service: <
|
|
186
|
-
TDeps,
|
|
187
|
-
TState,
|
|
188
|
-
TMethods
|
|
189
|
-
>(config: ServiceDef<TDeps, TState, TMethods>) => NamedServiceDef<TDeps, TState, TMethods>;
|
|
190
|
-
router: <TInject extends Record<string, unknown> = Record<string, unknown>>(config: RouterDef<TInject>) => NamedRouterDef<TMiddleware, TInject>;
|
|
191
|
-
}
|
|
192
|
-
declare function createModuleDef<
|
|
193
|
-
TImports extends Record<string, unknown> = Record<string, unknown>,
|
|
194
|
-
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
195
|
-
TMiddleware extends Record<string, unknown> = Record<string, unknown>
|
|
196
|
-
>(config: ModuleDef<TImports, TOptions>): NamedModuleDef<TImports, TOptions, TMiddleware>;
|
|
197
|
-
interface NamedModule {
|
|
198
|
-
definition: NamedModuleDef;
|
|
199
|
-
services: NamedServiceDef[];
|
|
200
|
-
routers: NamedRouterDef<any, any>[];
|
|
201
|
-
exports: NamedServiceDef[];
|
|
202
|
-
}
|
|
203
|
-
declare function createModule(definition: NamedModuleDef, config: {
|
|
204
|
-
services: NamedServiceDef[];
|
|
205
|
-
routers: NamedRouterDef<any, any>[];
|
|
206
|
-
exports: NamedServiceDef[];
|
|
207
|
-
}): NamedModule;
|
|
208
42
|
interface CorsConfig {
|
|
209
43
|
origins?: string | string[] | boolean;
|
|
210
44
|
methods?: string[];
|
|
@@ -223,6 +57,13 @@ interface EntityDefinition {
|
|
|
223
57
|
readonly actions: Record<string, unknown>;
|
|
224
58
|
readonly relations: Record<string, unknown>;
|
|
225
59
|
}
|
|
60
|
+
type SchemaLike = {
|
|
61
|
+
parse(value: unknown): {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
data?: unknown;
|
|
64
|
+
error?: unknown;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
226
67
|
/**
|
|
227
68
|
* An entity route entry generated by @vertz/server's route generator.
|
|
228
69
|
* Core doesn't know about entity internals — it just registers these as handlers.
|
|
@@ -230,7 +71,13 @@ interface EntityDefinition {
|
|
|
230
71
|
interface EntityRouteEntry {
|
|
231
72
|
method: string;
|
|
232
73
|
path: string;
|
|
233
|
-
handler: (ctx: Record<string, unknown>) =>
|
|
74
|
+
handler: (ctx: Record<string, unknown>) => unknown;
|
|
75
|
+
paramsSchema?: SchemaLike;
|
|
76
|
+
bodySchema?: SchemaLike;
|
|
77
|
+
querySchema?: SchemaLike;
|
|
78
|
+
headersSchema?: SchemaLike;
|
|
79
|
+
responseSchema?: SchemaLike;
|
|
80
|
+
errorsSchema?: Record<number, SchemaLike>;
|
|
234
81
|
}
|
|
235
82
|
interface AppConfig {
|
|
236
83
|
basePath?: string;
|
|
@@ -263,8 +110,7 @@ interface RouteInfo {
|
|
|
263
110
|
method: string;
|
|
264
111
|
path: string;
|
|
265
112
|
}
|
|
266
|
-
interface AppBuilder<
|
|
267
|
-
register(module: NamedModule, options?: Record<string, unknown>): AppBuilder<TMiddlewareCtx>;
|
|
113
|
+
interface AppBuilder<_TMiddlewareCtx extends Record<string, unknown> = Record<string, unknown>> {
|
|
268
114
|
middlewares<const M extends readonly NamedMiddlewareDef<any, any>[]>(list: M): AppBuilder<AccumulateProvides<M>>;
|
|
269
115
|
readonly handler: (request: Request) => Promise<Response>;
|
|
270
116
|
listen(port?: number, options?: ListenOptions): Promise<ServerHandle>;
|
|
@@ -274,36 +120,31 @@ interface AppBuilder<TMiddlewareCtx extends Record<string, unknown> = Record<str
|
|
|
274
120
|
};
|
|
275
121
|
}
|
|
276
122
|
declare function createApp(config: AppConfig): AppBuilder;
|
|
277
|
-
type
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
type: "service";
|
|
286
|
-
id: string;
|
|
287
|
-
deps: string[];
|
|
288
|
-
factory: ServiceFactory;
|
|
289
|
-
options?: Record<string, unknown>;
|
|
290
|
-
env?: Record<string, unknown>;
|
|
291
|
-
}
|
|
292
|
-
interface ModuleBootInstruction {
|
|
293
|
-
type: "module";
|
|
294
|
-
id: string;
|
|
295
|
-
services: string[];
|
|
296
|
-
options?: Record<string, unknown>;
|
|
123
|
+
type Primitive = string | number | boolean | bigint | symbol | undefined | null;
|
|
124
|
+
type BuiltinObject = Date | RegExp | Error | Map<unknown, unknown> | Set<unknown> | WeakMap<object, unknown> | WeakSet<object> | Promise<unknown> | Request | Response | Headers | ReadableStream | WritableStream;
|
|
125
|
+
type DeepReadonly<T> = unknown extends T ? T : T extends Primitive ? T : T extends BuiltinObject ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : { readonly [K in keyof T] : DeepReadonly<T[K]> };
|
|
126
|
+
interface RawRequest {
|
|
127
|
+
readonly request: Request;
|
|
128
|
+
readonly method: string;
|
|
129
|
+
readonly url: string;
|
|
130
|
+
readonly headers: Headers;
|
|
297
131
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
132
|
+
interface HandlerCtx {
|
|
133
|
+
params: Record<string, unknown>;
|
|
134
|
+
body: unknown;
|
|
135
|
+
query: Record<string, string>;
|
|
136
|
+
headers: Record<string, unknown>;
|
|
137
|
+
raw: RawRequest;
|
|
138
|
+
options: Record<string, unknown>;
|
|
139
|
+
env: Record<string, unknown>;
|
|
140
|
+
[key: string]: unknown;
|
|
302
141
|
}
|
|
303
|
-
|
|
142
|
+
type Deps<T extends Record<string, unknown>> = DeepReadonly<T>;
|
|
143
|
+
type Ctx<T extends Record<string, unknown>> = DeepReadonly<T>;
|
|
144
|
+
import { Schema as Schema2 } from "@vertz/schema";
|
|
304
145
|
interface EnvConfig<T = unknown> {
|
|
305
146
|
load?: string[];
|
|
306
|
-
schema:
|
|
147
|
+
schema: Schema2<T>;
|
|
307
148
|
env?: Record<string, string | undefined>;
|
|
308
149
|
}
|
|
309
150
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
@@ -370,8 +211,6 @@ declare function makeImmutable<T extends object>(obj: T, contextName: string): D
|
|
|
370
211
|
declare const vertz: {
|
|
371
212
|
readonly env: typeof createEnv;
|
|
372
213
|
readonly middleware: typeof createMiddleware;
|
|
373
|
-
readonly moduleDef: typeof createModuleDef;
|
|
374
|
-
readonly module: typeof createModule;
|
|
375
214
|
readonly app: typeof createApp;
|
|
376
215
|
/** @since 0.2.0 — preferred alias for `app` */
|
|
377
216
|
readonly server: typeof createApp;
|
|
@@ -385,4 +224,4 @@ declare const createServer: (config: AppConfig) => AppBuilder;
|
|
|
385
224
|
* @deprecated Use `createServer` instead. `createApp` will be removed in v0.3.0.
|
|
386
225
|
*/
|
|
387
226
|
declare const createApp2: (config: AppConfig) => AppBuilder;
|
|
388
|
-
export { vertz, makeImmutable, deepFreeze, createServer,
|
|
227
|
+
export { vertz, makeImmutable, deepFreeze, createServer, createMiddleware, createImmutableProxy, createEnv, createApp2 as createApp, VertzException, ValidationException, UnauthorizedException, ServiceUnavailableException, ServerHandle, ServerAdapter, RouteInfo, RawRequest, NotFoundException, NamedMiddlewareDef, MiddlewareDef, ListenOptions, InternalServerErrorException, Infer2 as InferSchema, Infer, HttpStatusCode, HttpMethod, HandlerCtx, ForbiddenException, EnvConfig, EntityRouteEntry, Deps, DeepReadonly, Ctx, CorsConfig, ConflictException, BadRequestException, AppConfig, AppBuilder, AccumulateProvides };
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
parseBody,
|
|
19
19
|
parseRequest,
|
|
20
20
|
runMiddlewareChain
|
|
21
|
-
} from "./shared/chunk-
|
|
21
|
+
} from "./shared/chunk-m3w3ytn5.js";
|
|
22
22
|
|
|
23
23
|
// src/result.ts
|
|
24
24
|
var RESULT_BRAND = Symbol.for("vertz.result");
|
|
@@ -49,27 +49,34 @@ function resolveOrigin(config, requestOrigin) {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
function handleCors(config, request) {
|
|
52
|
+
if (config.credentials && (config.origins === true || config.origins === "*")) {
|
|
53
|
+
throw new Error("CORS misconfiguration: credentials cannot be used with wildcard origins. " + "Browsers will reject the response. Use an explicit origin allowlist instead.");
|
|
54
|
+
}
|
|
52
55
|
const requestOrigin = request.headers.get("origin");
|
|
53
56
|
const origin = resolveOrigin(config, requestOrigin);
|
|
54
57
|
if (request.method === "OPTIONS") {
|
|
55
58
|
const headers = {};
|
|
56
|
-
if (origin)
|
|
59
|
+
if (origin) {
|
|
57
60
|
headers["access-control-allow-origin"] = origin;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
const methods = config.methods ?? DEFAULT_METHODS;
|
|
62
|
+
headers["access-control-allow-methods"] = methods.join(", ");
|
|
63
|
+
const allowHeaders = config.headers ?? DEFAULT_HEADERS;
|
|
64
|
+
headers["access-control-allow-headers"] = allowHeaders.join(", ");
|
|
65
|
+
if (config.maxAge !== undefined) {
|
|
66
|
+
headers["access-control-max-age"] = String(config.maxAge);
|
|
67
|
+
}
|
|
68
|
+
if (config.credentials) {
|
|
69
|
+
headers["access-control-allow-credentials"] = "true";
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
return new Response(null, { status: 204, headers });
|
|
69
73
|
}
|
|
70
74
|
return null;
|
|
71
75
|
}
|
|
72
76
|
function applyCorsHeaders(config, request, response) {
|
|
77
|
+
if (config.credentials && (config.origins === true || config.origins === "*")) {
|
|
78
|
+
throw new Error("CORS misconfiguration: credentials cannot be used with wildcard origins. " + "Browsers will reject the response. Use an explicit origin allowlist instead.");
|
|
79
|
+
}
|
|
73
80
|
const requestOrigin = request.headers.get("origin");
|
|
74
81
|
const origin = resolveOrigin(config, requestOrigin);
|
|
75
82
|
if (!origin)
|
|
@@ -97,35 +104,41 @@ function createResponseWithCors(data, status, config, request) {
|
|
|
97
104
|
}
|
|
98
105
|
return response;
|
|
99
106
|
}
|
|
107
|
+
var INTERNAL_DETAIL_PATTERNS = [
|
|
108
|
+
/(?:^|[\s:])\/[\w./-]+/,
|
|
109
|
+
/[A-Z]:\\[\w\\.-]+/,
|
|
110
|
+
/node_modules/,
|
|
111
|
+
/at\s+\w+\s+\(/,
|
|
112
|
+
/\.ts:\d+:\d+/,
|
|
113
|
+
/\.js:\d+:\d+/
|
|
114
|
+
];
|
|
115
|
+
function sanitizeValidationMessage(error, label) {
|
|
116
|
+
if (error != null && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
|
|
117
|
+
const issues = error.issues;
|
|
118
|
+
const messages = issues.map((issue) => typeof issue.message === "string" ? issue.message : "").filter(Boolean);
|
|
119
|
+
if (messages.length > 0) {
|
|
120
|
+
return messages.join(", ");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const message = error instanceof Error ? error.message : "";
|
|
124
|
+
if (!message) {
|
|
125
|
+
return `Invalid ${label}`;
|
|
126
|
+
}
|
|
127
|
+
for (const pattern of INTERNAL_DETAIL_PATTERNS) {
|
|
128
|
+
if (pattern.test(message)) {
|
|
129
|
+
return `Invalid ${label}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return message;
|
|
133
|
+
}
|
|
100
134
|
function validateSchema(schema, value, label) {
|
|
101
135
|
const result = schema.parse(value);
|
|
102
136
|
if (!result.ok) {
|
|
103
|
-
const message = result.error
|
|
137
|
+
const message = sanitizeValidationMessage(result.error, label);
|
|
104
138
|
throw new BadRequestException(message);
|
|
105
139
|
}
|
|
106
140
|
return result.data;
|
|
107
141
|
}
|
|
108
|
-
function resolveServices(registrations) {
|
|
109
|
-
const serviceMap = new Map;
|
|
110
|
-
for (const { module, options } of registrations) {
|
|
111
|
-
for (const service of module.services) {
|
|
112
|
-
if (!serviceMap.has(service)) {
|
|
113
|
-
let parsedOptions = {};
|
|
114
|
-
if (service.options && options) {
|
|
115
|
-
const parsed = service.options.safeParse(options);
|
|
116
|
-
if (parsed.ok) {
|
|
117
|
-
parsedOptions = parsed.data;
|
|
118
|
-
} else {
|
|
119
|
-
throw new Error(`Invalid options for service ${service.moduleName}: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const env = {};
|
|
123
|
-
serviceMap.set(service, service.methods({}, undefined, parsedOptions, env));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return serviceMap;
|
|
128
|
-
}
|
|
129
142
|
function resolveMiddlewares(globalMiddlewares) {
|
|
130
143
|
return globalMiddlewares.map((mw) => ({
|
|
131
144
|
name: mw.name,
|
|
@@ -133,58 +146,22 @@ function resolveMiddlewares(globalMiddlewares) {
|
|
|
133
146
|
resolvedInject: {}
|
|
134
147
|
}));
|
|
135
148
|
}
|
|
136
|
-
function
|
|
137
|
-
if (!inject)
|
|
138
|
-
return {};
|
|
139
|
-
const resolved = {};
|
|
140
|
-
for (const [name, serviceDef] of Object.entries(inject)) {
|
|
141
|
-
const methods = serviceMap.get(serviceDef);
|
|
142
|
-
if (methods)
|
|
143
|
-
resolved[name] = methods;
|
|
144
|
-
}
|
|
145
|
-
return resolved;
|
|
146
|
-
}
|
|
147
|
-
function registerRoutes(trie, basePath, registrations, serviceMap) {
|
|
148
|
-
for (const { module, options } of registrations) {
|
|
149
|
-
for (const router of module.routers) {
|
|
150
|
-
const resolvedServices = resolveRouterServices(router.inject, serviceMap);
|
|
151
|
-
for (const route of router.routes) {
|
|
152
|
-
const fullPath = basePath + router.prefix + route.path;
|
|
153
|
-
const routeMiddlewares = (route.config.middlewares ?? []).map((mw) => ({
|
|
154
|
-
name: mw.name,
|
|
155
|
-
handler: mw.handler,
|
|
156
|
-
resolvedInject: {}
|
|
157
|
-
}));
|
|
158
|
-
const entry = {
|
|
159
|
-
handler: route.config.handler,
|
|
160
|
-
options: options ?? {},
|
|
161
|
-
services: resolvedServices,
|
|
162
|
-
middlewares: routeMiddlewares,
|
|
163
|
-
paramsSchema: route.config.params,
|
|
164
|
-
bodySchema: route.config.body,
|
|
165
|
-
querySchema: route.config.query,
|
|
166
|
-
headersSchema: route.config.headers,
|
|
167
|
-
responseSchema: route.config.response,
|
|
168
|
-
errorsSchema: route.config.errors
|
|
169
|
-
};
|
|
170
|
-
trie.add(route.method, fullPath, entry);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function buildHandler(config, registrations, globalMiddlewares) {
|
|
149
|
+
function buildHandler(config, globalMiddlewares) {
|
|
176
150
|
const trie = new Trie;
|
|
177
|
-
const basePath = config.basePath ?? "";
|
|
178
151
|
const resolvedMiddlewares = resolveMiddlewares(globalMiddlewares);
|
|
179
|
-
const serviceMap = resolveServices(registrations);
|
|
180
|
-
registerRoutes(trie, basePath, registrations, serviceMap);
|
|
181
152
|
if (config._entityRoutes) {
|
|
182
153
|
for (const route of config._entityRoutes) {
|
|
183
154
|
const entry = {
|
|
184
155
|
handler: route.handler,
|
|
185
156
|
options: {},
|
|
186
157
|
services: {},
|
|
187
|
-
middlewares: []
|
|
158
|
+
middlewares: [],
|
|
159
|
+
paramsSchema: route.paramsSchema,
|
|
160
|
+
bodySchema: route.bodySchema,
|
|
161
|
+
querySchema: route.querySchema,
|
|
162
|
+
headersSchema: route.headersSchema,
|
|
163
|
+
responseSchema: route.responseSchema,
|
|
164
|
+
errorsSchema: route.errorsSchema
|
|
188
165
|
};
|
|
189
166
|
trie.add(route.method, route.path, entry);
|
|
190
167
|
}
|
|
@@ -224,7 +201,11 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
224
201
|
if (entry.middlewares.length > 0) {
|
|
225
202
|
const routeCtx = { ...requestCtx, ...middlewareState };
|
|
226
203
|
const routeState = await runMiddlewareChain(entry.middlewares, routeCtx);
|
|
227
|
-
Object.
|
|
204
|
+
for (const key of Object.keys(routeState)) {
|
|
205
|
+
if (key !== "__proto__" && key !== "constructor" && key !== "prototype") {
|
|
206
|
+
middlewareState[key] = routeState[key];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
228
209
|
}
|
|
229
210
|
const validatedParams = entry.paramsSchema ? validateSchema(entry.paramsSchema, match.params, "params") : match.params;
|
|
230
211
|
const validatedBody = entry.bodySchema ? validateSchema(entry.bodySchema, body, "body") : body;
|
|
@@ -320,27 +301,6 @@ function detectAdapter(hints) {
|
|
|
320
301
|
}
|
|
321
302
|
|
|
322
303
|
// src/app/route-log.ts
|
|
323
|
-
function normalizePath(path) {
|
|
324
|
-
let normalized = path.replace(/\/+/g, "/");
|
|
325
|
-
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
326
|
-
normalized = normalized.slice(0, -1);
|
|
327
|
-
}
|
|
328
|
-
return normalized || "/";
|
|
329
|
-
}
|
|
330
|
-
function collectRoutes(basePath, registrations) {
|
|
331
|
-
const routes = [];
|
|
332
|
-
for (const { module } of registrations) {
|
|
333
|
-
for (const router of module.routers) {
|
|
334
|
-
for (const route of router.routes) {
|
|
335
|
-
routes.push({
|
|
336
|
-
method: route.method,
|
|
337
|
-
path: normalizePath(basePath + router.prefix + route.path)
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return routes;
|
|
343
|
-
}
|
|
344
304
|
function formatRouteLog(listenUrl, routes) {
|
|
345
305
|
const header = `vertz server listening on ${listenUrl}`;
|
|
346
306
|
if (routes.length === 0) {
|
|
@@ -360,29 +320,18 @@ function formatRouteLog(listenUrl, routes) {
|
|
|
360
320
|
// src/app/app-builder.ts
|
|
361
321
|
var DEFAULT_PORT = 3000;
|
|
362
322
|
function createApp(config) {
|
|
363
|
-
const registrations = [];
|
|
364
323
|
let globalMiddlewares = [];
|
|
365
324
|
let cachedHandler = null;
|
|
366
325
|
const registeredRoutes = [];
|
|
367
326
|
const entityRoutes = [];
|
|
368
327
|
const builder = {
|
|
369
|
-
register(module, options) {
|
|
370
|
-
registrations.push({ module, options });
|
|
371
|
-
for (const router of module.routers) {
|
|
372
|
-
for (const route of router.routes) {
|
|
373
|
-
registeredRoutes.push({ method: route.method, path: router.prefix + route.path });
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
cachedHandler = null;
|
|
377
|
-
return builder;
|
|
378
|
-
},
|
|
379
328
|
middlewares(list) {
|
|
380
329
|
globalMiddlewares = [...list];
|
|
381
330
|
return builder;
|
|
382
331
|
},
|
|
383
332
|
get handler() {
|
|
384
333
|
if (!cachedHandler) {
|
|
385
|
-
cachedHandler = buildHandler(config,
|
|
334
|
+
cachedHandler = buildHandler(config, globalMiddlewares);
|
|
386
335
|
}
|
|
387
336
|
return cachedHandler;
|
|
388
337
|
},
|
|
@@ -393,10 +342,8 @@ function createApp(config) {
|
|
|
393
342
|
const adapter = detectAdapter();
|
|
394
343
|
const serverHandle = await adapter.listen(port ?? DEFAULT_PORT, builder.handler, options);
|
|
395
344
|
if (options?.logRoutes !== false) {
|
|
396
|
-
const moduleRoutes = collectRoutes(config.basePath ?? "", registrations);
|
|
397
|
-
const routes = [...moduleRoutes, ...entityRoutes];
|
|
398
345
|
const url = `http://${serverHandle.hostname}:${serverHandle.port}`;
|
|
399
|
-
console.log(formatRouteLog(url,
|
|
346
|
+
console.log(formatRouteLog(url, entityRoutes));
|
|
400
347
|
}
|
|
401
348
|
return serverHandle;
|
|
402
349
|
}
|
|
@@ -443,72 +390,10 @@ ${result.error.message}`);
|
|
|
443
390
|
function createMiddleware(def) {
|
|
444
391
|
return deepFreeze(def);
|
|
445
392
|
}
|
|
446
|
-
// src/module/module.ts
|
|
447
|
-
function validateOwnership(items, kind, expectedModule) {
|
|
448
|
-
for (const item of items) {
|
|
449
|
-
if (item.moduleName !== expectedModule) {
|
|
450
|
-
throw new Error(`${kind} belongs to module "${item.moduleName}", cannot add to module "${expectedModule}"`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
function createModule(definition, config) {
|
|
455
|
-
validateOwnership(config.services, "Service", definition.name);
|
|
456
|
-
validateOwnership(config.routers, "Router", definition.name);
|
|
457
|
-
for (const exp of config.exports) {
|
|
458
|
-
if (!config.services.includes(exp)) {
|
|
459
|
-
throw new Error("exports must be a subset of services");
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return deepFreeze({
|
|
463
|
-
definition,
|
|
464
|
-
...config
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
// src/module/router-def.ts
|
|
468
|
-
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head"];
|
|
469
|
-
function createRouterDef(moduleName, config) {
|
|
470
|
-
const routes = [];
|
|
471
|
-
function addRoute(method, path, routeConfig) {
|
|
472
|
-
if (!path.startsWith("/")) {
|
|
473
|
-
throw new Error(`Route path must start with '/', got '${path}'`);
|
|
474
|
-
}
|
|
475
|
-
routes.push({ method, path, config: routeConfig });
|
|
476
|
-
return router;
|
|
477
|
-
}
|
|
478
|
-
const router = {
|
|
479
|
-
...config,
|
|
480
|
-
moduleName,
|
|
481
|
-
routes
|
|
482
|
-
};
|
|
483
|
-
for (const method of HTTP_METHODS) {
|
|
484
|
-
router[method] = (path, cfg) => addRoute(method.toUpperCase(), path, cfg);
|
|
485
|
-
}
|
|
486
|
-
return router;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// src/module/service.ts
|
|
490
|
-
function createServiceDef(moduleName, config) {
|
|
491
|
-
return deepFreeze({
|
|
492
|
-
...config,
|
|
493
|
-
moduleName
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// src/module/module-def.ts
|
|
498
|
-
function createModuleDef(config) {
|
|
499
|
-
const def = {
|
|
500
|
-
...config,
|
|
501
|
-
service: (serviceConfig) => createServiceDef(config.name, serviceConfig),
|
|
502
|
-
router: (routerConfig) => createRouterDef(config.name, routerConfig)
|
|
503
|
-
};
|
|
504
|
-
return deepFreeze(def);
|
|
505
|
-
}
|
|
506
393
|
// src/vertz.ts
|
|
507
394
|
var vertz = /* @__PURE__ */ deepFreeze({
|
|
508
395
|
env: createEnv,
|
|
509
396
|
middleware: createMiddleware,
|
|
510
|
-
moduleDef: createModuleDef,
|
|
511
|
-
module: createModule,
|
|
512
397
|
app: createApp,
|
|
513
398
|
server: createApp
|
|
514
399
|
});
|
|
@@ -524,8 +409,6 @@ export {
|
|
|
524
409
|
makeImmutable,
|
|
525
410
|
deepFreeze,
|
|
526
411
|
createServer,
|
|
527
|
-
createModuleDef,
|
|
528
|
-
createModule,
|
|
529
412
|
createMiddleware,
|
|
530
413
|
createImmutableProxy,
|
|
531
414
|
createEnv,
|
package/dist/internals.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ interface ParsedRequest {
|
|
|
53
53
|
raw: Request;
|
|
54
54
|
}
|
|
55
55
|
declare function parseRequest(request: Request): ParsedRequest;
|
|
56
|
-
declare function parseBody(request: Request): Promise<unknown>;
|
|
56
|
+
declare function parseBody(request: Request, maxBodySize?: number): Promise<unknown>;
|
|
57
57
|
declare function createJsonResponse(data: unknown, status?: number, headers?: Record<string, string>): Response;
|
|
58
58
|
declare function createErrorResponse(error: unknown): Response;
|
|
59
59
|
export { runMiddlewareChain, parseRequest, parseBody, createJsonResponse, createErrorResponse, buildCtx, Trie, ResolvedMiddleware };
|
package/dist/internals.js
CHANGED
|
@@ -167,16 +167,24 @@ class ServiceUnavailableException extends VertzException {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
// src/middleware/middleware-runner.ts
|
|
170
|
+
var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
170
171
|
function isPlainObject(value) {
|
|
171
172
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
172
173
|
}
|
|
174
|
+
function safeAssign(target, source) {
|
|
175
|
+
for (const key of Object.keys(source)) {
|
|
176
|
+
if (!DANGEROUS_KEYS.has(key)) {
|
|
177
|
+
target[key] = source[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
173
181
|
async function runMiddlewareChain(middlewares, requestCtx) {
|
|
174
|
-
const accumulated =
|
|
182
|
+
const accumulated = Object.create(null);
|
|
175
183
|
for (const mw of middlewares) {
|
|
176
184
|
const ctx = { ...requestCtx, ...mw.resolvedInject, ...accumulated };
|
|
177
185
|
const contribution = await mw.handler(ctx);
|
|
178
186
|
if (isPlainObject(contribution)) {
|
|
179
|
-
|
|
187
|
+
safeAssign(accumulated, contribution);
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
return accumulated;
|
|
@@ -300,11 +308,16 @@ function parseRequest(request) {
|
|
|
300
308
|
method: request.method,
|
|
301
309
|
path: url.pathname,
|
|
302
310
|
query: Object.fromEntries(url.searchParams),
|
|
303
|
-
headers: Object.fromEntries(request.headers),
|
|
311
|
+
headers: Object.fromEntries([...request.headers].map(([k, v]) => [k.toLowerCase(), v])),
|
|
304
312
|
raw: request
|
|
305
313
|
};
|
|
306
314
|
}
|
|
307
|
-
|
|
315
|
+
var DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
316
|
+
async function parseBody(request, maxBodySize = DEFAULT_MAX_BODY_SIZE) {
|
|
317
|
+
const contentLength = parseInt(request.headers.get("content-length") ?? "0", 10);
|
|
318
|
+
if (maxBodySize && contentLength > maxBodySize) {
|
|
319
|
+
throw new BadRequestException("Request body too large");
|
|
320
|
+
}
|
|
308
321
|
const contentType = request.headers.get("content-type") ?? "";
|
|
309
322
|
if (contentType.includes("application/json")) {
|
|
310
323
|
try {
|
|
@@ -328,7 +341,7 @@ function createJsonResponse(data, status = 200, headers) {
|
|
|
328
341
|
return new Response(JSON.stringify(data), {
|
|
329
342
|
status,
|
|
330
343
|
headers: {
|
|
331
|
-
"content-type": "application/json",
|
|
344
|
+
"content-type": "application/json; charset=utf-8",
|
|
332
345
|
...headers
|
|
333
346
|
}
|
|
334
347
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz core framework primitives",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"typecheck": "tsc --noEmit"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@vertz/schema": "0.2.
|
|
39
|
+
"@vertz/schema": "0.2.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^25.3.1",
|
|
43
43
|
"@vitest/coverage-v8": "^4.0.18",
|
|
44
|
-
"bunup": "
|
|
44
|
+
"bunup": "^0.16.31",
|
|
45
45
|
"typescript": "^5.7.0",
|
|
46
46
|
"vitest": "^4.0.18"
|
|
47
47
|
},
|