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,92 @@
|
|
|
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('queue:work')
|
|
11
|
+
.description('Start the queue worker process')
|
|
12
|
+
.option('-q, --queue <queues>', 'Comma-separated queue names to process', 'default')
|
|
13
|
+
.option('-s, --sleep <seconds>', 'Seconds to sleep between polls', '3')
|
|
14
|
+
.option('--once', 'Process only one job then exit')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
17
|
+
if (!fs.existsSync(bootstrapPath)) {
|
|
18
|
+
console.error(chalk.red('\n ✖ Not inside a Millas project.\n'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const Queue = require('../queue/Queue');
|
|
23
|
+
const QueueWorker = require('../queue/workers/QueueWorker');
|
|
24
|
+
|
|
25
|
+
// Boot the app to get config + job registry
|
|
26
|
+
process.env.MILLAS_ROUTE_LIST = 'true'; // suppress server listen
|
|
27
|
+
let app;
|
|
28
|
+
try {
|
|
29
|
+
const bootstrap = require(bootstrapPath);
|
|
30
|
+
app = bootstrap.app;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(chalk.red(`\n ✖ Failed to load app: ${e.message}\n`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const queues = options.queue.split(',').map(q => q.trim());
|
|
37
|
+
const sleep = Number(options.sleep) || 3;
|
|
38
|
+
const maxJobs = options.once ? 1 : Infinity;
|
|
39
|
+
|
|
40
|
+
const worker = new QueueWorker(
|
|
41
|
+
Queue.getDriver(),
|
|
42
|
+
Queue.getRegistry(),
|
|
43
|
+
{ queues, sleep, maxJobs }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await worker.start();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command('queue:status')
|
|
51
|
+
.description('Show queue statistics')
|
|
52
|
+
.action(async () => {
|
|
53
|
+
const Queue = require('../queue/Queue');
|
|
54
|
+
|
|
55
|
+
let queueConfig;
|
|
56
|
+
try { queueConfig = require(path.resolve(process.cwd(), 'config/queue')); }
|
|
57
|
+
catch { queueConfig = { default: process.env.QUEUE_DRIVER || 'sync' }; }
|
|
58
|
+
|
|
59
|
+
Queue.configure(queueConfig);
|
|
60
|
+
|
|
61
|
+
console.log();
|
|
62
|
+
if (queueConfig.default === 'sync') {
|
|
63
|
+
console.log(chalk.yellow(' Queue driver: sync (jobs run immediately, no persistence)'));
|
|
64
|
+
} else {
|
|
65
|
+
const stats = await Queue.stats();
|
|
66
|
+
if (!stats || !stats.length) {
|
|
67
|
+
console.log(chalk.gray(' No jobs in queue.'));
|
|
68
|
+
} else {
|
|
69
|
+
console.log(chalk.bold(' Queue Statistics\n'));
|
|
70
|
+
for (const row of stats) {
|
|
71
|
+
const statusColor = row.status === 'completed' ? chalk.green
|
|
72
|
+
: row.status === 'failed' ? chalk.red
|
|
73
|
+
: chalk.yellow;
|
|
74
|
+
console.log(` ${chalk.cyan(row.queue.padEnd(20))} ${statusColor(row.status.padEnd(12))} ${row.count}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
program
|
|
83
|
+
.command('queue:clear')
|
|
84
|
+
.description('Clear all pending jobs from a queue')
|
|
85
|
+
.option('-q, --queue <name>', 'Queue name to clear', 'default')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
const Queue = require('../queue/Queue');
|
|
88
|
+
const n = await Queue.clear(options.queue);
|
|
89
|
+
console.log(chalk.green(`\n ✔ Cleared ${n} job(s) from "${options.queue}" queue.\n`));
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
program
|
|
9
|
+
.command('route:list')
|
|
10
|
+
.description('List all registered routes')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(bootstrapPath)) {
|
|
15
|
+
console.error(chalk.red('\n ✖ Not inside a Millas project.\n'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.env.MILLAS_ROUTE_LIST = 'true';
|
|
20
|
+
|
|
21
|
+
let route;
|
|
22
|
+
try {
|
|
23
|
+
const bootstrap = require(bootstrapPath);
|
|
24
|
+
route = bootstrap.route;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error(chalk.red(`\n ✖ Failed to load routes: ${err.message}\n`));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!route) {
|
|
31
|
+
console.log(chalk.yellow('\n ⚠ Bootstrap did not export { route }.\n'));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rows = route.list();
|
|
36
|
+
|
|
37
|
+
if (rows.length === 0) {
|
|
38
|
+
console.log(chalk.yellow('\n No routes registered.\n'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log();
|
|
43
|
+
console.log(chalk.bold(' Registered Routes\n'));
|
|
44
|
+
|
|
45
|
+
const col = {
|
|
46
|
+
verb: 8,
|
|
47
|
+
path: Math.max(6, ...rows.map(r => r.path.length)) + 2,
|
|
48
|
+
handler: Math.max(8, ...rows.map(r => formatHandler(r).length)) + 2,
|
|
49
|
+
mw: Math.max(10, ...rows.map(r => (r.middleware || []).join(', ').length || 1)) + 2,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const header =
|
|
53
|
+
' ' +
|
|
54
|
+
chalk.bold(pad('METHOD', col.verb)) +
|
|
55
|
+
chalk.bold(pad('PATH', col.path)) +
|
|
56
|
+
chalk.bold(pad('HANDLER', col.handler)) +
|
|
57
|
+
chalk.bold(pad('MIDDLEWARE', col.mw)) +
|
|
58
|
+
chalk.bold('NAME');
|
|
59
|
+
|
|
60
|
+
console.log(header);
|
|
61
|
+
console.log(chalk.gray(' ' + '─'.repeat(col.verb + col.path + col.handler + col.mw + 10)));
|
|
62
|
+
|
|
63
|
+
for (const r of rows) {
|
|
64
|
+
const mw = (r.middleware || []).join(', ') || chalk.gray('—');
|
|
65
|
+
const name = r.name || chalk.gray('—');
|
|
66
|
+
console.log(
|
|
67
|
+
' ' +
|
|
68
|
+
verbChalk(r.verb)(pad(r.verb, col.verb)) +
|
|
69
|
+
chalk.cyan(pad(r.path, col.path)) +
|
|
70
|
+
chalk.white(pad(formatHandler(r), col.handler)) +
|
|
71
|
+
chalk.yellow(pad(mw, col.mw)) +
|
|
72
|
+
chalk.gray(name)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(chalk.gray(`\n ${rows.length} route(s) total.\n`));
|
|
77
|
+
process.exit(0);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function pad(str, len) { return String(str).padEnd(len); }
|
|
82
|
+
|
|
83
|
+
function formatHandler(r) {
|
|
84
|
+
if (!r.handler) return '<none>';
|
|
85
|
+
if (typeof r.handler === 'function' && !r.method) return r.handler.name || '<closure>';
|
|
86
|
+
const name = r.handler.name || 'Controller';
|
|
87
|
+
return r.method ? `${name}@${r.method}` : name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function verbChalk(verb) {
|
|
91
|
+
return { GET: chalk.green, POST: chalk.blue, PUT: chalk.yellow,
|
|
92
|
+
PATCH: chalk.magenta, DELETE: chalk.red }[verb] || chalk.white;
|
|
93
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
program
|
|
9
|
+
.command('serve')
|
|
10
|
+
.description('Start the development server')
|
|
11
|
+
.option('-p, --port <port>', 'Port to listen on', '3000')
|
|
12
|
+
.option('-h, --host <host>', 'Host to bind to', 'localhost')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(appBootstrap)) {
|
|
17
|
+
console.error(chalk.red('\n ✖ No Millas project found in current directory.\n'));
|
|
18
|
+
console.log(` Make sure you are inside a Millas project and bootstrap/app.js exists.\n`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(chalk.cyan(' ⚡ Millas Dev Server'));
|
|
24
|
+
console.log(chalk.gray(` Starting on http://${options.host}:${options.port}\n`));
|
|
25
|
+
|
|
26
|
+
process.env.MILLAS_PORT = options.port;
|
|
27
|
+
process.env.MILLAS_HOST = options.host;
|
|
28
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
require(appBootstrap);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(chalk.red(`\n ✖ Failed to start server: ${err.message}\n`));
|
|
34
|
+
console.error(err.stack);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Exported so queue:work can import the pattern
|
|
41
|
+
module.exports.requireProject = function(command) {
|
|
42
|
+
const path = require('path');
|
|
43
|
+
const fs = require('fs-extra');
|
|
44
|
+
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
45
|
+
if (!fs.existsSync(bootstrapPath)) {
|
|
46
|
+
const chalk = require('chalk');
|
|
47
|
+
console.error(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n`));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Container = require('./Container');
|
|
4
|
+
const ProviderRegistry = require('../providers/ProviderRegistry');
|
|
5
|
+
const { Route, Router, MiddlewareRegistry } = require('../router');
|
|
6
|
+
const CorsMiddleware = require('../middleware/CorsMiddleware');
|
|
7
|
+
const ThrottleMiddleware = require('../middleware/ThrottleMiddleware');
|
|
8
|
+
const LogMiddleware = require('../middleware/LogMiddleware');
|
|
9
|
+
const AuthMiddleware = require('../auth/AuthMiddleware');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Application
|
|
13
|
+
*
|
|
14
|
+
* The central Millas kernel. Owns:
|
|
15
|
+
* - The DI container
|
|
16
|
+
* - The provider registry
|
|
17
|
+
* - The Express app
|
|
18
|
+
* - The route instance
|
|
19
|
+
* - The middleware registry
|
|
20
|
+
*
|
|
21
|
+
* Usage in bootstrap/app.js:
|
|
22
|
+
*
|
|
23
|
+
* const { Application } = require('millas/src/container');
|
|
24
|
+
* const app = new Application(expressApp);
|
|
25
|
+
* app.providers([AppServiceProvider, DatabaseServiceProvider]);
|
|
26
|
+
* await app.boot();
|
|
27
|
+
* app.routes(route => {
|
|
28
|
+
* require('../routes/web')(route);
|
|
29
|
+
* require('../routes/api')(route);
|
|
30
|
+
* });
|
|
31
|
+
* app.mount();
|
|
32
|
+
* app.listen();
|
|
33
|
+
*/
|
|
34
|
+
class Application {
|
|
35
|
+
constructor(expressApp) {
|
|
36
|
+
this._express = expressApp;
|
|
37
|
+
this._container = new Container();
|
|
38
|
+
this._providers = new ProviderRegistry(this._container, expressApp);
|
|
39
|
+
this._mwRegistry = new MiddlewareRegistry();
|
|
40
|
+
this._route = new Route();
|
|
41
|
+
this._booted = false;
|
|
42
|
+
|
|
43
|
+
// Register core framework bindings
|
|
44
|
+
this._registerCoreBindings();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add service providers.
|
|
51
|
+
* @param {Array} providers
|
|
52
|
+
*/
|
|
53
|
+
providers(providers = []) {
|
|
54
|
+
this._providers.addMany(providers);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Register a middleware alias.
|
|
60
|
+
*
|
|
61
|
+
* app.middleware('auth', AuthMiddleware)
|
|
62
|
+
* app.middleware('throttle', new ThrottleMiddleware({ max: 60 }))
|
|
63
|
+
*/
|
|
64
|
+
middleware(alias, handler) {
|
|
65
|
+
this._mwRegistry.register(alias, handler);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Define all routes via a callback.
|
|
71
|
+
*
|
|
72
|
+
* app.routes(route => {
|
|
73
|
+
* require('../routes/web')(route);
|
|
74
|
+
* require('../routes/api')(route);
|
|
75
|
+
* });
|
|
76
|
+
*/
|
|
77
|
+
routes(callback) {
|
|
78
|
+
callback(this._route);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run the full provider register → boot lifecycle.
|
|
86
|
+
*/
|
|
87
|
+
async boot() {
|
|
88
|
+
if (this._booted) return this;
|
|
89
|
+
await this._providers.boot();
|
|
90
|
+
this._booted = true;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Bind all routes onto the Express app + mount error handlers.
|
|
96
|
+
*/
|
|
97
|
+
mount() {
|
|
98
|
+
const router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
|
|
99
|
+
router.mount();
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Start the HTTP server.
|
|
105
|
+
*/
|
|
106
|
+
listen(port, host, callback) {
|
|
107
|
+
const _port = port || parseInt(process.env.APP_PORT, 10) || 3000;
|
|
108
|
+
const _host = host || process.env.MILLAS_HOST || 'localhost';
|
|
109
|
+
|
|
110
|
+
this._express.listen(_port, _host, () => {
|
|
111
|
+
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
112
|
+
const routeCount = this._route.list().length;
|
|
113
|
+
console.log(`\n ⚡ Millas running at http://${_host}:${_port}`);
|
|
114
|
+
console.log(` ${routeCount} route(s) registered\n`);
|
|
115
|
+
}
|
|
116
|
+
if (typeof callback === 'function') callback(_port, _host);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Container Proxy ─────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Bind a transient into the container.
|
|
126
|
+
*/
|
|
127
|
+
bind(abstract, concrete) {
|
|
128
|
+
this._container.bind(abstract, concrete);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Bind a singleton into the container.
|
|
134
|
+
*/
|
|
135
|
+
singleton(abstract, concrete) {
|
|
136
|
+
this._container.singleton(abstract, concrete);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Register a pre-built instance.
|
|
142
|
+
*/
|
|
143
|
+
instance(abstract, value) {
|
|
144
|
+
this._container.instance(abstract, value);
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve something from the container.
|
|
150
|
+
*/
|
|
151
|
+
make(abstract, overrides) {
|
|
152
|
+
return this._container.make(abstract, overrides);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Accessors ───────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
get container() { return this._container; }
|
|
158
|
+
get route() { return this._route; }
|
|
159
|
+
get express() { return this._express; }
|
|
160
|
+
get mwRegistry() { return this._mwRegistry; }
|
|
161
|
+
|
|
162
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
_registerCoreBindings() {
|
|
165
|
+
// Register the application itself
|
|
166
|
+
this._container.instance('app', this);
|
|
167
|
+
this._container.instance('container', this._container);
|
|
168
|
+
|
|
169
|
+
// Register built-in middleware
|
|
170
|
+
this._mwRegistry.register('cors', new CorsMiddleware());
|
|
171
|
+
this._mwRegistry.register('throttle', new ThrottleMiddleware({ max: 60, window: 60 }));
|
|
172
|
+
this._mwRegistry.register('log', new LogMiddleware());
|
|
173
|
+
this._mwRegistry.register('auth', AuthMiddleware);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = Application;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Container
|
|
5
|
+
*
|
|
6
|
+
* Millas dependency injection container.
|
|
7
|
+
*
|
|
8
|
+
* Three binding types:
|
|
9
|
+
*
|
|
10
|
+
* container.bind('UserService', UserService)
|
|
11
|
+
* — fresh instance each time make() is called
|
|
12
|
+
*
|
|
13
|
+
* container.singleton('DB', DatabaseService)
|
|
14
|
+
* — same instance returned on every make()
|
|
15
|
+
*
|
|
16
|
+
* container.instance('Config', configObject)
|
|
17
|
+
* — register a pre-built value or object directly
|
|
18
|
+
*
|
|
19
|
+
* Auto-resolution:
|
|
20
|
+
* If a class constructor lists dependencies by name in a static
|
|
21
|
+
* `inject` array, the container resolves them automatically:
|
|
22
|
+
*
|
|
23
|
+
* class OrderService {
|
|
24
|
+
* static inject = ['UserService', 'PaymentService'];
|
|
25
|
+
* constructor(userService, paymentService) { ... }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* container.bind('OrderService', OrderService);
|
|
29
|
+
* const svc = container.make('OrderService');
|
|
30
|
+
* // UserService and PaymentService injected automatically
|
|
31
|
+
*/
|
|
32
|
+
class Container {
|
|
33
|
+
constructor() {
|
|
34
|
+
this._bindings = new Map(); // abstract → { concrete, type }
|
|
35
|
+
this._resolved = new Map(); // abstract → instance (singletons)
|
|
36
|
+
this._aliases = new Map(); // alias → abstract
|
|
37
|
+
this._tags = new Map(); // tag → [abstract, ...]
|
|
38
|
+
this._resolving = new Set(); // circular dependency guard
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Registration ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Bind an abstract name to a concrete class or factory.
|
|
45
|
+
* A new instance is created every time make() is called.
|
|
46
|
+
*
|
|
47
|
+
* container.bind('UserService', UserService)
|
|
48
|
+
* container.bind('Logger', () => new Logger({ level: 'debug' }))
|
|
49
|
+
*/
|
|
50
|
+
bind(abstract, concrete) {
|
|
51
|
+
this._bindings.set(abstract, { concrete, type: 'transient' });
|
|
52
|
+
// Clear any resolved singleton if re-bound
|
|
53
|
+
this._resolved.delete(abstract);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Bind as a singleton — the same instance is returned on every make().
|
|
59
|
+
*
|
|
60
|
+
* container.singleton('DB', DatabaseService)
|
|
61
|
+
*/
|
|
62
|
+
singleton(abstract, concrete) {
|
|
63
|
+
this._bindings.set(abstract, { concrete, type: 'singleton' });
|
|
64
|
+
this._resolved.delete(abstract);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Register a pre-built value directly.
|
|
70
|
+
* make() always returns this exact value.
|
|
71
|
+
*
|
|
72
|
+
* container.instance('Config', { port: 3000 })
|
|
73
|
+
* container.instance('App', expressApp)
|
|
74
|
+
*/
|
|
75
|
+
instance(abstract, value) {
|
|
76
|
+
this._bindings.set(abstract, { concrete: null, type: 'instance' });
|
|
77
|
+
this._resolved.set(abstract, value);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register an alias for an abstract name.
|
|
83
|
+
*
|
|
84
|
+
* container.alias('db', 'DatabaseService')
|
|
85
|
+
* container.make('db') // resolves DatabaseService
|
|
86
|
+
*/
|
|
87
|
+
alias(alias, abstract) {
|
|
88
|
+
this._aliases.set(alias, abstract);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Tag multiple bindings under a group name.
|
|
94
|
+
*
|
|
95
|
+
* container.tag(['MySQLDriver', 'SQLiteDriver'], 'db.drivers')
|
|
96
|
+
* container.tagged('db.drivers') // → [MySQLDriver instance, SQLiteDriver instance]
|
|
97
|
+
*/
|
|
98
|
+
tag(abstracts, tag) {
|
|
99
|
+
if (!this._tags.has(tag)) this._tags.set(tag, []);
|
|
100
|
+
for (const a of [].concat(abstracts)) {
|
|
101
|
+
this._tags.get(tag).push(a);
|
|
102
|
+
}
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Resolution ──────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a binding by name, building and injecting dependencies.
|
|
110
|
+
*
|
|
111
|
+
* const svc = container.make('UserService')
|
|
112
|
+
* const svc = container.make(UserService) // class reference also works
|
|
113
|
+
*/
|
|
114
|
+
make(abstract, overrides = {}) {
|
|
115
|
+
// Accept class references directly
|
|
116
|
+
if (typeof abstract === 'function') {
|
|
117
|
+
return this._build(abstract, overrides);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Resolve alias
|
|
121
|
+
const resolved = this._aliases.get(abstract) || abstract;
|
|
122
|
+
|
|
123
|
+
// Guard against circular deps
|
|
124
|
+
if (this._resolving.has(resolved)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Circular dependency detected while resolving "${resolved}".`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const binding = this._bindings.get(resolved);
|
|
131
|
+
|
|
132
|
+
if (!binding) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`"${resolved}" is not bound in the container. ` +
|
|
135
|
+
`Did you forget to call container.bind('${resolved}', MyClass)?`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Pre-built instance
|
|
140
|
+
if (binding.type === 'instance') {
|
|
141
|
+
return this._resolved.get(resolved);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Return cached singleton
|
|
145
|
+
if (binding.type === 'singleton' && this._resolved.has(resolved)) {
|
|
146
|
+
return this._resolved.get(resolved);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this._resolving.add(resolved);
|
|
150
|
+
let instance;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
instance = this._build(binding.concrete, overrides);
|
|
154
|
+
} finally {
|
|
155
|
+
this._resolving.delete(resolved);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (binding.type === 'singleton') {
|
|
159
|
+
this._resolved.set(resolved, instance);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return instance;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve all bindings under a tag.
|
|
167
|
+
*
|
|
168
|
+
* const drivers = container.tagged('db.drivers')
|
|
169
|
+
*/
|
|
170
|
+
tagged(tag) {
|
|
171
|
+
const abstracts = this._tags.get(tag);
|
|
172
|
+
if (!abstracts) return [];
|
|
173
|
+
return abstracts.map(a => this.make(a));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check whether a name is bound.
|
|
178
|
+
*/
|
|
179
|
+
has(abstract) {
|
|
180
|
+
const resolved = this._aliases.get(abstract) || abstract;
|
|
181
|
+
return this._bindings.has(resolved);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Call a method or function, auto-injecting its declared dependencies.
|
|
186
|
+
*
|
|
187
|
+
* container.call(myService, 'processOrder', { orderId: 123 })
|
|
188
|
+
* container.call(myFunction)
|
|
189
|
+
*/
|
|
190
|
+
call(target, method, extras = {}) {
|
|
191
|
+
if (typeof target === 'function') {
|
|
192
|
+
return this._callFunction(target, extras);
|
|
193
|
+
}
|
|
194
|
+
if (typeof target === 'object' && typeof target[method] === 'function') {
|
|
195
|
+
return this._callFunction(target[method].bind(target), extras);
|
|
196
|
+
}
|
|
197
|
+
throw new Error(`Cannot call "${method}" on the given target.`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Forget a resolved singleton (force re-instantiation on next make()).
|
|
202
|
+
*/
|
|
203
|
+
forgetInstance(abstract) {
|
|
204
|
+
this._resolved.delete(abstract);
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Remove a binding entirely.
|
|
210
|
+
*/
|
|
211
|
+
unbind(abstract) {
|
|
212
|
+
this._bindings.delete(abstract);
|
|
213
|
+
this._resolved.delete(abstract);
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* List all registered abstract names.
|
|
219
|
+
*/
|
|
220
|
+
bindings() {
|
|
221
|
+
return [...this._bindings.keys()];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Build a concrete class or factory, resolving its inject[] dependencies.
|
|
228
|
+
*/
|
|
229
|
+
_build(concrete, overrides = {}) {
|
|
230
|
+
if (concrete === null || concrete === undefined) {
|
|
231
|
+
throw new Error('Cannot build a null concrete.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Factory function (not a class constructor)
|
|
235
|
+
if (this._isFactory(concrete)) {
|
|
236
|
+
return concrete(this, overrides);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Class — resolve static inject[] array
|
|
240
|
+
const deps = this._resolveDependencies(concrete, overrides);
|
|
241
|
+
return new concrete(...deps);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve the inject[] static array on a class.
|
|
246
|
+
*
|
|
247
|
+
* class MyService {
|
|
248
|
+
* static inject = ['Logger', 'Config'];
|
|
249
|
+
* }
|
|
250
|
+
*/
|
|
251
|
+
_resolveDependencies(Cls, overrides = {}) {
|
|
252
|
+
const inject = Cls.inject || [];
|
|
253
|
+
return inject.map(dep => {
|
|
254
|
+
if (dep in overrides) return overrides[dep];
|
|
255
|
+
return this.make(dep);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Detect whether `fn` is a plain factory function vs a class constructor.
|
|
261
|
+
* Heuristic: class constructors start with an uppercase letter.
|
|
262
|
+
*/
|
|
263
|
+
_isFactory(fn) {
|
|
264
|
+
if (typeof fn !== 'function') return false;
|
|
265
|
+
const str = fn.toString();
|
|
266
|
+
// Arrow functions and non-class functions = factory
|
|
267
|
+
if (str.startsWith('class ')) return false;
|
|
268
|
+
if (/^function\s+[A-Z]/.test(str)) return false; // old-style class
|
|
269
|
+
// Check if it's a regular function (not a constructor-style)
|
|
270
|
+
const name = fn.name || '';
|
|
271
|
+
return !(name[0] && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_callFunction(fn, extras = {}) {
|
|
275
|
+
const inject = fn.inject || [];
|
|
276
|
+
const deps = inject.map(dep => dep in extras ? extras[dep] : this.make(dep));
|
|
277
|
+
return fn(...deps);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = Container;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Container = require('./Container');
|
|
4
|
+
const Application = require('./Application');
|
|
5
|
+
const ServiceProvider = require('../providers/ServiceProvider');
|
|
6
|
+
const ProviderRegistry = require('../providers/ProviderRegistry');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
Container,
|
|
10
|
+
Application,
|
|
11
|
+
ServiceProvider,
|
|
12
|
+
ProviderRegistry,
|
|
13
|
+
};
|