@vertz/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +702 -0
- package/dist/index.d.ts +268 -65
- package/dist/index.js +179 -8
- package/dist/internals.d.ts +2 -2
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -77,7 +77,35 @@ function applyCorsHeaders(config, request, response) {
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
|
|
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
|
+
|
|
80
101
|
// src/app/app-runner.ts
|
|
102
|
+
function createResponseWithCors(data, status, config, request) {
|
|
103
|
+
const response = createJsonResponse(data, status);
|
|
104
|
+
if (config.cors) {
|
|
105
|
+
return applyCorsHeaders(config.cors, request, response);
|
|
106
|
+
}
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
81
109
|
function validateSchema(schema, value, label) {
|
|
82
110
|
try {
|
|
83
111
|
return schema.parse(value);
|
|
@@ -90,10 +118,20 @@ function validateSchema(schema, value, label) {
|
|
|
90
118
|
}
|
|
91
119
|
function resolveServices(registrations) {
|
|
92
120
|
const serviceMap = new Map;
|
|
93
|
-
for (const { module } of registrations) {
|
|
121
|
+
for (const { module, options } of registrations) {
|
|
94
122
|
for (const service of module.services) {
|
|
95
123
|
if (!serviceMap.has(service)) {
|
|
96
|
-
|
|
124
|
+
let parsedOptions = {};
|
|
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));
|
|
97
135
|
}
|
|
98
136
|
}
|
|
99
137
|
}
|
|
@@ -123,14 +161,22 @@ function registerRoutes(trie, basePath, registrations, serviceMap) {
|
|
|
123
161
|
const resolvedServices = resolveRouterServices(router.inject, serviceMap);
|
|
124
162
|
for (const route of router.routes) {
|
|
125
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
|
+
}));
|
|
126
169
|
const entry = {
|
|
127
170
|
handler: route.config.handler,
|
|
128
171
|
options: options ?? {},
|
|
129
172
|
services: resolvedServices,
|
|
173
|
+
middlewares: routeMiddlewares,
|
|
130
174
|
paramsSchema: route.config.params,
|
|
131
175
|
bodySchema: route.config.body,
|
|
132
176
|
querySchema: route.config.query,
|
|
133
|
-
headersSchema: route.config.headers
|
|
177
|
+
headersSchema: route.config.headers,
|
|
178
|
+
responseSchema: route.config.response,
|
|
179
|
+
errorsSchema: route.config.errors
|
|
134
180
|
};
|
|
135
181
|
trie.add(route.method, fullPath, entry);
|
|
136
182
|
}
|
|
@@ -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,6 +242,43 @@ function buildHandler(config, registrations, globalMiddlewares) {
|
|
|
191
242
|
env: {}
|
|
192
243
|
});
|
|
193
244
|
const result = await entry.handler(ctx);
|
|
245
|
+
if (isResult(result)) {
|
|
246
|
+
if (isOk(result)) {
|
|
247
|
+
const data = result.data;
|
|
248
|
+
if (config.validateResponses && entry.responseSchema) {
|
|
249
|
+
try {
|
|
250
|
+
entry.responseSchema.parse(data);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
const message = error instanceof Error ? error.message : "Response schema validation failed";
|
|
253
|
+
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return createResponseWithCors(data, 200, config, request);
|
|
257
|
+
} else {
|
|
258
|
+
const errorStatus = result.status;
|
|
259
|
+
const errorBody = result.body;
|
|
260
|
+
if (config.validateResponses && entry.errorsSchema) {
|
|
261
|
+
const errorSchema = entry.errorsSchema[errorStatus];
|
|
262
|
+
if (errorSchema) {
|
|
263
|
+
try {
|
|
264
|
+
errorSchema.parse(errorBody);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const message = error instanceof Error ? `Error schema validation failed for status ${errorStatus}: ${error.message}` : `Error schema validation failed for status ${errorStatus}`;
|
|
267
|
+
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return createResponseWithCors(errorBody, errorStatus, config, request);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (config.validateResponses && entry.responseSchema) {
|
|
275
|
+
try {
|
|
276
|
+
entry.responseSchema.parse(result);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
const message = error instanceof Error ? error.message : "Response schema validation failed";
|
|
279
|
+
console.warn(`[vertz] Response validation warning: ${message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
194
282
|
const response = result === undefined ? new Response(null, { status: 204 }) : createJsonResponse(result);
|
|
195
283
|
if (config.cors) {
|
|
196
284
|
return applyCorsHeaders(config.cors, request, response);
|
|
@@ -234,19 +322,64 @@ function detectAdapter(hints) {
|
|
|
234
322
|
throw new Error("No supported server runtime detected. Vertz requires Bun to use app.listen().");
|
|
235
323
|
}
|
|
236
324
|
|
|
325
|
+
// 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
|
+
function formatRouteLog(listenUrl, routes) {
|
|
348
|
+
const header = `vertz server listening on ${listenUrl}`;
|
|
349
|
+
if (routes.length === 0) {
|
|
350
|
+
return header;
|
|
351
|
+
}
|
|
352
|
+
const sorted = [...routes].sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
353
|
+
const MIN_METHOD_WIDTH = 6;
|
|
354
|
+
const maxMethodLen = Math.max(MIN_METHOD_WIDTH, ...sorted.map((r) => r.method.length));
|
|
355
|
+
const lines = sorted.map((r) => {
|
|
356
|
+
const paddedMethod = r.method.padEnd(maxMethodLen);
|
|
357
|
+
return ` ${paddedMethod} ${r.path}`;
|
|
358
|
+
});
|
|
359
|
+
return [header, "", ...lines].join(`
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
|
|
237
363
|
// src/app/app-builder.ts
|
|
238
364
|
var DEFAULT_PORT = 3000;
|
|
239
365
|
function createApp(config) {
|
|
240
366
|
const registrations = [];
|
|
241
367
|
let globalMiddlewares = [];
|
|
242
368
|
let cachedHandler = null;
|
|
369
|
+
const registeredRoutes = [];
|
|
243
370
|
const builder = {
|
|
244
371
|
register(module, options) {
|
|
245
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;
|
|
246
379
|
return builder;
|
|
247
380
|
},
|
|
248
381
|
middlewares(list) {
|
|
249
|
-
globalMiddlewares = list;
|
|
382
|
+
globalMiddlewares = [...list];
|
|
250
383
|
return builder;
|
|
251
384
|
},
|
|
252
385
|
get handler() {
|
|
@@ -255,11 +388,36 @@ function createApp(config) {
|
|
|
255
388
|
}
|
|
256
389
|
return cachedHandler;
|
|
257
390
|
},
|
|
391
|
+
get router() {
|
|
392
|
+
return { routes: registeredRoutes };
|
|
393
|
+
},
|
|
258
394
|
async listen(port, options) {
|
|
259
395
|
const adapter = detectAdapter();
|
|
260
|
-
|
|
396
|
+
const serverHandle = await adapter.listen(port ?? DEFAULT_PORT, builder.handler, options);
|
|
397
|
+
if (options?.logRoutes !== false) {
|
|
398
|
+
const routes = collectRoutes(config.basePath ?? "", registrations);
|
|
399
|
+
const url = `http://${serverHandle.hostname}:${serverHandle.port}`;
|
|
400
|
+
console.log(formatRouteLog(url, routes));
|
|
401
|
+
}
|
|
402
|
+
return serverHandle;
|
|
261
403
|
}
|
|
262
404
|
};
|
|
405
|
+
if (config.domains && config.domains.length > 0) {
|
|
406
|
+
const rawPrefix = config.apiPrefix === undefined ? "/api/" : config.apiPrefix;
|
|
407
|
+
for (const domain of config.domains) {
|
|
408
|
+
const domainPath = rawPrefix === "" ? "/" + domain.name : (rawPrefix.endsWith("/") ? rawPrefix : rawPrefix + "/") + domain.name;
|
|
409
|
+
registeredRoutes.push({ method: "GET", path: domainPath });
|
|
410
|
+
registeredRoutes.push({ method: "GET", path: `${domainPath}/:id` });
|
|
411
|
+
registeredRoutes.push({ method: "POST", path: domainPath });
|
|
412
|
+
registeredRoutes.push({ method: "PUT", path: `${domainPath}/:id` });
|
|
413
|
+
registeredRoutes.push({ method: "DELETE", path: `${domainPath}/:id` });
|
|
414
|
+
if (domain.actions) {
|
|
415
|
+
for (const actionName of Object.keys(domain.actions)) {
|
|
416
|
+
registeredRoutes.push({ method: "POST", path: `${domainPath}/:id/${actionName}` });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
263
421
|
return builder;
|
|
264
422
|
}
|
|
265
423
|
// src/env/env-validator.ts
|
|
@@ -336,23 +494,36 @@ function createModuleDef(config) {
|
|
|
336
494
|
return deepFreeze(def);
|
|
337
495
|
}
|
|
338
496
|
// src/vertz.ts
|
|
339
|
-
var vertz = deepFreeze({
|
|
497
|
+
var vertz = /* @__PURE__ */ deepFreeze({
|
|
340
498
|
env: createEnv,
|
|
341
499
|
middleware: createMiddleware,
|
|
342
500
|
moduleDef: createModuleDef,
|
|
343
501
|
module: createModule,
|
|
344
|
-
app: createApp
|
|
502
|
+
app: createApp,
|
|
503
|
+
server: createApp
|
|
345
504
|
});
|
|
505
|
+
|
|
506
|
+
// src/index.ts
|
|
507
|
+
var createServer = createApp;
|
|
508
|
+
var createApp2 = (...args) => {
|
|
509
|
+
console.warn("⚠️ createApp() is deprecated. Use createServer() from @vertz/server instead.");
|
|
510
|
+
return createApp(...args);
|
|
511
|
+
};
|
|
346
512
|
export {
|
|
347
513
|
vertz,
|
|
514
|
+
ok,
|
|
348
515
|
makeImmutable,
|
|
516
|
+
isOk,
|
|
517
|
+
isErr,
|
|
518
|
+
err,
|
|
349
519
|
deepFreeze,
|
|
520
|
+
createServer,
|
|
350
521
|
createModuleDef,
|
|
351
522
|
createModule,
|
|
352
523
|
createMiddleware,
|
|
353
524
|
createImmutableProxy,
|
|
354
525
|
createEnv,
|
|
355
|
-
createApp,
|
|
526
|
+
createApp2 as createApp,
|
|
356
527
|
VertzException,
|
|
357
528
|
ValidationException,
|
|
358
529
|
UnauthorizedException,
|
package/dist/internals.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ interface RawRequest {
|
|
|
7
7
|
interface HandlerCtx {
|
|
8
8
|
params: Record<string, unknown>;
|
|
9
9
|
body: unknown;
|
|
10
|
-
query: Record<string,
|
|
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,
|
|
20
|
+
query: Record<string, string>;
|
|
21
21
|
headers: Record<string, unknown>;
|
|
22
22
|
raw: RawRequest;
|
|
23
23
|
middlewareState: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -38,11 +39,13 @@
|
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/node": "^22.0.0",
|
|
42
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
41
43
|
"bunup": "latest",
|
|
42
44
|
"typescript": "^5.7.0",
|
|
43
|
-
"vitest": "^
|
|
45
|
+
"vitest": "^4.0.18"
|
|
44
46
|
},
|
|
45
47
|
"engines": {
|
|
46
48
|
"node": ">=22"
|
|
47
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"sideEffects": false
|
|
48
51
|
}
|