@vertz/core 0.1.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/dist/index.js CHANGED
@@ -18,7 +18,19 @@ import {
18
18
  parseBody,
19
19
  parseRequest,
20
20
  runMiddlewareChain
21
- } from "./shared/chunk-c77pg5gx.js";
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"];
@@ -78,22 +90,37 @@ function applyCorsHeaders(config, request, response) {
78
90
  }
79
91
 
80
92
  // src/app/app-runner.ts
93
+ function createResponseWithCors(data, status, config, request) {
94
+ const response = createJsonResponse(data, status);
95
+ if (config.cors) {
96
+ return applyCorsHeaders(config.cors, request, response);
97
+ }
98
+ return response;
99
+ }
81
100
  function validateSchema(schema, value, label) {
82
- try {
83
- return schema.parse(value);
84
- } catch (error) {
85
- if (error instanceof BadRequestException)
86
- throw error;
87
- 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}`;
88
104
  throw new BadRequestException(message);
89
105
  }
106
+ return result.data;
90
107
  }
91
108
  function resolveServices(registrations) {
92
109
  const serviceMap = new Map;
93
- for (const { module } of registrations) {
110
+ for (const { module, options } of registrations) {
94
111
  for (const service of module.services) {
95
112
  if (!serviceMap.has(service)) {
96
- serviceMap.set(service, service.methods({}, undefined));
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));
97
124
  }
98
125
  }
99
126
  }
@@ -123,14 +150,22 @@ function registerRoutes(trie, basePath, registrations, serviceMap) {
123
150
  const resolvedServices = resolveRouterServices(router.inject, serviceMap);
124
151
  for (const route of router.routes) {
125
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
+ }));
126
158
  const entry = {
127
159
  handler: route.config.handler,
128
160
  options: options ?? {},
129
161
  services: resolvedServices,
162
+ middlewares: routeMiddlewares,
130
163
  paramsSchema: route.config.params,
131
164
  bodySchema: route.config.body,
132
165
  querySchema: route.config.query,
133
- headersSchema: route.config.headers
166
+ headersSchema: route.config.headers,
167
+ responseSchema: route.config.response,
168
+ errorsSchema: route.config.errors
134
169
  };
135
170
  trie.add(route.method, fullPath, entry);
136
171
  }
@@ -143,6 +178,17 @@ function buildHandler(config, registrations, globalMiddlewares) {
143
178
  const resolvedMiddlewares = resolveMiddlewares(globalMiddlewares);
144
179
  const serviceMap = resolveServices(registrations);
145
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
+ }
146
192
  return async (request) => {
147
193
  try {
148
194
  if (config.cors) {
@@ -155,9 +201,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
155
201
  if (!match) {
156
202
  const allowed = trie.getAllowedMethods(parsed.path);
157
203
  if (allowed.length > 0) {
158
- return createJsonResponse({ error: "MethodNotAllowed", message: "Method Not Allowed", statusCode: 405 }, 405, { allow: allowed.join(", ") });
204
+ return createJsonResponse({ error: { code: "MethodNotAllowed", message: "Method Not Allowed" } }, 405, { allow: allowed.join(", ") });
159
205
  }
160
- return createJsonResponse({ error: "NotFound", message: "Not Found", statusCode: 404 }, 404);
206
+ return createJsonResponse({ error: { code: "NotFound", message: "Not Found" } }, 404);
161
207
  }
162
208
  const body = await parseBody(request);
163
209
  const raw = {
@@ -175,6 +221,11 @@ function buildHandler(config, registrations, globalMiddlewares) {
175
221
  };
176
222
  const middlewareState = await runMiddlewareChain(resolvedMiddlewares, requestCtx);
177
223
  const entry = match.handler;
224
+ if (entry.middlewares.length > 0) {
225
+ const routeCtx = { ...requestCtx, ...middlewareState };
226
+ const routeState = await runMiddlewareChain(entry.middlewares, routeCtx);
227
+ Object.assign(middlewareState, routeState);
228
+ }
178
229
  const validatedParams = entry.paramsSchema ? validateSchema(entry.paramsSchema, match.params, "params") : match.params;
179
230
  const validatedBody = entry.bodySchema ? validateSchema(entry.bodySchema, body, "body") : body;
180
231
  const validatedQuery = entry.querySchema ? validateSchema(entry.querySchema, parsed.query, "query") : parsed.query;
@@ -191,7 +242,41 @@ function buildHandler(config, registrations, globalMiddlewares) {
191
242
  env: {}
192
243
  });
193
244
  const result = await entry.handler(ctx);
194
- const response = result === undefined ? new Response(null, { status: 204 }) : createJsonResponse(result);
245
+ if (isResult(result)) {
246
+ if (isOk(result)) {
247
+ const data = result.data;
248
+ if (config.validateResponses && entry.responseSchema) {
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";
252
+ console.warn(`[vertz] Response validation warning: ${message}`);
253
+ }
254
+ }
255
+ return createResponseWithCors(data, 200, config, request);
256
+ } else {
257
+ const errorStatus = result.status;
258
+ const errorBody = result.body;
259
+ if (config.validateResponses && entry.errorsSchema) {
260
+ const errorSchema = entry.errorsSchema[errorStatus];
261
+ if (errorSchema) {
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}`;
265
+ console.warn(`[vertz] Response validation warning: ${message}`);
266
+ }
267
+ }
268
+ }
269
+ return createResponseWithCors(errorBody, errorStatus, config, request);
270
+ }
271
+ }
272
+ if (config.validateResponses && entry.responseSchema) {
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";
276
+ console.warn(`[vertz] Response validation warning: ${message}`);
277
+ }
278
+ }
279
+ const response = result === undefined ? new Response(null, { status: 204 }) : result instanceof Response ? result : createJsonResponse(result);
195
280
  if (config.cors) {
196
281
  return applyCorsHeaders(config.cors, request, response);
197
282
  }
@@ -234,19 +319,65 @@ function detectAdapter(hints) {
234
319
  throw new Error("No supported server runtime detected. Vertz requires Bun to use app.listen().");
235
320
  }
236
321
 
322
+ // 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
+ function formatRouteLog(listenUrl, routes) {
345
+ const header = `vertz server listening on ${listenUrl}`;
346
+ if (routes.length === 0) {
347
+ return header;
348
+ }
349
+ const sorted = [...routes].sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
350
+ const MIN_METHOD_WIDTH = 6;
351
+ const maxMethodLen = Math.max(MIN_METHOD_WIDTH, ...sorted.map((r) => r.method.length));
352
+ const lines = sorted.map((r) => {
353
+ const paddedMethod = r.method.padEnd(maxMethodLen);
354
+ return ` ${paddedMethod} ${r.path}`;
355
+ });
356
+ return [header, "", ...lines].join(`
357
+ `);
358
+ }
359
+
237
360
  // src/app/app-builder.ts
238
361
  var DEFAULT_PORT = 3000;
239
362
  function createApp(config) {
240
363
  const registrations = [];
241
364
  let globalMiddlewares = [];
242
365
  let cachedHandler = null;
366
+ const registeredRoutes = [];
367
+ const entityRoutes = [];
243
368
  const builder = {
244
369
  register(module, options) {
245
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;
246
377
  return builder;
247
378
  },
248
379
  middlewares(list) {
249
- globalMiddlewares = list;
380
+ globalMiddlewares = [...list];
250
381
  return builder;
251
382
  },
252
383
  get handler() {
@@ -255,17 +386,54 @@ function createApp(config) {
255
386
  }
256
387
  return cachedHandler;
257
388
  },
389
+ get router() {
390
+ return { routes: registeredRoutes };
391
+ },
258
392
  async listen(port, options) {
259
393
  const adapter = detectAdapter();
260
- return adapter.listen(port ?? DEFAULT_PORT, builder.handler, options);
394
+ const serverHandle = await adapter.listen(port ?? DEFAULT_PORT, builder.handler, options);
395
+ if (options?.logRoutes !== false) {
396
+ const moduleRoutes = collectRoutes(config.basePath ?? "", registrations);
397
+ const routes = [...moduleRoutes, ...entityRoutes];
398
+ const url = `http://${serverHandle.hostname}:${serverHandle.port}`;
399
+ console.log(formatRouteLog(url, routes));
400
+ }
401
+ return serverHandle;
261
402
  }
262
403
  };
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) {
411
+ const rawPrefix = config.apiPrefix === undefined ? "/api/" : config.apiPrefix;
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}` });
424
+ }
425
+ }
426
+ registeredRoutes.push(...routes);
427
+ entityRoutes.push(...routes);
428
+ }
429
+ }
263
430
  return builder;
264
431
  }
265
432
  // src/env/env-validator.ts
266
433
  function createEnv(config) {
267
- const result = config.schema.safeParse(process.env);
268
- if (!result.success) {
434
+ const envRecord = config.env ?? (typeof process !== "undefined" ? process.env : {});
435
+ const result = config.schema.safeParse(envRecord);
436
+ if (!result.ok) {
269
437
  throw new Error(`Environment validation failed:
270
438
  ${result.error.message}`);
271
439
  }
@@ -336,23 +504,32 @@ function createModuleDef(config) {
336
504
  return deepFreeze(def);
337
505
  }
338
506
  // src/vertz.ts
339
- var vertz = deepFreeze({
507
+ var vertz = /* @__PURE__ */ deepFreeze({
340
508
  env: createEnv,
341
509
  middleware: createMiddleware,
342
510
  moduleDef: createModuleDef,
343
511
  module: createModule,
344
- app: createApp
512
+ app: createApp,
513
+ server: createApp
345
514
  });
515
+
516
+ // src/index.ts
517
+ var createServer = createApp;
518
+ var createApp2 = (...args) => {
519
+ console.warn("⚠️ createApp() is deprecated. Use createServer() from @vertz/server instead.");
520
+ return createApp(...args);
521
+ };
346
522
  export {
347
523
  vertz,
348
524
  makeImmutable,
349
525
  deepFreeze,
526
+ createServer,
350
527
  createModuleDef,
351
528
  createModule,
352
529
  createMiddleware,
353
530
  createImmutableProxy,
354
531
  createEnv,
355
- createApp,
532
+ createApp2 as createApp,
356
533
  VertzException,
357
534
  ValidationException,
358
535
  UnauthorizedException,
@@ -7,7 +7,7 @@ interface RawRequest {
7
7
  interface HandlerCtx {
8
8
  params: Record<string, unknown>;
9
9
  body: unknown;
10
- query: Record<string, unknown>;
10
+ query: Record<string, string>;
11
11
  headers: Record<string, unknown>;
12
12
  raw: RawRequest;
13
13
  options: Record<string, unknown>;
@@ -17,7 +17,7 @@ interface HandlerCtx {
17
17
  interface CtxConfig {
18
18
  params: Record<string, unknown>;
19
19
  body: unknown;
20
- query: Record<string, unknown>;
20
+ query: Record<string, string>;
21
21
  headers: Record<string, unknown>;
22
22
  raw: RawRequest;
23
23
  middlewareState: Record<string, unknown>;
package/dist/internals.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  parseBody,
7
7
  parseRequest,
8
8
  runMiddlewareChain
9
- } from "./shared/chunk-c77pg5gx.js";
9
+ } from "./shared/chunk-k596zpc6.js";
10
10
  export {
11
11
  runMiddlewareChain,
12
12
  parseRequest,
@@ -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: this.name,
102
- message: this.message,
103
- statusCode: this.statusCode,
104
- code: this.code,
105
- ...this.details !== undefined && { details: this.details }
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, undefined, details);
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, undefined, details);
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, undefined, details);
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, undefined, details);
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, undefined, details);
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, undefined, undefined);
144
+ super("Validation failed", 422, "ValidationError", undefined);
145
145
  this.errors = errors;
146
146
  }
147
147
  toJSON() {
148
148
  return {
149
- ...super.toJSON(),
150
- errors: this.errors
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, undefined, details);
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, undefined, details);
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", statusCode: 500 }, 500);
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,8 +1,9 @@
1
1
  {
2
2
  "name": "@vertz/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
+ "description": "Vertz core framework primitives",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "https://github.com/vertz-dev/vertz.git",
@@ -29,20 +30,23 @@
29
30
  ],
30
31
  "scripts": {
31
32
  "build": "bunup",
32
- "test": "vitest run",
33
+ "test": "bun test",
34
+ "test:coverage": "vitest run --coverage",
33
35
  "test:watch": "vitest",
34
36
  "typecheck": "tsc --noEmit"
35
37
  },
36
38
  "dependencies": {
37
- "@vertz/schema": "workspace:*"
39
+ "@vertz/schema": "0.2.1"
38
40
  },
39
41
  "devDependencies": {
40
- "@types/node": "^22.0.0",
42
+ "@types/node": "^25.3.1",
43
+ "@vitest/coverage-v8": "^4.0.18",
41
44
  "bunup": "latest",
42
45
  "typescript": "^5.7.0",
43
- "vitest": "^3.0.0"
46
+ "vitest": "^4.0.18"
44
47
  },
45
48
  "engines": {
46
49
  "node": ">=22"
47
- }
50
+ },
51
+ "sideEffects": false
48
52
  }