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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/// <reference types="./query-parser.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* Query parsing utilities for performance analysis and optimization
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Utility class for parsing and analyzing SQL queries
|
|
7
|
+
*/
|
|
8
|
+
export class QueryParser {
|
|
9
|
+
/**
|
|
10
|
+
* Parse a SQL query into structured components
|
|
11
|
+
*/
|
|
12
|
+
static parseQuery(query) {
|
|
13
|
+
const normalized = this.normalizeQuery(query);
|
|
14
|
+
const lowerQuery = normalized.toLowerCase();
|
|
15
|
+
return {
|
|
16
|
+
type: this.extractQueryType(normalized),
|
|
17
|
+
tables: this.extractTables(normalized),
|
|
18
|
+
whereColumns: this.extractWhereColumns(normalized),
|
|
19
|
+
joinColumns: this.extractJoinColumns(normalized),
|
|
20
|
+
orderByColumns: this.extractOrderByColumns(normalized),
|
|
21
|
+
groupByColumns: this.extractGroupByColumns(normalized),
|
|
22
|
+
hasLimit: lowerQuery.includes('limit'),
|
|
23
|
+
hasAggregates: this.hasAggregates(normalized),
|
|
24
|
+
isComplex: this.isComplexQuery(normalized)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Normalize query for comparison (remove dynamic values)
|
|
29
|
+
*/
|
|
30
|
+
static normalizeQuery(query) {
|
|
31
|
+
return query
|
|
32
|
+
.replace(/\$\d+/g, '?') // Replace PostgreSQL parameters
|
|
33
|
+
.replace(/'[^']*'/g, '?') // Replace string literals
|
|
34
|
+
.replace(/\b\d+\b/g, '?') // Replace numbers
|
|
35
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract query type from SQL statement
|
|
40
|
+
*/
|
|
41
|
+
static extractQueryType(query) {
|
|
42
|
+
const normalized = query.trim().toUpperCase();
|
|
43
|
+
if (normalized.startsWith('SELECT'))
|
|
44
|
+
return 'SELECT';
|
|
45
|
+
if (normalized.startsWith('INSERT'))
|
|
46
|
+
return 'INSERT';
|
|
47
|
+
if (normalized.startsWith('UPDATE'))
|
|
48
|
+
return 'UPDATE';
|
|
49
|
+
if (normalized.startsWith('DELETE'))
|
|
50
|
+
return 'DELETE';
|
|
51
|
+
return 'OTHER';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract table names from query
|
|
55
|
+
*/
|
|
56
|
+
static extractTables(query) {
|
|
57
|
+
const tables = new Set();
|
|
58
|
+
// FROM clause
|
|
59
|
+
const fromMatches = query.match(/from\s+(\w+)/gi);
|
|
60
|
+
if (fromMatches) {
|
|
61
|
+
fromMatches.forEach(match => {
|
|
62
|
+
const table = match.split(/\s+/)[1];
|
|
63
|
+
if (table)
|
|
64
|
+
tables.add(table);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// JOIN clauses
|
|
68
|
+
const joinMatches = query.match(/join\s+(\w+)/gi);
|
|
69
|
+
if (joinMatches) {
|
|
70
|
+
joinMatches.forEach(match => {
|
|
71
|
+
const table = match.split(/\s+/)[1];
|
|
72
|
+
if (table)
|
|
73
|
+
tables.add(table);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return Array.from(tables);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extract columns used in WHERE clauses
|
|
80
|
+
*/
|
|
81
|
+
static extractWhereColumns(query) {
|
|
82
|
+
const whereMatch = query.match(/where\s+(.+?)(?:\s+group\s+by|\s+order\s+by|\s+limit|$)/i);
|
|
83
|
+
if (!whereMatch)
|
|
84
|
+
return [];
|
|
85
|
+
const whereClause = whereMatch[1];
|
|
86
|
+
const columnMatches = whereClause.match(/(\w+)\s*[=<>!]/g);
|
|
87
|
+
return columnMatches
|
|
88
|
+
? columnMatches.map(match => match.split(/\s+/)[0]).filter(Boolean)
|
|
89
|
+
: [];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extract columns used in JOIN conditions
|
|
93
|
+
*/
|
|
94
|
+
static extractJoinColumns(query) {
|
|
95
|
+
const joinMatches = query.match(/join\s+\w+\s+on\s+(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)/gi);
|
|
96
|
+
if (!joinMatches)
|
|
97
|
+
return [];
|
|
98
|
+
const columns = [];
|
|
99
|
+
for (const match of joinMatches) {
|
|
100
|
+
const parts = match.match(/join\s+(\w+)\s+on\s+(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)/i);
|
|
101
|
+
if (parts) {
|
|
102
|
+
columns.push({ table: parts[2], column: parts[3] });
|
|
103
|
+
columns.push({ table: parts[4], column: parts[5] });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return columns;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extract columns used in ORDER BY clauses
|
|
110
|
+
*/
|
|
111
|
+
static extractOrderByColumns(query) {
|
|
112
|
+
const orderByMatch = query.match(/order\s+by\s+(.+?)(?:\s+limit|$)/i);
|
|
113
|
+
if (!orderByMatch)
|
|
114
|
+
return [];
|
|
115
|
+
return orderByMatch[1]
|
|
116
|
+
.split(',')
|
|
117
|
+
.map(col => col.trim().split(/\s+/)[0])
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract columns used in GROUP BY clauses
|
|
122
|
+
*/
|
|
123
|
+
static extractGroupByColumns(query) {
|
|
124
|
+
const groupByMatch = query.match(/group\s+by\s+(.+?)(?:\s+having|\s+order\s+by|\s+limit|$)/i);
|
|
125
|
+
if (!groupByMatch)
|
|
126
|
+
return [];
|
|
127
|
+
return groupByMatch[1]
|
|
128
|
+
.split(',')
|
|
129
|
+
.map(col => col.trim())
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if query contains aggregate functions
|
|
134
|
+
*/
|
|
135
|
+
static hasAggregates(query) {
|
|
136
|
+
const lowerQuery = query.toLowerCase();
|
|
137
|
+
return lowerQuery.includes('group by') ||
|
|
138
|
+
lowerQuery.includes('having') ||
|
|
139
|
+
lowerQuery.includes('count(') ||
|
|
140
|
+
lowerQuery.includes('sum(') ||
|
|
141
|
+
lowerQuery.includes('avg(') ||
|
|
142
|
+
lowerQuery.includes('max(') ||
|
|
143
|
+
lowerQuery.includes('min(');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if query is complex (joins, subqueries, etc.)
|
|
147
|
+
*/
|
|
148
|
+
static isComplexQuery(query) {
|
|
149
|
+
const lowerQuery = query.toLowerCase();
|
|
150
|
+
return lowerQuery.includes('join') ||
|
|
151
|
+
lowerQuery.includes('subquery') ||
|
|
152
|
+
lowerQuery.includes('union') ||
|
|
153
|
+
lowerQuery.includes('except') ||
|
|
154
|
+
lowerQuery.includes('intersect') ||
|
|
155
|
+
(lowerQuery.includes('select') && lowerQuery.includes('('));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if query could return many rows
|
|
159
|
+
*/
|
|
160
|
+
static couldReturnManyRows(query) {
|
|
161
|
+
const lowerQuery = query.toLowerCase();
|
|
162
|
+
return lowerQuery.includes('where') && !lowerQuery.includes('limit');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Extract the primary table from a query
|
|
166
|
+
*/
|
|
167
|
+
static extractPrimaryTable(query) {
|
|
168
|
+
const fromMatch = query.match(/from\s+(\w+)/i);
|
|
169
|
+
return fromMatch ? fromMatch[1] : null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if query is a SELECT statement
|
|
173
|
+
*/
|
|
174
|
+
static isSelectQuery(query) {
|
|
175
|
+
return query.toLowerCase().trim().startsWith('select');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if query is a JOIN statement
|
|
179
|
+
*/
|
|
180
|
+
static isJoinQuery(query) {
|
|
181
|
+
return query.toLowerCase().includes('join');
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if query is an aggregate statement
|
|
185
|
+
*/
|
|
186
|
+
static isAggregateQuery(query) {
|
|
187
|
+
return this.hasAggregates(query);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Generate a cache key for a query
|
|
191
|
+
*/
|
|
192
|
+
static generateCacheKey(query, params = []) {
|
|
193
|
+
const normalized = this.normalizeQuery(query);
|
|
194
|
+
const paramsKey = params.map(p => String(p)).join('|');
|
|
195
|
+
return `${normalized}:${paramsKey}`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check if query should be cached
|
|
199
|
+
*/
|
|
200
|
+
static shouldCache(query, executionTime) {
|
|
201
|
+
// Don't cache very fast queries
|
|
202
|
+
if (executionTime < 10) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
// Don't cache queries that modify data
|
|
206
|
+
const lowerQuery = query.toLowerCase();
|
|
207
|
+
if (lowerQuery.includes('insert') ||
|
|
208
|
+
lowerQuery.includes('update') ||
|
|
209
|
+
lowerQuery.includes('delete') ||
|
|
210
|
+
lowerQuery.includes('drop')) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Extract operation type from query
|
|
217
|
+
*/
|
|
218
|
+
static extractOperation(query) {
|
|
219
|
+
return this.extractQueryType(query);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Analyze query patterns from a list of queries
|
|
223
|
+
*/
|
|
224
|
+
static analyzePatterns(queries) {
|
|
225
|
+
const patternMap = new Map();
|
|
226
|
+
for (const { query, executionTime } of queries) {
|
|
227
|
+
const normalized = this.normalizeQuery(query);
|
|
228
|
+
if (patternMap.has(normalized)) {
|
|
229
|
+
const pattern = patternMap.get(normalized);
|
|
230
|
+
pattern.frequency++;
|
|
231
|
+
pattern.averageTime = (pattern.averageTime + executionTime) / 2;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const parsed = this.parseQuery(query);
|
|
235
|
+
patternMap.set(normalized, {
|
|
236
|
+
normalized,
|
|
237
|
+
original: query,
|
|
238
|
+
frequency: 1,
|
|
239
|
+
averageTime: executionTime,
|
|
240
|
+
tables: parsed.tables
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return Array.from(patternMap.values())
|
|
245
|
+
.sort((a, b) => b.frequency - a.frequency);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Detect potential N+1 query patterns
|
|
249
|
+
*/
|
|
250
|
+
static detectNPlusOnePatterns(queries) {
|
|
251
|
+
const patterns = new Map();
|
|
252
|
+
// Group queries by normalized pattern
|
|
253
|
+
for (const queryInfo of queries) {
|
|
254
|
+
const normalized = this.normalizeQuery(queryInfo.query);
|
|
255
|
+
if (!patterns.has(normalized)) {
|
|
256
|
+
patterns.set(normalized, []);
|
|
257
|
+
}
|
|
258
|
+
patterns.get(normalized).push(queryInfo);
|
|
259
|
+
}
|
|
260
|
+
const nPlusOnePatterns = [];
|
|
261
|
+
// Analyze each pattern for N+1 behavior
|
|
262
|
+
for (const [pattern, queries] of patterns.entries()) {
|
|
263
|
+
if (queries.length < 5)
|
|
264
|
+
continue; // Need at least 5 occurrences
|
|
265
|
+
// Check for clustering in time windows
|
|
266
|
+
const timeWindows = [5000, 10000, 30000]; // 5s, 10s, 30s
|
|
267
|
+
for (const windowMs of timeWindows) {
|
|
268
|
+
let maxCountInWindow = 0;
|
|
269
|
+
for (const query of queries) {
|
|
270
|
+
const windowStart = query.timestamp - windowMs;
|
|
271
|
+
const countInWindow = queries.filter(q => q.timestamp >= windowStart && q.timestamp <= query.timestamp).length;
|
|
272
|
+
maxCountInWindow = Math.max(maxCountInWindow, countInWindow);
|
|
273
|
+
}
|
|
274
|
+
if (maxCountInWindow >= 5) {
|
|
275
|
+
let severity = 'low';
|
|
276
|
+
if (maxCountInWindow >= 20)
|
|
277
|
+
severity = 'high';
|
|
278
|
+
else if (maxCountInWindow >= 10)
|
|
279
|
+
severity = 'medium';
|
|
280
|
+
nPlusOnePatterns.push({
|
|
281
|
+
pattern,
|
|
282
|
+
count: maxCountInWindow,
|
|
283
|
+
timeWindow: windowMs,
|
|
284
|
+
severity
|
|
285
|
+
});
|
|
286
|
+
break; // Only report the most severe pattern
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return nPlusOnePatterns.sort((a, b) => b.count - a.count);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -136,6 +136,9 @@ export interface Sql {
|
|
|
136
136
|
* WARNING! Using this with unchecked inputs WILL lead to SQL injection
|
|
137
137
|
* vulnerabilities. The input is not checked or escaped by Kysely in any way.
|
|
138
138
|
*
|
|
139
|
+
* SECURITY: This method now includes built-in validation to prevent SQL injection.
|
|
140
|
+
* You should still validate that references come from trusted sources or a whitelist.
|
|
141
|
+
*
|
|
139
142
|
* ```ts
|
|
140
143
|
* const columnRef = 'first_name'
|
|
141
144
|
*
|
|
@@ -184,8 +187,9 @@ export interface Sql {
|
|
|
184
187
|
* transformed into parameters. You can use this function to tell
|
|
185
188
|
* Kysely to interpret them as table references instead.
|
|
186
189
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
190
|
+
* SECURITY: This method validates all inputs to prevent SQL injection.
|
|
191
|
+
* You should still ensure table names come from trusted sources or use
|
|
192
|
+
* a whitelist approach for additional safety.
|
|
189
193
|
*
|
|
190
194
|
* ```ts
|
|
191
195
|
* const table = 'person'
|
|
@@ -224,13 +228,13 @@ export interface Sql {
|
|
|
224
228
|
* instead of this whenever possible as they produce a more semantic
|
|
225
229
|
* operation node tree.
|
|
226
230
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
231
|
+
* SECURITY: This method validates all identifiers to prevent SQL injection.
|
|
232
|
+
* Each identifier is checked for dangerous patterns before use.
|
|
229
233
|
*
|
|
230
234
|
* ```ts
|
|
231
235
|
* const indexName = 'person_first_name_index'
|
|
232
236
|
*
|
|
233
|
-
* sql`create index ${indexName} on person`
|
|
237
|
+
* sql`create index ${sql.id(indexName)} on person`
|
|
234
238
|
* ```
|
|
235
239
|
*
|
|
236
240
|
* The generated SQL (PostgreSQL):
|
|
@@ -259,21 +263,33 @@ export interface Sql {
|
|
|
259
263
|
/**
|
|
260
264
|
* This can be used to add literal values to SQL snippets.
|
|
261
265
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* You almost always want to use normal substitutions that get sent to the
|
|
265
|
-
* db as parameters.
|
|
266
|
+
* @deprecated EXTREMELY DANGEROUS - This method bypasses all SQL injection protections.
|
|
267
|
+
* Use parameterized queries instead (normal ${} substitutions).
|
|
266
268
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
+
* ⚠️ CRITICAL SECURITY WARNING: This method is UNSAFE and should ONLY be used with
|
|
270
|
+
* trusted, hardcoded values. It does NOT escape or validate input. Using this with
|
|
271
|
+
* user input WILL lead to SQL injection vulnerabilities.
|
|
269
272
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
273
|
+
* 🚨 SECURITY POLICY: This method is marked as LEGACY and DANGEROUS.
|
|
274
|
+
* - NEVER use with user input or any untrusted data
|
|
275
|
+
* - NEVER use with dynamically constructed strings
|
|
276
|
+
* - ONLY use with compile-time constants if absolutely necessary
|
|
272
277
|
*
|
|
273
|
-
*
|
|
278
|
+
* You almost ALWAYS want to use normal substitutions (parameterized queries)
|
|
279
|
+
* instead of this method. Use this ONLY when something absolutely cannot be
|
|
280
|
+
* sent as a parameter.
|
|
274
281
|
*
|
|
275
|
-
* ```
|
|
276
|
-
*
|
|
282
|
+
* ```ts
|
|
283
|
+
* // ❌ NEVER do this with user input:
|
|
284
|
+
* const userInput = req.body.value
|
|
285
|
+
* sql`select * from person where name = ${sql.lit(userInput)}` // VULNERABLE!
|
|
286
|
+
*
|
|
287
|
+
* // ✅ Use parameterized queries instead:
|
|
288
|
+
* const userInput = req.body.value
|
|
289
|
+
* sql`select * from person where name = ${userInput}` // Safe - parameterized
|
|
290
|
+
*
|
|
291
|
+
* // ✅ Only use lit() for hardcoded constants:
|
|
292
|
+
* sql`select * from person where status = ${sql.lit('active')}` // Safe
|
|
277
293
|
* ```
|
|
278
294
|
*
|
|
279
295
|
* As you can see from the example above, the value was added directly to
|
|
@@ -288,24 +304,55 @@ export interface Sql {
|
|
|
288
304
|
/**
|
|
289
305
|
* This can be used to add arbitrary runtime SQL to SQL snippets.
|
|
290
306
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
307
|
+
* @deprecated CATASTROPHICALLY DANGEROUS - This method completely bypasses all security.
|
|
308
|
+
* Use safe alternatives from 'noormme/util/safe-sql-helpers' instead.
|
|
293
309
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
310
|
+
* 🚨 CRITICAL SECURITY WARNING: This method is EXTREMELY UNSAFE and should NEVER be
|
|
311
|
+
* used with any user input. It does NOT escape, validate, or sanitize input.
|
|
312
|
+
* The input is directly concatenated into the SQL string.
|
|
296
313
|
*
|
|
297
|
-
*
|
|
298
|
-
* ```
|
|
314
|
+
* Using this with user input WILL lead to SQL injection vulnerabilities.
|
|
299
315
|
*
|
|
300
|
-
*
|
|
316
|
+
* 🚨 SECURITY POLICY: This method is marked as LEGACY and EXTREMELY DANGEROUS.
|
|
317
|
+
* - NEVER use with user input or any untrusted data
|
|
318
|
+
* - NEVER use with data from external sources (requests, files, etc.)
|
|
319
|
+
* - NEVER use with dynamically constructed strings
|
|
320
|
+
* - Use safe alternatives instead: safeOrderDirection(), safeLimit(), etc.
|
|
301
321
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
322
|
+
* This method should ONLY be used for:
|
|
323
|
+
* - Hardcoded SQL fragments (compile-time constants only)
|
|
324
|
+
* - SQL generated by other trusted parts of your application
|
|
325
|
+
* - Database-specific syntax that cannot be expressed otherwise
|
|
326
|
+
*
|
|
327
|
+
* ✅ RECOMMENDED ALTERNATIVES:
|
|
328
|
+
* - For order direction: use safeOrderDirection() from 'noormme/util/safe-sql-helpers'
|
|
329
|
+
* - For limits/offsets: use safeLimit() or safeOffset()
|
|
330
|
+
* - For keywords: use safeKeyword() with a whitelist
|
|
331
|
+
* - For identifiers: use sql.ref(), sql.table(), or sql.id() (they're validated)
|
|
332
|
+
*
|
|
333
|
+
* ```ts
|
|
334
|
+
* // ❌ NEVER EVER do this:
|
|
335
|
+
* const userInput = req.body.sql
|
|
336
|
+
* sql`${sql.raw(userInput)}` // CATASTROPHIC VULNERABILITY!
|
|
337
|
+
*
|
|
338
|
+
* // ✅ Use safe alternatives instead:
|
|
339
|
+
* import { safeOrderDirection } from 'noormme/util/safe-sql-helpers'
|
|
340
|
+
* const direction = req.query.dir
|
|
341
|
+
* sql`select * from person order by name ${safeOrderDirection(direction)}`
|
|
342
|
+
*
|
|
343
|
+
* // ✅ Only use raw() with hardcoded SQL:
|
|
344
|
+
* sql`select * from person ${sql.raw('FOR UPDATE')}` // Safe - hardcoded
|
|
304
345
|
* ```
|
|
305
346
|
*
|
|
306
347
|
* Note that the difference to `sql.lit` is that this function
|
|
307
348
|
* doesn't assume the inputs are values. The input to this function
|
|
308
|
-
* can be any
|
|
349
|
+
* can be any SQL and it's simply glued to the parent string as-is.
|
|
350
|
+
*
|
|
351
|
+
* If you're considering using this method, ask yourself:
|
|
352
|
+
* - Can I use a parameterized query instead?
|
|
353
|
+
* - Can I use sql.ref(), sql.table(), or sql.id() instead? (They're validated!)
|
|
354
|
+
* - Can I use a safe helper from 'noormme/util/safe-sql-helpers'?
|
|
355
|
+
* - Is this input from a 100% trusted, hardcoded source?
|
|
309
356
|
*/
|
|
310
357
|
raw<R = unknown>(anySql: string): RawBuilder<R>;
|
|
311
358
|
/**
|
|
@@ -8,6 +8,7 @@ import { parseTable } from '../parser/table-parser.js';
|
|
|
8
8
|
import { parseValueExpression } from '../parser/value-parser.js';
|
|
9
9
|
import { createQueryId } from '../util/query-id.js';
|
|
10
10
|
import { createRawBuilder } from './raw-builder.js';
|
|
11
|
+
import { validateColumnReference, validateTableReference, validateIdentifier } from '../util/security-validator.js';
|
|
11
12
|
export const sql = Object.assign((sqlFragments, ...parameters) => {
|
|
12
13
|
return createRawBuilder({
|
|
13
14
|
queryId: createQueryId(),
|
|
@@ -15,6 +16,8 @@ export const sql = Object.assign((sqlFragments, ...parameters) => {
|
|
|
15
16
|
});
|
|
16
17
|
}, {
|
|
17
18
|
ref(columnReference) {
|
|
19
|
+
// Security validation to prevent SQL injection
|
|
20
|
+
validateColumnReference(columnReference);
|
|
18
21
|
return createRawBuilder({
|
|
19
22
|
queryId: createQueryId(),
|
|
20
23
|
rawNode: RawNode.createWithChild(parseStringReference(columnReference)),
|
|
@@ -30,12 +33,18 @@ export const sql = Object.assign((sqlFragments, ...parameters) => {
|
|
|
30
33
|
return this.val(value);
|
|
31
34
|
},
|
|
32
35
|
table(tableReference) {
|
|
36
|
+
// Security validation to prevent SQL injection
|
|
37
|
+
validateTableReference(tableReference);
|
|
33
38
|
return createRawBuilder({
|
|
34
39
|
queryId: createQueryId(),
|
|
35
40
|
rawNode: RawNode.createWithChild(parseTable(tableReference)),
|
|
36
41
|
});
|
|
37
42
|
},
|
|
38
43
|
id(...ids) {
|
|
44
|
+
// Security validation for each identifier
|
|
45
|
+
ids.forEach((id, index) => {
|
|
46
|
+
validateIdentifier(id, `identifier[${index}]`);
|
|
47
|
+
});
|
|
39
48
|
const fragments = new Array(ids.length + 1).fill('.');
|
|
40
49
|
fragments[0] = '';
|
|
41
50
|
fragments[fragments.length - 1] = '';
|
|
@@ -1,54 +1,22 @@
|
|
|
1
1
|
import type { Kysely } from '../kysely.js';
|
|
2
|
-
import { TableInfo, RelationshipInfo
|
|
2
|
+
import type { Repository, TableInfo, RelationshipInfo } from '../types/index.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Simple repository factory for creating table repositories
|
|
5
5
|
*/
|
|
6
6
|
export declare class RepositoryFactory {
|
|
7
7
|
private db;
|
|
8
|
-
private
|
|
9
|
-
constructor(db: Kysely<any>,
|
|
8
|
+
private performanceConfig?;
|
|
9
|
+
constructor(db: Kysely<any>, performanceConfig?: any | undefined);
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*/
|
|
13
|
-
createRepository<T>(table: TableInfo, relationships: RelationshipInfo[]): Repository<T>;
|
|
14
|
-
/**
|
|
15
|
-
* Add custom finder methods to repository
|
|
16
|
-
*/
|
|
17
|
-
private addCustomFinders;
|
|
18
|
-
/**
|
|
19
|
-
* Load relationships for entities
|
|
20
|
-
*/
|
|
21
|
-
private loadRelationships;
|
|
22
|
-
/**
|
|
23
|
-
* Load relationships for a single entity
|
|
24
|
-
*/
|
|
25
|
-
private loadEntityRelationships;
|
|
26
|
-
/**
|
|
27
|
-
* Batch load relationships for multiple entities
|
|
11
|
+
* Transform boolean columns from SQLite integers (0/1) to JavaScript booleans
|
|
28
12
|
*/
|
|
29
|
-
private
|
|
13
|
+
private transformBooleans;
|
|
30
14
|
/**
|
|
31
|
-
*
|
|
32
|
-
*/
|
|
33
|
-
private loadSingleRelationship;
|
|
34
|
-
/**
|
|
35
|
-
* Batch load a single relationship for multiple entities
|
|
36
|
-
*/
|
|
37
|
-
private batchLoadSingleRelationship;
|
|
38
|
-
/**
|
|
39
|
-
* Get primary key condition for queries
|
|
40
|
-
*/
|
|
41
|
-
private getPrimaryKeyCondition;
|
|
42
|
-
/**
|
|
43
|
-
* Get primary key value from entity
|
|
44
|
-
*/
|
|
45
|
-
private getPrimaryKeyValue;
|
|
46
|
-
/**
|
|
47
|
-
* Get update data (exclude primary key)
|
|
15
|
+
* Create a repository for the specified table
|
|
48
16
|
*/
|
|
49
|
-
|
|
17
|
+
createRepository<T>(table: TableInfo, relationships: RelationshipInfo[]): Repository<T>;
|
|
50
18
|
/**
|
|
51
|
-
*
|
|
19
|
+
* Wrap repository with Proxy to handle dynamic method calls
|
|
52
20
|
*/
|
|
53
|
-
private
|
|
21
|
+
private wrapWithDynamicMethods;
|
|
54
22
|
}
|