@onlineapps/service-wrapper 2.1.118 → 2.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.1.118",
3
+ "version": "2.2.1",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -32,7 +32,7 @@
32
32
  "@onlineapps/conn-orch-cookbook": "2.0.35",
33
33
  "@onlineapps/conn-orch-orchestrator": "1.0.103",
34
34
  "@onlineapps/conn-orch-registry": "1.1.52",
35
- "@onlineapps/conn-orch-validator": "2.0.30",
35
+ "@onlineapps/conn-orch-validator": "2.0.31",
36
36
  "@onlineapps/monitoring-core": "1.0.21",
37
37
  "@onlineapps/service-common": "1.0.17",
38
38
  "@onlineapps/runtime-config": "1.0.2"
@@ -483,6 +483,15 @@ class ServiceWrapper {
483
483
  // Konfigurace se načítá v konstruktoru, takže tady jen logujeme
484
484
  this._logPhase('0.1', 'Configuration Load', 'PASSED', null, Date.now() - startTime);
485
485
 
486
+ // FÁZE 0.1b: Operations-Routes alignment validation
487
+ const alignmentStartTime = Date.now();
488
+ try {
489
+ this._validateOperationsRouteAlignment();
490
+ this._logPhase('0.1b', 'Operations-Routes Alignment', 'PASSED', null, Date.now() - alignmentStartTime);
491
+ } catch (error) {
492
+ this._handleInitializationError('0.1b', 'Operations-Routes Alignment', error, false);
493
+ }
494
+
486
495
  // FÁZE 0.2: Tier 1 validace (PŘED MQ připojením)
487
496
  if (this.serviceRoot && this.config.wrapper?.validation?.enabled !== false) {
488
497
  const validationStartTime = Date.now();
@@ -1642,6 +1651,119 @@ class ServiceWrapper {
1642
1651
  });
1643
1652
  }
1644
1653
 
1654
+ /**
1655
+ * Extract all registered Express routes from the app router stack.
1656
+ * Handles nested Router instances (e.g. app.use('/api', router)).
1657
+ * Compatible with Express 4 (app._router) and Express 5 (app.router).
1658
+ * @private
1659
+ * @returns {Array<{method: string, path: string}>}
1660
+ */
1661
+ _extractExpressRoutes() {
1662
+ const routes = [];
1663
+ const stack = this.app._router?.stack || this.app.router?.stack || [];
1664
+
1665
+ const extractFromStack = (layers, prefix) => {
1666
+ for (const layer of layers) {
1667
+ if (layer.route) {
1668
+ for (const method of Object.keys(layer.route.methods)) {
1669
+ routes.push({ method: method.toUpperCase(), path: `${prefix}${layer.route.path}` });
1670
+ }
1671
+ } else if (layer.name === 'router' && layer.handle?.stack) {
1672
+ let mountPath = '';
1673
+
1674
+ // Express 4: extract prefix from regexp
1675
+ if (layer.regexp?.source) {
1676
+ const match = layer.regexp.source.match(/^\^\\\/([a-zA-Z0-9_/-]+)/);
1677
+ if (match) mountPath = `/${match[1]}`;
1678
+ }
1679
+
1680
+ // Express 5: probe the matcher to discover mount prefix
1681
+ if (!mountPath && layer.matchers?.[0]) {
1682
+ const probe = layer.matchers[0]('/api');
1683
+ if (probe && probe.path) mountPath = probe.path;
1684
+ }
1685
+
1686
+ extractFromStack(layer.handle.stack, `${prefix}${mountPath}`);
1687
+ }
1688
+ }
1689
+ };
1690
+
1691
+ extractFromStack(stack, '');
1692
+ return routes;
1693
+ }
1694
+
1695
+ /**
1696
+ * Bidirectional validation: operations.json <-> Express routes.
1697
+ * Check A: every declared operation has a matching Express route.
1698
+ * Check B: every /api/* route has a matching operation or is in _internal.
1699
+ * @private
1700
+ * @throws {Error} Permanent error listing all mismatches
1701
+ */
1702
+ _validateOperationsRouteAlignment() {
1703
+ const ops = this.operations?.operations || {};
1704
+ const internalEntries = this.operations?._internal || [];
1705
+ const expressRoutes = this._extractExpressRoutes();
1706
+
1707
+ const routeSet = new Set(
1708
+ expressRoutes.map(r => `${r.method}:${r.path}`)
1709
+ );
1710
+
1711
+ const errors = [];
1712
+
1713
+ // Check A: every operation endpoint+method must exist as an Express route
1714
+ for (const [opName, opDef] of Object.entries(ops)) {
1715
+ const method = (opDef.method || 'POST').toUpperCase();
1716
+ const endpoint = opDef.endpoint;
1717
+ if (!endpoint) continue;
1718
+
1719
+ if (!routeSet.has(`${method}:${endpoint}`)) {
1720
+ errors.push(`Operation '${opName}' endpoint ${method} ${endpoint} not found in Express routes`);
1721
+ }
1722
+ }
1723
+
1724
+ // Build lookup: endpoint -> set of operation methods
1725
+ const opEndpoints = new Map();
1726
+ for (const opDef of Object.values(ops)) {
1727
+ const method = (opDef.method || 'POST').toUpperCase();
1728
+ const endpoint = opDef.endpoint;
1729
+ if (!endpoint) continue;
1730
+ if (!opEndpoints.has(endpoint)) opEndpoints.set(endpoint, new Set());
1731
+ opEndpoints.get(endpoint).add(method);
1732
+ }
1733
+
1734
+ // Build _internal lookup
1735
+ const internalSet = new Set(
1736
+ internalEntries.map(e => `${(e.method || 'GET').toUpperCase()}:${e.endpoint}`)
1737
+ );
1738
+
1739
+ // Check B: every /api/* route must have a matching operation or be _internal
1740
+ for (const route of expressRoutes) {
1741
+ if (!route.path.startsWith('/api')) continue;
1742
+
1743
+ const key = `${route.method}:${route.path}`;
1744
+ if (internalSet.has(key)) continue;
1745
+
1746
+ const endpointOps = opEndpoints.get(route.path);
1747
+ if (!endpointOps) {
1748
+ errors.push(`Route ${route.method} ${route.path} has no matching operation in operations.json`);
1749
+ continue;
1750
+ }
1751
+
1752
+ if (!endpointOps.has(route.method)) {
1753
+ // Tolerate GET alongside a declared POST (common browser-test pattern)
1754
+ if (route.method === 'GET' && endpointOps.size > 0) continue;
1755
+ errors.push(`Route ${route.method} ${route.path} has no matching operation in operations.json`);
1756
+ }
1757
+ }
1758
+
1759
+ if (errors.length > 0) {
1760
+ throw new Error(
1761
+ `[ServiceWrapper] Operations-Routes alignment failed (${errors.length} issue${errors.length > 1 ? 's' : ''}):\n` +
1762
+ errors.map(e => ` - ${e}`).join('\n')
1763
+ );
1764
+ }
1765
+ }
1766
+
1645
1767
  /**
1646
1768
  * Subscribe to infra.invalidation events on the infrastructure.health.events fanout exchange.
1647
1769
  * When received, deletes local validation proof and triggers re-validation with backoff.