@nitronjs/framework 0.2.3 → 0.2.4
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 +3 -1
- package/cli/create.js +88 -72
- package/cli/njs.js +13 -6
- package/lib/Auth/Auth.js +167 -0
- package/lib/Build/CssBuilder.js +9 -0
- package/lib/Build/FileAnalyzer.js +16 -0
- package/lib/Build/HydrationBuilder.js +17 -0
- package/lib/Build/Manager.js +15 -0
- package/lib/Build/colors.js +4 -0
- package/lib/Build/plugins.js +84 -20
- package/lib/Console/Commands/DevCommand.js +13 -9
- package/lib/Console/Commands/MakeCommand.js +24 -10
- package/lib/Console/Commands/MigrateCommand.js +0 -1
- package/lib/Console/Commands/MigrateFreshCommand.js +18 -25
- package/lib/Console/Commands/MigrateRollbackCommand.js +6 -3
- package/lib/Console/Commands/MigrateStatusCommand.js +6 -3
- package/lib/Console/Commands/SeedCommand.js +4 -2
- package/lib/Console/Commands/StorageLinkCommand.js +20 -5
- package/lib/Console/Output.js +143 -0
- package/lib/Core/Config.js +2 -1
- package/lib/Core/Paths.js +8 -0
- package/lib/Database/DB.js +141 -51
- package/lib/Database/Drivers/MySQLDriver.js +102 -157
- package/lib/Database/Migration/Checksum.js +3 -8
- package/lib/Database/Migration/MigrationRepository.js +25 -35
- package/lib/Database/Migration/MigrationRunner.js +56 -61
- package/lib/Database/Model.js +157 -83
- package/lib/Database/QueryBuilder.js +31 -0
- package/lib/Database/QueryValidation.js +36 -44
- package/lib/Database/Schema/Blueprint.js +25 -36
- package/lib/Database/Schema/Manager.js +31 -68
- package/lib/Database/Seeder/SeederRunner.js +12 -31
- package/lib/Date/DateTime.js +9 -0
- package/lib/Encryption/Encryption.js +52 -0
- package/lib/Faker/Faker.js +11 -0
- package/lib/Filesystem/Storage.js +120 -0
- package/lib/HMR/Server.js +79 -9
- package/lib/Hashing/Hash.js +41 -0
- package/lib/Http/Server.js +177 -152
- package/lib/Logging/{Manager.js → Log.js} +68 -80
- package/lib/Mail/Mail.js +187 -0
- package/lib/Route/Router.js +416 -0
- package/lib/Session/File.js +135 -233
- package/lib/Session/Manager.js +117 -171
- package/lib/Session/Memory.js +28 -38
- package/lib/Session/Session.js +71 -107
- package/lib/Support/Str.js +103 -0
- package/lib/Translation/Lang.js +54 -0
- package/lib/View/Client/hmr-client.js +87 -51
- package/lib/View/Client/nitronjs-icon.png +0 -0
- package/lib/View/{Manager.js → View.js} +44 -29
- package/lib/index.d.ts +42 -8
- package/lib/index.js +19 -12
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +7 -1
- package/skeleton/resources/css/global.css +1 -0
- package/skeleton/resources/views/Site/Home.tsx +456 -79
- package/skeleton/tsconfig.json +6 -1
- package/lib/Auth/Manager.js +0 -111
- package/lib/Database/Connection.js +0 -61
- package/lib/Database/Manager.js +0 -162
- package/lib/Encryption/Manager.js +0 -47
- package/lib/Filesystem/Manager.js +0 -74
- package/lib/Hashing/Manager.js +0 -25
- package/lib/Mail/Manager.js +0 -120
- package/lib/Route/Loader.js +0 -80
- package/lib/Route/Manager.js +0 -286
- package/lib/Translation/Manager.js +0 -49
package/lib/Database/Model.js
CHANGED
|
@@ -1,29 +1,60 @@
|
|
|
1
1
|
import DB from './DB.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Base model class for database entities with Active Record pattern.
|
|
5
|
+
* Provides CRUD operations and query builder integration.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* class User extends Model {
|
|
9
|
+
* static table = "users";
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* const user = await User.find(1);
|
|
13
|
+
* user.name = "John";
|
|
14
|
+
* await user.save();
|
|
15
|
+
*/
|
|
3
16
|
class Model {
|
|
17
|
+
/** @type {string|null} Database table name - must be defined in subclass */
|
|
4
18
|
static table = null;
|
|
5
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new model instance with attribute proxy support.
|
|
22
|
+
* @param {Object} attrs - Initial attributes
|
|
23
|
+
*/
|
|
6
24
|
constructor(attrs = {}) {
|
|
7
25
|
Object.defineProperty(this, '_attributes', { value: {}, writable: true });
|
|
8
26
|
Object.defineProperty(this, '_original', { value: {}, writable: true });
|
|
9
27
|
Object.defineProperty(this, '_exists', { value: false, writable: true });
|
|
10
|
-
|
|
28
|
+
|
|
11
29
|
Object.assign(this._attributes, attrs);
|
|
12
30
|
this._original = { ...this._attributes };
|
|
13
|
-
|
|
31
|
+
|
|
14
32
|
return new Proxy(this, {
|
|
15
|
-
get(target, prop) {
|
|
16
|
-
if (
|
|
33
|
+
get: (target, prop) => {
|
|
34
|
+
if (typeof prop === 'symbol' || prop === 'constructor') {
|
|
35
|
+
return target[prop];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (prop.startsWith('_')) {
|
|
17
39
|
return target[prop];
|
|
18
40
|
}
|
|
41
|
+
|
|
42
|
+
if (prop in target && typeof target[prop] === 'function') {
|
|
43
|
+
return target[prop].bind(target);
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
return target._attributes[prop];
|
|
20
47
|
},
|
|
21
|
-
set(target, prop, value) {
|
|
22
|
-
if (prop.startsWith('_')) {
|
|
48
|
+
set: (target, prop, value) => {
|
|
49
|
+
if (typeof prop === 'symbol' || prop.startsWith('_')) {
|
|
23
50
|
target[prop] = value;
|
|
51
|
+
|
|
24
52
|
return true;
|
|
25
53
|
}
|
|
26
|
-
|
|
54
|
+
|
|
55
|
+
if (prop in target) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
27
58
|
|
|
28
59
|
target._attributes[prop] = value;
|
|
29
60
|
|
|
@@ -32,145 +63,188 @@ class Model {
|
|
|
32
63
|
});
|
|
33
64
|
}
|
|
34
65
|
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Static Query Methods
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all records from the table.
|
|
72
|
+
* @returns {Promise<Model[]>}
|
|
73
|
+
*/
|
|
35
74
|
static async get() {
|
|
36
|
-
|
|
37
|
-
throw new Error(`Model ${this.name} must define a static 'table' property`);
|
|
38
|
-
}
|
|
75
|
+
ensureTable(this);
|
|
39
76
|
|
|
40
77
|
const rows = await DB.table(this.table).get();
|
|
41
78
|
|
|
42
|
-
return rows.map(row =>
|
|
43
|
-
const instance = new this();
|
|
44
|
-
|
|
45
|
-
for (const [key, value] of Object.entries(row)) {
|
|
46
|
-
if (value instanceof Date) {
|
|
47
|
-
instance._attributes[key] = value.toISOString().slice(0, 19).replace('T', ' ');
|
|
48
|
-
} else {
|
|
49
|
-
instance._attributes[key] = value;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
instance._exists = true;
|
|
54
|
-
instance._original = { ...instance._attributes };
|
|
55
|
-
|
|
56
|
-
return instance;
|
|
57
|
-
});
|
|
79
|
+
return rows.map(row => hydrate(this, row));
|
|
58
80
|
}
|
|
59
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Find a record by ID.
|
|
84
|
+
* @param {number|string} id - Record ID
|
|
85
|
+
* @returns {Promise<Model|null>}
|
|
86
|
+
*/
|
|
60
87
|
static async find(id) {
|
|
61
|
-
|
|
62
|
-
throw new Error(`Model ${this.name} must define a static 'table' property`);
|
|
63
|
-
}
|
|
88
|
+
ensureTable(this);
|
|
64
89
|
|
|
65
|
-
const row = await DB.table(this.table).where(
|
|
90
|
+
const row = await DB.table(this.table).where('id', id).first();
|
|
66
91
|
|
|
67
|
-
|
|
92
|
+
return row ? hydrate(this, row) : null;
|
|
93
|
+
}
|
|
68
94
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
instance._exists = true;
|
|
80
|
-
instance._original = { ...instance._attributes };
|
|
81
|
-
|
|
82
|
-
return instance;
|
|
95
|
+
/**
|
|
96
|
+
* Get the first record from the table.
|
|
97
|
+
* @returns {Promise<Model|null>}
|
|
98
|
+
*/
|
|
99
|
+
static async first() {
|
|
100
|
+
ensureTable(this);
|
|
101
|
+
|
|
102
|
+
const row = await DB.table(this.table).first();
|
|
103
|
+
|
|
104
|
+
return row ? hydrate(this, row) : null;
|
|
83
105
|
}
|
|
84
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Add a WHERE clause to the query.
|
|
109
|
+
* @param {string|Object} column - Column name or conditions object
|
|
110
|
+
* @param {string} [operator] - Comparison operator or value (if 2 args)
|
|
111
|
+
* @param {*} [value] - Value to compare
|
|
112
|
+
* @returns {import('./QueryBuilder.js').default}
|
|
113
|
+
*/
|
|
85
114
|
static where(column, operator, value) {
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
ensureTable(this);
|
|
116
|
+
|
|
117
|
+
if (arguments.length === 2) {
|
|
118
|
+
return DB.table(this.table, null, this).where(column, operator);
|
|
88
119
|
}
|
|
89
120
|
|
|
90
121
|
return DB.table(this.table, null, this).where(column, operator, value);
|
|
91
122
|
}
|
|
92
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Select specific columns.
|
|
126
|
+
* @param {...string} columns - Column names
|
|
127
|
+
* @returns {import('./QueryBuilder.js').default}
|
|
128
|
+
*/
|
|
93
129
|
static select(...columns) {
|
|
94
|
-
|
|
95
|
-
throw new Error(`Model ${this.name} must define a static 'table' property`);
|
|
96
|
-
}
|
|
130
|
+
ensureTable(this);
|
|
97
131
|
|
|
98
132
|
return DB.table(this.table, null, this).select(...columns);
|
|
99
133
|
}
|
|
100
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Order results by column.
|
|
137
|
+
* @param {string} column - Column name
|
|
138
|
+
* @param {'ASC'|'DESC'} [direction='ASC'] - Sort direction
|
|
139
|
+
* @returns {import('./QueryBuilder.js').default}
|
|
140
|
+
*/
|
|
101
141
|
static orderBy(column, direction = 'ASC') {
|
|
102
|
-
|
|
103
|
-
throw new Error(`Model ${this.name} must define a static 'table' property`);
|
|
104
|
-
}
|
|
142
|
+
ensureTable(this);
|
|
105
143
|
|
|
106
144
|
return DB.table(this.table, null, this).orderBy(column, direction);
|
|
107
145
|
}
|
|
108
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Limit the number of results.
|
|
149
|
+
* @param {number} value - Maximum records to return
|
|
150
|
+
* @returns {import('./QueryBuilder.js').default}
|
|
151
|
+
*/
|
|
109
152
|
static limit(value) {
|
|
110
|
-
|
|
111
|
-
throw new Error(`Model ${this.name} must define a static 'table' property`);
|
|
112
|
-
}
|
|
153
|
+
ensureTable(this);
|
|
113
154
|
|
|
114
155
|
return DB.table(this.table, null, this).limit(value);
|
|
115
156
|
}
|
|
116
157
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return await DB.table(this.table, null, this).limit(1).first();
|
|
123
|
-
}
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// Instance Methods
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Save the model (insert or update).
|
|
164
|
+
* @returns {Promise<Model>}
|
|
165
|
+
*/
|
|
125
166
|
async save() {
|
|
126
|
-
const
|
|
167
|
+
const table = this.constructor.table;
|
|
127
168
|
const data = {};
|
|
128
169
|
|
|
129
170
|
for (const [key, value] of Object.entries(this._attributes)) {
|
|
130
|
-
if (value !== undefined && (key !==
|
|
171
|
+
if (value !== undefined && (key !== 'id' || !this._exists)) {
|
|
131
172
|
data[key] = value;
|
|
132
173
|
}
|
|
133
174
|
}
|
|
134
175
|
|
|
135
176
|
if (this._exists) {
|
|
136
|
-
|
|
137
|
-
await DB.table(constructor.table)
|
|
138
|
-
.where("id", primaryKeyValue)
|
|
139
|
-
.update(data);
|
|
140
|
-
|
|
141
|
-
Object.assign(this._attributes, data);
|
|
142
|
-
this._original = { ...this._attributes };
|
|
177
|
+
await DB.table(table).where('id', this._attributes.id).update(data);
|
|
143
178
|
}
|
|
144
179
|
else {
|
|
145
|
-
const id = await DB.table(
|
|
146
|
-
this._attributes
|
|
147
|
-
|
|
148
|
-
this._original = { ...this._attributes };
|
|
180
|
+
const id = await DB.table(table).insert(data);
|
|
181
|
+
this._attributes.id = id;
|
|
149
182
|
this._exists = true;
|
|
150
183
|
}
|
|
151
184
|
|
|
185
|
+
this._original = { ...this._attributes };
|
|
186
|
+
|
|
152
187
|
return this;
|
|
153
188
|
}
|
|
154
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Delete the model from database.
|
|
192
|
+
* @returns {Promise<boolean>}
|
|
193
|
+
* @throws {Error} If model doesn't exist in database
|
|
194
|
+
*/
|
|
155
195
|
async delete() {
|
|
156
|
-
const constructor = this.constructor;
|
|
157
|
-
const primaryKeyValue = this._attributes["id"];
|
|
158
|
-
|
|
159
196
|
if (!this._exists) {
|
|
160
197
|
throw new Error('Cannot delete a model that does not exist');
|
|
161
198
|
}
|
|
162
199
|
|
|
163
|
-
await DB.table(constructor.table)
|
|
164
|
-
.where("id", primaryKeyValue)
|
|
165
|
-
.delete();
|
|
166
|
-
|
|
200
|
+
await DB.table(this.constructor.table).where('id', this._attributes.id).delete();
|
|
167
201
|
this._exists = false;
|
|
202
|
+
|
|
168
203
|
return true;
|
|
169
204
|
}
|
|
170
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Convert model attributes to plain object.
|
|
208
|
+
* @returns {Object}
|
|
209
|
+
*/
|
|
171
210
|
toObject() {
|
|
172
211
|
return { ...this._attributes };
|
|
173
212
|
}
|
|
174
213
|
}
|
|
175
214
|
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
// Private Helper Functions (module-scoped to avoid ES6 static inheritance issues)
|
|
217
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validates that table property is defined on the model class.
|
|
221
|
+
* @param {typeof Model} modelClass
|
|
222
|
+
*/
|
|
223
|
+
function ensureTable(modelClass) {
|
|
224
|
+
if (!modelClass.table) {
|
|
225
|
+
throw new Error(`Model ${modelClass.name} must define a static 'table' property`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Creates a model instance from a database row.
|
|
231
|
+
* @param {typeof Model} modelClass
|
|
232
|
+
* @param {Object} row
|
|
233
|
+
* @returns {Model}
|
|
234
|
+
*/
|
|
235
|
+
function hydrate(modelClass, row) {
|
|
236
|
+
const instance = new modelClass();
|
|
237
|
+
|
|
238
|
+
for (const [key, value] of Object.entries(row)) {
|
|
239
|
+
instance._attributes[key] = value instanceof Date
|
|
240
|
+
? value.toISOString().slice(0, 19).replace('T', ' ')
|
|
241
|
+
: value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
instance._exists = true;
|
|
245
|
+
instance._original = { ...instance._attributes };
|
|
246
|
+
|
|
247
|
+
return instance;
|
|
248
|
+
}
|
|
249
|
+
|
|
176
250
|
export default Model;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { validateDirection, validateIdentifier, validateWhereOperator } from "./QueryValidation.js";
|
|
2
2
|
import Environment from "../Core/Environment.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* SQL query builder with fluent interface.
|
|
6
|
+
* Provides chainable methods for building safe parameterized queries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const users = await DB.table("users")
|
|
10
|
+
* .where("status", "active")
|
|
11
|
+
* .orderBy("created_at", "DESC")
|
|
12
|
+
* .limit(10)
|
|
13
|
+
* .get();
|
|
14
|
+
*/
|
|
4
15
|
class QueryBuilder {
|
|
5
16
|
#table = null;
|
|
6
17
|
#connection = null;
|
|
@@ -483,6 +494,26 @@ class QueryBuilder {
|
|
|
483
494
|
return parseInt(result) || 0;
|
|
484
495
|
}
|
|
485
496
|
|
|
497
|
+
async max(column) {
|
|
498
|
+
return await this.#aggregate('MAX', column);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async min(column) {
|
|
502
|
+
return await this.#aggregate('MIN', column);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async sum(column) {
|
|
506
|
+
const result = await this.#aggregate('SUM', column);
|
|
507
|
+
|
|
508
|
+
return parseFloat(result) || 0;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async avg(column) {
|
|
512
|
+
const result = await this.#aggregate('AVG', column);
|
|
513
|
+
|
|
514
|
+
return parseFloat(result) || 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
486
517
|
async #aggregate(func, column) {
|
|
487
518
|
const alias = func.toLowerCase();
|
|
488
519
|
const originalSelect = this.#selectColumns;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const RESERVED_KEYWORDS = new Set([
|
|
2
|
-
'order', 'key', 'group', 'index', 'table', 'column', 'select', 'insert',
|
|
3
|
-
'update', 'delete', 'from', 'where', 'join', 'left', 'right', 'inner',
|
|
4
|
-
'outer', 'on', 'and', 'or', 'not', 'null', 'true', 'false', 'like',
|
|
2
|
+
'order', 'key', 'group', 'index', 'table', 'column', 'select', 'insert',
|
|
3
|
+
'update', 'delete', 'from', 'where', 'join', 'left', 'right', 'inner',
|
|
4
|
+
'outer', 'on', 'and', 'or', 'not', 'null', 'true', 'false', 'like',
|
|
5
5
|
'in', 'between', 'is', 'as', 'by', 'asc', 'desc', 'limit', 'offset',
|
|
6
6
|
'having', 'distinct', 'all', 'any', 'exists', 'case', 'when', 'then',
|
|
7
7
|
'else', 'end', 'if', 'into', 'values', 'set', 'create', 'drop', 'alter',
|
|
@@ -12,72 +12,57 @@ const RESERVED_KEYWORDS = new Set([
|
|
|
12
12
|
'range', 'rows', 'rank', 'row', 'status', 'type', 'level', 'value', 'name'
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
const ALLOWED_WHERE_OPERATORS = new Set([
|
|
16
|
+
'=', '!=', '<>', '<', '>', '<=', '>=',
|
|
17
|
+
'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE',
|
|
18
|
+
'IN', 'NOT IN', 'IS', 'IS NOT',
|
|
19
|
+
'BETWEEN', 'NOT BETWEEN',
|
|
20
|
+
'REGEXP', 'NOT REGEXP', 'RLIKE', 'NOT RLIKE'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const IDENTIFIER_PATTERNS = [
|
|
24
|
+
/^[a-zA-Z_][a-zA-Z0-9_]*$/,
|
|
25
|
+
/^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/,
|
|
26
|
+
/^[a-zA-Z_][a-zA-Z0-9_]*\.\*$/
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Public Functions
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
32
|
|
|
22
33
|
export function validateIdentifier(identifier) {
|
|
23
34
|
if (typeof identifier !== 'string' || identifier.length === 0) {
|
|
24
35
|
throw new Error('Identifier must be a non-empty string');
|
|
25
36
|
}
|
|
26
|
-
|
|
27
37
|
if (identifier.length > 64) {
|
|
28
38
|
throw new Error(`Invalid identifier: "${identifier}". Maximum length is 64 characters.`);
|
|
29
39
|
}
|
|
30
|
-
|
|
31
40
|
if (identifier === '*' || identifier === '*.*') {
|
|
32
41
|
return identifier;
|
|
33
42
|
}
|
|
34
|
-
|
|
35
43
|
if (/--|#|\/\*|\*\//.test(identifier)) {
|
|
36
44
|
throw new Error(`Invalid identifier: "${identifier}". SQL comments are not allowed.`);
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/,
|
|
42
|
-
/^[a-zA-Z_][a-zA-Z0-9_]*\.\*$/
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const isValid = patterns.some(pattern => pattern.test(identifier));
|
|
46
|
-
|
|
47
|
-
if (!isValid) {
|
|
48
|
-
throw new Error(`Invalid identifier: "${identifier}". Must be alphanumeric with underscores, optionally qualified with table name.`);
|
|
47
|
+
if (!IDENTIFIER_PATTERNS.some(p => p.test(identifier))) {
|
|
48
|
+
throw new Error(`Invalid identifier: "${identifier}". Must be alphanumeric with underscores.`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
if (identifier.includes('.')) {
|
|
52
52
|
const [table, column] = identifier.split('.');
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return `${escapeIdentifier(table)}.${escapeIdentifier(column)}`;
|
|
53
|
+
return column === '*'
|
|
54
|
+
? `${escapeKeyword(table)}.*`
|
|
55
|
+
: `${escapeKeyword(table)}.${escapeKeyword(column)}`;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
return
|
|
58
|
+
return escapeKeyword(identifier);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
export function validateWhereOperator(operator) {
|
|
63
62
|
const normalized = String(operator).trim().toUpperCase();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE',
|
|
67
|
-
'IN', 'NOT IN',
|
|
68
|
-
'IS', 'IS NOT',
|
|
69
|
-
'BETWEEN', 'NOT BETWEEN',
|
|
70
|
-
'REGEXP', 'NOT REGEXP',
|
|
71
|
-
'RLIKE', 'NOT RLIKE'
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
if (!allowedOperators.has(normalized)) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
`Invalid WHERE operator: '${operator}'. ` +
|
|
77
|
-
`Allowed operators: ${[...allowedOperators].join(', ')}`
|
|
78
|
-
);
|
|
63
|
+
if (!ALLOWED_WHERE_OPERATORS.has(normalized)) {
|
|
64
|
+
throw new Error(`Invalid WHERE operator: '${operator}'.`);
|
|
79
65
|
}
|
|
80
|
-
|
|
81
66
|
return normalized;
|
|
82
67
|
}
|
|
83
68
|
|
|
@@ -86,6 +71,13 @@ export function validateDirection(direction) {
|
|
|
86
71
|
if (upper !== 'ASC' && upper !== 'DESC') {
|
|
87
72
|
throw new Error(`Invalid direction: ${direction}. Use 'ASC' or 'DESC'.`);
|
|
88
73
|
}
|
|
89
|
-
|
|
90
74
|
return upper;
|
|
91
75
|
}
|
|
76
|
+
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
// Private Functions
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function escapeKeyword(identifier) {
|
|
82
|
+
return RESERVED_KEYWORDS.has(identifier.toLowerCase()) ? `\`${identifier}\`` : identifier;
|
|
83
|
+
}
|
|
@@ -6,6 +6,10 @@ class Blueprint {
|
|
|
6
6
|
this.#tableName = tableName;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Public Methods
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
9
13
|
getTableName() {
|
|
10
14
|
return this.#tableName;
|
|
11
15
|
}
|
|
@@ -14,43 +18,8 @@ class Blueprint {
|
|
|
14
18
|
return this.#columns;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
#addColumn(type, name, options = {}) {
|
|
18
|
-
const column = {
|
|
19
|
-
name,
|
|
20
|
-
type,
|
|
21
|
-
...options,
|
|
22
|
-
modifiers: {
|
|
23
|
-
nullable: false,
|
|
24
|
-
default: null,
|
|
25
|
-
unique: false
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
this.#columns.push(column);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
nullable: () => {
|
|
33
|
-
column.modifiers.nullable = true;
|
|
34
|
-
return this;
|
|
35
|
-
},
|
|
36
|
-
default: (value) => {
|
|
37
|
-
column.modifiers.default = value;
|
|
38
|
-
return this;
|
|
39
|
-
},
|
|
40
|
-
unique: () => {
|
|
41
|
-
column.modifiers.unique = true;
|
|
42
|
-
return this;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
21
|
id() {
|
|
48
|
-
|
|
49
|
-
name: 'id',
|
|
50
|
-
type: 'id',
|
|
51
|
-
modifiers: {}
|
|
52
|
-
};
|
|
53
|
-
this.#columns.push(column);
|
|
22
|
+
this.#columns.push({ name: 'id', type: 'id', modifiers: {} });
|
|
54
23
|
return this;
|
|
55
24
|
}
|
|
56
25
|
|
|
@@ -81,6 +50,26 @@ class Blueprint {
|
|
|
81
50
|
json(name) {
|
|
82
51
|
return this.#addColumn('json', name);
|
|
83
52
|
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Private Methods
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
#addColumn(type, name, options = {}) {
|
|
59
|
+
const column = {
|
|
60
|
+
name,
|
|
61
|
+
type,
|
|
62
|
+
...options,
|
|
63
|
+
modifiers: { nullable: false, default: null, unique: false }
|
|
64
|
+
};
|
|
65
|
+
this.#columns.push(column);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
nullable: () => { column.modifiers.nullable = true; return this; },
|
|
69
|
+
default: (value) => { column.modifiers.default = value; return this; },
|
|
70
|
+
unique: () => { column.modifiers.unique = true; return this; }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
84
73
|
}
|
|
85
74
|
|
|
86
75
|
export default Blueprint;
|