@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,61 +0,0 @@
1
- class Connection {
2
- #driver = null;
3
- #name = null;
4
-
5
- constructor(name, driver) {
6
- this.#name = name;
7
- this.#driver = driver;
8
- }
9
-
10
- async query(sql, bindings = []) {
11
- return await this.#driver.query(sql, bindings);
12
- }
13
-
14
- async raw(sql) {
15
- return await this.#driver.raw(sql);
16
- }
17
-
18
- async withTransaction(callback, options = {}) {
19
- const timeout = options.timeout || 30000; // 30 seconds default
20
- const controller = new AbortController();
21
- const timeoutId = setTimeout(() => {
22
- controller.abort();
23
- }, timeout);
24
-
25
- try {
26
- const result = await this.#driver.withTransaction(callback, {
27
- signal: controller.signal
28
- });
29
-
30
- clearTimeout(timeoutId);
31
- return result;
32
- } catch (err) {
33
- clearTimeout(timeoutId);
34
- if (controller.signal.aborted) {
35
- throw new Error(
36
- `Transaction timeout after ${timeout}ms. ` +
37
- 'This may indicate a forgotten await in the callback or a slow query. ' +
38
- 'Consider: 1) Adding await to all async operations, 2) Increasing timeout, 3) Optimizing queries.'
39
- );
40
- }
41
-
42
- throw err;
43
- }
44
- }
45
-
46
- async healthCheck() {
47
- return await this.#driver.healthCheck();
48
- }
49
-
50
- getName() {
51
- return this.#name;
52
- }
53
-
54
- async close() {
55
- if (this.#driver && typeof this.#driver.close === 'function') {
56
- await this.#driver.close();
57
- }
58
- }
59
- }
60
-
61
- export default Connection;
@@ -1,162 +0,0 @@
1
- import Connection from './Connection.js';
2
- import MySQLDriver from './Drivers/MySQLDriver.js';
3
- import Config from '../Core/Config.js';
4
-
5
- class DatabaseManager {
6
-
7
- static instance = null;
8
- static #isInternal = false;
9
-
10
- #connections = new Map();
11
- #defaultConnection = null;
12
- #config = null;
13
- #credentials = null;
14
-
15
- constructor() {
16
- if (!DatabaseManager.#isInternal) {
17
- throw new Error('DatabaseManager must be initialized with await DatabaseManager.getInstance()');
18
- }
19
- }
20
-
21
- static createInstance() {
22
- DatabaseManager.#isInternal = true;
23
- const instance = new DatabaseManager();
24
- DatabaseManager.#isInternal = false;
25
-
26
- instance.#config = Config.all('database');
27
- instance.#initializeDefaultConnection();
28
- return instance;
29
- }
30
-
31
- #initializeDefaultConnection() {
32
- const envDriver = (process.env.DATABASE_DRIVER || 'mysql').toLowerCase();
33
- const normalized = envDriver === 'postgresql' ? 'postgres' : envDriver;
34
-
35
- this.#defaultConnection = normalized === 'none' ? null : normalized;
36
- }
37
-
38
- static getInstance() {
39
- if (!this.instance) {
40
- this.instance = DatabaseManager.createInstance();
41
- }
42
-
43
- return this.instance;
44
- }
45
-
46
- // ========================================
47
- // Connection Management
48
- // ========================================
49
-
50
- connection(name = null) {
51
- const connectionName = name || this.#defaultConnection;
52
-
53
- if (!connectionName) {
54
- throw new Error('Database is disabled (DATABASE_DRIVER=none)');
55
- }
56
-
57
- if (this.#connections.has(connectionName)) {
58
- return this.#connections.get(connectionName);
59
- }
60
-
61
- const config = this.#config.connections[connectionName];
62
-
63
- if (!config) {
64
- throw new Error(`Database connection '${connectionName}' is not configured`);
65
- }
66
-
67
- return this.#addConnection(connectionName, config);
68
- }
69
-
70
- #addConnection(name, config) {
71
- const driver = this.#createDriver(config);
72
- const connection = new Connection(name, driver);
73
-
74
- this.#connections.set(name, connection);
75
-
76
- return connection;
77
- }
78
-
79
- #createDriver(config) {
80
- switch (config.driver) {
81
- case 'mysql':
82
- return new MySQLDriver(config, this.#credentials);
83
-
84
- case 'postgres':
85
- throw new Error('Postgres driver not yet implemented');
86
-
87
- case 'mongodb':
88
- throw new Error('MongoDB driver not yet implemented');
89
-
90
- default:
91
- throw new Error(`Unknown database driver: ${config.driver}`);
92
- }
93
- }
94
-
95
- // ========================================
96
- // Reconfiguration (for Installer)
97
- // ========================================
98
-
99
- /**
100
- * Reconfigure database with new credentials
101
- * Closes existing connections and creates new pool with provided credentials
102
- */
103
- async reconfigure(credentials) {
104
- await this.closeAll();
105
-
106
- this.#credentials = {
107
- host: credentials.host,
108
- port: String(credentials.port),
109
- database: credentials.database,
110
- username: credentials.username,
111
- password: credentials.password || ""
112
- };
113
-
114
- this.#defaultConnection = "mysql";
115
- }
116
-
117
- // ========================================
118
- // Query & Health
119
- // ========================================
120
-
121
- async query(sql, bindings = []) {
122
- return await this.connection().query(sql, bindings);
123
- }
124
-
125
- async healthCheck() {
126
- const results = {};
127
-
128
- for (const [name, connection] of this.#connections.entries()) {
129
- try {
130
- results[name] = await connection.healthCheck();
131
- }
132
- catch (error) {
133
- results[name] = false;
134
- }
135
- }
136
-
137
- return results;
138
- }
139
-
140
- // ========================================
141
- // Lifecycle
142
- // ========================================
143
-
144
- async closeAll() {
145
- for (const [name, connection] of this.#connections.entries()) {
146
- try {
147
- await connection.close();
148
- }
149
- catch (error) {
150
- // Ignore close errors
151
- }
152
- }
153
-
154
- this.#connections.clear();
155
- }
156
-
157
- getDefaultConnection() {
158
- return this.#defaultConnection;
159
- }
160
- }
161
-
162
- export default DatabaseManager;
@@ -1,20 +0,0 @@
1
- import Schema from "../../Schema/Manager.js";
2
-
3
- class CreateSeedersTable {
4
-
5
- static async up() {
6
- await Schema.create("seeders", (table) => {
7
- table.id();
8
- table.string("name").unique();
9
- table.string("checksum", 64);
10
- table.timestamp("executed_at");
11
- });
12
- }
13
-
14
- static async down() {
15
- await Schema.dropIfExists("seeders");
16
- }
17
-
18
- }
19
-
20
- export default CreateSeedersTable;
@@ -1,45 +0,0 @@
1
- import DB from "../DB.js";
2
-
3
- class SeederRepository {
4
-
5
- static table = 'seeders';
6
-
7
- static async tableExists() {
8
- const [rows] = await DB.rawQuery(`SHOW TABLES LIKE '${this.table}'`);
9
- return rows.length > 0;
10
- }
11
-
12
- static async getExecuted() {
13
- if (!await this.tableExists()) return [];
14
- return await DB.table(this.table).orderBy("id", "asc").get();
15
- }
16
-
17
- static async getExecutedNames() {
18
- const seeders = await this.getExecuted();
19
- return new Set(seeders.map(s => s.name));
20
- }
21
-
22
- static async log(name, checksum) {
23
- await DB.table(this.table).insert({
24
- name,
25
- checksum,
26
- executed_at: new Date()
27
- });
28
- }
29
-
30
- static async find(name) {
31
- return await DB.table(this.table).where("name", name).first();
32
- }
33
-
34
- static async exists(name) {
35
- return (await this.find(name)) !== null;
36
- }
37
-
38
- static async getChecksum(name) {
39
- const seeder = await this.find(name);
40
- return seeder?.checksum || null;
41
- }
42
-
43
- }
44
-
45
- export default SeederRepository;
@@ -1,47 +0,0 @@
1
- import crypto from "crypto";
2
-
3
- class EncryptionManager {
4
- static encrypt(value) {
5
- if (typeof value === "object") {
6
- value = JSON.stringify(value);
7
- }
8
-
9
- const secretKey = this.#createSecretKey();
10
- const iv = this.#createIV();
11
-
12
- const cipher = crypto.createCipheriv("aes-256-cbc", secretKey, iv);
13
- let encrypted = cipher.update(value, "utf8", "hex");
14
- encrypted += cipher.final("hex");
15
- encrypted = iv.toString("hex") + ":" + encrypted;
16
-
17
- return encrypted;
18
- }
19
-
20
- static decrypt(encryptedValue) {
21
- try {
22
- const parts = encryptedValue.split(":");
23
- const iv = Buffer.from(parts.shift(), "hex");
24
- const encryptedText = Buffer.from(parts.join(":"), "hex");
25
-
26
- const secretKey = this.#createSecretKey();
27
-
28
- const decipher = crypto.createDecipheriv("aes-256-cbc", secretKey, iv);
29
- let decrypted = decipher.update(encryptedText, "hex", "utf8");
30
- decrypted += decipher.final("utf8");
31
-
32
- return decrypted;
33
- } catch (error) {
34
- return false;
35
- }
36
- }
37
-
38
- static #createSecretKey() {
39
- return crypto.createHash("sha256").update(process.env.APP_KEY).digest();
40
- }
41
-
42
- static #createIV() {
43
- return crypto.randomBytes(16);
44
- }
45
- }
46
-
47
- export default EncryptionManager;
@@ -1,74 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import Paths from "../Core/Paths.js";
4
-
5
- class FilesystemManager {
6
- static #publicRoot = Paths.storagePublic;
7
- static #privateRoot = Paths.storagePrivate;
8
-
9
- /**
10
- * Validate path to prevent directory traversal attacks
11
- * @throws Error if path escapes base directory
12
- */
13
- static #validatePath(base, filePath) {
14
- const normalizedBase = path.normalize(base) + path.sep;
15
- const fullPath = path.normalize(path.join(base, filePath));
16
-
17
- if (!fullPath.startsWith(normalizedBase)) {
18
- throw new Error("Invalid file path: directory traversal detected");
19
- }
20
-
21
- return fullPath;
22
- }
23
-
24
- static async get(filePath, isPrivate = false) {
25
- const base = isPrivate ? this.#privateRoot : this.#publicRoot;
26
-
27
- try {
28
- const fullPath = this.#validatePath(base, filePath);
29
- return await fs.promises.readFile(fullPath);
30
- } catch (err) {
31
- if (err.message.includes("directory traversal")) throw err;
32
- return null;
33
- }
34
- }
35
-
36
- static async put(file, dir, fileName, isPrivate = false) {
37
- const base = isPrivate ? this.#privateRoot : this.#publicRoot;
38
-
39
- // Validate both dir and fileName
40
- const folderPath = this.#validatePath(base, dir);
41
- const fullPath = this.#validatePath(base, path.join(dir, fileName));
42
-
43
- await fs.promises.mkdir(folderPath, { recursive: true });
44
- await fs.promises.writeFile(fullPath, file._buf);
45
-
46
- return true;
47
- }
48
-
49
- static async delete(filePath, isPrivate = false) {
50
- const base = isPrivate ? this.#privateRoot : this.#publicRoot;
51
- const fullPath = this.#validatePath(base, filePath);
52
- await fs.promises.unlink(fullPath);
53
- }
54
-
55
- static exists(filePath, isPrivate = false) {
56
- const base = isPrivate ? this.#privateRoot : this.#publicRoot;
57
-
58
- try {
59
- const fullPath = this.#validatePath(base, filePath);
60
- return fs.existsSync(fullPath);
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- static url(filePath) {
67
- if (filePath.startsWith("/")) {
68
- filePath = filePath.substring(1);
69
- }
70
- return `/storage/${filePath}`;
71
- }
72
- }
73
-
74
- export default FilesystemManager;
@@ -1,25 +0,0 @@
1
- import bcrypt from "bcrypt";
2
- import Config from "../Core/Config.js";
3
-
4
- class HashManager {
5
- static async make(textField) {
6
- if (!process.env.APP_KEY) {
7
- throw new Error("APP_KEY is required for hashing");
8
- }
9
-
10
- const saltRounds = Config.get("hash.salt_rounds", 10);
11
- const salt = await bcrypt.genSalt(saltRounds);
12
-
13
- return await bcrypt.hash(textField + process.env.APP_KEY, salt);
14
- }
15
-
16
- static async check(textField, hashedText) {
17
- if (!process.env.APP_KEY) {
18
- throw new Error("APP_KEY is required for hashing");
19
- }
20
-
21
- return await bcrypt.compare(textField + process.env.APP_KEY, hashedText);
22
- }
23
- }
24
-
25
- export default HashManager;
@@ -1,120 +0,0 @@
1
- import nodemailer from "nodemailer";
2
- import ViewManager from "../View/Manager.js";
3
-
4
- class MailManager {
5
- static from(email) {
6
- return MailManager.#fromService(new MailManager(), email);
7
- }
8
-
9
- from(email) {
10
- return MailManager.#fromService(this, email);
11
- }
12
-
13
- static to(email) {
14
- return MailManager.#toService(new MailManager(), email);
15
- }
16
-
17
- to(email) {
18
- return MailManager.#toService(this, email);
19
- }
20
-
21
- subject(subject) {
22
- this.subject = subject;
23
- return this;
24
- }
25
-
26
- attachment(attachment) {
27
- if (!this.attachments) {
28
- this.attachments = [];
29
- }
30
- this.attachments.push(attachment);
31
- return this;
32
- }
33
-
34
- calendar(icsContent) {
35
- this.calendarContent = icsContent;
36
- return this;
37
- }
38
-
39
- text(message) {
40
- this.text = message;
41
- return this;
42
- }
43
-
44
- view(res, view, data = null) {
45
- this.view = ViewManager.renderFile(view, data);
46
- return this;
47
- }
48
-
49
- async send(transportCallback = null) {
50
- let transporter;
51
-
52
- if (typeof transportCallback === "function") {
53
- const transportData = {
54
- MAIL_HOST: null,
55
- MAIL_PORT: null,
56
- MAIL_USERNAME: null,
57
- MAIL_PASSWORD: null,
58
- MAIL_SECURE: null
59
- };
60
- transportCallback(transportData);
61
- transporter = MailManager.#createTransport(transportData);
62
- } else {
63
- transporter = MailManager.#createTransport(process.env);
64
- }
65
-
66
- const response = await MailManager.#sendMail(this, transporter);
67
- return response;
68
- }
69
-
70
- static #fromService(object, email) {
71
- object.fromAddress = email;
72
- return object;
73
- }
74
-
75
- static #toService(object, email) {
76
- object.toAddress = email;
77
- return object;
78
- }
79
-
80
- static #createTransport(transportData) {
81
- return nodemailer.createTransport({
82
- host: transportData.MAIL_HOST,
83
- port: transportData.MAIL_PORT,
84
- secure: transportData.MAIL_SECURE || false,
85
- auth: {
86
- user: transportData.MAIL_USERNAME,
87
- pass: transportData.MAIL_PASSWORD
88
- }
89
- });
90
- }
91
-
92
- static async #sendMail(object, transporter) {
93
- const mailOptions = {
94
- from: object.fromAddress || null,
95
- to: object.toAddress || null,
96
- subject: object.subject || null,
97
- text: object.text || null,
98
- html: object.view || null,
99
- attachments: object.attachments || undefined,
100
- alternatives: object.calendarContent
101
- ? [
102
- {
103
- contentType: 'text/calendar; method=REQUEST; charset="UTF-8"',
104
- content: object.calendarContent,
105
- component: "VEVENT",
106
- headers: {
107
- "Content-Class": "urn:content-classes:calendarmessage"
108
- }
109
- }
110
- ]
111
- : undefined
112
- };
113
-
114
- const response = await transporter.sendMail(mailOptions);
115
- transporter.close();
116
- return response;
117
- }
118
- }
119
-
120
- export default MailManager;
@@ -1,80 +0,0 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import Paths from "../Core/Paths.js";
4
- import Environment from "../Core/Environment.js";
5
-
6
- const DIRECTORIES = [
7
- Paths.controllers,
8
- Paths.middlewares
9
- ];
10
-
11
- class Loader {
12
- #registry = new Map();
13
- #initialized = false;
14
-
15
- async initialize() {
16
- if (this.#initialized || !Environment.isDev) return;
17
-
18
- for (const dir of DIRECTORIES) {
19
- await this.#loadDirectory(dir);
20
- }
21
-
22
- this.#initialized = true;
23
- }
24
-
25
- wrapHandler(handler) {
26
- if (!Environment.isDev) return handler;
27
-
28
- const info = this.#registry.get(handler);
29
- if (!info) return handler;
30
-
31
- const { filePath, methodName } = info;
32
-
33
- return async (request, response, param) => {
34
- const module = await import(`file://${filePath}?t=${Date.now()}`);
35
- return module.default[methodName](request, response, param);
36
- };
37
- }
38
-
39
- async #loadDirectory(dir) {
40
- if (!fs.existsSync(dir)) return;
41
-
42
- for (const filePath of this.#getJsFiles(dir)) {
43
- try {
44
- const module = await import(`file://${filePath}`);
45
- const Class = module.default;
46
-
47
- if (!Class || typeof Class !== "function") continue;
48
-
49
- for (const name of Object.getOwnPropertyNames(Class)) {
50
- if (typeof Class[name] === "function" && !["length", "name", "prototype"].includes(name)) {
51
- this.#registry.set(Class[name], { filePath, methodName: name });
52
- }
53
- }
54
- }
55
- catch {
56
- //
57
- }
58
- }
59
- }
60
-
61
- #getJsFiles(dir) {
62
- const files = [];
63
- const entries = fs.readdirSync(dir, { withFileTypes: true });
64
-
65
- for (const entry of entries) {
66
- const fullPath = path.join(dir, entry.name);
67
-
68
- if (entry.isDirectory()) {
69
- files.push(...this.#getJsFiles(fullPath));
70
- }
71
- else if (entry.name.endsWith(".js")) {
72
- files.push(fullPath);
73
- }
74
- }
75
-
76
- return files;
77
- }
78
- }
79
-
80
- export default new Loader();