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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/bin/millas.js +6 -0
- package/package.json +56 -0
- package/src/admin/Admin.js +617 -0
- package/src/admin/index.js +13 -0
- package/src/admin/resources/AdminResource.js +317 -0
- package/src/auth/Auth.js +254 -0
- package/src/auth/AuthController.js +188 -0
- package/src/auth/AuthMiddleware.js +67 -0
- package/src/auth/Hasher.js +51 -0
- package/src/auth/JwtDriver.js +74 -0
- package/src/auth/RoleMiddleware.js +44 -0
- package/src/cache/Cache.js +231 -0
- package/src/cache/drivers/FileDriver.js +152 -0
- package/src/cache/drivers/MemoryDriver.js +158 -0
- package/src/cache/drivers/NullDriver.js +27 -0
- package/src/cache/index.js +8 -0
- package/src/cli.js +27 -0
- package/src/commands/make.js +61 -0
- package/src/commands/migrate.js +174 -0
- package/src/commands/new.js +50 -0
- package/src/commands/queue.js +92 -0
- package/src/commands/route.js +93 -0
- package/src/commands/serve.js +50 -0
- package/src/container/Application.js +177 -0
- package/src/container/Container.js +281 -0
- package/src/container/index.js +13 -0
- package/src/controller/Controller.js +367 -0
- package/src/errors/HttpError.js +29 -0
- package/src/events/Event.js +39 -0
- package/src/events/EventEmitter.js +151 -0
- package/src/events/Listener.js +46 -0
- package/src/events/index.js +15 -0
- package/src/index.js +93 -0
- package/src/mail/Mail.js +210 -0
- package/src/mail/MailMessage.js +196 -0
- package/src/mail/TemplateEngine.js +150 -0
- package/src/mail/drivers/LogDriver.js +36 -0
- package/src/mail/drivers/MailgunDriver.js +84 -0
- package/src/mail/drivers/SendGridDriver.js +97 -0
- package/src/mail/drivers/SmtpDriver.js +67 -0
- package/src/mail/index.js +19 -0
- package/src/middleware/AuthMiddleware.js +46 -0
- package/src/middleware/CorsMiddleware.js +59 -0
- package/src/middleware/LogMiddleware.js +61 -0
- package/src/middleware/Middleware.js +36 -0
- package/src/middleware/MiddlewarePipeline.js +94 -0
- package/src/middleware/ThrottleMiddleware.js +61 -0
- package/src/orm/drivers/DatabaseManager.js +135 -0
- package/src/orm/fields/index.js +132 -0
- package/src/orm/index.js +19 -0
- package/src/orm/migration/MigrationRunner.js +216 -0
- package/src/orm/migration/ModelInspector.js +338 -0
- package/src/orm/migration/SchemaBuilder.js +173 -0
- package/src/orm/model/Model.js +371 -0
- package/src/orm/query/QueryBuilder.js +197 -0
- package/src/providers/AdminServiceProvider.js +40 -0
- package/src/providers/AuthServiceProvider.js +53 -0
- package/src/providers/CacheStorageServiceProvider.js +71 -0
- package/src/providers/DatabaseServiceProvider.js +45 -0
- package/src/providers/EventServiceProvider.js +34 -0
- package/src/providers/MailServiceProvider.js +51 -0
- package/src/providers/ProviderRegistry.js +82 -0
- package/src/providers/QueueServiceProvider.js +52 -0
- package/src/providers/ServiceProvider.js +45 -0
- package/src/queue/Job.js +135 -0
- package/src/queue/Queue.js +147 -0
- package/src/queue/drivers/DatabaseDriver.js +194 -0
- package/src/queue/drivers/SyncDriver.js +72 -0
- package/src/queue/index.js +16 -0
- package/src/queue/workers/QueueWorker.js +140 -0
- package/src/router/MiddlewareRegistry.js +82 -0
- package/src/router/Route.js +255 -0
- package/src/router/RouteGroup.js +19 -0
- package/src/router/RouteRegistry.js +55 -0
- package/src/router/Router.js +138 -0
- package/src/router/index.js +15 -0
- package/src/scaffold/generator.js +34 -0
- package/src/scaffold/maker.js +272 -0
- package/src/scaffold/templates.js +350 -0
- package/src/storage/Storage.js +170 -0
- package/src/storage/drivers/LocalDriver.js +215 -0
- package/src/storage/index.js +6 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* FileDriver
|
|
8
|
+
*
|
|
9
|
+
* Filesystem-backed cache. Persists across restarts.
|
|
10
|
+
* Stores each key as a JSON file inside the cache directory.
|
|
11
|
+
*
|
|
12
|
+
* CACHE_DRIVER=file
|
|
13
|
+
*/
|
|
14
|
+
class FileDriver {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
this._dir = config.path || path.join(process.cwd(), 'storage/cache');
|
|
17
|
+
fs.ensureDirSync(this._dir);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async set(key, value, ttl = 0) {
|
|
21
|
+
const payload = {
|
|
22
|
+
value,
|
|
23
|
+
expiresAt: ttl > 0 ? Date.now() + ttl * 1000 : null,
|
|
24
|
+
storedAt: Date.now(),
|
|
25
|
+
};
|
|
26
|
+
await fs.writeJson(this._filePath(key), payload);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async get(key) {
|
|
31
|
+
const file = this._filePath(key);
|
|
32
|
+
if (!(await fs.pathExists(file))) return null;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const payload = await fs.readJson(file);
|
|
36
|
+
if (payload.expiresAt && Date.now() > payload.expiresAt) {
|
|
37
|
+
await fs.remove(file);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return payload.value;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async has(key) {
|
|
47
|
+
return (await this.get(key)) !== null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async delete(key) {
|
|
51
|
+
const file = this._filePath(key);
|
|
52
|
+
if (await fs.pathExists(file)) {
|
|
53
|
+
await fs.remove(file);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async deletePattern(prefix) {
|
|
60
|
+
const files = await fs.readdir(this._dir);
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
if (file.startsWith(this._hash(prefix))) {
|
|
64
|
+
await fs.remove(path.join(this._dir, file));
|
|
65
|
+
count++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async flush() {
|
|
72
|
+
const files = await fs.readdir(this._dir);
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
if (file.endsWith('.json')) await fs.remove(path.join(this._dir, file));
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async remember(key, ttl, fn) {
|
|
80
|
+
const cached = await this.get(key);
|
|
81
|
+
if (cached !== null) return cached;
|
|
82
|
+
const value = await fn();
|
|
83
|
+
await this.set(key, value, ttl);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async increment(key, amount = 1) {
|
|
88
|
+
const current = (await this.get(key)) ?? 0;
|
|
89
|
+
const next = Number(current) + amount;
|
|
90
|
+
await this.set(key, next);
|
|
91
|
+
return next;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async decrement(key, amount = 1) {
|
|
95
|
+
return this.increment(key, -amount);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async add(key, value, ttl = 0) {
|
|
99
|
+
if (await this.has(key)) return false;
|
|
100
|
+
await this.set(key, value, ttl);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async getMany(keys) {
|
|
105
|
+
const result = {};
|
|
106
|
+
for (const key of keys) result[key] = await this.get(key);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async setMany(entries, ttl = 0) {
|
|
111
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
112
|
+
await this.set(key, value, ttl);
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async gc() {
|
|
118
|
+
const files = await fs.readdir(this._dir);
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
let count = 0;
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
if (!file.endsWith('.json')) continue;
|
|
123
|
+
try {
|
|
124
|
+
const p = await fs.readJson(path.join(this._dir, file));
|
|
125
|
+
if (p.expiresAt && now > p.expiresAt) {
|
|
126
|
+
await fs.remove(path.join(this._dir, file));
|
|
127
|
+
count++;
|
|
128
|
+
}
|
|
129
|
+
} catch { /* skip */ }
|
|
130
|
+
}
|
|
131
|
+
return count;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
_filePath(key) {
|
|
137
|
+
return path.join(this._dir, `${this._hash(key)}.json`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_hash(key) {
|
|
141
|
+
// Simple deterministic hash for filenames
|
|
142
|
+
let h = 5381;
|
|
143
|
+
for (let i = 0; i < key.length; i++) {
|
|
144
|
+
h = ((h << 5) + h) ^ key.charCodeAt(i);
|
|
145
|
+
h = h >>> 0;
|
|
146
|
+
}
|
|
147
|
+
return h.toString(16).padStart(8, '0') + '_' +
|
|
148
|
+
key.replace(/[^a-z0-9_-]/gi, '_').slice(0, 40);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = FileDriver;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MemoryDriver
|
|
5
|
+
*
|
|
6
|
+
* In-memory cache with TTL support.
|
|
7
|
+
* Data does not persist across restarts.
|
|
8
|
+
* Default driver — zero config required.
|
|
9
|
+
*
|
|
10
|
+
* CACHE_DRIVER=memory
|
|
11
|
+
*/
|
|
12
|
+
class MemoryDriver {
|
|
13
|
+
constructor() {
|
|
14
|
+
this._store = new Map(); // key → { value, expiresAt }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Store a value. ttl = seconds (0 = no expiry).
|
|
19
|
+
*/
|
|
20
|
+
async set(key, value, ttl = 0) {
|
|
21
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : null;
|
|
22
|
+
this._store.set(String(key), {
|
|
23
|
+
value: JSON.stringify(value),
|
|
24
|
+
expiresAt,
|
|
25
|
+
});
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Retrieve a value. Returns null if missing or expired.
|
|
31
|
+
*/
|
|
32
|
+
async get(key) {
|
|
33
|
+
const entry = this._store.get(String(key));
|
|
34
|
+
if (!entry) return null;
|
|
35
|
+
|
|
36
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
37
|
+
this._store.delete(String(key));
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return JSON.parse(entry.value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a key exists and has not expired.
|
|
46
|
+
*/
|
|
47
|
+
async has(key) {
|
|
48
|
+
return (await this.get(key)) !== null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Delete a key.
|
|
53
|
+
*/
|
|
54
|
+
async delete(key) {
|
|
55
|
+
return this._store.delete(String(key));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Delete multiple keys matching a prefix.
|
|
60
|
+
*/
|
|
61
|
+
async deletePattern(prefix) {
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const key of this._store.keys()) {
|
|
64
|
+
if (key.startsWith(prefix)) {
|
|
65
|
+
this._store.delete(key);
|
|
66
|
+
count++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return count;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Flush the entire cache.
|
|
74
|
+
*/
|
|
75
|
+
async flush() {
|
|
76
|
+
this._store.clear();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get or set — return cached value, or run fn() and cache its result.
|
|
82
|
+
*/
|
|
83
|
+
async remember(key, ttl, fn) {
|
|
84
|
+
const cached = await this.get(key);
|
|
85
|
+
if (cached !== null) return cached;
|
|
86
|
+
const value = await fn();
|
|
87
|
+
await this.set(key, value, ttl);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Increment a numeric value.
|
|
93
|
+
*/
|
|
94
|
+
async increment(key, amount = 1) {
|
|
95
|
+
const current = (await this.get(key)) ?? 0;
|
|
96
|
+
const next = Number(current) + amount;
|
|
97
|
+
await this.set(key, next);
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Decrement a numeric value.
|
|
103
|
+
*/
|
|
104
|
+
async decrement(key, amount = 1) {
|
|
105
|
+
return this.increment(key, -amount);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Add only if the key does not exist.
|
|
110
|
+
*/
|
|
111
|
+
async add(key, value, ttl = 0) {
|
|
112
|
+
if (await this.has(key)) return false;
|
|
113
|
+
await this.set(key, value, ttl);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get multiple keys at once.
|
|
119
|
+
*/
|
|
120
|
+
async getMany(keys) {
|
|
121
|
+
const result = {};
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
result[key] = await this.get(key);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set multiple key/value pairs at once.
|
|
130
|
+
*/
|
|
131
|
+
async setMany(entries, ttl = 0) {
|
|
132
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
133
|
+
await this.set(key, value, ttl);
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Return the number of entries (including expired).
|
|
140
|
+
*/
|
|
141
|
+
size() {
|
|
142
|
+
return this._store.size;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Purge all expired entries.
|
|
147
|
+
*/
|
|
148
|
+
gc() {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
for (const [key, entry] of this._store) {
|
|
151
|
+
if (entry.expiresAt && now > entry.expiresAt) {
|
|
152
|
+
this._store.delete(key);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = MemoryDriver;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NullDriver
|
|
5
|
+
*
|
|
6
|
+
* A no-op cache driver — always misses, never stores.
|
|
7
|
+
* Use in tests to disable caching without code changes.
|
|
8
|
+
*
|
|
9
|
+
* CACHE_DRIVER=null
|
|
10
|
+
*/
|
|
11
|
+
class NullDriver {
|
|
12
|
+
async set() { return true; }
|
|
13
|
+
async get() { return null; }
|
|
14
|
+
async has() { return false; }
|
|
15
|
+
async delete() { return true; }
|
|
16
|
+
async deletePattern() { return 0; }
|
|
17
|
+
async flush() { return true; }
|
|
18
|
+
async remember(k, t, fn) { return fn(); }
|
|
19
|
+
async increment(k, n = 1) { return n; }
|
|
20
|
+
async decrement(k, n = 1) { return -n; }
|
|
21
|
+
async add() { return true; }
|
|
22
|
+
async getMany(keys) { return Object.fromEntries(keys.map(k => [k, null])); }
|
|
23
|
+
async setMany() { return true; }
|
|
24
|
+
async gc() { return 0; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = NullDriver;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Cache = require('./Cache');
|
|
4
|
+
const MemoryDriver = require('./drivers/MemoryDriver');
|
|
5
|
+
const FileDriver = require('./drivers/FileDriver');
|
|
6
|
+
const NullDriver = require('./drivers/NullDriver');
|
|
7
|
+
|
|
8
|
+
module.exports = { Cache, MemoryDriver, FileDriver, NullDriver };
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('millas')
|
|
9
|
+
.description(chalk.cyan('⚡ Millas — A modern batteries-included Node.js framework'))
|
|
10
|
+
.version('0.1.0');
|
|
11
|
+
|
|
12
|
+
// Load all command modules
|
|
13
|
+
require('./commands/new')(program);
|
|
14
|
+
require('./commands/serve')(program);
|
|
15
|
+
require('./commands/make')(program);
|
|
16
|
+
require('./commands/migrate')(program);
|
|
17
|
+
require('./commands/route')(program);
|
|
18
|
+
require('./commands/queue')(program);
|
|
19
|
+
|
|
20
|
+
// Unknown command handler
|
|
21
|
+
program.on('command:*', ([cmd]) => {
|
|
22
|
+
console.error(chalk.red(`\n Unknown command: ${chalk.bold(cmd)}\n`));
|
|
23
|
+
console.log(` Run ${chalk.cyan('millas --help')} to see available commands.\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = { program };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { makeController, makeModel, makeMiddleware, makeService, makeJob, makeMigration } = require('../scaffold/maker');
|
|
5
|
+
|
|
6
|
+
module.exports = function (program) {
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.command('make:controller <name>')
|
|
10
|
+
.description('Generate a new controller')
|
|
11
|
+
.option('--resource', 'Generate a resource controller with CRUD methods')
|
|
12
|
+
.action(async (name, options) => {
|
|
13
|
+
await run('Controller', () => makeController(name, options));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('make:model <name>')
|
|
18
|
+
.description('Generate a new model')
|
|
19
|
+
.option('-m, --migration', 'Also create a migration file')
|
|
20
|
+
.action(async (name, options) => {
|
|
21
|
+
await run('Model', () => makeModel(name, options));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('make:middleware <name>')
|
|
26
|
+
.description('Generate a new middleware')
|
|
27
|
+
.action(async (name) => {
|
|
28
|
+
await run('Middleware', () => makeMiddleware(name));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('make:service <name>')
|
|
33
|
+
.description('Generate a new service class')
|
|
34
|
+
.action(async (name) => {
|
|
35
|
+
await run('Service', () => makeService(name));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command('make:job <name>')
|
|
40
|
+
.description('Generate a new background job')
|
|
41
|
+
.action(async (name) => {
|
|
42
|
+
await run('Job', () => makeJob(name));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('make:migration <name>')
|
|
47
|
+
.description('Generate a blank migration file')
|
|
48
|
+
.action(async (name) => {
|
|
49
|
+
await run('Migration', () => makeMigration(name));
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
async function run(type, fn) {
|
|
54
|
+
try {
|
|
55
|
+
const filePath = await fn();
|
|
56
|
+
console.log(chalk.green(`\n ✔ ${type} created: `) + chalk.cyan(filePath) + '\n');
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(chalk.red(`\n ✖ Failed to create ${type}: ${err.message}\n`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
module.exports = function (program) {
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.command('makemigrations')
|
|
11
|
+
.description('Detect model changes and generate migration files')
|
|
12
|
+
.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();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('migrate')
|
|
32
|
+
.description('Run all pending migrations')
|
|
33
|
+
.action(async () => {
|
|
34
|
+
const runner = await getRunner();
|
|
35
|
+
const result = await runner.migrate();
|
|
36
|
+
printMigrationResult(result, 'Ran');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command('migrate:fresh')
|
|
41
|
+
.description('Drop all tables and re-run all migrations')
|
|
42
|
+
.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');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command('migrate:rollback')
|
|
51
|
+
.description('Rollback the last batch of migrations')
|
|
52
|
+
.option('--steps <n>', 'Number of batches to rollback', '1')
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
const runner = await getRunner();
|
|
55
|
+
const result = await runner.rollback(Number(options.steps));
|
|
56
|
+
printMigrationResult(result, 'Rolled back');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('migrate:reset')
|
|
61
|
+
.description('Rollback all migrations')
|
|
62
|
+
.action(async () => {
|
|
63
|
+
const runner = await getRunner();
|
|
64
|
+
const result = await runner.reset();
|
|
65
|
+
printMigrationResult(result, 'Rolled back');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('migrate:refresh')
|
|
70
|
+
.description('Rollback all and re-run all migrations')
|
|
71
|
+
.action(async () => {
|
|
72
|
+
const runner = await getRunner();
|
|
73
|
+
const result = await runner.refresh();
|
|
74
|
+
printMigrationResult(result, 'Ran');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command('migrate:status')
|
|
79
|
+
.description('Show the status of all migrations')
|
|
80
|
+
.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}`);
|
|
99
|
+
}
|
|
100
|
+
console.log();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
program
|
|
104
|
+
.command('db:seed')
|
|
105
|
+
.description('Run all database seeders')
|
|
106
|
+
.action(async () => {
|
|
107
|
+
const ctx = getProjectContext();
|
|
108
|
+
const seedersDir = ctx.seedersPath;
|
|
109
|
+
|
|
110
|
+
if (!fs.existsSync(seedersDir)) {
|
|
111
|
+
console.log(chalk.yellow('\n No seeders directory found.\n'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const files = fs.readdirSync(seedersDir)
|
|
116
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('.'))
|
|
117
|
+
.sort();
|
|
118
|
+
|
|
119
|
+
if (files.length === 0) {
|
|
120
|
+
console.log(chalk.yellow('\n No seeder files found.\n'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log();
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const seeder = require(path.join(seedersDir, file));
|
|
127
|
+
const db = await getDbConnection();
|
|
128
|
+
await seeder.run(db);
|
|
129
|
+
console.log(chalk.green(` ✔ Seeded: ${file}`));
|
|
130
|
+
}
|
|
131
|
+
console.log();
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function getProjectContext() {
|
|
138
|
+
const cwd = process.cwd();
|
|
139
|
+
return {
|
|
140
|
+
migrationsPath: path.join(cwd, 'database/migrations'),
|
|
141
|
+
seedersPath: path.join(cwd, 'database/seeders'),
|
|
142
|
+
modelsPath: path.join(cwd, 'app/models'),
|
|
143
|
+
snapshotPath: path.join(cwd, '.millas/schema.json'),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getDbConnection() {
|
|
148
|
+
const configPath = path.join(process.cwd(), 'config/database');
|
|
149
|
+
if (!fs.existsSync(configPath + '.js')) {
|
|
150
|
+
throw new Error('config/database.js not found. Are you inside a Millas project?');
|
|
151
|
+
}
|
|
152
|
+
const config = require(configPath);
|
|
153
|
+
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
154
|
+
DatabaseManager.configure(config);
|
|
155
|
+
return DatabaseManager.connection();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function getRunner() {
|
|
159
|
+
const { MigrationRunner } = require('../orm/migration/MigrationRunner');
|
|
160
|
+
const ctx = getProjectContext();
|
|
161
|
+
const db = await getDbConnection();
|
|
162
|
+
return new MigrationRunner(db, ctx.migrationsPath);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function printMigrationResult(result, verb) {
|
|
166
|
+
const list = result.ran || result.rolledBack || [];
|
|
167
|
+
if (list.length === 0) {
|
|
168
|
+
console.log(chalk.yellow(`\n ${result.message}\n`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.log(chalk.green(`\n ✔ ${result.message}`));
|
|
172
|
+
list.forEach(f => console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`)));
|
|
173
|
+
console.log();
|
|
174
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { generateProject } = require('../scaffold/generator');
|
|
8
|
+
|
|
9
|
+
module.exports = function (program) {
|
|
10
|
+
program
|
|
11
|
+
.command('new <project-name>')
|
|
12
|
+
.description('Create a new Millas project')
|
|
13
|
+
.option('--no-install', 'Skip npm install')
|
|
14
|
+
.action(async (projectName, options) => {
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(chalk.cyan(' ⚡ Millas Framework'));
|
|
17
|
+
console.log(chalk.gray(' Creating a new project...\n'));
|
|
18
|
+
|
|
19
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(targetDir)) {
|
|
22
|
+
console.error(chalk.red(` ✖ Directory "${projectName}" already exists.\n`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const spinner = ora(` Scaffolding project ${chalk.bold(projectName)}`).start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await generateProject(projectName, targetDir);
|
|
30
|
+
spinner.succeed(chalk.green(` Project "${projectName}" created successfully!`));
|
|
31
|
+
|
|
32
|
+
if (options.install !== false) {
|
|
33
|
+
const installSpinner = ora(' Installing dependencies...').start();
|
|
34
|
+
const { execSync } = require('child_process');
|
|
35
|
+
execSync('npm install', { cwd: targetDir, stdio: 'ignore' });
|
|
36
|
+
installSpinner.succeed(chalk.green(' Dependencies installed!'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.bold(' Next steps:'));
|
|
41
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
42
|
+
console.log(chalk.cyan(' millas serve'));
|
|
43
|
+
console.log();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
spinner.fail(chalk.red(' Failed to create project.'));
|
|
46
|
+
console.error(chalk.red(`\n Error: ${err.message}\n`));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|