@coherent.js/api 1.0.0-beta.3 → 1.0.0-beta.6

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