@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.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 fieldErrors = validateField(fieldSchema, fieldValue, field);
144
- errors.push(...fieldErrors);
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.log("WebSocket socket _error (connection likely closed):", err.code);
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.log("Failed to send message to connection:", id);
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
- return this.routeCompilationCache.get(pattern);
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
- if (constraint) {
1127
- const constraintPattern = constraint.slice(1, -1);
1128
- return optional ? `(?:/(?:${constraintPattern}))?` : `/(${constraintPattern})`;
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
- return optional ? "(?:/([^/]+))?" : "/([^/]+)";
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(/[.+?^${}|[\]\\]/g, "\\$&").replace(/\\\(/g, "(").replace(/\\\)/g, ")").replace(/\\\?/g, "?");
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 < 1e3) {
1141
- this.routeCompilationCache.set(pattern, compiled);
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
- addSecurityHeaders(res, corsOrigin);
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 = parsedUrl.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
- const routesToSearch = this.enableVersioning && this.versionedRoutes.has(requestVersion) ? this.versionedRoutes.get(requestVersion) : this.routes;
1353
- for (const route of routesToSearch) {
1354
- if (route.method === req.method) {
1355
- if (this.enableVersioning && route.version !== requestVersion) {
1356
- continue;
1357
- }
1358
- let params = null;
1359
- if (this.enableCompilation && route.compiled) {
1360
- params = this.matchCompiledRoute(route.compiled, pathname);
1361
- } else {
1362
- params = extractParams(route.path, pathname);
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
- if (params !== null) {
1365
- matchedRoute = { route, params };
1366
- if (this.routeCache.size < this.maxCacheSize) {
1367
- this.routeCache.set(cacheKey, matchedRoute);
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 && typeof result === "object" && !res.headersSent) {
1391
- res.writeHead(200, { "Content-Type": "application/json" });
1392
- res.end(JSON.stringify(result));
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.metrics.responseTime.push(responseTime);
1397
- if (this.metrics.responseTime.length > 1e3) {
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) {