@onlineapps/conn-orch-validator 2.0.28 → 2.0.30
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/README.md
CHANGED
|
@@ -88,20 +88,52 @@ services/my-service/
|
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
**Proof Lifecycle:**
|
|
91
|
-
- **Valid for:**
|
|
91
|
+
- **Valid for:** 7 days OR until service fingerprint changes
|
|
92
92
|
- **Fingerprint:** SHA256 hash of (version + operations + dependencies + config)
|
|
93
93
|
- **Revalidation:** Automatic if proof missing/invalid/expired
|
|
94
94
|
- **Registry:** Accepts service with valid proof (skips Tier 2 validation)
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
98
|
+
## Implementation Standard Levels
|
|
99
|
+
|
|
100
|
+
The validator evaluates each service against cumulative implementation standards. Levels are ordered — each requires all previous to pass:
|
|
101
|
+
|
|
102
|
+
| Level | Name | Checks | Since |
|
|
103
|
+
|-------|------|--------|-------|
|
|
104
|
+
| **v1.0** | Base Service Standard | `conn-config/`, `src/app.js`, `index.js`, valid `config.json` + `operations.json`, `@onlineapps/service-wrapper` dep | 2025-06 |
|
|
105
|
+
| **v1.1** | Multitenancy Standard | `wrapper.tenantContext` configured in `config.json` | 2026-03 |
|
|
106
|
+
| **v1.2** | Business Error Handling | `@onlineapps/service-common` dep + `businessErrorHandler` in `src/app.js` | 2026-03 |
|
|
107
|
+
|
|
108
|
+
**Key properties:**
|
|
109
|
+
- **Cumulative** — v1.2 requires v1.0 + v1.1 to also pass
|
|
110
|
+
- **Baked into validator** — older validator versions naturally know fewer levels (backward compatible)
|
|
111
|
+
- **Warnings** — next unsatisfied level generates `STANDARD_LEVEL_GAP` warnings with specific missing checks
|
|
112
|
+
- **Exposed in `/info`** — ServiceWrapper's `GET /info` endpoint returns the highest satisfied level
|
|
113
|
+
- **In validation results** — `validate()` returns `standardLevel` and `standardDetails`
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
// Programmatic access
|
|
117
|
+
const { ServiceStructureValidator } = require('@onlineapps/conn-orch-validator/src/validators/ServiceStructureValidator');
|
|
118
|
+
const validator = new ServiceStructureValidator('/path/to/service');
|
|
119
|
+
const { level, details } = validator.determineStandardLevel();
|
|
120
|
+
// level = 'v1.2', details = [{ level: 'v1.0', passed: true, checks: [...] }, ...]
|
|
121
|
+
|
|
122
|
+
// List all known levels
|
|
123
|
+
ServiceStructureValidator.getStandardLevels();
|
|
124
|
+
// [{ level: 'v1.0', name: 'Base Service Standard', since: '2025-06-01' }, ...]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Related:** [Service Endpoints — Standard Levels](/docs/standards/SERVICE_ENDPOINTS.md#implementation-standard-levels), [Error Handling Standard](/docs/standards/ERROR_HANDLING.md#business-service-error-standard)
|
|
128
|
+
|
|
98
129
|
## Related Documentation
|
|
99
130
|
|
|
100
131
|
- [SERVICE_REGISTRATION_FLOW.md](/services/hello-service/docs/SERVICE_REGISTRATION_FLOW.md)
|
|
101
132
|
- [/docs/architecture/validator.md](/docs/architecture/validator.md)
|
|
102
133
|
- [/docs/standards/OPERATIONS.md](/docs/standards/OPERATIONS.md)
|
|
134
|
+
- [/docs/standards/ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md)
|
|
103
135
|
- [@onlineapps/service-validator-core](/shared/service-validator-core/README.md)
|
|
104
136
|
|
|
105
137
|
---
|
|
106
138
|
|
|
107
|
-
*Last updated:
|
|
139
|
+
*Last updated: 2026-03-24*
|
package/jest.config.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
process.env.OA_VALIDATION_TENANT_ID = process.env.OA_VALIDATION_TENANT_ID || '99';
|
|
4
|
+
process.env.OA_VALIDATION_WORKSPACE_ID = process.env.OA_VALIDATION_WORKSPACE_ID || '200';
|
|
5
|
+
|
|
3
6
|
module.exports = {
|
|
4
7
|
testEnvironment: 'node',
|
|
5
8
|
coverageDirectory: 'coverage',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -157,13 +157,10 @@ class CookbookTestRunner {
|
|
|
157
157
|
// Resolve operation endpoint from operations.json
|
|
158
158
|
const endpoint = await this.resolveOperation(step.service, step.operation);
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const validationAccountId = process.env.OA_VALIDATION_ACCOUNT_ID;
|
|
165
|
-
if (!validationAccountId) {
|
|
166
|
-
throw new Error('[CookbookTestRunner] Missing required environment variable OA_VALIDATION_ACCOUNT_ID');
|
|
160
|
+
const validationTenantId = process.env.OA_VALIDATION_TENANT_ID;
|
|
161
|
+
const validationWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
|
|
162
|
+
if (!validationTenantId || !validationWorkspaceId) {
|
|
163
|
+
throw new Error('[CookbookTestRunner] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
|
|
167
164
|
}
|
|
168
165
|
const request = {
|
|
169
166
|
method: endpoint.method,
|
|
@@ -172,7 +169,8 @@ class CookbookTestRunner {
|
|
|
172
169
|
timeout: testConfig.timeout || this.timeout,
|
|
173
170
|
headers: {
|
|
174
171
|
'x-validation-request': 'true',
|
|
175
|
-
'
|
|
172
|
+
'x-tenant-id': validationTenantId,
|
|
173
|
+
'x-workspace-id': validationWorkspaceId,
|
|
176
174
|
...resolveHeaders(endpoint.headers),
|
|
177
175
|
...resolveHeaders(step.headers)
|
|
178
176
|
}
|
|
@@ -215,16 +215,15 @@ class ServiceReadinessValidator {
|
|
|
215
215
|
// Generate test input based on schema
|
|
216
216
|
const testInput = this.generateTestInput(operation.input);
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (!validationAccountId) {
|
|
223
|
-
throw new Error('[ServiceReadinessValidator] Missing required environment variable OA_VALIDATION_ACCOUNT_ID');
|
|
218
|
+
const validationTenantId = process.env.OA_VALIDATION_TENANT_ID;
|
|
219
|
+
const validationWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
|
|
220
|
+
if (!validationTenantId || !validationWorkspaceId) {
|
|
221
|
+
throw new Error('[ServiceReadinessValidator] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
|
|
224
222
|
}
|
|
225
223
|
const headers = {
|
|
226
224
|
'x-validation-request': 'true',
|
|
227
|
-
'
|
|
225
|
+
'x-tenant-id': validationTenantId,
|
|
226
|
+
'x-workspace-id': validationWorkspaceId,
|
|
228
227
|
...resolveHeaders(operation.headers)
|
|
229
228
|
};
|
|
230
229
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
|
|
5
|
+
const { ValidationProofCodec, ValidationProofVerifier } = require('@onlineapps/service-validator-core');
|
|
6
6
|
const FingerprintUtils = require('@onlineapps/service-validator-core').FingerprintUtils;
|
|
7
7
|
const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
|
|
8
8
|
const ServiceReadinessValidator = require('./ServiceReadinessValidator');
|
|
@@ -97,10 +97,10 @@ class ValidationOrchestrator {
|
|
|
97
97
|
return false;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
// Verify proof integrity + age using the shared core verifier defaults.
|
|
101
|
+
// This MUST match what Tier-2 validator enforces to avoid PROOF_EXPIRED drift.
|
|
102
|
+
const verifier = new ValidationProofVerifier();
|
|
103
|
+
const verificationResult = verifier.verifyProof(proof);
|
|
104
104
|
|
|
105
105
|
if (!verificationResult.valid) {
|
|
106
106
|
console.log(`[ValidationOrchestrator] Proof validation failed: ${verificationResult.reason}`);
|
|
@@ -136,7 +136,7 @@ class ValidationOrchestrator {
|
|
|
136
136
|
const packageFile = path.join(this.serviceRoot, 'package.json');
|
|
137
137
|
const dockerFile = path.join(this.serviceRoot, 'Dockerfile');
|
|
138
138
|
const dockerComposeFile = path.join(this.serviceRoot, 'docker-compose.yml');
|
|
139
|
-
const
|
|
139
|
+
const envTemplateFile = path.join(this.serviceRoot, '..', '..', 'config', 'env-templates', `${path.basename(this.serviceRoot)}.env`);
|
|
140
140
|
|
|
141
141
|
if (!fs.existsSync(configFile)) {
|
|
142
142
|
throw new Error('config.json not found');
|
|
@@ -154,8 +154,8 @@ class ValidationOrchestrator {
|
|
|
154
154
|
if (fs.existsSync(dockerComposeFile)) {
|
|
155
155
|
infra.dockerCompose = fs.readFileSync(dockerComposeFile, 'utf8');
|
|
156
156
|
}
|
|
157
|
-
if (fs.existsSync(
|
|
158
|
-
infra.
|
|
157
|
+
if (fs.existsSync(envTemplateFile)) {
|
|
158
|
+
infra.envTemplate = fs.readFileSync(envTemplateFile, 'utf8');
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// Extract @onlineapps/* dependencies
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Validates that a business service has correct directory structure,
|
|
5
5
|
* configuration files, and follows OA Drive standards.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Each standard level is cumulative — v1.2 requires all checks from v1.0 + v1.1 + v1.2.
|
|
8
|
+
* Older validator versions naturally support fewer levels (they don't know about newer ones).
|
|
8
9
|
*
|
|
9
10
|
* @module validators/ServiceStructureValidator
|
|
10
11
|
*/
|
|
@@ -14,6 +15,122 @@
|
|
|
14
15
|
const fs = require('fs');
|
|
15
16
|
const path = require('path');
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Try reading from new path first (config/service/), fall back to legacy (conn-config/).
|
|
20
|
+
* @param {string} root - Service root
|
|
21
|
+
* @param {string} filename - e.g. 'config.json'
|
|
22
|
+
* @returns {string|null} File contents or null
|
|
23
|
+
*/
|
|
24
|
+
function readConfigFile(root, filename) {
|
|
25
|
+
const candidates = [
|
|
26
|
+
path.join(root, 'config', 'service', filename),
|
|
27
|
+
path.join(root, 'conn-config', filename)
|
|
28
|
+
];
|
|
29
|
+
for (const p of candidates) {
|
|
30
|
+
try { return fs.readFileSync(p, 'utf-8'); } catch { /* try next */ }
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasConfigDir(root) {
|
|
36
|
+
return fs.existsSync(path.join(root, 'config', 'service')) ||
|
|
37
|
+
fs.existsSync(path.join(root, 'conn-config'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const STANDARD_LEVELS = [
|
|
41
|
+
{
|
|
42
|
+
level: 'v1.0',
|
|
43
|
+
name: 'Base Service Standard',
|
|
44
|
+
since: '2025-06-01',
|
|
45
|
+
checks: (root) => {
|
|
46
|
+
const results = [];
|
|
47
|
+
const has = (p) => fs.existsSync(path.join(root, p));
|
|
48
|
+
const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
|
|
49
|
+
|
|
50
|
+
results.push({ passed: hasConfigDir(root), id: 'dir_conn_config', message: 'config/service/ or conn-config/ directory' });
|
|
51
|
+
results.push({ passed: has('src'), id: 'dir_src', message: 'src/ directory' });
|
|
52
|
+
results.push({ passed: has('tests'), id: 'dir_tests', message: 'tests/ directory' });
|
|
53
|
+
results.push({ passed: has('src/app.js'), id: 'file_app', message: 'src/app.js' });
|
|
54
|
+
results.push({ passed: has('index.js'), id: 'file_index', message: 'index.js entry point' });
|
|
55
|
+
|
|
56
|
+
const configRaw = readConfigFile(root, 'config.json');
|
|
57
|
+
let configValid = false;
|
|
58
|
+
if (configRaw) {
|
|
59
|
+
try {
|
|
60
|
+
const cfg = JSON.parse(configRaw);
|
|
61
|
+
configValid = !!(cfg.service?.name && cfg.service?.port);
|
|
62
|
+
} catch { /* invalid JSON */ }
|
|
63
|
+
}
|
|
64
|
+
results.push({ passed: configValid, id: 'config_valid', message: 'Valid config.json with service.name + service.port' });
|
|
65
|
+
|
|
66
|
+
const opsRaw = readConfigFile(root, 'operations.json');
|
|
67
|
+
let opsValid = false;
|
|
68
|
+
if (opsRaw) {
|
|
69
|
+
try {
|
|
70
|
+
const ops = JSON.parse(opsRaw);
|
|
71
|
+
opsValid = !!(ops.operations && typeof ops.operations === 'object');
|
|
72
|
+
} catch { /* invalid JSON */ }
|
|
73
|
+
}
|
|
74
|
+
results.push({ passed: opsValid, id: 'operations_valid', message: 'Valid operations.json' });
|
|
75
|
+
|
|
76
|
+
const pkgRaw = read('package.json');
|
|
77
|
+
let hasWrapper = false;
|
|
78
|
+
if (pkgRaw) {
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(pkgRaw);
|
|
81
|
+
hasWrapper = !!pkg.dependencies?.['@onlineapps/service-wrapper'];
|
|
82
|
+
} catch { /* invalid JSON */ }
|
|
83
|
+
}
|
|
84
|
+
results.push({ passed: hasWrapper, id: 'dep_service_wrapper', message: '@onlineapps/service-wrapper dependency' });
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
level: 'v1.1',
|
|
91
|
+
name: 'Multitenancy Standard',
|
|
92
|
+
since: '2026-03-01',
|
|
93
|
+
checks: (root) => {
|
|
94
|
+
const configRaw = readConfigFile(root, 'config.json');
|
|
95
|
+
let hasTenantContext = false;
|
|
96
|
+
if (configRaw) {
|
|
97
|
+
try {
|
|
98
|
+
const cfg = JSON.parse(configRaw);
|
|
99
|
+
hasTenantContext = cfg.wrapper?.tenantContext !== undefined;
|
|
100
|
+
} catch { /* invalid JSON */ }
|
|
101
|
+
}
|
|
102
|
+
return [
|
|
103
|
+
{ passed: hasTenantContext, id: 'tenant_context', message: 'wrapper.tenantContext configured in config.json' }
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
level: 'v1.2',
|
|
109
|
+
name: 'Business Error Handling Standard',
|
|
110
|
+
since: '2026-03-24',
|
|
111
|
+
checks: (root) => {
|
|
112
|
+
const read = (p) => { try { return fs.readFileSync(path.join(root, p), 'utf-8'); } catch { return null; } };
|
|
113
|
+
|
|
114
|
+
const pkgRaw = read('package.json');
|
|
115
|
+
let hasServiceCommon = false;
|
|
116
|
+
if (pkgRaw) {
|
|
117
|
+
try {
|
|
118
|
+
const pkg = JSON.parse(pkgRaw);
|
|
119
|
+
hasServiceCommon = !!pkg.dependencies?.['@onlineapps/service-common'];
|
|
120
|
+
} catch { /* invalid JSON */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const appContent = read('src/app.js') || '';
|
|
124
|
+
const hasErrorMiddleware = appContent.includes('businessErrorHandler');
|
|
125
|
+
|
|
126
|
+
return [
|
|
127
|
+
{ passed: hasServiceCommon, id: 'dep_service_common', message: '@onlineapps/service-common dependency' },
|
|
128
|
+
{ passed: hasErrorMiddleware, id: 'error_middleware', message: 'businessErrorHandler middleware in src/app.js' }
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
|
|
17
134
|
class ServiceStructureValidator {
|
|
18
135
|
constructor(serviceRoot) {
|
|
19
136
|
this.serviceRoot = serviceRoot;
|
|
@@ -49,26 +166,100 @@ class ServiceStructureValidator {
|
|
|
49
166
|
// 5. Validate test structure
|
|
50
167
|
this.validateTestStructure();
|
|
51
168
|
|
|
169
|
+
// 6. Determine standard level
|
|
170
|
+
const standardResult = this.determineStandardLevel();
|
|
171
|
+
|
|
52
172
|
const valid = this.errors.length === 0;
|
|
53
173
|
|
|
54
174
|
return {
|
|
55
175
|
valid,
|
|
56
176
|
errors: this.errors,
|
|
57
177
|
warnings: this.warnings,
|
|
58
|
-
info: this.info
|
|
178
|
+
info: this.info,
|
|
179
|
+
standardLevel: standardResult.level,
|
|
180
|
+
standardDetails: standardResult.details
|
|
59
181
|
};
|
|
60
182
|
}
|
|
61
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Determine the highest satisfied implementation standard level.
|
|
186
|
+
* Levels are cumulative — each requires all previous levels to pass.
|
|
187
|
+
*
|
|
188
|
+
* @returns {{ level: string|null, details: Array<{ level: string, name: string, passed: boolean, checks: Array }> }}
|
|
189
|
+
*/
|
|
190
|
+
determineStandardLevel() {
|
|
191
|
+
const details = [];
|
|
192
|
+
let highestPassed = null;
|
|
193
|
+
|
|
194
|
+
for (const standard of STANDARD_LEVELS) {
|
|
195
|
+
const checkResults = standard.checks(this.serviceRoot);
|
|
196
|
+
const allPassed = checkResults.every(c => c.passed);
|
|
197
|
+
|
|
198
|
+
details.push({
|
|
199
|
+
level: standard.level,
|
|
200
|
+
name: standard.name,
|
|
201
|
+
since: standard.since,
|
|
202
|
+
passed: allPassed,
|
|
203
|
+
checks: checkResults
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (allPassed) {
|
|
207
|
+
highestPassed = standard.level;
|
|
208
|
+
} else {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (highestPassed) {
|
|
214
|
+
this.info.push(`✓ Implementation standard level: ${highestPassed}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const nextLevel = details.find(d => !d.passed);
|
|
218
|
+
if (nextLevel) {
|
|
219
|
+
const failing = nextLevel.checks.filter(c => !c.passed);
|
|
220
|
+
for (const check of failing) {
|
|
221
|
+
this.warnings.push({
|
|
222
|
+
type: 'STANDARD_LEVEL_GAP',
|
|
223
|
+
level: nextLevel.level,
|
|
224
|
+
check: check.id,
|
|
225
|
+
message: `Standard ${nextLevel.level} (${nextLevel.name}): missing ${check.message}`,
|
|
226
|
+
fix: `Implement ${check.message} to reach standard ${nextLevel.level}. See docs/standards/SERVICE_TEMPLATE.md`
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { level: highestPassed, details };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all known standard levels (for /info endpoint and external tools).
|
|
236
|
+
* @returns {Array<{ level: string, name: string, since: string }>}
|
|
237
|
+
*/
|
|
238
|
+
static getStandardLevels() {
|
|
239
|
+
return STANDARD_LEVELS.map(s => ({ level: s.level, name: s.name, since: s.since }));
|
|
240
|
+
}
|
|
241
|
+
|
|
62
242
|
/**
|
|
63
243
|
* Validate directory structure exists
|
|
64
244
|
*/
|
|
65
245
|
validateDirectoryStructure() {
|
|
66
246
|
const requiredDirs = [
|
|
67
|
-
{ path: 'conn-config', description: 'Configuration directory' },
|
|
68
247
|
{ path: 'src', description: 'Source code directory' },
|
|
69
248
|
{ path: 'tests', description: 'Tests directory' }
|
|
70
249
|
];
|
|
71
250
|
|
|
251
|
+
if (!hasConfigDir(this.serviceRoot)) {
|
|
252
|
+
this.errors.push({
|
|
253
|
+
type: 'MISSING_DIRECTORY',
|
|
254
|
+
path: 'config/service/ (or conn-config/)',
|
|
255
|
+
message: 'Required directory missing: config/service/ (or legacy conn-config/)',
|
|
256
|
+
description: 'Configuration directory',
|
|
257
|
+
fix: 'Create directory: mkdir -p config/service'
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
this.info.push('✓ Found Configuration directory');
|
|
261
|
+
}
|
|
262
|
+
|
|
72
263
|
const recommendedDirs = [
|
|
73
264
|
{ path: 'tests/unit', description: 'Unit tests' },
|
|
74
265
|
{ path: 'tests/integration', description: 'Integration tests' },
|
|
@@ -108,48 +299,46 @@ class ServiceStructureValidator {
|
|
|
108
299
|
* Validate configuration files
|
|
109
300
|
*/
|
|
110
301
|
validateConfigurationFiles() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!fs.existsSync(configPath)) {
|
|
302
|
+
const configRaw = readConfigFile(this.serviceRoot, 'config.json');
|
|
303
|
+
if (!configRaw) {
|
|
114
304
|
this.errors.push({
|
|
115
305
|
type: 'MISSING_CONFIG',
|
|
116
|
-
path: 'conn-config/config.json',
|
|
117
|
-
message: 'Service configuration missing: conn-config/config.json',
|
|
306
|
+
path: 'config/service/config.json (or conn-config/config.json)',
|
|
307
|
+
message: 'Service configuration missing: config/service/config.json (or conn-config/config.json)',
|
|
118
308
|
fix: 'Create config.json with service metadata. See: /docs/standards/SERVICE_TEMPLATE.md'
|
|
119
309
|
});
|
|
120
310
|
} else {
|
|
121
311
|
try {
|
|
122
|
-
const config = JSON.parse(
|
|
312
|
+
const config = JSON.parse(configRaw);
|
|
123
313
|
this.validateConfigStructure(config);
|
|
124
314
|
this.info.push('✓ Found valid config.json');
|
|
125
315
|
} catch (error) {
|
|
126
316
|
this.errors.push({
|
|
127
317
|
type: 'INVALID_CONFIG',
|
|
128
|
-
path: '
|
|
318
|
+
path: 'config.json',
|
|
129
319
|
message: `Invalid JSON in config.json: ${error.message}`,
|
|
130
320
|
fix: 'Fix JSON syntax errors in config.json'
|
|
131
321
|
});
|
|
132
322
|
}
|
|
133
323
|
}
|
|
134
324
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!fs.existsSync(operationsPath)) {
|
|
325
|
+
const opsRaw = readConfigFile(this.serviceRoot, 'operations.json');
|
|
326
|
+
if (!opsRaw) {
|
|
138
327
|
this.errors.push({
|
|
139
328
|
type: 'MISSING_OPERATIONS',
|
|
140
|
-
path: 'conn-config/operations.json',
|
|
141
|
-
message: 'Operations specification missing: conn-config/operations.json',
|
|
329
|
+
path: 'config/service/operations.json (or conn-config/operations.json)',
|
|
330
|
+
message: 'Operations specification missing: config/service/operations.json (or conn-config/operations.json)',
|
|
142
331
|
fix: 'Create operations.json. See: /docs/standards/OPERATIONS.md'
|
|
143
332
|
});
|
|
144
333
|
} else {
|
|
145
334
|
try {
|
|
146
|
-
const operations = JSON.parse(
|
|
335
|
+
const operations = JSON.parse(opsRaw);
|
|
147
336
|
this.validateOperationsStructure(operations);
|
|
148
337
|
this.info.push('✓ Found valid operations.json');
|
|
149
338
|
} catch (error) {
|
|
150
339
|
this.errors.push({
|
|
151
340
|
type: 'INVALID_OPERATIONS',
|
|
152
|
-
path: '
|
|
341
|
+
path: 'operations.json',
|
|
153
342
|
message: `Invalid JSON in operations.json: ${error.message}`,
|
|
154
343
|
fix: 'Fix JSON syntax errors in operations.json'
|
|
155
344
|
});
|
|
@@ -178,7 +367,7 @@ class ServiceStructureValidator {
|
|
|
178
367
|
if (!value) {
|
|
179
368
|
this.errors.push({
|
|
180
369
|
type: 'MISSING_CONFIG_FIELD',
|
|
181
|
-
path: '
|
|
370
|
+
path: 'config/service/config.json',
|
|
182
371
|
field: field,
|
|
183
372
|
message: `Required field missing in config.json: ${field}`,
|
|
184
373
|
fix: `Add "${field}" to config.json`
|
|
@@ -191,7 +380,7 @@ class ServiceStructureValidator {
|
|
|
191
380
|
if (!/^[a-z][a-z0-9-]*$/.test(config.service.name)) {
|
|
192
381
|
this.warnings.push({
|
|
193
382
|
type: 'INVALID_SERVICE_NAME',
|
|
194
|
-
path: '
|
|
383
|
+
path: 'config/service/config.json',
|
|
195
384
|
field: 'service.name',
|
|
196
385
|
value: config.service.name,
|
|
197
386
|
message: `Service name should be lowercase kebab-case: ${config.service.name}`,
|
|
@@ -227,7 +416,7 @@ class ServiceStructureValidator {
|
|
|
227
416
|
if (!operations.operations) {
|
|
228
417
|
this.errors.push({
|
|
229
418
|
type: 'INVALID_OPERATIONS_STRUCTURE',
|
|
230
|
-
path: '
|
|
419
|
+
path: 'config/service/operations.json',
|
|
231
420
|
message: 'operations.json must have "operations" key',
|
|
232
421
|
fix: 'Wrap operations in {"operations": {...}}. See: /docs/standards/OPERATIONS.md'
|
|
233
422
|
});
|
|
@@ -238,7 +427,7 @@ class ServiceStructureValidator {
|
|
|
238
427
|
if (typeof ops !== 'object' || Array.isArray(ops)) {
|
|
239
428
|
this.errors.push({
|
|
240
429
|
type: 'INVALID_OPERATIONS_TYPE',
|
|
241
|
-
path: '
|
|
430
|
+
path: 'config/service/operations.json',
|
|
242
431
|
message: 'operations must be an object',
|
|
243
432
|
fix: 'operations should be key-value pairs: {"operation-name": {...}}'
|
|
244
433
|
});
|
|
@@ -248,7 +437,7 @@ class ServiceStructureValidator {
|
|
|
248
437
|
if (Object.keys(ops).length === 0) {
|
|
249
438
|
this.warnings.push({
|
|
250
439
|
type: 'NO_OPERATIONS',
|
|
251
|
-
path: '
|
|
440
|
+
path: 'config/service/operations.json',
|
|
252
441
|
message: 'No operations defined',
|
|
253
442
|
fix: 'Add at least one operation to operations.json'
|
|
254
443
|
});
|
|
@@ -271,7 +460,7 @@ class ServiceStructureValidator {
|
|
|
271
460
|
if (!spec[field]) {
|
|
272
461
|
this.errors.push({
|
|
273
462
|
type: 'MISSING_OPERATION_FIELD',
|
|
274
|
-
path: '
|
|
463
|
+
path: 'config/service/operations.json',
|
|
275
464
|
operation: name,
|
|
276
465
|
field: field,
|
|
277
466
|
message: `Operation "${name}" missing required field: ${field}`,
|
|
@@ -284,7 +473,7 @@ class ServiceStructureValidator {
|
|
|
284
473
|
if (spec.endpoint && !spec.endpoint.startsWith('/')) {
|
|
285
474
|
this.errors.push({
|
|
286
475
|
type: 'INVALID_ENDPOINT',
|
|
287
|
-
path: '
|
|
476
|
+
path: 'config/service/operations.json',
|
|
288
477
|
operation: name,
|
|
289
478
|
field: 'endpoint',
|
|
290
479
|
value: spec.endpoint,
|
|
@@ -298,7 +487,7 @@ class ServiceStructureValidator {
|
|
|
298
487
|
if (spec.method && !validMethods.includes(spec.method)) {
|
|
299
488
|
this.errors.push({
|
|
300
489
|
type: 'INVALID_HTTP_METHOD',
|
|
301
|
-
path: '
|
|
490
|
+
path: 'config/service/operations.json',
|
|
302
491
|
operation: name,
|
|
303
492
|
field: 'method',
|
|
304
493
|
value: spec.method,
|
|
@@ -350,7 +539,8 @@ class ServiceStructureValidator {
|
|
|
350
539
|
// Check for @onlineapps dependencies
|
|
351
540
|
const requiredDeps = [
|
|
352
541
|
'@onlineapps/service-wrapper',
|
|
353
|
-
'@onlineapps/conn-orch-validator'
|
|
542
|
+
'@onlineapps/conn-orch-validator',
|
|
543
|
+
'@onlineapps/service-common'
|
|
354
544
|
];
|
|
355
545
|
|
|
356
546
|
for (const dep of requiredDeps) {
|
|
@@ -391,6 +581,26 @@ class ServiceStructureValidator {
|
|
|
391
581
|
});
|
|
392
582
|
} else {
|
|
393
583
|
this.info.push('✓ Found src/app.js');
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const appContent = fs.readFileSync(appPath, 'utf-8');
|
|
587
|
+
if (appContent.includes('businessErrorHandler')) {
|
|
588
|
+
this.info.push('✓ businessErrorHandler middleware registered');
|
|
589
|
+
} else {
|
|
590
|
+
this.warnings.push({
|
|
591
|
+
type: 'MISSING_ERROR_MIDDLEWARE',
|
|
592
|
+
path: 'src/app.js',
|
|
593
|
+
message: 'businessErrorHandler middleware not found in src/app.js',
|
|
594
|
+
fix: 'Register businessErrorHandler from @onlineapps/service-common as Express error middleware. See: /docs/standards/ERROR_HANDLING.md'
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
} catch (readError) {
|
|
598
|
+
this.warnings.push({
|
|
599
|
+
type: 'UNREADABLE_APP',
|
|
600
|
+
path: 'src/app.js',
|
|
601
|
+
message: `Could not read src/app.js: ${readError.message}`
|
|
602
|
+
});
|
|
603
|
+
}
|
|
394
604
|
}
|
|
395
605
|
|
|
396
606
|
const indexPath = path.join(this.serviceRoot, 'index.js');
|