@nitronjs/framework 0.2.2 → 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 +17 -19
- 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 +4 -3
- package/lib/Console/Commands/MigrateFreshCommand.js +22 -27
- package/lib/Console/Commands/MigrateRollbackCommand.js +8 -4
- package/lib/Console/Commands/MigrateStatusCommand.js +8 -4
- package/lib/Console/Commands/SeedCommand.js +8 -28
- 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 -8
- 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 +59 -67
- package/lib/Database/Model.js +165 -75
- package/lib/Database/QueryBuilder.js +43 -0
- package/lib/Database/QueryValidation.js +51 -30
- package/lib/Database/Schema/Blueprint.js +25 -36
- package/lib/Database/Schema/Manager.js +31 -68
- package/lib/Database/Seeder/SeederRunner.js +24 -145
- 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 +179 -151
- 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 +49 -27
- package/lib/index.js +19 -13
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +7 -1
- package/skeleton/package.json +2 -0
- 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/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +0 -20
- package/lib/Database/Seeder/SeederRepository.js +0 -45
- 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
|
@@ -3,22 +3,12 @@ import path from 'path';
|
|
|
3
3
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
4
4
|
import Checksum from './Checksum.js';
|
|
5
5
|
import MigrationRepository from './MigrationRepository.js';
|
|
6
|
-
import SeederRepository from '../Seeder/SeederRepository.js';
|
|
7
6
|
import Paths from '../../Core/Paths.js';
|
|
7
|
+
import Output from '../../Console/Output.js';
|
|
8
8
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
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
12
|
class MigrationRunner {
|
|
23
13
|
|
|
24
14
|
static get frameworkMigrationsDir() {
|
|
@@ -39,17 +29,14 @@ class MigrationRunner {
|
|
|
39
29
|
const ran = [];
|
|
40
30
|
|
|
41
31
|
for (const file of files) {
|
|
42
|
-
const
|
|
43
|
-
const tableExists = isMigrationsTable
|
|
44
|
-
? await MigrationRepository.tableExists()
|
|
45
|
-
: await SeederRepository.tableExists();
|
|
32
|
+
const tableExists = await MigrationRepository.tableExists();
|
|
46
33
|
|
|
47
34
|
if (tableExists) continue;
|
|
48
35
|
|
|
49
36
|
const filePath = path.join(frameworkDir, file);
|
|
50
37
|
const fileUrl = pathToFileURL(filePath).href;
|
|
51
38
|
|
|
52
|
-
|
|
39
|
+
Output.frameworkMigration('pending', file);
|
|
53
40
|
|
|
54
41
|
const { default: migration } = await import(fileUrl);
|
|
55
42
|
|
|
@@ -60,7 +47,7 @@ class MigrationRunner {
|
|
|
60
47
|
await migration.up();
|
|
61
48
|
ran.push(file);
|
|
62
49
|
|
|
63
|
-
|
|
50
|
+
Output.frameworkMigration('done', file);
|
|
64
51
|
}
|
|
65
52
|
|
|
66
53
|
return { success: true, ran };
|
|
@@ -75,7 +62,7 @@ class MigrationRunner {
|
|
|
75
62
|
const migrationsDir = Paths.migrations;
|
|
76
63
|
|
|
77
64
|
if (!fs.existsSync(migrationsDir)) {
|
|
78
|
-
|
|
65
|
+
Output.warn("No migrations directory found");
|
|
79
66
|
return { success: true, ran: [] };
|
|
80
67
|
}
|
|
81
68
|
|
|
@@ -84,7 +71,7 @@ class MigrationRunner {
|
|
|
84
71
|
.sort();
|
|
85
72
|
|
|
86
73
|
if (files.length === 0) {
|
|
87
|
-
|
|
74
|
+
Output.warn("No migration files found");
|
|
88
75
|
return { success: true, ran: [] };
|
|
89
76
|
}
|
|
90
77
|
|
|
@@ -97,11 +84,11 @@ class MigrationRunner {
|
|
|
97
84
|
const storedChecksum = await MigrationRepository.getChecksum(file);
|
|
98
85
|
|
|
99
86
|
if (currentChecksum !== storedChecksum) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
Output.error(`Checksum mismatch: ${file}`);
|
|
88
|
+
Output.errorDetail(`Stored: ${storedChecksum}`);
|
|
89
|
+
Output.errorDetail(`Current: ${currentChecksum}`);
|
|
90
|
+
Output.error("Migration files must NEVER be modified after execution.");
|
|
91
|
+
Output.error("Create a NEW migration for any schema changes.");
|
|
105
92
|
return {
|
|
106
93
|
success: false,
|
|
107
94
|
ran: [],
|
|
@@ -114,12 +101,13 @@ class MigrationRunner {
|
|
|
114
101
|
const pending = files.filter(f => !executedNames.has(f));
|
|
115
102
|
|
|
116
103
|
if (pending.length === 0) {
|
|
117
|
-
console.log(
|
|
104
|
+
console.log(` ${Output.COLORS.green}${Output.ICONS.success}${Output.COLORS.reset} ${Output.COLORS.dim}Nothing to migrate. All migrations are up to date.${Output.COLORS.reset}`);
|
|
105
|
+
console.log();
|
|
118
106
|
return { success: true, ran: [] };
|
|
119
107
|
}
|
|
120
108
|
|
|
121
109
|
const batch = await MigrationRepository.getNextBatchNumber();
|
|
122
|
-
|
|
110
|
+
Output.migrationHeader(batch);
|
|
123
111
|
|
|
124
112
|
const executedInBatch = [];
|
|
125
113
|
|
|
@@ -129,7 +117,7 @@ class MigrationRunner {
|
|
|
129
117
|
const fileUrl = pathToFileURL(filePath).href;
|
|
130
118
|
const checksum = Checksum.fromFile(filePath);
|
|
131
119
|
|
|
132
|
-
|
|
120
|
+
Output.pending("Migrating", file);
|
|
133
121
|
|
|
134
122
|
const { default: migration } = await import(fileUrl);
|
|
135
123
|
|
|
@@ -141,31 +129,37 @@ class MigrationRunner {
|
|
|
141
129
|
await MigrationRepository.log(file, batch, checksum);
|
|
142
130
|
executedInBatch.push({ file, migration });
|
|
143
131
|
|
|
144
|
-
|
|
132
|
+
Output.done("Migrated", file);
|
|
145
133
|
}
|
|
146
134
|
|
|
147
|
-
|
|
135
|
+
Output.migrationSuccess();
|
|
148
136
|
return { success: true, ran: executedInBatch.map(e => e.file) };
|
|
149
137
|
|
|
150
|
-
}
|
|
151
|
-
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
Output.newline();
|
|
141
|
+
Output.error(`Migration failed: ${error.message}`);
|
|
152
142
|
|
|
153
143
|
if (executedInBatch.length > 0) {
|
|
154
|
-
|
|
144
|
+
Output.newline();
|
|
145
|
+
Output.warn(`Rolling back ${executedInBatch.length} migration(s) from this batch...`);
|
|
146
|
+
Output.newline();
|
|
155
147
|
|
|
156
148
|
for (const { file, migration } of executedInBatch.reverse()) {
|
|
157
149
|
try {
|
|
158
|
-
|
|
150
|
+
Output.pending("Rolling back", file);
|
|
159
151
|
|
|
160
152
|
if (typeof migration.down === 'function') {
|
|
161
153
|
await migration.down();
|
|
162
154
|
}
|
|
163
155
|
|
|
164
156
|
await MigrationRepository.delete(file);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
Output.rollback("Rolled back", file);
|
|
158
|
+
Output.newline();
|
|
159
|
+
}
|
|
160
|
+
catch (rollbackError) {
|
|
161
|
+
Output.error(`Rollback failed for ${file}: ${rollbackError.message}`);
|
|
162
|
+
Output.error("Manual intervention may be required.");
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
}
|
|
@@ -177,68 +171,70 @@ class MigrationRunner {
|
|
|
177
171
|
static async rollback(steps = 1) {
|
|
178
172
|
const tableExists = await MigrationRepository.tableExists();
|
|
179
173
|
if (!tableExists) {
|
|
180
|
-
|
|
174
|
+
Output.warn("No migrations have been run yet.");
|
|
181
175
|
return { success: true, rolledBack: [] };
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
const lastBatch = await MigrationRepository.getLastBatchNumber();
|
|
185
179
|
if (lastBatch === 0) {
|
|
186
|
-
|
|
180
|
+
Output.warn("Nothing to rollback.");
|
|
187
181
|
return { success: true, rolledBack: [] };
|
|
188
182
|
}
|
|
189
183
|
|
|
190
184
|
const toRollback = await MigrationRepository.getLastBatches(steps);
|
|
191
185
|
|
|
192
186
|
if (toRollback.length === 0) {
|
|
193
|
-
|
|
187
|
+
Output.warn("Nothing to rollback.");
|
|
194
188
|
return { success: true, rolledBack: [] };
|
|
195
189
|
}
|
|
196
190
|
|
|
197
191
|
const migrationsDir = Paths.migrations;
|
|
198
192
|
const rolledBack = [];
|
|
199
193
|
|
|
200
|
-
|
|
194
|
+
Output.rollbackHeader(toRollback.length);
|
|
201
195
|
|
|
202
196
|
try {
|
|
203
197
|
for (const record of toRollback) {
|
|
204
198
|
const filePath = path.join(migrationsDir, record.name);
|
|
205
199
|
|
|
206
200
|
if (!fs.existsSync(filePath)) {
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
Output.error(`Migration file not found: ${record.name}`);
|
|
202
|
+
Output.errorDetail("Cannot rollback without the migration file.");
|
|
209
203
|
throw new Error(`Migration file not found: ${record.name}`);
|
|
210
204
|
}
|
|
211
205
|
|
|
212
206
|
const currentChecksum = Checksum.fromFile(filePath);
|
|
213
207
|
if (currentChecksum !== record.checksum) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
208
|
+
Output.error(`Checksum mismatch: ${record.name}`);
|
|
209
|
+
Output.errorDetail("Migration file was modified after execution.");
|
|
210
|
+
Output.errorDetail("Rollback cannot proceed safely.");
|
|
217
211
|
throw new Error(`Checksum mismatch for migration: ${record.name}`);
|
|
218
212
|
}
|
|
219
213
|
|
|
220
214
|
const fileUrl = pathToFileURL(filePath).href;
|
|
221
215
|
const { default: migration } = await import(fileUrl);
|
|
222
216
|
|
|
223
|
-
|
|
217
|
+
Output.pending("Rolling back", `${record.name} (batch ${record.batch})`);
|
|
224
218
|
|
|
225
219
|
if (typeof migration.down === 'function') {
|
|
226
220
|
await migration.down();
|
|
227
221
|
} else {
|
|
228
|
-
|
|
222
|
+
Output.warn("No down() method, skipping schema rollback");
|
|
229
223
|
}
|
|
230
224
|
|
|
231
225
|
await MigrationRepository.delete(record.name);
|
|
232
226
|
rolledBack.push(record.name);
|
|
233
227
|
|
|
234
|
-
|
|
228
|
+
Output.rollbackDone(record.name, record.batch);
|
|
235
229
|
}
|
|
236
230
|
|
|
237
|
-
|
|
231
|
+
Output.rollbackSuccess();
|
|
238
232
|
return { success: true, rolledBack };
|
|
239
233
|
|
|
240
|
-
}
|
|
241
|
-
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
Output.newline();
|
|
237
|
+
Output.error(`Rollback failed: ${error.message}`);
|
|
242
238
|
return { success: false, rolledBack, error };
|
|
243
239
|
}
|
|
244
240
|
}
|
|
@@ -246,17 +242,18 @@ class MigrationRunner {
|
|
|
246
242
|
static async reset() {
|
|
247
243
|
const tableExists = await MigrationRepository.tableExists();
|
|
248
244
|
if (!tableExists) {
|
|
249
|
-
|
|
245
|
+
Output.warn("No migrations have been run yet.");
|
|
250
246
|
return { success: true, rolledBack: [] };
|
|
251
247
|
}
|
|
252
248
|
|
|
253
249
|
const lastBatch = await MigrationRepository.getLastBatchNumber();
|
|
254
250
|
if (lastBatch === 0) {
|
|
255
|
-
|
|
251
|
+
Output.warn("Nothing to reset.");
|
|
256
252
|
return { success: true, rolledBack: [] };
|
|
257
253
|
}
|
|
258
254
|
|
|
259
|
-
|
|
255
|
+
Output.warn("Resetting all migrations...");
|
|
256
|
+
Output.newline();
|
|
260
257
|
return await this.rollback(lastBatch);
|
|
261
258
|
}
|
|
262
259
|
|
|
@@ -283,7 +280,8 @@ class MigrationRunner {
|
|
|
283
280
|
batch: record.batch,
|
|
284
281
|
executedAt: record.executed_at
|
|
285
282
|
};
|
|
286
|
-
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
287
285
|
return {
|
|
288
286
|
name: file,
|
|
289
287
|
status: 'Pending',
|
|
@@ -300,26 +298,20 @@ class MigrationRunner {
|
|
|
300
298
|
const status = await this.status();
|
|
301
299
|
|
|
302
300
|
if (status.length === 0) {
|
|
303
|
-
console.log(
|
|
301
|
+
console.log(` ${Output.COLORS.green}${Output.ICONS.success}${Output.COLORS.reset} ${Output.COLORS.dim}No migrations found${Output.COLORS.reset}`);
|
|
302
|
+
Output.newline();
|
|
304
303
|
return;
|
|
305
304
|
}
|
|
306
305
|
|
|
307
|
-
|
|
308
|
-
console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}`);
|
|
306
|
+
Output.statusHeader();
|
|
309
307
|
|
|
310
308
|
for (const migration of status) {
|
|
311
|
-
|
|
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}`);
|
|
309
|
+
Output.statusRow(migration.status, migration.name, migration.batch);
|
|
316
310
|
}
|
|
317
311
|
|
|
318
|
-
console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}\n`);
|
|
319
|
-
|
|
320
312
|
const ran = status.filter(m => m.status === 'Ran').length;
|
|
321
313
|
const pending = status.filter(m => m.status === 'Pending').length;
|
|
322
|
-
|
|
314
|
+
Output.statusFooter(status.length, ran, pending);
|
|
323
315
|
}
|
|
324
316
|
|
|
325
317
|
}
|
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,129 +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
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
*/
|
|
141
|
+
static orderBy(column, direction = 'ASC') {
|
|
142
|
+
ensureTable(this);
|
|
105
143
|
|
|
106
|
-
return
|
|
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
|
+
*/
|
|
152
|
+
static limit(value) {
|
|
153
|
+
ensureTable(this);
|
|
154
|
+
|
|
155
|
+
return DB.table(this.table, null, this).limit(value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// Instance Methods
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Save the model (insert or update).
|
|
164
|
+
* @returns {Promise<Model>}
|
|
165
|
+
*/
|
|
109
166
|
async save() {
|
|
110
|
-
const
|
|
167
|
+
const table = this.constructor.table;
|
|
111
168
|
const data = {};
|
|
112
169
|
|
|
113
170
|
for (const [key, value] of Object.entries(this._attributes)) {
|
|
114
|
-
if (value !== undefined && (key !==
|
|
171
|
+
if (value !== undefined && (key !== 'id' || !this._exists)) {
|
|
115
172
|
data[key] = value;
|
|
116
173
|
}
|
|
117
174
|
}
|
|
118
175
|
|
|
119
176
|
if (this._exists) {
|
|
120
|
-
|
|
121
|
-
await DB.table(constructor.table)
|
|
122
|
-
.where("id", primaryKeyValue)
|
|
123
|
-
.update(data);
|
|
124
|
-
|
|
125
|
-
Object.assign(this._attributes, data);
|
|
126
|
-
this._original = { ...this._attributes };
|
|
177
|
+
await DB.table(table).where('id', this._attributes.id).update(data);
|
|
127
178
|
}
|
|
128
179
|
else {
|
|
129
|
-
const id = await DB.table(
|
|
130
|
-
this._attributes
|
|
131
|
-
|
|
132
|
-
this._original = { ...this._attributes };
|
|
180
|
+
const id = await DB.table(table).insert(data);
|
|
181
|
+
this._attributes.id = id;
|
|
133
182
|
this._exists = true;
|
|
134
183
|
}
|
|
135
184
|
|
|
185
|
+
this._original = { ...this._attributes };
|
|
186
|
+
|
|
136
187
|
return this;
|
|
137
188
|
}
|
|
138
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Delete the model from database.
|
|
192
|
+
* @returns {Promise<boolean>}
|
|
193
|
+
* @throws {Error} If model doesn't exist in database
|
|
194
|
+
*/
|
|
139
195
|
async delete() {
|
|
140
|
-
const constructor = this.constructor;
|
|
141
|
-
const primaryKeyValue = this._attributes["id"];
|
|
142
|
-
|
|
143
196
|
if (!this._exists) {
|
|
144
197
|
throw new Error('Cannot delete a model that does not exist');
|
|
145
198
|
}
|
|
146
199
|
|
|
147
|
-
await DB.table(constructor.table)
|
|
148
|
-
.where("id", primaryKeyValue)
|
|
149
|
-
.delete();
|
|
150
|
-
|
|
200
|
+
await DB.table(this.constructor.table).where('id', this._attributes.id).delete();
|
|
151
201
|
this._exists = false;
|
|
202
|
+
|
|
152
203
|
return true;
|
|
153
204
|
}
|
|
154
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Convert model attributes to plain object.
|
|
208
|
+
* @returns {Object}
|
|
209
|
+
*/
|
|
155
210
|
toObject() {
|
|
156
211
|
return { ...this._attributes };
|
|
157
212
|
}
|
|
158
213
|
}
|
|
159
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
|
+
|
|
160
250
|
export default Model;
|