@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 +2 -2
- package/src/ServiceWrapper.js +122 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-wrapper",
|
|
3
|
-
"version": "2.1
|
|
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.
|
|
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"
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -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.
|