@onlineapps/service-wrapper 3.0.15 → 3.0.17
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 +4 -4
- package/src/ServiceWrapper.js +201 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-wrapper",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.17",
|
|
4
4
|
"description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@onlineapps/conn-base-state": "1.0.1",
|
|
30
30
|
"@onlineapps/conn-infra-error-handler": "1.0.11",
|
|
31
31
|
"@onlineapps/conn-infra-mq": "1.1.70",
|
|
32
|
-
"@onlineapps/conn-orch-cookbook": "2.1.
|
|
33
|
-
"@onlineapps/conn-orch-orchestrator": "2.0.
|
|
32
|
+
"@onlineapps/conn-orch-cookbook": "2.1.3",
|
|
33
|
+
"@onlineapps/conn-orch-orchestrator": "2.0.3",
|
|
34
34
|
"@onlineapps/conn-orch-registry": "1.2.1",
|
|
35
|
-
"@onlineapps/conn-orch-validator": "
|
|
35
|
+
"@onlineapps/conn-orch-validator": "3.1.2",
|
|
36
36
|
"@onlineapps/monitoring-core": "1.0.23",
|
|
37
37
|
"@onlineapps/runtime-config": "1.0.2",
|
|
38
38
|
"@onlineapps/service-common": "1.1.2",
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -156,6 +156,11 @@ class ServiceWrapper {
|
|
|
156
156
|
this._revalidationState = REVALIDATION_STATE.IDLE;
|
|
157
157
|
this._revalidationAttempt = 0;
|
|
158
158
|
this._revalidationTimer = null;
|
|
159
|
+
|
|
160
|
+
// Runtime connector contract + probes (loaded from config/service/integration-contract.json)
|
|
161
|
+
this.integrationContract = null;
|
|
162
|
+
this.requiredConnectors = null;
|
|
163
|
+
this._dbHealthConnector = null;
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
/**
|
|
@@ -438,6 +443,160 @@ class ServiceWrapper {
|
|
|
438
443
|
return processObject(config);
|
|
439
444
|
}
|
|
440
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Load and validate runtime integration contract used by health checks.
|
|
448
|
+
* @private
|
|
449
|
+
*/
|
|
450
|
+
_loadIntegrationContract() {
|
|
451
|
+
if (!this.serviceRoot) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
'[ServiceWrapper] Missing dependency - serviceRoot is required to load config/service/integration-contract.json for runtime health checks'
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const fs = require('fs');
|
|
458
|
+
const path = require('path');
|
|
459
|
+
const contractPath = path.join(this.serviceRoot, 'config', 'service', 'integration-contract.json');
|
|
460
|
+
|
|
461
|
+
if (!fs.existsSync(contractPath)) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
`[ServiceWrapper] Missing integration contract - Expected file at ${contractPath}. ` +
|
|
464
|
+
'Fix: add config/service/integration-contract.json with requiredConnectors booleans.'
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let raw;
|
|
469
|
+
try {
|
|
470
|
+
raw = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`[ServiceWrapper] Invalid integration contract - Cannot parse JSON at ${contractPath}. ${error.message}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const requiredConnectors = raw?.requiredConnectors;
|
|
478
|
+
if (!requiredConnectors || typeof requiredConnectors !== 'object' || Array.isArray(requiredConnectors)) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`[ServiceWrapper] Invalid integration contract - requiredConnectors object is required in ${contractPath}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const keys = ['db', 'redis', 'mq', 'minio'];
|
|
485
|
+
const normalized = {};
|
|
486
|
+
for (const key of keys) {
|
|
487
|
+
if (typeof requiredConnectors[key] !== 'boolean') {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`[ServiceWrapper] Invalid integration contract - requiredConnectors.${key} must be boolean in ${contractPath}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
normalized[key] = requiredConnectors[key];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.integrationContract = raw;
|
|
496
|
+
this.requiredConnectors = normalized;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Resolve DB connector used for runtime health checks.
|
|
501
|
+
* @private
|
|
502
|
+
* @returns {Object}
|
|
503
|
+
*/
|
|
504
|
+
_loadDatabaseHealthConnector() {
|
|
505
|
+
if (this._dbHealthConnector) {
|
|
506
|
+
return this._dbHealthConnector;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const path = require('path');
|
|
510
|
+
const dbModulePath = path.join(this.serviceRoot, 'src', 'config', 'database');
|
|
511
|
+
|
|
512
|
+
let dbConnector;
|
|
513
|
+
try {
|
|
514
|
+
dbConnector = require(dbModulePath);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`[ServiceWrapper] Database connector unavailable - Failed to load ${dbModulePath}.js. ${error.message}`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!dbConnector || typeof dbConnector.authenticate !== 'function') {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`[ServiceWrapper] Database connector invalid - Expected Sequelize-compatible module with authenticate() at ${dbModulePath}.js`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this._dbHealthConnector = dbConnector;
|
|
528
|
+
return dbConnector;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Check required DB dependency according to integration-contract.
|
|
533
|
+
* @private
|
|
534
|
+
* @returns {Promise<{status: string, reason: (string|null)}>}
|
|
535
|
+
*/
|
|
536
|
+
async _checkDatabaseDependency() {
|
|
537
|
+
if (!this.requiredConnectors?.db) {
|
|
538
|
+
return { status: 'not_required', reason: null };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const dbConnector = this._loadDatabaseHealthConnector();
|
|
543
|
+
await dbConnector.authenticate();
|
|
544
|
+
return { status: 'healthy', reason: null };
|
|
545
|
+
} catch (error) {
|
|
546
|
+
return { status: 'unhealthy', reason: error.message };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Check required Redis dependency according to integration-contract.
|
|
552
|
+
* Uses available cache/state connectors and requires at least one healthy probe.
|
|
553
|
+
* @private
|
|
554
|
+
* @returns {Promise<{status: string, reason: (string|null)}>}
|
|
555
|
+
*/
|
|
556
|
+
async _checkRedisDependency() {
|
|
557
|
+
if (!this.requiredConnectors?.redis) {
|
|
558
|
+
return { status: 'not_required', reason: null };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const checks = [];
|
|
562
|
+
const failures = [];
|
|
563
|
+
|
|
564
|
+
const runProbe = async (name, connector) => {
|
|
565
|
+
if (!connector || typeof connector.healthCheck !== 'function') {
|
|
566
|
+
failures.push(`${name}: connector missing healthCheck()`);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const healthy = await connector.healthCheck();
|
|
571
|
+
checks.push({ name, healthy: healthy === true });
|
|
572
|
+
if (healthy !== true) {
|
|
573
|
+
failures.push(`${name}: healthCheck returned false`);
|
|
574
|
+
}
|
|
575
|
+
} catch (error) {
|
|
576
|
+
failures.push(`${name}: ${error.message}`);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
await runProbe('cache', this.cacheConnector);
|
|
581
|
+
await runProbe('state', this.stateConnector);
|
|
582
|
+
|
|
583
|
+
if (checks.some((entry) => entry.healthy === true)) {
|
|
584
|
+
return { status: 'healthy', reason: null };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (checks.length === 0) {
|
|
588
|
+
return {
|
|
589
|
+
status: 'unhealthy',
|
|
590
|
+
reason: 'Redis is required but no Redis connector is initialized (cache/state missing)'
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
status: 'unhealthy',
|
|
596
|
+
reason: failures.length > 0 ? failures.join('; ') : 'Redis health probes failed'
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
441
600
|
/**
|
|
442
601
|
* Initialize all wrapper components
|
|
443
602
|
* @async
|
|
@@ -638,6 +797,19 @@ class ServiceWrapper {
|
|
|
638
797
|
// See RFC api/docs/architecture/biz-service-invocation-model.md §5.9 step 5.
|
|
639
798
|
this._logPhase('0.1b', 'Operations-Routes Alignment', 'SKIPPED', null, 0);
|
|
640
799
|
|
|
800
|
+
// FÁZE 0.15: Runtime integration connector contract (health + readiness source of truth)
|
|
801
|
+
if (this.config.wrapper?.health?.enabled !== false) {
|
|
802
|
+
const contractStartTime = Date.now();
|
|
803
|
+
try {
|
|
804
|
+
this._loadIntegrationContract();
|
|
805
|
+
this._logPhase('0.15', 'Integration Contract Load', 'PASSED', null, Date.now() - contractStartTime);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
return await this._handleInitializationError('0.15', 'Integration Contract Load', error, false); // Trvalá chyba
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
this._logPhase('0.15', 'Integration Contract Load', 'SKIPPED', null, 0);
|
|
811
|
+
}
|
|
812
|
+
|
|
641
813
|
// FÁZE 0.2: Tier 1 validace (PŘED MQ připojením)
|
|
642
814
|
if (this.serviceRoot && this.config.wrapper?.validation?.enabled !== false) {
|
|
643
815
|
const validationStartTime = Date.now();
|
|
@@ -1299,16 +1471,20 @@ class ServiceWrapper {
|
|
|
1299
1471
|
const healthEndpoint = this.config.wrapper?.health?.endpoint || '/health';
|
|
1300
1472
|
|
|
1301
1473
|
const healthHandler = async (req, res) => {
|
|
1474
|
+
const dependencyIssues = [];
|
|
1302
1475
|
const health = {
|
|
1303
1476
|
status: 'healthy',
|
|
1304
1477
|
service: this.config.service?.name || 'unnamed-service',
|
|
1305
1478
|
timestamp: new Date().toISOString(),
|
|
1479
|
+
requiredConnectors: this.requiredConnectors || null,
|
|
1306
1480
|
components: {
|
|
1307
1481
|
http: 'healthy',
|
|
1308
1482
|
mq: 'disabled',
|
|
1309
1483
|
registry: this.registryClient ? 'healthy' : 'disabled',
|
|
1310
1484
|
cache: this.cacheConnector ? 'healthy' : 'disabled',
|
|
1311
|
-
state: this.stateConnector ? 'healthy' : 'disabled'
|
|
1485
|
+
state: this.stateConnector ? 'healthy' : 'disabled',
|
|
1486
|
+
database: this.requiredConnectors?.db === true ? 'unknown' : 'not_required',
|
|
1487
|
+
redis: this.requiredConnectors?.redis === true ? 'unknown' : 'not_required'
|
|
1312
1488
|
}
|
|
1313
1489
|
};
|
|
1314
1490
|
|
|
@@ -1330,6 +1506,30 @@ class ServiceWrapper {
|
|
|
1330
1506
|
}
|
|
1331
1507
|
}
|
|
1332
1508
|
|
|
1509
|
+
if (!this.requiredConnectors) {
|
|
1510
|
+
health.components.database = 'unhealthy';
|
|
1511
|
+
health.components.redis = 'unhealthy';
|
|
1512
|
+
dependencyIssues.push(
|
|
1513
|
+
'Integration contract was not loaded - expected config/service/integration-contract.json during initialization'
|
|
1514
|
+
);
|
|
1515
|
+
} else {
|
|
1516
|
+
const dbDependency = await this._checkDatabaseDependency();
|
|
1517
|
+
health.components.database = dbDependency.status;
|
|
1518
|
+
if (dbDependency.status === 'unhealthy' && dbDependency.reason) {
|
|
1519
|
+
dependencyIssues.push(`[db] ${dbDependency.reason}`);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const redisDependency = await this._checkRedisDependency();
|
|
1523
|
+
health.components.redis = redisDependency.status;
|
|
1524
|
+
if (redisDependency.status === 'unhealthy' && redisDependency.reason) {
|
|
1525
|
+
dependencyIssues.push(`[redis] ${redisDependency.reason}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (dependencyIssues.length > 0) {
|
|
1530
|
+
health.dependencyIssues = dependencyIssues;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1333
1533
|
const statuses = Object.values(health.components);
|
|
1334
1534
|
if (statuses.includes('unhealthy')) {
|
|
1335
1535
|
health.status = 'unhealthy';
|