@coherent.js/api 1.0.0-beta.2 → 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 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 fieldErrors = validateField(fieldSchema, fieldValue, field);
198
- errors.push(...fieldErrors);
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.log("WebSocket socket _error (connection likely closed):", err.code);
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.log("Failed to send message to connection:", id);
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
- return this.routeCompilationCache.get(pattern);
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
- if (constraint) {
1181
- const constraintPattern = constraint.slice(1, -1);
1182
- return optional ? `(?:/(?:${constraintPattern}))?` : `/(${constraintPattern})`;
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
- return optional ? "(?:/([^/]+))?" : "/([^/]+)";
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(/[.+?^${}|[\]\\]/g, "\\$&").replace(/\\\(/g, "(").replace(/\\\)/g, ")").replace(/\\\?/g, "?");
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 < 1e3) {
1195
- this.routeCompilationCache.set(pattern, compiled);
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
- addSecurityHeaders(res, corsOrigin);
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 = parsedUrl.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
- const routesToSearch = this.enableVersioning && this.versionedRoutes.has(requestVersion) ? this.versionedRoutes.get(requestVersion) : this.routes;
1407
- for (const route of routesToSearch) {
1408
- if (route.method === req.method) {
1409
- if (this.enableVersioning && route.version !== requestVersion) {
1410
- continue;
1411
- }
1412
- let params = null;
1413
- if (this.enableCompilation && route.compiled) {
1414
- params = this.matchCompiledRoute(route.compiled, pathname);
1415
- } else {
1416
- params = extractParams(route.path, pathname);
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
- if (params !== null) {
1419
- matchedRoute = { route, params };
1420
- if (this.routeCache.size < this.maxCacheSize) {
1421
- this.routeCache.set(cacheKey, matchedRoute);
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 && typeof result === "object" && !res.headersSent) {
1445
- res.writeHead(200, { "Content-Type": "application/json" });
1446
- res.end(JSON.stringify(result));
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.metrics.responseTime.push(responseTime);
1451
- if (this.metrics.responseTime.length > 1e3) {
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) {