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
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
* Core type definitions for NOORMME
|
|
3
3
|
*/
|
|
4
4
|
export interface NOORMConfig {
|
|
5
|
-
dialect: 'sqlite';
|
|
5
|
+
dialect: 'sqlite' | 'postgresql' | 'mysql';
|
|
6
6
|
connection: ConnectionConfig;
|
|
7
7
|
introspection?: IntrospectionConfig;
|
|
8
8
|
cache?: CacheConfig;
|
|
9
9
|
logging?: LoggingConfig;
|
|
10
10
|
performance?: PerformanceConfig;
|
|
11
|
+
automation?: AutomationConfig;
|
|
12
|
+
optimization?: OptimizationConfig;
|
|
13
|
+
sqlite?: SQLiteConfig;
|
|
11
14
|
}
|
|
12
15
|
export interface ConnectionConfig {
|
|
13
|
-
host
|
|
14
|
-
port
|
|
16
|
+
host?: string;
|
|
17
|
+
port?: number;
|
|
15
18
|
database: string;
|
|
16
|
-
username
|
|
17
|
-
password
|
|
19
|
+
username?: string;
|
|
20
|
+
password?: string;
|
|
18
21
|
ssl?: boolean | object;
|
|
19
22
|
pool?: PoolConfig;
|
|
20
23
|
}
|
|
@@ -42,6 +45,36 @@ export interface PerformanceConfig {
|
|
|
42
45
|
enableQueryOptimization?: boolean;
|
|
43
46
|
enableBatchLoading?: boolean;
|
|
44
47
|
maxBatchSize?: number;
|
|
48
|
+
enableCaching?: boolean;
|
|
49
|
+
maxCacheSize?: number;
|
|
50
|
+
enableBatchOperations?: boolean;
|
|
51
|
+
slowQueryThreshold?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface AutomationConfig {
|
|
54
|
+
enableAutoOptimization?: boolean;
|
|
55
|
+
enableIndexRecommendations?: boolean;
|
|
56
|
+
enableQueryAnalysis?: boolean;
|
|
57
|
+
enableMigrationGeneration?: boolean;
|
|
58
|
+
enablePerformanceMonitoring?: boolean;
|
|
59
|
+
enableSchemaWatcher?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface OptimizationConfig {
|
|
62
|
+
enableWALMode?: boolean;
|
|
63
|
+
enableForeignKeys?: boolean;
|
|
64
|
+
cacheSize?: number;
|
|
65
|
+
synchronous?: 'OFF' | 'NORMAL' | 'FULL' | 'EXTRA';
|
|
66
|
+
tempStore?: 'DEFAULT' | 'FILE' | 'MEMORY';
|
|
67
|
+
autoVacuumMode?: 'NONE' | 'FULL' | 'INCREMENTAL';
|
|
68
|
+
journalMode?: 'DELETE' | 'TRUNCATE' | 'PERSIST' | 'MEMORY' | 'WAL' | 'OFF';
|
|
69
|
+
}
|
|
70
|
+
export interface SQLiteConfig {
|
|
71
|
+
enableWALMode?: boolean;
|
|
72
|
+
enableForeignKeys?: boolean;
|
|
73
|
+
cacheSize?: number;
|
|
74
|
+
synchronous?: 'OFF' | 'NORMAL' | 'FULL' | 'EXTRA';
|
|
75
|
+
tempStore?: 'DEFAULT' | 'FILE' | 'MEMORY';
|
|
76
|
+
autoVacuumMode?: 'NONE' | 'FULL' | 'INCREMENTAL';
|
|
77
|
+
journalMode?: 'DELETE' | 'TRUNCATE' | 'PERSIST' | 'MEMORY' | 'WAL' | 'OFF';
|
|
45
78
|
}
|
|
46
79
|
export interface SchemaInfo {
|
|
47
80
|
tables: TableInfo[];
|
|
@@ -60,7 +93,7 @@ export interface ColumnInfo {
|
|
|
60
93
|
name: string;
|
|
61
94
|
type: string;
|
|
62
95
|
nullable: boolean;
|
|
63
|
-
defaultValue?:
|
|
96
|
+
defaultValue?: unknown;
|
|
64
97
|
isPrimaryKey: boolean;
|
|
65
98
|
isAutoIncrement: boolean;
|
|
66
99
|
maxLength?: number;
|
|
@@ -111,15 +144,16 @@ export interface EntityType {
|
|
|
111
144
|
selectType: string;
|
|
112
145
|
}
|
|
113
146
|
export interface Repository<T> {
|
|
114
|
-
|
|
147
|
+
objects: any;
|
|
148
|
+
findById(id: string | number): Promise<T | null>;
|
|
115
149
|
findAll(): Promise<T[]>;
|
|
116
150
|
create(data: Partial<T>): Promise<T>;
|
|
117
151
|
update(entity: T): Promise<T>;
|
|
118
|
-
delete(id:
|
|
119
|
-
findWithRelations(id:
|
|
152
|
+
delete(id: string | number): Promise<boolean>;
|
|
153
|
+
findWithRelations(id: string | number, relations: string[]): Promise<T | null>;
|
|
120
154
|
loadRelationships(entities: T[], relations: string[]): Promise<void>;
|
|
121
155
|
count(): Promise<number>;
|
|
122
|
-
exists(id:
|
|
156
|
+
exists(id: string | number): Promise<boolean>;
|
|
123
157
|
paginate(options: {
|
|
124
158
|
page: number;
|
|
125
159
|
limit: number;
|
|
@@ -139,17 +173,48 @@ export interface Repository<T> {
|
|
|
139
173
|
hasPrev: boolean;
|
|
140
174
|
};
|
|
141
175
|
}>;
|
|
142
|
-
withCount(id:
|
|
143
|
-
[key: string]:
|
|
176
|
+
withCount(id: string | number, relationships: string[]): Promise<T & Record<string, number>>;
|
|
177
|
+
[key: string]: unknown;
|
|
144
178
|
}
|
|
145
179
|
export interface SchemaChange {
|
|
146
180
|
type: 'table_added' | 'table_removed' | 'column_added' | 'column_removed' | 'column_modified';
|
|
147
181
|
table: string;
|
|
148
182
|
column?: string;
|
|
149
|
-
details?:
|
|
183
|
+
details?: unknown;
|
|
150
184
|
}
|
|
151
185
|
export interface RefreshResult {
|
|
152
186
|
schemaInfo: SchemaInfo;
|
|
153
187
|
changes: SchemaChange[];
|
|
154
188
|
typesRegenerated: boolean;
|
|
155
189
|
}
|
|
190
|
+
export interface ConnectionPoolConfig {
|
|
191
|
+
min?: number;
|
|
192
|
+
max?: number;
|
|
193
|
+
idleTimeout?: number;
|
|
194
|
+
acquireTimeout?: number;
|
|
195
|
+
}
|
|
196
|
+
export interface QueryCacheConfig {
|
|
197
|
+
enabled: boolean;
|
|
198
|
+
ttl: number;
|
|
199
|
+
maxSize: number;
|
|
200
|
+
}
|
|
201
|
+
export interface BatchConfig {
|
|
202
|
+
maxBatchSize: number;
|
|
203
|
+
batchTimeout: number;
|
|
204
|
+
}
|
|
205
|
+
export interface OptimizationRecommendation {
|
|
206
|
+
type: 'index' | 'query' | 'schema' | 'performance';
|
|
207
|
+
priority: 'low' | 'medium' | 'high';
|
|
208
|
+
description: string;
|
|
209
|
+
suggestion: string;
|
|
210
|
+
estimatedImpact: string;
|
|
211
|
+
}
|
|
212
|
+
export interface BaseRepository<T> {
|
|
213
|
+
findAll(): Promise<T[]>;
|
|
214
|
+
findById(id: string | number): Promise<T | null>;
|
|
215
|
+
create(data: Partial<T>): Promise<T>;
|
|
216
|
+
update(id: string | number, data: Partial<T>): Promise<T>;
|
|
217
|
+
delete(id: string | number): Promise<boolean>;
|
|
218
|
+
count(): Promise<number>;
|
|
219
|
+
}
|
|
220
|
+
export declare function validateNOORMConfig(config: NOORMConfig): void;
|
package/dist/esm/types/index.js
CHANGED
|
@@ -2,4 +2,25 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Core type definitions for NOORMME
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
// Configuration validation function type
|
|
6
|
+
export function validateNOORMConfig(config) {
|
|
7
|
+
if (!config.dialect) {
|
|
8
|
+
throw new Error('Dialect is required');
|
|
9
|
+
}
|
|
10
|
+
if (!config.connection?.database) {
|
|
11
|
+
throw new Error('Database path is required');
|
|
12
|
+
}
|
|
13
|
+
// Validate dialect-specific requirements
|
|
14
|
+
if (config.dialect === 'sqlite') {
|
|
15
|
+
if (!config.connection.database.endsWith('.db') && !config.connection.database.endsWith('.sqlite')) {
|
|
16
|
+
console.warn('SQLite database path should typically end with .db or .sqlite');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Validate performance settings
|
|
20
|
+
if (config.performance?.maxBatchSize && config.performance.maxBatchSize <= 0) {
|
|
21
|
+
throw new Error('maxBatchSize must be greater than 0');
|
|
22
|
+
}
|
|
23
|
+
if (config.performance?.maxCacheSize && config.performance.maxCacheSize <= 0) {
|
|
24
|
+
throw new Error('maxCacheSize must be greater than 0');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -41,6 +41,14 @@ export declare class TypeGenerator {
|
|
|
41
41
|
* Get relationships for a specific table
|
|
42
42
|
*/
|
|
43
43
|
private getRelationshipsForTable;
|
|
44
|
+
/**
|
|
45
|
+
* Generate relationship interfaces
|
|
46
|
+
*/
|
|
47
|
+
private generateRelationshipInterfaces;
|
|
48
|
+
/**
|
|
49
|
+
* Convert string to camelCase
|
|
50
|
+
*/
|
|
51
|
+
private toCamelCase;
|
|
44
52
|
/**
|
|
45
53
|
* Get relationship type name
|
|
46
54
|
*/
|
|
@@ -16,7 +16,7 @@ export class TypeGenerator {
|
|
|
16
16
|
let types = '';
|
|
17
17
|
// Generate entity types for each table
|
|
18
18
|
for (const table of schemaInfo.tables) {
|
|
19
|
-
const entity = this.generateEntityType(table);
|
|
19
|
+
const entity = this.generateEntityType(table, schemaInfo.relationships);
|
|
20
20
|
entities.push(entity);
|
|
21
21
|
interfaces += entity.interface + '\n\n';
|
|
22
22
|
types += entity.insertType + '\n\n';
|
|
@@ -34,11 +34,11 @@ export class TypeGenerator {
|
|
|
34
34
|
/**
|
|
35
35
|
* Generate entity type for a table
|
|
36
36
|
*/
|
|
37
|
-
generateEntityType(table) {
|
|
37
|
+
generateEntityType(table, relationships) {
|
|
38
38
|
const entityName = this.toPascalCase(table.name);
|
|
39
39
|
const tableName = table.name;
|
|
40
40
|
// Generate main entity interface
|
|
41
|
-
const interfaceCode = this.generateEntityInterface(table, entityName);
|
|
41
|
+
const interfaceCode = this.generateEntityInterface(table, entityName, relationships);
|
|
42
42
|
// Generate insert type (all columns except auto-generated ones)
|
|
43
43
|
const insertType = this.generateInsertType(table, entityName);
|
|
44
44
|
// Generate update type (all columns optional except primary key)
|
|
@@ -57,7 +57,7 @@ export class TypeGenerator {
|
|
|
57
57
|
/**
|
|
58
58
|
* Generate main entity interface
|
|
59
59
|
*/
|
|
60
|
-
generateEntityInterface(table, entityName) {
|
|
60
|
+
generateEntityInterface(table, entityName, relationships) {
|
|
61
61
|
let interfaceCode = `export interface ${entityName} {\n`;
|
|
62
62
|
// Add primary key columns first
|
|
63
63
|
for (const column of table.columns) {
|
|
@@ -76,8 +76,8 @@ export class TypeGenerator {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
// Add relationship properties
|
|
79
|
-
const
|
|
80
|
-
for (const rel of
|
|
79
|
+
const tableRelationships = this.getRelationshipsForTable(table.name, relationships);
|
|
80
|
+
for (const rel of tableRelationships) {
|
|
81
81
|
const relType = this.getRelationshipType(rel);
|
|
82
82
|
interfaceCode += ` ${rel.name}?: ${relType}\n`;
|
|
83
83
|
}
|
|
@@ -187,8 +187,8 @@ export class TypeGenerator {
|
|
|
187
187
|
'timestamp': 'Date',
|
|
188
188
|
'timestamptz': 'Date',
|
|
189
189
|
'time': 'Date',
|
|
190
|
-
'json': '
|
|
191
|
-
'jsonb': '
|
|
190
|
+
'json': 'Record<string, unknown>',
|
|
191
|
+
'jsonb': 'Record<string, unknown>',
|
|
192
192
|
'uuid': 'string',
|
|
193
193
|
// MySQL specific types
|
|
194
194
|
'longtext': 'string',
|
|
@@ -200,8 +200,11 @@ export class TypeGenerator {
|
|
|
200
200
|
'double': 'number',
|
|
201
201
|
'bool': 'boolean',
|
|
202
202
|
'datetime': 'Date',
|
|
203
|
-
// SQLite specific types
|
|
203
|
+
// SQLite specific types (enhanced)
|
|
204
204
|
'blob': 'Buffer',
|
|
205
|
+
'int2': 'number',
|
|
206
|
+
'int8': 'number',
|
|
207
|
+
'clob': 'string',
|
|
205
208
|
// MSSQL specific types
|
|
206
209
|
'nvarchar': 'string',
|
|
207
210
|
'nchar': 'string',
|
|
@@ -212,23 +215,89 @@ export class TypeGenerator {
|
|
|
212
215
|
};
|
|
213
216
|
// Try exact match first
|
|
214
217
|
if (typeMapping[column.type.toLowerCase()]) {
|
|
215
|
-
|
|
218
|
+
let mappedType = typeMapping[column.type.toLowerCase()];
|
|
219
|
+
// Handle nullable columns
|
|
220
|
+
if (column.nullable && mappedType !== 'unknown') {
|
|
221
|
+
mappedType = `${mappedType} | null`;
|
|
222
|
+
}
|
|
223
|
+
return mappedType;
|
|
216
224
|
}
|
|
217
225
|
// Handle parameterized types (e.g., varchar(255), decimal(10,2))
|
|
218
226
|
const baseType = column.type.toLowerCase().split('(')[0];
|
|
219
227
|
if (typeMapping[baseType]) {
|
|
220
|
-
|
|
228
|
+
let mappedType = typeMapping[baseType];
|
|
229
|
+
// Handle nullable columns
|
|
230
|
+
if (column.nullable && mappedType !== 'unknown') {
|
|
231
|
+
mappedType = `${mappedType} | null`;
|
|
232
|
+
}
|
|
233
|
+
return mappedType;
|
|
221
234
|
}
|
|
222
|
-
// Default to
|
|
223
|
-
return '
|
|
235
|
+
// Default to unknown for unknown types
|
|
236
|
+
return column.nullable ? 'unknown | null' : 'unknown';
|
|
224
237
|
}
|
|
225
238
|
/**
|
|
226
239
|
* Get relationships for a specific table
|
|
227
240
|
*/
|
|
228
|
-
getRelationshipsForTable(tableName) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
241
|
+
getRelationshipsForTable(tableName, relationships) {
|
|
242
|
+
return relationships.filter(rel => rel.fromTable === tableName || rel.toTable === tableName);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Generate relationship interfaces
|
|
246
|
+
*/
|
|
247
|
+
generateRelationshipInterfaces(relationships) {
|
|
248
|
+
if (relationships.length === 0) {
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
let relationshipInterfaces = '// Relationship Types\n\n';
|
|
252
|
+
// Group relationships by type
|
|
253
|
+
const groupedRelationships = relationships.reduce((acc, rel) => {
|
|
254
|
+
const key = `${rel.fromTable}_${rel.toTable}_${rel.type}`;
|
|
255
|
+
if (!acc[key]) {
|
|
256
|
+
acc[key] = [];
|
|
257
|
+
}
|
|
258
|
+
acc[key].push(rel);
|
|
259
|
+
return acc;
|
|
260
|
+
}, {});
|
|
261
|
+
for (const [key, rels] of Object.entries(groupedRelationships)) {
|
|
262
|
+
const rel = rels[0]; // Use first relationship as template
|
|
263
|
+
const fromEntity = this.toPascalCase(rel.fromTable);
|
|
264
|
+
const toEntity = this.toPascalCase(rel.toTable);
|
|
265
|
+
switch (rel.type) {
|
|
266
|
+
case 'one-to-many':
|
|
267
|
+
relationshipInterfaces += `export interface ${fromEntity}With${toEntity}s extends ${fromEntity} {\n`;
|
|
268
|
+
relationshipInterfaces += ` ${this.toCamelCase(rel.toTable)}s: ${toEntity}[]\n`;
|
|
269
|
+
relationshipInterfaces += `}\n\n`;
|
|
270
|
+
relationshipInterfaces += `export interface ${toEntity}With${fromEntity} extends ${toEntity} {\n`;
|
|
271
|
+
relationshipInterfaces += ` ${this.toCamelCase(rel.fromTable)}: ${fromEntity}\n`;
|
|
272
|
+
relationshipInterfaces += `}\n\n`;
|
|
273
|
+
break;
|
|
274
|
+
case 'many-to-one':
|
|
275
|
+
relationshipInterfaces += `export interface ${fromEntity}With${toEntity} extends ${fromEntity} {\n`;
|
|
276
|
+
relationshipInterfaces += ` ${this.toCamelCase(rel.toTable)}: ${toEntity}\n`;
|
|
277
|
+
relationshipInterfaces += `}\n\n`;
|
|
278
|
+
relationshipInterfaces += `export interface ${toEntity}With${fromEntity}s extends ${toEntity} {\n`;
|
|
279
|
+
relationshipInterfaces += ` ${this.toCamelCase(rel.fromTable)}s: ${fromEntity}[]\n`;
|
|
280
|
+
relationshipInterfaces += `}\n\n`;
|
|
281
|
+
break;
|
|
282
|
+
case 'many-to-many':
|
|
283
|
+
relationshipInterfaces += `export interface ${fromEntity}With${toEntity}s extends ${fromEntity} {\n`;
|
|
284
|
+
relationshipInterfaces += ` ${this.toCamelCase(rel.toTable)}s: ${toEntity}[]\n`;
|
|
285
|
+
relationshipInterfaces += `}\n\n`;
|
|
286
|
+
relationshipInterfaces += `export interface ${toEntity}With${fromEntity}s extends ${toEntity} {\n`;
|
|
287
|
+
relationshipInterfaces += ` ${this.toCamelCase(rel.fromTable)}s: ${fromEntity}[]\n`;
|
|
288
|
+
relationshipInterfaces += `}\n\n`;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return relationshipInterfaces;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Convert string to camelCase
|
|
296
|
+
*/
|
|
297
|
+
toCamelCase(str) {
|
|
298
|
+
return str.replace(/([-_][a-z])/gi, ($1) => {
|
|
299
|
+
return $1.toUpperCase().replace('-', '').replace('_', '');
|
|
300
|
+
});
|
|
232
301
|
}
|
|
233
302
|
/**
|
|
234
303
|
* Get relationship type name
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe SQL helper utilities
|
|
3
|
+
* Provides secure alternatives to dangerous sql.raw() and sql.lit() methods
|
|
4
|
+
*/
|
|
5
|
+
import { RawBuilder } from '../raw-builder/raw-builder.js';
|
|
6
|
+
/**
|
|
7
|
+
* Safely creates a SQL ORDER BY direction clause
|
|
8
|
+
* Only allows 'ASC' or 'DESC'
|
|
9
|
+
*/
|
|
10
|
+
export declare function safeOrderDirection(direction: string): RawBuilder<unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Safely creates a SQL LIMIT clause
|
|
13
|
+
* Validates that the limit is a positive integer
|
|
14
|
+
*/
|
|
15
|
+
export declare function safeLimit(limit: number | string): RawBuilder<unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Safely creates a SQL OFFSET clause
|
|
18
|
+
* Validates that the offset is a non-negative integer
|
|
19
|
+
*/
|
|
20
|
+
export declare function safeOffset(offset: number | string): RawBuilder<unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Safely creates SQL keywords from a whitelist
|
|
23
|
+
* Useful for database-specific syntax that must be hardcoded
|
|
24
|
+
*/
|
|
25
|
+
export declare function safeKeyword(keyword: string, allowedKeywords: string[]): RawBuilder<unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Type-safe enum for common SQL keywords
|
|
28
|
+
*/
|
|
29
|
+
export declare const SafeSQLKeywords: {
|
|
30
|
+
/**
|
|
31
|
+
* Lock modes for SELECT statements
|
|
32
|
+
*/
|
|
33
|
+
readonly LockMode: {
|
|
34
|
+
readonly FOR_UPDATE: "FOR UPDATE";
|
|
35
|
+
readonly FOR_SHARE: "FOR SHARE";
|
|
36
|
+
readonly FOR_NO_KEY_UPDATE: "FOR NO KEY UPDATE";
|
|
37
|
+
readonly FOR_KEY_SHARE: "FOR KEY SHARE";
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Join types
|
|
41
|
+
*/
|
|
42
|
+
readonly JoinType: {
|
|
43
|
+
readonly INNER: "INNER";
|
|
44
|
+
readonly LEFT: "LEFT";
|
|
45
|
+
readonly RIGHT: "RIGHT";
|
|
46
|
+
readonly FULL: "FULL";
|
|
47
|
+
readonly CROSS: "CROSS";
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Set operations
|
|
51
|
+
*/
|
|
52
|
+
readonly SetOperation: {
|
|
53
|
+
readonly UNION: "UNION";
|
|
54
|
+
readonly UNION_ALL: "UNION ALL";
|
|
55
|
+
readonly INTERSECT: "INTERSECT";
|
|
56
|
+
readonly EXCEPT: "EXCEPT";
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* SQLite-specific pragmas (use with extreme caution)
|
|
60
|
+
*/
|
|
61
|
+
readonly SafePragma: {
|
|
62
|
+
readonly OPTIMIZE: "OPTIMIZE";
|
|
63
|
+
readonly WAL_CHECKPOINT: "WAL_CHECKPOINT";
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Safely creates a lock mode clause for SELECT statements
|
|
68
|
+
*/
|
|
69
|
+
export declare function safeLockMode(mode: keyof typeof SafeSQLKeywords.LockMode): RawBuilder<unknown>;
|
|
70
|
+
/**
|
|
71
|
+
* Type-safe ORDER BY builder
|
|
72
|
+
* Validates column names and direction
|
|
73
|
+
*/
|
|
74
|
+
export interface SafeOrderBy {
|
|
75
|
+
column: string;
|
|
76
|
+
direction?: 'ASC' | 'DESC';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Creates a safe ORDER BY clause from validated columns
|
|
80
|
+
*/
|
|
81
|
+
export declare function safeOrderBy(orderBy: SafeOrderBy[], allowedColumns: string[]): RawBuilder<unknown>;
|
|
82
|
+
/**
|
|
83
|
+
* Validates and creates a safe boolean value for SQL
|
|
84
|
+
*/
|
|
85
|
+
export declare function safeBoolean(value: boolean | string | number): RawBuilder<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* Creates a safe CASE statement with validated conditions
|
|
88
|
+
*/
|
|
89
|
+
export interface SafeCaseWhen<T> {
|
|
90
|
+
condition: RawBuilder<boolean>;
|
|
91
|
+
result: T;
|
|
92
|
+
}
|
|
93
|
+
export declare function safeCaseStatement<T>(cases: SafeCaseWhen<T>[], elseResult: T): RawBuilder<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Example usage and best practices
|
|
96
|
+
*/
|
|
97
|
+
export declare const SafeSQLExamples: {
|
|
98
|
+
/**
|
|
99
|
+
* Example: Safe pagination with user input
|
|
100
|
+
*/
|
|
101
|
+
safePagination: () => {
|
|
102
|
+
limit: RawBuilder<unknown>;
|
|
103
|
+
offset: RawBuilder<unknown>;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Example: Safe sorting with user input
|
|
107
|
+
*/
|
|
108
|
+
safeSorting: (userSortColumn: string, userDirection: string) => {
|
|
109
|
+
column: RawBuilder<unknown>;
|
|
110
|
+
direction: RawBuilder<unknown>;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Example: Safe lock mode for transactions
|
|
114
|
+
*/
|
|
115
|
+
safeTransactionLock: () => RawBuilder<unknown>;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Helper to validate that a value is from a specific enum
|
|
119
|
+
*/
|
|
120
|
+
export declare function validateEnum<T extends string>(value: string, enumValues: readonly T[], fieldName?: string): T;
|
|
121
|
+
/**
|
|
122
|
+
* Helper to validate numeric ranges
|
|
123
|
+
*/
|
|
124
|
+
export declare function validateNumericRange(value: number | string, min: number, max: number, fieldName?: string): number;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/// <reference types="./safe-sql-helpers.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* Safe SQL helper utilities
|
|
4
|
+
* Provides secure alternatives to dangerous sql.raw() and sql.lit() methods
|
|
5
|
+
*/
|
|
6
|
+
import { sql } from '../raw-builder/sql.js';
|
|
7
|
+
/**
|
|
8
|
+
* Safely creates a SQL ORDER BY direction clause
|
|
9
|
+
* Only allows 'ASC' or 'DESC'
|
|
10
|
+
*/
|
|
11
|
+
export function safeOrderDirection(direction) {
|
|
12
|
+
const normalized = direction.toUpperCase().trim();
|
|
13
|
+
if (normalized !== 'ASC' && normalized !== 'DESC') {
|
|
14
|
+
throw new Error(`Invalid order direction: "${direction}". Only "ASC" and "DESC" are allowed.`);
|
|
15
|
+
}
|
|
16
|
+
return sql.raw(normalized);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Safely creates a SQL LIMIT clause
|
|
20
|
+
* Validates that the limit is a positive integer
|
|
21
|
+
*/
|
|
22
|
+
export function safeLimit(limit) {
|
|
23
|
+
const numLimit = typeof limit === 'string' ? parseInt(limit, 10) : limit;
|
|
24
|
+
if (!Number.isInteger(numLimit) || numLimit <= 0 || numLimit > 10000) {
|
|
25
|
+
throw new Error(`Invalid limit: "${limit}". Must be a positive integer between 1 and 10000.`);
|
|
26
|
+
}
|
|
27
|
+
return sql.raw(numLimit.toString());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Safely creates a SQL OFFSET clause
|
|
31
|
+
* Validates that the offset is a non-negative integer
|
|
32
|
+
*/
|
|
33
|
+
export function safeOffset(offset) {
|
|
34
|
+
const numOffset = typeof offset === 'string' ? parseInt(offset, 10) : offset;
|
|
35
|
+
if (!Number.isInteger(numOffset) || numOffset < 0 || numOffset > 1000000) {
|
|
36
|
+
throw new Error(`Invalid offset: "${offset}". Must be a non-negative integer between 0 and 1000000.`);
|
|
37
|
+
}
|
|
38
|
+
return sql.raw(numOffset.toString());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Safely creates SQL keywords from a whitelist
|
|
42
|
+
* Useful for database-specific syntax that must be hardcoded
|
|
43
|
+
*/
|
|
44
|
+
export function safeKeyword(keyword, allowedKeywords) {
|
|
45
|
+
const normalized = keyword.toUpperCase().trim();
|
|
46
|
+
const allowedNormalized = allowedKeywords.map(k => k.toUpperCase().trim());
|
|
47
|
+
if (!allowedNormalized.includes(normalized)) {
|
|
48
|
+
throw new Error(`Invalid keyword: "${keyword}". Allowed keywords: ${allowedKeywords.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
return sql.raw(normalized);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Type-safe enum for common SQL keywords
|
|
54
|
+
*/
|
|
55
|
+
export const SafeSQLKeywords = {
|
|
56
|
+
/**
|
|
57
|
+
* Lock modes for SELECT statements
|
|
58
|
+
*/
|
|
59
|
+
LockMode: {
|
|
60
|
+
FOR_UPDATE: 'FOR UPDATE',
|
|
61
|
+
FOR_SHARE: 'FOR SHARE',
|
|
62
|
+
FOR_NO_KEY_UPDATE: 'FOR NO KEY UPDATE',
|
|
63
|
+
FOR_KEY_SHARE: 'FOR KEY SHARE',
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* Join types
|
|
67
|
+
*/
|
|
68
|
+
JoinType: {
|
|
69
|
+
INNER: 'INNER',
|
|
70
|
+
LEFT: 'LEFT',
|
|
71
|
+
RIGHT: 'RIGHT',
|
|
72
|
+
FULL: 'FULL',
|
|
73
|
+
CROSS: 'CROSS',
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Set operations
|
|
77
|
+
*/
|
|
78
|
+
SetOperation: {
|
|
79
|
+
UNION: 'UNION',
|
|
80
|
+
UNION_ALL: 'UNION ALL',
|
|
81
|
+
INTERSECT: 'INTERSECT',
|
|
82
|
+
EXCEPT: 'EXCEPT',
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* SQLite-specific pragmas (use with extreme caution)
|
|
86
|
+
*/
|
|
87
|
+
SafePragma: {
|
|
88
|
+
OPTIMIZE: 'OPTIMIZE',
|
|
89
|
+
WAL_CHECKPOINT: 'WAL_CHECKPOINT',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Safely creates a lock mode clause for SELECT statements
|
|
94
|
+
*/
|
|
95
|
+
export function safeLockMode(mode) {
|
|
96
|
+
return sql.raw(SafeSQLKeywords.LockMode[mode]);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Creates a safe ORDER BY clause from validated columns
|
|
100
|
+
*/
|
|
101
|
+
export function safeOrderBy(orderBy, allowedColumns) {
|
|
102
|
+
if (orderBy.length === 0) {
|
|
103
|
+
throw new Error('At least one order by clause is required');
|
|
104
|
+
}
|
|
105
|
+
const clauses = orderBy.map(({ column, direction = 'ASC' }) => {
|
|
106
|
+
if (!allowedColumns.includes(column)) {
|
|
107
|
+
throw new Error(`Invalid order by column: "${column}". Allowed columns: ${allowedColumns.join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
const dir = direction.toUpperCase();
|
|
110
|
+
if (dir !== 'ASC' && dir !== 'DESC') {
|
|
111
|
+
throw new Error(`Invalid direction: "${direction}". Must be ASC or DESC`);
|
|
112
|
+
}
|
|
113
|
+
return `${column} ${dir}`;
|
|
114
|
+
});
|
|
115
|
+
return sql.raw(clauses.join(', '));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validates and creates a safe boolean value for SQL
|
|
119
|
+
*/
|
|
120
|
+
export function safeBoolean(value) {
|
|
121
|
+
let boolValue;
|
|
122
|
+
if (typeof value === 'boolean') {
|
|
123
|
+
boolValue = value;
|
|
124
|
+
}
|
|
125
|
+
else if (value === 'true' || value === '1' || value === 1) {
|
|
126
|
+
boolValue = true;
|
|
127
|
+
}
|
|
128
|
+
else if (value === 'false' || value === '0' || value === 0) {
|
|
129
|
+
boolValue = false;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw new Error(`Invalid boolean value: "${value}". Must be true, false, 1, 0, "true", or "false"`);
|
|
133
|
+
}
|
|
134
|
+
// Use parameterized value instead of raw SQL
|
|
135
|
+
return sql `${boolValue}`;
|
|
136
|
+
}
|
|
137
|
+
export function safeCaseStatement(cases, elseResult) {
|
|
138
|
+
if (cases.length === 0) {
|
|
139
|
+
throw new Error('At least one WHEN clause is required');
|
|
140
|
+
}
|
|
141
|
+
// Note: This function needs to be refactored to not use sql.raw()
|
|
142
|
+
// For now, we construct the CASE statement carefully
|
|
143
|
+
const whenClauses = cases
|
|
144
|
+
.map((c, i) => {
|
|
145
|
+
// Conditions must be RawBuilder instances to ensure they're safe
|
|
146
|
+
const conditionSql = c.condition.toOperationNode();
|
|
147
|
+
return `WHEN ${conditionSql} THEN ${c.result}`;
|
|
148
|
+
})
|
|
149
|
+
.join(' ');
|
|
150
|
+
return sql.raw(`CASE ${whenClauses} ELSE ${elseResult} END`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Example usage and best practices
|
|
154
|
+
*/
|
|
155
|
+
export const SafeSQLExamples = {
|
|
156
|
+
/**
|
|
157
|
+
* Example: Safe pagination with user input
|
|
158
|
+
*/
|
|
159
|
+
safePagination: () => {
|
|
160
|
+
const userPage = '2'; // From request
|
|
161
|
+
const userLimit = '10'; // From request
|
|
162
|
+
const offset = (parseInt(userPage) - 1) * parseInt(userLimit);
|
|
163
|
+
return {
|
|
164
|
+
limit: safeLimit(userLimit),
|
|
165
|
+
offset: safeOffset(offset),
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Example: Safe sorting with user input
|
|
170
|
+
*/
|
|
171
|
+
safeSorting: (userSortColumn, userDirection) => {
|
|
172
|
+
const allowedColumns = ['name', 'email', 'created_at', 'updated_at'];
|
|
173
|
+
if (!allowedColumns.includes(userSortColumn)) {
|
|
174
|
+
throw new Error('Invalid sort column');
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
column: sql.ref(userSortColumn),
|
|
178
|
+
direction: safeOrderDirection(userDirection),
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* Example: Safe lock mode for transactions
|
|
183
|
+
*/
|
|
184
|
+
safeTransactionLock: () => {
|
|
185
|
+
return safeLockMode('FOR_UPDATE');
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Helper to validate that a value is from a specific enum
|
|
190
|
+
*/
|
|
191
|
+
export function validateEnum(value, enumValues, fieldName = 'value') {
|
|
192
|
+
if (!enumValues.includes(value)) {
|
|
193
|
+
throw new Error(`Invalid ${fieldName}: "${value}". Allowed values: ${enumValues.join(', ')}`);
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Helper to validate numeric ranges
|
|
199
|
+
*/
|
|
200
|
+
export function validateNumericRange(value, min, max, fieldName = 'value') {
|
|
201
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
202
|
+
if (isNaN(num)) {
|
|
203
|
+
throw new Error(`Invalid ${fieldName}: "${value}". Must be a number`);
|
|
204
|
+
}
|
|
205
|
+
if (num < min || num > max) {
|
|
206
|
+
throw new Error(`Invalid ${fieldName}: ${num}. Must be between ${min} and ${max}`);
|
|
207
|
+
}
|
|
208
|
+
return num;
|
|
209
|
+
}
|