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,338 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * ModelInspector
8
+ *
9
+ * Implements `millas makemigrations` — Django-style migration generation.
10
+ *
11
+ * Workflow:
12
+ * 1. Scan app/models/ for Model subclasses with static fields
13
+ * 2. Load the last known schema snapshot (.millas/schema.json)
14
+ * 3. Diff current fields vs snapshot
15
+ * 4. Generate timestamped migration file(s) for each change
16
+ * 5. Update the snapshot
17
+ *
18
+ * Detects:
19
+ * - New tables (model added)
20
+ * - Dropped tables (model removed)
21
+ * - Added columns
22
+ * - Removed columns
23
+ * - Changed column types
24
+ */
25
+ class ModelInspector {
26
+ constructor(modelsPath, migrationsPath, snapshotPath) {
27
+ this._modelsPath = modelsPath;
28
+ this._migrationsPath = migrationsPath;
29
+ this._snapshotPath = snapshotPath || path.join(process.cwd(), '.millas', 'schema.json');
30
+ }
31
+
32
+ /**
33
+ * Detect changes and generate migration files.
34
+ * Returns an array of generated file paths.
35
+ */
36
+ async makeMigrations() {
37
+ const current = this._scanModels();
38
+ const snapshot = this._loadSnapshot();
39
+ const diffs = this._diff(current, snapshot);
40
+
41
+ if (diffs.length === 0) {
42
+ return { files: [], message: 'No changes detected.' };
43
+ }
44
+
45
+ const files = [];
46
+ for (const diff of diffs) {
47
+ const file = await this._generateMigration(diff);
48
+ if (file) files.push(file);
49
+ }
50
+
51
+ this._saveSnapshot(current);
52
+ return { files, message: `Generated ${files.length} migration file(s).` };
53
+ }
54
+
55
+ // ─── Model scanning ───────────────────────────────────────────────────────
56
+
57
+ _scanModels() {
58
+ const schema = {};
59
+ if (!fs.existsSync(this._modelsPath)) return schema;
60
+
61
+ const files = fs.readdirSync(this._modelsPath)
62
+ .filter(f => f.endsWith('.js') && !f.startsWith('.'));
63
+
64
+ for (const file of files) {
65
+ try {
66
+ const ModelClass = require(path.join(this._modelsPath, file));
67
+ if (!ModelClass || !ModelClass.fields) continue;
68
+
69
+ const table = ModelClass.table || file.replace('.js', '').toLowerCase() + 's';
70
+ schema[table] = this._extractFields(ModelClass.fields);
71
+ } catch {
72
+ // Skip unloadable models
73
+ }
74
+ }
75
+
76
+ return schema;
77
+ }
78
+
79
+ _extractFields(fields) {
80
+ const result = {};
81
+ for (const [name, field] of Object.entries(fields)) {
82
+ result[name] = {
83
+ type: field.type,
84
+ nullable: field.nullable ?? false,
85
+ unique: field.unique ?? false,
86
+ default: field.default !== undefined ? field.default : null,
87
+ max: field.max ?? null,
88
+ unsigned: field.unsigned ?? false,
89
+ enumValues: field.enumValues ?? null,
90
+ references: field.references ?? null,
91
+ };
92
+ }
93
+ return result;
94
+ }
95
+
96
+ // ─── Diffing ──────────────────────────────────────────────────────────────
97
+
98
+ _diff(current, snapshot) {
99
+ const diffs = [];
100
+
101
+ // New tables
102
+ for (const table of Object.keys(current)) {
103
+ if (!snapshot[table]) {
104
+ diffs.push({ type: 'create_table', table, fields: current[table] });
105
+ }
106
+ }
107
+
108
+ // Dropped tables
109
+ for (const table of Object.keys(snapshot)) {
110
+ if (!current[table]) {
111
+ diffs.push({ type: 'drop_table', table });
112
+ }
113
+ }
114
+
115
+ // Column changes on existing tables
116
+ for (const table of Object.keys(current)) {
117
+ if (!snapshot[table]) continue;
118
+
119
+ const curr = current[table];
120
+ const prev = snapshot[table];
121
+
122
+ // Added columns
123
+ for (const col of Object.keys(curr)) {
124
+ if (!prev[col]) {
125
+ diffs.push({ type: 'add_column', table, column: col, field: curr[col] });
126
+ }
127
+ }
128
+
129
+ // Removed columns
130
+ for (const col of Object.keys(prev)) {
131
+ if (!curr[col]) {
132
+ diffs.push({ type: 'drop_column', table, column: col });
133
+ }
134
+ }
135
+
136
+ // Changed columns
137
+ for (const col of Object.keys(curr)) {
138
+ if (!prev[col]) continue;
139
+ if (JSON.stringify(curr[col]) !== JSON.stringify(prev[col])) {
140
+ diffs.push({ type: 'alter_column', table, column: col, field: curr[col], previous: prev[col] });
141
+ }
142
+ }
143
+ }
144
+
145
+ return diffs;
146
+ }
147
+
148
+ // ─── Migration generation ─────────────────────────────────────────────────
149
+
150
+ async _generateMigration(diff) {
151
+ const name = this._diffToName(diff);
152
+ const ts = new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
153
+ const fileName = `${ts}_${name}.js`;
154
+ const filePath = path.join(this._migrationsPath, fileName);
155
+
156
+ await fs.ensureDir(this._migrationsPath);
157
+
158
+ const content = this._renderMigration(diff, name);
159
+ await fs.writeFile(filePath, content, 'utf8');
160
+ return fileName;
161
+ }
162
+
163
+ _diffToName(diff) {
164
+ switch (diff.type) {
165
+ case 'create_table': return `create_${diff.table}_table`;
166
+ case 'drop_table': return `drop_${diff.table}_table`;
167
+ case 'add_column': return `add_${diff.column}_to_${diff.table}`;
168
+ case 'drop_column': return `remove_${diff.column}_from_${diff.table}`;
169
+ case 'alter_column': return `alter_${diff.column}_on_${diff.table}`;
170
+ default: return `migration_${diff.type}`;
171
+ }
172
+ }
173
+
174
+ _renderMigration(diff, name) {
175
+ switch (diff.type) {
176
+
177
+ case 'create_table':
178
+ return `'use strict';
179
+
180
+ /**
181
+ * Migration: ${name}
182
+ * Auto-generated by: millas makemigrations
183
+ */
184
+ module.exports = {
185
+ async up(db) {
186
+ await db.schema.createTable('${diff.table}', (t) => {
187
+ ${this._renderColumns(diff.fields)} });
188
+ },
189
+
190
+ async down(db) {
191
+ await db.schema.dropTableIfExists('${diff.table}');
192
+ },
193
+ };
194
+ `;
195
+
196
+ case 'drop_table':
197
+ return `'use strict';
198
+
199
+ /**
200
+ * Migration: ${name}
201
+ */
202
+ module.exports = {
203
+ async up(db) {
204
+ await db.schema.dropTableIfExists('${diff.table}');
205
+ },
206
+
207
+ async down(db) {
208
+ // Recreate if needed — add column definitions here
209
+ await db.schema.createTable('${diff.table}', (t) => {
210
+ t.increments('id');
211
+ t.timestamps();
212
+ });
213
+ },
214
+ };
215
+ `;
216
+
217
+ case 'add_column':
218
+ return `'use strict';
219
+
220
+ /**
221
+ * Migration: ${name}
222
+ */
223
+ module.exports = {
224
+ async up(db) {
225
+ await db.schema.table('${diff.table}', (t) => {
226
+ ${this._renderColumn(' ', diff.column, diff.field)}
227
+ });
228
+ },
229
+
230
+ async down(db) {
231
+ await db.schema.table('${diff.table}', (t) => {
232
+ t.dropColumn('${diff.column}');
233
+ });
234
+ },
235
+ };
236
+ `;
237
+
238
+ case 'drop_column':
239
+ return `'use strict';
240
+
241
+ /**
242
+ * Migration: ${name}
243
+ */
244
+ module.exports = {
245
+ async up(db) {
246
+ await db.schema.table('${diff.table}', (t) => {
247
+ t.dropColumn('${diff.column}');
248
+ });
249
+ },
250
+
251
+ async down(db) {
252
+ await db.schema.table('${diff.table}', (t) => {
253
+ t.string('${diff.column}').nullable();
254
+ });
255
+ },
256
+ };
257
+ `;
258
+
259
+ case 'alter_column':
260
+ return `'use strict';
261
+
262
+ /**
263
+ * Migration: ${name}
264
+ * Changed: ${JSON.stringify(diff.previous)} → ${JSON.stringify(diff.field)}
265
+ */
266
+ module.exports = {
267
+ async up(db) {
268
+ await db.schema.table('${diff.table}', (t) => {
269
+ ${this._renderColumn(' ', diff.column, diff.field, '.alter()')}
270
+ });
271
+ },
272
+
273
+ async down(db) {
274
+ await db.schema.table('${diff.table}', (t) => {
275
+ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
276
+ });
277
+ },
278
+ };
279
+ `;
280
+
281
+ default:
282
+ return `'use strict';\nmodule.exports = { async up(db) {}, async down(db) {} };\n`;
283
+ }
284
+ }
285
+
286
+ _renderColumns(fields) {
287
+ return Object.entries(fields)
288
+ .map(([name, field]) => this._renderColumn(' ', name, field))
289
+ .join('\n') + '\n';
290
+ }
291
+
292
+ _renderColumn(indent, name, field, suffix = '') {
293
+ let line;
294
+ switch (field.type) {
295
+ case 'id': line = `t.increments('${name}')`; break;
296
+ case 'string': line = `t.string('${name}', ${field.max || 255})`; break;
297
+ case 'text': line = `t.text('${name}')`; break;
298
+ case 'integer': line = field.unsigned ? `t.integer('${name}').unsigned()` : `t.integer('${name}')`; break;
299
+ case 'bigInteger': line = `t.bigInteger('${name}')`; break;
300
+ case 'float': line = `t.float('${name}')`; break;
301
+ case 'decimal': line = `t.decimal('${name}', ${field.precision||8}, ${field.scale||2})`; break;
302
+ case 'boolean': line = `t.boolean('${name}')`; break;
303
+ case 'json': line = `t.json('${name}')`; break;
304
+ case 'date': line = `t.date('${name}')`; break;
305
+ case 'timestamp': line = `t.timestamp('${name}', { useTz: false })`; break;
306
+ case 'enum': line = `t.enum('${name}', ${JSON.stringify(field.enumValues||[])})`;break;
307
+ case 'uuid': line = `t.uuid('${name}')`; break;
308
+ default: line = `t.string('${name}')`;
309
+ }
310
+
311
+ if (field.nullable) line += '.nullable()';
312
+ else if (field.type !== 'id') line += '.notNullable()';
313
+ if (field.unique) line += '.unique()';
314
+ if (field.default !== null && field.default !== undefined)
315
+ line += `.defaultTo(${JSON.stringify(field.default)})`;
316
+ if (field.references)
317
+ line += `.references('${field.references.column}').inTable('${field.references.table}').onDelete('CASCADE')`;
318
+
319
+ return `${indent}${line}${suffix};`;
320
+ }
321
+
322
+ // ─── Snapshot ─────────────────────────────────────────────────────────────
323
+
324
+ _loadSnapshot() {
325
+ try {
326
+ return fs.readJsonSync(this._snapshotPath);
327
+ } catch {
328
+ return {};
329
+ }
330
+ }
331
+
332
+ _saveSnapshot(schema) {
333
+ fs.ensureDirSync(path.dirname(this._snapshotPath));
334
+ fs.writeJsonSync(this._snapshotPath, schema, { spaces: 2 });
335
+ }
336
+ }
337
+
338
+ module.exports = ModelInspector;
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const { FieldDefinition } = require('../fields');
4
+
5
+ /**
6
+ * SchemaBuilder
7
+ *
8
+ * Converts Model.fields definitions into knex schema operations.
9
+ * Used by the migration system (Phase 6) and DatabaseServiceProvider.
10
+ *
11
+ * Usage:
12
+ * const sb = new SchemaBuilder(db);
13
+ * await sb.createFromModel(User);
14
+ * await sb.dropTable('users');
15
+ * await sb.tableExists('users');
16
+ */
17
+ class SchemaBuilder {
18
+ constructor(knexConnection) {
19
+ this._db = knexConnection;
20
+ }
21
+
22
+ /**
23
+ * Create a table from a Model class's static fields definition.
24
+ */
25
+ async createFromModel(ModelClass) {
26
+ const table = ModelClass.table;
27
+ const fields = ModelClass.fields;
28
+
29
+ await this._db.schema.createTable(table, (t) => {
30
+ this._applyFields(t, fields, ModelClass);
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Create table only if it doesn't exist.
36
+ */
37
+ async createFromModelIfNotExists(ModelClass) {
38
+ const exists = await this.tableExists(ModelClass.table);
39
+ if (!exists) await this.createFromModel(ModelClass);
40
+ }
41
+
42
+ /**
43
+ * Drop a table.
44
+ */
45
+ async dropTable(tableName) {
46
+ await this._db.schema.dropTableIfExists(tableName);
47
+ }
48
+
49
+ /**
50
+ * Check whether a table exists.
51
+ */
52
+ async tableExists(tableName) {
53
+ return this._db.schema.hasTable(tableName);
54
+ }
55
+
56
+ /**
57
+ * Add a column to an existing table.
58
+ */
59
+ async addColumn(tableName, columnName, fieldDef) {
60
+ await this._db.schema.table(tableName, (t) => {
61
+ this._applyField(t, columnName, fieldDef);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Drop a column from a table.
67
+ */
68
+ async dropColumn(tableName, columnName) {
69
+ await this._db.schema.table(tableName, (t) => {
70
+ t.dropColumn(columnName);
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Rename a table.
76
+ */
77
+ async renameTable(from, to) {
78
+ await this._db.schema.renameTable(from, to);
79
+ }
80
+
81
+ /**
82
+ * Return the raw knex schema builder for advanced use.
83
+ */
84
+ get schema() {
85
+ return this._db.schema;
86
+ }
87
+
88
+ // ─── Internal ─────────────────────────────────────────────────────────────
89
+
90
+ _applyFields(tableBuilder, fields, ModelClass) {
91
+ for (const [name, field] of Object.entries(fields)) {
92
+ this._applyField(tableBuilder, name, field, ModelClass);
93
+ }
94
+ }
95
+
96
+ _applyField(t, name, field) {
97
+ let col;
98
+
99
+ switch (field.type) {
100
+ case 'id':
101
+ t.increments(name);
102
+ return;
103
+
104
+ case 'string':
105
+ col = t.string(name, field.max || 255);
106
+ break;
107
+
108
+ case 'text':
109
+ col = t.text(name);
110
+ break;
111
+
112
+ case 'integer':
113
+ col = field.unsigned ? t.integer(name).unsigned() : t.integer(name);
114
+ break;
115
+
116
+ case 'bigInteger':
117
+ col = field.unsigned ? t.bigInteger(name).unsigned() : t.bigInteger(name);
118
+ break;
119
+
120
+ case 'float':
121
+ col = t.float(name);
122
+ break;
123
+
124
+ case 'decimal':
125
+ col = t.decimal(name, field.precision || 8, field.scale || 2);
126
+ break;
127
+
128
+ case 'boolean':
129
+ col = t.boolean(name);
130
+ break;
131
+
132
+ case 'json':
133
+ col = t.json(name);
134
+ break;
135
+
136
+ case 'date':
137
+ col = t.date(name);
138
+ break;
139
+
140
+ case 'timestamp':
141
+ col = t.timestamp(name, { useTz: false });
142
+ break;
143
+
144
+ case 'enum':
145
+ col = t.enum(name, field.enumValues || []);
146
+ break;
147
+
148
+ case 'uuid':
149
+ col = t.uuid(name);
150
+ break;
151
+
152
+ default:
153
+ col = t.string(name);
154
+ }
155
+
156
+ if (!col) return;
157
+
158
+ if (field.nullable) col = col.nullable();
159
+ else if (field.type !== 'id') col = col.notNullable();
160
+
161
+ if (field.unique) col = col.unique();
162
+
163
+ if (field.default !== undefined) col = col.defaultTo(field.default);
164
+
165
+ if (field.references) {
166
+ col = col.references(field.references.column)
167
+ .inTable(field.references.table)
168
+ .onDelete('CASCADE');
169
+ }
170
+ }
171
+ }
172
+
173
+ module.exports = SchemaBuilder;