outlet-orm 4.2.1 → 5.5.1
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/README.md +166 -53
- package/bin/init.js +18 -0
- package/bin/migrate.js +109 -7
- package/bin/reverse.js +602 -0
- package/package.json +22 -13
- package/src/Database/DatabaseConnection.js +4 -0
- package/src/DatabaseConnection.js +98 -46
- package/{lib → src}/Migrations/Migration.js +48 -48
- package/{lib → src}/Migrations/MigrationManager.js +22 -19
- package/src/Model.js +30 -7
- package/src/QueryBuilder.js +134 -35
- package/src/RawExpression.js +11 -0
- package/src/Relations/BelongsToManyRelation.js +466 -466
- package/{lib → src}/Schema/Schema.js +157 -117
- package/src/Seeders/Seeder.js +60 -0
- package/src/Seeders/SeederManager.js +105 -0
- package/src/index.js +25 -1
- package/types/index.d.ts +14 -0
- package/lib/Database/DatabaseConnection.js +0 -4
|
@@ -9,23 +9,27 @@ let sqlite3;
|
|
|
9
9
|
// Query log storage
|
|
10
10
|
let queryLog = [];
|
|
11
11
|
let queryLoggingEnabled = false;
|
|
12
|
+
// Maximum number of entries retained in the query log to prevent unbounded memory growth
|
|
13
|
+
const MAX_QUERY_LOG_SIZE = 1000;
|
|
14
|
+
|
|
15
|
+
const RawExpression = require('./RawExpression');
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Sanitize SQL identifier (table/column name) to prevent SQL injection
|
|
15
|
-
* @param {string} identifier
|
|
19
|
+
* @param {string|RawExpression} identifier
|
|
16
20
|
* @returns {string}
|
|
17
21
|
*/
|
|
18
22
|
function sanitizeIdentifier(identifier) {
|
|
23
|
+
if (identifier instanceof RawExpression) {
|
|
24
|
+
return identifier.value;
|
|
25
|
+
}
|
|
19
26
|
if (!identifier || typeof identifier !== 'string') {
|
|
20
27
|
throw new Error('Invalid SQL identifier');
|
|
21
28
|
}
|
|
22
|
-
//
|
|
29
|
+
// Strict allowlist: only alphanumeric, underscore, dot (for table.column)
|
|
30
|
+
// Any identifier not matching this pattern is rejected outright — no fallback.
|
|
23
31
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/.test(identifier)) {
|
|
24
|
-
|
|
25
|
-
// Note: Escape hyphen in character class to avoid range interpretation
|
|
26
|
-
if (/['";]|--|\/*|\*\/|xp_|sp_|0x/i.test(identifier)) {
|
|
27
|
-
throw new Error(`Potentially dangerous SQL identifier: ${identifier}`);
|
|
28
|
-
}
|
|
32
|
+
throw new Error('Invalid SQL identifier');
|
|
29
33
|
}
|
|
30
34
|
return identifier;
|
|
31
35
|
}
|
|
@@ -44,6 +48,10 @@ function logQuery(sql, params, duration) {
|
|
|
44
48
|
duration,
|
|
45
49
|
timestamp: new Date()
|
|
46
50
|
});
|
|
51
|
+
// Enforce size cap to prevent unbounded memory growth
|
|
52
|
+
if (queryLog.length > MAX_QUERY_LOG_SIZE) {
|
|
53
|
+
queryLog.shift();
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -272,18 +280,24 @@ class DatabaseConnection {
|
|
|
272
280
|
switch (this.driver) {
|
|
273
281
|
case 'mysql':
|
|
274
282
|
if (this._transactionConnection) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
283
|
+
try {
|
|
284
|
+
await this._transactionConnection.commit();
|
|
285
|
+
} finally {
|
|
286
|
+
this._transactionConnection.release();
|
|
287
|
+
this._transactionConnection = null;
|
|
288
|
+
}
|
|
278
289
|
}
|
|
279
290
|
break;
|
|
280
291
|
|
|
281
292
|
case 'postgres':
|
|
282
293
|
case 'postgresql':
|
|
283
294
|
if (this._transactionConnection) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
295
|
+
try {
|
|
296
|
+
await this._transactionConnection.query('COMMIT');
|
|
297
|
+
} finally {
|
|
298
|
+
this._transactionConnection.release();
|
|
299
|
+
this._transactionConnection = null;
|
|
300
|
+
}
|
|
287
301
|
}
|
|
288
302
|
break;
|
|
289
303
|
|
|
@@ -306,18 +320,24 @@ class DatabaseConnection {
|
|
|
306
320
|
switch (this.driver) {
|
|
307
321
|
case 'mysql':
|
|
308
322
|
if (this._transactionConnection) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
323
|
+
try {
|
|
324
|
+
await this._transactionConnection.rollback();
|
|
325
|
+
} finally {
|
|
326
|
+
this._transactionConnection.release();
|
|
327
|
+
this._transactionConnection = null;
|
|
328
|
+
}
|
|
312
329
|
}
|
|
313
330
|
break;
|
|
314
331
|
|
|
315
332
|
case 'postgres':
|
|
316
333
|
case 'postgresql':
|
|
317
334
|
if (this._transactionConnection) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
335
|
+
try {
|
|
336
|
+
await this._transactionConnection.query('ROLLBACK');
|
|
337
|
+
} finally {
|
|
338
|
+
this._transactionConnection.release();
|
|
339
|
+
this._transactionConnection = null;
|
|
340
|
+
}
|
|
321
341
|
}
|
|
322
342
|
break;
|
|
323
343
|
|
|
@@ -515,10 +535,7 @@ class DatabaseConnection {
|
|
|
515
535
|
switch (this.driver) {
|
|
516
536
|
case 'mysql': {
|
|
517
537
|
const conn = this._getConnection();
|
|
518
|
-
const [res] = await conn.execute(
|
|
519
|
-
this.convertToDriverPlaceholder(sql),
|
|
520
|
-
params
|
|
521
|
-
);
|
|
538
|
+
const [res] = await conn.execute(sql, params);
|
|
522
539
|
result = { affectedRows: res.affectedRows };
|
|
523
540
|
break;
|
|
524
541
|
}
|
|
@@ -566,10 +583,7 @@ class DatabaseConnection {
|
|
|
566
583
|
switch (this.driver) {
|
|
567
584
|
case 'mysql': {
|
|
568
585
|
const conn = this._getConnection();
|
|
569
|
-
const [res] = await conn.execute(
|
|
570
|
-
this.convertToDriverPlaceholder(sql),
|
|
571
|
-
params
|
|
572
|
-
);
|
|
586
|
+
const [res] = await conn.execute(sql, params);
|
|
573
587
|
result = { affectedRows: res.affectedRows };
|
|
574
588
|
break;
|
|
575
589
|
}
|
|
@@ -737,7 +751,10 @@ class DatabaseConnection {
|
|
|
737
751
|
}
|
|
738
752
|
|
|
739
753
|
/**
|
|
740
|
-
* Execute a raw query and return normalized results
|
|
754
|
+
* Execute a raw query and return normalized results.
|
|
755
|
+
* ⚠️ SECURITY WARNING: The `sql` parameter is passed to the database driver without
|
|
756
|
+
* any sanitization. NEVER pass user-controlled data in `sql`. User-controlled values
|
|
757
|
+
* must be supplied exclusively via the `params` array (parameterized queries).
|
|
741
758
|
* @param {string} sql
|
|
742
759
|
* @param {Array} params
|
|
743
760
|
* @returns {Promise<Array>}
|
|
@@ -776,7 +793,10 @@ class DatabaseConnection {
|
|
|
776
793
|
}
|
|
777
794
|
|
|
778
795
|
/**
|
|
779
|
-
* Execute raw SQL
|
|
796
|
+
* Execute raw SQL — driver-native results (intended for migrations).
|
|
797
|
+
* ⚠️ SECURITY WARNING: The `sql` parameter is passed to the database driver without
|
|
798
|
+
* any sanitization. NEVER pass user-controlled data in `sql`. User-controlled values
|
|
799
|
+
* must be supplied exclusively via the `params` array (parameterized queries).
|
|
780
800
|
* @param {string} sql
|
|
781
801
|
* @param {Array} params
|
|
782
802
|
* @returns {Promise<any>}
|
|
@@ -864,7 +884,7 @@ class DatabaseConnection {
|
|
|
864
884
|
// SELECT clause
|
|
865
885
|
let selectClause = '*';
|
|
866
886
|
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
|
|
867
|
-
selectClause = query.columns.join(', ');
|
|
887
|
+
selectClause = query.columns.map(col => sanitizeIdentifier(col)).join(', ');
|
|
868
888
|
}
|
|
869
889
|
|
|
870
890
|
// DISTINCT
|
|
@@ -876,7 +896,12 @@ class DatabaseConnection {
|
|
|
876
896
|
if (query.joins && query.joins.length > 0) {
|
|
877
897
|
for (const join of query.joins) {
|
|
878
898
|
const joinType = (join.type || 'inner').toUpperCase();
|
|
879
|
-
|
|
899
|
+
const ALLOWED_OPERATORS = ['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IS', 'IS NOT'];
|
|
900
|
+
const op = join.operator.toUpperCase();
|
|
901
|
+
if (!ALLOWED_OPERATORS.includes(op)) {
|
|
902
|
+
throw new Error(`Invalid operator: ${join.operator}`);
|
|
903
|
+
}
|
|
904
|
+
sql += ` ${joinType} JOIN ${sanitizeIdentifier(join.table)} ON ${sanitizeIdentifier(join.first)} ${op} ${sanitizeIdentifier(join.second)}`;
|
|
880
905
|
}
|
|
881
906
|
}
|
|
882
907
|
|
|
@@ -887,19 +912,24 @@ class DatabaseConnection {
|
|
|
887
912
|
|
|
888
913
|
// GROUP BY
|
|
889
914
|
if (query.groupBys && query.groupBys.length > 0) {
|
|
890
|
-
sql += ` GROUP BY ${query.groupBys.join(', ')}`;
|
|
915
|
+
sql += ` GROUP BY ${query.groupBys.map(col => sanitizeIdentifier(col)).join(', ')}`;
|
|
891
916
|
}
|
|
892
917
|
|
|
893
918
|
// HAVING
|
|
894
919
|
if (query.havings && query.havings.length > 0) {
|
|
895
920
|
const havingClauses = [];
|
|
896
921
|
for (const h of query.havings) {
|
|
922
|
+
const ALLOWED_OPERATORS = ['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IS', 'IS NOT'];
|
|
923
|
+
const op = h.operator.toUpperCase();
|
|
924
|
+
if (!ALLOWED_OPERATORS.includes(op)) {
|
|
925
|
+
throw new Error(`Invalid operator: ${h.operator}`);
|
|
926
|
+
}
|
|
897
927
|
if (h.type === 'basic') {
|
|
898
|
-
havingClauses.push(`${h.column} ${
|
|
928
|
+
havingClauses.push(`${sanitizeIdentifier(h.column)} ${op} ?`);
|
|
899
929
|
params.push(h.value);
|
|
900
930
|
} else if (h.type === 'count') {
|
|
901
|
-
const col = h.column && h.column !== '*' ? h.column : '*';
|
|
902
|
-
havingClauses.push(`COUNT(${col}) ${
|
|
931
|
+
const col = h.column && h.column !== '*' ? sanitizeIdentifier(h.column) : '*';
|
|
932
|
+
havingClauses.push(`COUNT(${col}) ${op} ?`);
|
|
903
933
|
params.push(h.value);
|
|
904
934
|
}
|
|
905
935
|
}
|
|
@@ -911,19 +941,28 @@ class DatabaseConnection {
|
|
|
911
941
|
// ORDER BY
|
|
912
942
|
if (query.orders && query.orders.length > 0) {
|
|
913
943
|
const orderClauses = query.orders.map(
|
|
914
|
-
order =>
|
|
944
|
+
order => {
|
|
945
|
+
if (order.type === 'raw') {
|
|
946
|
+
return order.sql;
|
|
947
|
+
}
|
|
948
|
+
const dir = order.direction.toUpperCase();
|
|
949
|
+
if (dir !== 'ASC' && dir !== 'DESC') throw new Error(`Invalid direction: ${dir}`);
|
|
950
|
+
return `${sanitizeIdentifier(order.column)} ${dir}`;
|
|
951
|
+
}
|
|
915
952
|
);
|
|
916
953
|
sql += ` ORDER BY ${orderClauses.join(', ')}`;
|
|
917
954
|
}
|
|
918
955
|
|
|
919
956
|
// LIMIT
|
|
920
957
|
if (query.limit !== null && query.limit !== undefined) {
|
|
921
|
-
sql +=
|
|
958
|
+
sql += ' LIMIT ?';
|
|
959
|
+
params.push(query.limit);
|
|
922
960
|
}
|
|
923
961
|
|
|
924
962
|
// OFFSET
|
|
925
963
|
if (query.offset !== null && query.offset !== undefined) {
|
|
926
|
-
sql +=
|
|
964
|
+
sql += ' OFFSET ?';
|
|
965
|
+
params.push(query.offset);
|
|
927
966
|
}
|
|
928
967
|
|
|
929
968
|
return { sql, params };
|
|
@@ -940,45 +979,58 @@ class DatabaseConnection {
|
|
|
940
979
|
|
|
941
980
|
const clauses = [];
|
|
942
981
|
const params = [];
|
|
982
|
+
const ALLOWED_OPERATORS = ['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IS', 'IS NOT'];
|
|
943
983
|
|
|
944
984
|
wheres.forEach((where, index) => {
|
|
945
985
|
const boolean = index === 0 ? 'WHERE' : (where.boolean || 'AND').toUpperCase();
|
|
986
|
+
const col = where.type !== 'raw' ? sanitizeIdentifier(where.column) : null;
|
|
946
987
|
|
|
947
988
|
switch (where.type) {
|
|
948
|
-
case '
|
|
949
|
-
clauses.push(`${boolean} ${where.
|
|
989
|
+
case 'raw': {
|
|
990
|
+
clauses.push(`${boolean} ${where.sql}`);
|
|
991
|
+
if (where.bindings) {
|
|
992
|
+
params.push(...where.bindings);
|
|
993
|
+
}
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
case 'basic': {
|
|
998
|
+
const op = where.operator.toUpperCase();
|
|
999
|
+
if (!ALLOWED_OPERATORS.includes(op)) throw new Error(`Invalid operator: ${where.operator}`);
|
|
1000
|
+
clauses.push(`${boolean} ${col} ${op} ?`);
|
|
950
1001
|
params.push(where.value);
|
|
951
1002
|
break;
|
|
1003
|
+
}
|
|
952
1004
|
|
|
953
1005
|
case 'in': {
|
|
954
1006
|
const inPlaceholders = where.values.map(() => '?').join(', ');
|
|
955
|
-
clauses.push(`${boolean} ${
|
|
1007
|
+
clauses.push(`${boolean} ${col} IN (${inPlaceholders})`);
|
|
956
1008
|
params.push(...where.values);
|
|
957
1009
|
break;
|
|
958
1010
|
}
|
|
959
1011
|
|
|
960
1012
|
case 'notIn': {
|
|
961
1013
|
const notInPlaceholders = where.values.map(() => '?').join(', ');
|
|
962
|
-
clauses.push(`${boolean} ${
|
|
1014
|
+
clauses.push(`${boolean} ${col} NOT IN (${notInPlaceholders})`);
|
|
963
1015
|
params.push(...where.values);
|
|
964
1016
|
break;
|
|
965
1017
|
}
|
|
966
1018
|
|
|
967
1019
|
case 'null':
|
|
968
|
-
clauses.push(`${boolean} ${
|
|
1020
|
+
clauses.push(`${boolean} ${col} IS NULL`);
|
|
969
1021
|
break;
|
|
970
1022
|
|
|
971
1023
|
case 'notNull':
|
|
972
|
-
clauses.push(`${boolean} ${
|
|
1024
|
+
clauses.push(`${boolean} ${col} IS NOT NULL`);
|
|
973
1025
|
break;
|
|
974
1026
|
|
|
975
1027
|
case 'between':
|
|
976
|
-
clauses.push(`${boolean} ${
|
|
1028
|
+
clauses.push(`${boolean} ${col} BETWEEN ? AND ?`);
|
|
977
1029
|
params.push(...where.values);
|
|
978
1030
|
break;
|
|
979
1031
|
|
|
980
1032
|
case 'like':
|
|
981
|
-
clauses.push(`${boolean} ${
|
|
1033
|
+
clauses.push(`${boolean} ${col} LIKE ?`);
|
|
982
1034
|
params.push(where.value);
|
|
983
1035
|
break;
|
|
984
1036
|
}
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base Migration Class
|
|
3
|
-
* All migrations should extend this class
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class Migration {
|
|
7
|
-
constructor(connection) {
|
|
8
|
-
this.connection = connection;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Run the migrations
|
|
13
|
-
*/
|
|
14
|
-
async up() {
|
|
15
|
-
throw new Error('Migration up() method must be implemented');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Reverse the migrations
|
|
20
|
-
*/
|
|
21
|
-
async down() {
|
|
22
|
-
throw new Error('Migration down() method must be implemented');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Get the migration name
|
|
27
|
-
*/
|
|
28
|
-
static getName() {
|
|
29
|
-
return this.name;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Execute raw SQL
|
|
34
|
-
*/
|
|
35
|
-
async execute(sql) {
|
|
36
|
-
return await this.connection.execute(sql);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Get the Schema builder
|
|
41
|
-
*/
|
|
42
|
-
getSchema() {
|
|
43
|
-
const { Schema } = require('../Schema/Schema');
|
|
44
|
-
return new Schema(this.connection);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
module.exports = Migration;
|
|
1
|
+
/**
|
|
2
|
+
* Base Migration Class
|
|
3
|
+
* All migrations should extend this class
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Migration {
|
|
7
|
+
constructor(connection) {
|
|
8
|
+
this.connection = connection;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run the migrations
|
|
13
|
+
*/
|
|
14
|
+
async up() {
|
|
15
|
+
throw new Error('Migration up() method must be implemented');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reverse the migrations
|
|
20
|
+
*/
|
|
21
|
+
async down() {
|
|
22
|
+
throw new Error('Migration down() method must be implemented');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the migration name
|
|
27
|
+
*/
|
|
28
|
+
static getName() {
|
|
29
|
+
return this.name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Execute raw SQL
|
|
34
|
+
*/
|
|
35
|
+
async execute(sql) {
|
|
36
|
+
return await this.connection.execute(sql);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the Schema builder
|
|
41
|
+
*/
|
|
42
|
+
getSchema() {
|
|
43
|
+
const { Schema } = require('../Schema/Schema');
|
|
44
|
+
return new Schema(this.connection);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = Migration;
|
|
@@ -7,10 +7,13 @@ const fs = require('fs').promises;
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
9
|
class MigrationManager {
|
|
10
|
-
constructor(connection, migrationsPath = './database/migrations') {
|
|
10
|
+
constructor(connection, migrationsPath = './database/migrations', migrationsTable = 'migrations') {
|
|
11
|
+
if (typeof migrationsTable !== 'string' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(migrationsTable)) {
|
|
12
|
+
throw new Error(`Invalid migrations table name: "${migrationsTable}"`);
|
|
13
|
+
}
|
|
11
14
|
this.connection = connection;
|
|
12
15
|
this.migrationsPath = path.resolve(process.cwd(), migrationsPath);
|
|
13
|
-
this.migrationsTable =
|
|
16
|
+
this.migrationsTable = migrationsTable;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -54,7 +57,7 @@ class MigrationManager {
|
|
|
54
57
|
await this.runMigration(migration, batch);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
console.log(
|
|
60
|
+
console.log('\n✓ All migrations completed successfully');
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -105,7 +108,7 @@ class MigrationManager {
|
|
|
105
108
|
await this.rollbackMigration(migration);
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
console.log(
|
|
111
|
+
console.log('\n✓ Rollback completed successfully');
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
/**
|
|
@@ -155,7 +158,7 @@ class MigrationManager {
|
|
|
155
158
|
await this.rollbackMigration(migration);
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
console.log(
|
|
161
|
+
console.log('\n✓ Reset completed successfully');
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
/**
|
|
@@ -263,11 +266,11 @@ class MigrationManager {
|
|
|
263
266
|
const sql = `
|
|
264
267
|
SELECT * FROM ${this.migrationsTable}
|
|
265
268
|
WHERE batch >= (
|
|
266
|
-
SELECT MAX(batch) -
|
|
269
|
+
SELECT MAX(batch) - ? FROM ${this.migrationsTable}
|
|
267
270
|
)
|
|
268
271
|
ORDER BY batch DESC, id DESC
|
|
269
272
|
`;
|
|
270
|
-
return await this.connection.execute(sql);
|
|
273
|
+
return await this.connection.execute(sql, [steps - 1]);
|
|
271
274
|
}
|
|
272
275
|
|
|
273
276
|
/**
|
|
@@ -304,18 +307,18 @@ class MigrationManager {
|
|
|
304
307
|
let sql;
|
|
305
308
|
|
|
306
309
|
switch (driver) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
310
|
+
case 'mysql':
|
|
311
|
+
sql = 'SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()';
|
|
312
|
+
break;
|
|
313
|
+
case 'postgres':
|
|
314
|
+
case 'postgresql':
|
|
315
|
+
sql = 'SELECT tablename FROM pg_tables WHERE schemaname = \'public\'';
|
|
316
|
+
break;
|
|
317
|
+
case 'sqlite':
|
|
318
|
+
sql = 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\'';
|
|
319
|
+
break;
|
|
320
|
+
default:
|
|
321
|
+
throw new Error(`Unsupported driver: ${driver}`);
|
|
319
322
|
}
|
|
320
323
|
|
|
321
324
|
const result = await this.connection.execute(sql);
|
package/src/Model.js
CHANGED
|
@@ -95,6 +95,13 @@ class Model {
|
|
|
95
95
|
* @param {Function} callback - Callback function
|
|
96
96
|
*/
|
|
97
97
|
static on(event, callback) {
|
|
98
|
+
if (!Object.prototype.hasOwnProperty.call(this, 'eventListeners')) {
|
|
99
|
+
this.eventListeners = {
|
|
100
|
+
creating: [], created: [], updating: [], updated: [],
|
|
101
|
+
saving: [], saved: [], deleting: [], deleted: [],
|
|
102
|
+
restoring: [], restored: []
|
|
103
|
+
};
|
|
104
|
+
}
|
|
98
105
|
if (!this.eventListeners[event]) {
|
|
99
106
|
this.eventListeners[event] = [];
|
|
100
107
|
}
|
|
@@ -108,6 +115,9 @@ class Model {
|
|
|
108
115
|
* @returns {Promise<boolean>} - Returns false if event should be cancelled
|
|
109
116
|
*/
|
|
110
117
|
static async fireEvent(event, model) {
|
|
118
|
+
if (!Object.prototype.hasOwnProperty.call(this, 'eventListeners')) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
111
121
|
const listeners = this.eventListeners[event] || [];
|
|
112
122
|
for (const listener of listeners) {
|
|
113
123
|
const result = await listener(model);
|
|
@@ -423,8 +433,13 @@ class Model {
|
|
|
423
433
|
break;
|
|
424
434
|
|
|
425
435
|
case 'regex':
|
|
426
|
-
|
|
427
|
-
|
|
436
|
+
try {
|
|
437
|
+
const re = new RegExp(ruleParam);
|
|
438
|
+
if (value && !re.test(value)) {
|
|
439
|
+
return `${field} format is invalid`;
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
return `${field} has an invalid regex rule`;
|
|
428
443
|
}
|
|
429
444
|
break;
|
|
430
445
|
}
|
|
@@ -556,7 +571,7 @@ class Model {
|
|
|
556
571
|
* @returns {Promise<any>}
|
|
557
572
|
*/
|
|
558
573
|
static async delete() {
|
|
559
|
-
|
|
574
|
+
throw new Error('Cannot call static delete() without conditions. Use query().delete() instead.');
|
|
560
575
|
}
|
|
561
576
|
|
|
562
577
|
/**
|
|
@@ -679,8 +694,9 @@ class Model {
|
|
|
679
694
|
* @returns {this}
|
|
680
695
|
*/
|
|
681
696
|
fill(attributes) {
|
|
697
|
+
const fillable = this.constructor.fillable || [];
|
|
682
698
|
for (const [key, value] of Object.entries(attributes)) {
|
|
683
|
-
if (
|
|
699
|
+
if (fillable.includes(key)) {
|
|
684
700
|
this.setAttribute(key, value);
|
|
685
701
|
}
|
|
686
702
|
}
|
|
@@ -952,6 +968,10 @@ class Model {
|
|
|
952
968
|
const head = segments[0];
|
|
953
969
|
const tail = segments.slice(1).join('.');
|
|
954
970
|
|
|
971
|
+
// Prevent prototype pollution and calling built-in methods
|
|
972
|
+
const builtIns = ['constructor', 'load', 'save', 'delete', 'update', 'query', 'with', 'withCount', 'hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'morphTo', 'morphOne', 'morphMany', 'hasOneThrough', 'hasManyThrough'];
|
|
973
|
+
if (builtIns.includes(head) || head.startsWith('__')) return;
|
|
974
|
+
|
|
955
975
|
const relationFn = this[head];
|
|
956
976
|
if (typeof relationFn !== 'function') return;
|
|
957
977
|
|
|
@@ -984,7 +1004,8 @@ class Model {
|
|
|
984
1004
|
hasOne(related, foreignKey, localKey) {
|
|
985
1005
|
const HasOneRelation = require('./Relations/HasOneRelation');
|
|
986
1006
|
localKey = localKey || this.constructor.primaryKey;
|
|
987
|
-
|
|
1007
|
+
const pluralize = require('pluralize');
|
|
1008
|
+
foreignKey = foreignKey || `${pluralize.singular(this.constructor.table)}_id`;
|
|
988
1009
|
|
|
989
1010
|
return new HasOneRelation(this, related, foreignKey, localKey);
|
|
990
1011
|
}
|
|
@@ -999,7 +1020,8 @@ class Model {
|
|
|
999
1020
|
hasMany(related, foreignKey, localKey) {
|
|
1000
1021
|
const HasManyRelation = require('./Relations/HasManyRelation');
|
|
1001
1022
|
localKey = localKey || this.constructor.primaryKey;
|
|
1002
|
-
|
|
1023
|
+
const pluralize = require('pluralize');
|
|
1024
|
+
foreignKey = foreignKey || `${pluralize.singular(this.constructor.table)}_id`;
|
|
1003
1025
|
|
|
1004
1026
|
return new HasManyRelation(this, related, foreignKey, localKey);
|
|
1005
1027
|
}
|
|
@@ -1014,7 +1036,8 @@ class Model {
|
|
|
1014
1036
|
belongsTo(related, foreignKey, ownerKey) {
|
|
1015
1037
|
const BelongsToRelation = require('./Relations/BelongsToRelation');
|
|
1016
1038
|
ownerKey = ownerKey || related.primaryKey;
|
|
1017
|
-
|
|
1039
|
+
const pluralize = require('pluralize');
|
|
1040
|
+
foreignKey = foreignKey || `${pluralize.singular(related.table)}_id`;
|
|
1018
1041
|
|
|
1019
1042
|
return new BelongsToRelation(this, related, foreignKey, ownerKey);
|
|
1020
1043
|
}
|