millas 0.1.0

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/bin/millas.js +6 -0
  4. package/package.json +56 -0
  5. package/src/admin/Admin.js +617 -0
  6. package/src/admin/index.js +13 -0
  7. package/src/admin/resources/AdminResource.js +317 -0
  8. package/src/auth/Auth.js +254 -0
  9. package/src/auth/AuthController.js +188 -0
  10. package/src/auth/AuthMiddleware.js +67 -0
  11. package/src/auth/Hasher.js +51 -0
  12. package/src/auth/JwtDriver.js +74 -0
  13. package/src/auth/RoleMiddleware.js +44 -0
  14. package/src/cache/Cache.js +231 -0
  15. package/src/cache/drivers/FileDriver.js +152 -0
  16. package/src/cache/drivers/MemoryDriver.js +158 -0
  17. package/src/cache/drivers/NullDriver.js +27 -0
  18. package/src/cache/index.js +8 -0
  19. package/src/cli.js +27 -0
  20. package/src/commands/make.js +61 -0
  21. package/src/commands/migrate.js +174 -0
  22. package/src/commands/new.js +50 -0
  23. package/src/commands/queue.js +92 -0
  24. package/src/commands/route.js +93 -0
  25. package/src/commands/serve.js +50 -0
  26. package/src/container/Application.js +177 -0
  27. package/src/container/Container.js +281 -0
  28. package/src/container/index.js +13 -0
  29. package/src/controller/Controller.js +367 -0
  30. package/src/errors/HttpError.js +29 -0
  31. package/src/events/Event.js +39 -0
  32. package/src/events/EventEmitter.js +151 -0
  33. package/src/events/Listener.js +46 -0
  34. package/src/events/index.js +15 -0
  35. package/src/index.js +93 -0
  36. package/src/mail/Mail.js +210 -0
  37. package/src/mail/MailMessage.js +196 -0
  38. package/src/mail/TemplateEngine.js +150 -0
  39. package/src/mail/drivers/LogDriver.js +36 -0
  40. package/src/mail/drivers/MailgunDriver.js +84 -0
  41. package/src/mail/drivers/SendGridDriver.js +97 -0
  42. package/src/mail/drivers/SmtpDriver.js +67 -0
  43. package/src/mail/index.js +19 -0
  44. package/src/middleware/AuthMiddleware.js +46 -0
  45. package/src/middleware/CorsMiddleware.js +59 -0
  46. package/src/middleware/LogMiddleware.js +61 -0
  47. package/src/middleware/Middleware.js +36 -0
  48. package/src/middleware/MiddlewarePipeline.js +94 -0
  49. package/src/middleware/ThrottleMiddleware.js +61 -0
  50. package/src/orm/drivers/DatabaseManager.js +135 -0
  51. package/src/orm/fields/index.js +132 -0
  52. package/src/orm/index.js +19 -0
  53. package/src/orm/migration/MigrationRunner.js +216 -0
  54. package/src/orm/migration/ModelInspector.js +338 -0
  55. package/src/orm/migration/SchemaBuilder.js +173 -0
  56. package/src/orm/model/Model.js +371 -0
  57. package/src/orm/query/QueryBuilder.js +197 -0
  58. package/src/providers/AdminServiceProvider.js +40 -0
  59. package/src/providers/AuthServiceProvider.js +53 -0
  60. package/src/providers/CacheStorageServiceProvider.js +71 -0
  61. package/src/providers/DatabaseServiceProvider.js +45 -0
  62. package/src/providers/EventServiceProvider.js +34 -0
  63. package/src/providers/MailServiceProvider.js +51 -0
  64. package/src/providers/ProviderRegistry.js +82 -0
  65. package/src/providers/QueueServiceProvider.js +52 -0
  66. package/src/providers/ServiceProvider.js +45 -0
  67. package/src/queue/Job.js +135 -0
  68. package/src/queue/Queue.js +147 -0
  69. package/src/queue/drivers/DatabaseDriver.js +194 -0
  70. package/src/queue/drivers/SyncDriver.js +72 -0
  71. package/src/queue/index.js +16 -0
  72. package/src/queue/workers/QueueWorker.js +140 -0
  73. package/src/router/MiddlewareRegistry.js +82 -0
  74. package/src/router/Route.js +255 -0
  75. package/src/router/RouteGroup.js +19 -0
  76. package/src/router/RouteRegistry.js +55 -0
  77. package/src/router/Router.js +138 -0
  78. package/src/router/index.js +15 -0
  79. package/src/scaffold/generator.js +34 -0
  80. package/src/scaffold/maker.js +272 -0
  81. package/src/scaffold/templates.js +350 -0
  82. package/src/storage/Storage.js +170 -0
  83. package/src/storage/drivers/LocalDriver.js +215 -0
  84. package/src/storage/index.js +6 -0
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * DatabaseManager
5
+ *
6
+ * Creates and caches knex connection instances per named connection.
7
+ * Reads from config/database.js (or the config object passed in).
8
+ *
9
+ * Usage:
10
+ * const db = DatabaseManager.connection(); // default
11
+ * const db = DatabaseManager.connection('mysql'); // named
12
+ * await db.schema.createTable(...)
13
+ * await db('users').select('*')
14
+ */
15
+ class DatabaseManager {
16
+ constructor() {
17
+ this._connections = new Map();
18
+ this._config = null;
19
+ this._default = null;
20
+ }
21
+
22
+ /**
23
+ * Configure the manager with a database config object.
24
+ * Called automatically by DatabaseServiceProvider.
25
+ */
26
+ configure(config) {
27
+ this._config = config;
28
+ this._default = config.default || 'sqlite';
29
+ }
30
+
31
+ /**
32
+ * Get (or create) a knex connection by name.
33
+ * @param {string} name — connection name from config/database.js
34
+ */
35
+ connection(name) {
36
+ const connName = name || this._default;
37
+
38
+ if (this._connections.has(connName)) {
39
+ return this._connections.get(connName);
40
+ }
41
+
42
+ const conn = this._makeConnection(connName);
43
+ this._connections.set(connName, conn);
44
+ return conn;
45
+ }
46
+
47
+ /**
48
+ * Shorthand for the default connection.
49
+ */
50
+ get db() {
51
+ return this.connection();
52
+ }
53
+
54
+ /**
55
+ * Close all connections.
56
+ */
57
+ async closeAll() {
58
+ for (const [, conn] of this._connections) {
59
+ await conn.destroy();
60
+ }
61
+ this._connections.clear();
62
+ }
63
+
64
+ /**
65
+ * Close a specific connection.
66
+ */
67
+ async close(name) {
68
+ const conn = this._connections.get(name || this._default);
69
+ if (conn) {
70
+ await conn.destroy();
71
+ this._connections.delete(name || this._default);
72
+ }
73
+ }
74
+
75
+ // ─── Internal ─────────────────────────────────────────────────────────────
76
+
77
+ _makeConnection(name) {
78
+ if (!this._config) {
79
+ throw new Error(
80
+ 'DatabaseManager not configured. ' +
81
+ 'Did you boot DatabaseServiceProvider?'
82
+ );
83
+ }
84
+
85
+ const conf = this._config.connections?.[name];
86
+ if (!conf) {
87
+ throw new Error(`Database connection "${name}" is not defined in config/database.js`);
88
+ }
89
+
90
+ const knex = require('knex');
91
+
92
+ switch (conf.driver) {
93
+ case 'sqlite':
94
+ return knex({
95
+ client: 'sqlite3',
96
+ connection: { filename: conf.database },
97
+ useNullAsDefault: true,
98
+ asyncStackTraces: process.env.NODE_ENV !== 'production',
99
+ });
100
+
101
+ case 'mysql':
102
+ return knex({
103
+ client: 'mysql2',
104
+ connection: {
105
+ host: conf.host,
106
+ port: conf.port,
107
+ database: conf.database,
108
+ user: conf.username,
109
+ password: conf.password,
110
+ },
111
+ pool: { min: 2, max: 10 },
112
+ });
113
+
114
+ case 'postgres':
115
+ return knex({
116
+ client: 'pg',
117
+ connection: {
118
+ host: conf.host,
119
+ port: conf.port,
120
+ database: conf.database,
121
+ user: conf.username,
122
+ password: conf.password,
123
+ },
124
+ pool: { min: 2, max: 10 },
125
+ });
126
+
127
+ default:
128
+ throw new Error(`Unsupported database driver: "${conf.driver}"`);
129
+ }
130
+ }
131
+ }
132
+
133
+ // Singleton — shared across the whole application
134
+ const manager = new DatabaseManager();
135
+ module.exports = manager;
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * fields
5
+ *
6
+ * Schema field definitions used inside Model.fields = { ... }
7
+ *
8
+ * Usage:
9
+ * class User extends Model {
10
+ * static fields = {
11
+ * id: fields.id(),
12
+ * name: fields.string({ max: 100 }),
13
+ * email: fields.string({ unique: true }),
14
+ * age: fields.integer({ nullable: true }),
15
+ * score: fields.float({ default: 0.0 }),
16
+ * active: fields.boolean({ default: true }),
17
+ * bio: fields.text({ nullable: true }),
18
+ * data: fields.json({ nullable: true }),
19
+ * role: fields.enum(['admin', 'user', 'guest'], { default: 'user' }),
20
+ * created_at: fields.timestamp(),
21
+ * updated_at: fields.timestamp(),
22
+ * };
23
+ * }
24
+ */
25
+
26
+ class FieldDefinition {
27
+ constructor(type, options = {}) {
28
+ this.type = type;
29
+ this.options = options;
30
+ this.nullable = options.nullable ?? false;
31
+ this.unique = options.unique ?? false;
32
+ this.default = options.default !== undefined ? options.default : undefined;
33
+ this.primary = options.primary ?? false;
34
+ this.unsigned = options.unsigned ?? false;
35
+ this.max = options.max ?? null;
36
+ this.enumValues = options.enumValues ?? null;
37
+ this.references = options.references ?? null; // { table, column }
38
+ }
39
+
40
+ // ─── Fluent modifiers ──────────────────────────────────────────
41
+
42
+ nullable_(val = true) { this.nullable = val; return this; }
43
+ unique_(val = true) { this.unique = val; return this; }
44
+ default_(val) { this.default = val; return this; }
45
+ unsigned_(val = true) { this.unsigned = val; return this; }
46
+ references_(table, col) { this.references = { table, column: col }; return this; }
47
+ }
48
+
49
+ const fields = {
50
+ /** Auto-incrementing primary key */
51
+ id(options = {}) {
52
+ return new FieldDefinition('id', { primary: true, unsigned: true, ...options });
53
+ },
54
+
55
+ /** VARCHAR / TEXT string */
56
+ string(options = {}) {
57
+ return new FieldDefinition('string', { max: 255, ...options });
58
+ },
59
+
60
+ /** LONGTEXT */
61
+ text(options = {}) {
62
+ return new FieldDefinition('text', options);
63
+ },
64
+
65
+ /** INT */
66
+ integer(options = {}) {
67
+ return new FieldDefinition('integer', options);
68
+ },
69
+
70
+ /** BIGINT */
71
+ bigInteger(options = {}) {
72
+ return new FieldDefinition('bigInteger', options);
73
+ },
74
+
75
+ /** FLOAT / DOUBLE */
76
+ float(options = {}) {
77
+ return new FieldDefinition('float', options);
78
+ },
79
+
80
+ /** DECIMAL(precision, scale) */
81
+ decimal(precision = 8, scale = 2, options = {}) {
82
+ return new FieldDefinition('decimal', { precision, scale, ...options });
83
+ },
84
+
85
+ /** TINYINT(1) boolean */
86
+ boolean(options = {}) {
87
+ return new FieldDefinition('boolean', options);
88
+ },
89
+
90
+ /** JSON blob */
91
+ json(options = {}) {
92
+ return new FieldDefinition('json', options);
93
+ },
94
+
95
+ /** DATE */
96
+ date(options = {}) {
97
+ return new FieldDefinition('date', options);
98
+ },
99
+
100
+ /** DATETIME / TIMESTAMP */
101
+ timestamp(options = {}) {
102
+ return new FieldDefinition('timestamp', { nullable: true, ...options });
103
+ },
104
+
105
+ /** ENUM */
106
+ enum(values, options = {}) {
107
+ return new FieldDefinition('enum', { enumValues: values, ...options });
108
+ },
109
+
110
+ /** UUID */
111
+ uuid(options = {}) {
112
+ return new FieldDefinition('uuid', options);
113
+ },
114
+
115
+ /**
116
+ * Foreign key integer shorthand.
117
+ * fields.foreignId('user_id') → unsigned integer, references users.id
118
+ */
119
+ foreignId(column, options = {}) {
120
+ const [table, col] = column.endsWith('_id')
121
+ ? [column.slice(0, -3) + 's', 'id']
122
+ : [null, null];
123
+ return new FieldDefinition('integer', {
124
+ unsigned: true,
125
+ nullable: options.nullable ?? false,
126
+ references: table ? { table, column: col } : null,
127
+ ...options,
128
+ });
129
+ },
130
+ };
131
+
132
+ module.exports = { fields, FieldDefinition };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const Model = require('./model/Model');
4
+ const { fields } = require('./fields');
5
+ const QueryBuilder = require('./query/QueryBuilder');
6
+ const DatabaseManager = require('./drivers/DatabaseManager');
7
+ const SchemaBuilder = require('./migration/SchemaBuilder');
8
+ const MigrationRunner = require('./migration/MigrationRunner');
9
+ const ModelInspector = require('./migration/ModelInspector');
10
+
11
+ module.exports = {
12
+ Model,
13
+ fields,
14
+ QueryBuilder,
15
+ DatabaseManager,
16
+ SchemaBuilder,
17
+ MigrationRunner,
18
+ ModelInspector,
19
+ };
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * MigrationRunner
8
+ *
9
+ * Handles the full migration lifecycle:
10
+ * - run pending migrations (migrate)
11
+ * - rollback last batch (migrate:rollback)
12
+ * - show status table (migrate:status)
13
+ * - drop all + re-run (migrate:fresh)
14
+ * - rollback all (migrate:reset)
15
+ * - rollback all + re-run (migrate:refresh)
16
+ *
17
+ * Migration history is tracked in the `millas_migrations` table.
18
+ * Each migration file must export { up(db), down(db) }.
19
+ */
20
+ class MigrationRunner {
21
+ /**
22
+ * @param {object} knexConn — live knex connection
23
+ * @param {string} migrationsPath — absolute path to migrations dir
24
+ */
25
+ constructor(knexConn, migrationsPath) {
26
+ this._db = knexConn;
27
+ this._path = migrationsPath;
28
+ }
29
+
30
+ // ─── Public commands ──────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Run all pending migrations.
34
+ */
35
+ async migrate() {
36
+ await this._ensureTable();
37
+ const pending = await this._pending();
38
+
39
+ if (pending.length === 0) {
40
+ return { ran: [], message: 'Nothing to migrate.' };
41
+ }
42
+
43
+ const batch = await this._nextBatch();
44
+ const ran = [];
45
+
46
+ for (const file of pending) {
47
+ const migration = this._load(file);
48
+ await migration.up(this._db);
49
+ await this._record(file, batch);
50
+ ran.push(file);
51
+ }
52
+
53
+ return { ran, batch, message: `Ran ${ran.length} migration(s).` };
54
+ }
55
+
56
+ /**
57
+ * Rollback the last batch of migrations.
58
+ */
59
+ async rollback(steps = 1) {
60
+ await this._ensureTable();
61
+ const batches = await this._lastBatches(steps);
62
+
63
+ if (batches.length === 0) {
64
+ return { rolledBack: [], message: 'Nothing to rollback.' };
65
+ }
66
+
67
+ const rolledBack = [];
68
+
69
+ // Run in reverse order
70
+ for (const row of [...batches].reverse()) {
71
+ const migration = this._load(row.name);
72
+ await migration.down(this._db);
73
+ await this._db('millas_migrations').where('name', row.name).delete();
74
+ rolledBack.push(row.name);
75
+ }
76
+
77
+ return { rolledBack, message: `Rolled back ${rolledBack.length} migration(s).` };
78
+ }
79
+
80
+ /**
81
+ * Drop all tables and re-run every migration.
82
+ */
83
+ async fresh() {
84
+ await this._dropAllTables();
85
+ await this._ensureTable();
86
+ return this.migrate();
87
+ }
88
+
89
+ /**
90
+ * Rollback ALL migrations.
91
+ */
92
+ async reset() {
93
+ await this._ensureTable();
94
+ const all = await this._db('millas_migrations').orderBy('id', 'desc');
95
+
96
+ if (all.length === 0) {
97
+ return { rolledBack: [], message: 'Nothing to reset.' };
98
+ }
99
+
100
+ const rolledBack = [];
101
+ for (const row of all) {
102
+ const migration = this._load(row.name);
103
+ await migration.down(this._db);
104
+ await this._db('millas_migrations').where('name', row.name).delete();
105
+ rolledBack.push(row.name);
106
+ }
107
+
108
+ return { rolledBack, message: `Reset ${rolledBack.length} migration(s).` };
109
+ }
110
+
111
+ /**
112
+ * Rollback all then re-run all.
113
+ */
114
+ async refresh() {
115
+ await this.reset();
116
+ return this.migrate();
117
+ }
118
+
119
+ /**
120
+ * Return status of all migration files.
121
+ */
122
+ async status() {
123
+ await this._ensureTable();
124
+ const files = this._files();
125
+ const ran = await this._ranNames();
126
+
127
+ return files.map(file => ({
128
+ name: file,
129
+ status: ran.has(file) ? 'Ran' : 'Pending',
130
+ batch: ran.get ? ran.get(file) : null,
131
+ }));
132
+ }
133
+
134
+ // ─── Internal ─────────────────────────────────────────────────────────────
135
+
136
+ async _ensureTable() {
137
+ const exists = await this._db.schema.hasTable('millas_migrations');
138
+ if (exists) return;
139
+
140
+ await this._db.schema.createTable('millas_migrations', (t) => {
141
+ t.increments('id');
142
+ t.string('name').notNullable().unique();
143
+ t.integer('batch').notNullable();
144
+ t.timestamp('ran_at').defaultTo(this._db.fn.now());
145
+ });
146
+ }
147
+
148
+ async _pending() {
149
+ const ran = await this._ranNames();
150
+ const files = this._files();
151
+ return files.filter(f => !ran.has(f));
152
+ }
153
+
154
+ async _ranNames() {
155
+ const rows = await this._db('millas_migrations').select('name', 'batch');
156
+ const map = new Map(rows.map(r => [r.name, r.batch]));
157
+ // Map with has() and get()
158
+ return map;
159
+ }
160
+
161
+ async _nextBatch() {
162
+ const result = await this._db('millas_migrations').max('batch as max').first();
163
+ return (result?.max || 0) + 1;
164
+ }
165
+
166
+ async _lastBatches(steps = 1) {
167
+ const maxBatch = await this._db('millas_migrations').max('batch as max').first();
168
+ if (!maxBatch?.max) return [];
169
+
170
+ const fromBatch = maxBatch.max - steps + 1;
171
+ // Fetch all then filter in JS to avoid needing >= support in all drivers
172
+ const all = await this._db('millas_migrations').orderBy('id', 'desc');
173
+ return all.filter(r => r.batch >= fromBatch);
174
+ }
175
+
176
+ async _dropAllTables() {
177
+ // Get all table names (SQLite compatible)
178
+ const tables = await this._db
179
+ .select('name')
180
+ .from('sqlite_master')
181
+ .where('type', 'table')
182
+ .whereNot('name', 'like', 'sqlite_%');
183
+
184
+ for (const { name } of tables) {
185
+ await this._db.schema.dropTableIfExists(name);
186
+ }
187
+ }
188
+
189
+ _files() {
190
+ if (!fs.existsSync(this._path)) return [];
191
+ return fs.readdirSync(this._path)
192
+ .filter(f => f.endsWith('.js') && !f.startsWith('.'))
193
+ .sort(); // Sorted by timestamp prefix
194
+ }
195
+
196
+ _load(name) {
197
+ const filePath = path.join(this._path, name);
198
+ // Clear require cache so fresh loads work in tests
199
+ delete require.cache[require.resolve(filePath)];
200
+ const migration = require(filePath);
201
+ if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
202
+ throw new Error(`Migration "${name}" must export { up(db), down(db) }`);
203
+ }
204
+ return migration;
205
+ }
206
+
207
+ async _record(name, batch) {
208
+ await this._db('millas_migrations').insert({
209
+ name,
210
+ batch,
211
+ ran_at: new Date().toISOString(),
212
+ });
213
+ }
214
+ }
215
+
216
+ module.exports = MigrationRunner;