millas 0.2.16 → 0.2.17

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,6 +1,6 @@
1
1
  {
2
2
  "name": "millas",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
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": {
@@ -11,20 +11,27 @@
11
11
  * installed if you actually handle uploads — multer is listed as an optional
12
12
  * peer dependency).
13
13
  *
14
- * ── Auto-injection (recommended) ─────────────────────────────────────────────
14
+ * ── Usage patterns ───────────────────────────────────────────────────────────
15
15
  *
16
- * When a route shape has encoding: 'multipart' or any file() field in its
17
- * "in" schema, the Router injects UploadMiddleware automatically.
18
- * You never touch it.
16
+ * 1. No shape, no config just add 'upload' to middleware:
19
17
  *
20
- * Route.post('/media/upload', MediaController, 'upload')
21
- * .shape({
22
- * label: 'Upload media',
23
- * encoding: 'multipart',
24
- * in: {
25
- * file: file().required().maxSize('50mb').mimeType(['image/jpeg','image/png','video/mp4']),
26
- * },
27
- * });
18
+ * Route.post('/media/upload', ['auth', 'upload'], MediaController, 'upload');
19
+ *
20
+ * async upload({ file, files, user }) {
21
+ * // file — first uploaded file (any field name), UploadedFile instance
22
+ * // files — all files keyed by field name
23
+ * const path = await file.store('media');
24
+ * }
25
+ *
26
+ * 2. Parameterized alias — configure field/count inline:
27
+ *
28
+ * 'upload' → any field, 50 MB limit (default)
29
+ * 'upload:avatar' → single field named 'avatar'
30
+ * 'upload:photos,5' → field 'photos', up to 5 files
31
+ *
32
+ * Route.post('/avatar', ['auth', 'upload:avatar'], UserController, 'updateAvatar');
33
+ *
34
+ * 3. With a shape — Router auto-injects UploadMiddleware, zero manual config:
28
35
  *
29
36
  * async upload({ body, file, user }) {
30
37
  * // file is a multer file object: { buffer, mimetype, size, originalname }
@@ -87,13 +94,14 @@ class UploadMiddleware {
87
94
  */
88
95
  constructor(options = {}) {
89
96
  this._options = {
90
- field: options.field || 'file',
91
- fields: options.fields || null,
92
- any: options.any || false,
93
- maxSize: options.maxSize !== undefined ? _parseSize(options.maxSize) : 50 * 1024 * 1024,
94
- storage: options.storage || 'memory',
95
- dest: options.dest || null,
96
- filter: options.filter || null,
97
+ field: options.field || 'file',
98
+ _explicitField: !!options.field, // true only when caller set field explicitly
99
+ fields: options.fields || null,
100
+ any: options.any || false,
101
+ maxSize: options.maxSize !== undefined ? _parseSize(options.maxSize) : 50 * 1024 * 1024,
102
+ storage: options.storage || 'memory',
103
+ dest: options.dest || null,
104
+ filter: options.filter || null,
97
105
  };
98
106
  this._multerInstance = null;
99
107
  }
@@ -106,15 +114,28 @@ class UploadMiddleware {
106
114
  *
107
115
  * Because multer is Express middleware internally, we reach into ctx.req.raw
108
116
  * to get the underlying Express req/res and call multer directly.
117
+ *
118
+ * Skips gracefully when the request is not multipart/form-data so that
119
+ * non-upload requests to the same route (or misconfigured clients) get a
120
+ * clear 400 rather than a multer internal error.
109
121
  */
110
122
  async handle(ctx, next) {
123
+ const expressReq = ctx.req.raw;
124
+ const contentType = expressReq.headers?.['content-type'] || '';
125
+
126
+ // Skip multer entirely for non-multipart requests.
127
+ // ctx.file / ctx.files stay null/{} — the handler or shape validation
128
+ // will surface a missing-required-field error if needed.
129
+ if (!contentType.includes('multipart/form-data')) {
130
+ return next();
131
+ }
132
+
111
133
  const multerMw = this._getMulterMiddleware();
112
- const expressReq = ctx.req.raw;
113
134
 
114
135
  await new Promise((resolve, reject) => {
115
136
  // multer needs a real Express res object; we synthesize a minimal one
116
- // that satisfies multer's internal usage (it only calls res.end / statusCode
117
- // on hard errors, which we convert to thrown exceptions via reject).
137
+ // that satisfies multer's internal usage (it only calls res.end /
138
+ // statusCode on hard errors, which we convert to thrown exceptions).
118
139
  const fakeRes = _buildFakeRes(reject);
119
140
  multerMw(expressReq, fakeRes, (err) => {
120
141
  if (err) return reject(err);
@@ -136,14 +157,45 @@ class UploadMiddleware {
136
157
  expressReq._millaFile = file;
137
158
  expressReq._millaFiles = files;
138
159
 
139
- // req.body is already a live reference multer mutated it in-place,
140
- // so non-file form fields are automatically visible via ctx.body.
160
+ // Also update ctx.body with any non-file multipart fields multer parsed.
161
+ // req.body is mutated in-place by multer, but ctx.body was built at
162
+ // RequestContext construction time. Reassign so the handler sees the fields.
163
+ if (expressReq.body && typeof expressReq.body === 'object') {
164
+ // Merge multer-parsed text fields into the existing ctx.body object
165
+ // without breaking the .validate() / .only() / .except() helpers
166
+ // that _buildBody attached as non-enumerable properties.
167
+ Object.assign(ctx.body, expressReq.body);
168
+ }
141
169
 
142
170
  return next();
143
171
  }
144
172
 
145
173
  // ── Static factory helpers ──────────────────────────────────────────────────
146
174
 
175
+ /**
176
+ * Build an UploadMiddleware from parameterized alias string parts.
177
+ * Called by MiddlewareRegistry when the alias includes parameters.
178
+ *
179
+ * 'upload' → any field, 50 MB limit (default)
180
+ * 'upload:avatar' → single field named 'avatar'
181
+ * 'upload:photos,5' → field 'photos', up to 5 files
182
+ * 'upload:*' → any field (explicit)
183
+ *
184
+ * @param {string[]} params Parts after the colon, split by comma
185
+ * @returns {UploadMiddleware}
186
+ */
187
+ static fromParams(params) {
188
+ if (!params || !params.length || params[0] === '*') {
189
+ return new UploadMiddleware({ any: true });
190
+ }
191
+ const field = params[0];
192
+ const maxCount = params[1] ? parseInt(params[1], 10) : 1;
193
+ if (maxCount > 1) {
194
+ return new UploadMiddleware({ fields: [{ name: field, maxCount }] });
195
+ }
196
+ return new UploadMiddleware({ field });
197
+ }
198
+
147
199
  /**
148
200
  * Build an UploadMiddleware from a shape definition.
149
201
  * Called internally by the Router when auto-injecting.
@@ -208,8 +260,10 @@ class UploadMiddleware {
208
260
 
209
261
  const upload = multer(multerConfig);
210
262
 
211
- // Pick the right multer handler based on options
212
- if (any) {
263
+ // Pick the right multer handler based on options.
264
+ // When used without a shape (e.g. just 'upload' alias), default to .any()
265
+ // so the handler receives whatever field the client sends, regardless of name.
266
+ if (any || (!fields && this._options.field === 'file' && !this._options._explicitField)) {
213
267
  this._multerInstance = upload.any();
214
268
  } else if (fields) {
215
269
  this._multerInstance = upload.fields(fields);