@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.js
CHANGED
|
@@ -140,8 +140,10 @@ function validateAgainstSchema(schema, data) {
|
|
|
140
140
|
for (const [field, fieldSchema] of Object.entries(schema.properties)) {
|
|
141
141
|
if (field in data) {
|
|
142
142
|
const fieldValue = data[field];
|
|
143
|
-
const
|
|
144
|
-
|
|
143
|
+
const fieldResult = validateField(fieldSchema, fieldValue, field);
|
|
144
|
+
if (!fieldResult.valid) {
|
|
145
|
+
errors.push(...fieldResult.errors);
|
|
146
|
+
}
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
}
|
|
@@ -153,6 +155,15 @@ function validateAgainstSchema(schema, data) {
|
|
|
153
155
|
}
|
|
154
156
|
function validateField(schema, value, fieldName) {
|
|
155
157
|
const errors = [];
|
|
158
|
+
if (value === null || value === void 0) {
|
|
159
|
+
if (schema.type && schema.type !== "null") {
|
|
160
|
+
errors.push({
|
|
161
|
+
field: fieldName,
|
|
162
|
+
message: `Expected ${schema.type}, got ${value === null ? "null" : "undefined"}`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return { valid: errors.length === 0, errors };
|
|
166
|
+
}
|
|
156
167
|
if (schema.type) {
|
|
157
168
|
if (schema.type === "string" && typeof value !== "string") {
|
|
158
169
|
errors.push({
|
|
@@ -176,7 +187,7 @@ function validateField(schema, value, fieldName) {
|
|
|
176
187
|
});
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
|
-
if (schema.type === "string") {
|
|
190
|
+
if (schema.type === "string" && typeof value === "string") {
|
|
180
191
|
if (schema.minLength && value.length < schema.minLength) {
|
|
181
192
|
errors.push({
|
|
182
193
|
field: fieldName,
|
|
@@ -196,7 +207,7 @@ function validateField(schema, value, fieldName) {
|
|
|
196
207
|
});
|
|
197
208
|
}
|
|
198
209
|
}
|
|
199
|
-
if (schema.type === "number") {
|
|
210
|
+
if (schema.type === "number" && typeof value === "number") {
|
|
200
211
|
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
201
212
|
errors.push({
|
|
202
213
|
field: fieldName,
|
|
@@ -210,7 +221,7 @@ function validateField(schema, value, fieldName) {
|
|
|
210
221
|
});
|
|
211
222
|
}
|
|
212
223
|
}
|
|
213
|
-
return errors;
|
|
224
|
+
return { valid: errors.length === 0, errors };
|
|
214
225
|
}
|
|
215
226
|
function withValidation(schema) {
|
|
216
227
|
return (req, res, next) => {
|
|
@@ -440,6 +451,9 @@ function registerRoute(method, config, router, path) {
|
|
|
440
451
|
}
|
|
441
452
|
}, { name });
|
|
442
453
|
}
|
|
454
|
+
function isStaticRoute(pattern) {
|
|
455
|
+
return !pattern.includes(":") && !pattern.includes("*") && !pattern.includes("(") && !pattern.includes("?");
|
|
456
|
+
}
|
|
443
457
|
var SimpleRouter = class {
|
|
444
458
|
constructor(options = {}) {
|
|
445
459
|
this.routes = [];
|
|
@@ -451,6 +465,7 @@ var SimpleRouter = class {
|
|
|
451
465
|
this.enableCompilation = options.enableCompilation !== false;
|
|
452
466
|
this.compiledRoutes = /* @__PURE__ */ new Map();
|
|
453
467
|
this.routeCompilationCache = /* @__PURE__ */ new Map();
|
|
468
|
+
this.maxCompilationCacheSize = options.maxCompilationCacheSize || 1e3;
|
|
454
469
|
this.enableVersioning = options.enableVersioning || false;
|
|
455
470
|
this.defaultVersion = options.defaultVersion || "v1";
|
|
456
471
|
this.versionHeader = options.versionHeader || "api-version";
|
|
@@ -479,10 +494,15 @@ var SimpleRouter = class {
|
|
|
479
494
|
// Track WebSocket messages
|
|
480
495
|
};
|
|
481
496
|
}
|
|
497
|
+
this.enableSecurityHeaders = options.enableSecurityHeaders !== false;
|
|
498
|
+
this.enableCORS = options.enableCORS !== false;
|
|
499
|
+
this.enableSmartRouting = options.enableSmartRouting !== false;
|
|
500
|
+
this.staticRoutes = /* @__PURE__ */ new Map();
|
|
501
|
+
this.enableRouteMetrics = options.enableRouteMetrics || false;
|
|
482
502
|
}
|
|
483
503
|
/**
|
|
484
504
|
* Add an HTTP route to the router
|
|
485
|
-
*
|
|
505
|
+
*
|
|
486
506
|
* @param {string} method - HTTP method (GET, POST, PUT, DELETE, PATCH)
|
|
487
507
|
* @param {string} path - Route path pattern (supports :param and wildcards)
|
|
488
508
|
* @param {Function} handler - Route handler function
|
|
@@ -490,7 +510,7 @@ var SimpleRouter = class {
|
|
|
490
510
|
* @param {Array} [options.middleware] - Route-specific middleware
|
|
491
511
|
* @param {string} [options.name] - Named route for URL generation
|
|
492
512
|
* @param {string} [options.version] - API version for this route
|
|
493
|
-
*
|
|
513
|
+
*
|
|
494
514
|
* @example
|
|
495
515
|
* router.addRoute('GET', '/users/:id', (req, res) => {
|
|
496
516
|
* return { user: { id: req.params.id } };
|
|
@@ -513,6 +533,11 @@ var SimpleRouter = class {
|
|
|
513
533
|
if (this.enableCompilation) {
|
|
514
534
|
route.compiled = this.compileRoute(fullPath);
|
|
515
535
|
}
|
|
536
|
+
if (this.enableSmartRouting && isStaticRoute(fullPath)) {
|
|
537
|
+
const staticKey = `${route.method}:${fullPath}`;
|
|
538
|
+
this.staticRoutes.set(staticKey, route);
|
|
539
|
+
route.isStatic = true;
|
|
540
|
+
}
|
|
516
541
|
this.routes.push(route);
|
|
517
542
|
if (this.enableVersioning) {
|
|
518
543
|
if (!this.versionedRoutes.has(route.version)) {
|
|
@@ -826,7 +851,7 @@ var SimpleRouter = class {
|
|
|
826
851
|
}
|
|
827
852
|
});
|
|
828
853
|
socket.on("_error", (err) => {
|
|
829
|
-
console.
|
|
854
|
+
console.error("WebSocket socket _error (connection likely closed):", err.code);
|
|
830
855
|
});
|
|
831
856
|
return ws;
|
|
832
857
|
}
|
|
@@ -885,7 +910,7 @@ var SimpleRouter = class {
|
|
|
885
910
|
try {
|
|
886
911
|
ws.send(message);
|
|
887
912
|
} catch {
|
|
888
|
-
console.
|
|
913
|
+
console.error("Failed to send message to connection:", id);
|
|
889
914
|
}
|
|
890
915
|
}
|
|
891
916
|
}
|
|
@@ -923,16 +948,16 @@ var SimpleRouter = class {
|
|
|
923
948
|
}
|
|
924
949
|
/**
|
|
925
950
|
* Generate URL for named route with parameter substitution
|
|
926
|
-
*
|
|
951
|
+
*
|
|
927
952
|
* @param {string} name - Route name (set during route registration)
|
|
928
953
|
* @param {Object} [params={}] - Parameters to substitute in the URL pattern
|
|
929
954
|
* @returns {string} Generated URL with parameters substituted
|
|
930
955
|
* @throws {Error} If named route is not found
|
|
931
|
-
*
|
|
956
|
+
*
|
|
932
957
|
* @example
|
|
933
958
|
* // Route registered as: router.addRoute('GET', '/users/:id', handler, { name: 'getUser' })
|
|
934
959
|
* const url = router.url('getUser', { id: 123 }); // '/users/123'
|
|
935
|
-
*
|
|
960
|
+
*
|
|
936
961
|
* // With constrained parameters
|
|
937
962
|
* const url = router.url('getUserPosts', { userId: 123, postId: 456 }); // '/users/123/posts/456'
|
|
938
963
|
*/
|
|
@@ -950,11 +975,11 @@ var SimpleRouter = class {
|
|
|
950
975
|
}
|
|
951
976
|
/**
|
|
952
977
|
* Add routes from configuration object
|
|
953
|
-
*
|
|
978
|
+
*
|
|
954
979
|
* @param {Object} routeConfig - Route configuration object with nested structure
|
|
955
980
|
* @description Processes nested route objects and registers HTTP and WebSocket routes.
|
|
956
981
|
* Supports declarative route definition with automatic method detection.
|
|
957
|
-
*
|
|
982
|
+
*
|
|
958
983
|
* @example
|
|
959
984
|
* router.addRoutes({
|
|
960
985
|
* 'api': {
|
|
@@ -970,17 +995,17 @@ var SimpleRouter = class {
|
|
|
970
995
|
}
|
|
971
996
|
/**
|
|
972
997
|
* Add global middleware to the router
|
|
973
|
-
*
|
|
998
|
+
*
|
|
974
999
|
* @param {Function|Object} middleware - Middleware function or conditional middleware object
|
|
975
1000
|
* @description Adds middleware that runs before all route handlers. Supports both
|
|
976
1001
|
* simple functions and conditional middleware objects.
|
|
977
|
-
*
|
|
1002
|
+
*
|
|
978
1003
|
* @example
|
|
979
1004
|
* // Simple middleware
|
|
980
1005
|
* router.use((req, res) => {
|
|
981
1006
|
* console.log(`${req.method} ${req.url}`);
|
|
982
1007
|
* });
|
|
983
|
-
*
|
|
1008
|
+
*
|
|
984
1009
|
* // Conditional middleware
|
|
985
1010
|
* router.use({
|
|
986
1011
|
* condition: (req) => req.url.startsWith('/api'),
|
|
@@ -1110,7 +1135,10 @@ var SimpleRouter = class {
|
|
|
1110
1135
|
compileRoute(pattern) {
|
|
1111
1136
|
if (this.routeCompilationCache.has(pattern)) {
|
|
1112
1137
|
if (this.enableMetrics) this.metrics.compilationHits++;
|
|
1113
|
-
|
|
1138
|
+
const compiled2 = this.routeCompilationCache.get(pattern);
|
|
1139
|
+
this.routeCompilationCache.delete(pattern);
|
|
1140
|
+
this.routeCompilationCache.set(pattern, compiled2);
|
|
1141
|
+
return compiled2;
|
|
1114
1142
|
}
|
|
1115
1143
|
const paramNames = [];
|
|
1116
1144
|
let regexPattern = pattern;
|
|
@@ -1121,25 +1149,38 @@ var SimpleRouter = class {
|
|
|
1121
1149
|
regexPattern = regexPattern.replace(/\/\*/g, "/([^/]+)");
|
|
1122
1150
|
paramNames.push("splat");
|
|
1123
1151
|
}
|
|
1124
|
-
regexPattern = regexPattern.replace(/:([^(/]+)(\([^)]+\))?(\?)?/g, (match, paramName, constraint, optional) => {
|
|
1152
|
+
regexPattern = regexPattern.replace(/:([^(/]+)(\([^)]+\))?(\?)?/g, (match, paramName, constraint, optional, offset, fullString) => {
|
|
1125
1153
|
paramNames.push(paramName);
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1154
|
+
const hasPrecedingSlash = offset > 0 && fullString[offset - 1] === "/";
|
|
1155
|
+
if (hasPrecedingSlash) {
|
|
1156
|
+
if (constraint) {
|
|
1157
|
+
const constraintPattern = constraint.slice(1, -1);
|
|
1158
|
+
return optional ? `(?:/(?:${constraintPattern}))?` : `(${constraintPattern})`;
|
|
1159
|
+
} else {
|
|
1160
|
+
return optional ? `(?:([^/]+))?` : `([^/]+)`;
|
|
1161
|
+
}
|
|
1129
1162
|
} else {
|
|
1130
|
-
|
|
1163
|
+
if (constraint) {
|
|
1164
|
+
const constraintPattern = constraint.slice(1, -1);
|
|
1165
|
+
return optional ? `(?:/(?:${constraintPattern}))?` : `/${constraintPattern}`;
|
|
1166
|
+
} else {
|
|
1167
|
+
return optional ? `(?:/([^/]+))?` : `/([^/]+)`;
|
|
1168
|
+
}
|
|
1131
1169
|
}
|
|
1132
1170
|
});
|
|
1133
|
-
regexPattern = regexPattern.replace(/[.+?^${}
|
|
1171
|
+
regexPattern = regexPattern.replace(/([.+?^${}|\\[\]()]])/g, "\\$1");
|
|
1172
|
+
regexPattern = regexPattern.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\^/g, "^");
|
|
1134
1173
|
regexPattern = `^${regexPattern}$`;
|
|
1135
1174
|
const compiled = {
|
|
1136
1175
|
regex: new RegExp(regexPattern),
|
|
1137
1176
|
paramNames,
|
|
1138
1177
|
pattern
|
|
1139
1178
|
};
|
|
1140
|
-
if (this.routeCompilationCache.size
|
|
1141
|
-
this.routeCompilationCache.
|
|
1179
|
+
if (this.routeCompilationCache.size >= this.maxCompilationCacheSize) {
|
|
1180
|
+
const firstKey = this.routeCompilationCache.keys().next().value;
|
|
1181
|
+
this.routeCompilationCache.delete(firstKey);
|
|
1142
1182
|
}
|
|
1183
|
+
this.routeCompilationCache.set(pattern, compiled);
|
|
1143
1184
|
return compiled;
|
|
1144
1185
|
}
|
|
1145
1186
|
/**
|
|
@@ -1315,7 +1356,15 @@ var SimpleRouter = class {
|
|
|
1315
1356
|
this.metrics.requests++;
|
|
1316
1357
|
}
|
|
1317
1358
|
const { corsOrigin, rateLimit = { windowMs: 6e4, maxRequests: 100 } } = options;
|
|
1318
|
-
|
|
1359
|
+
if (this.enableSecurityHeaders) {
|
|
1360
|
+
addSecurityHeaders(res, corsOrigin);
|
|
1361
|
+
} else if (this.enableCORS) {
|
|
1362
|
+
const origin = corsOrigin || "http://localhost:3000";
|
|
1363
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1364
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
|
1365
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1366
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
1367
|
+
}
|
|
1319
1368
|
if (req.method === "OPTIONS") {
|
|
1320
1369
|
res.writeHead(204);
|
|
1321
1370
|
res.end();
|
|
@@ -1329,7 +1378,9 @@ var SimpleRouter = class {
|
|
|
1329
1378
|
}
|
|
1330
1379
|
const parsedUrl = parseUrl(req.url, true);
|
|
1331
1380
|
const pathname = parsedUrl.pathname;
|
|
1332
|
-
req.query
|
|
1381
|
+
if (!req.query) {
|
|
1382
|
+
req.query = parsedUrl.query || {};
|
|
1383
|
+
}
|
|
1333
1384
|
try {
|
|
1334
1385
|
req.body = await parseBody(req, options.maxBodySize);
|
|
1335
1386
|
} catch (_error) {
|
|
@@ -1349,24 +1400,48 @@ var SimpleRouter = class {
|
|
|
1349
1400
|
if (this.enableMetrics && requestVersion) {
|
|
1350
1401
|
this.metrics.versionRequests.set(requestVersion, (this.metrics.versionRequests.get(requestVersion) || 0) + 1);
|
|
1351
1402
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1403
|
+
matchedRoute = null;
|
|
1404
|
+
if (this.enableSmartRouting) {
|
|
1405
|
+
const staticKey = `${req.method}:${pathname}`;
|
|
1406
|
+
const staticRoute = this.staticRoutes.get(staticKey);
|
|
1407
|
+
if (staticRoute) {
|
|
1408
|
+
if (!this.enableVersioning || staticRoute.version === requestVersion) {
|
|
1409
|
+
matchedRoute = { route: staticRoute, params: {} };
|
|
1410
|
+
if (this.enableRouteMetrics && this.enableMetrics) {
|
|
1411
|
+
if (!this.metrics.staticRouteMatches) {
|
|
1412
|
+
this.metrics.staticRouteMatches = 0;
|
|
1413
|
+
}
|
|
1414
|
+
this.metrics.staticRouteMatches++;
|
|
1415
|
+
}
|
|
1363
1416
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (!matchedRoute) {
|
|
1420
|
+
const routesToSearch = this.enableVersioning && this.versionedRoutes.has(requestVersion) ? this.versionedRoutes.get(requestVersion) : this.routes;
|
|
1421
|
+
for (const route of routesToSearch) {
|
|
1422
|
+
if (route.method === req.method) {
|
|
1423
|
+
if (this.enableVersioning && route.version !== requestVersion) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
let params = null;
|
|
1427
|
+
if (this.enableCompilation && route.compiled) {
|
|
1428
|
+
params = this.matchCompiledRoute(route.compiled, pathname);
|
|
1429
|
+
} else {
|
|
1430
|
+
params = extractParams(route.path, pathname);
|
|
1431
|
+
}
|
|
1432
|
+
if (params !== null) {
|
|
1433
|
+
matchedRoute = { route, params };
|
|
1434
|
+
if (this.enableRouteMetrics && this.enableMetrics) {
|
|
1435
|
+
if (!this.metrics.dynamicRouteMatches) {
|
|
1436
|
+
this.metrics.dynamicRouteMatches = 0;
|
|
1437
|
+
}
|
|
1438
|
+
this.metrics.dynamicRouteMatches++;
|
|
1439
|
+
}
|
|
1440
|
+
if (this.routeCache.size < this.maxCacheSize) {
|
|
1441
|
+
this.routeCache.set(cacheKey, matchedRoute);
|
|
1442
|
+
}
|
|
1443
|
+
break;
|
|
1368
1444
|
}
|
|
1369
|
-
break;
|
|
1370
1445
|
}
|
|
1371
1446
|
}
|
|
1372
1447
|
}
|
|
@@ -1377,7 +1452,6 @@ var SimpleRouter = class {
|
|
|
1377
1452
|
const routeKey = `${req.method}:${matchedRoute.route.path}`;
|
|
1378
1453
|
this.metrics.routeMatches.set(routeKey, (this.metrics.routeMatches.get(routeKey) || 0) + 1);
|
|
1379
1454
|
}
|
|
1380
|
-
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} ${req.method} ${pathname}`);
|
|
1381
1455
|
try {
|
|
1382
1456
|
if (matchedRoute.route.middleware && matchedRoute.route.middleware.length > 0) {
|
|
1383
1457
|
for (const middleware of matchedRoute.route.middleware) {
|
|
@@ -1387,16 +1461,26 @@ var SimpleRouter = class {
|
|
|
1387
1461
|
}
|
|
1388
1462
|
const { route } = matchedRoute;
|
|
1389
1463
|
const result = await route.handler(req, res);
|
|
1390
|
-
if (result &&
|
|
1391
|
-
|
|
1392
|
-
|
|
1464
|
+
if (result && !res.headersSent) {
|
|
1465
|
+
if (typeof result === "object") {
|
|
1466
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1467
|
+
res.end(JSON.stringify(result));
|
|
1468
|
+
} else if (typeof result === "string") {
|
|
1469
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1470
|
+
res.end(result);
|
|
1471
|
+
}
|
|
1393
1472
|
}
|
|
1394
1473
|
if (this.enableMetrics) {
|
|
1395
1474
|
const responseTime = Date.now() - startTime;
|
|
1396
|
-
this.
|
|
1397
|
-
|
|
1398
|
-
this.metrics.responseTime = this.metrics.responseTime.slice(-1e3);
|
|
1475
|
+
if (!this._metricsLock) {
|
|
1476
|
+
this._metricsLock = Promise.resolve();
|
|
1399
1477
|
}
|
|
1478
|
+
this._metricsLock = this._metricsLock.then(() => {
|
|
1479
|
+
this.metrics.responseTime.push(responseTime);
|
|
1480
|
+
if (this.metrics.responseTime.length > 1e3) {
|
|
1481
|
+
this.metrics.responseTime = this.metrics.responseTime.slice(-1e3);
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1400
1484
|
}
|
|
1401
1485
|
return;
|
|
1402
1486
|
} catch (_error) {
|