@nitronjs/framework 0.1.22 → 0.1.24
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/cli/njs.js +33 -5
- package/lib/Build/Manager.js +7 -0
- package/lib/Console/Commands/MigrateCommand.js +27 -70
- package/lib/Console/Commands/MigrateFreshCommand.js +49 -66
- package/lib/Console/Commands/MigrateRollbackCommand.js +52 -0
- package/lib/Console/Commands/MigrateStatusCommand.js +38 -0
- package/lib/Console/Commands/SeedCommand.js +36 -65
- package/lib/Console/Stubs/page-hydration.tsx +9 -9
- package/lib/Core/Paths.js +8 -0
- package/lib/Database/Migration/Checksum.js +23 -0
- package/lib/Database/Migration/MigrationRepository.js +92 -0
- package/lib/Database/Migration/MigrationRunner.js +327 -0
- package/lib/Database/Migration/migrations/0000_00_00_00_00_create_migrations_table.js +21 -0
- package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +20 -0
- package/lib/Database/Schema/Blueprint.js +0 -40
- package/lib/Database/Schema/Manager.js +29 -40
- package/lib/Database/Seeder/SeederRepository.js +49 -0
- package/lib/Database/Seeder/SeederRunner.js +183 -0
- package/lib/Faker/Data/Address.js +63 -0
- package/lib/Faker/Data/Color.js +72 -0
- package/lib/Faker/Data/Company.js +59 -0
- package/lib/Faker/Data/Date.js +49 -0
- package/lib/Faker/Data/Finance.js +65 -0
- package/lib/Faker/Data/Internet.js +73 -0
- package/lib/Faker/Data/Lorem.js +45 -0
- package/lib/Faker/Data/Person.js +67 -0
- package/lib/Faker/Data/Phone.js +26 -0
- package/lib/Faker/Faker.d.ts +205 -0
- package/lib/Faker/Faker.js +812 -0
- package/lib/View/Manager.js +26 -5
- package/lib/index.d.ts +407 -0
- package/lib/index.js +12 -0
- package/package.json +6 -2
- package/skeleton/config/app.js +20 -0
- package/skeleton/globals.d.ts +68 -1
- 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
|
-
|
|
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
|
}
|