@vertz/core 0.2.0 → 0.2.1
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 +41 -126
- package/dist/index.js +68 -62
- package/dist/internals.js +1 -1
- package/dist/shared/{chunk-c77pg5gx.js → chunk-k596zpc6.js} +21 -18
- package/package.json +5 -4
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
|
@@ -213,25 +213,39 @@ interface CorsConfig {
|
|
|
213
213
|
maxAge?: number;
|
|
214
214
|
exposedHeaders?: string[];
|
|
215
215
|
}
|
|
216
|
-
interface
|
|
216
|
+
interface EntityDefinition {
|
|
217
|
+
readonly kind?: string;
|
|
217
218
|
readonly name: string;
|
|
218
|
-
readonly
|
|
219
|
-
readonly table: unknown;
|
|
220
|
-
readonly exposedRelations: Record<string, unknown>;
|
|
219
|
+
readonly model: unknown;
|
|
221
220
|
readonly access: Record<string, unknown>;
|
|
222
|
-
readonly
|
|
221
|
+
readonly before: Record<string, unknown>;
|
|
222
|
+
readonly after: Record<string, unknown>;
|
|
223
223
|
readonly actions: Record<string, unknown>;
|
|
224
|
+
readonly relations: Record<string, unknown>;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* An entity route entry generated by @vertz/server's route generator.
|
|
228
|
+
* Core doesn't know about entity internals — it just registers these as handlers.
|
|
229
|
+
*/
|
|
230
|
+
interface EntityRouteEntry {
|
|
231
|
+
method: string;
|
|
232
|
+
path: string;
|
|
233
|
+
handler: (ctx: Record<string, unknown>) => Promise<Response>;
|
|
224
234
|
}
|
|
225
235
|
interface AppConfig {
|
|
226
236
|
basePath?: string;
|
|
227
237
|
version?: string;
|
|
228
238
|
cors?: CorsConfig;
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
/** API prefix for
|
|
239
|
+
/** Entity definitions for auto-CRUD route generation */
|
|
240
|
+
entities?: EntityDefinition[];
|
|
241
|
+
/** API prefix for entity routes (default: '/api/') */
|
|
232
242
|
apiPrefix?: string;
|
|
233
243
|
/** Enable response schema validation in dev mode (logs warnings but doesn't break response) */
|
|
234
244
|
validateResponses?: boolean;
|
|
245
|
+
/** Internal: pre-built entity route handlers injected by @vertz/server */
|
|
246
|
+
_entityRoutes?: EntityRouteEntry[];
|
|
247
|
+
/** Internal: factory for creating DB adapters per entity (used by @vertz/server) */
|
|
248
|
+
_entityDbFactory?: (entityDef: EntityDefinition) => unknown;
|
|
235
249
|
}
|
|
236
250
|
interface ListenOptions {
|
|
237
251
|
hostname?: string;
|
|
@@ -290,6 +304,7 @@ import { Schema as Schema3 } from "@vertz/schema";
|
|
|
290
304
|
interface EnvConfig<T = unknown> {
|
|
291
305
|
load?: string[];
|
|
292
306
|
schema: Schema3<T>;
|
|
307
|
+
env?: Record<string, string | undefined>;
|
|
293
308
|
}
|
|
294
309
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
295
310
|
type HttpStatusCode = 200 | 201 | 204 | 301 | 302 | 304 | 400 | 401 | 403 | 404 | 405 | 409 | 422 | 429 | 500 | 502 | 503 | 504;
|
|
@@ -300,7 +315,13 @@ declare class VertzException extends Error {
|
|
|
300
315
|
readonly code: string;
|
|
301
316
|
readonly details?: unknown;
|
|
302
317
|
constructor(message: string, statusCode?: number, code?: string, details?: unknown);
|
|
303
|
-
toJSON():
|
|
318
|
+
toJSON(): {
|
|
319
|
+
error: {
|
|
320
|
+
code: string;
|
|
321
|
+
message: string;
|
|
322
|
+
details?: unknown;
|
|
323
|
+
};
|
|
324
|
+
};
|
|
304
325
|
}
|
|
305
326
|
declare class BadRequestException extends VertzException {
|
|
306
327
|
constructor(message: string, details?: unknown);
|
|
@@ -326,7 +347,16 @@ declare class ValidationException extends VertzException {
|
|
|
326
347
|
path: string;
|
|
327
348
|
message: string;
|
|
328
349
|
}>);
|
|
329
|
-
toJSON():
|
|
350
|
+
toJSON(): {
|
|
351
|
+
error: {
|
|
352
|
+
code: string;
|
|
353
|
+
message: string;
|
|
354
|
+
details?: ReadonlyArray<{
|
|
355
|
+
path: string;
|
|
356
|
+
message: string;
|
|
357
|
+
}>;
|
|
358
|
+
};
|
|
359
|
+
};
|
|
330
360
|
}
|
|
331
361
|
declare class InternalServerErrorException extends VertzException {
|
|
332
362
|
constructor(message: string, details?: unknown);
|
|
@@ -347,121 +377,6 @@ declare const vertz: {
|
|
|
347
377
|
readonly server: typeof createApp;
|
|
348
378
|
};
|
|
349
379
|
/**
|
|
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
380
|
* Creates an HTTP server. Preferred entry point for building Vertz services.
|
|
466
381
|
* @since 0.2.0
|
|
467
382
|
*/
|
|
@@ -470,4 +385,4 @@ declare const createServer: (config: AppConfig) => AppBuilder;
|
|
|
470
385
|
* @deprecated Use `createServer` instead. `createApp` will be removed in v0.3.0.
|
|
471
386
|
*/
|
|
472
387
|
declare const createApp2: (config: AppConfig) => AppBuilder;
|
|
473
|
-
export { vertz,
|
|
388
|
+
export { vertz, makeImmutable, deepFreeze, createServer, createModuleDef, createModule, createMiddleware, createImmutableProxy, createEnv, createApp2 as createApp, VertzException, ValidationException, UnauthorizedException, ServiceUnavailableException, ServiceFactory, ServiceDef, ServiceBootInstruction, ServerHandle, ServerAdapter, RouterDef, RouteInfo, ResolveInjectMap, RawRequest, NotFoundException, NamedServiceDef, NamedRouterDef, NamedModuleDef, NamedModule, NamedMiddlewareDef, ModuleDef, ModuleBootInstruction, Module, MiddlewareDef, ListenOptions, InternalServerErrorException, Infer2 as InferSchema, Infer, HttpStatusCode, HttpMethod, HandlerCtx, ForbiddenException, ExtractMethods, EnvConfig, EntityRouteEntry, Deps, DeepReadonly, Ctx, CorsConfig, ConflictException, BootSequence, BootInstruction, 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-k596zpc6.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"];
|
|
@@ -77,27 +89,6 @@ function applyCorsHeaders(config, request, response) {
|
|
|
77
89
|
});
|
|
78
90
|
}
|
|
79
91
|
|
|
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
92
|
// src/app/app-runner.ts
|
|
102
93
|
function createResponseWithCors(data, status, config, request) {
|
|
103
94
|
const response = createJsonResponse(data, status);
|
|
@@ -107,14 +98,12 @@ function createResponseWithCors(data, status, config, request) {
|
|
|
107
98
|
return response;
|
|
108
99
|
}
|
|
109
100
|
function validateSchema(schema, value, label) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (error instanceof BadRequestException)
|
|
114
|
-
throw error;
|
|
115
|
-
const message = error instanceof Error ? error.message : `Invalid ${label}`;
|
|
101
|
+
const result = schema.parse(value);
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
const message = result.error instanceof Error ? result.error.message : `Invalid ${label}`;
|
|
116
104
|
throw new BadRequestException(message);
|
|
117
105
|
}
|
|
106
|
+
return result.data;
|
|
118
107
|
}
|
|
119
108
|
function resolveServices(registrations) {
|
|
120
109
|
const serviceMap = new Map;
|
|
@@ -124,7 +113,7 @@ function resolveServices(registrations) {
|
|
|
124
113
|
let parsedOptions = {};
|
|
125
114
|
if (service.options && options) {
|
|
126
115
|
const parsed = service.options.safeParse(options);
|
|
127
|
-
if (parsed.
|
|
116
|
+
if (parsed.ok) {
|
|
128
117
|
parsedOptions = parsed.data;
|
|
129
118
|
} else {
|
|
130
119
|
throw new Error(`Invalid options for service ${service.moduleName}: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
|
|
@@ -189,6 +178,17 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
189
178
|
const resolvedMiddlewares = resolveMiddlewares(globalMiddlewares);
|
|
190
179
|
const serviceMap = resolveServices(registrations);
|
|
191
180
|
registerRoutes(trie, basePath, registrations, serviceMap);
|
|
181
|
+
if (config._entityRoutes) {
|
|
182
|
+
for (const route of config._entityRoutes) {
|
|
183
|
+
const entry = {
|
|
184
|
+
handler: route.handler,
|
|
185
|
+
options: {},
|
|
186
|
+
services: {},
|
|
187
|
+
middlewares: []
|
|
188
|
+
};
|
|
189
|
+
trie.add(route.method, route.path, entry);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
192
|
return async (request) => {
|
|
193
193
|
try {
|
|
194
194
|
if (config.cors) {
|
|
@@ -201,9 +201,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
201
201
|
if (!match) {
|
|
202
202
|
const allowed = trie.getAllowedMethods(parsed.path);
|
|
203
203
|
if (allowed.length > 0) {
|
|
204
|
-
return createJsonResponse({ error: "MethodNotAllowed", message: "Method Not Allowed"
|
|
204
|
+
return createJsonResponse({ error: { code: "MethodNotAllowed", message: "Method Not Allowed" } }, 405, { allow: allowed.join(", ") });
|
|
205
205
|
}
|
|
206
|
-
return createJsonResponse({ error: "NotFound", message: "Not Found"
|
|
206
|
+
return createJsonResponse({ error: { code: "NotFound", message: "Not Found" } }, 404);
|
|
207
207
|
}
|
|
208
208
|
const body = await parseBody(request);
|
|
209
209
|
const raw = {
|
|
@@ -246,10 +246,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
246
246
|
if (isOk(result)) {
|
|
247
247
|
const data = result.data;
|
|
248
248
|
if (config.validateResponses && entry.responseSchema) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const message = error instanceof Error ? error.message : "Response schema validation failed";
|
|
249
|
+
const validation = entry.responseSchema.parse(data);
|
|
250
|
+
if (!validation.ok) {
|
|
251
|
+
const message = validation.error instanceof Error ? validation.error.message : "Response schema validation failed";
|
|
253
252
|
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
254
253
|
}
|
|
255
254
|
}
|
|
@@ -260,10 +259,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
260
259
|
if (config.validateResponses && entry.errorsSchema) {
|
|
261
260
|
const errorSchema = entry.errorsSchema[errorStatus];
|
|
262
261
|
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}`;
|
|
262
|
+
const validation = errorSchema.parse(errorBody);
|
|
263
|
+
if (!validation.ok) {
|
|
264
|
+
const message = validation.error instanceof Error ? `Error schema validation failed for status ${errorStatus}: ${validation.error.message}` : `Error schema validation failed for status ${errorStatus}`;
|
|
267
265
|
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
268
266
|
}
|
|
269
267
|
}
|
|
@@ -272,14 +270,13 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
272
270
|
}
|
|
273
271
|
}
|
|
274
272
|
if (config.validateResponses && entry.responseSchema) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const message = error instanceof Error ? error.message : "Response schema validation failed";
|
|
273
|
+
const validation = entry.responseSchema.parse(result);
|
|
274
|
+
if (!validation.ok) {
|
|
275
|
+
const message = validation.error instanceof Error ? validation.error.message : "Response schema validation failed";
|
|
279
276
|
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
280
277
|
}
|
|
281
278
|
}
|
|
282
|
-
const response = result === undefined ? new Response(null, { status: 204 }) : createJsonResponse(result);
|
|
279
|
+
const response = result === undefined ? new Response(null, { status: 204 }) : result instanceof Response ? result : createJsonResponse(result);
|
|
283
280
|
if (config.cors) {
|
|
284
281
|
return applyCorsHeaders(config.cors, request, response);
|
|
285
282
|
}
|
|
@@ -367,6 +364,7 @@ function createApp(config) {
|
|
|
367
364
|
let globalMiddlewares = [];
|
|
368
365
|
let cachedHandler = null;
|
|
369
366
|
const registeredRoutes = [];
|
|
367
|
+
const entityRoutes = [];
|
|
370
368
|
const builder = {
|
|
371
369
|
register(module, options) {
|
|
372
370
|
registrations.push({ module, options });
|
|
@@ -395,35 +393,47 @@ function createApp(config) {
|
|
|
395
393
|
const adapter = detectAdapter();
|
|
396
394
|
const serverHandle = await adapter.listen(port ?? DEFAULT_PORT, builder.handler, options);
|
|
397
395
|
if (options?.logRoutes !== false) {
|
|
398
|
-
const
|
|
396
|
+
const moduleRoutes = collectRoutes(config.basePath ?? "", registrations);
|
|
397
|
+
const routes = [...moduleRoutes, ...entityRoutes];
|
|
399
398
|
const url = `http://${serverHandle.hostname}:${serverHandle.port}`;
|
|
400
399
|
console.log(formatRouteLog(url, routes));
|
|
401
400
|
}
|
|
402
401
|
return serverHandle;
|
|
403
402
|
}
|
|
404
403
|
};
|
|
405
|
-
if (config.
|
|
404
|
+
if (config._entityRoutes) {
|
|
405
|
+
for (const route of config._entityRoutes) {
|
|
406
|
+
const info = { method: route.method, path: route.path };
|
|
407
|
+
registeredRoutes.push(info);
|
|
408
|
+
entityRoutes.push(info);
|
|
409
|
+
}
|
|
410
|
+
} else if (config.entities && config.entities.length > 0) {
|
|
406
411
|
const rawPrefix = config.apiPrefix === undefined ? "/api/" : config.apiPrefix;
|
|
407
|
-
for (const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
412
|
+
for (const entity of config.entities) {
|
|
413
|
+
const entityPath = rawPrefix === "" ? `/${entity.name}` : (rawPrefix.endsWith("/") ? rawPrefix : `${rawPrefix}/`) + entity.name;
|
|
414
|
+
const routes = [
|
|
415
|
+
{ method: "GET", path: entityPath },
|
|
416
|
+
{ method: "GET", path: `${entityPath}/:id` },
|
|
417
|
+
{ method: "POST", path: entityPath },
|
|
418
|
+
{ method: "PATCH", path: `${entityPath}/:id` },
|
|
419
|
+
{ method: "DELETE", path: `${entityPath}/:id` }
|
|
420
|
+
];
|
|
421
|
+
if (entity.actions) {
|
|
422
|
+
for (const actionName of Object.keys(entity.actions)) {
|
|
423
|
+
routes.push({ method: "POST", path: `${entityPath}/:id/${actionName}` });
|
|
417
424
|
}
|
|
418
425
|
}
|
|
426
|
+
registeredRoutes.push(...routes);
|
|
427
|
+
entityRoutes.push(...routes);
|
|
419
428
|
}
|
|
420
429
|
}
|
|
421
430
|
return builder;
|
|
422
431
|
}
|
|
423
432
|
// src/env/env-validator.ts
|
|
424
433
|
function createEnv(config) {
|
|
425
|
-
const
|
|
426
|
-
|
|
434
|
+
const envRecord = config.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
435
|
+
const result = config.schema.safeParse(envRecord);
|
|
436
|
+
if (!result.ok) {
|
|
427
437
|
throw new Error(`Environment validation failed:
|
|
428
438
|
${result.error.message}`);
|
|
429
439
|
}
|
|
@@ -511,11 +521,7 @@ var createApp2 = (...args) => {
|
|
|
511
521
|
};
|
|
512
522
|
export {
|
|
513
523
|
vertz,
|
|
514
|
-
ok,
|
|
515
524
|
makeImmutable,
|
|
516
|
-
isOk,
|
|
517
|
-
isErr,
|
|
518
|
-
err,
|
|
519
525
|
deepFreeze,
|
|
520
526
|
createServer,
|
|
521
527
|
createModuleDef,
|
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,57 +110,60 @@ 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
|
|
@@ -334,7 +337,7 @@ function createErrorResponse(error) {
|
|
|
334
337
|
if (error instanceof VertzException) {
|
|
335
338
|
return createJsonResponse(error.toJSON(), error.statusCode);
|
|
336
339
|
}
|
|
337
|
-
return createJsonResponse({ error: "InternalServerError", message: "Internal Server Error"
|
|
340
|
+
return createJsonResponse({ error: { code: "InternalServerError", message: "Internal Server Error" } }, 500);
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
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.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz core framework primitives",
|
|
@@ -30,15 +30,16 @@
|
|
|
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.1"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^
|
|
42
|
+
"@types/node": "^25.3.1",
|
|
42
43
|
"@vitest/coverage-v8": "^4.0.18",
|
|
43
44
|
"bunup": "latest",
|
|
44
45
|
"typescript": "^5.7.0",
|