@nitronjs/framework 0.1.22 → 0.1.23

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 (34) hide show
  1. package/cli/njs.js +33 -5
  2. package/lib/Console/Commands/MigrateCommand.js +27 -70
  3. package/lib/Console/Commands/MigrateFreshCommand.js +49 -66
  4. package/lib/Console/Commands/MigrateRollbackCommand.js +52 -0
  5. package/lib/Console/Commands/MigrateStatusCommand.js +38 -0
  6. package/lib/Console/Commands/SeedCommand.js +36 -65
  7. package/lib/Core/Paths.js +8 -0
  8. package/lib/Database/Migration/Checksum.js +23 -0
  9. package/lib/Database/Migration/MigrationRepository.js +92 -0
  10. package/lib/Database/Migration/MigrationRunner.js +327 -0
  11. package/lib/Database/Migration/migrations/0000_00_00_00_00_create_migrations_table.js +21 -0
  12. package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +20 -0
  13. package/lib/Database/Schema/Blueprint.js +0 -40
  14. package/lib/Database/Schema/Manager.js +29 -40
  15. package/lib/Database/Seeder/SeederRepository.js +49 -0
  16. package/lib/Database/Seeder/SeederRunner.js +183 -0
  17. package/lib/Faker/Data/Address.js +63 -0
  18. package/lib/Faker/Data/Color.js +72 -0
  19. package/lib/Faker/Data/Company.js +59 -0
  20. package/lib/Faker/Data/Date.js +49 -0
  21. package/lib/Faker/Data/Finance.js +65 -0
  22. package/lib/Faker/Data/Internet.js +73 -0
  23. package/lib/Faker/Data/Lorem.js +45 -0
  24. package/lib/Faker/Data/Person.js +67 -0
  25. package/lib/Faker/Data/Phone.js +26 -0
  26. package/lib/Faker/Faker.d.ts +205 -0
  27. package/lib/Faker/Faker.js +812 -0
  28. package/lib/View/Manager.js +26 -5
  29. package/lib/index.d.ts +407 -0
  30. package/lib/index.js +12 -0
  31. package/package.json +6 -2
  32. package/skeleton/config/app.js +20 -0
  33. package/skeleton/globals.d.ts +68 -1
  34. package/skeleton/tsconfig.json +6 -16
@@ -0,0 +1,92 @@
1
+ import DB from "../DB.js";
2
+
3
+ class MigrationRepository {
4
+
5
+ static table = 'migrations';
6
+
7
+ static async tableExists() {
8
+ try {
9
+ await DB.table(this.table).limit(1).get();
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ static async getExecuted() {
17
+ if (!await this.tableExists()) return [];
18
+ return await DB.table(this.table)
19
+ .orderBy("batch", "asc")
20
+ .orderBy("id", "asc")
21
+ .get();
22
+ }
23
+
24
+ static async getExecutedNames() {
25
+ const migrations = await this.getExecuted();
26
+ return new Set(migrations.map(m => m.name));
27
+ }
28
+
29
+ static async getNextBatchNumber() {
30
+ if (!await this.tableExists()) return 1;
31
+ const result = await DB.table(this.table)
32
+ .select(DB.rawExpr("MAX(batch) as max_batch"))
33
+ .first();
34
+ return (result?.max_batch || 0) + 1;
35
+ }
36
+
37
+ static async getLastBatchNumber() {
38
+ if (!await this.tableExists()) return 0;
39
+ const result = await DB.table(this.table)
40
+ .select(DB.rawExpr("MAX(batch) as max_batch"))
41
+ .first();
42
+ return result?.max_batch || 0;
43
+ }
44
+
45
+ static async getByBatch(batch) {
46
+ return await DB.table(this.table)
47
+ .where("batch", batch)
48
+ .orderBy("id", "desc")
49
+ .get();
50
+ }
51
+
52
+ static async getLastBatches(steps = 1) {
53
+ const lastBatch = await this.getLastBatchNumber();
54
+ if (lastBatch === 0) return [];
55
+
56
+ const minBatch = Math.max(1, lastBatch - steps + 1);
57
+ return await DB.table(this.table)
58
+ .where("batch", ">=", minBatch)
59
+ .orderBy("batch", "desc")
60
+ .orderBy("id", "desc")
61
+ .get();
62
+ }
63
+
64
+ static async log(name, batch, checksum) {
65
+ await DB.table(this.table).insert({
66
+ name,
67
+ batch,
68
+ checksum,
69
+ executed_at: new Date()
70
+ });
71
+ }
72
+
73
+ static async delete(name) {
74
+ await DB.table(this.table).where("name", name).delete();
75
+ }
76
+
77
+ static async find(name) {
78
+ return await DB.table(this.table).where("name", name).first();
79
+ }
80
+
81
+ static async exists(name) {
82
+ return (await this.find(name)) !== null;
83
+ }
84
+
85
+ static async getChecksum(name) {
86
+ const migration = await this.find(name);
87
+ return migration?.checksum || null;
88
+ }
89
+
90
+ }
91
+
92
+ export default MigrationRepository;
@@ -0,0 +1,327 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL, fileURLToPath } from 'url';
4
+ import Checksum from './Checksum.js';
5
+ import MigrationRepository from './MigrationRepository.js';
6
+ import SeederRepository from '../Seeder/SeederRepository.js';
7
+ import Paths from '../../Core/Paths.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const COLORS = {
13
+ reset: '\x1b[0m',
14
+ red: '\x1b[31m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ cyan: '\x1b[36m',
18
+ dim: '\x1b[2m',
19
+ bold: '\x1b[1m'
20
+ };
21
+
22
+ class MigrationRunner {
23
+
24
+ static get frameworkMigrationsDir() {
25
+ return path.join(__dirname, 'migrations');
26
+ }
27
+
28
+ static async runFrameworkMigrations() {
29
+ const frameworkDir = this.frameworkMigrationsDir;
30
+
31
+ if (!fs.existsSync(frameworkDir)) {
32
+ return { success: true, ran: [] };
33
+ }
34
+
35
+ const files = fs.readdirSync(frameworkDir)
36
+ .filter(f => f.endsWith('.js'))
37
+ .sort();
38
+
39
+ const ran = [];
40
+
41
+ for (const file of files) {
42
+ const isMigrationsTable = file.includes('migrations');
43
+ const tableExists = isMigrationsTable
44
+ ? await MigrationRepository.tableExists()
45
+ : await SeederRepository.tableExists();
46
+
47
+ if (tableExists) continue;
48
+
49
+ const filePath = path.join(frameworkDir, file);
50
+ const fileUrl = pathToFileURL(filePath).href;
51
+
52
+ console.log(`${COLORS.dim}Migrating:${COLORS.reset} ${COLORS.cyan}[framework] ${file}${COLORS.reset}`);
53
+
54
+ const { default: migration } = await import(fileUrl);
55
+
56
+ if (typeof migration.up !== 'function') {
57
+ throw new Error(`Framework migration ${file} does not have an up() method`);
58
+ }
59
+
60
+ await migration.up();
61
+ ran.push(file);
62
+
63
+ console.log(`${COLORS.green}✅ Migrated:${COLORS.reset} ${COLORS.cyan}[framework] ${file}${COLORS.reset}\n`);
64
+ }
65
+
66
+ return { success: true, ran };
67
+ }
68
+
69
+ static async run() {
70
+ const frameworkResult = await this.runFrameworkMigrations();
71
+ if (!frameworkResult.success) {
72
+ return frameworkResult;
73
+ }
74
+
75
+ const migrationsDir = Paths.migrations;
76
+
77
+ if (!fs.existsSync(migrationsDir)) {
78
+ console.log(`${COLORS.yellow}⚠️ No migrations directory found${COLORS.reset}`);
79
+ return { success: true, ran: [] };
80
+ }
81
+
82
+ const files = fs.readdirSync(migrationsDir)
83
+ .filter(f => f.endsWith('.js'))
84
+ .sort();
85
+
86
+ if (files.length === 0) {
87
+ console.log(`${COLORS.yellow}⚠️ No migration files found${COLORS.reset}`);
88
+ return { success: true, ran: [] };
89
+ }
90
+
91
+ const executedNames = await MigrationRepository.getExecutedNames();
92
+
93
+ for (const file of files) {
94
+ if (executedNames.has(file)) {
95
+ const filePath = path.join(migrationsDir, file);
96
+ const currentChecksum = Checksum.fromFile(filePath);
97
+ const storedChecksum = await MigrationRepository.getChecksum(file);
98
+
99
+ if (currentChecksum !== storedChecksum) {
100
+ console.error(`${COLORS.red}❌ CHECKSUM MISMATCH: ${file}${COLORS.reset}`);
101
+ console.error(`${COLORS.dim} Stored: ${storedChecksum}${COLORS.reset}`);
102
+ console.error(`${COLORS.dim} Current: ${currentChecksum}${COLORS.reset}`);
103
+ console.error(`${COLORS.red} Migration files must NEVER be modified after execution.${COLORS.reset}`);
104
+ console.error(`${COLORS.red} Create a NEW migration for any schema changes.${COLORS.reset}`);
105
+ return {
106
+ success: false,
107
+ ran: [],
108
+ error: new Error(`Checksum mismatch for migration: ${file}`)
109
+ };
110
+ }
111
+ }
112
+ }
113
+
114
+ const pending = files.filter(f => !executedNames.has(f));
115
+
116
+ if (pending.length === 0) {
117
+ console.log(`${COLORS.green}✅ Nothing to migrate. All migrations are up to date.${COLORS.reset}`);
118
+ return { success: true, ran: [] };
119
+ }
120
+
121
+ const batch = await MigrationRepository.getNextBatchNumber();
122
+ console.log(`${COLORS.cyan}📦 Running migrations (batch ${batch})${COLORS.reset}\n`);
123
+
124
+ const executedInBatch = [];
125
+
126
+ try {
127
+ for (const file of pending) {
128
+ const filePath = path.join(migrationsDir, file);
129
+ const fileUrl = pathToFileURL(filePath).href;
130
+ const checksum = Checksum.fromFile(filePath);
131
+
132
+ console.log(`${COLORS.dim}Migrating:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}`);
133
+
134
+ const { default: migration } = await import(fileUrl);
135
+
136
+ if (typeof migration.up !== 'function') {
137
+ throw new Error(`Migration ${file} does not have an up() method`);
138
+ }
139
+
140
+ await migration.up();
141
+ await MigrationRepository.log(file, batch, checksum);
142
+ executedInBatch.push({ file, migration });
143
+
144
+ console.log(`${COLORS.green}✅ Migrated:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}\n`);
145
+ }
146
+
147
+ console.log(`${COLORS.green}${COLORS.bold}✅ All migrations completed successfully.${COLORS.reset}`);
148
+ return { success: true, ran: executedInBatch.map(e => e.file) };
149
+
150
+ } catch (error) {
151
+ console.error(`\n${COLORS.red}❌ Migration failed: ${error.message}${COLORS.reset}`);
152
+
153
+ if (executedInBatch.length > 0) {
154
+ console.log(`\n${COLORS.yellow}⚠️ Rolling back ${executedInBatch.length} migration(s) from this batch...${COLORS.reset}\n`);
155
+
156
+ for (const { file, migration } of executedInBatch.reverse()) {
157
+ try {
158
+ console.log(`${COLORS.dim}Rolling back:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}`);
159
+
160
+ if (typeof migration.down === 'function') {
161
+ await migration.down();
162
+ }
163
+
164
+ await MigrationRepository.delete(file);
165
+ console.log(`${COLORS.yellow}↩️ Rolled back:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}\n`);
166
+ } catch (rollbackError) {
167
+ console.error(`${COLORS.red}❌ Rollback failed for ${file}: ${rollbackError.message}${COLORS.reset}`);
168
+ console.error(`${COLORS.red} Manual intervention may be required.${COLORS.reset}`);
169
+ }
170
+ }
171
+ }
172
+
173
+ return { success: false, ran: [], error };
174
+ }
175
+ }
176
+
177
+ static async rollback(steps = 1) {
178
+ const tableExists = await MigrationRepository.tableExists();
179
+ if (!tableExists) {
180
+ console.log(`${COLORS.yellow}⚠️ No migrations have been run yet.${COLORS.reset}`);
181
+ return { success: true, rolledBack: [] };
182
+ }
183
+
184
+ const lastBatch = await MigrationRepository.getLastBatchNumber();
185
+ if (lastBatch === 0) {
186
+ console.log(`${COLORS.yellow}⚠️ Nothing to rollback.${COLORS.reset}`);
187
+ return { success: true, rolledBack: [] };
188
+ }
189
+
190
+ const toRollback = await MigrationRepository.getLastBatches(steps);
191
+
192
+ if (toRollback.length === 0) {
193
+ console.log(`${COLORS.yellow}⚠️ Nothing to rollback.${COLORS.reset}`);
194
+ return { success: true, rolledBack: [] };
195
+ }
196
+
197
+ const migrationsDir = Paths.migrations;
198
+ const rolledBack = [];
199
+
200
+ console.log(`${COLORS.yellow}⚠️ Rolling back ${toRollback.length} migration(s)...${COLORS.reset}\n`);
201
+
202
+ try {
203
+ for (const record of toRollback) {
204
+ const filePath = path.join(migrationsDir, record.name);
205
+
206
+ if (!fs.existsSync(filePath)) {
207
+ console.error(`${COLORS.red}❌ Migration file not found: ${record.name}${COLORS.reset}`);
208
+ console.error(`${COLORS.red} Cannot rollback without the migration file.${COLORS.reset}`);
209
+ throw new Error(`Migration file not found: ${record.name}`);
210
+ }
211
+
212
+ const currentChecksum = Checksum.fromFile(filePath);
213
+ if (currentChecksum !== record.checksum) {
214
+ console.error(`${COLORS.red}❌ CHECKSUM MISMATCH: ${record.name}${COLORS.reset}`);
215
+ console.error(`${COLORS.red} Migration file was modified after execution.${COLORS.reset}`);
216
+ console.error(`${COLORS.red} Rollback cannot proceed safely.${COLORS.reset}`);
217
+ throw new Error(`Checksum mismatch for migration: ${record.name}`);
218
+ }
219
+
220
+ const fileUrl = pathToFileURL(filePath).href;
221
+ const { default: migration } = await import(fileUrl);
222
+
223
+ console.log(`${COLORS.dim}Rolling back:${COLORS.reset} ${COLORS.cyan}${record.name}${COLORS.reset} ${COLORS.dim}(batch ${record.batch})${COLORS.reset}`);
224
+
225
+ if (typeof migration.down === 'function') {
226
+ await migration.down();
227
+ } else {
228
+ console.warn(`${COLORS.yellow} ⚠️ No down() method, skipping schema rollback${COLORS.reset}`);
229
+ }
230
+
231
+ await MigrationRepository.delete(record.name);
232
+ rolledBack.push(record.name);
233
+
234
+ console.log(`${COLORS.yellow}↩️ Rolled back:${COLORS.reset} ${COLORS.cyan}${record.name}${COLORS.reset}\n`);
235
+ }
236
+
237
+ console.log(`${COLORS.green}${COLORS.bold}✅ Rollback completed successfully.${COLORS.reset}`);
238
+ return { success: true, rolledBack };
239
+
240
+ } catch (error) {
241
+ console.error(`\n${COLORS.red}❌ Rollback failed: ${error.message}${COLORS.reset}`);
242
+ return { success: false, rolledBack, error };
243
+ }
244
+ }
245
+
246
+ static async reset() {
247
+ const tableExists = await MigrationRepository.tableExists();
248
+ if (!tableExists) {
249
+ console.log(`${COLORS.yellow}⚠️ No migrations have been run yet.${COLORS.reset}`);
250
+ return { success: true, rolledBack: [] };
251
+ }
252
+
253
+ const lastBatch = await MigrationRepository.getLastBatchNumber();
254
+ if (lastBatch === 0) {
255
+ console.log(`${COLORS.yellow}⚠️ Nothing to reset.${COLORS.reset}`);
256
+ return { success: true, rolledBack: [] };
257
+ }
258
+
259
+ console.log(`${COLORS.yellow}⚠️ Resetting all migrations...${COLORS.reset}\n`);
260
+ return await this.rollback(lastBatch);
261
+ }
262
+
263
+ static async status() {
264
+ const migrationsDir = Paths.migrations;
265
+
266
+ if (!fs.existsSync(migrationsDir)) {
267
+ return [];
268
+ }
269
+
270
+ const files = fs.readdirSync(migrationsDir)
271
+ .filter(f => f.endsWith('.js'))
272
+ .sort();
273
+
274
+ const executed = await MigrationRepository.getExecuted();
275
+ const executedMap = new Map(executed.map(m => [m.name, m]));
276
+
277
+ const status = files.map(file => {
278
+ const record = executedMap.get(file);
279
+ if (record) {
280
+ return {
281
+ name: file,
282
+ status: 'Ran',
283
+ batch: record.batch,
284
+ executedAt: record.executed_at
285
+ };
286
+ } else {
287
+ return {
288
+ name: file,
289
+ status: 'Pending',
290
+ batch: null,
291
+ executedAt: null
292
+ };
293
+ }
294
+ });
295
+
296
+ return status;
297
+ }
298
+
299
+ static async printStatus() {
300
+ const status = await this.status();
301
+
302
+ if (status.length === 0) {
303
+ console.log(`${COLORS.yellow}⚠️ No migrations found.${COLORS.reset}`);
304
+ return;
305
+ }
306
+
307
+ console.log(`\n${COLORS.bold}Migration Status${COLORS.reset}\n`);
308
+ console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}`);
309
+
310
+ for (const migration of status) {
311
+ const statusColor = migration.status === 'Ran' ? COLORS.green : COLORS.yellow;
312
+ const statusIcon = migration.status === 'Ran' ? '✅' : '⏳';
313
+ const batchInfo = migration.batch ? ` ${COLORS.dim}(batch ${migration.batch})${COLORS.reset}` : '';
314
+
315
+ console.log(`${statusIcon} ${statusColor}${migration.status.padEnd(7)}${COLORS.reset} ${migration.name}${batchInfo}`);
316
+ }
317
+
318
+ console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}\n`);
319
+
320
+ const ran = status.filter(m => m.status === 'Ran').length;
321
+ const pending = status.filter(m => m.status === 'Pending').length;
322
+ console.log(`${COLORS.dim}Total: ${status.length} | Ran: ${ran} | Pending: ${pending}${COLORS.reset}\n`);
323
+ }
324
+
325
+ }
326
+
327
+ export default MigrationRunner;
@@ -0,0 +1,21 @@
1
+ import Schema from "../../Schema/Manager.js";
2
+
3
+ class CreateMigrationsTable {
4
+
5
+ static async up() {
6
+ await Schema.create("migrations", (table) => {
7
+ table.id();
8
+ table.string("name").unique();
9
+ table.integer("batch");
10
+ table.string("checksum", 64);
11
+ table.timestamp("executed_at");
12
+ });
13
+ }
14
+
15
+ static async down() {
16
+ await Schema.dropIfExists("migrations");
17
+ }
18
+
19
+ }
20
+
21
+ export default CreateMigrationsTable;
@@ -0,0 +1,20 @@
1
+ import Schema from "../../Schema/Manager.js";
2
+
3
+ class CreateSeedersTable {
4
+
5
+ static async up() {
6
+ await Schema.create("seeders", (table) => {
7
+ table.id();
8
+ table.string("name").unique();
9
+ table.string("checksum", 64);
10
+ table.timestamp("executed_at");
11
+ });
12
+ }
13
+
14
+ static async down() {
15
+ await Schema.dropIfExists("seeders");
16
+ }
17
+
18
+ }
19
+
20
+ export default CreateSeedersTable;
@@ -1,7 +1,3 @@
1
- /**
2
- * Blueprint - Table schema builder
3
- * Laravel-style fluent API for defining database tables
4
- */
5
1
  class Blueprint {
6
2
  #tableName;
7
3
  #columns = [];
@@ -10,23 +6,14 @@ class Blueprint {
10
6
  this.#tableName = tableName;
11
7
  }
12
8
 
13
- /**
14
- * Get table name
15
- */
16
9
  getTableName() {
17
10
  return this.#tableName;
18
11
  }
19
12
 
20
- /**
21
- * Get all columns
22
- */
23
13
  getColumns() {
24
14
  return this.#columns;
25
15
  }
26
16
 
27
- /**
28
- * Add a column definition
29
- */
30
17
  #addColumn(type, name, options = {}) {
31
18
  const column = {
32
19
  name,
@@ -41,7 +28,6 @@ class Blueprint {
41
28
 
42
29
  this.#columns.push(column);
43
30
 
44
- // Return modifier object for chaining
45
31
  return {
46
32
  nullable: () => {
47
33
  column.modifiers.nullable = true;
@@ -58,9 +44,6 @@ class Blueprint {
58
44
  };
59
45
  }
60
46
 
61
- /**
62
- * Primary key: BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
63
- */
64
47
  id() {
65
48
  const column = {
66
49
  name: 'id',
@@ -68,56 +51,33 @@ class Blueprint {
68
51
  modifiers: {}
69
52
  };
70
53
  this.#columns.push(column);
71
-
72
- // No chaining for id()
73
54
  return this;
74
55
  }
75
56
 
76
- /**
77
- * VARCHAR column
78
- */
79
57
  string(name, length = 255) {
80
58
  return this.#addColumn('string', name, { length });
81
59
  }
82
60
 
83
- /**
84
- * TEXT column
85
- */
86
61
  text(name) {
87
62
  return this.#addColumn('text', name);
88
63
  }
89
64
 
90
- /**
91
- * INT column
92
- */
93
65
  integer(name) {
94
66
  return this.#addColumn('integer', name);
95
67
  }
96
68
 
97
- /**
98
- * BIGINT column
99
- */
100
69
  bigInteger(name) {
101
70
  return this.#addColumn('bigInteger', name);
102
71
  }
103
72
 
104
- /**
105
- * TINYINT(1) column for boolean values
106
- */
107
73
  boolean(name) {
108
74
  return this.#addColumn('boolean', name);
109
75
  }
110
76
 
111
- /**
112
- * TIMESTAMP column
113
- */
114
77
  timestamp(name) {
115
78
  return this.#addColumn('timestamp', name);
116
79
  }
117
80
 
118
- /**
119
- * JSON column
120
- */
121
81
  json(name) {
122
82
  return this.#addColumn('json', name);
123
83
  }
@@ -1,66 +1,59 @@
1
1
  import DatabaseManager from "../Manager.js";
2
2
  import Config from "../../Core/Config.js";
3
3
 
4
- /**
5
- * Schema Builder
6
- * Laravel-style database schema operations
7
- */
8
4
  export default class Schema {
9
- /**
10
- * Create a new table
11
- */
5
+
12
6
  static async create(tableName, callback) {
13
7
  const blueprint = new (await import('./Blueprint.js')).default(tableName);
14
8
  callback(blueprint);
15
-
16
- const sql = this.#buildCreateTableSQL(blueprint);
17
-
9
+
10
+ const sql = this.#buildCreateTableSQL(blueprint, false);
11
+
12
+ const connection = DatabaseManager.getInstance().connection();
13
+ await connection.raw(sql);
14
+ }
15
+
16
+ static async createIfNotExists(tableName, callback) {
17
+ const blueprint = new (await import('./Blueprint.js')).default(tableName);
18
+ callback(blueprint);
19
+
20
+ const sql = this.#buildCreateTableSQL(blueprint, true);
21
+
18
22
  const connection = DatabaseManager.getInstance().connection();
19
23
  await connection.raw(sql);
20
24
  }
21
-
22
- /**
23
- * Drop table if exists
24
- */
25
+
25
26
  static async dropIfExists(tableName) {
26
27
  const connection = DatabaseManager.getInstance().connection();
27
28
  await connection.raw(`DROP TABLE IF EXISTS \`${tableName}\``);
28
29
  }
29
-
30
- /**
31
- * Build CREATE TABLE SQL
32
- */
33
- static #buildCreateTableSQL(blueprint) {
30
+
31
+ static #buildCreateTableSQL(blueprint, ifNotExists = false) {
34
32
  const columns = blueprint.getColumns();
35
33
  const columnsSql = columns.map(col => this.#buildColumnSQL(col));
36
-
37
- // Get connection config
34
+
38
35
  const manager = DatabaseManager.getInstance();
39
36
  const connection = manager.connection();
40
37
  const connectionName = connection.getName();
41
38
  const databaseConfig = Config.all('database');
42
39
  const dbConfig = databaseConfig.connections[connectionName];
43
-
40
+
44
41
  const charset = dbConfig.charset || 'utf8mb4';
45
42
  const collation = dbConfig.collation || 'utf8mb4_unicode_ci';
46
-
47
- let sql = `CREATE TABLE \`${blueprint.getTableName()}\` (\n`;
43
+
44
+ const ifNotExistsClause = ifNotExists ? 'IF NOT EXISTS ' : '';
45
+ let sql = `CREATE TABLE ${ifNotExistsClause}\`${blueprint.getTableName()}\` (\n`;
48
46
  sql += ' ' + columnsSql.join(',\n ');
49
47
  sql += `\n) ENGINE=InnoDB DEFAULT CHARSET=${charset} COLLATE=${collation}`;
50
-
48
+
51
49
  return sql;
52
50
  }
53
-
54
- /**
55
- * Build SQL for a single column
56
- */
51
+
57
52
  static #buildColumnSQL(column) {
58
53
  let sql = `\`${column.name}\` `;
59
-
60
- // Type
54
+
61
55
  switch (column.type) {
62
56
  case 'id':
63
- // id is special - no modifiers
64
57
  sql += 'BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY';
65
58
  return sql;
66
59
  case 'string':
@@ -87,30 +80,26 @@ export default class Schema {
87
80
  default:
88
81
  throw new Error(`Unknown column type: ${column.type}`);
89
82
  }
90
-
91
- // Modifiers (not for id)
83
+
92
84
  if (column.modifiers) {
93
- // NOT NULL / NULL
94
85
  if (column.modifiers.nullable) {
95
86
  sql += ' NULL';
96
87
  } else {
97
88
  sql += ' NOT NULL';
98
89
  }
99
-
100
- // DEFAULT
90
+
101
91
  if (column.modifiers.default !== null) {
102
92
  const defaultValue = typeof column.modifiers.default === 'string'
103
93
  ? `'${column.modifiers.default}'`
104
94
  : column.modifiers.default;
105
95
  sql += ` DEFAULT ${defaultValue}`;
106
96
  }
107
-
108
- // UNIQUE
97
+
109
98
  if (column.modifiers.unique) {
110
99
  sql += ' UNIQUE';
111
100
  }
112
101
  }
113
-
102
+
114
103
  return sql;
115
104
  }
116
105
  }