millas 0.2.11 → 0.2.12-beta

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 (47) hide show
  1. package/package.json +17 -3
  2. package/src/auth/AuthController.js +42 -133
  3. package/src/auth/AuthMiddleware.js +12 -23
  4. package/src/auth/RoleMiddleware.js +7 -17
  5. package/src/commands/migrate.js +46 -31
  6. package/src/commands/serve.js +266 -37
  7. package/src/container/Application.js +88 -8
  8. package/src/controller/Controller.js +79 -300
  9. package/src/errors/ErrorRenderer.js +640 -0
  10. package/src/facades/Admin.js +49 -0
  11. package/src/facades/Auth.js +46 -0
  12. package/src/facades/Cache.js +17 -0
  13. package/src/facades/Database.js +43 -0
  14. package/src/facades/Events.js +24 -0
  15. package/src/facades/Http.js +54 -0
  16. package/src/facades/Log.js +56 -0
  17. package/src/facades/Mail.js +40 -0
  18. package/src/facades/Queue.js +23 -0
  19. package/src/facades/Storage.js +17 -0
  20. package/src/facades/Validation.js +69 -0
  21. package/src/http/MillasRequest.js +253 -0
  22. package/src/http/MillasResponse.js +196 -0
  23. package/src/http/RequestContext.js +176 -0
  24. package/src/http/ResponseDispatcher.js +144 -0
  25. package/src/http/helpers.js +164 -0
  26. package/src/http/index.js +13 -0
  27. package/src/index.js +55 -2
  28. package/src/logger/internal.js +76 -0
  29. package/src/logger/patchConsole.js +135 -0
  30. package/src/middleware/CorsMiddleware.js +22 -30
  31. package/src/middleware/LogMiddleware.js +27 -59
  32. package/src/middleware/Middleware.js +24 -15
  33. package/src/middleware/MiddlewarePipeline.js +30 -67
  34. package/src/middleware/MiddlewareRegistry.js +126 -0
  35. package/src/middleware/ThrottleMiddleware.js +22 -26
  36. package/src/orm/fields/index.js +124 -56
  37. package/src/orm/migration/ModelInspector.js +7 -3
  38. package/src/orm/model/Model.js +96 -6
  39. package/src/orm/query/QueryBuilder.js +141 -3
  40. package/src/providers/LogServiceProvider.js +88 -18
  41. package/src/providers/ProviderRegistry.js +14 -1
  42. package/src/providers/ServiceProvider.js +40 -8
  43. package/src/router/Router.js +155 -223
  44. package/src/scaffold/maker.js +24 -59
  45. package/src/scaffold/templates.js +13 -12
  46. package/src/validation/BaseValidator.js +193 -0
  47. package/src/validation/Validator.js +680 -0
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Auth
5
+ *
6
+ * Authentication, JWT, and access control.
7
+ *
8
+ * const { Auth, Hasher } = require('millas/facades/Auth');
9
+ *
10
+ * // Hash a password
11
+ * const hash = await Hasher.make('secret123');
12
+ *
13
+ * // Login
14
+ * const { user, token } = await Auth.login(email, password);
15
+ *
16
+ * // Verify JWT
17
+ * const payload = Auth.verify(token);
18
+ *
19
+ * // Protect routes
20
+ * Route.prefix('/api').middleware(['auth']).group(() => {
21
+ * Route.get('/me', ({ user }) => jsonify(user));
22
+ * });
23
+ *
24
+ * // Role-based access
25
+ * middlewareRegistry.register('admin', new RoleMiddleware(['admin']));
26
+ */
27
+
28
+ const {
29
+ Auth,
30
+ Hasher,
31
+ JwtDriver,
32
+ AuthMiddleware,
33
+ RoleMiddleware,
34
+ AuthController,
35
+ AuthServiceProvider,
36
+ } = require('../index');
37
+
38
+ module.exports = {
39
+ Auth,
40
+ Hasher,
41
+ JwtDriver,
42
+ AuthMiddleware,
43
+ RoleMiddleware,
44
+ AuthController,
45
+ AuthServiceProvider,
46
+ };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Cache
5
+ *
6
+ * const { Cache } = require('millas/facades/Cache');
7
+ *
8
+ * await Cache.put('key', value, 300); // 300 second TTL
9
+ * const val = await Cache.get('key');
10
+ * const val = await Cache.remember('key', 300, () => expensive());
11
+ * await Cache.forget('key');
12
+ * await Cache.flush();
13
+ */
14
+
15
+ const { Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider } = require('../index');
16
+
17
+ module.exports = { Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Database
5
+ *
6
+ * ORM models, field definitions, and query builder.
7
+ *
8
+ * const { Model, fields } = require('millas/facades/Database');
9
+ *
10
+ * class Post extends Model {
11
+ * static table = 'posts';
12
+ * static fields = {
13
+ * id: fields.id(),
14
+ * title: fields.string({ max: 255 }),
15
+ * author: fields.ForeignKey('User', { relatedName: 'posts' }),
16
+ * published: fields.boolean({ default: false }),
17
+ * created_at: fields.timestamp(),
18
+ * updated_at: fields.timestamp(),
19
+ * };
20
+ * }
21
+ */
22
+
23
+ const {
24
+ Model,
25
+ fields,
26
+ QueryBuilder,
27
+ DatabaseManager,
28
+ SchemaBuilder,
29
+ MigrationRunner,
30
+ ModelInspector,
31
+ DatabaseServiceProvider,
32
+ } = require('../index');
33
+
34
+ module.exports = {
35
+ Model,
36
+ fields,
37
+ QueryBuilder,
38
+ DatabaseManager,
39
+ SchemaBuilder,
40
+ MigrationRunner,
41
+ ModelInspector,
42
+ DatabaseServiceProvider,
43
+ };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Events
5
+ *
6
+ * const { EventEmitter, emit } = require('millas/facades/Events');
7
+ *
8
+ * // Listen
9
+ * EventEmitter.on('user.created', async (event) => {
10
+ * await Mail.to(event.user.email).subject('Welcome').send();
11
+ * });
12
+ *
13
+ * // Emit
14
+ * await emit('user.created', { user });
15
+ *
16
+ * // Wildcard
17
+ * EventEmitter.onWildcard('user.*', async (event) => {
18
+ * Log.i('Events', event._name, event);
19
+ * });
20
+ */
21
+
22
+ const { EventEmitter, Event, Listener, emit, EventServiceProvider } = require('../index');
23
+
24
+ module.exports = { EventEmitter, Event, Listener, emit, EventServiceProvider };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Http
5
+ *
6
+ * Response builders and HTTP helpers.
7
+ * These are the primary tools for returning responses from route handlers.
8
+ *
9
+ * const { jsonify, redirect, abort } = require('millas/facades/Http');
10
+ *
11
+ * Route.get('/users', async ({ query }) => {
12
+ * const users = await User.all();
13
+ * return jsonify(users);
14
+ * });
15
+ *
16
+ * Route.post('/users', async ({ body }) => {
17
+ * const user = await User.create(body);
18
+ * return jsonify(user, { status: 201 });
19
+ * });
20
+ */
21
+
22
+ const {
23
+ MillasRequest,
24
+ MillasResponse,
25
+ ResponseDispatcher,
26
+ RequestContext,
27
+ jsonify,
28
+ view,
29
+ redirect,
30
+ text,
31
+ file,
32
+ empty,
33
+ abort,
34
+ notFound,
35
+ unauthorized,
36
+ forbidden,
37
+ } = require('../index');
38
+
39
+ module.exports = {
40
+ MillasRequest,
41
+ MillasResponse,
42
+ ResponseDispatcher,
43
+ RequestContext,
44
+ jsonify,
45
+ view,
46
+ redirect,
47
+ text,
48
+ send_file:file,
49
+ empty,
50
+ abort,
51
+ notFound,
52
+ unauthorized,
53
+ forbidden,
54
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Log
5
+ *
6
+ * Application and internal logging.
7
+ *
8
+ * const { Log, LEVELS } = require('millas/facades/Log');
9
+ *
10
+ * Log.i('Server started');
11
+ * Log.tag('UserService').d('Fetching user', { id: 5 });
12
+ * Log.w('Auth', 'Token expiring soon', { userId: 5 });
13
+ * Log.e('Payment', 'Stripe failed', error);
14
+ * Log.wtf('This should never happen');
15
+ *
16
+ * // Timer
17
+ * const done = Log.time('DB query');
18
+ * const rows = await db.select();
19
+ * done(); // → D Timer DB query: 42ms
20
+ *
21
+ * // Timed async
22
+ * const users = await Log.timed('fetchUsers', () => User.all());
23
+ */
24
+
25
+ const {
26
+ Log,
27
+ Logger,
28
+ LEVELS,
29
+ LEVEL_NAMES,
30
+ PrettyFormatter,
31
+ JsonFormatter,
32
+ SimpleFormatter,
33
+ ConsoleChannel,
34
+ FileChannel,
35
+ NullChannel,
36
+ StackChannel,
37
+ LogServiceProvider,
38
+ } = require('../index');
39
+
40
+ const MillasLog = require('../logger/internal');
41
+
42
+ module.exports = {
43
+ Log,
44
+ MillasLog,
45
+ Logger,
46
+ LEVELS,
47
+ LEVEL_NAMES,
48
+ PrettyFormatter,
49
+ JsonFormatter,
50
+ SimpleFormatter,
51
+ ConsoleChannel,
52
+ FileChannel,
53
+ NullChannel,
54
+ StackChannel,
55
+ LogServiceProvider,
56
+ };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Mail
5
+ *
6
+ * const { Mail, MailMessage } = require('millas/facades/Mail');
7
+ *
8
+ * await Mail.to('alice@example.com')
9
+ * .subject('Welcome!')
10
+ * .html('<h1>Hi Alice</h1>')
11
+ * .send();
12
+ *
13
+ * // Template
14
+ * await Mail.to(user.email)
15
+ * .subject('Reset your password')
16
+ * .view('emails/reset', { token, user })
17
+ * .send();
18
+ */
19
+
20
+ const {
21
+ Mail,
22
+ MailMessage,
23
+ TemplateEngine,
24
+ SmtpDriver,
25
+ SendGridDriver,
26
+ MailgunDriver,
27
+ LogDriver,
28
+ MailServiceProvider,
29
+ } = require('../index');
30
+
31
+ module.exports = {
32
+ Mail,
33
+ MailMessage,
34
+ TemplateEngine,
35
+ SmtpDriver,
36
+ SendGridDriver,
37
+ MailgunDriver,
38
+ LogDriver,
39
+ MailServiceProvider,
40
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Queue
5
+ *
6
+ * const { Queue, Job, dispatch } = require('millas/facades/Queue');
7
+ *
8
+ * class SendWelcomeEmail extends Job {
9
+ * async handle() {
10
+ * await Mail.to(this.data.email).subject('Welcome').send();
11
+ * }
12
+ * }
13
+ *
14
+ * // Dispatch a job
15
+ * await dispatch(new SendWelcomeEmail({ email: user.email }));
16
+ *
17
+ * // With delay
18
+ * await dispatch(new SendWelcomeEmail({ email }), { delay: 60 });
19
+ */
20
+
21
+ const { Queue, Job, QueueWorker, dispatch, QueueServiceProvider } = require('../index');
22
+
23
+ module.exports = { Queue, Job, QueueWorker, dispatch, QueueServiceProvider };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Storage
5
+ *
6
+ * const { Storage } = require('millas/facades/Storage');
7
+ *
8
+ * await Storage.put('avatars/user-5.jpg', fileBuffer);
9
+ * const url = await Storage.url('avatars/user-5.jpg');
10
+ * const data = await Storage.get('avatars/user-5.jpg');
11
+ * await Storage.delete('avatars/user-5.jpg');
12
+ * const exists = await Storage.exists('avatars/user-5.jpg');
13
+ */
14
+
15
+ const { Storage, LocalDriver, StorageServiceProvider } = require('../index');
16
+
17
+ module.exports = { Storage, LocalDriver, StorageServiceProvider };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/facades/Validation
5
+ *
6
+ * Field validators and schema runner.
7
+ *
8
+ * const { string, email, number, boolean, date, array, object, file, Validator }
9
+ * = require('millas/facades/Validation');
10
+ *
11
+ * Route.post('/register', async ({ body }) => {
12
+ * const data = await body.validate({
13
+ * name: string('Must be text').required('Name is required').min(2).max(100),
14
+ * email: email().required(),
15
+ * age: number().optional().min(0).max(120),
16
+ * password: string().required().min(8).confirmed(),
17
+ * role: string().oneOf(['admin','user']).default('user'),
18
+ * tags: array().of(string().required()).optional(),
19
+ * address: object({
20
+ * city: string().required(),
21
+ * zip: string().matches(/^\d{5}$/, 'Invalid ZIP'),
22
+ * }).optional(),
23
+ * avatar: file().optional().image().maxSize('2mb'),
24
+ * });
25
+ * return jsonify(await User.create(data), { status: 201 });
26
+ * });
27
+ */
28
+
29
+ const {
30
+ Validator,
31
+ BaseValidator,
32
+ StringValidator,
33
+ EmailValidator,
34
+ NumberValidator,
35
+ BooleanValidator,
36
+ DateValidator,
37
+ ArrayValidator,
38
+ ObjectValidator,
39
+ FileValidator,
40
+ string,
41
+ email,
42
+ number,
43
+ boolean,
44
+ date,
45
+ array,
46
+ } = require('../index');
47
+
48
+ const { object, file } = require('../validation/Validator');
49
+
50
+ module.exports = {
51
+ Validator,
52
+ BaseValidator,
53
+ StringValidator,
54
+ EmailValidator,
55
+ NumberValidator,
56
+ BooleanValidator,
57
+ DateValidator,
58
+ ArrayValidator,
59
+ ObjectValidator,
60
+ FileValidator,
61
+ string,
62
+ email,
63
+ number,
64
+ boolean,
65
+ date,
66
+ array,
67
+ object,
68
+ file,
69
+ };
@@ -0,0 +1,253 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MillasRequest
5
+ *
6
+ * Wraps an Express request and exposes a clean, framework-level API.
7
+ * Developers never touch the raw Express req — they use this instead.
8
+ *
9
+ * The raw Express req is accessible via req.raw for escape hatches,
10
+ * but should never be needed in normal application code.
11
+ *
12
+ * Usage in route handlers:
13
+ * Route.get('/users/:id', async (req) => {
14
+ * const id = req.param('id');
15
+ * const page = req.input('page', 1);
16
+ * const user = await User.findOrFail(id);
17
+ * return jsonify(user);
18
+ * });
19
+ */
20
+ class MillasRequest {
21
+ /**
22
+ * @param {import('express').Request} expressReq
23
+ */
24
+ constructor(expressReq) {
25
+ /** @private — access via req.raw if absolutely necessary */
26
+ this._req = expressReq;
27
+
28
+ // Proxy commonly-used scalar properties for convenience
29
+ this.method = expressReq.method;
30
+ this.path = expressReq.path;
31
+ this.url = expressReq.url;
32
+ this.baseUrl = expressReq.baseUrl;
33
+ this.originalUrl = expressReq.originalUrl;
34
+ }
35
+
36
+ // ─── Input ─────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Read a value from body, query, or route params — in that priority order.
40
+ * Returns defaultValue if not present.
41
+ *
42
+ * req.input('email')
43
+ * req.input('page', 1)
44
+ */
45
+ input(key, defaultValue = null) {
46
+ if (key === undefined) return this.all();
47
+ const r = this._req;
48
+ const v =
49
+ (r.body && r.body[key] !== undefined ? r.body[key] : undefined) ??
50
+ (r.query && r.query[key] !== undefined ? r.query[key] : undefined) ??
51
+ (r.params && r.params[key] !== undefined ? r.params[key] : undefined);
52
+ return v !== undefined ? v : defaultValue;
53
+ }
54
+
55
+ /**
56
+ * Read a route parameter.
57
+ * req.param('id')
58
+ */
59
+ param(key, defaultValue = null) {
60
+ return this._req.params?.[key] ?? defaultValue;
61
+ }
62
+
63
+ /**
64
+ * Read a query string value.
65
+ * req.query('page', 1)
66
+ */
67
+ query(key, defaultValue = null) {
68
+ if (key === undefined) return this._req.query || {};
69
+ return this._req.query?.[key] ?? defaultValue;
70
+ }
71
+
72
+ /**
73
+ * Read from the request body only.
74
+ * req.body('email')
75
+ */
76
+ body(key, defaultValue = null) {
77
+ if (key === undefined) return this._req.body || {};
78
+ return this._req.body?.[key] ?? defaultValue;
79
+ }
80
+
81
+ /**
82
+ * Merge body + query + params into one flat object.
83
+ */
84
+ all() {
85
+ return {
86
+ ...this._req.params,
87
+ ...this._req.query,
88
+ ...this._req.body,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Return only the specified keys from the merged input.
94
+ * req.only(['name', 'email'])
95
+ */
96
+ only(keys = []) {
97
+ const all = this.all();
98
+ return keys.reduce((acc, k) => {
99
+ if (k in all) acc[k] = all[k];
100
+ return acc;
101
+ }, {});
102
+ }
103
+
104
+ /**
105
+ * Return all input except the specified keys.
106
+ * req.except(['password', 'token'])
107
+ */
108
+ except(keys = []) {
109
+ const all = this.all();
110
+ return Object.fromEntries(
111
+ Object.entries(all).filter(([k]) => !keys.includes(k))
112
+ );
113
+ }
114
+
115
+ // ─── Headers ────────────────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Read a request header (case-insensitive).
119
+ * req.header('Authorization')
120
+ * req.header('content-type')
121
+ */
122
+ header(name, defaultValue = null) {
123
+ if (name === undefined) return this._req.headers || {};
124
+ return this._req.headers?.[name.toLowerCase()] ?? defaultValue;
125
+ }
126
+
127
+ /**
128
+ * All request headers as a plain object.
129
+ */
130
+ get headers() {
131
+ return this._req.headers || {};
132
+ }
133
+
134
+ // ─── Cookies ────────────────────────────────────────────────────────────────
135
+
136
+ /**
137
+ * Read a cookie value.
138
+ * req.cookie('session_id')
139
+ */
140
+ cookie(name, defaultValue = null) {
141
+ return this._req.cookies?.[name] ?? defaultValue;
142
+ }
143
+
144
+ // ─── Files ──────────────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Get an uploaded file (requires multer or similar middleware upstream).
148
+ * req.file('avatar')
149
+ */
150
+ file(name) {
151
+ if (this._req.files && this._req.files[name]) return this._req.files[name];
152
+ if (this._req.file && this._req.file.fieldname === name) return this._req.file;
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * All uploaded files.
158
+ */
159
+ get files() {
160
+ return this._req.files || {};
161
+ }
162
+
163
+ // ─── User ───────────────────────────────────────────────────────────────────
164
+
165
+ /**
166
+ * The authenticated user (set by AuthMiddleware).
167
+ */
168
+ get user() {
169
+ return this._req.user ?? null;
170
+ }
171
+
172
+ /** Set the authenticated user (used by AuthMiddleware). */
173
+ set user(value) {
174
+ this._req.user = value;
175
+ }
176
+
177
+ // ─── Content negotiation ────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Returns true if the request expects a JSON response.
181
+ */
182
+ wantsJson() {
183
+ const accept = this.header('accept', '');
184
+ return accept.includes('application/json') || accept.includes('*/*');
185
+ }
186
+
187
+ /**
188
+ * Returns true if the request body is JSON.
189
+ */
190
+ isJson() {
191
+ const ct = this.header('content-type', '');
192
+ return ct.includes('application/json');
193
+ }
194
+
195
+ /**
196
+ * Returns true if this was an XMLHttpRequest / fetch call.
197
+ */
198
+ isAjax() {
199
+ return this.header('x-requested-with', '').toLowerCase() === 'xmlhttprequest';
200
+ }
201
+
202
+ // ─── Network ────────────────────────────────────────────────────────────────
203
+
204
+ /**
205
+ * Client IP address.
206
+ */
207
+ get ip() {
208
+ return this._req.ip || this._req.connection?.remoteAddress || null;
209
+ }
210
+
211
+ /**
212
+ * Request hostname (from Host header).
213
+ */
214
+ get hostname() {
215
+ return this._req.hostname || '';
216
+ }
217
+
218
+ /**
219
+ * Whether the request is HTTPS.
220
+ */
221
+ get secure() {
222
+ return this._req.secure || false;
223
+ }
224
+
225
+ // ─── Validation ─────────────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * Validate request input against rules. Throws 422 HttpError on failure.
229
+ * Returns the validated data on success.
230
+ *
231
+ * const data = await req.validate({
232
+ * name: 'required|string|min:2|max:100',
233
+ * email: 'required|email',
234
+ * age: 'optional|number|min:0',
235
+ * });
236
+ */
237
+ async validate(rules) {
238
+ const { Validator } = require('../validation/Validator');
239
+ return Validator.validate(this.all(), rules);
240
+ }
241
+
242
+ // ─── Escape hatch ────────────────────────────────────────────────────────────
243
+
244
+ /**
245
+ * The raw underlying Express request.
246
+ * Only use this when you genuinely need something MillasRequest doesn't expose.
247
+ */
248
+ get raw() {
249
+ return this._req;
250
+ }
251
+ }
252
+
253
+ module.exports = MillasRequest;