@jammysunshine/astrology-shared 1.0.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/examples/index.js +455 -0
- package/examples/serviceExamples.js +359 -0
- package/index.js +75 -0
- package/interfaces/IDataAccessService.js +70 -0
- package/interfaces/IUserContextService.js +58 -0
- package/logger/index.js +417 -0
- package/logger/test_fix.js +56 -0
- package/package.json +43 -0
- package/schemas/README.md +97 -0
- package/schemas/index.js +940 -0
- package/validation/BaseValidator.js +308 -0
- package/validation/index.js +88 -0
package/schemas/index.js
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modular Schema Management System
|
|
3
|
+
* Centralizes loading, validation, and registration of JSON Schemas using AJV.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const Ajv = require('ajv');
|
|
9
|
+
const addFormats = require('ajv-formats');
|
|
10
|
+
const logger = require('../logger').default;
|
|
11
|
+
|
|
12
|
+
// Initialize AJV with moderate validation and proper reference resolution
|
|
13
|
+
const ajv = new Ajv({
|
|
14
|
+
allErrors: true, // Report all validation errors
|
|
15
|
+
removeAdditional: false, // Don't remove additional properties
|
|
16
|
+
useDefaults: true, // Apply default values
|
|
17
|
+
coerceTypes: false, // Don't coerce types
|
|
18
|
+
strict: false, // Turn off strict mode to allow proper allOf resolution
|
|
19
|
+
validateFormats: true, // Still validate format keywords
|
|
20
|
+
validateSchema: true, // Validate schema structure
|
|
21
|
+
// Enable proper reference resolution
|
|
22
|
+
schemaId: '$id', // Use $id for schema identification
|
|
23
|
+
loadSchema: async (uri) => {
|
|
24
|
+
// Custom schema loading function to handle cross-references
|
|
25
|
+
const schemaPath = path.join(__dirname, uri.replace('http://localhost/schemas/', ''));
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
throw new Error(`Failed to load schema ${uri}: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Add AJV plugins for better reference resolution
|
|
35
|
+
require('ajv/lib/refs/json-schema-draft-07.json'); // Load standard schema definition
|
|
36
|
+
|
|
37
|
+
// Add common formats like date-time, uuid, email, etc.
|
|
38
|
+
addFormats(ajv);
|
|
39
|
+
|
|
40
|
+
// Enhanced cache for loaded schemas and compiled validators with TTL
|
|
41
|
+
const schemaCache = new Map();
|
|
42
|
+
const validatorCache = new Map();
|
|
43
|
+
const schemaCacheTimestamps = new Map();
|
|
44
|
+
const validatorCacheTimestamps = new Map();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* High-performance iterative deep clone utility optimized for schema validation.
|
|
48
|
+
* Mimics JSON serialization behavior (Dates to strings, removes undefined/functions)
|
|
49
|
+
* but avoids the overhead of full stringification and recursion stack overflows.
|
|
50
|
+
*/
|
|
51
|
+
function fastClone(input) {
|
|
52
|
+
if (input === null) {
|
|
53
|
+
return input;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (input instanceof Date) {
|
|
57
|
+
return input.toISOString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof input !== 'object') {
|
|
61
|
+
return input;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = Array.isArray(input) ? [] : {};
|
|
65
|
+
const stack = [{ source: input, target: result }];
|
|
66
|
+
const seen = new WeakMap();
|
|
67
|
+
|
|
68
|
+
if (typeof input === 'object' && input !== null) {
|
|
69
|
+
seen.set(input, result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
while (stack.length > 0) {
|
|
73
|
+
const { source, target } = stack.pop();
|
|
74
|
+
|
|
75
|
+
for (const key in source) {
|
|
76
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
|
|
77
|
+
|
|
78
|
+
const value = source[key];
|
|
79
|
+
|
|
80
|
+
if (value === undefined || typeof value === 'function') continue;
|
|
81
|
+
|
|
82
|
+
if (value === null || typeof value !== 'object') {
|
|
83
|
+
target[key] = value;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle Date objects
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
target[key] = value.toISOString();
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle circular references or already processed objects
|
|
94
|
+
if (seen.has(value)) {
|
|
95
|
+
target[key] = seen.get(value);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const newValue = Array.isArray(value) ? [] : {};
|
|
100
|
+
target[key] = newValue;
|
|
101
|
+
seen.set(value, newValue);
|
|
102
|
+
stack.push({ source: value, target: newValue });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Cache TTL settings (in milliseconds)
|
|
110
|
+
const SCHEMA_CACHE_TTL = process.env.SCHEMA_CACHE_TTL || 3600000; // 1 hour default
|
|
111
|
+
const VALIDATOR_CACHE_TTL = process.env.VALIDATOR_CACHE_TTL || 3600000; // 1 hour default
|
|
112
|
+
|
|
113
|
+
// Method-to-schema mapping registry for automatic resolution
|
|
114
|
+
const methodSchemaMappings = new Map();
|
|
115
|
+
|
|
116
|
+
// Cache management functions
|
|
117
|
+
function isCacheExpired(cacheTimestamps, key, ttl) {
|
|
118
|
+
const timestamp = cacheTimestamps.get(key);
|
|
119
|
+
if (!timestamp) return true;
|
|
120
|
+
return (Date.now() - timestamp) > ttl;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getCachedSchema(key) {
|
|
124
|
+
if (isCacheExpired(schemaCacheTimestamps, key, SCHEMA_CACHE_TTL)) {
|
|
125
|
+
schemaCache.delete(key);
|
|
126
|
+
schemaCacheTimestamps.delete(key);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return schemaCache.get(key);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setCachedSchema(key, schema) {
|
|
133
|
+
schemaCache.set(key, schema);
|
|
134
|
+
schemaCacheTimestamps.set(key, Date.now());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getCachedValidator(key) {
|
|
138
|
+
if (isCacheExpired(validatorCacheTimestamps, key, VALIDATOR_CACHE_TTL)) {
|
|
139
|
+
validatorCache.delete(key);
|
|
140
|
+
validatorCacheTimestamps.delete(key);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return validatorCache.get(key);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function setCachedValidator(key, validator) {
|
|
147
|
+
validatorCache.set(key, validator);
|
|
148
|
+
validatorCacheTimestamps.set(key, Date.now());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Strict validation mode (can be controlled via environment)
|
|
152
|
+
|
|
153
|
+
// Schema version management
|
|
154
|
+
const schemaVersions = new Map();
|
|
155
|
+
const schemaVersionHistory = new Map();
|
|
156
|
+
|
|
157
|
+
function registerSchemaVersion(schemaName, version, schemaDefinition) {
|
|
158
|
+
const key = `${schemaName}@${version}`;
|
|
159
|
+
schemaVersions.set(key, schemaDefinition);
|
|
160
|
+
schemaVersionHistory.set(schemaName, version);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getSchemaVersion(schemaName, version = 'latest') {
|
|
164
|
+
if (version === 'latest') {
|
|
165
|
+
version = schemaVersionHistory.get(schemaName) || 'v1';
|
|
166
|
+
}
|
|
167
|
+
const key = `${schemaName}@${version}`;
|
|
168
|
+
return schemaVersions.get(key);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getSchemaVersionHistory(schemaName) {
|
|
172
|
+
return schemaVersionHistory.get(schemaName);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Method-to-schema mapping functions
|
|
176
|
+
function registerMethodSchemaMapping(serviceName, methodName, schemas) {
|
|
177
|
+
const key = `${serviceName}.${methodName}`;
|
|
178
|
+
methodSchemaMappings.set(key, schemas);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getMethodSchemaMapping(serviceName, methodName) {
|
|
182
|
+
const key = `${serviceName}.${methodName}`;
|
|
183
|
+
return methodSchemaMappings.get(key);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getValidationSchemas(serviceName, methodName) {
|
|
187
|
+
// First try explicit method mapping
|
|
188
|
+
const methodMapping = getMethodSchemaMapping(serviceName, methodName);
|
|
189
|
+
if (methodMapping) {
|
|
190
|
+
return methodMapping;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fallback to automatic resolution based on service and method patterns
|
|
194
|
+
const inputSchema = getAutomaticInputSchema(serviceName, methodName);
|
|
195
|
+
const outputSchema = getAutomaticOutputSchema(serviceName, methodName);
|
|
196
|
+
const dataSchema = getAutomaticDataSchema(serviceName, methodName);
|
|
197
|
+
const errorSchema = getAutomaticErrorSchema(serviceName, methodName);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
inputSchema,
|
|
201
|
+
outputSchema,
|
|
202
|
+
dataSchema,
|
|
203
|
+
errorSchema
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getAutomaticInputSchema(serviceName, methodName) {
|
|
208
|
+
// First check if service has a registered schema category
|
|
209
|
+
const registeredSchemaCategory = serviceSchemaMap.get(serviceName);
|
|
210
|
+
const schemaCategory = registeredSchemaCategory ||
|
|
211
|
+
serviceNameToSchemaCategory[serviceName] ||
|
|
212
|
+
'default';
|
|
213
|
+
|
|
214
|
+
if (schemaCategory !== 'default') {
|
|
215
|
+
const schemaName = schemaCategoryToSchemaName[schemaCategory];
|
|
216
|
+
if (schemaName) {
|
|
217
|
+
return schemaName;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Fallback to automatic mapping based on service type
|
|
222
|
+
if (serviceName.toLowerCase().includes('birth') || serviceName.toLowerCase().includes('chart')) {
|
|
223
|
+
return 'astrology/vedic/input';
|
|
224
|
+
} else if (serviceName.toLowerCase().includes('dasha')) {
|
|
225
|
+
return 'astrology/vedic/input';
|
|
226
|
+
} else if (serviceName.toLowerCase().includes('compatibility')) {
|
|
227
|
+
return 'astrology/compatibility/input';
|
|
228
|
+
}
|
|
229
|
+
return 'astrology/vedic/input'; // Default
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getAutomaticOutputSchema(serviceName, methodName) {
|
|
233
|
+
// Automatic mapping based on service type
|
|
234
|
+
if (serviceName.toLowerCase().includes('birth') || serviceName.toLowerCase().includes('chart')) {
|
|
235
|
+
return 'v1/domains/astrology/services/birth-chart/service.v1.json';
|
|
236
|
+
} else if (serviceName.toLowerCase().includes('dasha')) {
|
|
237
|
+
return 'v1/domains/astrology/services/dasha/service.v1.json';
|
|
238
|
+
}
|
|
239
|
+
return 'v1/domains/astrology/services/birth-chart/service.v1.json'; // Default
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getAutomaticDataSchema(serviceName, methodName) {
|
|
243
|
+
// Automatic mapping based on service type for data schemas
|
|
244
|
+
if (serviceName.toLowerCase().includes('birth') || serviceName.toLowerCase().includes('chart')) {
|
|
245
|
+
return 'astrology/birth-chart-data';
|
|
246
|
+
} else if (serviceName.toLowerCase().includes('dasha')) {
|
|
247
|
+
return 'astrology/dasha-data';
|
|
248
|
+
}
|
|
249
|
+
return 'astrology/birth-chart-data'; // Default
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getAutomaticErrorSchema(serviceName, methodName) {
|
|
253
|
+
return 'v1/shared/error.v1.json';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Hardcoded schema registry - maps schema names to file paths.
|
|
258
|
+
* @returns {object} Schema registry object.
|
|
259
|
+
*/
|
|
260
|
+
function getRegistry() {
|
|
261
|
+
return {
|
|
262
|
+
services: {
|
|
263
|
+
astrology: {
|
|
264
|
+
'birth-chart': {
|
|
265
|
+
service: { path: 'v1/domains/astrology/services/birth-chart/service.v1.json' }
|
|
266
|
+
},
|
|
267
|
+
dasha: {
|
|
268
|
+
service: { path: 'v1/domains/astrology/services/dasha/service.v1.json' }
|
|
269
|
+
},
|
|
270
|
+
sessionManager: {
|
|
271
|
+
service: { path: 'v1/domains/astrology/components/session-data.v1.json' }
|
|
272
|
+
},
|
|
273
|
+
astrologer: { // Assuming astrologer service uses vedic input for its service schema
|
|
274
|
+
v1: { path: 'v1/domains/astrology/inputs/vedic.v1.json' },
|
|
275
|
+
service: { path: 'v1/domains/astrology/inputs/vedic.v1.json' }
|
|
276
|
+
},
|
|
277
|
+
vedic: {
|
|
278
|
+
input: {
|
|
279
|
+
v1: { path: 'v1/domains/astrology/inputs/vedic.v1.json' }
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
compatibility: {
|
|
283
|
+
input: {
|
|
284
|
+
v1: { path: 'v1/domains/astrology/inputs/compatibility.v1.json' }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
components: {
|
|
290
|
+
astrology: {
|
|
291
|
+
planet: { path: 'v1/domains/astrology/components/planet.json' },
|
|
292
|
+
house: { path: 'v1/domains/astrology/components/house.json' },
|
|
293
|
+
aspect: { path: 'v1/domains/astrology/components/aspect.json' },
|
|
294
|
+
'dasha-period': { path: 'v1/domains/astrology/components/dasha-period.v1.json' },
|
|
295
|
+
'birth-data': { path: 'v1/domains/astrology/components/birthData.json' },
|
|
296
|
+
'user-data': { path: 'v1/domains/astrology/components/user-data.v1.json' },
|
|
297
|
+
'session-data': { path: 'v1/domains/astrology/components/session-data.v1.json' },
|
|
298
|
+
'birth-chart-data': { path: 'v1/domains/astrology/services/birth-chart/data.v1.json' },
|
|
299
|
+
'dasha-data': { path: 'v1/domains/astrology/services/dasha/data.v1.json' }
|
|
300
|
+
},
|
|
301
|
+
shared: {
|
|
302
|
+
metadata: { path: 'v1/shared/metadata.v1.json' },
|
|
303
|
+
error: { path: 'v1/shared/error.v1.json' },
|
|
304
|
+
performance: { path: 'v1/shared/performance.v1.json' },
|
|
305
|
+
'client-context': { path: 'v1/shared/client-context.v1.json' },
|
|
306
|
+
preferences: { path: 'v1/shared/preferences.v1.json' },
|
|
307
|
+
'platform-context': { path: 'v1/shared/PlatformContext.json' },
|
|
308
|
+
'response-metadata': { path: 'v1/shared/ResponseMetadata.json' },
|
|
309
|
+
'platform-user-id': { path: 'v1/shared/PlatformUserId.json' },
|
|
310
|
+
observability: { path: 'v1/services/observability/query.v1.json' },
|
|
311
|
+
'astrology/birthData': { path: 'v1/domains/astrology/components/birthData.json' },
|
|
312
|
+
sessionManager: { path: 'v1/domains/astrology/components/session-data.v1.json' },
|
|
313
|
+
dasha: { path: 'v1/domains/astrology/services/dasha/service.v1.json' },
|
|
314
|
+
birthchart: { path: 'v1/domains/astrology/services/birth-chart/service.v1.json' }
|
|
315
|
+
},
|
|
316
|
+
services: {
|
|
317
|
+
'ai-query': { path: 'v1/services/ai/query.v1.json' },
|
|
318
|
+
'database-query': { path: 'v1/services/database/query.v1.json' },
|
|
319
|
+
'location-query': { path: 'v1/services/location/query.v1.json' },
|
|
320
|
+
'logging-entry': { path: 'v1/services/logging/entry.v1.json' },
|
|
321
|
+
'observability-query': { path: 'v1/services/observability/query.v1.json' },
|
|
322
|
+
'observability-record': { path: 'v1/services/observability/record-metric.v1.json' },
|
|
323
|
+
'cache-query': { path: 'v1/services/cache/query.v1.json' }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Register explicit method-to-schema mappings
|
|
330
|
+
registerMethodSchemaMapping('observability', 'recordBusinessMetric', {
|
|
331
|
+
inputSchema: 'v1/services/observability/record-metric.v1.json',
|
|
332
|
+
outputSchema: 'v1/shared/ResponseMetadata.json'
|
|
333
|
+
});
|
|
334
|
+
registerMethodSchemaMapping('observability', 'recordMetric', {
|
|
335
|
+
inputSchema: 'v1/services/observability/record-metric.v1.json',
|
|
336
|
+
outputSchema: 'v1/shared/ResponseMetadata.json'
|
|
337
|
+
});
|
|
338
|
+
registerMethodSchemaMapping('observability', 'getMetrics', {
|
|
339
|
+
inputSchema: 'v1/services/observability/query.v1.json',
|
|
340
|
+
outputSchema: 'v1/shared/ResponseMetadata.json'
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
function preloadSchemas() {
|
|
344
|
+
const registry = getRegistry();
|
|
345
|
+
|
|
346
|
+
// Define schema dependencies to ensure proper loading order
|
|
347
|
+
const schemaDependencies = {
|
|
348
|
+
'v1/domains/astrology/inputs/vedic.v1.json': [
|
|
349
|
+
'v1/shared/PlatformContext.json',
|
|
350
|
+
'v1/shared/PlatformUserId.json',
|
|
351
|
+
'v1/domains/astrology/components/user-data.v1.json',
|
|
352
|
+
'v1/shared/preferences.v1.json',
|
|
353
|
+
'v1/domains/astrology/components/birthData.json'
|
|
354
|
+
],
|
|
355
|
+
// Add other dependencies as needed
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const schemasToLoad = [];
|
|
359
|
+
|
|
360
|
+
// Helper to collect schemas, ensuring the 'path' is used as the ID
|
|
361
|
+
const collectSchemas = (obj) => {
|
|
362
|
+
if (!obj || typeof obj !== 'object') return;
|
|
363
|
+
for (const key of Object.keys(obj)) {
|
|
364
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
365
|
+
if (obj[key].path) { // If this object directly has a path, it's a schema entry
|
|
366
|
+
schemasToLoad.push({ path: obj[key].path });
|
|
367
|
+
} else { // Otherwise, recurse deeper
|
|
368
|
+
collectSchemas(obj[key]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Initial calls to collectSchemas - load shared schemas first (referenced by others)
|
|
375
|
+
collectSchemas(registry.components); // This includes shared schemas
|
|
376
|
+
collectSchemas(registry.services); // Service schemas reference shared schemas
|
|
377
|
+
|
|
378
|
+
// Sort schemas to ensure dependencies are loaded first
|
|
379
|
+
const sortedSchemas = [];
|
|
380
|
+
const processedSchemas = new Set();
|
|
381
|
+
|
|
382
|
+
const addSchemaWithDependencies = (schemaPath) => {
|
|
383
|
+
if (processedSchemas.has(schemaPath)) return;
|
|
384
|
+
|
|
385
|
+
// Process dependencies first
|
|
386
|
+
const dependencies = schemaDependencies[schemaPath] || [];
|
|
387
|
+
for (const dep of dependencies) {
|
|
388
|
+
if (!processedSchemas.has(dep)) {
|
|
389
|
+
addSchemaWithDependencies(dep);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Then add the schema itself
|
|
394
|
+
sortedSchemas.push({ path: schemaPath });
|
|
395
|
+
processedSchemas.add(schemaPath);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Process all schemas with dependency resolution
|
|
399
|
+
for (const schemaData of schemasToLoad) {
|
|
400
|
+
addSchemaWithDependencies(schemaData.path);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const schemaData of sortedSchemas) {
|
|
404
|
+
const fullPath = path.join(__dirname, schemaData.path);
|
|
405
|
+
try {
|
|
406
|
+
let schema = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
407
|
+
|
|
408
|
+
// Ensure all schemas have an $id for proper reference resolution within AJV
|
|
409
|
+
if (!schema.$id) {
|
|
410
|
+
schema.$id = schemaData.path;
|
|
411
|
+
}
|
|
412
|
+
if (typeof schema.$id === 'string' && !schema.$id.startsWith('http://')) {
|
|
413
|
+
schema.$id = 'http://localhost/schemas/' + schema.$id;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Remove existing schema if it exists to ensure reload
|
|
417
|
+
if (ajv.getSchema(schema.$id)) {
|
|
418
|
+
ajv.removeSchema(schema.$id);
|
|
419
|
+
logger.info(`Removed existing schema ${schema.$id} from AJV for reload.`);
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
ajv.addSchema(schema, schema.$id);
|
|
423
|
+
|
|
424
|
+
} catch (e) {
|
|
425
|
+
logger.error(`Error adding schema ${schema.$id} to AJV: ${e.message}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
} catch (e) {
|
|
429
|
+
logger.error(`Error preloading schema ${schemaData.path}: ${e.message}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Call preloadSchemas once at the module level
|
|
435
|
+
preloadSchemas();
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Loads a schema by name (service or component) and version from the registry.
|
|
439
|
+
* This function handles recursive $ref resolution internally via AJV.
|
|
440
|
+
* @param {string} schemaName - The name of the schema (e.g., 'BirthChartService', 'astrology/vedic/input', 'v1/shared/PlatformContext').
|
|
441
|
+
* @param {string} [version='latest'] - The version of the service schema. Not applicable for components.
|
|
442
|
+
* @returns {object} The loaded and resolved JSON Schema object.
|
|
443
|
+
*/
|
|
444
|
+
function loadSchema(schemaName, version = 'latest') {
|
|
445
|
+
const cacheKey = `${schemaName}:${version}`;
|
|
446
|
+
// Try to get compiled validator from cache first
|
|
447
|
+
const cachedCompiledValidator = getCachedValidator(cacheKey);
|
|
448
|
+
if (cachedCompiledValidator) {
|
|
449
|
+
return cachedCompiledValidator.schema; // Return the raw schema object from the compiled validator
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let finalSchemaId = null;
|
|
453
|
+
|
|
454
|
+
const registry = getRegistry();
|
|
455
|
+
let schemaPath = null;
|
|
456
|
+
const schemaNameParts = schemaName.split('/');
|
|
457
|
+
|
|
458
|
+
// Try to find the schema in services and components based on the hierarchical path
|
|
459
|
+
const searchInRegistrySection = (section) => {
|
|
460
|
+
let current = section;
|
|
461
|
+
let found = true;
|
|
462
|
+
const normalizedSchemaNameParts = schemaName.replace(/([A-Z])/g, '-$1').toLowerCase().split('/'); // Normalize
|
|
463
|
+
for (const part of normalizedSchemaNameParts) { // Use normalized parts
|
|
464
|
+
if (current && current[part]) {
|
|
465
|
+
current = current[part];
|
|
466
|
+
} else {
|
|
467
|
+
found = false;
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (found && current) {
|
|
472
|
+
// Found the logical path, now determine the actual schema file path
|
|
473
|
+
let actualVersion = version;
|
|
474
|
+
if (version === 'latest' && current.latest) {
|
|
475
|
+
actualVersion = current.latest;
|
|
476
|
+
} else if (version === 'latest' && current.v1) { // Fallback to 'v1'
|
|
477
|
+
actualVersion = 'v1';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (current[actualVersion] && current[actualVersion].path) {
|
|
481
|
+
return current[actualVersion].path;
|
|
482
|
+
} else if (current.path) { // For components that might not be versioned
|
|
483
|
+
return current.path;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return null;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// For non-hierarchical schema names (like 'platform-context'), look in shared
|
|
491
|
+
if (schemaNameParts.length === 1) {
|
|
492
|
+
const sharedSchema = registry.components.shared[schemaName];
|
|
493
|
+
if (sharedSchema && sharedSchema.path) {
|
|
494
|
+
schemaPath = sharedSchema.path;
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
schemaPath = searchInRegistrySection(registry.services);
|
|
498
|
+
if (!schemaPath) {
|
|
499
|
+
schemaPath = searchInRegistrySection(registry.components);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (schemaPath) {
|
|
504
|
+
finalSchemaId = `http://localhost/schemas/${schemaPath}`;
|
|
505
|
+
} else if (schemaName.includes('/') && schemaName.endsWith('.json')) {
|
|
506
|
+
// Fallback: If schemaName looks like a direct file path and was not found in registry,
|
|
507
|
+
// assume it's a direct reference like 'v1/shared/error.v1.json'
|
|
508
|
+
finalSchemaId = `http://localhost/schemas/${schemaName}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!finalSchemaId) {
|
|
512
|
+
throw new Error(`Schema '${schemaName}' not found in registry and cannot construct its absolute ID.`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Retrieve the compiled validator from AJV. It should have been preloaded.
|
|
516
|
+
const compiledValidator = ajv.getSchema(finalSchemaId);
|
|
517
|
+
|
|
518
|
+
if (!compiledValidator) {
|
|
519
|
+
throw new Error(`Schema '${finalSchemaId}' was not found in AJV instance. This indicates a problem with preloadSchemas or an unregistered schema.`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Cache the compiled validator for future use
|
|
523
|
+
setCachedValidator(cacheKey, compiledValidator);
|
|
524
|
+
return compiledValidator.schema; // Return the raw schema object from the compiled validator
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Collects all allowed property names from a schema, resolving allOf and $ref.
|
|
530
|
+
* @param {object} schema - The schema object
|
|
531
|
+
* @returns {Set<string>} Set of allowed property names
|
|
532
|
+
*/
|
|
533
|
+
function collectAllowedProperties(schema) {
|
|
534
|
+
const allowedProps = new Set();
|
|
535
|
+
|
|
536
|
+
// Helper function to process a single schema
|
|
537
|
+
function processSchema(subSchema) {
|
|
538
|
+
if (subSchema.properties) {
|
|
539
|
+
Object.keys(subSchema.properties).forEach(prop => allowedProps.add(prop));
|
|
540
|
+
}
|
|
541
|
+
if (subSchema.required) {
|
|
542
|
+
subSchema.required.forEach(prop => allowedProps.add(prop));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Handle $ref
|
|
546
|
+
if (subSchema.$ref) {
|
|
547
|
+
const refSchema = ajv.getSchema(subSchema.$ref);
|
|
548
|
+
if (refSchema && refSchema.schema) {
|
|
549
|
+
processSchema(refSchema.schema);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Handle allOf
|
|
554
|
+
if (subSchema.allOf) {
|
|
555
|
+
subSchema.allOf.forEach(sub => processSchema(sub));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
processSchema(schema);
|
|
560
|
+
return allowedProps;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Validates input data against a specified schema.
|
|
565
|
+
* @param {string} schemaName - The name of the schema (service or component) to validate against.
|
|
566
|
+
* @param {object} input - The input data to validate.
|
|
567
|
+
* @param {string} [version='latest'] - The version of the schema.
|
|
568
|
+
* @returns {{isValid: boolean, errors?: object[], data?: object}} Validation result with processed data.
|
|
569
|
+
*/
|
|
570
|
+
function validateInput(schemaName, input, version = 'latest') {
|
|
571
|
+
try {
|
|
572
|
+
if (!input || typeof input !== 'object') {
|
|
573
|
+
return {
|
|
574
|
+
isValid: false,
|
|
575
|
+
errors: [{ message: 'Input must be a valid object' }],
|
|
576
|
+
data: input
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const schema = loadSchema(schemaName, version);
|
|
581
|
+
const cacheKey = `${schemaName}:${version}`;
|
|
582
|
+
let validate = getCachedValidator(cacheKey);
|
|
583
|
+
|
|
584
|
+
if (!validate) {
|
|
585
|
+
validate = ajv.compile(schema);
|
|
586
|
+
setCachedValidator(cacheKey, validate);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Create a fast deep copy to avoid modifying original input
|
|
590
|
+
const inputCopy = fastClone(input);
|
|
591
|
+
|
|
592
|
+
// AJV handles additional properties validation natively
|
|
593
|
+
|
|
594
|
+
// Additional strict checks
|
|
595
|
+
if (getStrictMode()) {
|
|
596
|
+
// Check for null/undefined values in required fields
|
|
597
|
+
if (schema.required) {
|
|
598
|
+
for (const requiredField of schema.required) {
|
|
599
|
+
if (inputCopy[requiredField] == null) {
|
|
600
|
+
return {
|
|
601
|
+
isValid: false,
|
|
602
|
+
errors: [{
|
|
603
|
+
field: `/${requiredField}`,
|
|
604
|
+
message: `Required property '${requiredField}' is null or undefined`,
|
|
605
|
+
keyword: 'required',
|
|
606
|
+
params: { missingProperty: requiredField }
|
|
607
|
+
}],
|
|
608
|
+
data: input
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Check for empty strings in string fields that shouldn't be empty
|
|
615
|
+
for (const [key, value] of Object.entries(inputCopy)) {
|
|
616
|
+
if (typeof value === 'string' && value.trim() === '' && schema.properties?.[key]?.type === 'string') {
|
|
617
|
+
return {
|
|
618
|
+
isValid: false,
|
|
619
|
+
errors: [{
|
|
620
|
+
field: `/${key}`,
|
|
621
|
+
message: `String property '${key}' cannot be empty`,
|
|
622
|
+
keyword: 'minLength',
|
|
623
|
+
params: { limit: 1 }
|
|
624
|
+
}],
|
|
625
|
+
data: input
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Validate with AJV
|
|
632
|
+
const isValid = validate(inputCopy);
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
isValid,
|
|
636
|
+
errors: isValid ? null : validate.errors.map(err => ({
|
|
637
|
+
field: err.instancePath || '/',
|
|
638
|
+
message: err.message,
|
|
639
|
+
keyword: err.keyword,
|
|
640
|
+
params: err.params
|
|
641
|
+
})),
|
|
642
|
+
data: inputCopy // Return processed data with defaults applied
|
|
643
|
+
};
|
|
644
|
+
} catch (error) {
|
|
645
|
+
return {
|
|
646
|
+
isValid: false,
|
|
647
|
+
errors: [{ field: '/', message: `Schema validation setup error: ${error.message}` }],
|
|
648
|
+
data: input
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Validates output data against a specified schema.
|
|
655
|
+
* More lenient than input validation - allows additional properties.
|
|
656
|
+
* @param {string} schemaName - The name of the schema to validate against.
|
|
657
|
+
* @param {object} output - The output data to validate.
|
|
658
|
+
* @param {string} [version='latest'] - The version of the schema.
|
|
659
|
+
* @returns {{isValid: boolean, errors?: object[]}} Validation result.
|
|
660
|
+
*/
|
|
661
|
+
function validateOutput(schemaName, output, version = 'latest') {
|
|
662
|
+
try {
|
|
663
|
+
if (!output || typeof output !== 'object') {
|
|
664
|
+
return {
|
|
665
|
+
isValid: false,
|
|
666
|
+
errors: [{ message: 'Output must be a valid object' }],
|
|
667
|
+
data: output
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Try to get the compiled validator from the main AJV instance
|
|
672
|
+
let validate = ajv.getSchema(schemaName);
|
|
673
|
+
|
|
674
|
+
if (!validate) {
|
|
675
|
+
// If not found, try loading and compiling the schema
|
|
676
|
+
const schema = loadSchema(schemaName, version);
|
|
677
|
+
const cacheKey = `${schemaName}:${version}:output`;
|
|
678
|
+
|
|
679
|
+
validate = getCachedValidator(cacheKey);
|
|
680
|
+
|
|
681
|
+
if (!validate) {
|
|
682
|
+
// Use the main AJV instance to compile (it has all schemas loaded)
|
|
683
|
+
try {
|
|
684
|
+
validate = ajv.compile(schema);
|
|
685
|
+
setCachedValidator(cacheKey, validate);
|
|
686
|
+
} catch (e) {
|
|
687
|
+
return {
|
|
688
|
+
isValid: false,
|
|
689
|
+
errors: [{ field: '/', message: `Schema compilation failed: ${e.message}` }],
|
|
690
|
+
data: output
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Create a fast deep copy to avoid modifying original output
|
|
697
|
+
const outputCopy = fastClone(output);
|
|
698
|
+
|
|
699
|
+
// Validate with strict AJV (rejects additional properties)
|
|
700
|
+
const isValid = validate(outputCopy);
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
isValid,
|
|
704
|
+
errors: isValid ? null : validate.errors.map(err => ({
|
|
705
|
+
field: err.instancePath || '/',
|
|
706
|
+
message: err.message,
|
|
707
|
+
keyword: err.keyword,
|
|
708
|
+
params: err.params
|
|
709
|
+
})),
|
|
710
|
+
data: outputCopy // Return processed data with defaults applied
|
|
711
|
+
};
|
|
712
|
+
} catch (error) {
|
|
713
|
+
return {
|
|
714
|
+
isValid: false,
|
|
715
|
+
errors: [{ field: '/', message: `Schema validation setup error: ${error.message}` }],
|
|
716
|
+
data: output
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Registers a new schema (service or component) in `schemaRegistry.json`.
|
|
723
|
+
* @param {string} schemaName - The name of the schema to register.
|
|
724
|
+
* @param {string} schemaPath - The path to the schema `.json` file, relative to `schemas/services` or `schemas/components` or `schemas/` itself.
|
|
725
|
+
* @param {string} [version='v1'] - The version of the schema (for services).
|
|
726
|
+
* @param {string} [type='service'] - 'service' or 'component' or 'top-level'.
|
|
727
|
+
* @param {boolean} [setAsLatest=true] - Whether to set this version as the latest for services.
|
|
728
|
+
*/
|
|
729
|
+
function registerSchema(schemaName, schemaPath, version = 'v1', type = 'service', setAsLatest = true) {
|
|
730
|
+
try {
|
|
731
|
+
const registryPath = path.join(__dirname, 'schemaRegistry.json');
|
|
732
|
+
const registry = getRegistry(); // Use getRegistry to ensure fresh data
|
|
733
|
+
|
|
734
|
+
if (type === 'service') {
|
|
735
|
+
if (!registry.services[schemaName]) {
|
|
736
|
+
registry.services[schemaName] = {};
|
|
737
|
+
}
|
|
738
|
+
registry.services[schemaName][version] = {
|
|
739
|
+
path: schemaPath,
|
|
740
|
+
status: 'active',
|
|
741
|
+
deprecated: false,
|
|
742
|
+
deprecationDate: null,
|
|
743
|
+
compatibility: []
|
|
744
|
+
};
|
|
745
|
+
if (setAsLatest) {
|
|
746
|
+
registry.services[schemaName].latest = version;
|
|
747
|
+
}
|
|
748
|
+
} else if (type === 'component') {
|
|
749
|
+
if (!registry.components[schemaName]) {
|
|
750
|
+
registry.components[schemaName] = {}; // Ensure components is an object
|
|
751
|
+
}
|
|
752
|
+
registry.components[schemaName] = {
|
|
753
|
+
path: schemaPath,
|
|
754
|
+
description: `Reusable component schema for '${schemaName}'`
|
|
755
|
+
};
|
|
756
|
+
} else if (type === 'top-level') { // For schemas like astrologyInputSchema.json
|
|
757
|
+
// For top-level schemas, path is directly in __dirname
|
|
758
|
+
registry.components[schemaName] = {
|
|
759
|
+
path: schemaPath,
|
|
760
|
+
description: `Top-level component schema for '${schemaName}'`
|
|
761
|
+
};
|
|
762
|
+
} else {
|
|
763
|
+
throw new Error(`Invalid schema type for registration: ${type}`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
registry.lastUpdated = new Date().toISOString().split('T')[0];
|
|
767
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
768
|
+
|
|
769
|
+
// Clear caches
|
|
770
|
+
schemaCache.clear();
|
|
771
|
+
validatorCache.clear();
|
|
772
|
+
|
|
773
|
+
logger.info(`Registered schema '${schemaName}' (type: ${type}, version: ${version}) at '${schemaPath}'.`);
|
|
774
|
+
|
|
775
|
+
} catch (error) {
|
|
776
|
+
throw new Error(`Failed to register schema '${schemaName}': ${error.message}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Clears all schema and validator caches.
|
|
782
|
+
*/
|
|
783
|
+
/**
|
|
784
|
+
* Clears all cached schemas and validators.
|
|
785
|
+
* Useful for development or when schemas are updated.
|
|
786
|
+
*/
|
|
787
|
+
function clearCache() {
|
|
788
|
+
schemaCache.clear();
|
|
789
|
+
validatorCache.clear();
|
|
790
|
+
schemaCacheTimestamps.clear();
|
|
791
|
+
validatorCacheTimestamps.clear();
|
|
792
|
+
|
|
793
|
+
// Remove all schemas from AJV to force reload
|
|
794
|
+
// Note: We can't easily enumerate all schemas in AJV, so we'll just reload
|
|
795
|
+
try {
|
|
796
|
+
// Try to remove the specific schema we know about
|
|
797
|
+
ajv.removeSchema('http://localhost/schemas/v1/domains/astrology/components/birthData.json');
|
|
798
|
+
} catch (error) {
|
|
799
|
+
// Ignore if schema doesn't exist
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Reload schemas into AJV
|
|
803
|
+
preloadSchemas();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Sets the strict validation mode.
|
|
808
|
+
* @param {boolean} enabled - Whether to enable strict validation
|
|
809
|
+
*/
|
|
810
|
+
function setStrictMode(enabled) {
|
|
811
|
+
// Note: This is a global setting that affects all validations
|
|
812
|
+
// In a production system, you might want per-request strictness
|
|
813
|
+
process.env.SCHEMA_STRICT_MODE = enabled.toString();
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Gets the current strict validation mode.
|
|
818
|
+
* @returns {boolean} Whether strict mode is enabled
|
|
819
|
+
*/
|
|
820
|
+
function getStrictMode() {
|
|
821
|
+
return process.env.SCHEMA_STRICT_MODE !== 'false';
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Validates a JSON Schema itself for correctness.
|
|
826
|
+
* @param {object} schema - The JSON Schema to validate.
|
|
827
|
+
* @returns {{isValid: boolean, errors?: string[]}} Schema validation result.
|
|
828
|
+
*/
|
|
829
|
+
function validateSchema(schema) {
|
|
830
|
+
try {
|
|
831
|
+
// Basic schema structure validation
|
|
832
|
+
if (!schema || typeof schema !== 'object') {
|
|
833
|
+
return { isValid: false, errors: ['Schema must be a valid object'] };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!schema.$schema && !schema.type) {
|
|
837
|
+
return { isValid: false, errors: ['Schema must have $schema or type property'] };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Try to compile with AJV to check for structural issues
|
|
841
|
+
ajv.compile(schema);
|
|
842
|
+
return { isValid: true };
|
|
843
|
+
} catch (error) {
|
|
844
|
+
return { isValid: false, errors: [error.message] };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Service-to-schema category mapping, storing registered service schema associations
|
|
849
|
+
const serviceSchemaMap = new Map();
|
|
850
|
+
|
|
851
|
+
// Register a service with its schema category
|
|
852
|
+
const registerServiceSchema = (serviceName, schemaCategory) => {
|
|
853
|
+
serviceSchemaMap.set(serviceName, schemaCategory);
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// Schema category to full schema name mapping
|
|
857
|
+
const schemaCategoryToSchemaName = {
|
|
858
|
+
'vedic': 'v1/domains/astrology/inputs/vedic.v1.json',
|
|
859
|
+
'compatibility': 'v1/domains/astrology/inputs/compatibility.v1.json',
|
|
860
|
+
'location': 'v1/services/location/query.v1.json',
|
|
861
|
+
'ai': 'v1/services/ai/query.v1.json',
|
|
862
|
+
'database': 'v1/services/database/query.v1.json',
|
|
863
|
+
'logging': 'v1/services/logging/entry.v1.json',
|
|
864
|
+
'observability': 'v1/services/observability/query.v1.json',
|
|
865
|
+
'session': 'v1/domains/astrology/components/session-data.v1.json',
|
|
866
|
+
'default': 'v1/domains/astrology/inputs/vedic.v1.json'
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// Service name to schema category mapping as fallback
|
|
870
|
+
const serviceNameToSchemaCategory = {
|
|
871
|
+
'birthchart': 'vedic',
|
|
872
|
+
'dasha': 'vedic',
|
|
873
|
+
'ephemeris': 'vedic',
|
|
874
|
+
'astrologer': 'vedic',
|
|
875
|
+
'location': 'location',
|
|
876
|
+
'ai': 'ai',
|
|
877
|
+
'mongodb': 'database',
|
|
878
|
+
'logging': 'logging',
|
|
879
|
+
'observability': 'observability',
|
|
880
|
+
'sessionmanager': 'session'
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// Create a service-specific validation function that maps service names to schemas
|
|
884
|
+
const validateServiceInput = (serviceName, request) => {
|
|
885
|
+
// First check if service has a registered schema category
|
|
886
|
+
const registeredSchemaCategory = serviceSchemaMap.get(serviceName);
|
|
887
|
+
const schemaCategory = registeredSchemaCategory ||
|
|
888
|
+
serviceNameToSchemaCategory[serviceName] ||
|
|
889
|
+
'default';
|
|
890
|
+
|
|
891
|
+
const schemaName = schemaCategoryToSchemaName[schemaCategory] ||
|
|
892
|
+
schemaCategoryToSchemaName['default'];
|
|
893
|
+
|
|
894
|
+
// Create a fast deep copy of the request to avoid modifying the original object during validation
|
|
895
|
+
// This prevents AJV from modifying the original request when applying defaults
|
|
896
|
+
const requestCopy = fastClone(request);
|
|
897
|
+
|
|
898
|
+
return validateInput(schemaName, requestCopy);
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// Create service input schema getter function
|
|
902
|
+
const getServiceInputSchema = (serviceName) => {
|
|
903
|
+
// First check if service has a registered schema category
|
|
904
|
+
const registeredSchemaCategory = serviceSchemaMap.get(serviceName);
|
|
905
|
+
const schemaCategory = registeredSchemaCategory ||
|
|
906
|
+
serviceNameToSchemaCategory[serviceName] ||
|
|
907
|
+
'default';
|
|
908
|
+
|
|
909
|
+
const schemaName = schemaCategoryToSchemaName[schemaCategory] ||
|
|
910
|
+
schemaCategoryToSchemaName['default'];
|
|
911
|
+
|
|
912
|
+
return loadSchema(schemaName);
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// Export the unified AJV schema management API
|
|
916
|
+
module.exports = {
|
|
917
|
+
loadSchema,
|
|
918
|
+
validateInput,
|
|
919
|
+
validateOutput,
|
|
920
|
+
validateSchema, // New: Validate schema structure itself
|
|
921
|
+
registerSchema, // Renamed from registerServiceSchema for broader use
|
|
922
|
+
getRegistry,
|
|
923
|
+
clearCache,
|
|
924
|
+
setStrictMode, // New: Control strict validation mode
|
|
925
|
+
getStrictMode, // New: Check strict validation mode
|
|
926
|
+
validateServiceInput, // Added: Service-specific validation function
|
|
927
|
+
getServiceInputSchema, // Added: Service-specific schema getter
|
|
928
|
+
registerServiceSchema, // Added: Function to register service-to-schema mappings
|
|
929
|
+
// Enhanced schema registry functions
|
|
930
|
+
registerMethodSchemaMapping,
|
|
931
|
+
getMethodSchemaMapping,
|
|
932
|
+
getValidationSchemas,
|
|
933
|
+
registerSchemaVersion,
|
|
934
|
+
getSchemaVersion,
|
|
935
|
+
getSchemaVersionHistory,
|
|
936
|
+
clearSchemaCache: () => {
|
|
937
|
+
schemaCache.clear();
|
|
938
|
+
schemaCacheTimestamps.clear();
|
|
939
|
+
},
|
|
940
|
+
};
|