outlet-orm 2.5.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.
- package/LICENSE +21 -0
- package/README.md +705 -0
- package/bin/convert.js +679 -0
- package/bin/init.js +190 -0
- package/bin/migrate.js +442 -0
- package/lib/Database/DatabaseConnection.js +4 -0
- package/lib/Migrations/Migration.js +48 -0
- package/lib/Migrations/MigrationManager.js +326 -0
- package/lib/Schema/Schema.js +790 -0
- package/package.json +75 -0
- package/src/DatabaseConnection.js +697 -0
- package/src/Model.js +659 -0
- package/src/QueryBuilder.js +710 -0
- package/src/Relations/BelongsToManyRelation.js +466 -0
- package/src/Relations/BelongsToRelation.js +127 -0
- package/src/Relations/HasManyRelation.js +125 -0
- package/src/Relations/HasManyThroughRelation.js +112 -0
- package/src/Relations/HasOneRelation.js +114 -0
- package/src/Relations/HasOneThroughRelation.js +105 -0
- package/src/Relations/MorphManyRelation.js +69 -0
- package/src/Relations/MorphOneRelation.js +68 -0
- package/src/Relations/MorphToRelation.js +110 -0
- package/src/Relations/Relation.js +31 -0
- package/src/index.js +23 -0
- package/types/index.d.ts +272 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Builder - Inspired by Laravel Schema
|
|
3
|
+
* Provides a fluent interface for creating and modifying database tables
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Schema {
|
|
7
|
+
constructor(connection) {
|
|
8
|
+
this.connection = connection;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a new table
|
|
13
|
+
* @param {string} tableName
|
|
14
|
+
* @param {Function} callback
|
|
15
|
+
*/
|
|
16
|
+
async create(tableName, callback) {
|
|
17
|
+
const blueprint = new Blueprint(tableName, this.connection);
|
|
18
|
+
callback(blueprint);
|
|
19
|
+
const statements = blueprint.toSql('create');
|
|
20
|
+
|
|
21
|
+
for (const sql of statements) {
|
|
22
|
+
await this.connection.execute(sql);
|
|
23
|
+
}
|
|
24
|
+
console.log(`✓ Table '${tableName}' created successfully`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Modify an existing table
|
|
29
|
+
* @param {string} tableName
|
|
30
|
+
* @param {Function} callback
|
|
31
|
+
*/
|
|
32
|
+
async table(tableName, callback) {
|
|
33
|
+
const blueprint = new Blueprint(tableName, this.connection);
|
|
34
|
+
blueprint.isModifying = true;
|
|
35
|
+
callback(blueprint);
|
|
36
|
+
const statements = blueprint.toSql('alter');
|
|
37
|
+
|
|
38
|
+
for (const sql of statements) {
|
|
39
|
+
await this.connection.execute(sql);
|
|
40
|
+
}
|
|
41
|
+
console.log(`✓ Table '${tableName}' modified successfully`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Rename a table
|
|
46
|
+
* @param {string} from
|
|
47
|
+
* @param {string} to
|
|
48
|
+
*/
|
|
49
|
+
async rename(from, to) {
|
|
50
|
+
const driver = this.connection.config.driver;
|
|
51
|
+
let sql;
|
|
52
|
+
|
|
53
|
+
switch (driver) {
|
|
54
|
+
case 'mysql':
|
|
55
|
+
sql = `RENAME TABLE ${from} TO ${to}`;
|
|
56
|
+
break;
|
|
57
|
+
case 'postgres':
|
|
58
|
+
case 'postgresql':
|
|
59
|
+
case 'sqlite':
|
|
60
|
+
sql = `ALTER TABLE ${from} RENAME TO ${to}`;
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
throw new Error(`Unsupported driver: ${driver}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await this.connection.execute(sql);
|
|
67
|
+
console.log(`✓ Table '${from}' renamed to '${to}'`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Drop a table
|
|
72
|
+
* @param {string} tableName
|
|
73
|
+
*/
|
|
74
|
+
async drop(tableName) {
|
|
75
|
+
const sql = `DROP TABLE ${tableName}`;
|
|
76
|
+
await this.connection.execute(sql);
|
|
77
|
+
console.log(`✓ Table '${tableName}' dropped successfully`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drop a table if it exists
|
|
82
|
+
* @param {string} tableName
|
|
83
|
+
*/
|
|
84
|
+
async dropIfExists(tableName) {
|
|
85
|
+
const sql = `DROP TABLE IF EXISTS ${tableName}`;
|
|
86
|
+
await this.connection.execute(sql);
|
|
87
|
+
console.log(`✓ Table '${tableName}' dropped if existed`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a table exists
|
|
92
|
+
* @param {string} tableName
|
|
93
|
+
* @returns {Promise<boolean>}
|
|
94
|
+
*/
|
|
95
|
+
async hasTable(tableName) {
|
|
96
|
+
const driver = this.connection.config.driver;
|
|
97
|
+
let sql;
|
|
98
|
+
|
|
99
|
+
switch (driver) {
|
|
100
|
+
case 'mysql':
|
|
101
|
+
sql = `SELECT COUNT(*) as count FROM information_schema.tables
|
|
102
|
+
WHERE table_schema = DATABASE() AND table_name = '${tableName}'`;
|
|
103
|
+
break;
|
|
104
|
+
case 'postgres':
|
|
105
|
+
case 'postgresql':
|
|
106
|
+
sql = `SELECT COUNT(*) as count FROM information_schema.tables
|
|
107
|
+
WHERE table_schema = 'public' AND table_name = '${tableName}'`;
|
|
108
|
+
break;
|
|
109
|
+
case 'sqlite':
|
|
110
|
+
sql = `SELECT COUNT(*) as count FROM sqlite_master
|
|
111
|
+
WHERE type='table' AND name='${tableName}'`;
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
throw new Error(`Unsupported driver: ${driver}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await this.connection.execute(sql);
|
|
118
|
+
return result[0].count > 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if a column exists in a table
|
|
123
|
+
* @param {string} tableName
|
|
124
|
+
* @param {string} columnName
|
|
125
|
+
* @returns {Promise<boolean>}
|
|
126
|
+
*/
|
|
127
|
+
async hasColumn(tableName, columnName) {
|
|
128
|
+
const driver = this.connection.config.driver;
|
|
129
|
+
let sql;
|
|
130
|
+
|
|
131
|
+
switch (driver) {
|
|
132
|
+
case 'mysql':
|
|
133
|
+
sql = `SELECT COUNT(*) as count FROM information_schema.columns
|
|
134
|
+
WHERE table_schema = DATABASE()
|
|
135
|
+
AND table_name = '${tableName}'
|
|
136
|
+
AND column_name = '${columnName}'`;
|
|
137
|
+
break;
|
|
138
|
+
case 'postgres':
|
|
139
|
+
case 'postgresql':
|
|
140
|
+
sql = `SELECT COUNT(*) as count FROM information_schema.columns
|
|
141
|
+
WHERE table_schema = 'public'
|
|
142
|
+
AND table_name = '${tableName}'
|
|
143
|
+
AND column_name = '${columnName}'`;
|
|
144
|
+
break;
|
|
145
|
+
case 'sqlite':
|
|
146
|
+
sql = `SELECT COUNT(*) as count FROM pragma_table_info('${tableName}')
|
|
147
|
+
WHERE name = '${columnName}'`;
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`Unsupported driver: ${driver}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await this.connection.execute(sql);
|
|
154
|
+
return result[0].count > 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Blueprint - Represents a table structure
|
|
160
|
+
*/
|
|
161
|
+
class Blueprint {
|
|
162
|
+
constructor(tableName, connection) {
|
|
163
|
+
this.tableName = tableName;
|
|
164
|
+
this.connection = connection;
|
|
165
|
+
this.columns = [];
|
|
166
|
+
this.commands = [];
|
|
167
|
+
this.isModifying = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create an auto-incrementing ID column
|
|
172
|
+
*/
|
|
173
|
+
id(columnName = 'id') {
|
|
174
|
+
return this.bigIncrements(columnName);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a big integer auto-increment column
|
|
179
|
+
*/
|
|
180
|
+
bigIncrements(columnName) {
|
|
181
|
+
const column = new ColumnDefinition(columnName, 'BIGINT');
|
|
182
|
+
column.autoIncrement().unsigned().primary();
|
|
183
|
+
this.columns.push(column);
|
|
184
|
+
return column;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create a string column
|
|
189
|
+
*/
|
|
190
|
+
string(columnName, length = 255) {
|
|
191
|
+
const column = new ColumnDefinition(columnName, 'VARCHAR', { length });
|
|
192
|
+
this.columns.push(column);
|
|
193
|
+
return column;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create a text column
|
|
198
|
+
*/
|
|
199
|
+
text(columnName) {
|
|
200
|
+
const column = new ColumnDefinition(columnName, 'TEXT');
|
|
201
|
+
this.columns.push(column);
|
|
202
|
+
return column;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create an integer column
|
|
207
|
+
*/
|
|
208
|
+
integer(columnName) {
|
|
209
|
+
const column = new ColumnDefinition(columnName, 'INT');
|
|
210
|
+
this.columns.push(column);
|
|
211
|
+
return column;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a big integer column
|
|
216
|
+
*/
|
|
217
|
+
bigInteger(columnName) {
|
|
218
|
+
const column = new ColumnDefinition(columnName, 'BIGINT');
|
|
219
|
+
this.columns.push(column);
|
|
220
|
+
return column;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a boolean column
|
|
225
|
+
*/
|
|
226
|
+
boolean(columnName) {
|
|
227
|
+
const column = new ColumnDefinition(columnName, 'TINYINT', { length: 1 });
|
|
228
|
+
this.columns.push(column);
|
|
229
|
+
return column;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create a date column
|
|
234
|
+
*/
|
|
235
|
+
date(columnName) {
|
|
236
|
+
const column = new ColumnDefinition(columnName, 'DATE');
|
|
237
|
+
this.columns.push(column);
|
|
238
|
+
return column;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create a datetime column
|
|
243
|
+
*/
|
|
244
|
+
datetime(columnName) {
|
|
245
|
+
const column = new ColumnDefinition(columnName, 'DATETIME');
|
|
246
|
+
this.columns.push(column);
|
|
247
|
+
return column;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create a timestamp column
|
|
252
|
+
*/
|
|
253
|
+
timestamp(columnName) {
|
|
254
|
+
const column = new ColumnDefinition(columnName, 'TIMESTAMP');
|
|
255
|
+
this.columns.push(column);
|
|
256
|
+
return column;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create timestamps (created_at, updated_at)
|
|
261
|
+
*/
|
|
262
|
+
timestamps(nullable = false) {
|
|
263
|
+
const createdAt = this.timestamp('created_at');
|
|
264
|
+
const updatedAt = this.timestamp('updated_at');
|
|
265
|
+
|
|
266
|
+
if (nullable) {
|
|
267
|
+
createdAt.nullable();
|
|
268
|
+
updatedAt.nullable();
|
|
269
|
+
} else {
|
|
270
|
+
createdAt.useCurrent();
|
|
271
|
+
updatedAt.useCurrent().useCurrentOnUpdate();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Create a soft delete column (deleted_at)
|
|
279
|
+
*/
|
|
280
|
+
softDeletes(columnName = 'deleted_at') {
|
|
281
|
+
return this.timestamp(columnName).nullable();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Create a decimal column
|
|
286
|
+
*/
|
|
287
|
+
decimal(columnName, precision = 8, scale = 2) {
|
|
288
|
+
const column = new ColumnDefinition(columnName, 'DECIMAL', { precision, scale });
|
|
289
|
+
this.columns.push(column);
|
|
290
|
+
return column;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Create a float column
|
|
295
|
+
*/
|
|
296
|
+
float(columnName, precision = 8, scale = 2) {
|
|
297
|
+
const column = new ColumnDefinition(columnName, 'FLOAT', { precision, scale });
|
|
298
|
+
this.columns.push(column);
|
|
299
|
+
return column;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Create a JSON column
|
|
304
|
+
*/
|
|
305
|
+
json(columnName) {
|
|
306
|
+
const column = new ColumnDefinition(columnName, 'JSON');
|
|
307
|
+
this.columns.push(column);
|
|
308
|
+
return column;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create an enum column
|
|
313
|
+
*/
|
|
314
|
+
enum(columnName, values) {
|
|
315
|
+
const column = new ColumnDefinition(columnName, 'ENUM', { values });
|
|
316
|
+
this.columns.push(column);
|
|
317
|
+
return column;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create a UUID column
|
|
322
|
+
*/
|
|
323
|
+
uuid(columnName) {
|
|
324
|
+
const column = new ColumnDefinition(columnName, 'CHAR', { length: 36 });
|
|
325
|
+
this.columns.push(column);
|
|
326
|
+
return column;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Create a foreign ID column
|
|
331
|
+
*/
|
|
332
|
+
foreignId(columnName) {
|
|
333
|
+
const column = new ColumnDefinition(columnName, 'BIGINT');
|
|
334
|
+
column.unsigned();
|
|
335
|
+
this.columns.push(column);
|
|
336
|
+
return column;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Add a foreign key constraint
|
|
341
|
+
*/
|
|
342
|
+
foreign(columnName) {
|
|
343
|
+
const foreignKey = new ForeignKeyDefinition(columnName);
|
|
344
|
+
this.commands.push({ type: 'foreign', foreignKey });
|
|
345
|
+
return foreignKey;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Add an index
|
|
350
|
+
*/
|
|
351
|
+
index(columns, indexName = null) {
|
|
352
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
353
|
+
this.commands.push({
|
|
354
|
+
type: 'index',
|
|
355
|
+
columns: cols,
|
|
356
|
+
name: indexName || `${this.tableName}_${cols.join('_')}_index`
|
|
357
|
+
});
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Add a unique index
|
|
363
|
+
*/
|
|
364
|
+
unique(columns, indexName = null) {
|
|
365
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
366
|
+
this.commands.push({
|
|
367
|
+
type: 'unique',
|
|
368
|
+
columns: cols,
|
|
369
|
+
name: indexName || `${this.tableName}_${cols.join('_')}_unique`
|
|
370
|
+
});
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Add a fulltext index
|
|
376
|
+
*/
|
|
377
|
+
fullText(columns, indexName = null) {
|
|
378
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
379
|
+
this.commands.push({
|
|
380
|
+
type: 'fulltext',
|
|
381
|
+
columns: cols,
|
|
382
|
+
name: indexName || `${this.tableName}_${cols.join('_')}_fulltext`
|
|
383
|
+
});
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Drop a column
|
|
389
|
+
*/
|
|
390
|
+
dropColumn(columns) {
|
|
391
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
392
|
+
this.commands.push({ type: 'dropColumn', columns: cols });
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Drop a foreign key
|
|
398
|
+
*/
|
|
399
|
+
dropForeign(columns) {
|
|
400
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
401
|
+
this.commands.push({ type: 'dropForeign', columns: cols });
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Drop an index
|
|
407
|
+
*/
|
|
408
|
+
dropIndex(columns) {
|
|
409
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
410
|
+
const indexName = `${this.tableName}_${cols.join('_')}_index`;
|
|
411
|
+
this.commands.push({ type: 'dropIndex', name: indexName });
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Drop timestamps
|
|
417
|
+
*/
|
|
418
|
+
dropTimestamps() {
|
|
419
|
+
return this.dropColumn(['created_at', 'updated_at']);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Rename a column
|
|
424
|
+
*/
|
|
425
|
+
renameColumn(from, to) {
|
|
426
|
+
this.commands.push({ type: 'renameColumn', from, to });
|
|
427
|
+
return this;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Generate SQL statements
|
|
432
|
+
* @param {string} action - 'create' or 'alter'
|
|
433
|
+
* @returns {string[]} Array of SQL statements
|
|
434
|
+
*/
|
|
435
|
+
toSql(action) {
|
|
436
|
+
if (action === 'create') {
|
|
437
|
+
return [this.toCreateSql()];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (action === 'alter') {
|
|
441
|
+
return this.toAlterSql();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Generate CREATE TABLE SQL
|
|
449
|
+
* @returns {string} SQL statement
|
|
450
|
+
*/
|
|
451
|
+
toCreateSql() {
|
|
452
|
+
const driver = this.connection.config.driver;
|
|
453
|
+
const columnDefinitions = this.columns.map(col => col.toSql(driver)).join(',\n ');
|
|
454
|
+
const constraints = this.getConstraints();
|
|
455
|
+
|
|
456
|
+
let sql = `CREATE TABLE ${this.tableName} (\n ${columnDefinitions}`;
|
|
457
|
+
|
|
458
|
+
if (constraints) {
|
|
459
|
+
sql += `,\n ${constraints}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
sql += '\n)';
|
|
463
|
+
|
|
464
|
+
return sql;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Generate ALTER TABLE SQL
|
|
469
|
+
* @returns {string[]} Array of SQL statements
|
|
470
|
+
*/
|
|
471
|
+
toAlterSql() {
|
|
472
|
+
const statements = [];
|
|
473
|
+
|
|
474
|
+
// Add new columns
|
|
475
|
+
const driver = this.connection.config.driver;
|
|
476
|
+
for (const column of this.columns) {
|
|
477
|
+
let sql = `ALTER TABLE ${this.tableName} ADD COLUMN ${column.toSql(driver)}`;
|
|
478
|
+
statements.push(sql);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Process commands
|
|
482
|
+
for (const command of this.commands) {
|
|
483
|
+
switch (command.type) {
|
|
484
|
+
case 'dropColumn':
|
|
485
|
+
for (const col of command.columns) {
|
|
486
|
+
statements.push(`ALTER TABLE ${this.tableName} DROP COLUMN ${col}`);
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'renameColumn':
|
|
491
|
+
statements.push(`ALTER TABLE ${this.tableName} CHANGE ${command.from} ${command.to}`);
|
|
492
|
+
break;
|
|
493
|
+
|
|
494
|
+
case 'foreign': {
|
|
495
|
+
const fk = command.foreignKey;
|
|
496
|
+
statements.push(
|
|
497
|
+
`ALTER TABLE ${this.tableName} ADD CONSTRAINT ${fk.name} ` +
|
|
498
|
+
`FOREIGN KEY (${fk.column}) REFERENCES ${fk.references.table}(${fk.references.column})` +
|
|
499
|
+
(fk.onDelete ? ` ON DELETE ${fk.onDelete}` : '') +
|
|
500
|
+
(fk.onUpdate ? ` ON UPDATE ${fk.onUpdate}` : '')
|
|
501
|
+
);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
case 'dropForeign': {
|
|
506
|
+
const fkName = `${this.tableName}_${command.columns.join('_')}_foreign`;
|
|
507
|
+
statements.push(`ALTER TABLE ${this.tableName} DROP FOREIGN KEY ${fkName}`);
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
case 'index':
|
|
512
|
+
statements.push(
|
|
513
|
+
`ALTER TABLE ${this.tableName} ADD INDEX ${command.name} (${command.columns.join(', ')})`
|
|
514
|
+
);
|
|
515
|
+
break;
|
|
516
|
+
|
|
517
|
+
case 'unique':
|
|
518
|
+
statements.push(
|
|
519
|
+
`ALTER TABLE ${this.tableName} ADD UNIQUE ${command.name} (${command.columns.join(', ')})`
|
|
520
|
+
);
|
|
521
|
+
break;
|
|
522
|
+
|
|
523
|
+
case 'fulltext':
|
|
524
|
+
statements.push(
|
|
525
|
+
`ALTER TABLE ${this.tableName} ADD FULLTEXT ${command.name} (${command.columns.join(', ')})`
|
|
526
|
+
);
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'dropIndex':
|
|
530
|
+
statements.push(`ALTER TABLE ${this.tableName} DROP INDEX ${command.name}`);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return statements;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get table constraints (PRIMARY KEY, FOREIGN KEY, etc.)
|
|
540
|
+
*/
|
|
541
|
+
getConstraints() {
|
|
542
|
+
const constraints = [];
|
|
543
|
+
const driver = this.connection?.config?.driver || 'mysql';
|
|
544
|
+
|
|
545
|
+
// Primary keys
|
|
546
|
+
const primaryKeys = this.columns.filter(col => col.isPrimary);
|
|
547
|
+
// In SQLite, if a column is autoincrementing integer PK, it must be declared at column level,
|
|
548
|
+
// so skip table-level PRIMARY KEY constraints to avoid duplication.
|
|
549
|
+
const hasSqliteAutoInc = driver === 'sqlite' && this.columns.some(col => col.isAutoIncrement);
|
|
550
|
+
if (primaryKeys.length > 0 && !hasSqliteAutoInc) {
|
|
551
|
+
const pkColumns = primaryKeys.map(col => col.name).join(', ');
|
|
552
|
+
constraints.push(`PRIMARY KEY (${pkColumns})`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Foreign keys
|
|
556
|
+
for (const command of this.commands) {
|
|
557
|
+
if (command.type === 'foreign') {
|
|
558
|
+
const fk = command.foreignKey;
|
|
559
|
+
let constraint = `CONSTRAINT ${fk.name} FOREIGN KEY (${fk.column}) ` +
|
|
560
|
+
`REFERENCES ${fk.references.table}(${fk.references.column})`;
|
|
561
|
+
|
|
562
|
+
if (fk.onDelete) {
|
|
563
|
+
constraint += ` ON DELETE ${fk.onDelete}`;
|
|
564
|
+
}
|
|
565
|
+
if (fk.onUpdate) {
|
|
566
|
+
constraint += ` ON UPDATE ${fk.onUpdate}`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
constraints.push(constraint);
|
|
570
|
+
} else if (command.type === 'unique') {
|
|
571
|
+
if (driver === 'sqlite') {
|
|
572
|
+
constraints.push(`UNIQUE (${command.columns.join(', ')})`);
|
|
573
|
+
} else {
|
|
574
|
+
constraints.push(`UNIQUE KEY ${command.name} (${command.columns.join(', ')})`);
|
|
575
|
+
}
|
|
576
|
+
} else if (command.type === 'index') {
|
|
577
|
+
if (driver !== 'sqlite') {
|
|
578
|
+
constraints.push(`KEY ${command.name} (${command.columns.join(', ')})`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return constraints.join(',\n ');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Column Definition
|
|
589
|
+
*/
|
|
590
|
+
class ColumnDefinition {
|
|
591
|
+
constructor(name, type, options = {}) {
|
|
592
|
+
this.name = name;
|
|
593
|
+
this.type = type;
|
|
594
|
+
this.options = options;
|
|
595
|
+
this.isPrimary = false;
|
|
596
|
+
this.isUnique = false;
|
|
597
|
+
this.isNullable = false;
|
|
598
|
+
this.isUnsigned = false;
|
|
599
|
+
this.isAutoIncrement = false;
|
|
600
|
+
this.defaultValue = null;
|
|
601
|
+
this.commentText = null;
|
|
602
|
+
this.afterColumn = null;
|
|
603
|
+
this.isFirst = false;
|
|
604
|
+
this.useCurrentTimestamp = false;
|
|
605
|
+
this.useCurrentOnUpdateTimestamp = false;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
primary() {
|
|
609
|
+
this.isPrimary = true;
|
|
610
|
+
return this;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
unique() {
|
|
614
|
+
this.isUnique = true;
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
nullable() {
|
|
619
|
+
this.isNullable = true;
|
|
620
|
+
return this;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
unsigned() {
|
|
624
|
+
this.isUnsigned = true;
|
|
625
|
+
return this;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
autoIncrement() {
|
|
629
|
+
this.isAutoIncrement = true;
|
|
630
|
+
return this;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
default(value) {
|
|
634
|
+
this.defaultValue = value;
|
|
635
|
+
return this;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
comment(text) {
|
|
639
|
+
this.commentText = text;
|
|
640
|
+
return this;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
after(columnName) {
|
|
644
|
+
this.afterColumn = columnName;
|
|
645
|
+
return this;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
first() {
|
|
649
|
+
this.isFirst = true;
|
|
650
|
+
return this;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
useCurrent() {
|
|
654
|
+
this.useCurrentTimestamp = true;
|
|
655
|
+
return this;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
useCurrentOnUpdate() {
|
|
659
|
+
this.useCurrentOnUpdateTimestamp = true;
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Generate SQL for this column
|
|
665
|
+
*/
|
|
666
|
+
toSql(driver = 'mysql') {
|
|
667
|
+
let sql = `${this.name} ${this.getTypeDefinition(driver)}`;
|
|
668
|
+
|
|
669
|
+
if (this.isUnsigned && ['INT', 'BIGINT', 'TINYINT'].includes(this.type) && driver !== 'sqlite') {
|
|
670
|
+
sql += ' UNSIGNED';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!this.isNullable && !this.isPrimary) {
|
|
674
|
+
sql += ' NOT NULL';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (this.isAutoIncrement) {
|
|
678
|
+
if (driver === 'sqlite') {
|
|
679
|
+
// In SQLite, autoincrement must be declared as INTEGER PRIMARY KEY AUTOINCREMENT
|
|
680
|
+
sql = `${this.name} INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
681
|
+
} else {
|
|
682
|
+
sql += ' AUTO_INCREMENT';
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (this.useCurrentTimestamp) {
|
|
687
|
+
sql += ' DEFAULT CURRENT_TIMESTAMP';
|
|
688
|
+
} else if (this.defaultValue !== null) {
|
|
689
|
+
sql += ` DEFAULT ${this.formatDefaultValue()}`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (this.useCurrentOnUpdateTimestamp && driver !== 'sqlite') {
|
|
693
|
+
sql += ' ON UPDATE CURRENT_TIMESTAMP';
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (this.isUnique) {
|
|
697
|
+
sql += ' UNIQUE';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (this.commentText) {
|
|
701
|
+
sql += ` COMMENT '${this.commentText}'`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return sql;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
getTypeDefinition(driver = 'mysql') {
|
|
708
|
+
const { length, precision, scale, values } = this.options;
|
|
709
|
+
|
|
710
|
+
switch (this.type) {
|
|
711
|
+
case 'VARCHAR':
|
|
712
|
+
return driver === 'sqlite' ? 'TEXT' : `VARCHAR(${length})`;
|
|
713
|
+
case 'CHAR':
|
|
714
|
+
return driver === 'sqlite' ? 'TEXT' : `CHAR(${length})`;
|
|
715
|
+
case 'DECIMAL':
|
|
716
|
+
return `DECIMAL(${precision}, ${scale})`;
|
|
717
|
+
case 'FLOAT':
|
|
718
|
+
return precision ? `FLOAT(${precision}, ${scale})` : 'FLOAT';
|
|
719
|
+
case 'ENUM': {
|
|
720
|
+
if (driver === 'sqlite') return 'TEXT';
|
|
721
|
+
const enumValues = values.map(v => `'${v}'`).join(', ');
|
|
722
|
+
return `ENUM(${enumValues})`;
|
|
723
|
+
}
|
|
724
|
+
default:
|
|
725
|
+
return this.type;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
formatDefaultValue() {
|
|
730
|
+
if (typeof this.defaultValue === 'string') {
|
|
731
|
+
return `'${this.defaultValue}'`;
|
|
732
|
+
}
|
|
733
|
+
return this.defaultValue;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Foreign Key Definition
|
|
739
|
+
*/
|
|
740
|
+
class ForeignKeyDefinition {
|
|
741
|
+
constructor(column) {
|
|
742
|
+
this.column = column;
|
|
743
|
+
this.references = { table: null, column: 'id' };
|
|
744
|
+
this.onDelete = null;
|
|
745
|
+
this.onUpdate = null;
|
|
746
|
+
this.name = null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
references(column) {
|
|
750
|
+
this.references.column = column;
|
|
751
|
+
return this;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
on(table) {
|
|
755
|
+
this.references.table = table;
|
|
756
|
+
this.name = `${table}_${this.column}_foreign`;
|
|
757
|
+
return this;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
constrained(table = null) {
|
|
761
|
+
if (table) {
|
|
762
|
+
this.references.table = table;
|
|
763
|
+
} else {
|
|
764
|
+
// Infer table name from column name (remove _id suffix)
|
|
765
|
+
this.references.table = this.column.replace(/_id$/, '') + 's';
|
|
766
|
+
}
|
|
767
|
+
this.name = `${this.references.table}_${this.column}_foreign`;
|
|
768
|
+
return this;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
onDelete(action) {
|
|
772
|
+
this.onDelete = action.toUpperCase();
|
|
773
|
+
return this;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
onUpdate(action) {
|
|
777
|
+
this.onUpdate = action.toUpperCase();
|
|
778
|
+
return this;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
cascadeOnDelete() {
|
|
782
|
+
return this.onDelete('cascade');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
cascadeOnUpdate() {
|
|
786
|
+
return this.onUpdate('cascade');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
module.exports = { Schema, Blueprint, ColumnDefinition, ForeignKeyDefinition };
|