@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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const { validateBirthData, createValidationError } = require('./index');
|
|
2
|
+
|
|
3
|
+
const { getLogger } = require('../logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base Validator class for astrology services
|
|
7
|
+
* Provides common validation functionality and patterns
|
|
8
|
+
*/
|
|
9
|
+
class BaseValidator {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.logger = getLogger('BaseValidator');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate required fields
|
|
20
|
+
* @param {Object} data - Data object
|
|
21
|
+
* @param {Array<string>} requiredFields - Required field names
|
|
22
|
+
* @returns {Object} Validation result
|
|
23
|
+
*/
|
|
24
|
+
validateRequired(data, requiredFields) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
|
|
27
|
+
for (const field of requiredFields) {
|
|
28
|
+
if (!data || !(field in data) || data[field] === null || data[field] === undefined) {
|
|
29
|
+
errors.push({
|
|
30
|
+
field,
|
|
31
|
+
message: `${field} is required`,
|
|
32
|
+
code: 'MISSING_REQUIRED_FIELD'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
isValid: errors.length === 0,
|
|
39
|
+
errors
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate string fields with length constraints
|
|
45
|
+
* @param {Object} data - Data object
|
|
46
|
+
* @param {Object} fieldRules - Field validation rules {fieldName: {minLength, maxLength, pattern}}
|
|
47
|
+
* @returns {Object} Validation result
|
|
48
|
+
*/
|
|
49
|
+
validateStringFields(data, fieldRules) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
|
|
52
|
+
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
53
|
+
const value = data[field];
|
|
54
|
+
|
|
55
|
+
if (value !== undefined && value !== null) {
|
|
56
|
+
if (typeof value !== 'string') {
|
|
57
|
+
errors.push({
|
|
58
|
+
field,
|
|
59
|
+
message: `${field} must be a string`,
|
|
60
|
+
code: 'INVALID_TYPE'
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (rules.minLength && value.length < rules.minLength) {
|
|
66
|
+
errors.push({
|
|
67
|
+
field,
|
|
68
|
+
message: `${field} must be at least ${rules.minLength} characters`,
|
|
69
|
+
code: 'TOO_SHORT'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (rules.maxLength && value.length > rules.maxLength) {
|
|
74
|
+
errors.push({
|
|
75
|
+
field,
|
|
76
|
+
message: `${field} must be at most ${rules.maxLength} characters`,
|
|
77
|
+
code: 'TOO_LONG'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (rules.pattern && !rules.pattern.test(value)) {
|
|
82
|
+
errors.push({
|
|
83
|
+
field,
|
|
84
|
+
message: `${field} format is invalid`,
|
|
85
|
+
code: 'INVALID_FORMAT'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
isValid: errors.length === 0,
|
|
93
|
+
errors
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate numeric ranges
|
|
99
|
+
* @param {Object} data - Data object
|
|
100
|
+
* @param {Object} fieldRules - Field validation rules {fieldName: {min, max, integer}}
|
|
101
|
+
* @returns {Object} Validation result
|
|
102
|
+
*/
|
|
103
|
+
validateNumericFields(data, fieldRules) {
|
|
104
|
+
const errors = [];
|
|
105
|
+
|
|
106
|
+
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
107
|
+
const value = data[field];
|
|
108
|
+
|
|
109
|
+
if (value !== undefined && value !== null) {
|
|
110
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
111
|
+
errors.push({
|
|
112
|
+
field,
|
|
113
|
+
message: `${field} must be a valid number`,
|
|
114
|
+
code: 'INVALID_NUMBER'
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (rules.integer && !Number.isInteger(value)) {
|
|
120
|
+
errors.push({
|
|
121
|
+
field,
|
|
122
|
+
message: `${field} must be an integer`,
|
|
123
|
+
code: 'NOT_INTEGER'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (rules.min !== undefined && value < rules.min) {
|
|
128
|
+
errors.push({
|
|
129
|
+
field,
|
|
130
|
+
message: `${field} must be at least ${rules.min}`,
|
|
131
|
+
code: 'TOO_SMALL'
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (rules.max !== undefined && value > rules.max) {
|
|
136
|
+
errors.push({
|
|
137
|
+
field,
|
|
138
|
+
message: `${field} must be at most ${rules.max}`,
|
|
139
|
+
code: 'TOO_LARGE'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
isValid: errors.length === 0,
|
|
147
|
+
errors
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate array fields
|
|
153
|
+
* @param {Object} data - Data object
|
|
154
|
+
* @param {Object} fieldRules - Field validation rules {fieldName: {minItems, maxItems, itemValidator}}
|
|
155
|
+
* @returns {Object} Validation result
|
|
156
|
+
*/
|
|
157
|
+
validateArrayFields(data, fieldRules) {
|
|
158
|
+
const errors = [];
|
|
159
|
+
|
|
160
|
+
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
161
|
+
const value = data[field];
|
|
162
|
+
|
|
163
|
+
if (value !== undefined && value !== null) {
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
errors.push({
|
|
166
|
+
field,
|
|
167
|
+
message: `${field} must be an array`,
|
|
168
|
+
code: 'NOT_ARRAY'
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (rules.minItems && value.length < rules.minItems) {
|
|
174
|
+
errors.push({
|
|
175
|
+
field,
|
|
176
|
+
message: `${field} must have at least ${rules.minItems} items`,
|
|
177
|
+
code: 'TOO_FEW_ITEMS'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (rules.maxItems && value.length > rules.maxItems) {
|
|
182
|
+
errors.push({
|
|
183
|
+
field,
|
|
184
|
+
message: `${field} must have at most ${rules.maxItems} items`,
|
|
185
|
+
code: 'TOO_MANY_ITEMS'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validate individual items if validator provided
|
|
190
|
+
if (rules.itemValidator && typeof rules.itemValidator === 'function') {
|
|
191
|
+
value.forEach((item, index) => {
|
|
192
|
+
try {
|
|
193
|
+
const itemResult = rules.itemValidator(item);
|
|
194
|
+
if (!itemResult.isValid) {
|
|
195
|
+
errors.push(...itemResult.errors.map(error => ({
|
|
196
|
+
...error,
|
|
197
|
+
field: `${field}[${index}].${error.field}`
|
|
198
|
+
})));
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
errors.push({
|
|
202
|
+
field: `${field}[${index}]`,
|
|
203
|
+
message: error.message,
|
|
204
|
+
code: 'ITEM_VALIDATION_ERROR'
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
isValid: errors.length === 0,
|
|
214
|
+
errors
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Validate coordinates (latitude/longitude)
|
|
220
|
+
* @param {Object} coordinates - Coordinates object {latitude, longitude}
|
|
221
|
+
* @returns {Object} Validation result
|
|
222
|
+
*/
|
|
223
|
+
validateCoordinates(coordinates) {
|
|
224
|
+
const errors = [];
|
|
225
|
+
|
|
226
|
+
if (!coordinates || typeof coordinates !== 'object') {
|
|
227
|
+
return {
|
|
228
|
+
isValid: false,
|
|
229
|
+
errors: [{
|
|
230
|
+
field: 'coordinates',
|
|
231
|
+
message: 'Coordinates object is required',
|
|
232
|
+
code: 'MISSING_COORDINATES'
|
|
233
|
+
}]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { latitude, longitude } = coordinates;
|
|
238
|
+
|
|
239
|
+
if (latitude === undefined || latitude === null) {
|
|
240
|
+
errors.push({
|
|
241
|
+
field: 'coordinates.latitude',
|
|
242
|
+
message: 'Latitude is required',
|
|
243
|
+
code: 'MISSING_LATITUDE'
|
|
244
|
+
});
|
|
245
|
+
} else if (typeof latitude !== 'number' || isNaN(latitude)) {
|
|
246
|
+
errors.push({
|
|
247
|
+
field: 'coordinates.latitude',
|
|
248
|
+
message: 'Latitude must be a valid number',
|
|
249
|
+
code: 'INVALID_LATITUDE'
|
|
250
|
+
});
|
|
251
|
+
} else if (latitude < -90 || latitude > 90) {
|
|
252
|
+
errors.push({
|
|
253
|
+
field: 'coordinates.latitude',
|
|
254
|
+
message: 'Latitude must be between -90 and 90',
|
|
255
|
+
code: 'LATITUDE_OUT_OF_RANGE'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (longitude === undefined || longitude === null) {
|
|
260
|
+
errors.push({
|
|
261
|
+
field: 'coordinates.longitude',
|
|
262
|
+
message: 'Longitude is required',
|
|
263
|
+
code: 'MISSING_LONGITUDE'
|
|
264
|
+
});
|
|
265
|
+
} else if (typeof longitude !== 'number' || isNaN(longitude)) {
|
|
266
|
+
errors.push({
|
|
267
|
+
field: 'coordinates.longitude',
|
|
268
|
+
message: 'Longitude must be a valid number',
|
|
269
|
+
code: 'INVALID_LONGITUDE'
|
|
270
|
+
});
|
|
271
|
+
} else if (longitude < -180 || longitude > 180) {
|
|
272
|
+
errors.push({
|
|
273
|
+
field: 'coordinates.longitude',
|
|
274
|
+
message: 'Longitude must be between -180 and 180',
|
|
275
|
+
code: 'LONGITUDE_OUT_OF_RANGE'
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
isValid: errors.length === 0,
|
|
281
|
+
errors
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Combine multiple validation results
|
|
289
|
+
* @param {Array<Object>} validationResults - Array of validation result objects
|
|
290
|
+
* @returns {Object} Combined validation result
|
|
291
|
+
*/
|
|
292
|
+
combineValidations(validationResults) {
|
|
293
|
+
const allErrors = [];
|
|
294
|
+
|
|
295
|
+
for (const result of validationResults) {
|
|
296
|
+
if (!result.isValid && result.errors) {
|
|
297
|
+
allErrors.push(...result.errors);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
isValid: allErrors.length === 0,
|
|
303
|
+
errors: allErrors
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = BaseValidator;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// This file is now refactored to use the AJV/JSON Schema system.
|
|
2
|
+
|
|
3
|
+
const { isValid, parse } = require('date-fns'); // Keep date-fns for now if AJV patterns are not strict enough for parsing.
|
|
4
|
+
const { validateInput, loadSchema } = require('../schemas'); // Import validateInput and loadSchema from central schema management
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate birth data with strict DD/MM/YYYY format using AJV.
|
|
8
|
+
* @param {Object} data - Input data to validate
|
|
9
|
+
* @returns {Object} Validated and sanitized data
|
|
10
|
+
* @throws {Error} If validation fails
|
|
11
|
+
*/
|
|
12
|
+
function validateBirthData(data) {
|
|
13
|
+
// Use the birthData component schema for validating just the birthData object
|
|
14
|
+
const schemaName = 'astrology/birthData';
|
|
15
|
+
|
|
16
|
+
const result = validateInput(schemaName, data); // Validate birthData directly
|
|
17
|
+
|
|
18
|
+
if (!result.isValid) {
|
|
19
|
+
const errorDetails = result.errors.map(d => d.message).join(', ');
|
|
20
|
+
throw createValidationError('VALIDATION_ERROR', `Invalid birth data: ${errorDetails}`, result.errors);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If validation is successful, return the validated data (which may have defaults applied by AJV)
|
|
24
|
+
return result.data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a standardized validation error
|
|
29
|
+
* @param {string} code - Error code
|
|
30
|
+
* @param {string} message - Error message
|
|
31
|
+
* @param {Object} details - Additional error details
|
|
32
|
+
* @returns {Error} Validation error
|
|
33
|
+
*/
|
|
34
|
+
function createValidationError(code, message, details = {}) {
|
|
35
|
+
const error = new Error(message);
|
|
36
|
+
error.code = code;
|
|
37
|
+
error.details = details;
|
|
38
|
+
error.type = 'VALIDATION_ERROR';
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate client context for multi-frontend support using AJV.
|
|
44
|
+
* @param {Object} context - Client context
|
|
45
|
+
* @returns {Object} Validated context
|
|
46
|
+
*/
|
|
47
|
+
function validateClientContext(context) {
|
|
48
|
+
if (!context) return {};
|
|
49
|
+
|
|
50
|
+
const result = validateInput('client-context', context); // Validate against client-context schema
|
|
51
|
+
|
|
52
|
+
if (!result.isValid) {
|
|
53
|
+
const errorDetails = result.errors.map(d => d.message).join(', ');
|
|
54
|
+
throw createValidationError('INVALID_CLIENT_CONTEXT', `Invalid client context: ${errorDetails}`, result.errors);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If validated, AJV might have applied defaults/coerced types.
|
|
58
|
+
// We still need to validate preferences separately if they are nested.
|
|
59
|
+
if (result.data.preferences) {
|
|
60
|
+
result.data.preferences = validatePreferences(result.data.preferences);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result.data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate user preferences using AJV.
|
|
68
|
+
* @param {Object} preferences - User preferences
|
|
69
|
+
* @returns {Object} Validated preferences
|
|
70
|
+
*/
|
|
71
|
+
function validatePreferences(preferences) {
|
|
72
|
+
const result = validateInput('preferences', preferences); // Validate against preferences schema
|
|
73
|
+
|
|
74
|
+
if (!result.isValid) {
|
|
75
|
+
const errorDetails = result.errors.map(d => d.message).join(', ');
|
|
76
|
+
throw createValidationError('INVALID_PREFERENCES', `Invalid user preferences: ${errorDetails}`, result.errors);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If validated, AJV might have applied defaults/coerced types.
|
|
80
|
+
return result.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
validateBirthData,
|
|
85
|
+
validateClientContext,
|
|
86
|
+
validatePreferences,
|
|
87
|
+
createValidationError
|
|
88
|
+
};
|