@sschepis/magazine 0.1.0

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,506 @@
1
+ /**
2
+ * ConfigSchema - Configuration schema definition and validation
3
+ */
4
+ export default class ConfigSchema {
5
+ constructor() {
6
+ // Define the configuration schema
7
+ this.schema = {
8
+ // Required fields
9
+ required: ['name', 'address', 'abi', 'provider'],
10
+
11
+ // Field definitions
12
+ fields: {
13
+ name: {
14
+ type: 'string',
15
+ description: 'Name of the magazine',
16
+ minLength: 1,
17
+ maxLength: 100,
18
+ pattern: /^[a-zA-Z0-9\s\-_]+$/,
19
+ example: 'My Event Magazine'
20
+ },
21
+
22
+ address: {
23
+ type: 'string',
24
+ description: 'Contract address',
25
+ pattern: /^0x[a-fA-F0-9]{40}$/,
26
+ example: '0x1234567890123456789012345678901234567890'
27
+ },
28
+
29
+ abi: {
30
+ type: 'array',
31
+ description: 'Contract ABI',
32
+ minItems: 1,
33
+ items: {
34
+ type: 'object',
35
+ required: ['type'],
36
+ properties: {
37
+ type: {
38
+ type: 'string',
39
+ enum: ['function', 'event', 'constructor', 'fallback', 'receive']
40
+ },
41
+ name: { type: 'string' },
42
+ inputs: { type: 'array' },
43
+ outputs: { type: 'array' },
44
+ anonymous: { type: 'boolean' }
45
+ }
46
+ }
47
+ },
48
+
49
+ provider: {
50
+ type: ['string', 'object'],
51
+ description: 'Ethereum provider URL or object',
52
+ validate: (value) => {
53
+ if (typeof value === 'string') {
54
+ return value.startsWith('http://') ||
55
+ value.startsWith('https://') ||
56
+ value.startsWith('ws://') ||
57
+ value.startsWith('wss://');
58
+ }
59
+ return typeof value === 'object' && value !== null;
60
+ },
61
+ example: 'http://localhost:8545'
62
+ },
63
+
64
+ networkId: {
65
+ type: 'number',
66
+ description: 'Network ID',
67
+ optional: true,
68
+ min: 1,
69
+ example: 1
70
+ },
71
+
72
+ gun: {
73
+ type: 'object',
74
+ description: 'Gun.js configuration',
75
+ optional: true,
76
+ properties: {
77
+ peers: {
78
+ type: 'array',
79
+ items: { type: 'string' },
80
+ example: ['http://localhost:8765/gun']
81
+ },
82
+ file: {
83
+ type: 'string',
84
+ example: 'radata'
85
+ },
86
+ localStorage: {
87
+ type: 'boolean',
88
+ default: true
89
+ },
90
+ radisk: {
91
+ type: 'boolean',
92
+ default: true
93
+ },
94
+ multicast: {
95
+ type: ['boolean', 'object'],
96
+ default: false
97
+ },
98
+ compression: {
99
+ type: 'object',
100
+ properties: {
101
+ enabled: {
102
+ type: 'boolean',
103
+ default: true
104
+ },
105
+ strategy: {
106
+ type: 'string',
107
+ enum: ['none', 'json', 'gzip', 'optimized'],
108
+ default: 'optimized'
109
+ }
110
+ }
111
+ }
112
+ }
113
+ },
114
+
115
+ logLevel: {
116
+ type: 'string',
117
+ description: 'Logging level',
118
+ optional: true,
119
+ enum: ['error', 'warn', 'info', 'debug', 'trace'],
120
+ default: 'info'
121
+ },
122
+
123
+ maxRetries: {
124
+ type: 'number',
125
+ description: 'Maximum retry attempts',
126
+ optional: true,
127
+ min: 0,
128
+ max: 10,
129
+ default: 3
130
+ },
131
+
132
+ retryDelay: {
133
+ type: 'number',
134
+ description: 'Delay between retries in milliseconds',
135
+ optional: true,
136
+ min: 100,
137
+ max: 60000,
138
+ default: 1000
139
+ },
140
+
141
+ timeout: {
142
+ type: 'number',
143
+ description: 'Operation timeout in milliseconds',
144
+ optional: true,
145
+ min: 1000,
146
+ max: 300000,
147
+ default: 30000
148
+ },
149
+
150
+ batchSize: {
151
+ type: 'number',
152
+ description: 'Event sync batch size',
153
+ optional: true,
154
+ min: 1,
155
+ max: 10000,
156
+ default: 1000
157
+ },
158
+
159
+ startBlockNumber: {
160
+ type: 'number',
161
+ description: 'Starting block number for sync',
162
+ optional: true,
163
+ min: 0,
164
+ default: 1
165
+ },
166
+
167
+ autoSync: {
168
+ type: 'boolean',
169
+ description: 'Enable automatic syncing',
170
+ optional: true,
171
+ default: false
172
+ },
173
+
174
+ syncInterval: {
175
+ type: 'number',
176
+ description: 'Auto-sync interval in milliseconds',
177
+ optional: true,
178
+ min: 10000,
179
+ max: 3600000,
180
+ default: 60000,
181
+ dependsOn: 'autoSync'
182
+ }
183
+ }
184
+ };
185
+
186
+ // Validation error messages
187
+ this.errorMessages = {
188
+ required: (field) => `${field} is required`,
189
+ type: (field, expected, actual) => `${field} must be of type ${expected}, got ${actual}`,
190
+ minLength: (field, min) => `${field} must be at least ${min} characters long`,
191
+ maxLength: (field, max) => `${field} must be at most ${max} characters long`,
192
+ pattern: (field) => `${field} has invalid format`,
193
+ min: (field, min) => `${field} must be at least ${min}`,
194
+ max: (field, max) => `${field} must be at most ${max}`,
195
+ enum: (field, values) => `${field} must be one of: ${values.join(', ')}`,
196
+ minItems: (field, min) => `${field} must have at least ${min} items`,
197
+ custom: (field, message) => `${field}: ${message}`
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Validate configuration against schema
203
+ * @param {Object} config - Configuration to validate
204
+ * @returns {Object} Validation result
205
+ */
206
+ validate(config) {
207
+ const errors = [];
208
+ const warnings = [];
209
+ const validated = {};
210
+
211
+ // Check required fields
212
+ for (const field of this.schema.required) {
213
+ if (!config.hasOwnProperty(field) || config[field] === undefined || config[field] === null) {
214
+ errors.push({
215
+ field,
216
+ type: 'required',
217
+ message: this.errorMessages.required(field)
218
+ });
219
+ }
220
+ }
221
+
222
+ // Validate each field
223
+ for (const [field, definition] of Object.entries(this.schema.fields)) {
224
+ if (config.hasOwnProperty(field)) {
225
+ const result = this._validateField(field, config[field], definition);
226
+
227
+ if (result.errors.length > 0) {
228
+ errors.push(...result.errors);
229
+ } else {
230
+ validated[field] = result.value;
231
+ }
232
+
233
+ if (result.warnings.length > 0) {
234
+ warnings.push(...result.warnings);
235
+ }
236
+ } else if (!definition.optional && !this.schema.required.includes(field)) {
237
+ // Field is not required but has a default value
238
+ if (definition.hasOwnProperty('default')) {
239
+ validated[field] = definition.default;
240
+ }
241
+ }
242
+ }
243
+
244
+ // Check for unknown fields
245
+ for (const field of Object.keys(config)) {
246
+ if (!this.schema.fields.hasOwnProperty(field)) {
247
+ warnings.push({
248
+ field,
249
+ type: 'unknown',
250
+ message: `Unknown configuration field: ${field}`
251
+ });
252
+ // Include unknown fields in validated config
253
+ validated[field] = config[field];
254
+ }
255
+ }
256
+
257
+ // Check field dependencies
258
+ for (const [field, definition] of Object.entries(this.schema.fields)) {
259
+ if (definition.dependsOn && validated[field] !== undefined) {
260
+ if (!validated[definition.dependsOn]) {
261
+ warnings.push({
262
+ field,
263
+ type: 'dependency',
264
+ message: `${field} depends on ${definition.dependsOn} being set`
265
+ });
266
+ }
267
+ }
268
+ }
269
+
270
+ return {
271
+ valid: errors.length === 0,
272
+ errors,
273
+ warnings,
274
+ validated
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Validate a single field
280
+ * @private
281
+ */
282
+ _validateField(field, value, definition) {
283
+ const errors = [];
284
+ const warnings = [];
285
+ let validatedValue = value;
286
+
287
+ // Type validation
288
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
289
+ const expectedTypes = Array.isArray(definition.type) ? definition.type : [definition.type];
290
+
291
+ if (!expectedTypes.includes(actualType)) {
292
+ errors.push({
293
+ field,
294
+ type: 'type',
295
+ message: this.errorMessages.type(field, expectedTypes.join(' or '), actualType)
296
+ });
297
+ return { errors, warnings, value };
298
+ }
299
+
300
+ // Apply transformation if defined
301
+ if (definition.transform && typeof definition.transform === 'function') {
302
+ validatedValue = definition.transform(validatedValue);
303
+ }
304
+
305
+ // String validations
306
+ if (actualType === 'string') {
307
+ if (definition.minLength && validatedValue.length < definition.minLength) {
308
+ errors.push({
309
+ field,
310
+ type: 'minLength',
311
+ message: this.errorMessages.minLength(field, definition.minLength)
312
+ });
313
+ }
314
+
315
+ if (definition.maxLength && validatedValue.length > definition.maxLength) {
316
+ errors.push({
317
+ field,
318
+ type: 'maxLength',
319
+ message: this.errorMessages.maxLength(field, definition.maxLength)
320
+ });
321
+ }
322
+
323
+ if (definition.pattern && !definition.pattern.test(validatedValue)) {
324
+ errors.push({
325
+ field,
326
+ type: 'pattern',
327
+ message: this.errorMessages.pattern(field)
328
+ });
329
+ }
330
+
331
+ if (definition.enum && !definition.enum.includes(validatedValue)) {
332
+ errors.push({
333
+ field,
334
+ type: 'enum',
335
+ message: this.errorMessages.enum(field, definition.enum)
336
+ });
337
+ }
338
+ }
339
+
340
+ // Number validations
341
+ if (actualType === 'number') {
342
+ if (definition.min !== undefined && validatedValue < definition.min) {
343
+ errors.push({
344
+ field,
345
+ type: 'min',
346
+ message: this.errorMessages.min(field, definition.min)
347
+ });
348
+ }
349
+
350
+ if (definition.max !== undefined && validatedValue > definition.max) {
351
+ errors.push({
352
+ field,
353
+ type: 'max',
354
+ message: this.errorMessages.max(field, definition.max)
355
+ });
356
+ }
357
+ }
358
+
359
+ // Array validations
360
+ if (actualType === 'array') {
361
+ if (definition.minItems && validatedValue.length < definition.minItems) {
362
+ errors.push({
363
+ field,
364
+ type: 'minItems',
365
+ message: this.errorMessages.minItems(field, definition.minItems)
366
+ });
367
+ }
368
+
369
+ // Validate array items if schema is provided
370
+ if (definition.items) {
371
+ validatedValue.forEach((item, index) => {
372
+ const itemResult = this._validateField(
373
+ `${field}[${index}]`,
374
+ item,
375
+ definition.items
376
+ );
377
+ errors.push(...itemResult.errors);
378
+ warnings.push(...itemResult.warnings);
379
+ });
380
+ }
381
+ }
382
+
383
+ // Object validations
384
+ if (actualType === 'object' && definition.properties) {
385
+ for (const [prop, propDef] of Object.entries(definition.properties)) {
386
+ if (validatedValue.hasOwnProperty(prop)) {
387
+ const propResult = this._validateField(
388
+ `${field}.${prop}`,
389
+ validatedValue[prop],
390
+ propDef
391
+ );
392
+ errors.push(...propResult.errors);
393
+ warnings.push(...propResult.warnings);
394
+ }
395
+ }
396
+ }
397
+
398
+ // Custom validation
399
+ if (definition.validate && typeof definition.validate === 'function') {
400
+ const isValid = definition.validate(validatedValue);
401
+ if (!isValid) {
402
+ errors.push({
403
+ field,
404
+ type: 'custom',
405
+ message: this.errorMessages.custom(field, 'Custom validation failed')
406
+ });
407
+ }
408
+ }
409
+
410
+ return { errors, warnings, value: validatedValue };
411
+ }
412
+
413
+ /**
414
+ * Get schema documentation
415
+ * @returns {Object} Schema documentation
416
+ */
417
+ getDocumentation() {
418
+ const docs = {
419
+ description: 'Magazine configuration schema',
420
+ required: this.schema.required,
421
+ fields: {}
422
+ };
423
+
424
+ for (const [field, definition] of Object.entries(this.schema.fields)) {
425
+ docs.fields[field] = {
426
+ type: definition.type,
427
+ description: definition.description,
428
+ required: this.schema.required.includes(field),
429
+ optional: definition.optional,
430
+ default: definition.default,
431
+ example: definition.example
432
+ };
433
+
434
+ if (definition.enum) {
435
+ docs.fields[field].enum = definition.enum;
436
+ }
437
+
438
+ if (definition.min !== undefined || definition.max !== undefined) {
439
+ docs.fields[field].range = {
440
+ min: definition.min,
441
+ max: definition.max
442
+ };
443
+ }
444
+
445
+ if (definition.pattern) {
446
+ docs.fields[field].pattern = definition.pattern.toString();
447
+ }
448
+
449
+ if (definition.dependsOn) {
450
+ docs.fields[field].dependsOn = definition.dependsOn;
451
+ }
452
+ }
453
+
454
+ return docs;
455
+ }
456
+
457
+ /**
458
+ * Generate example configuration
459
+ * @param {Object} options - Generation options
460
+ * @returns {Object} Example configuration
461
+ */
462
+ generateExample(options = {}) {
463
+ const { includeOptional = true, useDefaults = true } = options;
464
+ const example = {};
465
+
466
+ for (const [field, definition] of Object.entries(this.schema.fields)) {
467
+ if (this.schema.required.includes(field) || (includeOptional && definition.optional)) {
468
+ if (definition.example !== undefined) {
469
+ example[field] = definition.example;
470
+ } else if (useDefaults && definition.default !== undefined) {
471
+ example[field] = definition.default;
472
+ } else {
473
+ // Generate a placeholder based on type
474
+ example[field] = this._generatePlaceholder(definition);
475
+ }
476
+ }
477
+ }
478
+
479
+ return example;
480
+ }
481
+
482
+ /**
483
+ * Generate placeholder value based on type
484
+ * @private
485
+ */
486
+ _generatePlaceholder(definition) {
487
+ const type = Array.isArray(definition.type) ? definition.type[0] : definition.type;
488
+
489
+ switch (type) {
490
+ case 'string':
491
+ if (definition.enum) return definition.enum[0];
492
+ return 'example-value';
493
+ case 'number':
494
+ if (definition.min !== undefined) return definition.min;
495
+ return 0;
496
+ case 'boolean':
497
+ return false;
498
+ case 'array':
499
+ return [];
500
+ case 'object':
501
+ return {};
502
+ default:
503
+ return null;
504
+ }
505
+ }
506
+ }