@tamyla/clodo-framework 1.0.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +564 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1393 -0
  4. package/bin/README.md +71 -0
  5. package/bin/clodo-service.js +416 -0
  6. package/bin/security/security-cli.js +96 -0
  7. package/bin/service-management/README.md +74 -0
  8. package/bin/service-management/create-service.js +129 -0
  9. package/bin/service-management/init-service.js +102 -0
  10. package/bin/service-management/init-service.js.backup +889 -0
  11. package/bin/shared/config/customer-cli.js +293 -0
  12. package/dist/config/ConfigurationManager.js +159 -0
  13. package/dist/config/CustomerConfigCLI.js +220 -0
  14. package/dist/config/FeatureManager.js +426 -0
  15. package/dist/config/customers.js +441 -0
  16. package/dist/config/domains.js +180 -0
  17. package/dist/config/features.js +225 -0
  18. package/dist/config/index.js +6 -0
  19. package/dist/database/database-orchestrator.js +730 -0
  20. package/dist/database/index.js +4 -0
  21. package/dist/deployment/auditor.js +971 -0
  22. package/dist/deployment/index.js +10 -0
  23. package/dist/deployment/rollback-manager.js +523 -0
  24. package/dist/deployment/testers/api-tester.js +80 -0
  25. package/dist/deployment/testers/auth-tester.js +129 -0
  26. package/dist/deployment/testers/core.js +217 -0
  27. package/dist/deployment/testers/database-tester.js +105 -0
  28. package/dist/deployment/testers/index.js +74 -0
  29. package/dist/deployment/testers/load-tester.js +120 -0
  30. package/dist/deployment/testers/performance-tester.js +105 -0
  31. package/dist/deployment/validator.js +558 -0
  32. package/dist/deployment/wrangler-deployer.js +574 -0
  33. package/dist/handlers/GenericRouteHandler.js +532 -0
  34. package/dist/index.js +39 -0
  35. package/dist/migration/MigrationAdapters.js +562 -0
  36. package/dist/modules/ModuleManager.js +668 -0
  37. package/dist/modules/security.js +98 -0
  38. package/dist/orchestration/cross-domain-coordinator.js +1083 -0
  39. package/dist/orchestration/index.js +5 -0
  40. package/dist/orchestration/modules/DeploymentCoordinator.js +258 -0
  41. package/dist/orchestration/modules/DomainResolver.js +196 -0
  42. package/dist/orchestration/modules/StateManager.js +332 -0
  43. package/dist/orchestration/multi-domain-orchestrator.js +255 -0
  44. package/dist/routing/EnhancedRouter.js +158 -0
  45. package/dist/schema/SchemaManager.js +778 -0
  46. package/dist/security/ConfigurationValidator.js +490 -0
  47. package/dist/security/DeploymentManager.js +208 -0
  48. package/dist/security/SecretGenerator.js +142 -0
  49. package/dist/security/SecurityCLI.js +228 -0
  50. package/dist/security/index.js +51 -0
  51. package/dist/security/patterns/environment-rules.js +66 -0
  52. package/dist/security/patterns/insecure-patterns.js +21 -0
  53. package/dist/service-management/ConfirmationEngine.js +411 -0
  54. package/dist/service-management/ErrorTracker.js +294 -0
  55. package/dist/service-management/GenerationEngine.js +3109 -0
  56. package/dist/service-management/InputCollector.js +237 -0
  57. package/dist/service-management/ServiceCreator.js +229 -0
  58. package/dist/service-management/ServiceInitializer.js +448 -0
  59. package/dist/service-management/ServiceOrchestrator.js +638 -0
  60. package/dist/service-management/handlers/ConfigMutator.js +130 -0
  61. package/dist/service-management/handlers/ConfirmationHandler.js +71 -0
  62. package/dist/service-management/handlers/GenerationHandler.js +80 -0
  63. package/dist/service-management/handlers/InputHandler.js +59 -0
  64. package/dist/service-management/handlers/ValidationHandler.js +203 -0
  65. package/dist/service-management/index.js +7 -0
  66. package/dist/services/GenericDataService.js +488 -0
  67. package/dist/shared/cloudflare/domain-discovery.js +562 -0
  68. package/dist/shared/cloudflare/domain-manager.js +912 -0
  69. package/dist/shared/cloudflare/index.js +8 -0
  70. package/dist/shared/cloudflare/ops.js +387 -0
  71. package/dist/shared/config/cache.js +1167 -0
  72. package/dist/shared/config/command-config-manager.js +174 -0
  73. package/dist/shared/config/customer-cli.js +258 -0
  74. package/dist/shared/config/index.js +9 -0
  75. package/dist/shared/config/manager.js +289 -0
  76. package/dist/shared/database/connection-manager.js +338 -0
  77. package/dist/shared/database/index.js +7 -0
  78. package/dist/shared/database/orchestrator.js +632 -0
  79. package/dist/shared/deployment/auditor.js +971 -0
  80. package/dist/shared/deployment/index.js +10 -0
  81. package/dist/shared/deployment/rollback-manager.js +523 -0
  82. package/dist/shared/deployment/validator.js +558 -0
  83. package/dist/shared/index.js +32 -0
  84. package/dist/shared/monitoring/health-checker.js +250 -0
  85. package/dist/shared/monitoring/index.js +8 -0
  86. package/dist/shared/monitoring/memory-manager.js +382 -0
  87. package/dist/shared/monitoring/production-monitor.js +390 -0
  88. package/dist/shared/production-tester/api-tester.js +80 -0
  89. package/dist/shared/production-tester/auth-tester.js +129 -0
  90. package/dist/shared/production-tester/core.js +217 -0
  91. package/dist/shared/production-tester/database-tester.js +105 -0
  92. package/dist/shared/production-tester/index.js +74 -0
  93. package/dist/shared/production-tester/load-tester.js +120 -0
  94. package/dist/shared/production-tester/performance-tester.js +105 -0
  95. package/dist/shared/security/api-token-manager.js +296 -0
  96. package/dist/shared/security/index.js +8 -0
  97. package/dist/shared/security/secret-generator.js +918 -0
  98. package/dist/shared/security/secure-token-manager.js +379 -0
  99. package/dist/shared/utils/error-recovery.js +240 -0
  100. package/dist/shared/utils/graceful-shutdown-manager.js +380 -0
  101. package/dist/shared/utils/index.js +9 -0
  102. package/dist/shared/utils/interactive-prompts.js +134 -0
  103. package/dist/shared/utils/rate-limiter.js +249 -0
  104. package/dist/utils/ErrorHandler.js +173 -0
  105. package/dist/utils/deployment/config-cache.js +1160 -0
  106. package/dist/utils/deployment/index.js +6 -0
  107. package/dist/utils/deployment/interactive-prompts.js +97 -0
  108. package/dist/utils/deployment/secret-generator.js +896 -0
  109. package/dist/utils/dirname-helper.js +35 -0
  110. package/dist/utils/domain-config.js +159 -0
  111. package/dist/utils/error-recovery.js +240 -0
  112. package/dist/utils/esm-helper.js +52 -0
  113. package/dist/utils/framework-config.js +481 -0
  114. package/dist/utils/graceful-shutdown-manager.js +379 -0
  115. package/dist/utils/health-checker.js +114 -0
  116. package/dist/utils/index.js +36 -0
  117. package/dist/utils/prompt-handler.js +98 -0
  118. package/dist/utils/usage-tracker.js +252 -0
  119. package/dist/utils/validation.js +112 -0
  120. package/dist/version/VersionDetector.js +723 -0
  121. package/dist/worker/index.js +4 -0
  122. package/dist/worker/integration.js +332 -0
  123. package/docs/FRAMEWORK-ARCHITECTURE-OVERVIEW.md +206 -0
  124. package/docs/INTEGRATION_GUIDE.md +2045 -0
  125. package/docs/README.md +82 -0
  126. package/docs/SECURITY.md +242 -0
  127. package/docs/deployment/deployment-guide.md +540 -0
  128. package/docs/overview.md +280 -0
  129. package/package.json +176 -0
  130. package/types/index.d.ts +575 -0
@@ -0,0 +1,778 @@
1
+ /**
2
+ * Configurable Schema System
3
+ * Allows defining data models externally for maximum reusability
4
+ */
5
+
6
+ export class SchemaManager {
7
+ constructor() {
8
+ this.schemas = new Map();
9
+ this.relationships = new Map();
10
+
11
+ // Schema caching for performance
12
+ this.schemaCache = new Map();
13
+ this.sqlCache = new Map();
14
+ this.cacheEnabled = true;
15
+
16
+ // Validation cache
17
+ this.validationCache = new Map();
18
+ this.cacheStats = {
19
+ hits: 0,
20
+ misses: 0,
21
+ validationHits: 0,
22
+ validationMisses: 0
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Register a data model schema
28
+ * @param {string} modelName - Name of the model
29
+ * @param {Object} schema - Schema definition
30
+ */
31
+ registerModel(modelName, schema) {
32
+ // Clear caches when schema is updated
33
+ this.clearSchemaCache(modelName);
34
+ const processedSchema = {
35
+ name: modelName,
36
+ tableName: schema.tableName || modelName.toLowerCase(),
37
+ columns: schema.columns || {},
38
+ indexes: schema.indexes || [],
39
+ relationships: schema.relationships || {},
40
+ hooks: schema.hooks || {},
41
+ validation: schema.validation || {},
42
+ ...schema
43
+ };
44
+ this.schemas.set(modelName, processedSchema);
45
+ this.schemaCache.set(modelName, processedSchema);
46
+
47
+ // Register relationships
48
+ if (schema.relationships) {
49
+ Object.entries(schema.relationships).forEach(([relName, relConfig]) => {
50
+ this.relationships.set(`${modelName}.${relName}`, {
51
+ from: modelName,
52
+ to: relConfig.model,
53
+ type: relConfig.type || 'belongsTo',
54
+ foreignKey: relConfig.foreignKey,
55
+ localKey: relConfig.localKey || 'id'
56
+ });
57
+ });
58
+ }
59
+ console.log(`✅ Registered model: ${modelName}`);
60
+ }
61
+
62
+ /**
63
+ * Get a registered model schema
64
+ * @param {string} modelName - Name of the model
65
+ * @returns {Object} Schema definition
66
+ */
67
+ getModel(modelName) {
68
+ // Check cache first
69
+ if (this.cacheEnabled && this.schemaCache.has(modelName)) {
70
+ return this.schemaCache.get(modelName);
71
+ }
72
+
73
+ // Get from main storage
74
+ const schema = this.schemas.get(modelName);
75
+
76
+ // Cache if found
77
+ if (schema && this.cacheEnabled) {
78
+ this.schemaCache.set(modelName, schema);
79
+ }
80
+ return schema;
81
+ }
82
+
83
+ /**
84
+ * Get all registered models
85
+ * @returns {Map} All schemas
86
+ */
87
+ getAllModels() {
88
+ return this.schemas;
89
+ }
90
+
91
+ /**
92
+ * Check if a model exists
93
+ * @param {string} modelName - Name of the model
94
+ * @returns {boolean}
95
+ */
96
+ hasModel(modelName) {
97
+ return this.schemas.has(modelName);
98
+ }
99
+
100
+ /**
101
+ * Get relationship definition
102
+ * @param {string} modelName - Source model
103
+ * @param {string} relationshipName - Relationship name
104
+ * @returns {Object} Relationship definition
105
+ */
106
+ getRelationship(modelName, relationshipName) {
107
+ return this.relationships.get(`${modelName}.${relationshipName}`);
108
+ }
109
+
110
+ /**
111
+ * Generate SQL for model operations
112
+ * @param {string} modelName - Model name
113
+ * @param {string} operation - Operation type (create, read, update, delete)
114
+ * @param {Object} params - Operation parameters
115
+ * @returns {Object} SQL query object with sql and params properties
116
+ */
117
+ generateSQL(modelName, operation, params = {}) {
118
+ // Generate cache key for this operation
119
+ const cacheKey = this.generateCacheKey(modelName, operation, params);
120
+
121
+ // Check SQL cache first
122
+ if (this.cacheEnabled && this.sqlCache.has(cacheKey)) {
123
+ this.cacheStats.hits++;
124
+ return this.sqlCache.get(cacheKey);
125
+ }
126
+ this.cacheStats.misses++;
127
+ const schema = this.getModel(modelName);
128
+ if (!schema) {
129
+ throw new Error(`Model '${modelName}' not found`);
130
+ }
131
+ const tableName = schema.tableName;
132
+ const columns = Object.keys(schema.columns);
133
+ let result;
134
+ let parameterMapping = [];
135
+ switch (operation) {
136
+ case 'create':
137
+ {
138
+ const insertColumns = columns.filter(col => params[col] !== undefined);
139
+ const placeholders = insertColumns.map(() => '?');
140
+ result = {
141
+ sql: `INSERT INTO ${tableName} (${insertColumns.join(', ')}) VALUES (${placeholders.join(', ')})`,
142
+ params: insertColumns.map(col => params[col])
143
+ };
144
+ parameterMapping = insertColumns;
145
+ break;
146
+ }
147
+ case 'read':
148
+ {
149
+ let whereClause = '';
150
+ let whereParams = [];
151
+ if (params.id) {
152
+ whereClause = 'WHERE id = ?';
153
+ whereParams = [params.id];
154
+ parameterMapping = ['id'];
155
+ } else if (params.where) {
156
+ const conditions = [];
157
+ Object.entries(params.where).forEach(([key, value]) => {
158
+ conditions.push(`${key} = ?`);
159
+ whereParams.push(value);
160
+ parameterMapping.push(`where.${key}`);
161
+ });
162
+ whereClause = `WHERE ${conditions.join(' AND ')}`;
163
+ }
164
+ const selectFields = params.fields ? params.fields.join(', ') : columns.join(', ');
165
+ result = {
166
+ sql: `SELECT ${selectFields} FROM ${tableName} ${whereClause}`,
167
+ params: whereParams
168
+ };
169
+ break;
170
+ }
171
+ case 'update':
172
+ {
173
+ const updateColumns = columns.filter(col => params[col] !== undefined && col !== 'id');
174
+ const setClause = updateColumns.map(col => `${col} = ?`).join(', ');
175
+ const updateParams = updateColumns.map(col => params[col]);
176
+ result = {
177
+ sql: `UPDATE ${tableName} SET ${setClause} WHERE id = ?`,
178
+ params: [...updateParams, params.id]
179
+ };
180
+ parameterMapping = [...updateColumns, 'id'];
181
+ break;
182
+ }
183
+ case 'delete':
184
+ {
185
+ result = {
186
+ sql: `DELETE FROM ${tableName} WHERE id = ?`,
187
+ params: [params.id]
188
+ };
189
+ parameterMapping = ['id'];
190
+ break;
191
+ }
192
+ default:
193
+ throw new Error(`Unknown operation: ${operation}`);
194
+ }
195
+
196
+ // Cache the SQL template for future use
197
+ if (this.cacheEnabled) {
198
+ this.sqlCache.set(cacheKey, {
199
+ sql: result.sql,
200
+ params: result.params,
201
+ parameterMapping
202
+ });
203
+ }
204
+ return result;
205
+ }
206
+
207
+ /**
208
+ * Clear schema cache for specific model or all models
209
+ * @param {string} modelName - Optional: specific model to clear
210
+ */
211
+ clearSchemaCache(modelName = null) {
212
+ if (modelName) {
213
+ this.schemaCache.delete(modelName);
214
+ // Clear related SQL cache entries
215
+ for (const [key] of this.sqlCache) {
216
+ if (key.startsWith(`${modelName}:`)) {
217
+ this.sqlCache.delete(key);
218
+ }
219
+ }
220
+ } else {
221
+ this.schemaCache.clear();
222
+ this.sqlCache.clear();
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Generate cache key for SQL queries
228
+ * @param {string} modelName - Model name
229
+ * @param {string} operation - Operation type
230
+ * @param {Object} params - Query parameters
231
+ * @returns {string} Cache key
232
+ */
233
+ generateCacheKey(modelName, operation, params = {}) {
234
+ return `${modelName}:${operation}:${JSON.stringify(params)}`;
235
+ }
236
+
237
+ /**
238
+ * Enhanced validation with comprehensive error reporting
239
+ * @param {string} modelName - Model name
240
+ * @param {Object} data - Data to validate
241
+ * @returns {Object} Detailed validation result
242
+ */
243
+ validateData(modelName, data) {
244
+ const schema = this.getModel(modelName);
245
+ if (!schema) {
246
+ return {
247
+ valid: false,
248
+ errors: [{
249
+ field: '_model',
250
+ message: `Model '${modelName}' not found`,
251
+ code: 'MODEL_NOT_FOUND'
252
+ }],
253
+ fieldErrors: {},
254
+ data: null
255
+ };
256
+ }
257
+ const errors = [];
258
+ const validatedData = {
259
+ ...data
260
+ };
261
+ const fieldErrors = {};
262
+
263
+ // Check required fields
264
+ if (schema.validation?.required) {
265
+ schema.validation.required.forEach(field => {
266
+ if (data[field] === undefined || data[field] === null || data[field] === '') {
267
+ const error = {
268
+ field,
269
+ message: `Field '${field}' is required`,
270
+ code: 'REQUIRED_FIELD_MISSING',
271
+ value: data[field]
272
+ };
273
+ errors.push(error);
274
+ fieldErrors[field] = fieldErrors[field] || [];
275
+ fieldErrors[field].push(error);
276
+ }
277
+ });
278
+ }
279
+
280
+ // Validate each field against schema
281
+ Object.entries(schema.columns).forEach(([fieldName, fieldConfig]) => {
282
+ if (data[fieldName] !== undefined && data[fieldName] !== null) {
283
+ const value = data[fieldName];
284
+ const validationResult = this._validateField(fieldName, value, fieldConfig, schema);
285
+ if (!validationResult.valid) {
286
+ validationResult.errors.forEach(error => {
287
+ errors.push(error);
288
+ fieldErrors[fieldName] = fieldErrors[fieldName] || [];
289
+ fieldErrors[fieldName].push(error);
290
+ });
291
+ }
292
+
293
+ // Apply transformations
294
+ if (validationResult.transformed !== undefined) {
295
+ validatedData[fieldName] = validationResult.transformed;
296
+ }
297
+ }
298
+ });
299
+
300
+ // Custom validation rules
301
+ if (schema.validation?.custom) {
302
+ Object.entries(schema.validation.custom).forEach(([ruleName, ruleConfig]) => {
303
+ const result = this._validateCustomRule(ruleName, ruleConfig, data, validatedData);
304
+ if (!result.valid) {
305
+ result.errors.forEach(error => {
306
+ errors.push(error);
307
+ const field = error.field || '_custom';
308
+ fieldErrors[field] = fieldErrors[field] || [];
309
+ fieldErrors[field].push(error);
310
+ });
311
+ }
312
+ });
313
+ }
314
+ return {
315
+ valid: errors.length === 0,
316
+ errors,
317
+ fieldErrors,
318
+ data: validatedData
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Validate individual field
324
+ * @param {string} fieldName - Field name
325
+ * @param {any} value - Field value
326
+ * @param {Object} fieldConfig - Field configuration
327
+ * @returns {Object} Field validation result
328
+ * @private
329
+ */
330
+ _validateField(fieldName, value, fieldConfig) {
331
+ const errors = [];
332
+ let transformedValue = value;
333
+
334
+ // Type validation
335
+ if (fieldConfig.type) {
336
+ const typeValidation = this._validateFieldType(fieldName, value, fieldConfig.type);
337
+ if (!typeValidation.valid) {
338
+ errors.push(...typeValidation.errors);
339
+ } else if (typeValidation.transformed !== undefined) {
340
+ transformedValue = typeValidation.transformed;
341
+ }
342
+ }
343
+
344
+ // Length validation
345
+ if (fieldConfig.minLength || fieldConfig.maxLength) {
346
+ const lengthValidation = this._validateFieldLength(fieldName, transformedValue, fieldConfig);
347
+ if (!lengthValidation.valid) {
348
+ errors.push(...lengthValidation.errors);
349
+ }
350
+ }
351
+
352
+ // Pattern validation
353
+ if (fieldConfig.pattern) {
354
+ const patternValidation = this._validateFieldPattern(fieldName, transformedValue, fieldConfig.pattern);
355
+ if (!patternValidation.valid) {
356
+ errors.push(...patternValidation.errors);
357
+ }
358
+ }
359
+ return {
360
+ valid: errors.length === 0,
361
+ errors,
362
+ transformed: transformedValue !== value ? transformedValue : undefined
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Validate field type
368
+ * @param {string} fieldName - Field name
369
+ * @param {any} value - Field value
370
+ * @param {string} expectedType - Expected type
371
+ * @returns {Object} Type validation result
372
+ * @private
373
+ */
374
+ _validateFieldType(fieldName, value, expectedType) {
375
+ const errors = [];
376
+ let transformedValue = value;
377
+ switch (expectedType) {
378
+ case 'text':
379
+ if (typeof value !== 'string') {
380
+ transformedValue = String(value);
381
+ }
382
+ // Trim whitespace for text fields
383
+ if (typeof transformedValue === 'string') {
384
+ transformedValue = transformedValue.trim();
385
+ }
386
+ break;
387
+ case 'integer':
388
+ if (!Number.isInteger(Number(value))) {
389
+ errors.push({
390
+ field: fieldName,
391
+ message: `Field '${fieldName}' must be an integer`,
392
+ code: 'INVALID_TYPE',
393
+ value
394
+ });
395
+ } else {
396
+ transformedValue = parseInt(value, 10);
397
+ }
398
+ break;
399
+ case 'real':
400
+ if (isNaN(Number(value))) {
401
+ errors.push({
402
+ field: fieldName,
403
+ message: `Field '${fieldName}' must be a number`,
404
+ code: 'INVALID_TYPE',
405
+ value
406
+ });
407
+ } else {
408
+ transformedValue = parseFloat(value);
409
+ }
410
+ break;
411
+ case 'blob':
412
+ // No specific validation for blob type
413
+ break;
414
+ default:
415
+ console.warn(`Unknown field type: ${expectedType} for field ${fieldName}`);
416
+ }
417
+ return {
418
+ valid: errors.length === 0,
419
+ errors,
420
+ transformed: transformedValue !== value ? transformedValue : undefined
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Validate field length constraints
426
+ * @param {string} fieldName - Field name
427
+ * @param {any} value - Field value
428
+ * @param {Object} fieldConfig - Field configuration
429
+ * @returns {Object} Length validation result
430
+ * @private
431
+ */
432
+ _validateFieldLength(fieldName, value, fieldConfig) {
433
+ const errors = [];
434
+ const stringValue = String(value);
435
+ if (fieldConfig.minLength && stringValue.length < fieldConfig.minLength) {
436
+ errors.push({
437
+ field: fieldName,
438
+ message: `Field '${fieldName}' must be at least ${fieldConfig.minLength} characters`,
439
+ code: 'MIN_LENGTH_VIOLATION',
440
+ value
441
+ });
442
+ }
443
+ if (fieldConfig.maxLength && stringValue.length > fieldConfig.maxLength) {
444
+ errors.push({
445
+ field: fieldName,
446
+ message: `Field '${fieldName}' cannot exceed ${fieldConfig.maxLength} characters`,
447
+ code: 'MAX_LENGTH_VIOLATION',
448
+ value
449
+ });
450
+ }
451
+ return {
452
+ valid: errors.length === 0,
453
+ errors
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Validate field pattern
459
+ * @param {string} fieldName - Field name
460
+ * @param {any} value - Field value
461
+ * @param {RegExp|string} pattern - Pattern to match
462
+ * @returns {Object} Pattern validation result
463
+ * @private
464
+ */
465
+ _validateFieldPattern(fieldName, value, pattern) {
466
+ const errors = [];
467
+ const stringValue = String(value);
468
+ const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
469
+ if (!regex.test(stringValue)) {
470
+ errors.push({
471
+ field: fieldName,
472
+ message: `Field '${fieldName}' does not match required pattern`,
473
+ code: 'PATTERN_MISMATCH',
474
+ value,
475
+ pattern: pattern.toString()
476
+ });
477
+ }
478
+ return {
479
+ valid: errors.length === 0,
480
+ errors
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Validate custom rules
486
+ * @param {string} ruleName - Rule name
487
+ * @param {Object} ruleConfig - Rule configuration
488
+ * @param {Object} originalData - Original data
489
+ * @param {Object} validatedData - Validated data so far
490
+ * @returns {Object} Custom validation result
491
+ * @private
492
+ */
493
+ _validateCustomRule(ruleName, ruleConfig, originalData, validatedData) {
494
+ const errors = [];
495
+ try {
496
+ if (typeof ruleConfig === 'function') {
497
+ const result = ruleConfig(originalData, validatedData);
498
+ if (result !== true) {
499
+ errors.push({
500
+ field: '_custom',
501
+ message: typeof result === 'string' ? result : `Custom rule '${ruleName}' failed`,
502
+ code: 'CUSTOM_RULE_VIOLATION',
503
+ rule: ruleName
504
+ });
505
+ }
506
+ } else if (ruleConfig.validator && typeof ruleConfig.validator === 'function') {
507
+ const result = ruleConfig.validator(originalData, validatedData);
508
+ if (result !== true) {
509
+ errors.push({
510
+ field: ruleConfig.field || '_custom',
511
+ message: result || ruleConfig.message || `Custom rule '${ruleName}' failed`,
512
+ code: 'CUSTOM_RULE_VIOLATION',
513
+ rule: ruleName
514
+ });
515
+ }
516
+ }
517
+ } catch (error) {
518
+ errors.push({
519
+ field: '_custom',
520
+ message: `Custom rule '${ruleName}' threw an error: ${error.message}`,
521
+ code: 'CUSTOM_RULE_ERROR',
522
+ rule: ruleName
523
+ });
524
+ }
525
+ return {
526
+ valid: errors.length === 0,
527
+ errors
528
+ };
529
+ }
530
+
531
+ /**
532
+ * Extract template parameters for SQL caching
533
+ * @param {Object} params - Full parameters
534
+ * @param {string} operation - Operation type
535
+ * @returns {Object} Template parameters for caching
536
+ * @private
537
+ */
538
+ _extractTemplateParams(params, operation) {
539
+ const templateParams = {};
540
+ switch (operation) {
541
+ case 'create':
542
+ templateParams.columns = Object.keys(params).filter(key => key !== 'id');
543
+ break;
544
+ case 'read':
545
+ templateParams.hasId = !!params.id;
546
+ templateParams.hasWhere = !!params.where;
547
+ templateParams.whereKeys = params.where ? Object.keys(params.where) : [];
548
+ templateParams.fields = params.fields || null;
549
+ break;
550
+ case 'update':
551
+ templateParams.columns = Object.keys(params).filter(key => key !== 'id');
552
+ break;
553
+ case 'delete':
554
+ templateParams.hasId = !!params.id;
555
+ break;
556
+ }
557
+ return templateParams;
558
+ }
559
+
560
+ /**
561
+ * Bind actual parameter values to cached SQL template
562
+ * @param {Object} template - Cached SQL template
563
+ * @param {Object} params - Actual parameters
564
+ * @returns {Array} Bound parameters
565
+ * @private
566
+ */
567
+ _bindParametersToTemplate(template, params) {
568
+ if (!template.parameterMapping) {
569
+ return template.params || [];
570
+ }
571
+ return template.parameterMapping.map(paramPath => {
572
+ if (paramPath.startsWith('where.')) {
573
+ const key = paramPath.substring(6);
574
+ return params.where[key];
575
+ }
576
+ return params[paramPath];
577
+ });
578
+ }
579
+ }
580
+
581
+ // Create singleton instance
582
+ export const schemaManager = new SchemaManager();
583
+
584
+ // Pre-register existing models for backward compatibility
585
+ schemaManager.registerModel('users', {
586
+ tableName: 'users',
587
+ columns: {
588
+ id: {
589
+ type: 'text',
590
+ primaryKey: true
591
+ },
592
+ email: {
593
+ type: 'text',
594
+ unique: true,
595
+ required: true
596
+ },
597
+ name: {
598
+ type: 'text'
599
+ },
600
+ is_email_verified: {
601
+ type: 'integer',
602
+ default: 0
603
+ },
604
+ created_at: {
605
+ type: 'text',
606
+ required: true
607
+ },
608
+ updated_at: {
609
+ type: 'text',
610
+ required: true
611
+ }
612
+ },
613
+ indexes: ['email'],
614
+ validation: {
615
+ required: ['email']
616
+ }
617
+ });
618
+ schemaManager.registerModel('magic_links', {
619
+ tableName: 'magic_links',
620
+ columns: {
621
+ id: {
622
+ type: 'text',
623
+ primaryKey: true
624
+ },
625
+ token: {
626
+ type: 'text',
627
+ unique: true,
628
+ required: true
629
+ },
630
+ user_id: {
631
+ type: 'text',
632
+ required: true
633
+ },
634
+ email: {
635
+ type: 'text',
636
+ required: true
637
+ },
638
+ expires_at: {
639
+ type: 'text',
640
+ required: true
641
+ },
642
+ used: {
643
+ type: 'integer',
644
+ default: 0
645
+ },
646
+ created_at: {
647
+ type: 'text',
648
+ required: true
649
+ }
650
+ },
651
+ relationships: {
652
+ user: {
653
+ model: 'users',
654
+ type: 'belongsTo',
655
+ foreignKey: 'user_id'
656
+ }
657
+ },
658
+ indexes: ['token', 'user_id', 'expires_at']
659
+ });
660
+ schemaManager.registerModel('tokens', {
661
+ tableName: 'tokens',
662
+ columns: {
663
+ id: {
664
+ type: 'text',
665
+ primaryKey: true
666
+ },
667
+ token: {
668
+ type: 'text',
669
+ unique: true,
670
+ required: true
671
+ },
672
+ user_id: {
673
+ type: 'text',
674
+ required: true
675
+ },
676
+ type: {
677
+ type: 'text',
678
+ required: true
679
+ },
680
+ expires_at: {
681
+ type: 'text'
682
+ },
683
+ created_at: {
684
+ type: 'text',
685
+ required: true
686
+ }
687
+ },
688
+ relationships: {
689
+ user: {
690
+ model: 'users',
691
+ type: 'belongsTo',
692
+ foreignKey: 'user_id'
693
+ }
694
+ },
695
+ indexes: ['token', 'user_id', 'type']
696
+ });
697
+ schemaManager.registerModel('files', {
698
+ tableName: 'files',
699
+ columns: {
700
+ id: {
701
+ type: 'text',
702
+ primaryKey: true
703
+ },
704
+ filename: {
705
+ type: 'text',
706
+ required: true
707
+ },
708
+ original_name: {
709
+ type: 'text',
710
+ required: true
711
+ },
712
+ mime_type: {
713
+ type: 'text',
714
+ required: true
715
+ },
716
+ size: {
717
+ type: 'integer',
718
+ required: true
719
+ },
720
+ user_id: {
721
+ type: 'text',
722
+ required: true
723
+ },
724
+ path: {
725
+ type: 'text',
726
+ required: true
727
+ },
728
+ status: {
729
+ type: 'text',
730
+ default: 'uploaded'
731
+ },
732
+ created_at: {
733
+ type: 'text',
734
+ required: true
735
+ },
736
+ updated_at: {
737
+ type: 'text',
738
+ required: true
739
+ }
740
+ },
741
+ relationships: {
742
+ user: {
743
+ model: 'users',
744
+ type: 'belongsTo',
745
+ foreignKey: 'user_id'
746
+ }
747
+ },
748
+ indexes: ['user_id', 'status']
749
+ });
750
+ schemaManager.registerModel('logs', {
751
+ tableName: 'logs',
752
+ columns: {
753
+ id: {
754
+ type: 'text',
755
+ primaryKey: true
756
+ },
757
+ level: {
758
+ type: 'text',
759
+ required: true
760
+ },
761
+ message: {
762
+ type: 'text',
763
+ required: true
764
+ },
765
+ user_id: {
766
+ type: 'text'
767
+ },
768
+ timestamp: {
769
+ type: 'text',
770
+ required: true
771
+ },
772
+ metadata: {
773
+ type: 'text'
774
+ }
775
+ },
776
+ indexes: ['level', 'user_id', 'timestamp']
777
+ });
778
+ console.log('✅ Schema Manager initialized with existing models');