@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.
Files changed (71) hide show
  1. package/README.md +3 -1
  2. package/cli/create.js +88 -72
  3. package/cli/njs.js +17 -19
  4. package/lib/Auth/Auth.js +167 -0
  5. package/lib/Build/CssBuilder.js +9 -0
  6. package/lib/Build/FileAnalyzer.js +16 -0
  7. package/lib/Build/HydrationBuilder.js +17 -0
  8. package/lib/Build/Manager.js +15 -0
  9. package/lib/Build/colors.js +4 -0
  10. package/lib/Build/plugins.js +84 -20
  11. package/lib/Console/Commands/DevCommand.js +13 -9
  12. package/lib/Console/Commands/MakeCommand.js +24 -10
  13. package/lib/Console/Commands/MigrateCommand.js +4 -3
  14. package/lib/Console/Commands/MigrateFreshCommand.js +22 -27
  15. package/lib/Console/Commands/MigrateRollbackCommand.js +8 -4
  16. package/lib/Console/Commands/MigrateStatusCommand.js +8 -4
  17. package/lib/Console/Commands/SeedCommand.js +8 -28
  18. package/lib/Console/Commands/StorageLinkCommand.js +20 -5
  19. package/lib/Console/Output.js +143 -0
  20. package/lib/Core/Config.js +2 -1
  21. package/lib/Core/Paths.js +8 -8
  22. package/lib/Database/DB.js +141 -51
  23. package/lib/Database/Drivers/MySQLDriver.js +102 -157
  24. package/lib/Database/Migration/Checksum.js +3 -8
  25. package/lib/Database/Migration/MigrationRepository.js +25 -35
  26. package/lib/Database/Migration/MigrationRunner.js +59 -67
  27. package/lib/Database/Model.js +165 -75
  28. package/lib/Database/QueryBuilder.js +43 -0
  29. package/lib/Database/QueryValidation.js +51 -30
  30. package/lib/Database/Schema/Blueprint.js +25 -36
  31. package/lib/Database/Schema/Manager.js +31 -68
  32. package/lib/Database/Seeder/SeederRunner.js +24 -145
  33. package/lib/Date/DateTime.js +9 -0
  34. package/lib/Encryption/Encryption.js +52 -0
  35. package/lib/Faker/Faker.js +11 -0
  36. package/lib/Filesystem/Storage.js +120 -0
  37. package/lib/HMR/Server.js +79 -9
  38. package/lib/Hashing/Hash.js +41 -0
  39. package/lib/Http/Server.js +179 -151
  40. package/lib/Logging/{Manager.js → Log.js} +68 -80
  41. package/lib/Mail/Mail.js +187 -0
  42. package/lib/Route/Router.js +416 -0
  43. package/lib/Session/File.js +135 -233
  44. package/lib/Session/Manager.js +117 -171
  45. package/lib/Session/Memory.js +28 -38
  46. package/lib/Session/Session.js +71 -107
  47. package/lib/Support/Str.js +103 -0
  48. package/lib/Translation/Lang.js +54 -0
  49. package/lib/View/Client/hmr-client.js +87 -51
  50. package/lib/View/Client/nitronjs-icon.png +0 -0
  51. package/lib/View/{Manager.js → View.js} +44 -29
  52. package/lib/index.d.ts +49 -27
  53. package/lib/index.js +19 -13
  54. package/package.json +1 -1
  55. package/skeleton/app/Controllers/HomeController.js +7 -1
  56. package/skeleton/package.json +2 -0
  57. package/skeleton/resources/css/global.css +1 -0
  58. package/skeleton/resources/views/Site/Home.tsx +456 -79
  59. package/skeleton/tsconfig.json +6 -1
  60. package/lib/Auth/Manager.js +0 -111
  61. package/lib/Database/Connection.js +0 -61
  62. package/lib/Database/Manager.js +0 -162
  63. package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +0 -20
  64. package/lib/Database/Seeder/SeederRepository.js +0 -45
  65. package/lib/Encryption/Manager.js +0 -47
  66. package/lib/Filesystem/Manager.js +0 -74
  67. package/lib/Hashing/Manager.js +0 -25
  68. package/lib/Mail/Manager.js +0 -120
  69. package/lib/Route/Loader.js +0 -80
  70. package/lib/Route/Manager.js +0 -286
  71. 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 isMigrationsTable = file.includes('migrations');
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
- console.log(`${COLORS.dim}Migrating:${COLORS.reset} ${COLORS.cyan}[framework] ${file}${COLORS.reset}`);
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
- console.log(`${COLORS.green}✅ Migrated:${COLORS.reset} ${COLORS.cyan}[framework] ${file}${COLORS.reset}\n`);
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
- console.log(`${COLORS.yellow}⚠️ No migrations directory found${COLORS.reset}`);
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
- console.log(`${COLORS.yellow}⚠️ No migration files found${COLORS.reset}`);
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
- 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}`);
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(`${COLORS.green}Nothing to migrate. All migrations are up to date.${COLORS.reset}`);
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
- console.log(`${COLORS.cyan}📦 Running migrations (batch ${batch})${COLORS.reset}\n`);
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
- console.log(`${COLORS.dim}Migrating:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}`);
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
- console.log(`${COLORS.green}✅ Migrated:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}\n`);
132
+ Output.done("Migrated", file);
145
133
  }
146
134
 
147
- console.log(`${COLORS.green}${COLORS.bold}✅ All migrations completed successfully.${COLORS.reset}`);
135
+ Output.migrationSuccess();
148
136
  return { success: true, ran: executedInBatch.map(e => e.file) };
149
137
 
150
- } catch (error) {
151
- console.error(`\n${COLORS.red}❌ Migration failed: ${error.message}${COLORS.reset}`);
138
+ }
139
+ catch (error) {
140
+ Output.newline();
141
+ Output.error(`Migration failed: ${error.message}`);
152
142
 
153
143
  if (executedInBatch.length > 0) {
154
- console.log(`\n${COLORS.yellow}⚠️ Rolling back ${executedInBatch.length} migration(s) from this batch...${COLORS.reset}\n`);
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
- console.log(`${COLORS.dim}Rolling back:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}`);
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
- 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}`);
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
- console.log(`${COLORS.yellow}⚠️ No migrations have been run yet.${COLORS.reset}`);
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
- console.log(`${COLORS.yellow}⚠️ Nothing to rollback.${COLORS.reset}`);
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
- console.log(`${COLORS.yellow}⚠️ Nothing to rollback.${COLORS.reset}`);
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
- console.log(`${COLORS.yellow}⚠️ Rolling back ${toRollback.length} migration(s)...${COLORS.reset}\n`);
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
- 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}`);
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
- 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}`);
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
- console.log(`${COLORS.dim}Rolling back:${COLORS.reset} ${COLORS.cyan}${record.name}${COLORS.reset} ${COLORS.dim}(batch ${record.batch})${COLORS.reset}`);
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
- console.warn(`${COLORS.yellow} ⚠️ No down() method, skipping schema rollback${COLORS.reset}`);
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
- console.log(`${COLORS.yellow}↩️ Rolled back:${COLORS.reset} ${COLORS.cyan}${record.name}${COLORS.reset}\n`);
228
+ Output.rollbackDone(record.name, record.batch);
235
229
  }
236
230
 
237
- console.log(`${COLORS.green}${COLORS.bold}✅ Rollback completed successfully.${COLORS.reset}`);
231
+ Output.rollbackSuccess();
238
232
  return { success: true, rolledBack };
239
233
 
240
- } catch (error) {
241
- console.error(`\n${COLORS.red}❌ Rollback failed: ${error.message}${COLORS.reset}`);
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
- console.log(`${COLORS.yellow}⚠️ No migrations have been run yet.${COLORS.reset}`);
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
- console.log(`${COLORS.yellow}⚠️ Nothing to reset.${COLORS.reset}`);
251
+ Output.warn("Nothing to reset.");
256
252
  return { success: true, rolledBack: [] };
257
253
  }
258
254
 
259
- console.log(`${COLORS.yellow}⚠️ Resetting all migrations...${COLORS.reset}\n`);
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
- } else {
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(`${COLORS.yellow}⚠️ No migrations found.${COLORS.reset}`);
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
- console.log(`\n${COLORS.bold}Migration Status${COLORS.reset}\n`);
308
- console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}`);
306
+ Output.statusHeader();
309
307
 
310
308
  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}`);
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
- console.log(`${COLORS.dim}Total: ${status.length} | Ran: ${ran} | Pending: ${pending}${COLORS.reset}\n`);
314
+ Output.statusFooter(status.length, ran, pending);
323
315
  }
324
316
 
325
317
  }
@@ -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 (prop.startsWith('_') || typeof target[prop] === 'function' || prop === 'constructor') {
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
- if (prop in target) return false;
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
- if (!this.table) {
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
- if (!this.table) {
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("id", id).first();
90
+ const row = await DB.table(this.table).where('id', id).first();
66
91
 
67
- if (!row) return null;
92
+ return row ? hydrate(this, row) : null;
93
+ }
68
94
 
69
- const instance = new this();
70
-
71
- for (const [key, value] of Object.entries(row)) {
72
- if (value instanceof Date) {
73
- instance._attributes[key] = value.toISOString().slice(0, 19).replace('T', ' ');
74
- } else {
75
- instance._attributes[key] = value;
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
- if (!this.table) {
87
- throw new Error(`Model ${this.name} must define a static 'table' property`);
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
- if (!this.table) {
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
- static async first() {
102
- if (!this.table) {
103
- throw new Error(`Model ${this.name} must define a static 'table' property`);
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 await DB.table(this.table, null, this).limit(1).first();
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 constructor = this.constructor;
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 !== "id" || !this._exists)) {
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
- const primaryKeyValue = this._attributes["id"];
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(constructor.table).insert(data);
130
- this._attributes["id"] = id;
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;