millas 0.1.7 → 0.1.9

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.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "millas",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
5
5
  "main": "src/index.js",
6
6
  "exports": {
7
- ".": "./src/index.js",
8
- "./src": "./src/index.js",
9
- "./src/*": "./src/*.js",
10
- "./bin/*": "./bin/*.js"
7
+ ".": "./src/index.js",
8
+ "./src": "./src/index.js",
9
+ "./src/*": "./src/*.js",
10
+ "./bin/*": "./bin/*.js"
11
11
  },
12
12
  "bin": {
13
13
  "millas": "./bin/millas.js"
@@ -16,9 +16,20 @@
16
16
  "test": "echo \"Run phase-specific tests in tests/\""
17
17
  },
18
18
  "keywords": [
19
- "framework", "express", "orm", "cli", "auth",
20
- "queue", "cache", "admin", "mail", "events",
21
- "laravel", "django", "fastapi", "millas"
19
+ "framework",
20
+ "express",
21
+ "orm",
22
+ "cli",
23
+ "auth",
24
+ "queue",
25
+ "cache",
26
+ "admin",
27
+ "mail",
28
+ "events",
29
+ "laravel",
30
+ "django",
31
+ "fastapi",
32
+ "millas"
22
33
  ],
23
34
  "author": "Millas Framework",
24
35
  "license": "MIT",
@@ -26,22 +37,24 @@
26
37
  "node": ">=18.0.0"
27
38
  },
28
39
  "dependencies": {
29
- "bcryptjs": "3.0.2",
30
- "chalk": "4.1.2",
31
- "commander": "^11.0.0",
32
- "fs-extra": "^11.0.0",
33
- "inquirer": "8.2.6",
34
- "jsonwebtoken":"^9.0.3",
35
- "knex": "^3.1.0",
36
- "nodemailer": "^6.9.0",
37
- "nunjucks": "^3.2.4",
38
- "ora": "5.4.1"
40
+ "bcryptjs": "3.0.2",
41
+ "chalk": "4.1.2",
42
+ "commander": "^11.0.0",
43
+ "fs-extra": "^11.0.0",
44
+ "inquirer": "8.2.6",
45
+ "jsonwebtoken": "^9.0.3",
46
+ "knex": "^3.1.0",
47
+ "nodemailer": "^6.9.0",
48
+ "nunjucks": "^3.2.4",
49
+ "ora": "5.4.1"
39
50
  },
40
51
  "peerDependencies": {
41
52
  "express": "^4.18.0"
42
53
  },
43
54
  "peerDependenciesMeta": {
44
- "express": { "optional": false }
55
+ "express": {
56
+ "optional": false
57
+ }
45
58
  },
46
59
  "files": [
47
60
  "bin/",
@@ -6,133 +6,185 @@ const fs = require('fs-extra');
6
6
 
7
7
  module.exports = function (program) {
8
8
 
9
+ // ── makemigrations ────────────────────────────────────────────────────────
10
+
9
11
  program
10
12
  .command('makemigrations')
11
- .description('Detect model changes and generate migration files')
13
+ .description('Scan model files, detect schema changes, generate migration files')
12
14
  .action(async () => {
13
- const ctx = getProjectContext();
14
- const ModelInspector = require('../orm/migration/ModelInspector');
15
- const inspector = new ModelInspector(
16
- ctx.modelsPath,
17
- ctx.migrationsPath,
18
- ctx.snapshotPath
19
- );
20
- const result = await inspector.makeMigrations();
21
- if (result.files.length === 0) {
22
- console.log(chalk.yellow(`\n ${result.message}\n`));
23
- } else {
24
- console.log(chalk.green(`\n ✔ ${result.message}`));
25
- result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
26
- console.log();
15
+ try {
16
+ const ctx = getProjectContext();
17
+ // Fixed: was incorrectly destructured as { ModelInspector }
18
+ const ModelInspector = require('../orm/migration/ModelInspector');
19
+ const inspector = new ModelInspector(
20
+ ctx.modelsPath,
21
+ ctx.migrationsPath,
22
+ ctx.snapshotPath,
23
+ );
24
+ const result = await inspector.makeMigrations();
25
+
26
+ if (result.files.length === 0) {
27
+ console.log(chalk.yellow(`\n ${result.message}\n`));
28
+ } else {
29
+ console.log(chalk.green(`\n ✔ ${result.message}`));
30
+ result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
31
+ console.log(chalk.gray('\n Run: millas migrate to apply these migrations.\n'));
32
+ }
33
+ } catch (err) {
34
+ console.error(chalk.red(`\n ✖ makemigrations failed: ${err.message}\n`));
35
+ if (process.env.DEBUG) console.error(err.stack);
36
+ process.exit(1);
27
37
  }
28
38
  });
29
39
 
40
+ // ── migrate ───────────────────────────────────────────────────────────────
41
+
30
42
  program
31
43
  .command('migrate')
32
44
  .description('Run all pending migrations')
33
45
  .action(async () => {
34
- const runner = await getRunner();
35
- const result = await runner.migrate();
36
- printMigrationResult(result, 'Ran');
46
+ try {
47
+ const runner = await getRunner();
48
+ const result = await runner.migrate();
49
+ printResult(result, 'Ran');
50
+ } catch (err) {
51
+ bail('migrate', err);
52
+ }
37
53
  });
38
54
 
55
+ // ── migrate:fresh ─────────────────────────────────────────────────────────
56
+
39
57
  program
40
58
  .command('migrate:fresh')
41
- .description('Drop all tables and re-run all migrations')
59
+ .description('Drop ALL tables then re-run every migration from scratch')
42
60
  .action(async () => {
43
- console.log(chalk.yellow('\n ⚠ Dropping all tables...\n'));
44
- const runner = await getRunner();
45
- const result = await runner.fresh();
46
- printMigrationResult(result, 'Ran');
61
+ try {
62
+ console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
63
+ const runner = await getRunner();
64
+ const result = await runner.fresh();
65
+ printResult(result, 'Ran');
66
+ } catch (err) {
67
+ bail('migrate:fresh', err);
68
+ }
47
69
  });
48
70
 
71
+ // ── migrate:rollback ──────────────────────────────────────────────────────
72
+
49
73
  program
50
74
  .command('migrate:rollback')
51
75
  .description('Rollback the last batch of migrations')
52
76
  .option('--steps <n>', 'Number of batches to rollback', '1')
53
77
  .action(async (options) => {
54
- const runner = await getRunner();
55
- const result = await runner.rollback(Number(options.steps));
56
- printMigrationResult(result, 'Rolled back');
78
+ try {
79
+ const runner = await getRunner();
80
+ const result = await runner.rollback(Number(options.steps));
81
+ printResult(result, 'Rolled back');
82
+ } catch (err) {
83
+ bail('migrate:rollback', err);
84
+ }
57
85
  });
58
86
 
87
+ // ── migrate:reset ─────────────────────────────────────────────────────────
88
+
59
89
  program
60
90
  .command('migrate:reset')
61
- .description('Rollback all migrations')
91
+ .description('Rollback ALL migrations')
62
92
  .action(async () => {
63
- const runner = await getRunner();
64
- const result = await runner.reset();
65
- printMigrationResult(result, 'Rolled back');
93
+ try {
94
+ const runner = await getRunner();
95
+ const result = await runner.reset();
96
+ printResult(result, 'Rolled back');
97
+ } catch (err) {
98
+ bail('migrate:reset', err);
99
+ }
66
100
  });
67
101
 
102
+ // ── migrate:refresh ───────────────────────────────────────────────────────
103
+
68
104
  program
69
105
  .command('migrate:refresh')
70
- .description('Rollback all and re-run all migrations')
106
+ .description('Rollback all then re-run all migrations')
71
107
  .action(async () => {
72
- const runner = await getRunner();
73
- const result = await runner.refresh();
74
- printMigrationResult(result, 'Ran');
108
+ try {
109
+ const runner = await getRunner();
110
+ const result = await runner.refresh();
111
+ printResult(result, 'Ran');
112
+ } catch (err) {
113
+ bail('migrate:refresh', err);
114
+ }
75
115
  });
76
116
 
117
+ // ── migrate:status ────────────────────────────────────────────────────────
118
+
77
119
  program
78
120
  .command('migrate:status')
79
- .description('Show the status of all migrations')
121
+ .description('Show the status of all migration files')
80
122
  .action(async () => {
81
- const runner = await getRunner();
82
- const rows = await runner.status();
83
-
84
- if (rows.length === 0) {
85
- console.log(chalk.yellow('\n No migration files found.\n'));
86
- return;
87
- }
88
-
89
- const colW = Math.max(...rows.map(r => r.name.length)) + 2;
90
- console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
91
- console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
92
-
93
- for (const row of rows) {
94
- const status = row.status === 'Ran'
95
- ? chalk.green(row.status.padEnd(10))
96
- : chalk.yellow(row.status.padEnd(10));
97
- const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
98
- console.log(` ${chalk.cyan(row.name.padEnd(colW))} ${status} ${batch}`);
123
+ try {
124
+ const runner = await getRunner();
125
+ const rows = await runner.status();
126
+
127
+ if (rows.length === 0) {
128
+ console.log(chalk.yellow('\n No migration files found.\n'));
129
+ return;
130
+ }
131
+
132
+ const colW = Math.max(...rows.map(r => r.name.length)) + 2;
133
+ console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
134
+ console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
135
+
136
+ for (const row of rows) {
137
+ const status = row.status === 'Ran'
138
+ ? chalk.green(row.status.padEnd(10))
139
+ : chalk.yellow(row.status.padEnd(10));
140
+ const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
141
+ console.log(` ${chalk.cyan(row.name.padEnd(colW))} ${status} ${batch}`);
142
+ }
143
+ console.log();
144
+ } catch (err) {
145
+ bail('migrate:status', err);
99
146
  }
100
- console.log();
101
147
  });
102
148
 
149
+ // ── db:seed ───────────────────────────────────────────────────────────────
150
+
103
151
  program
104
152
  .command('db:seed')
105
153
  .description('Run all database seeders')
106
154
  .action(async () => {
107
- const ctx = getProjectContext();
108
- const seedersDir = ctx.seedersPath;
155
+ try {
156
+ const ctx = getProjectContext();
157
+ const seedersDir = ctx.seedersPath;
109
158
 
110
- if (!fs.existsSync(seedersDir)) {
111
- console.log(chalk.yellow('\n No seeders directory found.\n'));
112
- return;
113
- }
159
+ if (!fs.existsSync(seedersDir)) {
160
+ console.log(chalk.yellow('\n No seeders directory found.\n'));
161
+ return;
162
+ }
114
163
 
115
- const files = fs.readdirSync(seedersDir)
116
- .filter(f => f.endsWith('.js') && !f.startsWith('.'))
117
- .sort();
164
+ const files = fs.readdirSync(seedersDir)
165
+ .filter(f => f.endsWith('.js') && !f.startsWith('.'))
166
+ .sort();
118
167
 
119
- if (files.length === 0) {
120
- console.log(chalk.yellow('\n No seeder files found.\n'));
121
- return;
122
- }
168
+ if (files.length === 0) {
169
+ console.log(chalk.yellow('\n No seeder files found.\n'));
170
+ return;
171
+ }
123
172
 
124
- console.log();
125
- for (const file of files) {
126
- const seeder = require(path.join(seedersDir, file));
127
173
  const db = await getDbConnection();
128
- await seeder.run(db);
129
- console.log(chalk.green(` ✔ Seeded: ${file}`));
174
+ console.log();
175
+ for (const file of files) {
176
+ const seeder = require(path.join(seedersDir, file));
177
+ await seeder.run(db);
178
+ console.log(chalk.green(` ✔ Seeded: ${file}`));
179
+ }
180
+ console.log();
181
+ } catch (err) {
182
+ bail('db:seed', err);
130
183
  }
131
- console.log();
132
184
  });
133
185
  };
134
186
 
135
- // ─── Helpers ─────────────────────────────────────────────────────────────────
187
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
136
188
 
137
189
  function getProjectContext() {
138
190
  const cwd = process.cwd();
@@ -149,26 +201,35 @@ async function getDbConnection() {
149
201
  if (!fs.existsSync(configPath + '.js')) {
150
202
  throw new Error('config/database.js not found. Are you inside a Millas project?');
151
203
  }
152
- const config = require(configPath);
204
+ const config = require(configPath);
153
205
  const DatabaseManager = require('../orm/drivers/DatabaseManager');
154
206
  DatabaseManager.configure(config);
155
207
  return DatabaseManager.connection();
156
208
  }
157
209
 
158
210
  async function getRunner() {
211
+ // Fixed: was incorrectly destructured as { MigrationRunner }
159
212
  const MigrationRunner = require('../orm/migration/MigrationRunner');
160
213
  const ctx = getProjectContext();
161
214
  const db = await getDbConnection();
162
215
  return new MigrationRunner(db, ctx.migrationsPath);
163
216
  }
164
217
 
165
- function printMigrationResult(result, verb) {
218
+ function printResult(result, verb) {
166
219
  const list = result.ran || result.rolledBack || [];
167
220
  if (list.length === 0) {
168
221
  console.log(chalk.yellow(`\n ${result.message}\n`));
169
222
  return;
170
223
  }
171
224
  console.log(chalk.green(`\n ✔ ${result.message}`));
172
- list.forEach(f => console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`)));
225
+ list.forEach(f =>
226
+ console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`)),
227
+ );
173
228
  console.log();
174
229
  }
230
+
231
+ function bail(cmd, err) {
232
+ console.error(chalk.red(`\n ✖ ${cmd} failed: ${err.message}\n`));
233
+ if (process.env.DEBUG) console.error(err.stack);
234
+ process.exit(1);
235
+ }
package/src/index.js CHANGED
@@ -16,8 +16,13 @@ const ServiceProvider = require('./providers/ServiceProvider');
16
16
  const ProviderRegistry = require('./providers/ProviderRegistry');
17
17
 
18
18
  // ── ORM ───────────────────────────────────────────────────────────
19
- const { Model, fields, QueryBuilder, DatabaseManager,
20
- SchemaBuilder, MigrationRunner, ModelInspector } = require('./orm');
19
+ const {
20
+ Model, fields, QueryBuilder, DatabaseManager,
21
+ SchemaBuilder, MigrationRunner, ModelInspector,
22
+ Q, LookupParser,
23
+ Sum, Avg, Min, Max, Count, AggregateExpression,
24
+ HasOne, HasMany, BelongsTo, BelongsToMany,
25
+ } = require('./orm');
21
26
  const DatabaseServiceProvider = require('./providers/DatabaseServiceProvider');
22
27
 
23
28
  // ── Auth ──────────────────────────────────────────────────────────
@@ -68,6 +73,9 @@ module.exports = {
68
73
  // ORM
69
74
  Model, fields, QueryBuilder, DatabaseManager, SchemaBuilder,
70
75
  MigrationRunner, ModelInspector, DatabaseServiceProvider,
76
+ Q, LookupParser,
77
+ Sum, Avg, Min, Max, Count, AggregateExpression,
78
+ HasOne, HasMany, BelongsTo, BelongsToMany,
71
79
  // Auth
72
80
  Auth, Hasher, JwtDriver, AuthMiddleware, RoleMiddleware,
73
81
  AuthController, AuthServiceProvider,
package/src/orm/index.js CHANGED
@@ -3,12 +3,17 @@
3
3
  const Model = require('./model/Model');
4
4
  const { fields } = require('./fields');
5
5
  const QueryBuilder = require('./query/QueryBuilder');
6
+ const Q = require('./query/Q');
7
+ const { AggregateExpression, Sum, Avg, Min, Max, Count } = require('./query/Aggregates');
8
+ const LookupParser = require('./query/LookupParser');
6
9
  const DatabaseManager = require('./drivers/DatabaseManager');
7
10
  const SchemaBuilder = require('./migration/SchemaBuilder');
8
11
  const MigrationRunner = require('./migration/MigrationRunner');
9
12
  const ModelInspector = require('./migration/ModelInspector');
13
+ const { HasOne, HasMany, BelongsTo, BelongsToMany } = require('./relations');
10
14
 
11
15
  module.exports = {
16
+ // Core
12
17
  Model,
13
18
  fields,
14
19
  QueryBuilder,
@@ -16,4 +21,18 @@ module.exports = {
16
21
  SchemaBuilder,
17
22
  MigrationRunner,
18
23
  ModelInspector,
24
+
25
+ // Query helpers
26
+ Q,
27
+ LookupParser,
28
+
29
+ // Aggregate expressions
30
+ Sum, Avg, Min, Max, Count,
31
+ AggregateExpression,
32
+
33
+ // Relations
34
+ HasOne,
35
+ HasMany,
36
+ BelongsTo,
37
+ BelongsToMany,
19
38
  };
@@ -7,19 +7,19 @@ const path = require('path');
7
7
  * MigrationRunner
8
8
  *
9
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)
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
16
  *
17
17
  * Migration history is tracked in the `millas_migrations` table.
18
18
  * Each migration file must export { up(db), down(db) }.
19
19
  */
20
20
  class MigrationRunner {
21
21
  /**
22
- * @param {object} knexConn — live knex connection
22
+ * @param {object} knexConn — live knex connection
23
23
  * @param {string} migrationsPath — absolute path to migrations dir
24
24
  */
25
25
  constructor(knexConn, migrationsPath) {
@@ -29,9 +29,7 @@ class MigrationRunner {
29
29
 
30
30
  // ─── Public commands ──────────────────────────────────────────────────────
31
31
 
32
- /**
33
- * Run all pending migrations.
34
- */
32
+ /** Run all pending migrations. */
35
33
  async migrate() {
36
34
  await this._ensureTable();
37
35
  const pending = await this._pending();
@@ -53,9 +51,7 @@ class MigrationRunner {
53
51
  return { ran, batch, message: `Ran ${ran.length} migration(s).` };
54
52
  }
55
53
 
56
- /**
57
- * Rollback the last batch of migrations.
58
- */
54
+ /** Rollback the last batch of migrations. */
59
55
  async rollback(steps = 1) {
60
56
  await this._ensureTable();
61
57
  const batches = await this._lastBatches(steps);
@@ -66,7 +62,6 @@ class MigrationRunner {
66
62
 
67
63
  const rolledBack = [];
68
64
 
69
- // Run in reverse order
70
65
  for (const row of [...batches].reverse()) {
71
66
  const migration = this._load(row.name);
72
67
  await migration.down(this._db);
@@ -77,18 +72,14 @@ class MigrationRunner {
77
72
  return { rolledBack, message: `Rolled back ${rolledBack.length} migration(s).` };
78
73
  }
79
74
 
80
- /**
81
- * Drop all tables and re-run every migration.
82
- */
75
+ /** Drop all tables and re-run every migration. */
83
76
  async fresh() {
84
77
  await this._dropAllTables();
85
78
  await this._ensureTable();
86
79
  return this.migrate();
87
80
  }
88
81
 
89
- /**
90
- * Rollback ALL migrations.
91
- */
82
+ /** Rollback ALL migrations. */
92
83
  async reset() {
93
84
  await this._ensureTable();
94
85
  const all = await this._db('millas_migrations').orderBy('id', 'desc');
@@ -108,17 +99,13 @@ class MigrationRunner {
108
99
  return { rolledBack, message: `Reset ${rolledBack.length} migration(s).` };
109
100
  }
110
101
 
111
- /**
112
- * Rollback all then re-run all.
113
- */
102
+ /** Rollback all then re-run all. */
114
103
  async refresh() {
115
104
  await this.reset();
116
105
  return this.migrate();
117
106
  }
118
107
 
119
- /**
120
- * Return status of all migration files.
121
- */
108
+ /** Return status of all migration files. */
122
109
  async status() {
123
110
  await this._ensureTable();
124
111
  const files = this._files();
@@ -127,7 +114,7 @@ class MigrationRunner {
127
114
  return files.map(file => ({
128
115
  name: file,
129
116
  status: ran.has(file) ? 'Ran' : 'Pending',
130
- batch: ran.get ? ran.get(file) : null,
117
+ batch: ran.get(file) || null,
131
118
  }));
132
119
  }
133
120
 
@@ -153,9 +140,7 @@ class MigrationRunner {
153
140
 
154
141
  async _ranNames() {
155
142
  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;
143
+ return new Map(rows.map(r => [r.name, r.batch]));
159
144
  }
160
145
 
161
146
  async _nextBatch() {
@@ -168,34 +153,39 @@ class MigrationRunner {
168
153
  if (!maxBatch?.max) return [];
169
154
 
170
155
  const fromBatch = maxBatch.max - steps + 1;
171
- // Fetch all then filter in JS to avoid needing >= support in all drivers
172
156
  const all = await this._db('millas_migrations').orderBy('id', 'desc');
173
157
  return all.filter(r => r.batch >= fromBatch);
174
158
  }
175
159
 
160
+ /**
161
+ * Drop all user tables — dialect-aware.
162
+ * Resolves the knex client name and delegates to the right helper.
163
+ */
176
164
  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);
165
+ const clientName = this._db.client.config.client || 'sqlite3';
166
+
167
+ let dialect;
168
+ if (clientName.includes('pg') || clientName.includes('postgres')) {
169
+ dialect = require('./dialects/postgres');
170
+ } else if (clientName.includes('mysql') || clientName.includes('maria')) {
171
+ dialect = require('./dialects/mysql');
172
+ } else {
173
+ // Default: sqlite / sqlite3
174
+ dialect = require('./dialects/sqlite');
186
175
  }
176
+
177
+ await dialect.dropAllTables(this._db);
187
178
  }
188
179
 
189
180
  _files() {
190
181
  if (!fs.existsSync(this._path)) return [];
191
182
  return fs.readdirSync(this._path)
192
183
  .filter(f => f.endsWith('.js') && !f.startsWith('.'))
193
- .sort(); // Sorted by timestamp prefix
184
+ .sort();
194
185
  }
195
186
 
196
187
  _load(name) {
197
188
  const filePath = path.join(this._path, name);
198
- // Clear require cache so fresh loads work in tests
199
189
  delete require.cache[require.resolve(filePath)];
200
190
  const migration = require(filePath);
201
191
  if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {