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.
Files changed (137) hide show
  1. package/LICENSE +89 -21
  2. package/README.md +195 -578
  3. package/dist/cjs/cli/commands/analyze.js +4 -1
  4. package/dist/cjs/cli/commands/generate.js +48 -8
  5. package/dist/cjs/cli/commands/init.js +54 -14
  6. package/dist/cjs/cli/commands/inspect.js +10 -3
  7. package/dist/cjs/cli/commands/migrate.js +38 -2
  8. package/dist/cjs/cli/commands/optimize.js +4 -1
  9. package/dist/cjs/cli/commands/status.js +41 -5
  10. package/dist/cjs/cli/commands/watch.js +4 -1
  11. package/dist/cjs/cli/index.js +4 -1
  12. package/dist/cjs/dialect/database-introspector.js +16 -21
  13. package/dist/cjs/dialect/sqlite/sqlite-introspector.js +13 -24
  14. package/dist/cjs/dynamic/dynamic.d.ts +20 -0
  15. package/dist/cjs/dynamic/dynamic.js +25 -0
  16. package/dist/cjs/edge-runtime/edge-config.d.ts +125 -0
  17. package/dist/cjs/edge-runtime/edge-config.js +323 -0
  18. package/dist/cjs/errors/NoormError.d.ts +27 -4
  19. package/dist/cjs/errors/NoormError.js +142 -21
  20. package/dist/cjs/logging/logger.d.ts +7 -2
  21. package/dist/cjs/logging/logger.js +21 -7
  22. package/dist/cjs/noormme.d.ts +12 -8
  23. package/dist/cjs/noormme.js +133 -61
  24. package/dist/cjs/operation-node/column-node.js +4 -0
  25. package/dist/cjs/operation-node/identifier-node.js +4 -0
  26. package/dist/cjs/operation-node/table-node.js +7 -0
  27. package/dist/cjs/parser/reference-parser.js +5 -0
  28. package/dist/cjs/parser/table-parser.js +2 -0
  29. package/dist/cjs/performance/index.d.ts +44 -0
  30. package/dist/cjs/performance/index.js +64 -0
  31. package/dist/cjs/performance/query-optimizer.d.ts +134 -0
  32. package/dist/cjs/performance/query-optimizer.js +391 -0
  33. package/dist/cjs/performance/services/cache-service.d.ts +177 -0
  34. package/dist/cjs/performance/services/cache-service.js +415 -0
  35. package/dist/cjs/performance/services/connection-factory.d.ts +198 -0
  36. package/dist/cjs/performance/services/connection-factory.js +498 -0
  37. package/dist/cjs/performance/services/metrics-collector.d.ts +162 -0
  38. package/dist/cjs/performance/services/metrics-collector.js +406 -0
  39. package/dist/cjs/performance/utils/query-parser.d.ts +123 -0
  40. package/dist/cjs/performance/utils/query-parser.js +295 -0
  41. package/dist/cjs/raw-builder/sql.d.ts +73 -26
  42. package/dist/cjs/raw-builder/sql.js +9 -0
  43. package/dist/cjs/repository/repository-factory.d.ts +10 -42
  44. package/dist/cjs/repository/repository-factory.js +276 -394
  45. package/dist/cjs/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
  46. package/dist/cjs/schema/core/factories/discovery-factory.js +5 -5
  47. package/dist/cjs/schema/core/utils/name-generator.js +34 -2
  48. package/dist/cjs/schema/core/utils/type-mapper.d.ts +19 -14
  49. package/dist/cjs/schema/core/utils/type-mapper.js +4 -7
  50. package/dist/cjs/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
  51. package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
  52. package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
  53. package/dist/cjs/schema/test/dialect-capabilities.test.js +6 -0
  54. package/dist/cjs/schema/test/error-handling.test.js +52 -33
  55. package/dist/cjs/schema/test/integration.test.js +51 -5
  56. package/dist/cjs/schema/test/sqlite-discovery-coordinator.test.js +8 -4
  57. package/dist/cjs/sqlite-migration/index.js +35 -2
  58. package/dist/cjs/testing/test-utils.d.ts +5 -0
  59. package/dist/cjs/testing/test-utils.js +66 -6
  60. package/dist/cjs/types/index.d.ts +78 -13
  61. package/dist/cjs/types/index.js +23 -0
  62. package/dist/cjs/types/type-generator.d.ts +8 -0
  63. package/dist/cjs/types/type-generator.js +86 -17
  64. package/dist/cjs/util/safe-sql-helpers.d.ts +124 -0
  65. package/dist/cjs/util/safe-sql-helpers.js +221 -0
  66. package/dist/cjs/util/security-validator.d.ts +44 -0
  67. package/dist/cjs/util/security-validator.js +256 -0
  68. package/dist/cjs/util/security.d.ts +60 -0
  69. package/dist/cjs/util/security.js +137 -0
  70. package/dist/cjs/watch/schema-watcher.js +26 -7
  71. package/dist/esm/cli/commands/generate.js +10 -6
  72. package/dist/esm/cli/commands/init.js +15 -11
  73. package/dist/esm/cli/commands/inspect.js +6 -2
  74. package/dist/esm/cli/commands/status.js +3 -3
  75. package/dist/esm/dialect/database-introspector.js +16 -21
  76. package/dist/esm/dialect/sqlite/sqlite-introspector.js +13 -24
  77. package/dist/esm/dynamic/dynamic.d.ts +20 -0
  78. package/dist/esm/dynamic/dynamic.js +25 -0
  79. package/dist/esm/edge-runtime/edge-config.d.ts +125 -0
  80. package/dist/esm/edge-runtime/edge-config.js +281 -0
  81. package/dist/esm/errors/NoormError.d.ts +27 -4
  82. package/dist/esm/errors/NoormError.js +134 -18
  83. package/dist/esm/logging/logger.d.ts +7 -2
  84. package/dist/esm/logging/logger.js +21 -7
  85. package/dist/esm/noormme.d.ts +12 -8
  86. package/dist/esm/noormme.js +130 -61
  87. package/dist/esm/operation-node/column-node.js +4 -0
  88. package/dist/esm/operation-node/identifier-node.js +4 -0
  89. package/dist/esm/operation-node/table-node.js +7 -0
  90. package/dist/esm/parser/reference-parser.js +5 -0
  91. package/dist/esm/parser/table-parser.js +2 -0
  92. package/dist/esm/performance/index.d.ts +44 -0
  93. package/dist/esm/performance/index.js +48 -0
  94. package/dist/esm/performance/query-optimizer.d.ts +134 -0
  95. package/dist/esm/performance/query-optimizer.js +387 -0
  96. package/dist/esm/performance/services/cache-service.d.ts +177 -0
  97. package/dist/esm/performance/services/cache-service.js +410 -0
  98. package/dist/esm/performance/services/connection-factory.d.ts +198 -0
  99. package/dist/esm/performance/services/connection-factory.js +493 -0
  100. package/dist/esm/performance/services/metrics-collector.d.ts +162 -0
  101. package/dist/esm/performance/services/metrics-collector.js +402 -0
  102. package/dist/esm/performance/utils/query-parser.d.ts +123 -0
  103. package/dist/esm/performance/utils/query-parser.js +292 -0
  104. package/dist/esm/raw-builder/sql.d.ts +73 -26
  105. package/dist/esm/raw-builder/sql.js +9 -0
  106. package/dist/esm/repository/repository-factory.d.ts +10 -42
  107. package/dist/esm/repository/repository-factory.js +277 -395
  108. package/dist/esm/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
  109. package/dist/esm/schema/core/factories/discovery-factory.js +5 -5
  110. package/dist/esm/schema/core/utils/name-generator.js +34 -2
  111. package/dist/esm/schema/core/utils/type-mapper.d.ts +19 -14
  112. package/dist/esm/schema/core/utils/type-mapper.js +4 -7
  113. package/dist/esm/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
  114. package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
  115. package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
  116. package/dist/esm/schema/test/dialect-capabilities.test.js +6 -0
  117. package/dist/esm/schema/test/error-handling.test.js +52 -33
  118. package/dist/esm/schema/test/integration.test.js +18 -5
  119. package/dist/esm/schema/test/sqlite-discovery-coordinator.test.js +8 -4
  120. package/dist/esm/testing/test-utils.d.ts +5 -0
  121. package/dist/esm/testing/test-utils.js +66 -6
  122. package/dist/esm/types/index.d.ts +78 -13
  123. package/dist/esm/types/index.js +22 -1
  124. package/dist/esm/types/type-generator.d.ts +8 -0
  125. package/dist/esm/types/type-generator.js +86 -17
  126. package/dist/esm/util/safe-sql-helpers.d.ts +124 -0
  127. package/dist/esm/util/safe-sql-helpers.js +209 -0
  128. package/dist/esm/util/security-validator.d.ts +44 -0
  129. package/dist/esm/util/security-validator.js +246 -0
  130. package/dist/esm/util/security.d.ts +60 -0
  131. package/dist/esm/util/security.js +114 -0
  132. package/dist/esm/watch/schema-watcher.js +26 -7
  133. package/package.json +1 -1
  134. package/dist/cjs/performance/query-analyzer.d.ts +0 -89
  135. package/dist/cjs/performance/query-analyzer.js +0 -263
  136. package/dist/esm/performance/query-analyzer.d.ts +0 -89
  137. 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
- * WARNING! Using this with unchecked inputs WILL lead to SQL injection
188
- * vulnerabilities. The input is not checked or escaped by Kysely in any way.
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
- * WARNING! Using this with unchecked inputs WILL lead to SQL injection
228
- * vulnerabilities. The input is not checked or escaped by Kysely in any way.
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
- * WARNING! Using this with unchecked inputs WILL lead to SQL injection
263
- * vulnerabilities. The input is not checked or escaped by Kysely in any way.
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
- * ```ts
268
- * const firstName = 'first_name'
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
- * sql`select * from person where first_name = ${sql.lit(firstName)}`
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
- * The generated SQL (PostgreSQL):
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
- * ```sql
276
- * select * from person where first_name = 'first_name'
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
- * WARNING! Using this with unchecked inputs WILL lead to SQL injection
292
- * vulnerabilities. The input is not checked or escaped by Kysely in any way.
307
+ * @deprecated CATASTROPHICALLY DANGEROUS - This method completely bypasses all security.
308
+ * Use safe alternatives from 'noormme/util/safe-sql-helpers' instead.
293
309
  *
294
- * ```ts
295
- * const firstName = "'first_name'"
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
- * sql`select * from person where first_name = ${sql.raw(firstName)}`
298
- * ```
314
+ * Using this with user input WILL lead to SQL injection vulnerabilities.
299
315
  *
300
- * The generated SQL (PostgreSQL):
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
- * ```sql
303
- * select * from person where first_name = 'first_name'
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 sql and it's simply glued to the parent string as-is.
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, Repository, PerformanceConfig } from '../types';
2
+ import type { Repository, TableInfo, RelationshipInfo } from '../types/index.js';
3
3
  /**
4
- * Repository factory that creates dynamic repository classes
4
+ * Simple repository factory for creating table repositories
5
5
  */
6
6
  export declare class RepositoryFactory {
7
7
  private db;
8
- private config;
9
- constructor(db: Kysely<any>, config?: PerformanceConfig);
8
+ private performanceConfig?;
9
+ constructor(db: Kysely<any>, performanceConfig?: any | undefined);
10
10
  /**
11
- * Create a repository for the specified table
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 batchLoadRelationships;
13
+ private transformBooleans;
30
14
  /**
31
- * Load a single relationship for an entity
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
- private getUpdateData;
17
+ createRepository<T>(table: TableInfo, relationships: RelationshipInfo[]): Repository<T>;
50
18
  /**
51
- * Convert string to PascalCase
19
+ * Wrap repository with Proxy to handle dynamic method calls
52
20
  */
53
- private toPascalCase;
21
+ private wrapWithDynamicMethods;
54
22
  }