@coherent.js/api 1.0.0-beta.3 → 1.0.0-beta.5
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.cjs +135 -51
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +135 -51
- package/dist/index.js.map +3 -3
- package/package.json +26 -2
- package/dist/api/errors.d.ts +0 -92
- package/dist/api/errors.d.ts.map +0 -1
- package/dist/api/errors.js +0 -161
- package/dist/api/errors.js.map +0 -1
- package/dist/api/index.d.ts +0 -61
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -41
- package/dist/api/index.js.map +0 -1
- package/dist/api/middleware.d.ts +0 -57
- package/dist/api/middleware.d.ts.map +0 -1
- package/dist/api/middleware.js +0 -244
- package/dist/api/middleware.js.map +0 -1
- package/dist/api/openapi.d.ts +0 -54
- package/dist/api/openapi.d.ts.map +0 -1
- package/dist/api/openapi.js +0 -144
- package/dist/api/openapi.js.map +0 -1
- package/dist/api/router.d.ts +0 -30
- package/dist/api/router.d.ts.map +0 -1
- package/dist/api/router.js +0 -1476
- package/dist/api/router.js.map +0 -1
- package/dist/api/security.d.ts +0 -64
- package/dist/api/security.d.ts.map +0 -1
- package/dist/api/security.js +0 -239
- package/dist/api/security.js.map +0 -1
- package/dist/api/serialization.d.ts +0 -86
- package/dist/api/serialization.d.ts.map +0 -1
- package/dist/api/serialization.js +0 -151
- package/dist/api/serialization.js.map +0 -1
- package/dist/api/validation.d.ts +0 -34
- package/dist/api/validation.d.ts.map +0 -1
- package/dist/api/validation.js +0 -172
- package/dist/api/validation.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -194,8 +194,10 @@ function validateAgainstSchema(schema, data) {
|
|
|
194
194
|
for (const [field, fieldSchema] of Object.entries(schema.properties)) {
|
|
195
195
|
if (field in data) {
|
|
196
196
|
const fieldValue = data[field];
|
|
197
|
-
const
|
|
198
|
-
|
|
197
|
+
const fieldResult = validateField(fieldSchema, fieldValue, field);
|
|
198
|
+
if (!fieldResult.valid) {
|
|
199
|
+
errors.push(...fieldResult.errors);
|
|
200
|
+
}
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
203
|
}
|
|
@@ -207,6 +209,15 @@ function validateAgainstSchema(schema, data) {
|
|
|
207
209
|
}
|
|
208
210
|
function validateField(schema, value, fieldName) {
|
|
209
211
|
const errors = [];
|
|
212
|
+
if (value === null || value === void 0) {
|
|
213
|
+
if (schema.type && schema.type !== "null") {
|
|
214
|
+
errors.push({
|
|
215
|
+
field: fieldName,
|
|
216
|
+
message: `Expected ${schema.type}, got ${value === null ? "null" : "undefined"}`
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return { valid: errors.length === 0, errors };
|
|
220
|
+
}
|
|
210
221
|
if (schema.type) {
|
|
211
222
|
if (schema.type === "string" && typeof value !== "string") {
|
|
212
223
|
errors.push({
|
|
@@ -230,7 +241,7 @@ function validateField(schema, value, fieldName) {
|
|
|
230
241
|
});
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
|
-
if (schema.type === "string") {
|
|
244
|
+
if (schema.type === "string" && typeof value === "string") {
|
|
234
245
|
if (schema.minLength && value.length < schema.minLength) {
|
|
235
246
|
errors.push({
|
|
236
247
|
field: fieldName,
|
|
@@ -250,7 +261,7 @@ function validateField(schema, value, fieldName) {
|
|
|
250
261
|
});
|
|
251
262
|
}
|
|
252
263
|
}
|
|
253
|
-
if (schema.type === "number") {
|
|
264
|
+
if (schema.type === "number" && typeof value === "number") {
|
|
254
265
|
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
255
266
|
errors.push({
|
|
256
267
|
field: fieldName,
|
|
@@ -264,7 +275,7 @@ function validateField(schema, value, fieldName) {
|
|
|
264
275
|
});
|
|
265
276
|
}
|
|
266
277
|
}
|
|
267
|
-
return errors;
|
|
278
|
+
return { valid: errors.length === 0, errors };
|
|
268
279
|
}
|
|
269
280
|
function withValidation(schema) {
|
|
270
281
|
return (req, res, next) => {
|
|
@@ -494,6 +505,9 @@ function registerRoute(method, config, router, path) {
|
|
|
494
505
|
}
|
|
495
506
|
}, { name });
|
|
496
507
|
}
|
|
508
|
+
function isStaticRoute(pattern) {
|
|
509
|
+
return !pattern.includes(":") && !pattern.includes("*") && !pattern.includes("(") && !pattern.includes("?");
|
|
510
|
+
}
|
|
497
511
|
var SimpleRouter = class {
|
|
498
512
|
constructor(options = {}) {
|
|
499
513
|
this.routes = [];
|
|
@@ -505,6 +519,7 @@ var SimpleRouter = class {
|
|
|
505
519
|
this.enableCompilation = options.enableCompilation !== false;
|
|
506
520
|
this.compiledRoutes = /* @__PURE__ */ new Map();
|
|
507
521
|
this.routeCompilationCache = /* @__PURE__ */ new Map();
|
|
522
|
+
this.maxCompilationCacheSize = options.maxCompilationCacheSize || 1e3;
|
|
508
523
|
this.enableVersioning = options.enableVersioning || false;
|
|
509
524
|
this.defaultVersion = options.defaultVersion || "v1";
|
|
510
525
|
this.versionHeader = options.versionHeader || "api-version";
|
|
@@ -533,10 +548,15 @@ var SimpleRouter = class {
|
|
|
533
548
|
// Track WebSocket messages
|
|
534
549
|
};
|
|
535
550
|
}
|
|
551
|
+
this.enableSecurityHeaders = options.enableSecurityHeaders !== false;
|
|
552
|
+
this.enableCORS = options.enableCORS !== false;
|
|
553
|
+
this.enableSmartRouting = options.enableSmartRouting !== false;
|
|
554
|
+
this.staticRoutes = /* @__PURE__ */ new Map();
|
|
555
|
+
this.enableRouteMetrics = options.enableRouteMetrics || false;
|
|
536
556
|
}
|
|
537
557
|
/**
|
|
538
558
|
* Add an HTTP route to the router
|
|
539
|
-
*
|
|
559
|
+
*
|
|
540
560
|
* @param {string} method - HTTP method (GET, POST, PUT, DELETE, PATCH)
|
|
541
561
|
* @param {string} path - Route path pattern (supports :param and wildcards)
|
|
542
562
|
* @param {Function} handler - Route handler function
|
|
@@ -544,7 +564,7 @@ var SimpleRouter = class {
|
|
|
544
564
|
* @param {Array} [options.middleware] - Route-specific middleware
|
|
545
565
|
* @param {string} [options.name] - Named route for URL generation
|
|
546
566
|
* @param {string} [options.version] - API version for this route
|
|
547
|
-
*
|
|
567
|
+
*
|
|
548
568
|
* @example
|
|
549
569
|
* router.addRoute('GET', '/users/:id', (req, res) => {
|
|
550
570
|
* return { user: { id: req.params.id } };
|
|
@@ -567,6 +587,11 @@ var SimpleRouter = class {
|
|
|
567
587
|
if (this.enableCompilation) {
|
|
568
588
|
route.compiled = this.compileRoute(fullPath);
|
|
569
589
|
}
|
|
590
|
+
if (this.enableSmartRouting && isStaticRoute(fullPath)) {
|
|
591
|
+
const staticKey = `${route.method}:${fullPath}`;
|
|
592
|
+
this.staticRoutes.set(staticKey, route);
|
|
593
|
+
route.isStatic = true;
|
|
594
|
+
}
|
|
570
595
|
this.routes.push(route);
|
|
571
596
|
if (this.enableVersioning) {
|
|
572
597
|
if (!this.versionedRoutes.has(route.version)) {
|
|
@@ -880,7 +905,7 @@ var SimpleRouter = class {
|
|
|
880
905
|
}
|
|
881
906
|
});
|
|
882
907
|
socket.on("_error", (err) => {
|
|
883
|
-
console.
|
|
908
|
+
console.error("WebSocket socket _error (connection likely closed):", err.code);
|
|
884
909
|
});
|
|
885
910
|
return ws;
|
|
886
911
|
}
|
|
@@ -939,7 +964,7 @@ var SimpleRouter = class {
|
|
|
939
964
|
try {
|
|
940
965
|
ws.send(message);
|
|
941
966
|
} catch {
|
|
942
|
-
console.
|
|
967
|
+
console.error("Failed to send message to connection:", id);
|
|
943
968
|
}
|
|
944
969
|
}
|
|
945
970
|
}
|
|
@@ -977,16 +1002,16 @@ var SimpleRouter = class {
|
|
|
977
1002
|
}
|
|
978
1003
|
/**
|
|
979
1004
|
* Generate URL for named route with parameter substitution
|
|
980
|
-
*
|
|
1005
|
+
*
|
|
981
1006
|
* @param {string} name - Route name (set during route registration)
|
|
982
1007
|
* @param {Object} [params={}] - Parameters to substitute in the URL pattern
|
|
983
1008
|
* @returns {string} Generated URL with parameters substituted
|
|
984
1009
|
* @throws {Error} If named route is not found
|
|
985
|
-
*
|
|
1010
|
+
*
|
|
986
1011
|
* @example
|
|
987
1012
|
* // Route registered as: router.addRoute('GET', '/users/:id', handler, { name: 'getUser' })
|
|
988
1013
|
* const url = router.url('getUser', { id: 123 }); // '/users/123'
|
|
989
|
-
*
|
|
1014
|
+
*
|
|
990
1015
|
* // With constrained parameters
|
|
991
1016
|
* const url = router.url('getUserPosts', { userId: 123, postId: 456 }); // '/users/123/posts/456'
|
|
992
1017
|
*/
|
|
@@ -1004,11 +1029,11 @@ var SimpleRouter = class {
|
|
|
1004
1029
|
}
|
|
1005
1030
|
/**
|
|
1006
1031
|
* Add routes from configuration object
|
|
1007
|
-
*
|
|
1032
|
+
*
|
|
1008
1033
|
* @param {Object} routeConfig - Route configuration object with nested structure
|
|
1009
1034
|
* @description Processes nested route objects and registers HTTP and WebSocket routes.
|
|
1010
1035
|
* Supports declarative route definition with automatic method detection.
|
|
1011
|
-
*
|
|
1036
|
+
*
|
|
1012
1037
|
* @example
|
|
1013
1038
|
* router.addRoutes({
|
|
1014
1039
|
* 'api': {
|
|
@@ -1024,17 +1049,17 @@ var SimpleRouter = class {
|
|
|
1024
1049
|
}
|
|
1025
1050
|
/**
|
|
1026
1051
|
* Add global middleware to the router
|
|
1027
|
-
*
|
|
1052
|
+
*
|
|
1028
1053
|
* @param {Function|Object} middleware - Middleware function or conditional middleware object
|
|
1029
1054
|
* @description Adds middleware that runs before all route handlers. Supports both
|
|
1030
1055
|
* simple functions and conditional middleware objects.
|
|
1031
|
-
*
|
|
1056
|
+
*
|
|
1032
1057
|
* @example
|
|
1033
1058
|
* // Simple middleware
|
|
1034
1059
|
* router.use((req, res) => {
|
|
1035
1060
|
* console.log(`${req.method} ${req.url}`);
|
|
1036
1061
|
* });
|
|
1037
|
-
*
|
|
1062
|
+
*
|
|
1038
1063
|
* // Conditional middleware
|
|
1039
1064
|
* router.use({
|
|
1040
1065
|
* condition: (req) => req.url.startsWith('/api'),
|
|
@@ -1164,7 +1189,10 @@ var SimpleRouter = class {
|
|
|
1164
1189
|
compileRoute(pattern) {
|
|
1165
1190
|
if (this.routeCompilationCache.has(pattern)) {
|
|
1166
1191
|
if (this.enableMetrics) this.metrics.compilationHits++;
|
|
1167
|
-
|
|
1192
|
+
const compiled2 = this.routeCompilationCache.get(pattern);
|
|
1193
|
+
this.routeCompilationCache.delete(pattern);
|
|
1194
|
+
this.routeCompilationCache.set(pattern, compiled2);
|
|
1195
|
+
return compiled2;
|
|
1168
1196
|
}
|
|
1169
1197
|
const paramNames = [];
|
|
1170
1198
|
let regexPattern = pattern;
|
|
@@ -1175,25 +1203,38 @@ var SimpleRouter = class {
|
|
|
1175
1203
|
regexPattern = regexPattern.replace(/\/\*/g, "/([^/]+)");
|
|
1176
1204
|
paramNames.push("splat");
|
|
1177
1205
|
}
|
|
1178
|
-
regexPattern = regexPattern.replace(/:([^(/]+)(\([^)]+\))?(\?)?/g, (match, paramName, constraint, optional) => {
|
|
1206
|
+
regexPattern = regexPattern.replace(/:([^(/]+)(\([^)]+\))?(\?)?/g, (match, paramName, constraint, optional, offset, fullString) => {
|
|
1179
1207
|
paramNames.push(paramName);
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1208
|
+
const hasPrecedingSlash = offset > 0 && fullString[offset - 1] === "/";
|
|
1209
|
+
if (hasPrecedingSlash) {
|
|
1210
|
+
if (constraint) {
|
|
1211
|
+
const constraintPattern = constraint.slice(1, -1);
|
|
1212
|
+
return optional ? `(?:/(?:${constraintPattern}))?` : `(${constraintPattern})`;
|
|
1213
|
+
} else {
|
|
1214
|
+
return optional ? `(?:([^/]+))?` : `([^/]+)`;
|
|
1215
|
+
}
|
|
1183
1216
|
} else {
|
|
1184
|
-
|
|
1217
|
+
if (constraint) {
|
|
1218
|
+
const constraintPattern = constraint.slice(1, -1);
|
|
1219
|
+
return optional ? `(?:/(?:${constraintPattern}))?` : `/${constraintPattern}`;
|
|
1220
|
+
} else {
|
|
1221
|
+
return optional ? `(?:/([^/]+))?` : `/([^/]+)`;
|
|
1222
|
+
}
|
|
1185
1223
|
}
|
|
1186
1224
|
});
|
|
1187
|
-
regexPattern = regexPattern.replace(/[.+?^${}
|
|
1225
|
+
regexPattern = regexPattern.replace(/([.+?^${}|\\[\]()]])/g, "\\$1");
|
|
1226
|
+
regexPattern = regexPattern.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\^/g, "^");
|
|
1188
1227
|
regexPattern = `^${regexPattern}$`;
|
|
1189
1228
|
const compiled = {
|
|
1190
1229
|
regex: new RegExp(regexPattern),
|
|
1191
1230
|
paramNames,
|
|
1192
1231
|
pattern
|
|
1193
1232
|
};
|
|
1194
|
-
if (this.routeCompilationCache.size
|
|
1195
|
-
this.routeCompilationCache.
|
|
1233
|
+
if (this.routeCompilationCache.size >= this.maxCompilationCacheSize) {
|
|
1234
|
+
const firstKey = this.routeCompilationCache.keys().next().value;
|
|
1235
|
+
this.routeCompilationCache.delete(firstKey);
|
|
1196
1236
|
}
|
|
1237
|
+
this.routeCompilationCache.set(pattern, compiled);
|
|
1197
1238
|
return compiled;
|
|
1198
1239
|
}
|
|
1199
1240
|
/**
|
|
@@ -1369,7 +1410,15 @@ var SimpleRouter = class {
|
|
|
1369
1410
|
this.metrics.requests++;
|
|
1370
1411
|
}
|
|
1371
1412
|
const { corsOrigin, rateLimit = { windowMs: 6e4, maxRequests: 100 } } = options;
|
|
1372
|
-
|
|
1413
|
+
if (this.enableSecurityHeaders) {
|
|
1414
|
+
addSecurityHeaders(res, corsOrigin);
|
|
1415
|
+
} else if (this.enableCORS) {
|
|
1416
|
+
const origin = corsOrigin || "http://localhost:3000";
|
|
1417
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1418
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
|
1419
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1420
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
1421
|
+
}
|
|
1373
1422
|
if (req.method === "OPTIONS") {
|
|
1374
1423
|
res.writeHead(204);
|
|
1375
1424
|
res.end();
|
|
@@ -1383,7 +1432,9 @@ var SimpleRouter = class {
|
|
|
1383
1432
|
}
|
|
1384
1433
|
const parsedUrl = (0, import_node_url.parse)(req.url, true);
|
|
1385
1434
|
const pathname = parsedUrl.pathname;
|
|
1386
|
-
req.query
|
|
1435
|
+
if (!req.query) {
|
|
1436
|
+
req.query = parsedUrl.query || {};
|
|
1437
|
+
}
|
|
1387
1438
|
try {
|
|
1388
1439
|
req.body = await parseBody(req, options.maxBodySize);
|
|
1389
1440
|
} catch (_error) {
|
|
@@ -1403,24 +1454,48 @@ var SimpleRouter = class {
|
|
|
1403
1454
|
if (this.enableMetrics && requestVersion) {
|
|
1404
1455
|
this.metrics.versionRequests.set(requestVersion, (this.metrics.versionRequests.get(requestVersion) || 0) + 1);
|
|
1405
1456
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1457
|
+
matchedRoute = null;
|
|
1458
|
+
if (this.enableSmartRouting) {
|
|
1459
|
+
const staticKey = `${req.method}:${pathname}`;
|
|
1460
|
+
const staticRoute = this.staticRoutes.get(staticKey);
|
|
1461
|
+
if (staticRoute) {
|
|
1462
|
+
if (!this.enableVersioning || staticRoute.version === requestVersion) {
|
|
1463
|
+
matchedRoute = { route: staticRoute, params: {} };
|
|
1464
|
+
if (this.enableRouteMetrics && this.enableMetrics) {
|
|
1465
|
+
if (!this.metrics.staticRouteMatches) {
|
|
1466
|
+
this.metrics.staticRouteMatches = 0;
|
|
1467
|
+
}
|
|
1468
|
+
this.metrics.staticRouteMatches++;
|
|
1469
|
+
}
|
|
1417
1470
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (!matchedRoute) {
|
|
1474
|
+
const routesToSearch = this.enableVersioning && this.versionedRoutes.has(requestVersion) ? this.versionedRoutes.get(requestVersion) : this.routes;
|
|
1475
|
+
for (const route of routesToSearch) {
|
|
1476
|
+
if (route.method === req.method) {
|
|
1477
|
+
if (this.enableVersioning && route.version !== requestVersion) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
let params = null;
|
|
1481
|
+
if (this.enableCompilation && route.compiled) {
|
|
1482
|
+
params = this.matchCompiledRoute(route.compiled, pathname);
|
|
1483
|
+
} else {
|
|
1484
|
+
params = extractParams(route.path, pathname);
|
|
1485
|
+
}
|
|
1486
|
+
if (params !== null) {
|
|
1487
|
+
matchedRoute = { route, params };
|
|
1488
|
+
if (this.enableRouteMetrics && this.enableMetrics) {
|
|
1489
|
+
if (!this.metrics.dynamicRouteMatches) {
|
|
1490
|
+
this.metrics.dynamicRouteMatches = 0;
|
|
1491
|
+
}
|
|
1492
|
+
this.metrics.dynamicRouteMatches++;
|
|
1493
|
+
}
|
|
1494
|
+
if (this.routeCache.size < this.maxCacheSize) {
|
|
1495
|
+
this.routeCache.set(cacheKey, matchedRoute);
|
|
1496
|
+
}
|
|
1497
|
+
break;
|
|
1422
1498
|
}
|
|
1423
|
-
break;
|
|
1424
1499
|
}
|
|
1425
1500
|
}
|
|
1426
1501
|
}
|
|
@@ -1431,7 +1506,6 @@ var SimpleRouter = class {
|
|
|
1431
1506
|
const routeKey = `${req.method}:${matchedRoute.route.path}`;
|
|
1432
1507
|
this.metrics.routeMatches.set(routeKey, (this.metrics.routeMatches.get(routeKey) || 0) + 1);
|
|
1433
1508
|
}
|
|
1434
|
-
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} ${req.method} ${pathname}`);
|
|
1435
1509
|
try {
|
|
1436
1510
|
if (matchedRoute.route.middleware && matchedRoute.route.middleware.length > 0) {
|
|
1437
1511
|
for (const middleware of matchedRoute.route.middleware) {
|
|
@@ -1441,16 +1515,26 @@ var SimpleRouter = class {
|
|
|
1441
1515
|
}
|
|
1442
1516
|
const { route } = matchedRoute;
|
|
1443
1517
|
const result = await route.handler(req, res);
|
|
1444
|
-
if (result &&
|
|
1445
|
-
|
|
1446
|
-
|
|
1518
|
+
if (result && !res.headersSent) {
|
|
1519
|
+
if (typeof result === "object") {
|
|
1520
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1521
|
+
res.end(JSON.stringify(result));
|
|
1522
|
+
} else if (typeof result === "string") {
|
|
1523
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1524
|
+
res.end(result);
|
|
1525
|
+
}
|
|
1447
1526
|
}
|
|
1448
1527
|
if (this.enableMetrics) {
|
|
1449
1528
|
const responseTime = Date.now() - startTime;
|
|
1450
|
-
this.
|
|
1451
|
-
|
|
1452
|
-
this.metrics.responseTime = this.metrics.responseTime.slice(-1e3);
|
|
1529
|
+
if (!this._metricsLock) {
|
|
1530
|
+
this._metricsLock = Promise.resolve();
|
|
1453
1531
|
}
|
|
1532
|
+
this._metricsLock = this._metricsLock.then(() => {
|
|
1533
|
+
this.metrics.responseTime.push(responseTime);
|
|
1534
|
+
if (this.metrics.responseTime.length > 1e3) {
|
|
1535
|
+
this.metrics.responseTime = this.metrics.responseTime.slice(-1e3);
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1454
1538
|
}
|
|
1455
1539
|
return;
|
|
1456
1540
|
} catch (_error) {
|