@kanun-hq/plugin-file 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 +670 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +665 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Toneflix, LLC. and Arkstack Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
# @kanun-hq/plugin-file
|
|
2
|
+
|
|
3
|
+
File validation plugin for Kanun.
|
|
4
|
+
|
|
5
|
+
`@kanun-hq/plugin-file` adds first-class file rules, request-context helpers, framework adapters, and wildcard utilities for upload-heavy validation flows.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Adds `file`, `files`, `image`, `extensions`, `mimes`, `mimetypes`, and `dimensions` rules
|
|
10
|
+
- Extends built-in `min`, `max`, and `size` so file sizes are measured in kilobytes
|
|
11
|
+
- Supports direct file values, request-scoped uploads, and wildcard multi-file validation
|
|
12
|
+
- Ships adapters for Express, Fastify, Hono, and h3
|
|
13
|
+
- Supports custom file resolution through `createFileValidatorPlugin()`
|
|
14
|
+
- Returns validated files from `await validator.validate()`
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add kanun @kanun-hq/plugin-file
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install kanun @kanun-hq/plugin-file
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add kanun @kanun-hq/plugin-file
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Registering The Plugin
|
|
31
|
+
|
|
32
|
+
Register the plugin once during application startup.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Validator } from 'kanun';
|
|
36
|
+
import { fileValidatorPlugin } from '@kanun-hq/plugin-file';
|
|
37
|
+
|
|
38
|
+
Validator.use(fileValidatorPlugin);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you need custom file lookup behavior, register your own instance instead:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Validator } from 'kanun';
|
|
45
|
+
import { createFileValidatorPlugin } from '@kanun-hq/plugin-file';
|
|
46
|
+
|
|
47
|
+
Validator.use(
|
|
48
|
+
createFileValidatorPlugin({
|
|
49
|
+
resolveFiles: ({ attribute, context, value }) => {
|
|
50
|
+
if (typeof value !== 'undefined') {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return context.requestFiles?.[attribute];
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Supported File Shapes
|
|
61
|
+
|
|
62
|
+
The plugin accepts file-like values that expose one or more of these properties:
|
|
63
|
+
|
|
64
|
+
- `buffer`
|
|
65
|
+
- `filename`
|
|
66
|
+
- `originalname`
|
|
67
|
+
- `name`
|
|
68
|
+
- `path`
|
|
69
|
+
- `mimetype`
|
|
70
|
+
- `type`
|
|
71
|
+
- `size`
|
|
72
|
+
- `width`
|
|
73
|
+
- `height`
|
|
74
|
+
|
|
75
|
+
That means these input styles work:
|
|
76
|
+
|
|
77
|
+
- Multer-style file objects
|
|
78
|
+
- Fastify multipart parts after normalization
|
|
79
|
+
- plain objects with `path`, `size`, and `mimetype`
|
|
80
|
+
- `File` and `Blob` values from `FormData`
|
|
81
|
+
- arrays of any supported file shape
|
|
82
|
+
|
|
83
|
+
Example direct file value:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const avatar = {
|
|
87
|
+
buffer,
|
|
88
|
+
mimetype: 'image/png',
|
|
89
|
+
originalname: 'avatar.png',
|
|
90
|
+
size: 2048,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const validator = Validator.make(
|
|
94
|
+
{ avatar },
|
|
95
|
+
{ avatar: 'file|image|extensions:png|max:4' },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const passes = await validator.passes();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Rule Reference
|
|
102
|
+
|
|
103
|
+
### `file`
|
|
104
|
+
|
|
105
|
+
Validates that the value resolves to exactly one file.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const validator = Validator.make({ avatar }, { avatar: 'file' });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `files`
|
|
112
|
+
|
|
113
|
+
Validates that the value resolves to an array of files.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const validator = Validator.make({ attachments }, { attachments: 'files' });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `image`
|
|
120
|
+
|
|
121
|
+
Validates that all resolved files are images.
|
|
122
|
+
|
|
123
|
+
Image detection succeeds when either of these is true:
|
|
124
|
+
|
|
125
|
+
- the MIME type starts with `image/`
|
|
126
|
+
- the file extension is a known image extension such as `png`, `jpg`, `gif`, `webp`, or `svg`
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const validator = Validator.make({ avatar }, { avatar: 'file|image' });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### `extensions:...`
|
|
133
|
+
|
|
134
|
+
Validates the filename or path suffix.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const validator = Validator.make(
|
|
138
|
+
{ avatar },
|
|
139
|
+
{ avatar: 'file|extensions:png,jpg,jpeg' },
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use this when your rule should match the actual extension directly.
|
|
144
|
+
|
|
145
|
+
### `mimes:...`
|
|
146
|
+
|
|
147
|
+
Validates by resolved extension using the familiar Laravel-style rule syntax.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const validator = Validator.make({ avatar }, { avatar: 'file|mimes:png,jpg' });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Use this when you want extension-like validation but prefer the conventional `mimes` rule name.
|
|
154
|
+
|
|
155
|
+
### `mimetypes:...`
|
|
156
|
+
|
|
157
|
+
Validates the file MIME type.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const validator = Validator.make(
|
|
161
|
+
{ avatar },
|
|
162
|
+
{ avatar: 'file|mimetypes:image/png,image/jpeg' },
|
|
163
|
+
);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `dimensions:...`
|
|
167
|
+
|
|
168
|
+
Validates image dimensions using any combination of:
|
|
169
|
+
|
|
170
|
+
- `min_width`
|
|
171
|
+
- `max_width`
|
|
172
|
+
- `min_height`
|
|
173
|
+
- `max_height`
|
|
174
|
+
- `ratio`
|
|
175
|
+
|
|
176
|
+
`ratio` can be written as a number or fraction.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const validator = Validator.make(
|
|
180
|
+
{ avatar },
|
|
181
|
+
{
|
|
182
|
+
avatar:
|
|
183
|
+
'file|image|dimensions:min_width=256,min_height=256,max_width=2048,max_height=2048,ratio=1',
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const validator = Validator.make(
|
|
190
|
+
{ banner },
|
|
191
|
+
{
|
|
192
|
+
banner: 'file|image|dimensions:min_width=1200,ratio=3/1',
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Dimension lookup works in this order:
|
|
198
|
+
|
|
199
|
+
- explicit `width` and `height` on the file object
|
|
200
|
+
- image data from `buffer`
|
|
201
|
+
- image data from `path`
|
|
202
|
+
|
|
203
|
+
## File Size Rules
|
|
204
|
+
|
|
205
|
+
The plugin extends Kanun's built-in `min`, `max`, and `size` rules for file values.
|
|
206
|
+
|
|
207
|
+
All file sizes are interpreted in kilobytes.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const validator = Validator.make({ avatar }, { avatar: 'file|min:1|max:2048' });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const validator = Validator.make({ avatar }, { avatar: 'file|size:512' });
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Validating Files Passed Directly In Data
|
|
218
|
+
|
|
219
|
+
If your application already has file objects on the data payload, you can validate them without any context helpers.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const validator = Validator.make(
|
|
223
|
+
{
|
|
224
|
+
avatar: {
|
|
225
|
+
buffer,
|
|
226
|
+
mimetype: 'image/png',
|
|
227
|
+
originalname: 'avatar.png',
|
|
228
|
+
size: 2048,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
avatar: 'file|image|extensions:png|mimetypes:image/png|max:4',
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await validator.validate();
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Validating Request-Scoped Uploads With `.withContext()`
|
|
240
|
+
|
|
241
|
+
If files are not part of the data object, attach them through validator context.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const validator = Validator.make(
|
|
245
|
+
{},
|
|
246
|
+
{ avatar: 'file|image|mimes:png' },
|
|
247
|
+
).withContext({
|
|
248
|
+
requestFiles: {
|
|
249
|
+
avatar,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const validated = await validator.validate();
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`validated` will include the file:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
{
|
|
260
|
+
avatar,
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Global Request Context With `Validator.useContext()`
|
|
265
|
+
|
|
266
|
+
If you register request-scoped data in middleware and create validators later in the same request lifecycle, use the static API.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { Validator } from 'kanun';
|
|
270
|
+
|
|
271
|
+
Validator.useContext({
|
|
272
|
+
requestFiles: {
|
|
273
|
+
avatar,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const validator = Validator.make({}, { avatar: 'file|image|mimes:png' });
|
|
278
|
+
const validated = await validator.validate();
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Returning Validated Files From `validate()`
|
|
282
|
+
|
|
283
|
+
`await validator.validate()` returns the validated values for rule keys.
|
|
284
|
+
|
|
285
|
+
That includes:
|
|
286
|
+
|
|
287
|
+
- files supplied directly in `data`
|
|
288
|
+
- files resolved from `context.requestFiles`
|
|
289
|
+
- file collections mirrored into data for wildcard validation
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const validator = Validator.make(
|
|
295
|
+
{},
|
|
296
|
+
{ avatar: 'file|image|extensions:png' },
|
|
297
|
+
).withContext({
|
|
298
|
+
requestFiles: {
|
|
299
|
+
avatar,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const validated = await validator.validate();
|
|
304
|
+
|
|
305
|
+
validated.avatar;
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Express
|
|
309
|
+
|
|
310
|
+
### Middleware-style usage
|
|
311
|
+
|
|
312
|
+
Use `useExpressUploadContext()` after middleware such as Multer has populated `req.file` or `req.files`.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import express from 'express';
|
|
316
|
+
import multer from 'multer';
|
|
317
|
+
import { Validator } from 'kanun';
|
|
318
|
+
import {
|
|
319
|
+
fileValidatorPlugin,
|
|
320
|
+
useExpressUploadContext,
|
|
321
|
+
} from '@kanun-hq/plugin-file';
|
|
322
|
+
|
|
323
|
+
Validator.use(fileValidatorPlugin);
|
|
324
|
+
|
|
325
|
+
const app = express();
|
|
326
|
+
const upload = multer({ storage: multer.memoryStorage() });
|
|
327
|
+
|
|
328
|
+
app.post('/profile', upload.single('avatar'), async (req, res) => {
|
|
329
|
+
useExpressUploadContext(req);
|
|
330
|
+
|
|
331
|
+
const validator = Validator.make(
|
|
332
|
+
{},
|
|
333
|
+
{
|
|
334
|
+
avatar: 'file|image|mimes:png,jpg|max:2048',
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const validated = await validator.validate();
|
|
339
|
+
res.json(validated);
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Single validator instance usage
|
|
344
|
+
|
|
345
|
+
Use `withExpressUploadContext()` when you only want to enrich one validator instance.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import { withExpressUploadContext } from '@kanun-hq/plugin-file';
|
|
349
|
+
|
|
350
|
+
const validator = withExpressUploadContext(
|
|
351
|
+
Validator.make({}, { avatar: 'file|image|mimes:png' }),
|
|
352
|
+
req,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
await validator.validate();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Fastify
|
|
359
|
+
|
|
360
|
+
### Hook or route usage
|
|
361
|
+
|
|
362
|
+
Use `useFastifyUploadContext()` with `@fastify/multipart`.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import Fastify from 'fastify';
|
|
366
|
+
import multipart from '@fastify/multipart';
|
|
367
|
+
import { Validator } from 'kanun';
|
|
368
|
+
import {
|
|
369
|
+
fileValidatorPlugin,
|
|
370
|
+
useFastifyUploadContext,
|
|
371
|
+
} from '@kanun-hq/plugin-file';
|
|
372
|
+
|
|
373
|
+
Validator.use(fileValidatorPlugin);
|
|
374
|
+
|
|
375
|
+
const app = Fastify();
|
|
376
|
+
await app.register(multipart);
|
|
377
|
+
|
|
378
|
+
app.addHook('preHandler', async (request) => {
|
|
379
|
+
await useFastifyUploadContext(request);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
app.post('/attachments', async () => {
|
|
383
|
+
const validator = Validator.make(
|
|
384
|
+
{},
|
|
385
|
+
{
|
|
386
|
+
attachments: 'files|mimes:png,jpg,pdf',
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
return await validator.validate();
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`useFastifyUploadContext()` can normalize uploads from:
|
|
395
|
+
|
|
396
|
+
- `request.files()`
|
|
397
|
+
- `request.parts()`
|
|
398
|
+
- `request.file()`
|
|
399
|
+
- `request.body`
|
|
400
|
+
- `request.raw.files`
|
|
401
|
+
|
|
402
|
+
### Single validator instance usage
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { withFastifyUploadContext } from '@kanun-hq/plugin-file';
|
|
406
|
+
|
|
407
|
+
const validator = await withFastifyUploadContext(
|
|
408
|
+
Validator.make({}, { attachments: 'files|mimes:png,jpg' }),
|
|
409
|
+
request,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
await validator.validate();
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Hono
|
|
416
|
+
|
|
417
|
+
### Middleware-style usage
|
|
418
|
+
|
|
419
|
+
Use `useHonoUploadContext()` to read files from `c.req.parseBody({ all: true })`.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import { Hono } from 'hono';
|
|
423
|
+
import { Validator } from 'kanun';
|
|
424
|
+
import {
|
|
425
|
+
fileValidatorPlugin,
|
|
426
|
+
useHonoUploadContext,
|
|
427
|
+
} from '@kanun-hq/plugin-file';
|
|
428
|
+
|
|
429
|
+
Validator.use(fileValidatorPlugin);
|
|
430
|
+
|
|
431
|
+
const app = new Hono();
|
|
432
|
+
|
|
433
|
+
app.use('/profile', async (c, next) => {
|
|
434
|
+
await useHonoUploadContext(c);
|
|
435
|
+
await next();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
app.post('/profile', async (c) => {
|
|
439
|
+
const validator = Validator.make(
|
|
440
|
+
{},
|
|
441
|
+
{
|
|
442
|
+
avatar: 'file|image|mimetypes:image/png',
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
return c.json(await validator.validate());
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Single validator instance usage
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import { withHonoUploadContext } from '@kanun-hq/plugin-file';
|
|
454
|
+
|
|
455
|
+
const validator = await withHonoUploadContext(
|
|
456
|
+
Validator.make({}, { avatar: 'file|image|mimetypes:image/png' }),
|
|
457
|
+
c,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
await validator.validate();
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
## h3
|
|
464
|
+
|
|
465
|
+
### Middleware-style usage
|
|
466
|
+
|
|
467
|
+
Use `useH3UploadContext()` inside middleware or handlers.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { H3 } from 'h3';
|
|
471
|
+
import { Validator } from 'kanun';
|
|
472
|
+
import { fileValidatorPlugin, useH3UploadContext } from '@kanun-hq/plugin-file';
|
|
473
|
+
|
|
474
|
+
Validator.use(fileValidatorPlugin);
|
|
475
|
+
|
|
476
|
+
const app = new H3();
|
|
477
|
+
|
|
478
|
+
app.use('/profile', async (event, next) => {
|
|
479
|
+
await useH3UploadContext(event);
|
|
480
|
+
return next();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
app.post('/profile', async () => {
|
|
484
|
+
const validator = Validator.make(
|
|
485
|
+
{},
|
|
486
|
+
{
|
|
487
|
+
avatar: 'file|image|mimetypes:image/png',
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
return await validator.validate();
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
`useH3UploadContext()` can resolve files from:
|
|
496
|
+
|
|
497
|
+
- `event.context.requestFiles`
|
|
498
|
+
- `event.req.formData()`
|
|
499
|
+
- `event.request.formData()`
|
|
500
|
+
- multipart parsing when available
|
|
501
|
+
|
|
502
|
+
### Single validator instance usage
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
import { withH3UploadContext } from '@kanun-hq/plugin-file';
|
|
506
|
+
|
|
507
|
+
const validator = await withH3UploadContext(
|
|
508
|
+
Validator.make({}, { avatar: 'file|image|mimetypes:image/png' }),
|
|
509
|
+
event,
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
await validator.validate();
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Wildcard Multi-file Validation
|
|
516
|
+
|
|
517
|
+
Use `createWildcardFileRules()` when you want both collection-level and item-level rules.
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
import { createWildcardFileRules } from '@kanun-hq/plugin-file';
|
|
521
|
+
|
|
522
|
+
const rules = createWildcardFileRules(
|
|
523
|
+
'attachments',
|
|
524
|
+
'file|image|extensions:png,jpg',
|
|
525
|
+
);
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
This produces rules equivalent to:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
{
|
|
532
|
+
attachments: ['files'],
|
|
533
|
+
'attachments.*': ['file', 'image', 'extensions:png,jpg'],
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
If your uploads live in `requestFiles` rather than `data`, mirror them into the validator data before validation so wildcard expansion can see the collection.
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
import {
|
|
541
|
+
createWildcardFileRules,
|
|
542
|
+
syncRequestFilesToData,
|
|
543
|
+
useExpressUploadContext,
|
|
544
|
+
} from '@kanun-hq/plugin-file';
|
|
545
|
+
|
|
546
|
+
useExpressUploadContext(req);
|
|
547
|
+
|
|
548
|
+
const validator = syncRequestFilesToData(
|
|
549
|
+
Validator.make(
|
|
550
|
+
{},
|
|
551
|
+
createWildcardFileRules(
|
|
552
|
+
'attachments',
|
|
553
|
+
'file|image|extensions:png,jpg',
|
|
554
|
+
'files',
|
|
555
|
+
),
|
|
556
|
+
),
|
|
557
|
+
['attachments'],
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const validated = await validator.validate();
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
`validated.attachments` will contain the uploaded files.
|
|
564
|
+
|
|
565
|
+
## Custom File Resolution
|
|
566
|
+
|
|
567
|
+
If your framework or upload pipeline stores files somewhere other than `context.requestFiles`, provide a custom resolver.
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
import { Validator } from 'kanun';
|
|
571
|
+
import { createFileValidatorPlugin } from '@kanun-hq/plugin-file';
|
|
572
|
+
|
|
573
|
+
Validator.use(
|
|
574
|
+
createFileValidatorPlugin({
|
|
575
|
+
resolveFiles: ({ attribute, context, data, value }) => {
|
|
576
|
+
if (typeof value !== 'undefined') {
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (context.uploads?.[attribute]) {
|
|
581
|
+
return context.uploads[attribute];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return data[`__files.${attribute}`];
|
|
585
|
+
},
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Resolver arguments:
|
|
591
|
+
|
|
592
|
+
- `attribute`: the rule key being validated
|
|
593
|
+
- `context`: the validator context
|
|
594
|
+
- `data`: the validator data object
|
|
595
|
+
- `value`: the current field value from `data`, if present
|
|
596
|
+
|
|
597
|
+
## Common Patterns
|
|
598
|
+
|
|
599
|
+
### Validate one avatar upload
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
const validator = Validator.make(
|
|
603
|
+
{},
|
|
604
|
+
{
|
|
605
|
+
avatar: 'file|image|extensions:png,jpg|max:2048',
|
|
606
|
+
},
|
|
607
|
+
).withContext({
|
|
608
|
+
requestFiles: { avatar },
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
await validator.validate();
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Validate many attachments
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
const validator = Validator.make(
|
|
618
|
+
{ attachments },
|
|
619
|
+
{
|
|
620
|
+
attachments: 'files|mimes:png,jpg,pdf',
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
await validator.validate();
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Validate an image banner with dimensions
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
const validator = Validator.make(
|
|
631
|
+
{},
|
|
632
|
+
{
|
|
633
|
+
banner:
|
|
634
|
+
'file|image|mimetypes:image/png|dimensions:min_width=1200,max_width=2400,ratio=3/1',
|
|
635
|
+
},
|
|
636
|
+
).withContext({
|
|
637
|
+
requestFiles: { banner },
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await validator.validate();
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Validate request uploads and return them
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
const validated = await Validator.make(
|
|
647
|
+
{},
|
|
648
|
+
{
|
|
649
|
+
avatar: 'file|image|extensions:png',
|
|
650
|
+
},
|
|
651
|
+
)
|
|
652
|
+
.withContext({
|
|
653
|
+
requestFiles: { avatar },
|
|
654
|
+
})
|
|
655
|
+
.validate();
|
|
656
|
+
|
|
657
|
+
validated.avatar;
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Notes
|
|
661
|
+
|
|
662
|
+
- Register the plugin once at startup, not per request.
|
|
663
|
+
- `file` expects a single file, while `files` expects an array.
|
|
664
|
+
- `min`, `max`, and `size` use kilobytes for file values.
|
|
665
|
+
- `dimensions` only makes sense for image inputs.
|
|
666
|
+
- Wildcard file validation usually needs `syncRequestFilesToData()` when the source of truth is request context.
|
|
667
|
+
|
|
668
|
+
## License
|
|
669
|
+
|
|
670
|
+
MIT
|