@asterflow/multipart 1.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.
@@ -0,0 +1,416 @@
1
+ // plugins/multipart/src/index.ts
2
+ import { Plugin as re } from "@asterflow/plugin";
3
+ import { getRouteExtensions as ne, Method as oe } from "@asterflow/router";
4
+ import { unlink as se } from "fs/promises";
5
+ import { tmpdir as ae } from "os";
6
+ // plugins/multipart/package.json
7
+ var _ = "1.1.0";
8
+ // plugins/multipart/src/controllers/multipartExtension.ts
9
+ import { Method as H, RouteMethodBuilder as j } from "@asterflow/router";
10
+ var v = !1;
11
+ function A() {
12
+ return v || (v = !0, H.prototype.multipart = function(i) {
13
+ return this.extend({}, { multipart: i });
14
+ }, j.prototype.multipart = function(i) {
15
+ return this.extend({}, { multipart: i });
16
+ }), !0;
17
+ }
18
+ // plugins/multipart/src/types/multipart.ts
19
+ var h = {
20
+ limits: {
21
+ fieldNameSize: 100,
22
+ fieldSize: 1048576,
23
+ fields: 1 / 0,
24
+ fileSize: 10485760,
25
+ files: 10,
26
+ parts: 1 / 0
27
+ },
28
+ fileHandling: {
29
+ keepInMemory: !0
30
+ },
31
+ validation: {
32
+ allowedMimeTypes: [],
33
+ allowedExtensions: []
34
+ }
35
+ };
36
+ function w(i, e) {
37
+ return {
38
+ limits: {
39
+ ...h.limits,
40
+ ...i.limits
41
+ },
42
+ fileHandling: {
43
+ keepInMemory: !0,
44
+ tempDir: e,
45
+ ...i.fileHandling
46
+ },
47
+ validation: {
48
+ ...h.validation,
49
+ ...i.validation
50
+ }
51
+ };
52
+ }
53
+ var r = {
54
+ LIMIT_FILE_SIZE: "LIMIT_FILE_SIZE",
55
+ LIMIT_FILE_COUNT: "LIMIT_FILE_COUNT",
56
+ LIMIT_FIELD_SIZE: "LIMIT_FIELD_SIZE",
57
+ LIMIT_FIELD_COUNT: "LIMIT_FIELD_COUNT",
58
+ LIMIT_PARTS: "LIMIT_PARTS",
59
+ INVALID_MIME_TYPE: "INVALID_MIME_TYPE",
60
+ INVALID_EXTENSION: "INVALID_EXTENSION",
61
+ VALIDATION_FAILED: "VALIDATION_FAILED",
62
+ FIELD_REQUIRED: "FIELD_REQUIRED",
63
+ FIELD_TOO_MANY_FILES: "FIELD_TOO_MANY_FILES",
64
+ PARSE_ERROR: "PARSE_ERROR"
65
+ }, o = class extends Error {
66
+ code;
67
+ details;
68
+ constructor(e, t, n) {
69
+ super(e), this.name = "MultipartError", this.code = t, this.details = n;
70
+ }
71
+ };
72
+ // plugins/multipart/src/controllers/validateMultipartFields.ts
73
+ function N(i, e) {
74
+ for (let [t, n] of Object.entries(e)) {
75
+ let d = i.files.filter((l) => l.fieldName === t), c = n.multiple ?? !1;
76
+ if ((n.required ?? !1) && d.length === 0)
77
+ throw new o(`Field "${t}" is required`, r.FIELD_REQUIRED, { field: t });
78
+ if (!c && d.length > 1)
79
+ throw new o(
80
+ `Field "${t}" accepts only a single file`,
81
+ r.FIELD_TOO_MANY_FILES,
82
+ { field: t, count: d.length }
83
+ );
84
+ for (let l of d) {
85
+ if (n.mimeTypes && !n.mimeTypes.includes(l.mimeType))
86
+ throw new o(
87
+ `Field "${t}" has unsupported MIME type: ${l.mimeType}`,
88
+ r.INVALID_MIME_TYPE,
89
+ { field: t, mimeType: l.mimeType }
90
+ );
91
+ if (n.extensions && !n.extensions.map((p) => p.toLowerCase()).includes(l.extension.toLowerCase()))
92
+ throw new o(
93
+ `Field "${t}" has unsupported extension: ${l.extension}`,
94
+ r.INVALID_EXTENSION,
95
+ { field: t, extension: l.extension }
96
+ );
97
+ if (n.maxSize && l.size > n.maxSize)
98
+ throw new o(
99
+ `Field "${t}" exceeds max size of ${n.maxSize} bytes`,
100
+ r.LIMIT_FILE_SIZE,
101
+ { field: t, size: l.size, maxSize: n.maxSize }
102
+ );
103
+ }
104
+ }
105
+ }
106
+ // plugins/multipart/src/controllers/MultipartParser.ts
107
+ import * as L from "busboy";
108
+ import { randomUUID as X } from "crypto";
109
+ import { createReadStream as D, createWriteStream as k } from "fs";
110
+ import { mkdir as O, readFile as G, writeFile as J } from "fs/promises";
111
+ import { tmpdir as Q } from "os";
112
+ import { dirname as K, extname as C, join as W } from "path";
113
+ import { Readable as ee } from "stream";
114
+ import { finished as te, pipeline as z } from "stream/promises";
115
+ // plugins/multipart/src/utils/stream.ts
116
+ import { Readable as Y } from "stream";
117
+ function P(i) {
118
+ return typeof i == "object" && i !== null && typeof i.pipe == "function";
119
+ }
120
+ function Z(i) {
121
+ return typeof i == "object" && i !== null && typeof i.headers?.forEach == "function" && "body" in i;
122
+ }
123
+ function S(i) {
124
+ if (P(i)) return i;
125
+ let e = i?.raw;
126
+ if (P(e)) return e;
127
+ if (Z(i)) {
128
+ let t = i.body;
129
+ return t ? Y.fromWeb(t) : null;
130
+ }
131
+ throw new o(
132
+ "Unsupported request type for multipart parsing",
133
+ r.PARSE_ERROR
134
+ );
135
+ }
136
+ // plugins/multipart/src/controllers/MultipartParser.ts
137
+ var ie = L.default ?? L, F = class {
138
+ fieldName;
139
+ filename;
140
+ encoding;
141
+ mimeType;
142
+ size;
143
+ extension;
144
+ buffer;
145
+ tempPath;
146
+ constructor(e) {
147
+ this.fieldName = e.fieldName, this.filename = e.filename, this.encoding = e.encoding, this.mimeType = e.mimeType, this.size = e.size, this.extension = e.extension, this.buffer = e.buffer, this.tempPath = e.tempPath;
148
+ }
149
+ async toBuffer() {
150
+ if (this.buffer) return this.buffer;
151
+ if (this.tempPath) return G(this.tempPath);
152
+ throw new Error(`No data available for file "${this.filename}"`);
153
+ }
154
+ async save(e) {
155
+ if (await O(K(e), { recursive: !0 }), this.buffer) {
156
+ await J(e, this.buffer);
157
+ return;
158
+ }
159
+ if (this.tempPath) {
160
+ await z(D(this.tempPath), k(e));
161
+ return;
162
+ }
163
+ throw new Error(`No data available for file "${this.filename}"`);
164
+ }
165
+ stream() {
166
+ if (this.buffer) return ee.from(this.buffer);
167
+ if (this.tempPath) return D(this.tempPath);
168
+ throw new Error(`No data available for file "${this.filename}"`);
169
+ }
170
+ }, T = class {
171
+ config;
172
+ events;
173
+ constructor(e = {}, t = {}) {
174
+ this.config = w(e, e.fileHandling?.tempDir ?? Q()), this.events = t;
175
+ }
176
+ async parse(e) {
177
+ let t = Date.now(), n = S(e.raw);
178
+ return n ? new Promise((d, c) => {
179
+ let u = !1, l = 0, f = 0, p = {}, I = [], y = [], s;
180
+ try {
181
+ s = ie({ headers: e.getHeaders(), limits: this.config.limits });
182
+ } catch (a) {
183
+ c(new o(
184
+ a instanceof Error ? a.message : "Failed to initialize the multipart parser",
185
+ r.PARSE_ERROR
186
+ ));
187
+ return;
188
+ }
189
+ let m = (a) => {
190
+ if (u) return;
191
+ u = !0;
192
+ let g = a instanceof o ? a : new o(
193
+ a instanceof Error ? a.message : "Unknown multipart parsing error",
194
+ r.PARSE_ERROR
195
+ );
196
+ this.events.onError?.(g), n.unpipe(s), queueMicrotask(() => s.destroy()), c(g);
197
+ };
198
+ s.on("field", (a, g, R) => {
199
+ if (u) return;
200
+ if (R.valueTruncated) {
201
+ m(new o(`Field "${a}" exceeds the configured size limit`, r.LIMIT_FIELD_SIZE));
202
+ return;
203
+ }
204
+ this.events.onField?.(a, g);
205
+ let M = p[a];
206
+ p[a] = M === void 0 ? g : Array.isArray(M) ? [...M, g] : [M, g], f++;
207
+ }), s.on("file", (a, g, R) => {
208
+ if (u) {
209
+ g.resume();
210
+ return;
211
+ }
212
+ let M = this.consumeFile(a, g, R, m, (E) => {
213
+ l += E.length, this.events.onProgress?.(l);
214
+ }).then((E) => {
215
+ u || !E || (I.push(E), this.events.onFileEnd?.(E));
216
+ }).catch((E) => m(E));
217
+ y.push(M);
218
+ }), s.on("partsLimit", () => m(new o("Parts limit exceeded", r.LIMIT_PARTS))), s.on("filesLimit", () => m(new o("Files limit exceeded", r.LIMIT_FILE_COUNT))), s.on("fieldsLimit", () => m(new o("Fields limit exceeded", r.LIMIT_FIELD_COUNT))), s.on("error", (a) => m(a)), n.on("error", (a) => m(a)), s.on("close", async () => {
219
+ try {
220
+ await Promise.all(y);
221
+ } catch {
222
+ return;
223
+ }
224
+ u || (u = !0, d({
225
+ fields: p,
226
+ files: I,
227
+ metadata: {
228
+ processingTime: Date.now() - t,
229
+ totalSize: l,
230
+ fieldsCount: f,
231
+ filesCount: I.length
232
+ }
233
+ }));
234
+ }), n.pipe(s);
235
+ }) : {
236
+ fields: {},
237
+ files: [],
238
+ metadata: { processingTime: Date.now() - t, totalSize: 0, fieldsCount: 0, filesCount: 0 }
239
+ };
240
+ }
241
+ async consumeFile(e, t, n, d, c) {
242
+ let u = {
243
+ fieldName: e,
244
+ filename: n.filename || "unknown",
245
+ encoding: n.encoding,
246
+ mimeType: n.mimeType
247
+ }, l = !1;
248
+ t.once("limit", () => {
249
+ l = !0, d(new o(`File "${u.filename}" exceeds the configured size limit`, r.LIMIT_FILE_SIZE));
250
+ }), t.on("error", (s) => d(s));
251
+ let f = await this.validateFile(u);
252
+ if (!f.valid)
253
+ return t.resume(), d(f.error), null;
254
+ this.events.onFileStart?.(u);
255
+ let p = 0, I = C(u.filename), y = (s) => {
256
+ p += s.length, c(s), this.events.onFileData?.(u, s);
257
+ };
258
+ try {
259
+ if (this.config.fileHandling.keepInMemory) {
260
+ let m = [];
261
+ return t.on("data", (a) => {
262
+ y(a), m.push(a);
263
+ }), await te(t), l ? null : new F({ ...u, size: p, extension: I, buffer: Buffer.concat(m) });
264
+ }
265
+ await O(this.config.fileHandling.tempDir, { recursive: !0 });
266
+ let s = W(this.config.fileHandling.tempDir, `multipart-${X()}${I}`);
267
+ return t.on("data", y), await z(t, k(s)), l ? null : new F({ ...u, size: p, extension: I, tempPath: s });
268
+ } catch (s) {
269
+ return l || d(s), null;
270
+ }
271
+ }
272
+ async validateFile(e) {
273
+ let { validation: t } = this.config;
274
+ if (t.allowedMimeTypes && t.allowedMimeTypes.length > 0 && !t.allowedMimeTypes.includes(e.mimeType))
275
+ return {
276
+ valid: !1,
277
+ error: new o(`MIME type "${e.mimeType}" is not allowed`, r.INVALID_MIME_TYPE)
278
+ };
279
+ if (t.allowedExtensions && t.allowedExtensions.length > 0) {
280
+ let n = C(e.filename).toLowerCase();
281
+ if (!t.allowedExtensions.some((d) => d.toLowerCase() === n))
282
+ return {
283
+ valid: !1,
284
+ error: new o(`Extension "${n}" is not allowed`, r.INVALID_EXTENSION)
285
+ };
286
+ }
287
+ if (t.validator)
288
+ try {
289
+ if (!await t.validator(e))
290
+ return {
291
+ valid: !1,
292
+ error: new o(`File "${e.filename}" failed custom validation`, r.VALIDATION_FAILED)
293
+ };
294
+ } catch (n) {
295
+ return {
296
+ valid: !1,
297
+ error: new o(
298
+ `Validator threw an error: ${n instanceof Error ? n.message : "Unknown error"}`,
299
+ r.VALIDATION_FAILED
300
+ )
301
+ };
302
+ }
303
+ return { valid: !0 };
304
+ }
305
+ };
306
+ async function $(i, e, t) {
307
+ return new T(e, t).parse(i);
308
+ }
309
+ // plugins/multipart/src/utils/errors.ts
310
+ function x(i, e) {
311
+ let t = {
312
+ code: e.code,
313
+ message: e.message,
314
+ ...e.details !== void 0 ? { details: e.details } : {}
315
+ };
316
+ switch (e.code) {
317
+ case r.LIMIT_FILE_SIZE:
318
+ case r.LIMIT_FILE_COUNT:
319
+ case r.LIMIT_FIELD_SIZE:
320
+ case r.LIMIT_FIELD_COUNT:
321
+ case r.LIMIT_PARTS:
322
+ return i.status(413).json({ error: "PAYLOAD_TOO_LARGE", ...t });
323
+ case r.INVALID_MIME_TYPE:
324
+ case r.INVALID_EXTENSION:
325
+ return i.status(415).json({ error: "UNSUPPORTED_MEDIA_TYPE", ...t });
326
+ case r.VALIDATION_FAILED:
327
+ case r.FIELD_REQUIRED:
328
+ case r.FIELD_TOO_MANY_FILES:
329
+ return i.validationError({ error: "VALIDATION_FAILED", ...t });
330
+ default:
331
+ return i.badRequest({ error: "MULTIPART_ERROR", ...t });
332
+ }
333
+ }
334
+ // plugins/multipart/src/utils/log.ts
335
+ var b = {
336
+ reset: "\x1B[0m",
337
+ red: "\x1B[31m",
338
+ blue: "\x1B[34m"
339
+ };
340
+ function B(...i) {
341
+ process.env.DEBUG === "true" && console.log(`${b.blue}[AsterFlow Multipart]${b.reset}`, ...i);
342
+ }
343
+ function U(i, e) {
344
+ console.error(`${b.red}%s %s${b.reset}`, "[AsterFlow Multipart]", i), console.group(), console.error("Error:", e instanceof Error ? e.message : e), e instanceof Error && "code" in e && console.error("Code:", e.code), console.groupEnd();
345
+ }
346
+ // plugins/multipart/src/index.ts
347
+ function le(i) {
348
+ return {
349
+ body: i.fields,
350
+ files: i.files,
351
+ multipartMetadata: i.metadata,
352
+ getFile(e) {
353
+ return i.files.find((t) => t.fieldName === e);
354
+ },
355
+ getFiles(e) {
356
+ return e ? i.files.filter((t) => t.fieldName === e) : i.files;
357
+ },
358
+ hasFiles() {
359
+ return i.files.length > 0;
360
+ },
361
+ getFilesByType(e) {
362
+ return i.files.filter((t) => t.mimeType === e);
363
+ },
364
+ async saveAll(e) {
365
+ let t = [];
366
+ for (let n of i.files) {
367
+ let d = `${e}/${n.filename}`;
368
+ await n.save(d), t.push(d);
369
+ }
370
+ return t;
371
+ },
372
+ async cleanupMultipart() {
373
+ await Promise.all(i.files.map(async (e) => {
374
+ if (e.tempPath)
375
+ try {
376
+ await se(e.tempPath);
377
+ } catch {
378
+ }
379
+ }));
380
+ }
381
+ };
382
+ }
383
+ var ue = re.create({ name: "multipart" }).decorate("creator", "Ashu11-A").decorate("version", _).decorate("installed", A()).config(h).on("onRequest", async ({ request: i, response: e, router: t, plugin: n }) => {
384
+ let d = i.getMethod().toLowerCase(), c = t.route instanceof oe ? t.route.extensions.multipart : ne(t.route)?.[d]?.multipart, u = i.getHeaders()["content-type"];
385
+ if (!(!!u && u.toLowerCase().includes("multipart/form-data")))
386
+ return c ? x(e, new o(
387
+ "Expected a multipart/form-data request",
388
+ r.PARSE_ERROR
389
+ )) : void 0;
390
+ try {
391
+ let f = w(n.context, ae()), p = await $(i, f);
392
+ c && N(p, c), B("Multipart request parsed", {
393
+ Fields: String(p.metadata.fieldsCount),
394
+ Files: String(p.metadata.filesCount),
395
+ "Total Size": `${p.metadata.totalSize} bytes`,
396
+ "Processing Time": `${p.metadata.processingTime}ms`
397
+ }), i.extend(le(p));
398
+ } catch (f) {
399
+ let p = f instanceof o ? f : new o(f instanceof Error ? f.message : "Unknown error", r.PARSE_ERROR);
400
+ return U("Failed to parse multipart request", p), x(e, p);
401
+ }
402
+ }).on("onResponse", async ({ request: i }) => {
403
+ await i.cleanupMultipart?.();
404
+ }), Ue = ue;
405
+ export {
406
+ h as DEFAULT_CONFIG,
407
+ r as ErrorCodes,
408
+ o as MultipartError,
409
+ T as MultipartParser,
410
+ Ue as default,
411
+ x as errorResponse,
412
+ ue as multipartPlugin,
413
+ $ as parseMultipart,
414
+ w as resolveConfig,
415
+ N as validateFields
416
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,18 @@
1
+ import { type MultipartConfig, type MultipartResult, type ParserEvents } from '../types/multipart';
2
+ /**
3
+ * The minimal request shape the parser needs. `AsterRequest` satisfies this
4
+ * structurally, so no dependency on `@asterflow/request`'s generics is needed here.
5
+ */
6
+ export interface MultipartRequestLike {
7
+ raw: unknown;
8
+ getHeaders(): Record<string, string>;
9
+ }
10
+ export declare class MultipartParser {
11
+ private readonly config;
12
+ private readonly events;
13
+ constructor(config?: MultipartConfig, events?: ParserEvents);
14
+ parse(request: MultipartRequestLike): Promise<MultipartResult>;
15
+ private consumeFile;
16
+ private validateFile;
17
+ }
18
+ export declare function parseMultipart(request: MultipartRequestLike, config?: MultipartConfig, events?: ParserEvents): Promise<MultipartResult>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runtime half of `.multipart(schema)`, declared in `types/asterflow.d.ts`
3
+ * (declaration merging is types-only). Called from `index.ts`; its return
4
+ * value is threaded into `.decorate(...)` rather than called as a bare
5
+ * statement because Bun's transpiler dead-code-eliminates a side-effecting
6
+ * call whose result isn't otherwise used.
7
+ */
8
+ export declare function installExtension(): true;
@@ -0,0 +1,3 @@
1
+ import { type MultipartFields, type MultipartResult } from '../types/multipart';
2
+ /** Validates parsed files against a route's declared `multipart` field criteria. Throws the first `MultipartError` found. */
3
+ export declare function validateFields(result: MultipartResult, schema: MultipartFields): void;
@@ -0,0 +1,27 @@
1
+ import { Plugin } from '@asterflow/plugin';
2
+ import './types/asterflow.d.ts';
3
+ import { type MultipartConfig } from './types/multipart';
4
+ export * from './controllers/MultipartParser';
5
+ export * from './controllers/validateMultipartFields';
6
+ export * from './types/inferRequest';
7
+ export * from './types/mime';
8
+ export * from './types/multipart';
9
+ export * from './utils/errors';
10
+ export declare const multipartPlugin: Plugin<{
11
+ decorate: {
12
+ creator: string;
13
+ version: string;
14
+ installed: true;
15
+ };
16
+ path: "multipart";
17
+ instance: import("asterflow").AnyAsterflow;
18
+ derive: {};
19
+ extension: {};
20
+ config: {
21
+ limits?: import("./index.ts").MultipartLimits | undefined;
22
+ fileHandling?: import("./index.ts").MultipartFileHandling | undefined;
23
+ validation?: import("./index.ts").MultipartValidation | undefined;
24
+ defaultConfig: MultipartConfig;
25
+ };
26
+ }>;
27
+ export default multipartPlugin;
@@ -0,0 +1,61 @@
1
+ import type {
2
+ DefaultMethodProps,
3
+ DefaultRouteMethodBuilderProps,
4
+ MergeProps,
5
+ MethodProps,
6
+ RouteMethodBuilderProps
7
+ } from '@asterflow/router'
8
+ import type { InferMultipartRequestFragment } from './inferRequest'
9
+ import type { MultipartFields, MultipartFile, MultipartResult } from './multipart'
10
+
11
+ /**
12
+ * Ambient fallback for routes that don't call `.multipart(...)` - optional,
13
+ * since these only exist on `request` for an actual multipart request.
14
+ * Routes that do call `.multipart(...)` get the stricter, non-optional
15
+ * typing below instead, which wins for `getFile`/`getFiles`.
16
+ */
17
+ declare module '@asterflow/request' {
18
+ interface AsterRequest {
19
+ /** Parsed form fields. Only set when the request was `multipart/form-data`. */
20
+ body?: Record<string, string | string[]>
21
+ /** All processed files from the request. Only set when it was `multipart/form-data`. */
22
+ files?: MultipartFile[]
23
+ /** Parsing metadata (timing, counts). Only set when it was `multipart/form-data`. */
24
+ multipartMetadata?: MultipartResult['metadata']
25
+
26
+ getFile?(fieldName: string): MultipartFile | undefined
27
+ getFiles?(fieldName?: string): MultipartFile[]
28
+ hasFiles?(): boolean
29
+ getFilesByType?(mimeType: string): MultipartFile[]
30
+ /** Saves every file into `directory`, keyed by original filename. Returns the written paths. */
31
+ saveAll?(directory: string): Promise<string[]>
32
+ /** Removes any temp files created on disk for this request. Safe to call more than once. */
33
+ cleanupMultipart?(): Promise<void>
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Adds `.multipart(schema)` onto `Method` and `RouteMethodBuilder` via
39
+ * declaration merging - the parameter list must match each class's real one
40
+ * exactly (count, constraints, defaults). No `const` modifier here: Bun's
41
+ * transpiler can't parse `const` type parameters inside a merged interface,
42
+ * even though `tsc` accepts it. Runtime implementation lives in
43
+ * `controllers/multipartExtension.ts`.
44
+ */
45
+ declare module '@asterflow/router' {
46
+ interface Method<
47
+ Props extends MethodProps = DefaultMethodProps
48
+ > {
49
+ multipart<Fields extends MultipartFields>(
50
+ schema: Fields
51
+ ): Method<MergeProps<Props, { requestExt: Props['requestExt'] & InferMultipartRequestFragment<Fields>, handler: Props['handler'] extends undefined ? undefined : any }>>
52
+ }
53
+
54
+ interface RouteMethodBuilder<
55
+ Props extends RouteMethodBuilderProps = DefaultRouteMethodBuilderProps
56
+ > {
57
+ multipart<Fields extends MultipartFields>(
58
+ schema: Fields
59
+ ): RouteMethodBuilder<MergeProps<Props, { requestExt: Props['requestExt'] & InferMultipartRequestFragment<Fields> }>>
60
+ }
61
+ }
@@ -0,0 +1,18 @@
1
+ import type { MultipartFieldCriteria, MultipartFields, MultipartFile } from './multipart';
2
+ type NarrowedFile<Criteria extends MultipartFieldCriteria> = Criteria['mimeTypes'] extends readonly (infer M extends string)[] ? Omit<MultipartFile, 'mimeType'> & {
3
+ mimeType: M;
4
+ } : MultipartFile;
5
+ type InferFieldFile<Criteria extends MultipartFieldCriteria> = Criteria['required'] extends true ? NarrowedFile<Criteria> : NarrowedFile<Criteria> | undefined;
6
+ type InferFieldFiles<Criteria extends MultipartFieldCriteria> = NarrowedFile<Criteria>[];
7
+ /**
8
+ * `request` extension for a route that called `.multipart(schema)` -
9
+ * `getFile`/`getFiles` keyed to the declared field names, schema-narrowed.
10
+ * `ExtendedRequest` (in `@asterflow/router`) omits the ambient untyped
11
+ * versions before intersecting this in, so this is the only signature
12
+ * available, not an extra overload.
13
+ */
14
+ export type InferMultipartRequestFragment<Schema extends MultipartFields> = {
15
+ getFile<K extends keyof Schema & string>(field: K): InferFieldFile<Schema[K]>;
16
+ getFiles<K extends keyof Schema & string>(field: K): InferFieldFiles<Schema[K]>;
17
+ };
18
+ export {};