@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.
@@ -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
+ };