@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
@@ -1,33 +1,37 @@
1
- import mysql from 'mysql2/promise';
2
- import Environment from '../../Core/Environment.js';
1
+ import mysql from "mysql2/promise";
2
+ import Environment from "../../Core/Environment.js";
3
3
 
4
+ /**
5
+ * MySQL database driver using connection pooling.
6
+ * Handles query execution, transactions, and error management.
7
+ */
4
8
  class MySQLDriver {
5
9
  #pool = null;
6
10
  #config = null;
7
11
  #credentials = null;
8
12
 
13
+ /**
14
+ * Creates a new MySQL driver instance with connection pool.
15
+ * @param {Object} config - Database configuration from config/database.js
16
+ * @param {Object|null} credentials - Optional credential override for reconfiguration
17
+ */
9
18
  constructor(config, credentials = null) {
10
19
  this.#config = config;
11
20
  this.#credentials = credentials;
12
- this.#createPool();
13
- }
14
21
 
15
- #createPool() {
16
22
  const creds = this.#credentials;
17
23
 
18
24
  this.#pool = mysql.createPool({
19
- host: creds?.host ?? process.env.DATABASE_HOST ?? '127.0.0.1',
20
- port: parseInt(creds?.port ?? process.env.DATABASE_PORT ?? '3306'),
21
- user: creds?.username ?? process.env.DATABASE_USERNAME ?? 'root',
22
- password: creds?.password ?? process.env.DATABASE_PASSWORD ?? '',
23
- database: creds?.database ?? process.env.DATABASE_NAME ?? 'test',
24
- charset: this.#config.charset || 'utf8mb4',
25
-
25
+ host: creds?.host ?? process.env.DATABASE_HOST ?? "127.0.0.1",
26
+ port: parseInt(creds?.port ?? process.env.DATABASE_PORT ?? "3306"),
27
+ user: creds?.username ?? process.env.DATABASE_USERNAME ?? "root",
28
+ password: creds?.password ?? process.env.DATABASE_PASSWORD ?? "",
29
+ database: creds?.database ?? process.env.DATABASE_NAME ?? "test",
30
+ charset: this.#config.charset || "utf8mb4",
26
31
  waitForConnections: true,
27
32
  connectionLimit: this.#config.pool?.max ?? 10,
28
33
  queueLimit: this.#config.pool?.queueLimit ?? 100,
29
34
  connectTimeout: 10000,
30
-
31
35
  enableKeepAlive: true,
32
36
  keepAliveInitialDelay: 10000,
33
37
  namedPlaceholders: true,
@@ -35,201 +39,142 @@ class MySQLDriver {
35
39
  });
36
40
  }
37
41
 
42
+ /**
43
+ * Executes a prepared SQL query with bindings.
44
+ * @param {string} sql - SQL query string
45
+ * @param {Array} bindings - Parameter bindings
46
+ * @returns {Promise<Array>} Query result [rows, fields]
47
+ * @throws {Error} On query failure
48
+ */
38
49
  async query(sql, bindings = []) {
39
- const isProduction = Environment.isProd;
50
+ const connection = await this.#pool.getConnection();
40
51
 
41
52
  try {
42
- const connection = await this.#pool.getConnection();
43
-
44
- try {
45
- await connection.query('SET SESSION MAX_EXECUTION_TIME = 60000');
46
- const [rows, fields] = await connection.execute(sql, bindings);
53
+ await connection.query("SET SESSION MAX_EXECUTION_TIME = 60000");
47
54
 
48
- return [rows, fields];
49
- }
50
- finally {
51
- connection.release();
52
- }
55
+ return await connection.execute(sql, bindings);
53
56
  }
54
-
55
57
  catch (error) {
56
- if (isProduction) {
57
- console.error('[MySQLDriver] Query failed', {
58
- code: error.code,
59
- errno: error.errno,
60
- sqlState: error.sqlState
61
- });
62
-
63
- const sanitized = new Error('Database query failed');
64
- sanitized.code = error.code;
65
- sanitized.driver = 'mysql';
66
- throw sanitized;
67
- }
68
-
69
- error.sql = sql;
70
- error.bindings = this.#maskSensitiveData(bindings);
71
- error.driver = 'mysql';
72
- throw error;
58
+ throw this.#handleError(error, sql, bindings);
59
+ }
60
+ finally {
61
+ connection.release();
73
62
  }
74
63
  }
75
64
 
65
+ /**
66
+ * Executes a raw SQL query without parameter binding.
67
+ * @param {string} sql - Raw SQL string
68
+ * @returns {Promise<Array>} Query result
69
+ */
76
70
  async raw(sql) {
77
- const isProduction = Environment.isProd;
78
-
79
71
  try {
80
- const [rows, fields] = await this.#pool.query(sql);
81
- return [rows, fields];
72
+ return await this.#pool.query(sql);
82
73
  }
83
-
84
74
  catch (error) {
85
- if (isProduction) {
86
- console.error('[MySQLDriver] Raw query failed', {
87
- code: error.code,
88
- errno: error.errno,
89
- sqlState: error.sqlState
90
- });
91
-
92
- const sanitized = new Error('Database query failed');
93
- sanitized.code = error.code;
94
- sanitized.driver = 'mysql';
95
- throw sanitized;
96
- }
97
-
98
- error.sql = sql;
99
- error.driver = 'mysql';
100
- throw error;
75
+ throw this.#handleError(error, sql);
101
76
  }
102
77
  }
103
78
 
104
- #maskSensitiveData(bindings) {
105
- if (Environment.isDev) {
106
- return bindings; // Show all in development
107
- }
108
-
109
- return bindings.map((value, index) => {
110
- if (typeof value !== 'string') {
111
- return value;
112
- }
113
-
114
- const sensitivePatterns = /password|token|secret|key|auth|bearer|jwt|otp|pin|credential/i;
115
- const valuePreview = value.substring(0, 30).toLowerCase();
116
- const looksLikeBase64 = /^[A-Za-z0-9+/]+=*$/.test(value) && value.length > 16;
117
-
118
- if (sensitivePatterns.test(valuePreview) || value.length > 20 || looksLikeBase64) {
119
- return '***MASKED***';
120
- }
121
-
122
- return value;
123
- });
124
- }
125
-
126
- async beginTransaction() {
79
+ /**
80
+ * Executes callback within a database transaction.
81
+ * Automatically commits on success or rolls back on failure.
82
+ * @param {Function} callback - Async function receiving connection
83
+ * @param {Object} options - Transaction options
84
+ * @param {AbortSignal} options.signal - Abort signal for timeout
85
+ * @returns {Promise<*>} Callback result
86
+ */
87
+ async withTransaction(callback, options = {}) {
127
88
  const connection = await this.#pool.getConnection();
128
- await connection.query('SET SESSION MAX_EXECUTION_TIME = 60000');
129
89
 
90
+ await connection.query("SET SESSION MAX_EXECUTION_TIME = 60000");
130
91
  await connection.beginTransaction();
131
92
 
132
- return connection;
133
- }
134
-
135
- async commit(connection) {
136
93
  try {
137
- await connection.commit();
138
- }
94
+ if (options.signal?.aborted) {
95
+ throw new Error("Transaction aborted");
96
+ }
139
97
 
140
- finally {
141
- connection.release();
142
- }
143
- }
98
+ const result = await callback(connection);
144
99
 
145
- async rollback(connection) {
146
- try {
147
- await connection.rollback();
148
- }
100
+ if (options.signal?.aborted) {
101
+ await connection.rollback();
102
+ connection.release();
149
103
 
150
- catch (error) {
151
- }
104
+ throw new Error("Transaction aborted");
105
+ }
152
106
 
153
- finally {
107
+ await connection.commit();
154
108
  connection.release();
155
- }
156
- }
157
109
 
158
- async transactionQuery(connection, sql, bindings = []) {
159
- try {
160
- const [rows, fields] = await connection.execute(sql, bindings);
161
- return [rows, fields];
110
+ return result;
162
111
  }
163
-
164
112
  catch (error) {
165
- const isProduction = Environment.isProd;
166
-
167
- if (isProduction) {
168
- console.error('[MySQLDriver] Transaction query failed', {
169
- code: error.code,
170
- errno: error.errno,
171
- sqlState: error.sqlState,
172
- inTransaction: true
173
- });
174
-
175
- const sanitized = new Error('Database query failed');
176
- sanitized.code = error.code;
177
- sanitized.driver = 'mysql';
178
- sanitized.inTransaction = true;
179
- throw sanitized;
113
+ try {
114
+ await connection.rollback();
180
115
  }
181
-
182
- error.sql = sql;
183
- error.bindings = this.#maskSensitiveData(bindings);
184
- error.driver = 'mysql';
185
- error.inTransaction = true;
186
- throw error;
187
- }
188
- }
189
-
190
- async withTransaction(callback, options = {}) {
191
- const signal = options?.signal;
192
- const connection = await this.beginTransaction();
193
-
194
- try {
195
- if (signal?.aborted) {
196
- throw new Error('Transaction aborted');
116
+ catch {
117
+ // Ignore rollback errors
197
118
  }
198
-
199
- const result = await callback(connection);
200
- if (signal?.aborted) {
201
- await this.rollback(connection);
202
- throw new Error('Transaction aborted');
119
+ finally {
120
+ connection.release();
203
121
  }
204
122
 
205
- await this.commit(connection);
206
-
207
- return result;
208
- }
209
-
210
- catch (error) {
211
- await this.rollback(connection);
212
123
  throw error;
213
124
  }
214
125
  }
215
126
 
127
+ /**
128
+ * Checks if database connection is healthy.
129
+ * @returns {Promise<boolean>} True if connection is alive
130
+ */
216
131
  async healthCheck() {
217
132
  try {
218
- await this.#pool.query('SELECT 1');
133
+ await this.#pool.query("SELECT 1");
134
+
219
135
  return true;
220
136
  }
221
-
222
- catch (error) {
137
+ catch {
223
138
  return false;
224
139
  }
225
140
  }
226
141
 
142
+ /**
143
+ * Closes connection pool and releases resources.
144
+ * @returns {Promise<void>}
145
+ */
227
146
  async close() {
228
147
  if (this.#pool) {
229
148
  await this.#pool.end();
230
149
  this.#pool = null;
231
150
  }
232
151
  }
152
+
153
+ /** @private */
154
+ #handleError(error, sql, bindings = null) {
155
+ if (Environment.isProd) {
156
+ console.error("[MySQLDriver] Query failed", {
157
+ code: error.code,
158
+ errno: error.errno,
159
+ sqlState: error.sqlState
160
+ });
161
+
162
+ const sanitized = new Error("Database query failed");
163
+ sanitized.code = error.code;
164
+ sanitized.driver = "mysql";
165
+
166
+ return sanitized;
167
+ }
168
+
169
+ error.sql = sql;
170
+ error.driver = "mysql";
171
+
172
+ if (bindings) {
173
+ error.bindings = bindings;
174
+ }
175
+
176
+ return error;
177
+ }
233
178
  }
234
179
 
235
180
  export default MySQLDriver;
@@ -2,22 +2,17 @@ import { createHash } from 'crypto';
2
2
  import fs from 'fs';
3
3
 
4
4
  class Checksum {
5
-
6
5
  static fromFile(filePath) {
7
- const content = fs.readFileSync(filePath, 'utf8');
8
- return this.fromContent(content);
6
+ return this.fromContent(fs.readFileSync(filePath, 'utf8'));
9
7
  }
10
8
 
11
9
  static fromContent(content) {
12
- const normalized = content.replace(/\r\n/g, '\n').trim();
13
- return createHash('sha256').update(normalized, 'utf8').digest('hex');
10
+ return createHash('sha256').update(content.replace(/\r\n/g, '\n').trim(), 'utf8').digest('hex');
14
11
  }
15
12
 
16
13
  static verify(filePath, expectedChecksum) {
17
- const actualChecksum = this.fromFile(filePath);
18
- return actualChecksum === expectedChecksum;
14
+ return this.fromFile(filePath) === expectedChecksum;
19
15
  }
20
-
21
16
  }
22
17
 
23
18
  export default Checksum;
@@ -1,9 +1,12 @@
1
1
  import DB from "../DB.js";
2
2
 
3
3
  class MigrationRepository {
4
-
5
4
  static table = 'migrations';
6
5
 
6
+ // ─────────────────────────────────────────────────────────────────────────
7
+ // Public Methods
8
+ // ─────────────────────────────────────────────────────────────────────────
9
+
7
10
  static async tableExists() {
8
11
  const [rows] = await DB.rawQuery(`SHOW TABLES LIKE '${this.table}'`);
9
12
  return rows.length > 0;
@@ -11,67 +14,46 @@ class MigrationRepository {
11
14
 
12
15
  static async getExecuted() {
13
16
  if (!await this.tableExists()) return [];
14
- return await DB.table(this.table)
15
- .orderBy("batch", "asc")
16
- .orderBy("id", "asc")
17
- .get();
17
+ return await DB.table(this.table).orderBy('batch', 'asc').orderBy('id', 'asc').get();
18
18
  }
19
19
 
20
20
  static async getExecutedNames() {
21
- const migrations = await this.getExecuted();
22
- return new Set(migrations.map(m => m.name));
21
+ return new Set((await this.getExecuted()).map(m => m.name));
23
22
  }
24
23
 
25
24
  static async getNextBatchNumber() {
26
- if (!await this.tableExists()) return 1;
27
- const result = await DB.table(this.table)
28
- .select(DB.rawExpr("MAX(batch) as max_batch"))
29
- .first();
30
- return (result?.max_batch || 0) + 1;
25
+ return (await this.#getMaxBatch()) + 1;
31
26
  }
32
27
 
33
28
  static async getLastBatchNumber() {
34
- if (!await this.tableExists()) return 0;
35
- const result = await DB.table(this.table)
36
- .select(DB.rawExpr("MAX(batch) as max_batch"))
37
- .first();
38
- return result?.max_batch || 0;
29
+ return await this.#getMaxBatch();
39
30
  }
40
31
 
41
32
  static async getByBatch(batch) {
42
- return await DB.table(this.table)
43
- .where("batch", batch)
44
- .orderBy("id", "desc")
45
- .get();
33
+ return await DB.table(this.table).where('batch', batch).orderBy('id', 'desc').get();
46
34
  }
47
35
 
48
36
  static async getLastBatches(steps = 1) {
49
37
  const lastBatch = await this.getLastBatchNumber();
50
38
  if (lastBatch === 0) return [];
51
39
 
52
- const minBatch = Math.max(1, lastBatch - steps + 1);
53
40
  return await DB.table(this.table)
54
- .where("batch", ">=", minBatch)
55
- .orderBy("batch", "desc")
56
- .orderBy("id", "desc")
41
+ .where('batch', '>=', Math.max(1, lastBatch - steps + 1))
42
+ .orderBy('batch', 'desc')
43
+ .orderBy('id', 'desc')
57
44
  .get();
58
45
  }
59
46
 
60
47
  static async log(name, batch, checksum) {
61
- await DB.table(this.table).insert({
62
- name,
63
- batch,
64
- checksum,
65
- executed_at: new Date()
66
- });
48
+ await DB.table(this.table).insert({ name, batch, checksum, executed_at: new Date() });
67
49
  }
68
50
 
69
51
  static async delete(name) {
70
- await DB.table(this.table).where("name", name).delete();
52
+ await DB.table(this.table).where('name', name).delete();
71
53
  }
72
54
 
73
55
  static async find(name) {
74
- return await DB.table(this.table).where("name", name).first();
56
+ return await DB.table(this.table).where('name', name).first();
75
57
  }
76
58
 
77
59
  static async exists(name) {
@@ -79,10 +61,18 @@ class MigrationRepository {
79
61
  }
80
62
 
81
63
  static async getChecksum(name) {
82
- const migration = await this.find(name);
83
- return migration?.checksum || null;
64
+ return (await this.find(name))?.checksum || null;
84
65
  }
85
66
 
67
+ // ─────────────────────────────────────────────────────────────────────────
68
+ // Private Methods
69
+ // ─────────────────────────────────────────────────────────────────────────
70
+
71
+ static async #getMaxBatch() {
72
+ if (!await this.tableExists()) return 0;
73
+ const result = await DB.table(this.table).select(DB.rawExpr('MAX(batch) as max_batch')).first();
74
+ return result?.max_batch || 0;
75
+ }
86
76
  }
87
77
 
88
78
  export default MigrationRepository;