noormme 1.0.0 → 1.0.2
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/LICENSE +89 -21
- package/README.md +195 -578
- package/dist/cjs/cli/commands/analyze.js +4 -1
- package/dist/cjs/cli/commands/generate.js +48 -8
- package/dist/cjs/cli/commands/init.js +54 -14
- package/dist/cjs/cli/commands/inspect.js +10 -3
- package/dist/cjs/cli/commands/migrate.js +38 -2
- package/dist/cjs/cli/commands/optimize.js +4 -1
- package/dist/cjs/cli/commands/status.js +41 -5
- package/dist/cjs/cli/commands/watch.js +4 -1
- package/dist/cjs/cli/index.js +4 -1
- package/dist/cjs/dialect/database-introspector.js +16 -21
- package/dist/cjs/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/cjs/dynamic/dynamic.d.ts +20 -0
- package/dist/cjs/dynamic/dynamic.js +25 -0
- package/dist/cjs/edge-runtime/edge-config.d.ts +125 -0
- package/dist/cjs/edge-runtime/edge-config.js +323 -0
- package/dist/cjs/errors/NoormError.d.ts +27 -4
- package/dist/cjs/errors/NoormError.js +142 -21
- package/dist/cjs/logging/logger.d.ts +7 -2
- package/dist/cjs/logging/logger.js +21 -7
- package/dist/cjs/noormme.d.ts +12 -8
- package/dist/cjs/noormme.js +133 -61
- package/dist/cjs/operation-node/column-node.js +4 -0
- package/dist/cjs/operation-node/identifier-node.js +4 -0
- package/dist/cjs/operation-node/table-node.js +7 -0
- package/dist/cjs/parser/reference-parser.js +5 -0
- package/dist/cjs/parser/table-parser.js +2 -0
- package/dist/cjs/performance/index.d.ts +44 -0
- package/dist/cjs/performance/index.js +64 -0
- package/dist/cjs/performance/query-optimizer.d.ts +134 -0
- package/dist/cjs/performance/query-optimizer.js +391 -0
- package/dist/cjs/performance/services/cache-service.d.ts +177 -0
- package/dist/cjs/performance/services/cache-service.js +415 -0
- package/dist/cjs/performance/services/connection-factory.d.ts +198 -0
- package/dist/cjs/performance/services/connection-factory.js +498 -0
- package/dist/cjs/performance/services/metrics-collector.d.ts +162 -0
- package/dist/cjs/performance/services/metrics-collector.js +406 -0
- package/dist/cjs/performance/utils/query-parser.d.ts +123 -0
- package/dist/cjs/performance/utils/query-parser.js +295 -0
- package/dist/cjs/raw-builder/sql.d.ts +73 -26
- package/dist/cjs/raw-builder/sql.js +9 -0
- package/dist/cjs/repository/repository-factory.d.ts +10 -42
- package/dist/cjs/repository/repository-factory.js +276 -394
- package/dist/cjs/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/cjs/schema/core/factories/discovery-factory.js +5 -5
- package/dist/cjs/schema/core/utils/name-generator.js +34 -2
- package/dist/cjs/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/cjs/schema/core/utils/type-mapper.js +4 -7
- package/dist/cjs/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/cjs/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/cjs/schema/test/error-handling.test.js +52 -33
- package/dist/cjs/schema/test/integration.test.js +51 -5
- package/dist/cjs/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/cjs/sqlite-migration/index.js +35 -2
- package/dist/cjs/testing/test-utils.d.ts +5 -0
- package/dist/cjs/testing/test-utils.js +66 -6
- package/dist/cjs/types/index.d.ts +78 -13
- package/dist/cjs/types/index.js +23 -0
- package/dist/cjs/types/type-generator.d.ts +8 -0
- package/dist/cjs/types/type-generator.js +86 -17
- package/dist/cjs/util/safe-sql-helpers.d.ts +124 -0
- package/dist/cjs/util/safe-sql-helpers.js +221 -0
- package/dist/cjs/util/security-validator.d.ts +44 -0
- package/dist/cjs/util/security-validator.js +256 -0
- package/dist/cjs/util/security.d.ts +60 -0
- package/dist/cjs/util/security.js +137 -0
- package/dist/cjs/watch/schema-watcher.js +26 -7
- package/dist/esm/cli/commands/generate.js +10 -6
- package/dist/esm/cli/commands/init.js +15 -11
- package/dist/esm/cli/commands/inspect.js +6 -2
- package/dist/esm/cli/commands/status.js +3 -3
- package/dist/esm/dialect/database-introspector.js +16 -21
- package/dist/esm/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/esm/dynamic/dynamic.d.ts +20 -0
- package/dist/esm/dynamic/dynamic.js +25 -0
- package/dist/esm/edge-runtime/edge-config.d.ts +125 -0
- package/dist/esm/edge-runtime/edge-config.js +281 -0
- package/dist/esm/errors/NoormError.d.ts +27 -4
- package/dist/esm/errors/NoormError.js +134 -18
- package/dist/esm/logging/logger.d.ts +7 -2
- package/dist/esm/logging/logger.js +21 -7
- package/dist/esm/noormme.d.ts +12 -8
- package/dist/esm/noormme.js +130 -61
- package/dist/esm/operation-node/column-node.js +4 -0
- package/dist/esm/operation-node/identifier-node.js +4 -0
- package/dist/esm/operation-node/table-node.js +7 -0
- package/dist/esm/parser/reference-parser.js +5 -0
- package/dist/esm/parser/table-parser.js +2 -0
- package/dist/esm/performance/index.d.ts +44 -0
- package/dist/esm/performance/index.js +48 -0
- package/dist/esm/performance/query-optimizer.d.ts +134 -0
- package/dist/esm/performance/query-optimizer.js +387 -0
- package/dist/esm/performance/services/cache-service.d.ts +177 -0
- package/dist/esm/performance/services/cache-service.js +410 -0
- package/dist/esm/performance/services/connection-factory.d.ts +198 -0
- package/dist/esm/performance/services/connection-factory.js +493 -0
- package/dist/esm/performance/services/metrics-collector.d.ts +162 -0
- package/dist/esm/performance/services/metrics-collector.js +402 -0
- package/dist/esm/performance/utils/query-parser.d.ts +123 -0
- package/dist/esm/performance/utils/query-parser.js +292 -0
- package/dist/esm/raw-builder/sql.d.ts +73 -26
- package/dist/esm/raw-builder/sql.js +9 -0
- package/dist/esm/repository/repository-factory.d.ts +10 -42
- package/dist/esm/repository/repository-factory.js +277 -395
- package/dist/esm/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/esm/schema/core/factories/discovery-factory.js +5 -5
- package/dist/esm/schema/core/utils/name-generator.js +34 -2
- package/dist/esm/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/esm/schema/core/utils/type-mapper.js +4 -7
- package/dist/esm/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/esm/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/esm/schema/test/error-handling.test.js +52 -33
- package/dist/esm/schema/test/integration.test.js +18 -5
- package/dist/esm/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/esm/testing/test-utils.d.ts +5 -0
- package/dist/esm/testing/test-utils.js +66 -6
- package/dist/esm/types/index.d.ts +78 -13
- package/dist/esm/types/index.js +22 -1
- package/dist/esm/types/type-generator.d.ts +8 -0
- package/dist/esm/types/type-generator.js +86 -17
- package/dist/esm/util/safe-sql-helpers.d.ts +124 -0
- package/dist/esm/util/safe-sql-helpers.js +209 -0
- package/dist/esm/util/security-validator.d.ts +44 -0
- package/dist/esm/util/security-validator.js +246 -0
- package/dist/esm/util/security.d.ts +60 -0
- package/dist/esm/util/security.js +114 -0
- package/dist/esm/watch/schema-watcher.js +26 -7
- package/package.json +1 -1
- package/dist/cjs/performance/query-analyzer.d.ts +0 -89
- package/dist/cjs/performance/query-analyzer.js +0 -263
- package/dist/esm/performance/query-analyzer.d.ts +0 -89
- package/dist/esm/performance/query-analyzer.js +0 -260
package/dist/cjs/noormme.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.NOORMME = void 0;
|
|
7
|
+
const kysely_js_1 = require("./kysely.js");
|
|
4
8
|
const schema_discovery_js_1 = require("./schema/schema-discovery.js");
|
|
5
9
|
const type_generator_js_1 = require("./types/type-generator.js");
|
|
6
10
|
const repository_factory_js_1 = require("./repository/repository-factory.js");
|
|
@@ -10,9 +14,12 @@ const logger_js_1 = require("./logging/logger.js");
|
|
|
10
14
|
const NoormError_js_1 = require("./errors/NoormError.js");
|
|
11
15
|
const dotenv_1 = require("dotenv");
|
|
12
16
|
const schema_watcher_js_1 = require("./watch/schema-watcher.js");
|
|
13
|
-
const
|
|
17
|
+
const metrics_collector_js_1 = require("./performance/services/metrics-collector.js");
|
|
14
18
|
const sqlite_auto_optimizer_js_1 = require("./dialect/sqlite/sqlite-auto-optimizer.js");
|
|
15
19
|
const sqlite_auto_indexer_js_1 = require("./dialect/sqlite/sqlite-auto-indexer.js");
|
|
20
|
+
const sqlite_dialect_js_1 = require("./dialect/sqlite/sqlite-dialect.js");
|
|
21
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
22
|
+
const compiled_query_js_1 = require("./query-compiler/compiled-query.js");
|
|
16
23
|
// Global initialization lock to prevent concurrent initialization
|
|
17
24
|
const globalInitLock = new Map();
|
|
18
25
|
/**
|
|
@@ -30,12 +37,13 @@ class NOORMME {
|
|
|
30
37
|
cacheManager;
|
|
31
38
|
logger;
|
|
32
39
|
schemaWatcher = null;
|
|
33
|
-
|
|
40
|
+
metricsCollector = null;
|
|
34
41
|
sqliteAutoOptimizer = null;
|
|
35
42
|
sqliteAutoIndexer = null;
|
|
36
43
|
initialized = false;
|
|
37
44
|
repositories = new Map();
|
|
38
45
|
instanceId;
|
|
46
|
+
schemaChangeCallbacks = [];
|
|
39
47
|
constructor(configOrConnectionString) {
|
|
40
48
|
// Load .env if it exists
|
|
41
49
|
(0, dotenv_1.config)({ path: '.env' });
|
|
@@ -65,7 +73,7 @@ class NOORMME {
|
|
|
65
73
|
this.instanceId = `${this.config.dialect}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
66
74
|
// Initialize Kysely with the provided dialect
|
|
67
75
|
this.dialect = this.createDialect();
|
|
68
|
-
this.db = new
|
|
76
|
+
this.db = new kysely_js_1.Kysely({
|
|
69
77
|
dialect: this.dialect,
|
|
70
78
|
log: this.config.logging?.enabled ? this.logger.createKyselyLogger() : undefined
|
|
71
79
|
});
|
|
@@ -110,34 +118,80 @@ class NOORMME {
|
|
|
110
118
|
this.logger.info('Initializing NOORMME...');
|
|
111
119
|
// Test database connection using the dialect-specific introspector
|
|
112
120
|
const introspector = this.dialect.createIntrospector(this.db);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
let tables = [];
|
|
122
|
+
try {
|
|
123
|
+
tables = await introspector.getTables();
|
|
124
|
+
this.logger.info('Database connection successful');
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.logger.warn('Database connection test failed, but continuing with initialization:', error);
|
|
128
|
+
// Continue with empty schema if connection test fails
|
|
129
|
+
}
|
|
130
|
+
// Discover schema - handle empty databases gracefully
|
|
116
131
|
this.logger.info('Discovering database schema...');
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
let schemaInfo;
|
|
133
|
+
try {
|
|
134
|
+
schemaInfo = await this.schemaDiscovery.discoverSchema();
|
|
135
|
+
this.logger.info(`Discovered ${schemaInfo.tables.length} tables`);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// In test mode, throw the error instead of silently continuing
|
|
139
|
+
if (process.env.NODE_ENV === 'test') {
|
|
140
|
+
this.logger.error('Schema discovery failed in test mode:', error);
|
|
141
|
+
throw new Error(`Schema discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
142
|
+
}
|
|
143
|
+
this.logger.warn('Schema discovery failed, using empty schema:', error);
|
|
144
|
+
// Create empty schema info if discovery fails
|
|
145
|
+
schemaInfo = {
|
|
146
|
+
tables: [],
|
|
147
|
+
relationships: [],
|
|
148
|
+
views: []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Generate types - handle empty schema gracefully
|
|
120
152
|
this.logger.info('Generating TypeScript types...');
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
let generatedTypes;
|
|
154
|
+
try {
|
|
155
|
+
generatedTypes = this.typeGenerator.generateTypes(schemaInfo);
|
|
156
|
+
this.logger.info(`Generated types for ${generatedTypes.entities.length} entities`);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
this.logger.warn('Type generation failed, using empty types:', error);
|
|
160
|
+
// Create empty type info if generation fails
|
|
161
|
+
generatedTypes = {
|
|
162
|
+
entities: [],
|
|
163
|
+
relationships: [],
|
|
164
|
+
types: {}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Cache schema and types - handle caching errors gracefully
|
|
168
|
+
try {
|
|
169
|
+
await this.cacheManager.set('schema', schemaInfo);
|
|
170
|
+
await this.cacheManager.set('types', generatedTypes);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.logger.warn('Failed to cache schema/types, continuing without cache:', error);
|
|
174
|
+
}
|
|
175
|
+
// Initialize relationship engine - handle empty relationships
|
|
176
|
+
try {
|
|
177
|
+
this.relationshipEngine.initialize(schemaInfo.relationships);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
this.logger.warn('Failed to initialize relationship engine:', error);
|
|
181
|
+
}
|
|
182
|
+
// Initialize metrics collector for development mode
|
|
183
|
+
this.metricsCollector = new metrics_collector_js_1.MetricsCollector({
|
|
184
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
131
185
|
slowQueryThreshold: 1000,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
186
|
+
nPlusOneDetection: true,
|
|
187
|
+
missingIndexDetection: true
|
|
188
|
+
}, this.logger);
|
|
135
189
|
// Initialize SQLite-specific auto-optimization features
|
|
136
190
|
if (this.config.dialect === 'sqlite') {
|
|
137
191
|
this.sqliteAutoOptimizer = sqlite_auto_optimizer_js_1.SQLiteAutoOptimizer.getInstance(this.logger);
|
|
138
192
|
this.sqliteAutoIndexer = sqlite_auto_indexer_js_1.SQLiteAutoIndexer.getInstance(this.logger);
|
|
139
193
|
// Apply automatic optimizations if enabled (default: true)
|
|
140
|
-
const enableAutoOptimization = this.config.
|
|
194
|
+
const enableAutoOptimization = this.config.automation?.enableAutoOptimization !== false;
|
|
141
195
|
if (enableAutoOptimization) {
|
|
142
196
|
await this.applySQLiteAutoOptimizations();
|
|
143
197
|
}
|
|
@@ -209,8 +263,8 @@ class NOORMME {
|
|
|
209
263
|
if (this.config.dialect === 'sqlite' && this.sqliteAutoIndexer) {
|
|
210
264
|
this.sqliteAutoIndexer.recordQuery(query, executionTime, table);
|
|
211
265
|
}
|
|
212
|
-
if (this.
|
|
213
|
-
this.
|
|
266
|
+
if (this.metricsCollector) {
|
|
267
|
+
this.metricsCollector.recordQuery(query, executionTime, { table });
|
|
214
268
|
}
|
|
215
269
|
}
|
|
216
270
|
/**
|
|
@@ -247,7 +301,8 @@ class NOORMME {
|
|
|
247
301
|
}
|
|
248
302
|
const table = schemaInfo.tables.find(t => t.name === tableName);
|
|
249
303
|
if (!table) {
|
|
250
|
-
|
|
304
|
+
const availableTables = schemaInfo.tables.map(t => t.name);
|
|
305
|
+
throw new NoormError_js_1.TableNotFoundError(tableName, availableTables);
|
|
251
306
|
}
|
|
252
307
|
const repository = this.repositoryFactory.createRepository(table, schemaInfo.relationships);
|
|
253
308
|
this.repositories.set(tableName, repository);
|
|
@@ -301,28 +356,34 @@ class NOORMME {
|
|
|
301
356
|
/**
|
|
302
357
|
* Start monitoring schema changes in development mode
|
|
303
358
|
*/
|
|
304
|
-
startSchemaWatching(options) {
|
|
359
|
+
async startSchemaWatching(options) {
|
|
305
360
|
if (!this.initialized) {
|
|
306
361
|
throw new NoormError_js_1.NoormError('NOORMME must be initialized before starting schema watching');
|
|
307
362
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
await this.refreshSchema();
|
|
318
|
-
this.logger.info('Schema refreshed successfully');
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
this.logger.error('Failed to refresh schema:', error);
|
|
322
|
-
}
|
|
323
|
-
});
|
|
363
|
+
// If watcher already exists (e.g., from onSchemaChange), recreate it with new options
|
|
364
|
+
if (this.schemaWatcher) {
|
|
365
|
+
this.schemaWatcher.stopWatching();
|
|
366
|
+
}
|
|
367
|
+
this.schemaWatcher = new schema_watcher_js_1.SchemaWatcher(this.db, this.schemaDiscovery, this.logger, options);
|
|
368
|
+
// Register all previously registered callbacks
|
|
369
|
+
for (const callback of this.schemaChangeCallbacks) {
|
|
370
|
+
this.schemaWatcher.onSchemaChange(callback);
|
|
324
371
|
}
|
|
325
|
-
|
|
372
|
+
// Auto-refresh schema when changes detected
|
|
373
|
+
this.schemaWatcher.onSchemaChange(async (changes) => {
|
|
374
|
+
this.logger.info(`Schema changes detected: ${changes.length} changes`);
|
|
375
|
+
changes.forEach(change => {
|
|
376
|
+
this.logger.info(` - ${change.type}: ${change.table}`);
|
|
377
|
+
});
|
|
378
|
+
try {
|
|
379
|
+
await this.refreshSchema();
|
|
380
|
+
this.logger.info('Schema refreshed successfully');
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
this.logger.error('Failed to refresh schema:', error);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
await this.schemaWatcher.startWatching();
|
|
326
387
|
}
|
|
327
388
|
/**
|
|
328
389
|
* Stop monitoring schema changes
|
|
@@ -330,16 +391,19 @@ class NOORMME {
|
|
|
330
391
|
stopSchemaWatching() {
|
|
331
392
|
if (this.schemaWatcher) {
|
|
332
393
|
this.schemaWatcher.stopWatching();
|
|
394
|
+
// Don't set to null - keep watcher instance for potential restart
|
|
333
395
|
}
|
|
334
396
|
}
|
|
335
397
|
/**
|
|
336
398
|
* Register callback for schema changes
|
|
337
399
|
*/
|
|
338
400
|
onSchemaChange(callback) {
|
|
339
|
-
if
|
|
340
|
-
|
|
401
|
+
// Store callback so it can be re-registered if watcher is recreated
|
|
402
|
+
this.schemaChangeCallbacks.push(callback);
|
|
403
|
+
// If watcher already exists, register callback immediately
|
|
404
|
+
if (this.schemaWatcher) {
|
|
405
|
+
this.schemaWatcher.onSchemaChange(callback);
|
|
341
406
|
}
|
|
342
|
-
this.schemaWatcher.onSchemaChange(callback);
|
|
343
407
|
}
|
|
344
408
|
/**
|
|
345
409
|
* Get performance metrics
|
|
@@ -351,10 +415,10 @@ class NOORMME {
|
|
|
351
415
|
cacheHitRate: this.cacheManager.getHitRate(),
|
|
352
416
|
repositoryCount: this.repositories.size
|
|
353
417
|
};
|
|
354
|
-
if (this.
|
|
418
|
+
if (this.metricsCollector) {
|
|
355
419
|
return {
|
|
356
420
|
...baseMetrics,
|
|
357
|
-
...this.
|
|
421
|
+
...this.metricsCollector.getPerformanceStats()
|
|
358
422
|
};
|
|
359
423
|
}
|
|
360
424
|
return baseMetrics;
|
|
@@ -370,16 +434,21 @@ class NOORMME {
|
|
|
370
434
|
if (!schemaInfo) {
|
|
371
435
|
throw new NoormError_js_1.NoormError('Schema not found. Please reinitialize NOORMME.');
|
|
372
436
|
}
|
|
373
|
-
this.
|
|
437
|
+
this.metricsCollector = new metrics_collector_js_1.MetricsCollector({
|
|
438
|
+
enabled: true,
|
|
439
|
+
slowQueryThreshold: options?.slowQueryThreshold || 1000,
|
|
440
|
+
nPlusOneDetection: true,
|
|
441
|
+
missingIndexDetection: true
|
|
442
|
+
}, this.logger);
|
|
374
443
|
this.logger.info('Query performance monitoring enabled');
|
|
375
444
|
}
|
|
376
445
|
/**
|
|
377
446
|
* Disable query performance monitoring
|
|
378
447
|
*/
|
|
379
448
|
disablePerformanceMonitoring() {
|
|
380
|
-
if (this.
|
|
381
|
-
this.
|
|
382
|
-
this.
|
|
449
|
+
if (this.metricsCollector) {
|
|
450
|
+
this.metricsCollector.clear();
|
|
451
|
+
this.metricsCollector = null;
|
|
383
452
|
this.logger.info('Query performance monitoring disabled');
|
|
384
453
|
}
|
|
385
454
|
}
|
|
@@ -396,6 +465,12 @@ class NOORMME {
|
|
|
396
465
|
this.repositories.clear();
|
|
397
466
|
this.logger.info('NOORMME closed');
|
|
398
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Alias for close() - for backward compatibility
|
|
470
|
+
*/
|
|
471
|
+
async destroy() {
|
|
472
|
+
return this.close();
|
|
473
|
+
}
|
|
399
474
|
/**
|
|
400
475
|
* Get the underlying Kysely instance for custom queries
|
|
401
476
|
*/
|
|
@@ -411,11 +486,9 @@ class NOORMME {
|
|
|
411
486
|
/**
|
|
412
487
|
* Execute raw SQL
|
|
413
488
|
*/
|
|
414
|
-
async execute(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
parameters: parameters || []
|
|
418
|
-
});
|
|
489
|
+
async execute(sqlString, parameters) {
|
|
490
|
+
const compiledQuery = compiled_query_js_1.CompiledQuery.raw(sqlString, parameters || []);
|
|
491
|
+
return await this.db.executeQuery(compiledQuery);
|
|
419
492
|
}
|
|
420
493
|
mergeConfig(config) {
|
|
421
494
|
return {
|
|
@@ -507,9 +580,8 @@ class NOORMME {
|
|
|
507
580
|
const { dialect, connection } = this.config;
|
|
508
581
|
switch (dialect) {
|
|
509
582
|
case 'sqlite':
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
database: new Database(connection.database)
|
|
583
|
+
return new sqlite_dialect_js_1.SqliteDialect({
|
|
584
|
+
database: new better_sqlite3_1.default(connection.database)
|
|
513
585
|
});
|
|
514
586
|
default:
|
|
515
587
|
throw new Error(`Unsupported dialect: ${dialect}`);
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ColumnNode = void 0;
|
|
4
4
|
const object_utils_js_1 = require("../util/object-utils.js");
|
|
5
5
|
const identifier_node_js_1 = require("./identifier-node.js");
|
|
6
|
+
const security_validator_js_1 = require("../util/security-validator.js");
|
|
6
7
|
/**
|
|
7
8
|
* @internal
|
|
8
9
|
*/
|
|
@@ -11,6 +12,9 @@ exports.ColumnNode = (0, object_utils_js_1.freeze)({
|
|
|
11
12
|
return node.kind === 'ColumnNode';
|
|
12
13
|
},
|
|
13
14
|
create(column) {
|
|
15
|
+
// SECURITY: Validate column name to prevent SQL injection
|
|
16
|
+
// Even though IdentifierNode will validate, we validate here too for defense in depth
|
|
17
|
+
(0, security_validator_js_1.validateIdentifier)(column, 'column name');
|
|
14
18
|
return (0, object_utils_js_1.freeze)({
|
|
15
19
|
kind: 'ColumnNode',
|
|
16
20
|
column: identifier_node_js_1.IdentifierNode.create(column),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.IdentifierNode = void 0;
|
|
4
4
|
const object_utils_js_1 = require("../util/object-utils.js");
|
|
5
|
+
const security_validator_js_1 = require("../util/security-validator.js");
|
|
5
6
|
/**
|
|
6
7
|
* @internal
|
|
7
8
|
*/
|
|
@@ -10,6 +11,9 @@ exports.IdentifierNode = (0, object_utils_js_1.freeze)({
|
|
|
10
11
|
return node.kind === 'IdentifierNode';
|
|
11
12
|
},
|
|
12
13
|
create(name) {
|
|
14
|
+
// SECURITY: Validate identifier to prevent SQL injection at the lowest level
|
|
15
|
+
// This ensures ALL identifiers are validated, even when parsers are called directly
|
|
16
|
+
(0, security_validator_js_1.validateIdentifier)(name, 'identifier');
|
|
13
17
|
return (0, object_utils_js_1.freeze)({
|
|
14
18
|
kind: 'IdentifierNode',
|
|
15
19
|
name,
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TableNode = void 0;
|
|
4
4
|
const object_utils_js_1 = require("../util/object-utils.js");
|
|
5
5
|
const schemable_identifier_node_js_1 = require("./schemable-identifier-node.js");
|
|
6
|
+
const security_validator_js_1 = require("../util/security-validator.js");
|
|
6
7
|
/**
|
|
7
8
|
* @internal
|
|
8
9
|
*/
|
|
@@ -11,12 +12,18 @@ exports.TableNode = (0, object_utils_js_1.freeze)({
|
|
|
11
12
|
return node.kind === 'TableNode';
|
|
12
13
|
},
|
|
13
14
|
create(table) {
|
|
15
|
+
// SECURITY: Validate table name to prevent SQL injection
|
|
16
|
+
// Even though SchemableIdentifierNode will validate, we validate here too for defense in depth
|
|
17
|
+
(0, security_validator_js_1.validateIdentifier)(table, 'table name');
|
|
14
18
|
return (0, object_utils_js_1.freeze)({
|
|
15
19
|
kind: 'TableNode',
|
|
16
20
|
table: schemable_identifier_node_js_1.SchemableIdentifierNode.create(table),
|
|
17
21
|
});
|
|
18
22
|
},
|
|
19
23
|
createWithSchema(schema, table) {
|
|
24
|
+
// SECURITY: Validate both schema and table names
|
|
25
|
+
(0, security_validator_js_1.validateIdentifier)(schema, 'schema name');
|
|
26
|
+
(0, security_validator_js_1.validateIdentifier)(table, 'table name');
|
|
20
27
|
return (0, object_utils_js_1.freeze)({
|
|
21
28
|
kind: 'TableNode',
|
|
22
29
|
table: schemable_identifier_node_js_1.SchemableIdentifierNode.createWithSchema(schema, table),
|
|
@@ -54,13 +54,16 @@ function parseJSONReference(ref, op) {
|
|
|
54
54
|
function parseStringReference(ref) {
|
|
55
55
|
const COLUMN_SEPARATOR = '.';
|
|
56
56
|
if (!ref.includes(COLUMN_SEPARATOR)) {
|
|
57
|
+
// SECURITY: ColumnNode.create will validate the column name
|
|
57
58
|
return reference_node_js_1.ReferenceNode.create(column_node_js_1.ColumnNode.create(ref));
|
|
58
59
|
}
|
|
59
60
|
const parts = ref.split(COLUMN_SEPARATOR).map(trim);
|
|
60
61
|
if (parts.length === 3) {
|
|
62
|
+
// SECURITY: parseStringReferenceWithTableAndSchema uses ColumnNode and TableNode which validate
|
|
61
63
|
return parseStringReferenceWithTableAndSchema(parts);
|
|
62
64
|
}
|
|
63
65
|
if (parts.length === 2) {
|
|
66
|
+
// SECURITY: parseStringReferenceWithTable uses ColumnNode and TableNode which validate
|
|
64
67
|
return parseStringReferenceWithTable(parts);
|
|
65
68
|
}
|
|
66
69
|
throw new Error(`invalid column reference ${ref}`);
|
|
@@ -93,10 +96,12 @@ function parseOrderedColumnName(column) {
|
|
|
93
96
|
}
|
|
94
97
|
function parseStringReferenceWithTableAndSchema(parts) {
|
|
95
98
|
const [schema, table, column] = parts;
|
|
99
|
+
// SECURITY: Both ColumnNode.create and TableNode.createWithSchema validate their inputs
|
|
96
100
|
return reference_node_js_1.ReferenceNode.create(column_node_js_1.ColumnNode.create(column), table_node_js_1.TableNode.createWithSchema(schema, table));
|
|
97
101
|
}
|
|
98
102
|
function parseStringReferenceWithTable(parts) {
|
|
99
103
|
const [table, column] = parts;
|
|
104
|
+
// SECURITY: Both ColumnNode.create and TableNode.create validate their inputs
|
|
100
105
|
return reference_node_js_1.ReferenceNode.create(column_node_js_1.ColumnNode.create(column), table_node_js_1.TableNode.create(table));
|
|
101
106
|
}
|
|
102
107
|
function trim(str) {
|
|
@@ -43,9 +43,11 @@ function parseTable(from) {
|
|
|
43
43
|
const SCHEMA_SEPARATOR = '.';
|
|
44
44
|
if (from.includes(SCHEMA_SEPARATOR)) {
|
|
45
45
|
const [schema, table] = from.split(SCHEMA_SEPARATOR).map(trim);
|
|
46
|
+
// SECURITY: TableNode.createWithSchema will validate both parts
|
|
46
47
|
return table_node_js_1.TableNode.createWithSchema(schema, table);
|
|
47
48
|
}
|
|
48
49
|
else {
|
|
50
|
+
// SECURITY: TableNode.create will validate the table name
|
|
49
51
|
return table_node_js_1.TableNode.create(from);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance module exports
|
|
3
|
+
* Clean, focused services, utilities, and classes
|
|
4
|
+
*/
|
|
5
|
+
export * from './utils/query-parser';
|
|
6
|
+
export * from './services/cache-service';
|
|
7
|
+
export * from './services/metrics-collector';
|
|
8
|
+
export * from './services/connection-factory';
|
|
9
|
+
export * from './query-optimizer';
|
|
10
|
+
/**
|
|
11
|
+
* Default performance configuration
|
|
12
|
+
*/
|
|
13
|
+
export declare const defaultPerformanceConfig: {
|
|
14
|
+
cache: {
|
|
15
|
+
maxSize: number;
|
|
16
|
+
defaultTtl: number;
|
|
17
|
+
cleanupInterval: number;
|
|
18
|
+
enableMetrics: boolean;
|
|
19
|
+
};
|
|
20
|
+
metrics: {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
slowQueryThreshold: number;
|
|
23
|
+
nPlusOneDetection: boolean;
|
|
24
|
+
missingIndexDetection: boolean;
|
|
25
|
+
largeResultSetThreshold: number;
|
|
26
|
+
maxHistorySize: number;
|
|
27
|
+
warningRetentionDays: number;
|
|
28
|
+
};
|
|
29
|
+
connectionPool: {
|
|
30
|
+
minConnections: number;
|
|
31
|
+
maxConnections: number;
|
|
32
|
+
acquireTimeout: number;
|
|
33
|
+
idleTimeout: number;
|
|
34
|
+
validationInterval: number;
|
|
35
|
+
};
|
|
36
|
+
optimizer: {
|
|
37
|
+
enableQueryCache: boolean;
|
|
38
|
+
enableIndexRecommendations: boolean;
|
|
39
|
+
enableQueryRewriting: boolean;
|
|
40
|
+
slowQueryThreshold: number;
|
|
41
|
+
cacheSize: number;
|
|
42
|
+
maxCacheAge: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Performance module exports
|
|
4
|
+
* Clean, focused services, utilities, and classes
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.defaultPerformanceConfig = void 0;
|
|
22
|
+
// Utilities
|
|
23
|
+
__exportStar(require("./utils/query-parser"), exports);
|
|
24
|
+
// Services
|
|
25
|
+
__exportStar(require("./services/cache-service"), exports);
|
|
26
|
+
__exportStar(require("./services/metrics-collector"), exports);
|
|
27
|
+
__exportStar(require("./services/connection-factory"), exports);
|
|
28
|
+
// Main optimizer
|
|
29
|
+
__exportStar(require("./query-optimizer"), exports);
|
|
30
|
+
/**
|
|
31
|
+
* Default performance configuration
|
|
32
|
+
*/
|
|
33
|
+
exports.defaultPerformanceConfig = {
|
|
34
|
+
cache: {
|
|
35
|
+
maxSize: 1000,
|
|
36
|
+
defaultTtl: 300000, // 5 minutes
|
|
37
|
+
cleanupInterval: 60000, // 1 minute
|
|
38
|
+
enableMetrics: true
|
|
39
|
+
},
|
|
40
|
+
metrics: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
slowQueryThreshold: 1000,
|
|
43
|
+
nPlusOneDetection: true,
|
|
44
|
+
missingIndexDetection: true,
|
|
45
|
+
largeResultSetThreshold: 1000,
|
|
46
|
+
maxHistorySize: 10000,
|
|
47
|
+
warningRetentionDays: 7
|
|
48
|
+
},
|
|
49
|
+
connectionPool: {
|
|
50
|
+
minConnections: 2,
|
|
51
|
+
maxConnections: 10,
|
|
52
|
+
acquireTimeout: 30000,
|
|
53
|
+
idleTimeout: 300000,
|
|
54
|
+
validationInterval: 60000
|
|
55
|
+
},
|
|
56
|
+
optimizer: {
|
|
57
|
+
enableQueryCache: true,
|
|
58
|
+
enableIndexRecommendations: true,
|
|
59
|
+
enableQueryRewriting: true,
|
|
60
|
+
slowQueryThreshold: 1000,
|
|
61
|
+
cacheSize: 1000,
|
|
62
|
+
maxCacheAge: 300000
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Kysely } from '../kysely';
|
|
2
|
+
import { Logger } from '../logging/logger.js';
|
|
3
|
+
import { SchemaInfo } from '../types';
|
|
4
|
+
export interface QueryOptimizationOptions {
|
|
5
|
+
enableQueryCache: boolean;
|
|
6
|
+
enableIndexRecommendations: boolean;
|
|
7
|
+
enableQueryRewriting: boolean;
|
|
8
|
+
slowQueryThreshold: number;
|
|
9
|
+
cacheSize: number;
|
|
10
|
+
maxCacheAge: number;
|
|
11
|
+
}
|
|
12
|
+
export interface QueryOptimizationResult {
|
|
13
|
+
originalQuery: string;
|
|
14
|
+
optimizedQuery: string;
|
|
15
|
+
optimizationType: 'index_hint' | 'query_rewrite' | 'cache_hit' | 'no_optimization';
|
|
16
|
+
executionTime: number;
|
|
17
|
+
improvement: number;
|
|
18
|
+
suggestions: string[];
|
|
19
|
+
result?: any;
|
|
20
|
+
}
|
|
21
|
+
export interface IndexRecommendation {
|
|
22
|
+
table: string;
|
|
23
|
+
columns: string[];
|
|
24
|
+
type: 'btree' | 'hash' | 'unique';
|
|
25
|
+
reason: string;
|
|
26
|
+
estimatedImprovement: number;
|
|
27
|
+
priority: 'low' | 'medium' | 'high';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Refactored query optimizer using focused services
|
|
31
|
+
*/
|
|
32
|
+
export declare class QueryOptimizer {
|
|
33
|
+
private db;
|
|
34
|
+
private cacheService;
|
|
35
|
+
private metricsCollector;
|
|
36
|
+
private options;
|
|
37
|
+
private logger;
|
|
38
|
+
private schemaInfo;
|
|
39
|
+
constructor(db: Kysely<any>, schemaInfo: SchemaInfo, options?: Partial<QueryOptimizationOptions>, logger?: Logger);
|
|
40
|
+
/**
|
|
41
|
+
* Optimize and execute a query
|
|
42
|
+
*/
|
|
43
|
+
optimizeQuery<T>(query: string, params?: any[], context?: {
|
|
44
|
+
table?: string;
|
|
45
|
+
operation?: string;
|
|
46
|
+
}): Promise<QueryOptimizationResult & {
|
|
47
|
+
result: T;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Get index recommendations
|
|
51
|
+
*/
|
|
52
|
+
getIndexRecommendations(): Promise<IndexRecommendation[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Get query performance statistics
|
|
55
|
+
*/
|
|
56
|
+
getQueryStats(): {
|
|
57
|
+
cache: import("./services/cache-service").CacheStats;
|
|
58
|
+
indexRecommendations: number;
|
|
59
|
+
totalQueries: number;
|
|
60
|
+
averageExecutionTime: number;
|
|
61
|
+
slowQueries: number;
|
|
62
|
+
errorRate: number;
|
|
63
|
+
warningCount: {
|
|
64
|
+
[key: string]: number;
|
|
65
|
+
};
|
|
66
|
+
topSlowQueries: Array<{
|
|
67
|
+
query: string;
|
|
68
|
+
averageTime: number;
|
|
69
|
+
count: number;
|
|
70
|
+
}>;
|
|
71
|
+
queryPatterns: Array<{
|
|
72
|
+
pattern: string;
|
|
73
|
+
frequency: number;
|
|
74
|
+
averageTime: number;
|
|
75
|
+
}>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Clear query cache
|
|
79
|
+
*/
|
|
80
|
+
clearCache(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Clear metrics
|
|
83
|
+
*/
|
|
84
|
+
clearMetrics(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Get cache statistics
|
|
87
|
+
*/
|
|
88
|
+
getCacheStats(): import("./services/cache-service").CacheStats;
|
|
89
|
+
/**
|
|
90
|
+
* Get performance warnings
|
|
91
|
+
*/
|
|
92
|
+
getPerformanceWarnings(): import("./services/metrics-collector").PerformanceWarning[];
|
|
93
|
+
/**
|
|
94
|
+
* Shutdown optimizer
|
|
95
|
+
*/
|
|
96
|
+
shutdown(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Analyze query for optimization opportunities
|
|
99
|
+
*/
|
|
100
|
+
private analyzeQuery;
|
|
101
|
+
/**
|
|
102
|
+
* Optimize SELECT queries
|
|
103
|
+
*/
|
|
104
|
+
private optimizeSelectQuery;
|
|
105
|
+
/**
|
|
106
|
+
* Optimize complex queries (joins, aggregates)
|
|
107
|
+
*/
|
|
108
|
+
private optimizeComplexQuery;
|
|
109
|
+
/**
|
|
110
|
+
* Analyze query for index recommendations
|
|
111
|
+
*/
|
|
112
|
+
private analyzeQueryForIndexes;
|
|
113
|
+
/**
|
|
114
|
+
* Deduplicate index recommendations
|
|
115
|
+
*/
|
|
116
|
+
private deduplicateRecommendations;
|
|
117
|
+
/**
|
|
118
|
+
* Generate optimization suggestions
|
|
119
|
+
*/
|
|
120
|
+
private generateSuggestions;
|
|
121
|
+
/**
|
|
122
|
+
* Execute a query with error handling
|
|
123
|
+
* Note: This creates a proper SQL query with parameters
|
|
124
|
+
*/
|
|
125
|
+
private executeQuery;
|
|
126
|
+
/**
|
|
127
|
+
* Check if column has an index
|
|
128
|
+
*/
|
|
129
|
+
private hasIndexOnColumn;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Factory function to create query optimizer
|
|
133
|
+
*/
|
|
134
|
+
export declare function createQueryOptimizer(db: Kysely<any>, schemaInfo: SchemaInfo, options?: Partial<QueryOptimizationOptions>, logger?: Logger): QueryOptimizer;
|