@vertz/core 0.2.0 → 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/README.md +44 -0
- package/dist/index.d.ts +76 -322
- package/dist/index.js +123 -234
- package/dist/internals.d.ts +1 -1
- package/dist/internals.js +1 -1
- package/dist/shared/{chunk-c77pg5gx.js → chunk-m3w3ytn5.js} +39 -23
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -629,6 +629,50 @@ router.post('/send', {
|
|
|
629
629
|
});
|
|
630
630
|
```
|
|
631
631
|
|
|
632
|
+
### Environment Validation
|
|
633
|
+
|
|
634
|
+
`createEnv` validates environment variables against a schema at startup, returning a frozen, typed configuration object.
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
import { createEnv } from '@vertz/core';
|
|
638
|
+
import { s } from '@vertz/schema';
|
|
639
|
+
|
|
640
|
+
const env = createEnv({
|
|
641
|
+
schema: s.object({
|
|
642
|
+
DATABASE_URL: s.string(),
|
|
643
|
+
PORT: s.coerce.number().default(3000),
|
|
644
|
+
NODE_ENV: s.enum(['development', 'production', 'test']),
|
|
645
|
+
}),
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// env.DATABASE_URL — fully typed, validated, immutable
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
By default, `createEnv` reads from `process.env`. You can pass an explicit `env` record instead — useful for edge runtimes (Cloudflare Workers, Deno Deploy) or testing:
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
// Edge runtime — pass env explicitly
|
|
655
|
+
const env = createEnv({
|
|
656
|
+
schema: s.object({
|
|
657
|
+
DATABASE_URL: s.string(),
|
|
658
|
+
API_KEY: s.string(),
|
|
659
|
+
}),
|
|
660
|
+
env: context.env, // Cloudflare Workers env bindings
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Testing — inject controlled values
|
|
664
|
+
const env = createEnv({
|
|
665
|
+
schema: s.object({ PORT: s.coerce.number() }),
|
|
666
|
+
env: { PORT: '4000' },
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
| Option | Type | Description |
|
|
671
|
+
|--------|------|-------------|
|
|
672
|
+
| `schema` | `Schema<T>` | A `@vertz/schema` schema to validate against |
|
|
673
|
+
| `env` | `Record<string, string \| undefined>` | Explicit env record. Defaults to `process.env` with a `typeof process` guard for non-Node runtimes |
|
|
674
|
+
| `load` | `string[]` | Dotenv file paths to load before validation |
|
|
675
|
+
|
|
632
676
|
### Custom Server Adapters
|
|
633
677
|
|
|
634
678
|
Use the `.handler` property to integrate with custom servers:
|
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[];
|
|
@@ -213,25 +47,52 @@ interface CorsConfig {
|
|
|
213
47
|
maxAge?: number;
|
|
214
48
|
exposedHeaders?: string[];
|
|
215
49
|
}
|
|
216
|
-
interface
|
|
50
|
+
interface EntityDefinition {
|
|
51
|
+
readonly kind?: string;
|
|
217
52
|
readonly name: string;
|
|
218
|
-
readonly
|
|
219
|
-
readonly table: unknown;
|
|
220
|
-
readonly exposedRelations: Record<string, unknown>;
|
|
53
|
+
readonly model: unknown;
|
|
221
54
|
readonly access: Record<string, unknown>;
|
|
222
|
-
readonly
|
|
55
|
+
readonly before: Record<string, unknown>;
|
|
56
|
+
readonly after: Record<string, unknown>;
|
|
223
57
|
readonly actions: Record<string, unknown>;
|
|
58
|
+
readonly relations: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
type SchemaLike = {
|
|
61
|
+
parse(value: unknown): {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
data?: unknown;
|
|
64
|
+
error?: unknown;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* An entity route entry generated by @vertz/server's route generator.
|
|
69
|
+
* Core doesn't know about entity internals — it just registers these as handlers.
|
|
70
|
+
*/
|
|
71
|
+
interface EntityRouteEntry {
|
|
72
|
+
method: string;
|
|
73
|
+
path: string;
|
|
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>;
|
|
224
81
|
}
|
|
225
82
|
interface AppConfig {
|
|
226
83
|
basePath?: string;
|
|
227
84
|
version?: string;
|
|
228
85
|
cors?: CorsConfig;
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
/** API prefix for
|
|
86
|
+
/** Entity definitions for auto-CRUD route generation */
|
|
87
|
+
entities?: EntityDefinition[];
|
|
88
|
+
/** API prefix for entity routes (default: '/api/') */
|
|
232
89
|
apiPrefix?: string;
|
|
233
90
|
/** Enable response schema validation in dev mode (logs warnings but doesn't break response) */
|
|
234
91
|
validateResponses?: boolean;
|
|
92
|
+
/** Internal: pre-built entity route handlers injected by @vertz/server */
|
|
93
|
+
_entityRoutes?: EntityRouteEntry[];
|
|
94
|
+
/** Internal: factory for creating DB adapters per entity (used by @vertz/server) */
|
|
95
|
+
_entityDbFactory?: (entityDef: EntityDefinition) => unknown;
|
|
235
96
|
}
|
|
236
97
|
interface ListenOptions {
|
|
237
98
|
hostname?: string;
|
|
@@ -249,8 +110,7 @@ interface RouteInfo {
|
|
|
249
110
|
method: string;
|
|
250
111
|
path: string;
|
|
251
112
|
}
|
|
252
|
-
interface AppBuilder<
|
|
253
|
-
register(module: NamedModule, options?: Record<string, unknown>): AppBuilder<TMiddlewareCtx>;
|
|
113
|
+
interface AppBuilder<_TMiddlewareCtx extends Record<string, unknown> = Record<string, unknown>> {
|
|
254
114
|
middlewares<const M extends readonly NamedMiddlewareDef<any, any>[]>(list: M): AppBuilder<AccumulateProvides<M>>;
|
|
255
115
|
readonly handler: (request: Request) => Promise<Response>;
|
|
256
116
|
listen(port?: number, options?: ListenOptions): Promise<ServerHandle>;
|
|
@@ -260,36 +120,32 @@ interface AppBuilder<TMiddlewareCtx extends Record<string, unknown> = Record<str
|
|
|
260
120
|
};
|
|
261
121
|
}
|
|
262
122
|
declare function createApp(config: AppConfig): AppBuilder;
|
|
263
|
-
type
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
type: "service";
|
|
272
|
-
id: string;
|
|
273
|
-
deps: string[];
|
|
274
|
-
factory: ServiceFactory;
|
|
275
|
-
options?: Record<string, unknown>;
|
|
276
|
-
env?: Record<string, unknown>;
|
|
277
|
-
}
|
|
278
|
-
interface ModuleBootInstruction {
|
|
279
|
-
type: "module";
|
|
280
|
-
id: string;
|
|
281
|
-
services: string[];
|
|
282
|
-
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;
|
|
283
131
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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;
|
|
288
141
|
}
|
|
289
|
-
|
|
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";
|
|
290
145
|
interface EnvConfig<T = unknown> {
|
|
291
146
|
load?: string[];
|
|
292
|
-
schema:
|
|
147
|
+
schema: Schema2<T>;
|
|
148
|
+
env?: Record<string, string | undefined>;
|
|
293
149
|
}
|
|
294
150
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
295
151
|
type HttpStatusCode = 200 | 201 | 204 | 301 | 302 | 304 | 400 | 401 | 403 | 404 | 405 | 409 | 422 | 429 | 500 | 502 | 503 | 504;
|
|
@@ -300,7 +156,13 @@ declare class VertzException extends Error {
|
|
|
300
156
|
readonly code: string;
|
|
301
157
|
readonly details?: unknown;
|
|
302
158
|
constructor(message: string, statusCode?: number, code?: string, details?: unknown);
|
|
303
|
-
toJSON():
|
|
159
|
+
toJSON(): {
|
|
160
|
+
error: {
|
|
161
|
+
code: string;
|
|
162
|
+
message: string;
|
|
163
|
+
details?: unknown;
|
|
164
|
+
};
|
|
165
|
+
};
|
|
304
166
|
}
|
|
305
167
|
declare class BadRequestException extends VertzException {
|
|
306
168
|
constructor(message: string, details?: unknown);
|
|
@@ -326,7 +188,16 @@ declare class ValidationException extends VertzException {
|
|
|
326
188
|
path: string;
|
|
327
189
|
message: string;
|
|
328
190
|
}>);
|
|
329
|
-
toJSON():
|
|
191
|
+
toJSON(): {
|
|
192
|
+
error: {
|
|
193
|
+
code: string;
|
|
194
|
+
message: string;
|
|
195
|
+
details?: ReadonlyArray<{
|
|
196
|
+
path: string;
|
|
197
|
+
message: string;
|
|
198
|
+
}>;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
330
201
|
}
|
|
331
202
|
declare class InternalServerErrorException extends VertzException {
|
|
332
203
|
constructor(message: string, details?: unknown);
|
|
@@ -340,128 +211,11 @@ declare function makeImmutable<T extends object>(obj: T, contextName: string): D
|
|
|
340
211
|
declare const vertz: {
|
|
341
212
|
readonly env: typeof createEnv;
|
|
342
213
|
readonly middleware: typeof createMiddleware;
|
|
343
|
-
readonly moduleDef: typeof createModuleDef;
|
|
344
|
-
readonly module: typeof createModule;
|
|
345
214
|
readonly app: typeof createApp;
|
|
346
215
|
/** @since 0.2.0 — preferred alias for `app` */
|
|
347
216
|
readonly server: typeof createApp;
|
|
348
217
|
};
|
|
349
218
|
/**
|
|
350
|
-
* Symbol used to brand Result objects to prevent accidental matches with user data.
|
|
351
|
-
* Using a Symbol makes it impossible for user objects to accidentally match isResult().
|
|
352
|
-
*/
|
|
353
|
-
declare const RESULT_BRAND: unique symbol;
|
|
354
|
-
/**
|
|
355
|
-
* Result type for explicit error handling in route handlers.
|
|
356
|
-
*
|
|
357
|
-
* This provides an alternative to exception-based error handling,
|
|
358
|
-
* making error cases visible in type signatures.
|
|
359
|
-
*
|
|
360
|
-
* @example
|
|
361
|
-
* ```typescript
|
|
362
|
-
* router.get('/:id', {
|
|
363
|
-
* handler: async (ctx) => {
|
|
364
|
-
* const user = await ctx.userService.find(ctx.params.id);
|
|
365
|
-
* if (!user) {
|
|
366
|
-
* return err(404, { message: 'User not found' });
|
|
367
|
-
* }
|
|
368
|
-
* return ok({ id: user.id, name: user.name });
|
|
369
|
-
* }
|
|
370
|
-
* });
|
|
371
|
-
* ```
|
|
372
|
-
*/
|
|
373
|
-
/**
|
|
374
|
-
* Represents a successful result containing data.
|
|
375
|
-
*/
|
|
376
|
-
interface Ok<T> {
|
|
377
|
-
readonly ok: true;
|
|
378
|
-
readonly data: T;
|
|
379
|
-
readonly [RESULT_BRAND]: true;
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Represents an error result containing status code and error body.
|
|
383
|
-
*/
|
|
384
|
-
interface Err<E> {
|
|
385
|
-
readonly ok: false;
|
|
386
|
-
readonly status: number;
|
|
387
|
-
readonly body: E;
|
|
388
|
-
readonly [RESULT_BRAND]: true;
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* A discriminated union type representing either a success (Ok) or failure (Err).
|
|
392
|
-
*
|
|
393
|
-
* @typeParam T - The type of the success data
|
|
394
|
-
* @typeParam E - The type of the error body
|
|
395
|
-
*
|
|
396
|
-
* @example
|
|
397
|
-
* ```typescript
|
|
398
|
-
* type UserResult = Result<{ id: number; name: string }, { message: string }>;
|
|
399
|
-
* ```
|
|
400
|
-
*/
|
|
401
|
-
type Result<
|
|
402
|
-
T,
|
|
403
|
-
E = unknown
|
|
404
|
-
> = Ok<T> | Err<E>;
|
|
405
|
-
/**
|
|
406
|
-
* Creates a successful Result containing the given data.
|
|
407
|
-
*
|
|
408
|
-
* @param data - The success data
|
|
409
|
-
* @returns An Ok result with the data
|
|
410
|
-
*
|
|
411
|
-
* @example
|
|
412
|
-
* ```typescript
|
|
413
|
-
* return ok({ id: 1, name: 'John' });
|
|
414
|
-
* ```
|
|
415
|
-
*/
|
|
416
|
-
declare function ok<T>(data: T): Ok<T>;
|
|
417
|
-
/**
|
|
418
|
-
* Creates an error Result with the given status code and body.
|
|
419
|
-
*
|
|
420
|
-
* @param status - HTTP status code for the error
|
|
421
|
-
* @param body - Error body/response
|
|
422
|
-
* @returns An Err result with status and body
|
|
423
|
-
*
|
|
424
|
-
* @example
|
|
425
|
-
* ```typescript
|
|
426
|
-
* return err(404, { message: 'Not found' });
|
|
427
|
-
* ```
|
|
428
|
-
*/
|
|
429
|
-
declare function err<E>(status: number, body: E): Err<E>;
|
|
430
|
-
/**
|
|
431
|
-
* Type guard to check if a Result is Ok.
|
|
432
|
-
*
|
|
433
|
-
* @param result - The result to check
|
|
434
|
-
* @returns True if the result is Ok
|
|
435
|
-
*
|
|
436
|
-
* @example
|
|
437
|
-
* ```typescript
|
|
438
|
-
* if (isOk(result)) {
|
|
439
|
-
* console.log(result.data);
|
|
440
|
-
* }
|
|
441
|
-
* ```
|
|
442
|
-
*/
|
|
443
|
-
declare function isOk<
|
|
444
|
-
T,
|
|
445
|
-
E
|
|
446
|
-
>(result: Result<T, E>): result is Ok<T>;
|
|
447
|
-
/**
|
|
448
|
-
* Type guard to check if a Result is Err.
|
|
449
|
-
*
|
|
450
|
-
* @param result - The result to check
|
|
451
|
-
* @returns True if the result is Err
|
|
452
|
-
*
|
|
453
|
-
* @example
|
|
454
|
-
* ```typescript
|
|
455
|
-
* if (isErr(result)) {
|
|
456
|
-
* console.log(result.status, result.body);
|
|
457
|
-
* }
|
|
458
|
-
* ```
|
|
459
|
-
*/
|
|
460
|
-
declare function isErr<
|
|
461
|
-
T,
|
|
462
|
-
E
|
|
463
|
-
>(result: Result<T, E>): result is Err<E>;
|
|
464
|
-
/**
|
|
465
219
|
* Creates an HTTP server. Preferred entry point for building Vertz services.
|
|
466
220
|
* @since 0.2.0
|
|
467
221
|
*/
|
|
@@ -470,4 +224,4 @@ declare const createServer: (config: AppConfig) => AppBuilder;
|
|
|
470
224
|
* @deprecated Use `createServer` instead. `createApp` will be removed in v0.3.0.
|
|
471
225
|
*/
|
|
472
226
|
declare const createApp2: (config: AppConfig) => AppBuilder;
|
|
473
|
-
export { vertz,
|
|
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,19 @@ import {
|
|
|
18
18
|
parseBody,
|
|
19
19
|
parseRequest,
|
|
20
20
|
runMiddlewareChain
|
|
21
|
-
} from "./shared/chunk-
|
|
21
|
+
} from "./shared/chunk-m3w3ytn5.js";
|
|
22
|
+
|
|
23
|
+
// src/result.ts
|
|
24
|
+
var RESULT_BRAND = Symbol.for("vertz.result");
|
|
25
|
+
function isOk(result) {
|
|
26
|
+
return result.ok === true;
|
|
27
|
+
}
|
|
28
|
+
function isResult(value) {
|
|
29
|
+
if (value === null || typeof value !== "object")
|
|
30
|
+
return false;
|
|
31
|
+
const obj = value;
|
|
32
|
+
return obj[RESULT_BRAND] === true;
|
|
33
|
+
}
|
|
22
34
|
|
|
23
35
|
// src/server/cors.ts
|
|
24
36
|
var DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
@@ -37,27 +49,34 @@ function resolveOrigin(config, requestOrigin) {
|
|
|
37
49
|
return null;
|
|
38
50
|
}
|
|
39
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
|
+
}
|
|
40
55
|
const requestOrigin = request.headers.get("origin");
|
|
41
56
|
const origin = resolveOrigin(config, requestOrigin);
|
|
42
57
|
if (request.method === "OPTIONS") {
|
|
43
58
|
const headers = {};
|
|
44
|
-
if (origin)
|
|
59
|
+
if (origin) {
|
|
45
60
|
headers["access-control-allow-origin"] = origin;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
|
55
71
|
}
|
|
56
72
|
return new Response(null, { status: 204, headers });
|
|
57
73
|
}
|
|
58
74
|
return null;
|
|
59
75
|
}
|
|
60
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
|
+
}
|
|
61
80
|
const requestOrigin = request.headers.get("origin");
|
|
62
81
|
const origin = resolveOrigin(config, requestOrigin);
|
|
63
82
|
if (!origin)
|
|
@@ -77,27 +96,6 @@ function applyCorsHeaders(config, request, response) {
|
|
|
77
96
|
});
|
|
78
97
|
}
|
|
79
98
|
|
|
80
|
-
// src/result.ts
|
|
81
|
-
var RESULT_BRAND = Symbol.for("vertz.result");
|
|
82
|
-
function ok(data) {
|
|
83
|
-
return { ok: true, data, [RESULT_BRAND]: true };
|
|
84
|
-
}
|
|
85
|
-
function err(status, body) {
|
|
86
|
-
return { ok: false, status, body, [RESULT_BRAND]: true };
|
|
87
|
-
}
|
|
88
|
-
function isOk(result) {
|
|
89
|
-
return result.ok === true;
|
|
90
|
-
}
|
|
91
|
-
function isErr(result) {
|
|
92
|
-
return result.ok === false;
|
|
93
|
-
}
|
|
94
|
-
function isResult(value) {
|
|
95
|
-
if (value === null || typeof value !== "object")
|
|
96
|
-
return false;
|
|
97
|
-
const obj = value;
|
|
98
|
-
return obj[RESULT_BRAND] === true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
99
|
// src/app/app-runner.ts
|
|
102
100
|
function createResponseWithCors(data, status, config, request) {
|
|
103
101
|
const response = createJsonResponse(data, status);
|
|
@@ -106,36 +104,40 @@ function createResponseWithCors(data, status, config, request) {
|
|
|
106
104
|
}
|
|
107
105
|
return response;
|
|
108
106
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
122
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (service.options && options) {
|
|
126
|
-
const parsed = service.options.safeParse(options);
|
|
127
|
-
if (parsed.success) {
|
|
128
|
-
parsedOptions = parsed.data;
|
|
129
|
-
} else {
|
|
130
|
-
throw new Error(`Invalid options for service ${service.moduleName}: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const env = {};
|
|
134
|
-
serviceMap.set(service, service.methods({}, undefined, parsedOptions, env));
|
|
135
|
-
}
|
|
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}`;
|
|
136
130
|
}
|
|
137
131
|
}
|
|
138
|
-
return
|
|
132
|
+
return message;
|
|
133
|
+
}
|
|
134
|
+
function validateSchema(schema, value, label) {
|
|
135
|
+
const result = schema.parse(value);
|
|
136
|
+
if (!result.ok) {
|
|
137
|
+
const message = sanitizeValidationMessage(result.error, label);
|
|
138
|
+
throw new BadRequestException(message);
|
|
139
|
+
}
|
|
140
|
+
return result.data;
|
|
139
141
|
}
|
|
140
142
|
function resolveMiddlewares(globalMiddlewares) {
|
|
141
143
|
return globalMiddlewares.map((mw) => ({
|
|
@@ -144,51 +146,26 @@ function resolveMiddlewares(globalMiddlewares) {
|
|
|
144
146
|
resolvedInject: {}
|
|
145
147
|
}));
|
|
146
148
|
}
|
|
147
|
-
function
|
|
148
|
-
if (!inject)
|
|
149
|
-
return {};
|
|
150
|
-
const resolved = {};
|
|
151
|
-
for (const [name, serviceDef] of Object.entries(inject)) {
|
|
152
|
-
const methods = serviceMap.get(serviceDef);
|
|
153
|
-
if (methods)
|
|
154
|
-
resolved[name] = methods;
|
|
155
|
-
}
|
|
156
|
-
return resolved;
|
|
157
|
-
}
|
|
158
|
-
function registerRoutes(trie, basePath, registrations, serviceMap) {
|
|
159
|
-
for (const { module, options } of registrations) {
|
|
160
|
-
for (const router of module.routers) {
|
|
161
|
-
const resolvedServices = resolveRouterServices(router.inject, serviceMap);
|
|
162
|
-
for (const route of router.routes) {
|
|
163
|
-
const fullPath = basePath + router.prefix + route.path;
|
|
164
|
-
const routeMiddlewares = (route.config.middlewares ?? []).map((mw) => ({
|
|
165
|
-
name: mw.name,
|
|
166
|
-
handler: mw.handler,
|
|
167
|
-
resolvedInject: {}
|
|
168
|
-
}));
|
|
169
|
-
const entry = {
|
|
170
|
-
handler: route.config.handler,
|
|
171
|
-
options: options ?? {},
|
|
172
|
-
services: resolvedServices,
|
|
173
|
-
middlewares: routeMiddlewares,
|
|
174
|
-
paramsSchema: route.config.params,
|
|
175
|
-
bodySchema: route.config.body,
|
|
176
|
-
querySchema: route.config.query,
|
|
177
|
-
headersSchema: route.config.headers,
|
|
178
|
-
responseSchema: route.config.response,
|
|
179
|
-
errorsSchema: route.config.errors
|
|
180
|
-
};
|
|
181
|
-
trie.add(route.method, fullPath, entry);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function buildHandler(config, registrations, globalMiddlewares) {
|
|
149
|
+
function buildHandler(config, globalMiddlewares) {
|
|
187
150
|
const trie = new Trie;
|
|
188
|
-
const basePath = config.basePath ?? "";
|
|
189
151
|
const resolvedMiddlewares = resolveMiddlewares(globalMiddlewares);
|
|
190
|
-
|
|
191
|
-
|
|
152
|
+
if (config._entityRoutes) {
|
|
153
|
+
for (const route of config._entityRoutes) {
|
|
154
|
+
const entry = {
|
|
155
|
+
handler: route.handler,
|
|
156
|
+
options: {},
|
|
157
|
+
services: {},
|
|
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
|
|
165
|
+
};
|
|
166
|
+
trie.add(route.method, route.path, entry);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
192
169
|
return async (request) => {
|
|
193
170
|
try {
|
|
194
171
|
if (config.cors) {
|
|
@@ -201,9 +178,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
201
178
|
if (!match) {
|
|
202
179
|
const allowed = trie.getAllowedMethods(parsed.path);
|
|
203
180
|
if (allowed.length > 0) {
|
|
204
|
-
return createJsonResponse({ error: "MethodNotAllowed", message: "Method Not Allowed"
|
|
181
|
+
return createJsonResponse({ error: { code: "MethodNotAllowed", message: "Method Not Allowed" } }, 405, { allow: allowed.join(", ") });
|
|
205
182
|
}
|
|
206
|
-
return createJsonResponse({ error: "NotFound", message: "Not Found"
|
|
183
|
+
return createJsonResponse({ error: { code: "NotFound", message: "Not Found" } }, 404);
|
|
207
184
|
}
|
|
208
185
|
const body = await parseBody(request);
|
|
209
186
|
const raw = {
|
|
@@ -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;
|
|
@@ -246,10 +227,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
246
227
|
if (isOk(result)) {
|
|
247
228
|
const data = result.data;
|
|
248
229
|
if (config.validateResponses && entry.responseSchema) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const message = error instanceof Error ? error.message : "Response schema validation failed";
|
|
230
|
+
const validation = entry.responseSchema.parse(data);
|
|
231
|
+
if (!validation.ok) {
|
|
232
|
+
const message = validation.error instanceof Error ? validation.error.message : "Response schema validation failed";
|
|
253
233
|
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
254
234
|
}
|
|
255
235
|
}
|
|
@@ -260,10 +240,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
260
240
|
if (config.validateResponses && entry.errorsSchema) {
|
|
261
241
|
const errorSchema = entry.errorsSchema[errorStatus];
|
|
262
242
|
if (errorSchema) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const message = error instanceof Error ? `Error schema validation failed for status ${errorStatus}: ${error.message}` : `Error schema validation failed for status ${errorStatus}`;
|
|
243
|
+
const validation = errorSchema.parse(errorBody);
|
|
244
|
+
if (!validation.ok) {
|
|
245
|
+
const message = validation.error instanceof Error ? `Error schema validation failed for status ${errorStatus}: ${validation.error.message}` : `Error schema validation failed for status ${errorStatus}`;
|
|
267
246
|
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
268
247
|
}
|
|
269
248
|
}
|
|
@@ -272,14 +251,13 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
272
251
|
}
|
|
273
252
|
}
|
|
274
253
|
if (config.validateResponses && entry.responseSchema) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const message = error instanceof Error ? error.message : "Response schema validation failed";
|
|
254
|
+
const validation = entry.responseSchema.parse(result);
|
|
255
|
+
if (!validation.ok) {
|
|
256
|
+
const message = validation.error instanceof Error ? validation.error.message : "Response schema validation failed";
|
|
279
257
|
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
280
258
|
}
|
|
281
259
|
}
|
|
282
|
-
const response = result === undefined ? new Response(null, { status: 204 }) : createJsonResponse(result);
|
|
260
|
+
const response = result === undefined ? new Response(null, { status: 204 }) : result instanceof Response ? result : createJsonResponse(result);
|
|
283
261
|
if (config.cors) {
|
|
284
262
|
return applyCorsHeaders(config.cors, request, response);
|
|
285
263
|
}
|
|
@@ -323,27 +301,6 @@ function detectAdapter(hints) {
|
|
|
323
301
|
}
|
|
324
302
|
|
|
325
303
|
// src/app/route-log.ts
|
|
326
|
-
function normalizePath(path) {
|
|
327
|
-
let normalized = path.replace(/\/+/g, "/");
|
|
328
|
-
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
329
|
-
normalized = normalized.slice(0, -1);
|
|
330
|
-
}
|
|
331
|
-
return normalized || "/";
|
|
332
|
-
}
|
|
333
|
-
function collectRoutes(basePath, registrations) {
|
|
334
|
-
const routes = [];
|
|
335
|
-
for (const { module } of registrations) {
|
|
336
|
-
for (const router of module.routers) {
|
|
337
|
-
for (const route of router.routes) {
|
|
338
|
-
routes.push({
|
|
339
|
-
method: route.method,
|
|
340
|
-
path: normalizePath(basePath + router.prefix + route.path)
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return routes;
|
|
346
|
-
}
|
|
347
304
|
function formatRouteLog(listenUrl, routes) {
|
|
348
305
|
const header = `vertz server listening on ${listenUrl}`;
|
|
349
306
|
if (routes.length === 0) {
|
|
@@ -363,28 +320,18 @@ function formatRouteLog(listenUrl, routes) {
|
|
|
363
320
|
// src/app/app-builder.ts
|
|
364
321
|
var DEFAULT_PORT = 3000;
|
|
365
322
|
function createApp(config) {
|
|
366
|
-
const registrations = [];
|
|
367
323
|
let globalMiddlewares = [];
|
|
368
324
|
let cachedHandler = null;
|
|
369
325
|
const registeredRoutes = [];
|
|
326
|
+
const entityRoutes = [];
|
|
370
327
|
const builder = {
|
|
371
|
-
register(module, options) {
|
|
372
|
-
registrations.push({ module, options });
|
|
373
|
-
for (const router of module.routers) {
|
|
374
|
-
for (const route of router.routes) {
|
|
375
|
-
registeredRoutes.push({ method: route.method, path: router.prefix + route.path });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
cachedHandler = null;
|
|
379
|
-
return builder;
|
|
380
|
-
},
|
|
381
328
|
middlewares(list) {
|
|
382
329
|
globalMiddlewares = [...list];
|
|
383
330
|
return builder;
|
|
384
331
|
},
|
|
385
332
|
get handler() {
|
|
386
333
|
if (!cachedHandler) {
|
|
387
|
-
cachedHandler = buildHandler(config,
|
|
334
|
+
cachedHandler = buildHandler(config, globalMiddlewares);
|
|
388
335
|
}
|
|
389
336
|
return cachedHandler;
|
|
390
337
|
},
|
|
@@ -395,35 +342,45 @@ function createApp(config) {
|
|
|
395
342
|
const adapter = detectAdapter();
|
|
396
343
|
const serverHandle = await adapter.listen(port ?? DEFAULT_PORT, builder.handler, options);
|
|
397
344
|
if (options?.logRoutes !== false) {
|
|
398
|
-
const routes = collectRoutes(config.basePath ?? "", registrations);
|
|
399
345
|
const url = `http://${serverHandle.hostname}:${serverHandle.port}`;
|
|
400
|
-
console.log(formatRouteLog(url,
|
|
346
|
+
console.log(formatRouteLog(url, entityRoutes));
|
|
401
347
|
}
|
|
402
348
|
return serverHandle;
|
|
403
349
|
}
|
|
404
350
|
};
|
|
405
|
-
if (config.
|
|
351
|
+
if (config._entityRoutes) {
|
|
352
|
+
for (const route of config._entityRoutes) {
|
|
353
|
+
const info = { method: route.method, path: route.path };
|
|
354
|
+
registeredRoutes.push(info);
|
|
355
|
+
entityRoutes.push(info);
|
|
356
|
+
}
|
|
357
|
+
} else if (config.entities && config.entities.length > 0) {
|
|
406
358
|
const rawPrefix = config.apiPrefix === undefined ? "/api/" : config.apiPrefix;
|
|
407
|
-
for (const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
359
|
+
for (const entity of config.entities) {
|
|
360
|
+
const entityPath = rawPrefix === "" ? `/${entity.name}` : (rawPrefix.endsWith("/") ? rawPrefix : `${rawPrefix}/`) + entity.name;
|
|
361
|
+
const routes = [
|
|
362
|
+
{ method: "GET", path: entityPath },
|
|
363
|
+
{ method: "GET", path: `${entityPath}/:id` },
|
|
364
|
+
{ method: "POST", path: entityPath },
|
|
365
|
+
{ method: "PATCH", path: `${entityPath}/:id` },
|
|
366
|
+
{ method: "DELETE", path: `${entityPath}/:id` }
|
|
367
|
+
];
|
|
368
|
+
if (entity.actions) {
|
|
369
|
+
for (const actionName of Object.keys(entity.actions)) {
|
|
370
|
+
routes.push({ method: "POST", path: `${entityPath}/:id/${actionName}` });
|
|
417
371
|
}
|
|
418
372
|
}
|
|
373
|
+
registeredRoutes.push(...routes);
|
|
374
|
+
entityRoutes.push(...routes);
|
|
419
375
|
}
|
|
420
376
|
}
|
|
421
377
|
return builder;
|
|
422
378
|
}
|
|
423
379
|
// src/env/env-validator.ts
|
|
424
380
|
function createEnv(config) {
|
|
425
|
-
const
|
|
426
|
-
|
|
381
|
+
const envRecord = config.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
382
|
+
const result = config.schema.safeParse(envRecord);
|
|
383
|
+
if (!result.ok) {
|
|
427
384
|
throw new Error(`Environment validation failed:
|
|
428
385
|
${result.error.message}`);
|
|
429
386
|
}
|
|
@@ -433,72 +390,10 @@ ${result.error.message}`);
|
|
|
433
390
|
function createMiddleware(def) {
|
|
434
391
|
return deepFreeze(def);
|
|
435
392
|
}
|
|
436
|
-
// src/module/module.ts
|
|
437
|
-
function validateOwnership(items, kind, expectedModule) {
|
|
438
|
-
for (const item of items) {
|
|
439
|
-
if (item.moduleName !== expectedModule) {
|
|
440
|
-
throw new Error(`${kind} belongs to module "${item.moduleName}", cannot add to module "${expectedModule}"`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
function createModule(definition, config) {
|
|
445
|
-
validateOwnership(config.services, "Service", definition.name);
|
|
446
|
-
validateOwnership(config.routers, "Router", definition.name);
|
|
447
|
-
for (const exp of config.exports) {
|
|
448
|
-
if (!config.services.includes(exp)) {
|
|
449
|
-
throw new Error("exports must be a subset of services");
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return deepFreeze({
|
|
453
|
-
definition,
|
|
454
|
-
...config
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
// src/module/router-def.ts
|
|
458
|
-
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head"];
|
|
459
|
-
function createRouterDef(moduleName, config) {
|
|
460
|
-
const routes = [];
|
|
461
|
-
function addRoute(method, path, routeConfig) {
|
|
462
|
-
if (!path.startsWith("/")) {
|
|
463
|
-
throw new Error(`Route path must start with '/', got '${path}'`);
|
|
464
|
-
}
|
|
465
|
-
routes.push({ method, path, config: routeConfig });
|
|
466
|
-
return router;
|
|
467
|
-
}
|
|
468
|
-
const router = {
|
|
469
|
-
...config,
|
|
470
|
-
moduleName,
|
|
471
|
-
routes
|
|
472
|
-
};
|
|
473
|
-
for (const method of HTTP_METHODS) {
|
|
474
|
-
router[method] = (path, cfg) => addRoute(method.toUpperCase(), path, cfg);
|
|
475
|
-
}
|
|
476
|
-
return router;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// src/module/service.ts
|
|
480
|
-
function createServiceDef(moduleName, config) {
|
|
481
|
-
return deepFreeze({
|
|
482
|
-
...config,
|
|
483
|
-
moduleName
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// src/module/module-def.ts
|
|
488
|
-
function createModuleDef(config) {
|
|
489
|
-
const def = {
|
|
490
|
-
...config,
|
|
491
|
-
service: (serviceConfig) => createServiceDef(config.name, serviceConfig),
|
|
492
|
-
router: (routerConfig) => createRouterDef(config.name, routerConfig)
|
|
493
|
-
};
|
|
494
|
-
return deepFreeze(def);
|
|
495
|
-
}
|
|
496
393
|
// src/vertz.ts
|
|
497
394
|
var vertz = /* @__PURE__ */ deepFreeze({
|
|
498
395
|
env: createEnv,
|
|
499
396
|
middleware: createMiddleware,
|
|
500
|
-
moduleDef: createModuleDef,
|
|
501
|
-
module: createModule,
|
|
502
397
|
app: createApp,
|
|
503
398
|
server: createApp
|
|
504
399
|
});
|
|
@@ -511,15 +406,9 @@ var createApp2 = (...args) => {
|
|
|
511
406
|
};
|
|
512
407
|
export {
|
|
513
408
|
vertz,
|
|
514
|
-
ok,
|
|
515
409
|
makeImmutable,
|
|
516
|
-
isOk,
|
|
517
|
-
isErr,
|
|
518
|
-
err,
|
|
519
410
|
deepFreeze,
|
|
520
411
|
createServer,
|
|
521
|
-
createModuleDef,
|
|
522
|
-
createModule,
|
|
523
412
|
createMiddleware,
|
|
524
413
|
createImmutableProxy,
|
|
525
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
|
@@ -41,7 +41,7 @@ function deepFreeze(obj, visited = new WeakSet) {
|
|
|
41
41
|
|
|
42
42
|
// src/immutability/make-immutable.ts
|
|
43
43
|
function makeImmutable(obj, contextName) {
|
|
44
|
-
if (true) {
|
|
44
|
+
if (typeof process !== "undefined" && true) {
|
|
45
45
|
return createImmutableProxy(obj, contextName);
|
|
46
46
|
}
|
|
47
47
|
return obj;
|
|
@@ -65,7 +65,7 @@ function validateCollisions(config) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
function buildCtx(config) {
|
|
68
|
-
if (true) {
|
|
68
|
+
if (typeof process !== "undefined" && true) {
|
|
69
69
|
validateCollisions(config);
|
|
70
70
|
}
|
|
71
71
|
return makeImmutable({
|
|
@@ -98,11 +98,11 @@ class VertzException extends Error {
|
|
|
98
98
|
}
|
|
99
99
|
toJSON() {
|
|
100
100
|
return {
|
|
101
|
-
error:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
error: {
|
|
102
|
+
code: this.code,
|
|
103
|
+
message: this.message,
|
|
104
|
+
...this.details !== undefined && { details: this.details }
|
|
105
|
+
}
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -110,70 +110,81 @@ class VertzException extends Error {
|
|
|
110
110
|
// src/exceptions/http-exceptions.ts
|
|
111
111
|
class BadRequestException extends VertzException {
|
|
112
112
|
constructor(message, details) {
|
|
113
|
-
super(message, 400,
|
|
113
|
+
super(message, 400, "BadRequest", details);
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
class UnauthorizedException extends VertzException {
|
|
118
118
|
constructor(message, details) {
|
|
119
|
-
super(message, 401,
|
|
119
|
+
super(message, 401, "Unauthorized", details);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
class ForbiddenException extends VertzException {
|
|
124
124
|
constructor(message, details) {
|
|
125
|
-
super(message, 403,
|
|
125
|
+
super(message, 403, "Forbidden", details);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
class NotFoundException extends VertzException {
|
|
130
130
|
constructor(message, details) {
|
|
131
|
-
super(message, 404,
|
|
131
|
+
super(message, 404, "NotFound", details);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
class ConflictException extends VertzException {
|
|
136
136
|
constructor(message, details) {
|
|
137
|
-
super(message, 409,
|
|
137
|
+
super(message, 409, "Conflict", details);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
class ValidationException extends VertzException {
|
|
142
142
|
errors;
|
|
143
143
|
constructor(errors) {
|
|
144
|
-
super("Validation failed", 422,
|
|
144
|
+
super("Validation failed", 422, "ValidationError", undefined);
|
|
145
145
|
this.errors = errors;
|
|
146
146
|
}
|
|
147
147
|
toJSON() {
|
|
148
148
|
return {
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
error: {
|
|
150
|
+
code: this.code,
|
|
151
|
+
message: this.message,
|
|
152
|
+
details: this.errors
|
|
153
|
+
}
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
class InternalServerErrorException extends VertzException {
|
|
156
159
|
constructor(message, details) {
|
|
157
|
-
super(message, 500,
|
|
160
|
+
super(message, 500, "InternalError", details);
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
class ServiceUnavailableException extends VertzException {
|
|
162
165
|
constructor(message, details) {
|
|
163
|
-
super(message, 503,
|
|
166
|
+
super(message, 503, "ServiceUnavailable", details);
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
// src/middleware/middleware-runner.ts
|
|
170
|
+
var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
167
171
|
function isPlainObject(value) {
|
|
168
172
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
169
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
|
+
}
|
|
170
181
|
async function runMiddlewareChain(middlewares, requestCtx) {
|
|
171
|
-
const accumulated =
|
|
182
|
+
const accumulated = Object.create(null);
|
|
172
183
|
for (const mw of middlewares) {
|
|
173
184
|
const ctx = { ...requestCtx, ...mw.resolvedInject, ...accumulated };
|
|
174
185
|
const contribution = await mw.handler(ctx);
|
|
175
186
|
if (isPlainObject(contribution)) {
|
|
176
|
-
|
|
187
|
+
safeAssign(accumulated, contribution);
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
return accumulated;
|
|
@@ -297,11 +308,16 @@ function parseRequest(request) {
|
|
|
297
308
|
method: request.method,
|
|
298
309
|
path: url.pathname,
|
|
299
310
|
query: Object.fromEntries(url.searchParams),
|
|
300
|
-
headers: Object.fromEntries(request.headers),
|
|
311
|
+
headers: Object.fromEntries([...request.headers].map(([k, v]) => [k.toLowerCase(), v])),
|
|
301
312
|
raw: request
|
|
302
313
|
};
|
|
303
314
|
}
|
|
304
|
-
|
|
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
|
+
}
|
|
305
321
|
const contentType = request.headers.get("content-type") ?? "";
|
|
306
322
|
if (contentType.includes("application/json")) {
|
|
307
323
|
try {
|
|
@@ -325,7 +341,7 @@ function createJsonResponse(data, status = 200, headers) {
|
|
|
325
341
|
return new Response(JSON.stringify(data), {
|
|
326
342
|
status,
|
|
327
343
|
headers: {
|
|
328
|
-
"content-type": "application/json",
|
|
344
|
+
"content-type": "application/json; charset=utf-8",
|
|
329
345
|
...headers
|
|
330
346
|
}
|
|
331
347
|
});
|
|
@@ -334,7 +350,7 @@ function createErrorResponse(error) {
|
|
|
334
350
|
if (error instanceof VertzException) {
|
|
335
351
|
return createJsonResponse(error.toJSON(), error.statusCode);
|
|
336
352
|
}
|
|
337
|
-
return createJsonResponse({ error: "InternalServerError", message: "Internal Server Error"
|
|
353
|
+
return createJsonResponse({ error: { code: "InternalServerError", message: "Internal Server Error" } }, 500);
|
|
338
354
|
}
|
|
339
355
|
|
|
340
356
|
export { createImmutableProxy, deepFreeze, makeImmutable, buildCtx, VertzException, BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException, ConflictException, ValidationException, InternalServerErrorException, ServiceUnavailableException, runMiddlewareChain, Trie, parseRequest, parseBody, createJsonResponse, createErrorResponse };
|
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",
|
|
@@ -30,17 +30,18 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "bunup",
|
|
33
|
-
"test": "
|
|
33
|
+
"test": "bun test",
|
|
34
|
+
"test:coverage": "vitest run --coverage",
|
|
34
35
|
"test:watch": "vitest",
|
|
35
36
|
"typecheck": "tsc --noEmit"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@vertz/schema": "
|
|
39
|
+
"@vertz/schema": "0.2.2"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^
|
|
42
|
+
"@types/node": "^25.3.1",
|
|
42
43
|
"@vitest/coverage-v8": "^4.0.18",
|
|
43
|
-
"bunup": "
|
|
44
|
+
"bunup": "^0.16.31",
|
|
44
45
|
"typescript": "^5.7.0",
|
|
45
46
|
"vitest": "^4.0.18"
|
|
46
47
|
},
|