@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.
- package/CHANGELOG.md +564 -0
- package/LICENSE +21 -0
- package/README.md +1393 -0
- package/bin/README.md +71 -0
- package/bin/clodo-service.js +416 -0
- package/bin/security/security-cli.js +96 -0
- package/bin/service-management/README.md +74 -0
- package/bin/service-management/create-service.js +129 -0
- package/bin/service-management/init-service.js +102 -0
- package/bin/service-management/init-service.js.backup +889 -0
- package/bin/shared/config/customer-cli.js +293 -0
- package/dist/config/ConfigurationManager.js +159 -0
- package/dist/config/CustomerConfigCLI.js +220 -0
- package/dist/config/FeatureManager.js +426 -0
- package/dist/config/customers.js +441 -0
- package/dist/config/domains.js +180 -0
- package/dist/config/features.js +225 -0
- package/dist/config/index.js +6 -0
- package/dist/database/database-orchestrator.js +730 -0
- package/dist/database/index.js +4 -0
- package/dist/deployment/auditor.js +971 -0
- package/dist/deployment/index.js +10 -0
- package/dist/deployment/rollback-manager.js +523 -0
- package/dist/deployment/testers/api-tester.js +80 -0
- package/dist/deployment/testers/auth-tester.js +129 -0
- package/dist/deployment/testers/core.js +217 -0
- package/dist/deployment/testers/database-tester.js +105 -0
- package/dist/deployment/testers/index.js +74 -0
- package/dist/deployment/testers/load-tester.js +120 -0
- package/dist/deployment/testers/performance-tester.js +105 -0
- package/dist/deployment/validator.js +558 -0
- package/dist/deployment/wrangler-deployer.js +574 -0
- package/dist/handlers/GenericRouteHandler.js +532 -0
- package/dist/index.js +39 -0
- package/dist/migration/MigrationAdapters.js +562 -0
- package/dist/modules/ModuleManager.js +668 -0
- package/dist/modules/security.js +98 -0
- package/dist/orchestration/cross-domain-coordinator.js +1083 -0
- package/dist/orchestration/index.js +5 -0
- package/dist/orchestration/modules/DeploymentCoordinator.js +258 -0
- package/dist/orchestration/modules/DomainResolver.js +196 -0
- package/dist/orchestration/modules/StateManager.js +332 -0
- package/dist/orchestration/multi-domain-orchestrator.js +255 -0
- package/dist/routing/EnhancedRouter.js +158 -0
- package/dist/schema/SchemaManager.js +778 -0
- package/dist/security/ConfigurationValidator.js +490 -0
- package/dist/security/DeploymentManager.js +208 -0
- package/dist/security/SecretGenerator.js +142 -0
- package/dist/security/SecurityCLI.js +228 -0
- package/dist/security/index.js +51 -0
- package/dist/security/patterns/environment-rules.js +66 -0
- package/dist/security/patterns/insecure-patterns.js +21 -0
- package/dist/service-management/ConfirmationEngine.js +411 -0
- package/dist/service-management/ErrorTracker.js +294 -0
- package/dist/service-management/GenerationEngine.js +3109 -0
- package/dist/service-management/InputCollector.js +237 -0
- package/dist/service-management/ServiceCreator.js +229 -0
- package/dist/service-management/ServiceInitializer.js +448 -0
- package/dist/service-management/ServiceOrchestrator.js +638 -0
- package/dist/service-management/handlers/ConfigMutator.js +130 -0
- package/dist/service-management/handlers/ConfirmationHandler.js +71 -0
- package/dist/service-management/handlers/GenerationHandler.js +80 -0
- package/dist/service-management/handlers/InputHandler.js +59 -0
- package/dist/service-management/handlers/ValidationHandler.js +203 -0
- package/dist/service-management/index.js +7 -0
- package/dist/services/GenericDataService.js +488 -0
- package/dist/shared/cloudflare/domain-discovery.js +562 -0
- package/dist/shared/cloudflare/domain-manager.js +912 -0
- package/dist/shared/cloudflare/index.js +8 -0
- package/dist/shared/cloudflare/ops.js +387 -0
- package/dist/shared/config/cache.js +1167 -0
- package/dist/shared/config/command-config-manager.js +174 -0
- package/dist/shared/config/customer-cli.js +258 -0
- package/dist/shared/config/index.js +9 -0
- package/dist/shared/config/manager.js +289 -0
- package/dist/shared/database/connection-manager.js +338 -0
- package/dist/shared/database/index.js +7 -0
- package/dist/shared/database/orchestrator.js +632 -0
- package/dist/shared/deployment/auditor.js +971 -0
- package/dist/shared/deployment/index.js +10 -0
- package/dist/shared/deployment/rollback-manager.js +523 -0
- package/dist/shared/deployment/validator.js +558 -0
- package/dist/shared/index.js +32 -0
- package/dist/shared/monitoring/health-checker.js +250 -0
- package/dist/shared/monitoring/index.js +8 -0
- package/dist/shared/monitoring/memory-manager.js +382 -0
- package/dist/shared/monitoring/production-monitor.js +390 -0
- package/dist/shared/production-tester/api-tester.js +80 -0
- package/dist/shared/production-tester/auth-tester.js +129 -0
- package/dist/shared/production-tester/core.js +217 -0
- package/dist/shared/production-tester/database-tester.js +105 -0
- package/dist/shared/production-tester/index.js +74 -0
- package/dist/shared/production-tester/load-tester.js +120 -0
- package/dist/shared/production-tester/performance-tester.js +105 -0
- package/dist/shared/security/api-token-manager.js +296 -0
- package/dist/shared/security/index.js +8 -0
- package/dist/shared/security/secret-generator.js +918 -0
- package/dist/shared/security/secure-token-manager.js +379 -0
- package/dist/shared/utils/error-recovery.js +240 -0
- package/dist/shared/utils/graceful-shutdown-manager.js +380 -0
- package/dist/shared/utils/index.js +9 -0
- package/dist/shared/utils/interactive-prompts.js +134 -0
- package/dist/shared/utils/rate-limiter.js +249 -0
- package/dist/utils/ErrorHandler.js +173 -0
- package/dist/utils/deployment/config-cache.js +1160 -0
- package/dist/utils/deployment/index.js +6 -0
- package/dist/utils/deployment/interactive-prompts.js +97 -0
- package/dist/utils/deployment/secret-generator.js +896 -0
- package/dist/utils/dirname-helper.js +35 -0
- package/dist/utils/domain-config.js +159 -0
- package/dist/utils/error-recovery.js +240 -0
- package/dist/utils/esm-helper.js +52 -0
- package/dist/utils/framework-config.js +481 -0
- package/dist/utils/graceful-shutdown-manager.js +379 -0
- package/dist/utils/health-checker.js +114 -0
- package/dist/utils/index.js +36 -0
- package/dist/utils/prompt-handler.js +98 -0
- package/dist/utils/usage-tracker.js +252 -0
- package/dist/utils/validation.js +112 -0
- package/dist/version/VersionDetector.js +723 -0
- package/dist/worker/index.js +4 -0
- package/dist/worker/integration.js +332 -0
- package/docs/FRAMEWORK-ARCHITECTURE-OVERVIEW.md +206 -0
- package/docs/INTEGRATION_GUIDE.md +2045 -0
- package/docs/README.md +82 -0
- package/docs/SECURITY.md +242 -0
- package/docs/deployment/deployment-guide.md +540 -0
- package/docs/overview.md +280 -0
- package/package.json +176 -0
- 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');
|