@iamkirbki/database-handler-core 2.0.0

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.
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Validator class for validating table names, column names, column types, SQL queries, and query parameters
3
+ *
4
+ * Provides static methods to ensure data integrity and prevent SQL injection attacks by:
5
+ * - Validating table and column naming conventions
6
+ * - Checking column type compatibility with SQLite standards
7
+ * - Detecting SQL injection patterns in queries
8
+ * - Verifying parameter types match column types
9
+ * - Enforcing NOT NULL constraints
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // Validate a table name
14
+ * Validator.ValidateTableName('users'); // OK
15
+ * Validator.ValidateTableName('invalid-name'); // throws Error
16
+ *
17
+ * // Validate a column type
18
+ * Validator.ValidateColumnType('TEXT NOT NULL'); // OK
19
+ * Validator.ValidateColumnType('INVALID_TYPE'); // throws Error
20
+ *
21
+ * // Validate a query for SQL injection
22
+ * Validator.ValidateQuery('SELECT * FROM users WHERE id = @id', columnInfo); // OK
23
+ * Validator.ValidateQuery('SELECT * FROM users; DROP TABLE users', columnInfo); // throws Error
24
+ * ```
25
+ */
26
+ class Validator {
27
+ /**
28
+ * Validates a table name according to SQLite naming conventions
29
+ *
30
+ * Rules:
31
+ * - Must be a non-empty string
32
+ * - Cannot contain commas
33
+ * - Must only contain letters, numbers, and underscores
34
+ *
35
+ * @param name - The table name to validate
36
+ * @throws Error if the table name is invalid
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * Validator.ValidateTableName('users'); // OK
41
+ * Validator.ValidateTableName('user_accounts'); // OK
42
+ * Validator.ValidateTableName('users123'); // OK
43
+ * Validator.ValidateTableName('invalid-name'); // throws Error
44
+ * Validator.ValidateTableName(''); // throws Error
45
+ * ```
46
+ */
47
+ static ValidateTableName(name) {
48
+ this.ValidateName(name, "Table");
49
+ }
50
+ /**
51
+ * Validates a column name according to SQLite naming conventions
52
+ *
53
+ * Rules:
54
+ * - Must be a non-empty string
55
+ * - Cannot contain commas
56
+ * - Must only contain letters, numbers, and underscores
57
+ *
58
+ * @param name - The column name to validate
59
+ * @throws Error if the column name is invalid
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * Validator.ValidateColumnName('email'); // OK
64
+ * Validator.ValidateColumnName('user_id'); // OK
65
+ * Validator.ValidateColumnName('created_at'); // OK
66
+ * Validator.ValidateColumnName('invalid-name'); // throws Error
67
+ * Validator.ValidateColumnName(''); // throws Error
68
+ * ```
69
+ */
70
+ static ValidateColumnName(name) {
71
+ this.ValidateName(name, "Column");
72
+ }
73
+ /**
74
+ * Generic name validation for tables and columns
75
+ *
76
+ * Rules:
77
+ * - Must be a non-empty string
78
+ * - Cannot contain commas
79
+ * - Must only contain letters, numbers, and underscores
80
+ *
81
+ * @param name - The table/column name to validate
82
+ * @param type - The type, either table or column
83
+ * @throws Error if the name is invalid
84
+ */
85
+ static ValidateName(name, type) {
86
+ if (!name || typeof name !== "string") {
87
+ throw new Error(`${type} name must be a non-empty string.`);
88
+ }
89
+ if (name.includes(",")) {
90
+ throw new Error(`${type} name cannot contain commas.`);
91
+ }
92
+ if (/[^a-zA-Z0-9_]/.test(name)) {
93
+ throw new Error(`${type} name must only contain letters, numbers, and underscores.`);
94
+ }
95
+ }
96
+ /**
97
+ * Validates a column type definition against SQLite type standards
98
+ *
99
+ * Strips SQL keywords (NOT NULL, PRIMARY KEY, etc.) and length specifications
100
+ * before checking if the base type is valid. Supports all SQLite types and
101
+ * common types from other SQL dialects for compatibility.
102
+ *
103
+ * @param type - The column type definition to validate (e.g., "TEXT NOT NULL", "VARCHAR(255)")
104
+ * @throws Error if the column type is invalid
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * Validator.ValidateColumnType('TEXT'); // OK
109
+ * Validator.ValidateColumnType('INTEGER NOT NULL'); // OK
110
+ * Validator.ValidateColumnType('VARCHAR(255)'); // OK
111
+ * Validator.ValidateColumnType('TEXT PRIMARY KEY'); // OK
112
+ * Validator.ValidateColumnType('INVALID_TYPE'); // throws Error
113
+ * ```
114
+ */
115
+ static ValidateColumnType(type) {
116
+ if (!type || typeof type !== "string") {
117
+ throw new Error("Column type must be a non-empty string.");
118
+ }
119
+ const cleanedType = type
120
+ .toUpperCase()
121
+ .replace(/\([0-9 , a-zA-Z]*\)/g, "") // Remove (255), (10,2), etc.
122
+ .replace(new RegExp(`\\b(${this.sqlKeywords.join("|")})\\b`, "gi"), "")
123
+ .trim();
124
+ if (!this.validTypes.includes(cleanedType)) {
125
+ throw new Error(`Invalid column type "${type}". Valid types are: ${this.validTypes.join(", ")}.`);
126
+ }
127
+ }
128
+ /**
129
+ * Validates an SQL query for security and correctness
130
+ *
131
+ * Security checks:
132
+ * - Detects SQL injection attempts (semicolon followed by DROP, DELETE, UPDATE, INSERT, ALTER)
133
+ * - Ensures all field references (@fieldName) exist in the table schema
134
+ *
135
+ * Correctness checks:
136
+ * - Verifies all required (NOT NULL) fields are provided in INSERT queries
137
+ * - Validates query is a non-empty string
138
+ *
139
+ * @param query - The SQL query string to validate
140
+ * @param TableColumnInformation - Array of column metadata from the table schema
141
+ * @throws Error if the query contains forbidden operations, references unknown fields, or missing required fields
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const columnInfo = [
146
+ * { name: 'id', type: 'INTEGER', notnull: 1 },
147
+ * { name: 'name', type: 'TEXT', notnull: 1 },
148
+ * { name: 'email', type: 'TEXT', notnull: 0 }
149
+ * ];
150
+ *
151
+ * // Valid queries
152
+ * Validator.ValidateQuery('SELECT * FROM users WHERE id = @id', columnInfo); // OK
153
+ * Validator.ValidateQuery('INSERT INTO users (name, email) VALUES (@name, @email)', columnInfo); // OK
154
+ *
155
+ * // Invalid queries
156
+ * Validator.ValidateQuery('SELECT * FROM users; DROP TABLE users', columnInfo); // throws Error - SQL injection
157
+ * Validator.ValidateQuery('SELECT * FROM users WHERE @nonexistent = 1', columnInfo); // throws Error - unknown field
158
+ * Validator.ValidateQuery('INSERT INTO users (email) VALUES (@email)', columnInfo); // throws Error - missing required field 'name'
159
+ * ```
160
+ */
161
+ static ValidateQuery(query, TableColumnInformation) {
162
+ if (!query || typeof query !== "string" || query.trim() === "") {
163
+ throw new Error("Query must be a non-empty string.");
164
+ }
165
+ const forbiddenPatterns = [
166
+ /;\s*drop\s+table/i,
167
+ /;\s*delete\s+from/i,
168
+ /;\s*update\s+/i,
169
+ /;\s*insert\s+into/i,
170
+ /;\s*alter\s+table/i
171
+ ];
172
+ for (const pattern of forbiddenPatterns) {
173
+ if (pattern.test(query)) {
174
+ throw new Error("Query contains forbidden operations.");
175
+ }
176
+ }
177
+ const fieldPattern = /@([a-zA-Z0-9_]+)/g;
178
+ let match;
179
+ let requiredFields = TableColumnInformation
180
+ .filter(col => col.notnull === 1);
181
+ while ((match = fieldPattern.exec(query)) !== null) {
182
+ const fieldName = match[1];
183
+ const found = TableColumnInformation.some(col => col.name === fieldName);
184
+ if (!found) {
185
+ throw new Error(`Query references unknown field "@${fieldName}".`);
186
+ }
187
+ requiredFields = requiredFields.filter(col => col.name != fieldName);
188
+ }
189
+ if (requiredFields.length > 0 && /insert\s+into/i.test(query)) {
190
+ const fieldNames = requiredFields.map(col => col.name).join(", ");
191
+ throw new Error(`Query is missing required fields: ${fieldNames}.`);
192
+ }
193
+ }
194
+ /**
195
+ * Validates query parameters against table schema
196
+ *
197
+ * Checks performed:
198
+ * - Each parameter key must match a column in the table
199
+ * - NOT NULL columns cannot receive null or undefined values
200
+ * - Parameter types must match their corresponding column types
201
+ * - All field references in the query must have corresponding parameters
202
+ *
203
+ * @param query - The SQL query string containing field references
204
+ * @param parameters - Object mapping parameter names to values
205
+ * @param TableColumnInformation - Array of column metadata from the table schema
206
+ * @throws Error if parameters don't match schema, have wrong types, or violate NOT NULL constraints
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const columnInfo = [
211
+ * { name: 'id', type: 'INTEGER', notnull: 1 },
212
+ * { name: 'name', type: 'TEXT', notnull: 1 },
213
+ * { name: 'age', type: 'INTEGER', notnull: 0 }
214
+ * ];
215
+ *
216
+ * const query = 'INSERT INTO users (name, age) VALUES (@name, @age)';
217
+ *
218
+ * // Valid parameters
219
+ * Validator.ValidateQueryParameters(query, { name: 'John', age: 30 }, columnInfo); // OK
220
+ * Validator.ValidateQueryParameters(query, { name: 'Jane', age: null }, columnInfo); // OK - age is nullable
221
+ *
222
+ * // Invalid parameters
223
+ * Validator.ValidateQueryParameters(query, { name: null, age: 30 }, columnInfo); // throws Error - name is NOT NULL
224
+ * Validator.ValidateQueryParameters(query, { name: 'John', age: 'thirty' }, columnInfo); // throws Error - wrong type
225
+ * Validator.ValidateQueryParameters(query, { name: 'John' }, columnInfo); // throws Error - missing @age parameter
226
+ * Validator.ValidateQueryParameters(query, { name: 'John', age: 30, extra: 'value' }, columnInfo); // throws Error - extra is not a column
227
+ * ```
228
+ */
229
+ static ValidateQueryParameters(query, parameters, TableColumnInformation) {
230
+ for (const [key, value] of Object.entries(parameters)) {
231
+ const columnInfo = TableColumnInformation.find(col => col.name === key);
232
+ if (!columnInfo) {
233
+ throw new Error(`Parameter "${key}" does not match any column in the table.`);
234
+ }
235
+ if (columnInfo.notnull === 1 && (value === null || value === undefined)) {
236
+ throw new Error(`Parameter "${key}" cannot be null or undefined for a NOT NULL column.`);
237
+ }
238
+ const parameterType = typeof value;
239
+ const columnType = columnInfo.type;
240
+ const isValidType = Validator.CompareTypes(columnType, parameterType);
241
+ if (!isValidType) {
242
+ throw new Error(`Parameter "${key}" has type "${parameterType}" which does not match column type "${columnType}".`);
243
+ }
244
+ const fieldPattern = /@([a-zA-Z0-9_]+)/g;
245
+ let match;
246
+ while ((match = fieldPattern.exec(query)) !== null) {
247
+ const fieldName = match[1];
248
+ const found = parameters[fieldName] !== undefined;
249
+ if (!found) {
250
+ throw new Error(`Missing parameter for column "${fieldName}".`);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ /**
256
+ * Compares a column type with a JavaScript parameter type for compatibility
257
+ *
258
+ * Type mappings:
259
+ * - TEXT/CHAR types → string
260
+ * - INTEGER/INT types → number
261
+ * - REAL/FLOAT/DOUBLE types → number
262
+ * - BOOLEAN types → boolean
263
+ * - BLOB types → object (Buffer/Uint8Array)
264
+ * - UUID → string (with format validation)
265
+ *
266
+ * @param columnType - The SQLite column type from the schema
267
+ * @param parameterType - The JavaScript typeof value for the parameter
268
+ * @returns true if the types are compatible, false otherwise
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * Validator.CompareTypes('TEXT', 'string'); // true
273
+ * Validator.CompareTypes('INTEGER', 'number'); // true
274
+ * Validator.CompareTypes('VARCHAR(255)', 'string'); // true
275
+ * Validator.CompareTypes('REAL', 'number'); // true
276
+ * Validator.CompareTypes('TEXT', 'number'); // false
277
+ * Validator.CompareTypes('INTEGER', 'string'); // false
278
+ * ```
279
+ */
280
+ static CompareTypes(columnType, parameterType) {
281
+ if (!columnType || !parameterType) {
282
+ return false;
283
+ }
284
+ const lowerType = columnType.toLowerCase();
285
+ // SQLite text types (TEXT, VARCHAR, CHAR, etc.)
286
+ if (lowerType.includes('text') || lowerType.includes('char')) {
287
+ return parameterType === 'string' || parameterType === 'number';
288
+ }
289
+ // SQLite integer and real/float types (INTEGER, INT, TINYINT, SMALLINT, REAL, FLOAT, DOUBLE etc.)
290
+ if (lowerType.includes('int') || lowerType.includes('real') || lowerType.includes('float') || lowerType.includes('double')) {
291
+ return parameterType === 'number';
292
+ }
293
+ // Boolean
294
+ if (lowerType.includes('bool')) {
295
+ return parameterType === 'boolean';
296
+ }
297
+ // BLOB types
298
+ if (lowerType.includes('blob')) {
299
+ return parameterType === 'object'; // Buffer or Uint8Array
300
+ }
301
+ // UUID with validation
302
+ if (lowerType === 'uuid' && parameterType === 'string') {
303
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(parameterType);
304
+ }
305
+ // Default: allow any type if not explicitly restricted
306
+ // This handles custom SQLite types gracefully
307
+ return true;
308
+ }
309
+ }
310
+ /**
311
+ * List of valid SQLite column types
312
+ * Includes numeric, text, binary, date/time types and compatibility types for other SQL dialects
313
+ */
314
+ Validator.validTypes = [
315
+ // Numeric types
316
+ "INTEGER", "INT", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", "UNSIGNED BIG INT",
317
+ "INT2", "INT8", "NUMERIC", "DECIMAL", "BOOLEAN", "FLOAT", "DOUBLE", "DOUBLE PRECISION", "REAL",
318
+ // Text types
319
+ "TEXT", "CHARACTER", "CHAR", "VARCHAR", "VARYING CHARACTER", "NCHAR",
320
+ "NATIVE CHARACTER", "NVARCHAR", "CLOB",
321
+ // Binary types
322
+ "BLOB", "BINARY", "VARBINARY",
323
+ // Date/time types
324
+ "DATE", "DATETIME", "TIME", "TIMESTAMP",
325
+ // Other types (for compatibility with other SQL dialects)
326
+ "ENUM", "SET", "YEAR", "JSON", "GEOMETRY", "POINT", "LINESTRING", "POLYGON",
327
+ "MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION"
328
+ ];
329
+ /**
330
+ * List of SQL keywords that are valid in column type definitions
331
+ * These keywords are stripped when validating the base column type
332
+ */
333
+ Validator.sqlKeywords = [
334
+ "NOT",
335
+ "NULL",
336
+ "PRIMARY",
337
+ "KEY",
338
+ "AUTOINCREMENT",
339
+ "UNIQUE",
340
+ "CHECK",
341
+ "DEFAULT",
342
+ "COLLATE",
343
+ "REFERENCES"
344
+ ];
345
+ export default Validator;
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * kirbkis-bettersqlite3-handler
3
+ * A TypeScript wrapper for better-sqlite3 with type-safe operations and parameter validation
4
+ *
5
+ * Features:
6
+ * - Type-safe database operations with TypeScript generics
7
+ * - Automatic parameter validation against table schema
8
+ * - SQL injection prevention through query validation
9
+ * - Named parameters using @fieldName syntax
10
+ * - Record-based API for easy updates and deletes
11
+ * - Transaction support for atomic operations
12
+ * - Comprehensive error messages for debugging
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { Database } from 'kirbkis-bettersqlite3-handler';
17
+ *
18
+ * // Create/open database
19
+ * const db = new Database('./myapp.db');
20
+ *
21
+ * // Create table
22
+ * const users = db.CreateTable('users', {
23
+ * id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
24
+ * name: 'TEXT NOT NULL',
25
+ * email: 'TEXT UNIQUE',
26
+ * age: 'INTEGER'
27
+ * });
28
+ *
29
+ * // Insert data
30
+ * users.Insert({ name: 'John', email: 'john@example.com', age: 30 });
31
+ *
32
+ * // Query data
33
+ * const activeUsers = users.Records({ where: { age: 30 } });
34
+ *
35
+ * // Update record
36
+ * const user = users.Record({ where: { id: 1 } });
37
+ * user?.Update({ age: 31 });
38
+ *
39
+ * // Custom query
40
+ * const query = db.Query(users, 'SELECT * FROM users WHERE age > @minAge');
41
+ * query.Parameters = { minAge: 25 };
42
+ * const results = query.All();
43
+ * ```
44
+ *
45
+ * @packageDocumentation
46
+ */
47
+ import Database from "./Database";
48
+ import Model from "./abstract/Model";
49
+ export { Database, Model };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ // Source - https://stackoverflow.com/a
2
+ // Posted by KPD, modified by community. See post 'Timeline' for change history
3
+ // Retrieved 2025-11-19, License - CC BY-SA 4.0
4
+ export * from './query';
5
+ export * from './table';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ // export type Join = RequireAtLeastOne<SingleJoin, 'table' | 'join'>;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@iamkirbki/database-handler-core",
3
+ "version": "2.0.0",
4
+ "author": "iamkirbki",
5
+ "description": "Core database abstractions and interfaces",
6
+ "license": "ISC",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "keywords": [
15
+ "database",
16
+ "orm",
17
+ "query-builder",
18
+ "typescript"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/IamKirbki/bettersqlite3-handler.git",
23
+ "directory": "packages/core"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.9.3"
30
+ }
31
+ }