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
|
@@ -3,19 +3,33 @@
|
|
|
3
3
|
* Logger for NOORMME
|
|
4
4
|
*/
|
|
5
5
|
export class Logger {
|
|
6
|
-
config;
|
|
7
6
|
queryLogs = [];
|
|
8
7
|
queryCount = 0;
|
|
9
8
|
totalQueryTime = 0;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
config;
|
|
10
|
+
namespace;
|
|
11
|
+
constructor(configOrNamespace) {
|
|
12
|
+
if (typeof configOrNamespace === 'string') {
|
|
13
|
+
this.namespace = configOrNamespace;
|
|
14
|
+
this.config = { enabled: true, level: 'info' };
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
this.config = configOrNamespace || {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Format namespace prefix
|
|
22
|
+
*/
|
|
23
|
+
getPrefix(level) {
|
|
24
|
+
const prefix = this.namespace ? `[${this.namespace}]` : '';
|
|
25
|
+
return `[NOORMME ${level.toUpperCase()}]${prefix}`;
|
|
12
26
|
}
|
|
13
27
|
/**
|
|
14
28
|
* Log debug message
|
|
15
29
|
*/
|
|
16
30
|
debug(message, ...args) {
|
|
17
31
|
if (this.shouldLog('debug')) {
|
|
18
|
-
console.debug(
|
|
32
|
+
console.debug(`${this.getPrefix('debug')} ${message}`, ...args);
|
|
19
33
|
}
|
|
20
34
|
}
|
|
21
35
|
/**
|
|
@@ -23,7 +37,7 @@ export class Logger {
|
|
|
23
37
|
*/
|
|
24
38
|
info(message, ...args) {
|
|
25
39
|
if (this.shouldLog('info')) {
|
|
26
|
-
console.info(
|
|
40
|
+
console.info(`${this.getPrefix('info')} ${message}`, ...args);
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
/**
|
|
@@ -31,7 +45,7 @@ export class Logger {
|
|
|
31
45
|
*/
|
|
32
46
|
warn(message, ...args) {
|
|
33
47
|
if (this.shouldLog('warn')) {
|
|
34
|
-
console.warn(
|
|
48
|
+
console.warn(`${this.getPrefix('warn')} ${message}`, ...args);
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
/**
|
|
@@ -39,7 +53,7 @@ export class Logger {
|
|
|
39
53
|
*/
|
|
40
54
|
error(message, ...args) {
|
|
41
55
|
if (this.shouldLog('error')) {
|
|
42
|
-
console.error(
|
|
56
|
+
console.error(`${this.getPrefix('error')} ${message}`, ...args);
|
|
43
57
|
}
|
|
44
58
|
}
|
|
45
59
|
/**
|
package/dist/esm/noormme.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { NOORMConfig, SchemaInfo, Repository } from './types/index.js';
|
|
1
|
+
import { Kysely } from './kysely.js';
|
|
2
|
+
import { NOORMConfig, SchemaInfo, Repository, SchemaChange } from './types/index.js';
|
|
3
3
|
import { WatchOptions } from './watch/schema-watcher.js';
|
|
4
|
-
import { QueryAnalyzerOptions } from './performance/query-analyzer.js';
|
|
5
4
|
/**
|
|
6
5
|
* NOORMME - No ORM, just magic!
|
|
7
6
|
* Zero-configuration pseudo-ORM that works with any existing database
|
|
@@ -17,12 +16,13 @@ export declare class NOORMME {
|
|
|
17
16
|
private cacheManager;
|
|
18
17
|
private logger;
|
|
19
18
|
private schemaWatcher;
|
|
20
|
-
private
|
|
19
|
+
private metricsCollector;
|
|
21
20
|
private sqliteAutoOptimizer;
|
|
22
21
|
private sqliteAutoIndexer;
|
|
23
22
|
private initialized;
|
|
24
23
|
private repositories;
|
|
25
24
|
private instanceId;
|
|
25
|
+
private schemaChangeCallbacks;
|
|
26
26
|
constructor(configOrConnectionString?: NOORMConfig | string);
|
|
27
27
|
/**
|
|
28
28
|
* Initialize NOORMME - discovers schema and generates types
|
|
@@ -81,7 +81,7 @@ export declare class NOORMME {
|
|
|
81
81
|
/**
|
|
82
82
|
* Start monitoring schema changes in development mode
|
|
83
83
|
*/
|
|
84
|
-
startSchemaWatching(options?: WatchOptions): void
|
|
84
|
+
startSchemaWatching(options?: WatchOptions): Promise<void>;
|
|
85
85
|
/**
|
|
86
86
|
* Stop monitoring schema changes
|
|
87
87
|
*/
|
|
@@ -89,7 +89,7 @@ export declare class NOORMME {
|
|
|
89
89
|
/**
|
|
90
90
|
* Register callback for schema changes
|
|
91
91
|
*/
|
|
92
|
-
onSchemaChange(callback: (changes:
|
|
92
|
+
onSchemaChange(callback: (changes: SchemaChange[]) => void): void;
|
|
93
93
|
/**
|
|
94
94
|
* Get performance metrics
|
|
95
95
|
*/
|
|
@@ -102,7 +102,7 @@ export declare class NOORMME {
|
|
|
102
102
|
/**
|
|
103
103
|
* Enable query performance monitoring
|
|
104
104
|
*/
|
|
105
|
-
enablePerformanceMonitoring(options?:
|
|
105
|
+
enablePerformanceMonitoring(options?: any): void;
|
|
106
106
|
/**
|
|
107
107
|
* Disable query performance monitoring
|
|
108
108
|
*/
|
|
@@ -111,6 +111,10 @@ export declare class NOORMME {
|
|
|
111
111
|
* Close database connections
|
|
112
112
|
*/
|
|
113
113
|
close(): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Alias for close() - for backward compatibility
|
|
116
|
+
*/
|
|
117
|
+
destroy(): Promise<void>;
|
|
114
118
|
/**
|
|
115
119
|
* Get the underlying Kysely instance for custom queries
|
|
116
120
|
*/
|
|
@@ -122,7 +126,7 @@ export declare class NOORMME {
|
|
|
122
126
|
/**
|
|
123
127
|
* Execute raw SQL
|
|
124
128
|
*/
|
|
125
|
-
execute(
|
|
129
|
+
execute(sqlString: string, parameters?: unknown[]): Promise<unknown>;
|
|
126
130
|
private mergeConfig;
|
|
127
131
|
/**
|
|
128
132
|
* Parse connection string into NOORMConfig
|
package/dist/esm/noormme.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/// <reference types="./noormme.d.ts" />
|
|
2
|
+
import { Kysely } from './kysely.js';
|
|
2
3
|
import { SchemaDiscovery } from './schema/schema-discovery.js';
|
|
3
4
|
import { TypeGenerator } from './types/type-generator.js';
|
|
4
5
|
import { RepositoryFactory } from './repository/repository-factory.js';
|
|
5
6
|
import { RelationshipEngine } from './relationships/relationship-engine.js';
|
|
6
7
|
import { CacheManager } from './cache/cache-manager.js';
|
|
7
8
|
import { Logger } from './logging/logger.js';
|
|
8
|
-
import { NoormError } from './errors/NoormError.js';
|
|
9
|
+
import { NoormError, TableNotFoundError } from './errors/NoormError.js';
|
|
9
10
|
import { config as loadDotenv } from 'dotenv';
|
|
10
11
|
import { SchemaWatcher } from './watch/schema-watcher.js';
|
|
11
|
-
import {
|
|
12
|
+
import { MetricsCollector } from './performance/services/metrics-collector.js';
|
|
12
13
|
import { SQLiteAutoOptimizer } from './dialect/sqlite/sqlite-auto-optimizer.js';
|
|
13
14
|
import { SQLiteAutoIndexer } from './dialect/sqlite/sqlite-auto-indexer.js';
|
|
15
|
+
import { SqliteDialect } from './dialect/sqlite/sqlite-dialect.js';
|
|
16
|
+
import Database from 'better-sqlite3';
|
|
17
|
+
import { CompiledQuery } from './query-compiler/compiled-query.js';
|
|
14
18
|
// Global initialization lock to prevent concurrent initialization
|
|
15
19
|
const globalInitLock = new Map();
|
|
16
20
|
/**
|
|
@@ -28,12 +32,13 @@ export class NOORMME {
|
|
|
28
32
|
cacheManager;
|
|
29
33
|
logger;
|
|
30
34
|
schemaWatcher = null;
|
|
31
|
-
|
|
35
|
+
metricsCollector = null;
|
|
32
36
|
sqliteAutoOptimizer = null;
|
|
33
37
|
sqliteAutoIndexer = null;
|
|
34
38
|
initialized = false;
|
|
35
39
|
repositories = new Map();
|
|
36
40
|
instanceId;
|
|
41
|
+
schemaChangeCallbacks = [];
|
|
37
42
|
constructor(configOrConnectionString) {
|
|
38
43
|
// Load .env if it exists
|
|
39
44
|
loadDotenv({ path: '.env' });
|
|
@@ -63,7 +68,7 @@ export class NOORMME {
|
|
|
63
68
|
this.instanceId = `${this.config.dialect}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
64
69
|
// Initialize Kysely with the provided dialect
|
|
65
70
|
this.dialect = this.createDialect();
|
|
66
|
-
this.db = new
|
|
71
|
+
this.db = new Kysely({
|
|
67
72
|
dialect: this.dialect,
|
|
68
73
|
log: this.config.logging?.enabled ? this.logger.createKyselyLogger() : undefined
|
|
69
74
|
});
|
|
@@ -108,34 +113,80 @@ export class NOORMME {
|
|
|
108
113
|
this.logger.info('Initializing NOORMME...');
|
|
109
114
|
// Test database connection using the dialect-specific introspector
|
|
110
115
|
const introspector = this.dialect.createIntrospector(this.db);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
let tables = [];
|
|
117
|
+
try {
|
|
118
|
+
tables = await introspector.getTables();
|
|
119
|
+
this.logger.info('Database connection successful');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
this.logger.warn('Database connection test failed, but continuing with initialization:', error);
|
|
123
|
+
// Continue with empty schema if connection test fails
|
|
124
|
+
}
|
|
125
|
+
// Discover schema - handle empty databases gracefully
|
|
114
126
|
this.logger.info('Discovering database schema...');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
let schemaInfo;
|
|
128
|
+
try {
|
|
129
|
+
schemaInfo = await this.schemaDiscovery.discoverSchema();
|
|
130
|
+
this.logger.info(`Discovered ${schemaInfo.tables.length} tables`);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// In test mode, throw the error instead of silently continuing
|
|
134
|
+
if (process.env.NODE_ENV === 'test') {
|
|
135
|
+
this.logger.error('Schema discovery failed in test mode:', error);
|
|
136
|
+
throw new Error(`Schema discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
this.logger.warn('Schema discovery failed, using empty schema:', error);
|
|
139
|
+
// Create empty schema info if discovery fails
|
|
140
|
+
schemaInfo = {
|
|
141
|
+
tables: [],
|
|
142
|
+
relationships: [],
|
|
143
|
+
views: []
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Generate types - handle empty schema gracefully
|
|
118
147
|
this.logger.info('Generating TypeScript types...');
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
let generatedTypes;
|
|
149
|
+
try {
|
|
150
|
+
generatedTypes = this.typeGenerator.generateTypes(schemaInfo);
|
|
151
|
+
this.logger.info(`Generated types for ${generatedTypes.entities.length} entities`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
this.logger.warn('Type generation failed, using empty types:', error);
|
|
155
|
+
// Create empty type info if generation fails
|
|
156
|
+
generatedTypes = {
|
|
157
|
+
entities: [],
|
|
158
|
+
relationships: [],
|
|
159
|
+
types: {}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Cache schema and types - handle caching errors gracefully
|
|
163
|
+
try {
|
|
164
|
+
await this.cacheManager.set('schema', schemaInfo);
|
|
165
|
+
await this.cacheManager.set('types', generatedTypes);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
this.logger.warn('Failed to cache schema/types, continuing without cache:', error);
|
|
169
|
+
}
|
|
170
|
+
// Initialize relationship engine - handle empty relationships
|
|
171
|
+
try {
|
|
172
|
+
this.relationshipEngine.initialize(schemaInfo.relationships);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.logger.warn('Failed to initialize relationship engine:', error);
|
|
176
|
+
}
|
|
177
|
+
// Initialize metrics collector for development mode
|
|
178
|
+
this.metricsCollector = new MetricsCollector({
|
|
179
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
129
180
|
slowQueryThreshold: 1000,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
181
|
+
nPlusOneDetection: true,
|
|
182
|
+
missingIndexDetection: true
|
|
183
|
+
}, this.logger);
|
|
133
184
|
// Initialize SQLite-specific auto-optimization features
|
|
134
185
|
if (this.config.dialect === 'sqlite') {
|
|
135
186
|
this.sqliteAutoOptimizer = SQLiteAutoOptimizer.getInstance(this.logger);
|
|
136
187
|
this.sqliteAutoIndexer = SQLiteAutoIndexer.getInstance(this.logger);
|
|
137
188
|
// Apply automatic optimizations if enabled (default: true)
|
|
138
|
-
const enableAutoOptimization = this.config.
|
|
189
|
+
const enableAutoOptimization = this.config.automation?.enableAutoOptimization !== false;
|
|
139
190
|
if (enableAutoOptimization) {
|
|
140
191
|
await this.applySQLiteAutoOptimizations();
|
|
141
192
|
}
|
|
@@ -207,8 +258,8 @@ export class NOORMME {
|
|
|
207
258
|
if (this.config.dialect === 'sqlite' && this.sqliteAutoIndexer) {
|
|
208
259
|
this.sqliteAutoIndexer.recordQuery(query, executionTime, table);
|
|
209
260
|
}
|
|
210
|
-
if (this.
|
|
211
|
-
this.
|
|
261
|
+
if (this.metricsCollector) {
|
|
262
|
+
this.metricsCollector.recordQuery(query, executionTime, { table });
|
|
212
263
|
}
|
|
213
264
|
}
|
|
214
265
|
/**
|
|
@@ -245,7 +296,8 @@ export class NOORMME {
|
|
|
245
296
|
}
|
|
246
297
|
const table = schemaInfo.tables.find(t => t.name === tableName);
|
|
247
298
|
if (!table) {
|
|
248
|
-
|
|
299
|
+
const availableTables = schemaInfo.tables.map(t => t.name);
|
|
300
|
+
throw new TableNotFoundError(tableName, availableTables);
|
|
249
301
|
}
|
|
250
302
|
const repository = this.repositoryFactory.createRepository(table, schemaInfo.relationships);
|
|
251
303
|
this.repositories.set(tableName, repository);
|
|
@@ -299,28 +351,34 @@ export class NOORMME {
|
|
|
299
351
|
/**
|
|
300
352
|
* Start monitoring schema changes in development mode
|
|
301
353
|
*/
|
|
302
|
-
startSchemaWatching(options) {
|
|
354
|
+
async startSchemaWatching(options) {
|
|
303
355
|
if (!this.initialized) {
|
|
304
356
|
throw new NoormError('NOORMME must be initialized before starting schema watching');
|
|
305
357
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
await this.refreshSchema();
|
|
316
|
-
this.logger.info('Schema refreshed successfully');
|
|
317
|
-
}
|
|
318
|
-
catch (error) {
|
|
319
|
-
this.logger.error('Failed to refresh schema:', error);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
358
|
+
// If watcher already exists (e.g., from onSchemaChange), recreate it with new options
|
|
359
|
+
if (this.schemaWatcher) {
|
|
360
|
+
this.schemaWatcher.stopWatching();
|
|
361
|
+
}
|
|
362
|
+
this.schemaWatcher = new SchemaWatcher(this.db, this.schemaDiscovery, this.logger, options);
|
|
363
|
+
// Register all previously registered callbacks
|
|
364
|
+
for (const callback of this.schemaChangeCallbacks) {
|
|
365
|
+
this.schemaWatcher.onSchemaChange(callback);
|
|
322
366
|
}
|
|
323
|
-
|
|
367
|
+
// Auto-refresh schema when changes detected
|
|
368
|
+
this.schemaWatcher.onSchemaChange(async (changes) => {
|
|
369
|
+
this.logger.info(`Schema changes detected: ${changes.length} changes`);
|
|
370
|
+
changes.forEach(change => {
|
|
371
|
+
this.logger.info(` - ${change.type}: ${change.table}`);
|
|
372
|
+
});
|
|
373
|
+
try {
|
|
374
|
+
await this.refreshSchema();
|
|
375
|
+
this.logger.info('Schema refreshed successfully');
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
this.logger.error('Failed to refresh schema:', error);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
await this.schemaWatcher.startWatching();
|
|
324
382
|
}
|
|
325
383
|
/**
|
|
326
384
|
* Stop monitoring schema changes
|
|
@@ -328,16 +386,19 @@ export class NOORMME {
|
|
|
328
386
|
stopSchemaWatching() {
|
|
329
387
|
if (this.schemaWatcher) {
|
|
330
388
|
this.schemaWatcher.stopWatching();
|
|
389
|
+
// Don't set to null - keep watcher instance for potential restart
|
|
331
390
|
}
|
|
332
391
|
}
|
|
333
392
|
/**
|
|
334
393
|
* Register callback for schema changes
|
|
335
394
|
*/
|
|
336
395
|
onSchemaChange(callback) {
|
|
337
|
-
if
|
|
338
|
-
|
|
396
|
+
// Store callback so it can be re-registered if watcher is recreated
|
|
397
|
+
this.schemaChangeCallbacks.push(callback);
|
|
398
|
+
// If watcher already exists, register callback immediately
|
|
399
|
+
if (this.schemaWatcher) {
|
|
400
|
+
this.schemaWatcher.onSchemaChange(callback);
|
|
339
401
|
}
|
|
340
|
-
this.schemaWatcher.onSchemaChange(callback);
|
|
341
402
|
}
|
|
342
403
|
/**
|
|
343
404
|
* Get performance metrics
|
|
@@ -349,10 +410,10 @@ export class NOORMME {
|
|
|
349
410
|
cacheHitRate: this.cacheManager.getHitRate(),
|
|
350
411
|
repositoryCount: this.repositories.size
|
|
351
412
|
};
|
|
352
|
-
if (this.
|
|
413
|
+
if (this.metricsCollector) {
|
|
353
414
|
return {
|
|
354
415
|
...baseMetrics,
|
|
355
|
-
...this.
|
|
416
|
+
...this.metricsCollector.getPerformanceStats()
|
|
356
417
|
};
|
|
357
418
|
}
|
|
358
419
|
return baseMetrics;
|
|
@@ -368,16 +429,21 @@ export class NOORMME {
|
|
|
368
429
|
if (!schemaInfo) {
|
|
369
430
|
throw new NoormError('Schema not found. Please reinitialize NOORMME.');
|
|
370
431
|
}
|
|
371
|
-
this.
|
|
432
|
+
this.metricsCollector = new MetricsCollector({
|
|
433
|
+
enabled: true,
|
|
434
|
+
slowQueryThreshold: options?.slowQueryThreshold || 1000,
|
|
435
|
+
nPlusOneDetection: true,
|
|
436
|
+
missingIndexDetection: true
|
|
437
|
+
}, this.logger);
|
|
372
438
|
this.logger.info('Query performance monitoring enabled');
|
|
373
439
|
}
|
|
374
440
|
/**
|
|
375
441
|
* Disable query performance monitoring
|
|
376
442
|
*/
|
|
377
443
|
disablePerformanceMonitoring() {
|
|
378
|
-
if (this.
|
|
379
|
-
this.
|
|
380
|
-
this.
|
|
444
|
+
if (this.metricsCollector) {
|
|
445
|
+
this.metricsCollector.clear();
|
|
446
|
+
this.metricsCollector = null;
|
|
381
447
|
this.logger.info('Query performance monitoring disabled');
|
|
382
448
|
}
|
|
383
449
|
}
|
|
@@ -394,6 +460,12 @@ export class NOORMME {
|
|
|
394
460
|
this.repositories.clear();
|
|
395
461
|
this.logger.info('NOORMME closed');
|
|
396
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Alias for close() - for backward compatibility
|
|
465
|
+
*/
|
|
466
|
+
async destroy() {
|
|
467
|
+
return this.close();
|
|
468
|
+
}
|
|
397
469
|
/**
|
|
398
470
|
* Get the underlying Kysely instance for custom queries
|
|
399
471
|
*/
|
|
@@ -409,11 +481,9 @@ export class NOORMME {
|
|
|
409
481
|
/**
|
|
410
482
|
* Execute raw SQL
|
|
411
483
|
*/
|
|
412
|
-
async execute(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
parameters: parameters || []
|
|
416
|
-
});
|
|
484
|
+
async execute(sqlString, parameters) {
|
|
485
|
+
const compiledQuery = CompiledQuery.raw(sqlString, parameters || []);
|
|
486
|
+
return await this.db.executeQuery(compiledQuery);
|
|
417
487
|
}
|
|
418
488
|
mergeConfig(config) {
|
|
419
489
|
return {
|
|
@@ -505,8 +575,7 @@ export class NOORMME {
|
|
|
505
575
|
const { dialect, connection } = this.config;
|
|
506
576
|
switch (dialect) {
|
|
507
577
|
case 'sqlite':
|
|
508
|
-
|
|
509
|
-
return new (require('./dialect/sqlite/sqlite-dialect').SqliteDialect)({
|
|
578
|
+
return new SqliteDialect({
|
|
510
579
|
database: new Database(connection.database)
|
|
511
580
|
});
|
|
512
581
|
default:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="./column-node.d.ts" />
|
|
2
2
|
import { freeze } from '../util/object-utils.js';
|
|
3
3
|
import { IdentifierNode } from './identifier-node.js';
|
|
4
|
+
import { validateIdentifier } from '../util/security-validator.js';
|
|
4
5
|
/**
|
|
5
6
|
* @internal
|
|
6
7
|
*/
|
|
@@ -9,6 +10,9 @@ export const ColumnNode = freeze({
|
|
|
9
10
|
return node.kind === 'ColumnNode';
|
|
10
11
|
},
|
|
11
12
|
create(column) {
|
|
13
|
+
// SECURITY: Validate column name to prevent SQL injection
|
|
14
|
+
// Even though IdentifierNode will validate, we validate here too for defense in depth
|
|
15
|
+
validateIdentifier(column, 'column name');
|
|
12
16
|
return freeze({
|
|
13
17
|
kind: 'ColumnNode',
|
|
14
18
|
column: IdentifierNode.create(column),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="./identifier-node.d.ts" />
|
|
2
2
|
import { freeze } from '../util/object-utils.js';
|
|
3
|
+
import { validateIdentifier } from '../util/security-validator.js';
|
|
3
4
|
/**
|
|
4
5
|
* @internal
|
|
5
6
|
*/
|
|
@@ -8,6 +9,9 @@ export const IdentifierNode = freeze({
|
|
|
8
9
|
return node.kind === 'IdentifierNode';
|
|
9
10
|
},
|
|
10
11
|
create(name) {
|
|
12
|
+
// SECURITY: Validate identifier to prevent SQL injection at the lowest level
|
|
13
|
+
// This ensures ALL identifiers are validated, even when parsers are called directly
|
|
14
|
+
validateIdentifier(name, 'identifier');
|
|
11
15
|
return freeze({
|
|
12
16
|
kind: 'IdentifierNode',
|
|
13
17
|
name,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="./table-node.d.ts" />
|
|
2
2
|
import { freeze } from '../util/object-utils.js';
|
|
3
3
|
import { SchemableIdentifierNode } from './schemable-identifier-node.js';
|
|
4
|
+
import { validateIdentifier } from '../util/security-validator.js';
|
|
4
5
|
/**
|
|
5
6
|
* @internal
|
|
6
7
|
*/
|
|
@@ -9,12 +10,18 @@ export const TableNode = freeze({
|
|
|
9
10
|
return node.kind === 'TableNode';
|
|
10
11
|
},
|
|
11
12
|
create(table) {
|
|
13
|
+
// SECURITY: Validate table name to prevent SQL injection
|
|
14
|
+
// Even though SchemableIdentifierNode will validate, we validate here too for defense in depth
|
|
15
|
+
validateIdentifier(table, 'table name');
|
|
12
16
|
return freeze({
|
|
13
17
|
kind: 'TableNode',
|
|
14
18
|
table: SchemableIdentifierNode.create(table),
|
|
15
19
|
});
|
|
16
20
|
},
|
|
17
21
|
createWithSchema(schema, table) {
|
|
22
|
+
// SECURITY: Validate both schema and table names
|
|
23
|
+
validateIdentifier(schema, 'schema name');
|
|
24
|
+
validateIdentifier(table, 'table name');
|
|
18
25
|
return freeze({
|
|
19
26
|
kind: 'TableNode',
|
|
20
27
|
table: SchemableIdentifierNode.createWithSchema(schema, table),
|
|
@@ -45,13 +45,16 @@ export function parseJSONReference(ref, op) {
|
|
|
45
45
|
export function parseStringReference(ref) {
|
|
46
46
|
const COLUMN_SEPARATOR = '.';
|
|
47
47
|
if (!ref.includes(COLUMN_SEPARATOR)) {
|
|
48
|
+
// SECURITY: ColumnNode.create will validate the column name
|
|
48
49
|
return ReferenceNode.create(ColumnNode.create(ref));
|
|
49
50
|
}
|
|
50
51
|
const parts = ref.split(COLUMN_SEPARATOR).map(trim);
|
|
51
52
|
if (parts.length === 3) {
|
|
53
|
+
// SECURITY: parseStringReferenceWithTableAndSchema uses ColumnNode and TableNode which validate
|
|
52
54
|
return parseStringReferenceWithTableAndSchema(parts);
|
|
53
55
|
}
|
|
54
56
|
if (parts.length === 2) {
|
|
57
|
+
// SECURITY: parseStringReferenceWithTable uses ColumnNode and TableNode which validate
|
|
55
58
|
return parseStringReferenceWithTable(parts);
|
|
56
59
|
}
|
|
57
60
|
throw new Error(`invalid column reference ${ref}`);
|
|
@@ -84,10 +87,12 @@ export function parseOrderedColumnName(column) {
|
|
|
84
87
|
}
|
|
85
88
|
function parseStringReferenceWithTableAndSchema(parts) {
|
|
86
89
|
const [schema, table, column] = parts;
|
|
90
|
+
// SECURITY: Both ColumnNode.create and TableNode.createWithSchema validate their inputs
|
|
87
91
|
return ReferenceNode.create(ColumnNode.create(column), TableNode.createWithSchema(schema, table));
|
|
88
92
|
}
|
|
89
93
|
function parseStringReferenceWithTable(parts) {
|
|
90
94
|
const [table, column] = parts;
|
|
95
|
+
// SECURITY: Both ColumnNode.create and TableNode.create validate their inputs
|
|
91
96
|
return ReferenceNode.create(ColumnNode.create(column), TableNode.create(table));
|
|
92
97
|
}
|
|
93
98
|
function trim(str) {
|
|
@@ -38,9 +38,11 @@ export function parseTable(from) {
|
|
|
38
38
|
const SCHEMA_SEPARATOR = '.';
|
|
39
39
|
if (from.includes(SCHEMA_SEPARATOR)) {
|
|
40
40
|
const [schema, table] = from.split(SCHEMA_SEPARATOR).map(trim);
|
|
41
|
+
// SECURITY: TableNode.createWithSchema will validate both parts
|
|
41
42
|
return TableNode.createWithSchema(schema, table);
|
|
42
43
|
}
|
|
43
44
|
else {
|
|
45
|
+
// SECURITY: TableNode.create will validate the table name
|
|
44
46
|
return TableNode.create(from);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -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,48 @@
|
|
|
1
|
+
/// <reference types="./index.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* Performance module exports
|
|
4
|
+
* Clean, focused services, utilities, and classes
|
|
5
|
+
*/
|
|
6
|
+
// Utilities
|
|
7
|
+
export * from './utils/query-parser';
|
|
8
|
+
// Services
|
|
9
|
+
export * from './services/cache-service';
|
|
10
|
+
export * from './services/metrics-collector';
|
|
11
|
+
export * from './services/connection-factory';
|
|
12
|
+
// Main optimizer
|
|
13
|
+
export * from './query-optimizer';
|
|
14
|
+
/**
|
|
15
|
+
* Default performance configuration
|
|
16
|
+
*/
|
|
17
|
+
export const defaultPerformanceConfig = {
|
|
18
|
+
cache: {
|
|
19
|
+
maxSize: 1000,
|
|
20
|
+
defaultTtl: 300000, // 5 minutes
|
|
21
|
+
cleanupInterval: 60000, // 1 minute
|
|
22
|
+
enableMetrics: true
|
|
23
|
+
},
|
|
24
|
+
metrics: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
slowQueryThreshold: 1000,
|
|
27
|
+
nPlusOneDetection: true,
|
|
28
|
+
missingIndexDetection: true,
|
|
29
|
+
largeResultSetThreshold: 1000,
|
|
30
|
+
maxHistorySize: 10000,
|
|
31
|
+
warningRetentionDays: 7
|
|
32
|
+
},
|
|
33
|
+
connectionPool: {
|
|
34
|
+
minConnections: 2,
|
|
35
|
+
maxConnections: 10,
|
|
36
|
+
acquireTimeout: 30000,
|
|
37
|
+
idleTimeout: 300000,
|
|
38
|
+
validationInterval: 60000
|
|
39
|
+
},
|
|
40
|
+
optimizer: {
|
|
41
|
+
enableQueryCache: true,
|
|
42
|
+
enableIndexRecommendations: true,
|
|
43
|
+
enableQueryRewriting: true,
|
|
44
|
+
slowQueryThreshold: 1000,
|
|
45
|
+
cacheSize: 1000,
|
|
46
|
+
maxCacheAge: 300000
|
|
47
|
+
}
|
|
48
|
+
};
|