@nitronjs/framework 0.2.3 → 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 (68) hide show
  1. package/README.md +3 -1
  2. package/cli/create.js +88 -72
  3. package/cli/njs.js +13 -6
  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 +0 -1
  14. package/lib/Console/Commands/MigrateFreshCommand.js +18 -25
  15. package/lib/Console/Commands/MigrateRollbackCommand.js +6 -3
  16. package/lib/Console/Commands/MigrateStatusCommand.js +6 -3
  17. package/lib/Console/Commands/SeedCommand.js +4 -2
  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 -0
  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 +56 -61
  27. package/lib/Database/Model.js +157 -83
  28. package/lib/Database/QueryBuilder.js +31 -0
  29. package/lib/Database/QueryValidation.js +36 -44
  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 +12 -31
  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 +177 -152
  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 +42 -8
  53. package/lib/index.js +19 -12
  54. package/package.json +1 -1
  55. package/skeleton/app/Controllers/HomeController.js +7 -1
  56. package/skeleton/resources/css/global.css +1 -0
  57. package/skeleton/resources/views/Site/Home.tsx +456 -79
  58. package/skeleton/tsconfig.json +6 -1
  59. package/lib/Auth/Manager.js +0 -111
  60. package/lib/Database/Connection.js +0 -61
  61. package/lib/Database/Manager.js +0 -162
  62. package/lib/Encryption/Manager.js +0 -47
  63. package/lib/Filesystem/Manager.js +0 -74
  64. package/lib/Hashing/Manager.js +0 -25
  65. package/lib/Mail/Manager.js +0 -120
  66. package/lib/Route/Loader.js +0 -80
  67. package/lib/Route/Manager.js +0 -286
  68. package/lib/Translation/Manager.js +0 -49
@@ -1,84 +1,174 @@
1
- import DatabaseManager from "./Manager.js";
1
+ import MySQLDriver from "./Drivers/MySQLDriver.js";
2
2
  import { query as createQueryBuilder, RawExpression } from "./QueryBuilder.js";
3
+ import Config from "../Core/Config.js";
3
4
 
5
+ /**
6
+ * Database manager providing query builder access, raw queries, and transactions.
7
+ * Supports multiple database drivers (currently MySQL).
8
+ *
9
+ * @example
10
+ * const users = await DB.table("users").where("active", 1).get();
11
+ * await DB.transaction(async (conn) => { ... });
12
+ */
4
13
  class DB {
5
- static manager() {
6
- return DatabaseManager.getInstance();
14
+ static #driver = null;
15
+ static #config = null;
16
+ static #credentials = null;
17
+
18
+ /**
19
+ * Initializes database connection with configured driver.
20
+ * @returns {Promise<void>}
21
+ * @throws {Error} If health check fails
22
+ */
23
+ static async setup() {
24
+ this.#config = Config.all("database");
25
+
26
+ const envDriver = (process.env.DATABASE_DRIVER || "mysql").toLowerCase();
27
+
28
+ if (envDriver === "none") {
29
+ return;
30
+ }
31
+
32
+ this.#driver = this.#createDriver(envDriver);
33
+
34
+ const isHealthy = await this.#driver.healthCheck();
35
+
36
+ if (!isHealthy) {
37
+ throw new Error("Database connection health check failed. Check your .env credentials and ensure MySQL is running.");
38
+ }
7
39
  }
8
40
 
9
- static connection(name = null) {
10
- if (!this.#isEnabled()) return null;
11
- return this.manager().connection(name);
41
+ /**
42
+ * Closes database connection and releases resources.
43
+ * @returns {Promise<void>}
44
+ */
45
+ static async close() {
46
+ if (this.#driver) {
47
+ await this.#driver.close();
48
+ this.#driver = null;
49
+ }
12
50
  }
13
51
 
14
- static async query(sql, bindings = []) {
15
- if (!this.#isEnabled()) return null;
16
- return await this.manager().query(sql, bindings);
52
+ /**
53
+ * Creates a query builder for the specified table.
54
+ * @param {string} table - Table name
55
+ * @param {Function|null} modelClass - Optional model class for result mapping
56
+ * @returns {Object} Query builder instance
57
+ * @throws {Error} If database is disabled
58
+ */
59
+ static table(table, modelClass = null) {
60
+ if (!this.#driver) {
61
+ throw new Error("Database is disabled (DATABASE_DRIVER=none)");
62
+ }
63
+
64
+ return createQueryBuilder(table, this.#driver, modelClass);
17
65
  }
18
66
 
67
+ /**
68
+ * Creates a raw SQL expression for use in queries.
69
+ * @param {string} expression - Raw SQL expression
70
+ * @returns {RawExpression} Raw expression wrapper
71
+ */
19
72
  static rawExpr(expression) {
20
73
  return new RawExpression(expression);
21
74
  }
22
75
 
76
+ /**
77
+ * Executes a raw SQL query.
78
+ * @param {string} sql - Raw SQL string
79
+ * @returns {Promise<Array>} Query result
80
+ * @throws {Error} If database is disabled
81
+ */
23
82
  static async rawQuery(sql) {
24
- if (!this.#isEnabled()) return null;
25
- return await this.connection().raw(sql);
26
- }
83
+ if (!this.#driver) {
84
+ throw new Error("Database is disabled (DATABASE_DRIVER=none)");
85
+ }
27
86
 
28
- static async transaction(callback) {
29
- if (!this.#isEnabled()) return null;
30
- return await this.connection().withTransaction(callback);
87
+ return await this.#driver.raw(sql);
31
88
  }
32
89
 
33
90
  /**
34
- * Create a query builder for a table.
35
- * Uses lazy connection loading - connection is fetched only when query is executed.
36
- * @param {string} table - The table name
37
- * @param {string|null} connectionName - Optional connection name
38
- * @param {Function|null} modelClass - Optional model class for hydration
39
- * @returns {QueryBuilder} A query builder instance
91
+ * Executes a callback within a database transaction.
92
+ * @param {Function} callback - Async function receiving connection
93
+ * @param {Object} options - Transaction options
94
+ * @param {number} options.timeout - Timeout in milliseconds (default: 30000)
95
+ * @returns {Promise<*>} Callback result
96
+ * @throws {Error} If transaction times out or fails
40
97
  */
41
- static table(table, connectionName = null, modelClass = null) {
42
- // Pass connection directly now (synchronous)
43
- const connection = this.connection(connectionName);
44
- return createQueryBuilder(table, connection, modelClass);
45
- }
46
-
47
- static async setup() {
48
- if (!this.#isEnabled()) return;
98
+ static async transaction(callback, options = {}) {
99
+ if (!this.#driver) {
100
+ throw new Error("Database is disabled (DATABASE_DRIVER=none)");
101
+ }
49
102
 
50
- const manager = this.manager();
51
- const defaultConnection = manager.getDefaultConnection();
103
+ const timeout = options.timeout || 30000;
104
+ const controller = new AbortController();
105
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
52
106
 
53
107
  try {
54
- const connection = manager.connection(defaultConnection);
55
- const isHealthy = await connection.healthCheck();
108
+ const result = await this.#driver.withTransaction(callback, { signal: controller.signal });
109
+ clearTimeout(timeoutId);
56
110
 
57
- if (!isHealthy) {
58
- throw new Error(`Database connection '${defaultConnection}' health check failed`);
59
- }
111
+ return result;
60
112
  }
61
- catch (error) {
62
- console.error('[Database] Setup failed:', error.message);
63
- console.error('[Database] Check your .env credentials and ensure MySQL is running');
64
- throw error;
113
+ catch (err) {
114
+ clearTimeout(timeoutId);
115
+
116
+ if (controller.signal.aborted) {
117
+ throw new Error(
118
+ `Transaction timeout after ${timeout}ms. ` +
119
+ "Consider: 1) Adding await to all async operations, 2) Increasing timeout, 3) Optimizing queries."
120
+ );
121
+ }
122
+
123
+ throw err;
65
124
  }
66
125
  }
67
126
 
68
- static async close() {
69
- if (!this.#isEnabled()) return;
127
+ /**
128
+ * Reconfigures database with new credentials.
129
+ * Used by installer to test and save database settings.
130
+ * @param {Object} credentials - Database credentials
131
+ * @param {string} credentials.host - Database host
132
+ * @param {number} credentials.port - Database port
133
+ * @param {string} credentials.database - Database name
134
+ * @param {string} credentials.username - Database username
135
+ * @param {string} credentials.password - Database password
136
+ * @returns {Promise<void>}
137
+ */
138
+ static async reconfigure(credentials) {
139
+ await this.close();
70
140
 
71
- try {
72
- await this.manager().closeAll();
73
- }
74
- catch (error) {
75
- console.error('[Database] Error during shutdown:', error.message);
76
- }
141
+ this.#credentials = {
142
+ host: credentials.host,
143
+ port: String(credentials.port),
144
+ database: credentials.database,
145
+ username: credentials.username,
146
+ password: credentials.password || ""
147
+ };
148
+
149
+ this.#driver = this.#createDriver("mysql");
77
150
  }
78
151
 
79
- static #isEnabled() {
80
- return !!DatabaseManager.getInstance().getDefaultConnection();
152
+ /** @private */
153
+ static #createDriver(driverName) {
154
+ const connections = this.#config?.connections || {};
155
+ const config = connections[driverName];
156
+
157
+ if (!config) {
158
+ throw new Error(`Database connection '${driverName}' is not configured`);
159
+ }
160
+
161
+ switch (config.driver) {
162
+ case "mysql":
163
+ return new MySQLDriver(config, this.#credentials);
164
+ case "postgres":
165
+ throw new Error("Postgres driver not yet implemented");
166
+ case "mongodb":
167
+ throw new Error("MongoDB driver not yet implemented");
168
+ default:
169
+ throw new Error(`Unknown database driver: ${config.driver}`);
170
+ }
81
171
  }
82
172
  }
83
173
 
84
- export default DB;
174
+ export default DB;
@@ -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;