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