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,44 @@
1
+ /**
2
+ * Security validation utilities for NOORMME
3
+ * Provides input validation and sanitization to prevent injection attacks
4
+ */
5
+ /**
6
+ * Validates identifier names (table names, column names, schema names)
7
+ * to prevent SQL injection through dynamic identifiers
8
+ */
9
+ export declare function validateIdentifier(identifier: string, context?: string): void;
10
+ /**
11
+ * Validates table reference for dynamic table operations
12
+ */
13
+ export declare function validateTableReference(tableRef: string): void;
14
+ /**
15
+ * Validates column reference for dynamic column operations
16
+ */
17
+ export declare function validateColumnReference(columnRef: string): void;
18
+ /**
19
+ * Validates file paths to prevent path traversal attacks
20
+ */
21
+ export declare function validateFilePath(filePath: string, allowedExtensions?: string[]): void;
22
+ /**
23
+ * Sanitizes database path for CLI commands
24
+ */
25
+ export declare function sanitizeDatabasePath(dbPath: string): string;
26
+ /**
27
+ * Validates output directory for code generation
28
+ */
29
+ export declare function validateOutputDirectory(dirPath: string): void;
30
+ /**
31
+ * Validates migration name
32
+ */
33
+ export declare function validateMigrationName(name: string): void;
34
+ /**
35
+ * Rate limiting for security-sensitive operations
36
+ */
37
+ export declare class RateLimiter {
38
+ private maxAttempts;
39
+ private windowMs;
40
+ private attempts;
41
+ constructor(maxAttempts?: number, windowMs?: number);
42
+ checkLimit(key: string): void;
43
+ private cleanup;
44
+ }
@@ -0,0 +1,246 @@
1
+ /// <reference types="./security-validator.d.ts" />
2
+ /**
3
+ * Security validation utilities for NOORMME
4
+ * Provides input validation and sanitization to prevent injection attacks
5
+ */
6
+ /**
7
+ * Validates identifier names (table names, column names, schema names)
8
+ * to prevent SQL injection through dynamic identifiers
9
+ */
10
+ export function validateIdentifier(identifier, context = 'identifier') {
11
+ if (typeof identifier !== 'string') {
12
+ throw new Error(`${context} must be a string`);
13
+ }
14
+ if (identifier.length === 0) {
15
+ throw new Error(`${context} cannot be empty`);
16
+ }
17
+ if (identifier.length > 255) {
18
+ throw new Error(`${context} exceeds maximum length of 255 characters`);
19
+ }
20
+ // Check for SQL injection patterns
21
+ const dangerousPatterns = [
22
+ /;/, // SQL statement separator
23
+ /--/, // SQL comment
24
+ /\/\*/, // Multi-line comment start
25
+ /\*\//, // Multi-line comment end
26
+ /\bUNION\b/i, // UNION injection
27
+ /\bSELECT\b.*\bFROM\b/i, // SELECT injection
28
+ /\bINSERT\b.*\bINTO\b/i, // INSERT injection
29
+ /\bUPDATE\b.*\bSET\b/i, // UPDATE injection
30
+ /\bDELETE\b.*\bFROM\b/i, // DELETE injection
31
+ /\bDROP\b/i, // DROP injection
32
+ /\bEXEC\b/i, // EXEC injection
33
+ /\bEXECUTE\b/i, // EXECUTE injection
34
+ /\bCREATE\b/i, // CREATE injection
35
+ /\bALTER\b/i, // ALTER injection
36
+ /\bTRUNCATE\b/i, // TRUNCATE injection
37
+ /['"`]/, // Quote characters
38
+ /\\/, // Backslash escape
39
+ /\x00/, // Null byte
40
+ ];
41
+ for (const pattern of dangerousPatterns) {
42
+ if (pattern.test(identifier)) {
43
+ throw new Error(`Invalid ${context}: "${identifier}" contains potentially dangerous characters or SQL keywords. ` +
44
+ `Identifiers must contain only alphanumeric characters, underscores, and dots (for schema.table references).`);
45
+ }
46
+ }
47
+ // Validate format: alphanumeric, underscore, and dots for schema.table.column
48
+ const validIdentifierPattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
49
+ if (!validIdentifierPattern.test(identifier)) {
50
+ throw new Error(`Invalid ${context}: "${identifier}". ` +
51
+ `Identifiers must start with a letter or underscore, followed by alphanumeric characters or underscores. ` +
52
+ `Schema/table references can use dots (e.g., "schema.table" or "table.column").`);
53
+ }
54
+ // Additional check: prevent reserved SQLite keywords that could be dangerous
55
+ const reservedKeywords = [
56
+ 'PRAGMA', 'ATTACH', 'DETACH', 'VACUUM', 'ANALYZE', 'REINDEX'
57
+ ];
58
+ for (const keyword of reservedKeywords) {
59
+ if (identifier.toUpperCase() === keyword) {
60
+ throw new Error(`Invalid ${context}: "${identifier}" is a reserved SQLite keyword and cannot be used as an identifier`);
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * Validates table reference for dynamic table operations
66
+ */
67
+ export function validateTableReference(tableRef) {
68
+ validateIdentifier(tableRef, 'table reference');
69
+ // Additional validation for table references
70
+ const parts = tableRef.split('.');
71
+ if (parts.length > 2) {
72
+ throw new Error(`Invalid table reference: "${tableRef}". ` +
73
+ `Table references can have at most 2 parts (schema.table).`);
74
+ }
75
+ // Validate each part
76
+ parts.forEach((part, index) => {
77
+ const context = index === 0 ? (parts.length === 2 ? 'schema name' : 'table name') : 'table name';
78
+ validateIdentifier(part, context);
79
+ });
80
+ }
81
+ /**
82
+ * Validates column reference for dynamic column operations
83
+ */
84
+ export function validateColumnReference(columnRef) {
85
+ validateIdentifier(columnRef, 'column reference');
86
+ // Column references can have: column, table.column, or schema.table.column
87
+ const parts = columnRef.split('.');
88
+ if (parts.length > 3) {
89
+ throw new Error(`Invalid column reference: "${columnRef}". ` +
90
+ `Column references can have at most 3 parts (schema.table.column).`);
91
+ }
92
+ // Validate each part
93
+ parts.forEach((part, index) => {
94
+ let context;
95
+ if (parts.length === 1) {
96
+ context = 'column name';
97
+ }
98
+ else if (parts.length === 2) {
99
+ context = index === 0 ? 'table name' : 'column name';
100
+ }
101
+ else {
102
+ context = index === 0 ? 'schema name' : (index === 1 ? 'table name' : 'column name');
103
+ }
104
+ validateIdentifier(part, context);
105
+ });
106
+ }
107
+ /**
108
+ * Validates file paths to prevent path traversal attacks
109
+ */
110
+ export function validateFilePath(filePath, allowedExtensions) {
111
+ if (typeof filePath !== 'string') {
112
+ throw new Error('File path must be a string');
113
+ }
114
+ if (filePath.length === 0) {
115
+ throw new Error('File path cannot be empty');
116
+ }
117
+ // Check for path traversal patterns
118
+ const pathTraversalPatterns = [
119
+ /\.\./, // Parent directory reference
120
+ /~\//, // Home directory reference
121
+ /^\/+/, // Absolute path (starts with /)
122
+ /^[a-zA-Z]:\\/, // Windows absolute path (C:\)
123
+ /\\/, // Backslash (Windows path separator)
124
+ /\x00/, // Null byte injection
125
+ ];
126
+ for (const pattern of pathTraversalPatterns) {
127
+ if (pattern.test(filePath)) {
128
+ throw new Error(`Invalid file path: "${filePath}" contains path traversal or absolute path patterns. ` +
129
+ `Only relative paths within the current directory are allowed.`);
130
+ }
131
+ }
132
+ // Validate file extension if specified
133
+ if (allowedExtensions && allowedExtensions.length > 0) {
134
+ const hasValidExtension = allowedExtensions.some(ext => filePath.toLowerCase().endsWith(ext.toLowerCase()));
135
+ if (!hasValidExtension) {
136
+ throw new Error(`Invalid file extension for "${filePath}". ` +
137
+ `Allowed extensions: ${allowedExtensions.join(', ')}`);
138
+ }
139
+ }
140
+ // Check for suspicious file names
141
+ const suspiciousNames = [
142
+ /^\.+$/, // Only dots
143
+ /^\s*$/, // Only whitespace
144
+ /[<>:"|?*]/, // Windows forbidden characters
145
+ ];
146
+ for (const pattern of suspiciousNames) {
147
+ if (pattern.test(filePath)) {
148
+ throw new Error(`Invalid file path: "${filePath}" contains forbidden characters or is malformed`);
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * Sanitizes database path for CLI commands
154
+ */
155
+ export function sanitizeDatabasePath(dbPath) {
156
+ validateFilePath(dbPath, ['.db', '.sqlite', '.sqlite3', '.db3']);
157
+ // Ensure the path doesn't escape current directory
158
+ if (dbPath.includes('..') || dbPath.startsWith('/') || /^[a-zA-Z]:/.test(dbPath)) {
159
+ throw new Error(`Database path "${dbPath}" must be a relative path within the current directory`);
160
+ }
161
+ return dbPath;
162
+ }
163
+ /**
164
+ * Validates output directory for code generation
165
+ */
166
+ export function validateOutputDirectory(dirPath) {
167
+ if (typeof dirPath !== 'string') {
168
+ throw new Error('Output directory must be a string');
169
+ }
170
+ if (dirPath.length === 0) {
171
+ throw new Error('Output directory cannot be empty');
172
+ }
173
+ // Prevent path traversal
174
+ if (dirPath.includes('..')) {
175
+ throw new Error(`Output directory "${dirPath}" cannot contain parent directory references (..)`);
176
+ }
177
+ // Prevent absolute paths
178
+ if (dirPath.startsWith('/') || /^[a-zA-Z]:/.test(dirPath)) {
179
+ throw new Error(`Output directory "${dirPath}" must be a relative path`);
180
+ }
181
+ // Validate format
182
+ const validDirPattern = /^[a-zA-Z0-9_\-./]+$/;
183
+ if (!validDirPattern.test(dirPath)) {
184
+ throw new Error(`Invalid output directory: "${dirPath}". ` +
185
+ `Directory paths must contain only alphanumeric characters, underscores, hyphens, dots, and forward slashes.`);
186
+ }
187
+ }
188
+ /**
189
+ * Validates migration name
190
+ */
191
+ export function validateMigrationName(name) {
192
+ if (typeof name !== 'string') {
193
+ throw new Error('Migration name must be a string');
194
+ }
195
+ if (name.length === 0) {
196
+ throw new Error('Migration name cannot be empty');
197
+ }
198
+ if (name.length > 100) {
199
+ throw new Error('Migration name exceeds maximum length of 100 characters');
200
+ }
201
+ // Allow only alphanumeric, underscores, and hyphens
202
+ const validNamePattern = /^[a-zA-Z0-9_\-]+$/;
203
+ if (!validNamePattern.test(name)) {
204
+ throw new Error(`Invalid migration name: "${name}". ` +
205
+ `Migration names must contain only alphanumeric characters, underscores, and hyphens.`);
206
+ }
207
+ }
208
+ /**
209
+ * Rate limiting for security-sensitive operations
210
+ */
211
+ export class RateLimiter {
212
+ maxAttempts;
213
+ windowMs;
214
+ attempts = new Map();
215
+ constructor(maxAttempts = 10, windowMs = 60000 // 1 minute
216
+ ) {
217
+ this.maxAttempts = maxAttempts;
218
+ this.windowMs = windowMs;
219
+ }
220
+ checkLimit(key) {
221
+ const now = Date.now();
222
+ const attempts = this.attempts.get(key) || [];
223
+ // Remove old attempts outside the window
224
+ const recentAttempts = attempts.filter(time => now - time < this.windowMs);
225
+ if (recentAttempts.length >= this.maxAttempts) {
226
+ throw new Error(`Rate limit exceeded for ${key}. Please wait before trying again.`);
227
+ }
228
+ recentAttempts.push(now);
229
+ this.attempts.set(key, recentAttempts);
230
+ // Cleanup old entries periodically
231
+ if (this.attempts.size > 1000) {
232
+ this.cleanup(now);
233
+ }
234
+ }
235
+ cleanup(now) {
236
+ for (const [key, attempts] of this.attempts.entries()) {
237
+ const recentAttempts = attempts.filter(time => now - time < this.windowMs);
238
+ if (recentAttempts.length === 0) {
239
+ this.attempts.delete(key);
240
+ }
241
+ else {
242
+ this.attempts.set(key, recentAttempts);
243
+ }
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Security utilities for NOORMME
3
+ * Export all security-related functions for easy import
4
+ */
5
+ export { validateIdentifier, validateTableReference, validateColumnReference, validateFilePath, sanitizeDatabasePath, validateOutputDirectory, validateMigrationName, RateLimiter, } from './security-validator.js';
6
+ export { safeOrderDirection, safeLimit, safeOffset, safeKeyword, safeLockMode, safeOrderBy, safeBoolean, safeCaseStatement, SafeSQLKeywords, SafeSQLExamples, validateEnum, validateNumericRange, type SafeOrderBy, type SafeCaseWhen, } from './safe-sql-helpers.js';
7
+ /**
8
+ * Security best practices and guidelines
9
+ */
10
+ export declare const SecurityGuidelines: {
11
+ /**
12
+ * NEVER use sql.raw() or sql.lit() with user input
13
+ */
14
+ readonly NEVER_USE_RAW_WITH_USER_INPUT: "Use safe alternatives or parameterized queries";
15
+ /**
16
+ * ALWAYS validate dynamic identifiers
17
+ */
18
+ readonly ALWAYS_VALIDATE_IDENTIFIERS: "Use validateColumnReference() or validateTableReference()";
19
+ /**
20
+ * ALWAYS use whitelists for user-controlled values
21
+ */
22
+ readonly ALWAYS_USE_WHITELISTS: "Define allowed values and validate against them";
23
+ /**
24
+ * ALWAYS sanitize file paths
25
+ */
26
+ readonly ALWAYS_SANITIZE_PATHS: "Use validateFilePath() or sanitizeDatabasePath()";
27
+ /**
28
+ * PREFER parameterized queries
29
+ */
30
+ readonly PREFER_PARAMETERIZED_QUERIES: "Use sql`${value}` instead of sql.raw()";
31
+ /**
32
+ * PREFER safe helpers over raw SQL
33
+ */
34
+ readonly PREFER_SAFE_HELPERS: "Use safeOrderDirection(), safeLimit(), etc.";
35
+ };
36
+ /**
37
+ * Quick security checklist for developers
38
+ */
39
+ export declare function securityChecklist(): string;
40
+ /**
41
+ * Common security patterns
42
+ */
43
+ export declare const SecurityPatterns: {
44
+ /**
45
+ * Safe column selection from user input
46
+ */
47
+ safeColumnSelection: (userColumn: string, allowedColumns: string[]) => string;
48
+ /**
49
+ * Safe table selection from user input
50
+ */
51
+ safeTableSelection: (userTable: string, allowedTables: string[]) => string;
52
+ /**
53
+ * Safe enum validation
54
+ */
55
+ safeEnumValue: <T extends string>(value: string, allowedValues: readonly T[]) => T;
56
+ /**
57
+ * Safe numeric range validation
58
+ */
59
+ safeNumericValue: (value: number | string, min: number, max: number) => number;
60
+ };
@@ -0,0 +1,114 @@
1
+ /// <reference types="./security.d.ts" />
2
+ /**
3
+ * Security utilities for NOORMME
4
+ * Export all security-related functions for easy import
5
+ */
6
+ // Input validation and sanitization
7
+ export { validateIdentifier, validateTableReference, validateColumnReference, validateFilePath, sanitizeDatabasePath, validateOutputDirectory, validateMigrationName, RateLimiter, } from './security-validator.js';
8
+ // Safe SQL helpers
9
+ export { safeOrderDirection, safeLimit, safeOffset, safeKeyword, safeLockMode, safeOrderBy, safeBoolean, safeCaseStatement, SafeSQLKeywords, SafeSQLExamples, validateEnum, validateNumericRange, } from './safe-sql-helpers.js';
10
+ /**
11
+ * Security best practices and guidelines
12
+ */
13
+ export const SecurityGuidelines = {
14
+ /**
15
+ * NEVER use sql.raw() or sql.lit() with user input
16
+ */
17
+ NEVER_USE_RAW_WITH_USER_INPUT: 'Use safe alternatives or parameterized queries',
18
+ /**
19
+ * ALWAYS validate dynamic identifiers
20
+ */
21
+ ALWAYS_VALIDATE_IDENTIFIERS: 'Use validateColumnReference() or validateTableReference()',
22
+ /**
23
+ * ALWAYS use whitelists for user-controlled values
24
+ */
25
+ ALWAYS_USE_WHITELISTS: 'Define allowed values and validate against them',
26
+ /**
27
+ * ALWAYS sanitize file paths
28
+ */
29
+ ALWAYS_SANITIZE_PATHS: 'Use validateFilePath() or sanitizeDatabasePath()',
30
+ /**
31
+ * PREFER parameterized queries
32
+ */
33
+ PREFER_PARAMETERIZED_QUERIES: 'Use sql`${value}` instead of sql.raw()',
34
+ /**
35
+ * PREFER safe helpers over raw SQL
36
+ */
37
+ PREFER_SAFE_HELPERS: 'Use safeOrderDirection(), safeLimit(), etc.',
38
+ };
39
+ /**
40
+ * Quick security checklist for developers
41
+ */
42
+ export function securityChecklist() {
43
+ return `
44
+ NOORMME Security Checklist:
45
+
46
+ ✅ Input Validation
47
+ □ All user inputs validated with whitelists
48
+ □ Dynamic columns/tables use validateColumnReference()/validateTableReference()
49
+ □ File paths use validateFilePath() or sanitizeDatabasePath()
50
+
51
+ ✅ SQL Injection Prevention
52
+ □ No sql.raw() with user input
53
+ □ No sql.lit() with user input
54
+ □ Use safe alternatives: safeOrderDirection(), safeLimit(), etc.
55
+ □ Parameterized queries used for all values
56
+
57
+ ✅ Path Traversal Prevention
58
+ □ All file operations validated
59
+ □ No parent directory references (..) allowed
60
+ □ No absolute paths accepted
61
+
62
+ ✅ Production Readiness
63
+ □ Environment variables for sensitive config
64
+ □ Error messages don't leak sensitive info
65
+ □ Rate limiting enabled for auth endpoints
66
+ □ Database file permissions set (600/640)
67
+ □ Dependencies updated regularly
68
+ □ Backups encrypted and secure
69
+
70
+ Use this checklist before deploying to production!
71
+ `;
72
+ }
73
+ /**
74
+ * Common security patterns
75
+ */
76
+ export const SecurityPatterns = {
77
+ /**
78
+ * Safe column selection from user input
79
+ */
80
+ safeColumnSelection: (userColumn, allowedColumns) => {
81
+ if (!allowedColumns.includes(userColumn)) {
82
+ throw new Error(`Invalid column: ${userColumn}`);
83
+ }
84
+ return userColumn;
85
+ },
86
+ /**
87
+ * Safe table selection from user input
88
+ */
89
+ safeTableSelection: (userTable, allowedTables) => {
90
+ if (!allowedTables.includes(userTable)) {
91
+ throw new Error(`Invalid table: ${userTable}`);
92
+ }
93
+ return userTable;
94
+ },
95
+ /**
96
+ * Safe enum validation
97
+ */
98
+ safeEnumValue: (value, allowedValues) => {
99
+ if (!allowedValues.includes(value)) {
100
+ throw new Error(`Invalid value: ${value}. Allowed: ${allowedValues.join(', ')}`);
101
+ }
102
+ return value;
103
+ },
104
+ /**
105
+ * Safe numeric range validation
106
+ */
107
+ safeNumericValue: (value, min, max) => {
108
+ const num = typeof value === 'string' ? parseFloat(value) : value;
109
+ if (isNaN(num) || num < min || num > max) {
110
+ throw new Error(`Invalid number: ${value}. Must be between ${min} and ${max}`);
111
+ }
112
+ return num;
113
+ },
114
+ };
@@ -16,11 +16,13 @@ export class SchemaWatcher {
16
16
  this.schemaDiscovery = schemaDiscovery;
17
17
  this.logger = logger;
18
18
  this.options = options;
19
+ // Merge options, giving priority to explicitly passed values
20
+ const defaultEnabled = process.env.NODE_ENV === 'development';
19
21
  this.options = {
20
22
  pollInterval: 5000,
21
23
  ignoreViews: true,
22
24
  ignoredTables: [],
23
- enabled: process.env.NODE_ENV === 'development',
25
+ enabled: options.enabled !== undefined ? options.enabled : defaultEnabled,
24
26
  ...options
25
27
  };
26
28
  }
@@ -38,29 +40,40 @@ export class SchemaWatcher {
38
40
  }
39
41
  this.logger.info('Starting schema change monitoring...');
40
42
  // Get initial schema snapshot
41
- const initialSchema = await this.getCurrentSchema();
42
- this.lastSchemaHash = this.hashSchema(initialSchema);
43
+ try {
44
+ const initialSchema = await this.getCurrentSchema();
45
+ this.lastSchemaHash = this.hashSchema(initialSchema);
46
+ }
47
+ catch (error) {
48
+ this.logger.error('Failed to get initial schema snapshot:', error);
49
+ // Set a default hash to allow watching to continue
50
+ this.lastSchemaHash = '0';
51
+ }
43
52
  this.isWatching = true;
44
53
  this.intervalId = setInterval(() => {
45
54
  this.checkForChanges().catch(error => {
46
55
  this.logger.error('Error checking for schema changes:', error);
47
56
  });
48
57
  }, this.options.pollInterval);
58
+ // Unref the interval to allow the process to exit
59
+ if (this.intervalId && typeof this.intervalId.unref === 'function') {
60
+ this.intervalId.unref();
61
+ }
49
62
  this.logger.info(`Schema watcher started (polling every ${this.options.pollInterval}ms)`);
50
63
  }
51
64
  /**
52
65
  * Stop watching for schema changes
53
66
  */
54
67
  stopWatching() {
55
- if (!this.isWatching) {
56
- return;
57
- }
58
68
  if (this.intervalId) {
59
69
  clearInterval(this.intervalId);
60
70
  this.intervalId = null;
61
71
  }
62
72
  this.isWatching = false;
63
- this.logger.info('Schema watcher stopped');
73
+ this.callbacks = []; // Clear callbacks on stop
74
+ if (this.logger) {
75
+ this.logger.info('Schema watcher stopped');
76
+ }
64
77
  }
65
78
  /**
66
79
  * Register callback for schema changes
@@ -84,6 +97,8 @@ export class SchemaWatcher {
84
97
  try {
85
98
  const currentSchema = await this.getCurrentSchema();
86
99
  const currentHash = this.hashSchema(currentSchema);
100
+ // Debug logging
101
+ this.logger.debug(`Schema check - Last hash: ${this.lastSchemaHash}, Current hash: ${currentHash}, Tables: ${currentSchema.tables.length}`);
87
102
  if (this.lastSchemaHash && currentHash !== this.lastSchemaHash) {
88
103
  this.logger.info('Schema changes detected, analyzing...');
89
104
  // Since we only have the hash, we'll need to do a full schema comparison
@@ -96,6 +111,10 @@ export class SchemaWatcher {
96
111
  }
97
112
  return changes;
98
113
  }
114
+ // Update hash even if no changes detected yet (for first check after initialization)
115
+ if (!this.lastSchemaHash) {
116
+ this.lastSchemaHash = currentHash;
117
+ }
99
118
  return [];
100
119
  }
101
120
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noormme",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "NOORMME - The NO-ORM for Normies. SQLite automation so simple, even normies can use it.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,89 +0,0 @@
1
- import type { Kysely } from '../kysely.js';
2
- import { Logger } from '../logging/logger.js';
3
- import { SchemaInfo } from '../types/index.js';
4
- export interface QueryMetrics {
5
- query: string;
6
- executionTime: number;
7
- timestamp: Date;
8
- table?: string;
9
- operation?: string;
10
- }
11
- export interface PerformanceWarning {
12
- type: 'n_plus_one' | 'missing_index' | 'slow_query' | 'full_table_scan' | 'large_result_set';
13
- message: string;
14
- suggestion: string;
15
- query?: string;
16
- table?: string;
17
- severity: 'low' | 'medium' | 'high';
18
- }
19
- export interface QueryAnalyzerOptions {
20
- enabled?: boolean;
21
- slowQueryThreshold?: number;
22
- nPlusOneDetection?: boolean;
23
- missingIndexDetection?: boolean;
24
- largeResultSetThreshold?: number;
25
- }
26
- /**
27
- * Analyzes query performance and provides warnings for optimization
28
- */
29
- export declare class QueryAnalyzer {
30
- private db;
31
- private logger;
32
- private schemaInfo;
33
- private options;
34
- private queryHistory;
35
- private recentQueries;
36
- private maxHistorySize;
37
- constructor(db: Kysely<any>, logger: Logger, schemaInfo: SchemaInfo, options?: QueryAnalyzerOptions);
38
- /**
39
- * Record a query execution for analysis
40
- */
41
- recordQuery(query: string, executionTime: number, resultCount?: number, table?: string): void;
42
- /**
43
- * Analyze a query for performance issues
44
- */
45
- private analyzeQuery;
46
- /**
47
- * Detect N+1 query patterns
48
- */
49
- private detectNPlusOne;
50
- /**
51
- * Check for missing indexes based on WHERE clauses
52
- */
53
- private checkForMissingIndexes;
54
- /**
55
- * Normalize query for comparison (remove dynamic values)
56
- */
57
- private normalizeQuery;
58
- /**
59
- * Extract operation type from query
60
- */
61
- private extractOperation;
62
- /**
63
- * Log performance warning
64
- */
65
- private logWarning;
66
- /**
67
- * Get emoji for warning type
68
- */
69
- private getWarningEmoji;
70
- /**
71
- * Get performance statistics
72
- */
73
- getPerformanceStats(): {
74
- totalQueries: number;
75
- averageExecutionTime: number;
76
- slowQueries: number;
77
- warningCount: {
78
- [key: string]: number;
79
- };
80
- };
81
- /**
82
- * Clear query history
83
- */
84
- clearHistory(): void;
85
- /**
86
- * Update schema info for index checking
87
- */
88
- updateSchema(schemaInfo: SchemaInfo): void;
89
- }