@ditsmod/body-parser
У цьому модулі зроблена інтеграція з @ts-stack/body-parser та @ts-stack/multer. По-дефолту, підтримуються наступні формати даних:
application/json
application/x-www-form-urlencoded
text/plain
application/octet-stream
multipart/form-data
За перші чотири формати із цього списку відповідає пакет @ts-stack/body-parser
, за останій - @ts-stack/multer
, який використовується для завантаження файлів. І оскільки налаштування для завантаження файлів може сильно відрізнятись від роута до роута, відповідно - для завантаження файлів Ditsmod надає сервіс, що спрощує роботу з файлами, замість готових результатів.
Для парсингу перших чотирьох форматів, цей модуль додає інтерсептор до усіх роутів, що мають HTTP-методи вказа ні у bodyParserConfig.acceptMethods
, по-дефолту це:
POST
PUT
PATCH
Готовий приклад використання @ditsmod/body-parser
можете проглянути в репозиторії Ditsmod.
Встановлення
npm i @ditsmod/body-parser
Підключення
Щоб глобально підключити @ditsmod/body-parser
, потрібно імпортувати та експортувати BodyParserModule
в кореневому модулі:
import { rootModule } from '@ditsmod/core';
import { BodyParserModule } from '@ditsmod/body-parser';
@rootModule({
imports: [
BodyParserModule,
// ...
],
exports: [BodyParserModule]
})
export class AppModule {}
В такому разі будуть працювати дефолтні налаштування. Якщо ж вам потрібно змінити деякі опції, можете це зробити наступним чином:
import { rootModule } from '@ditsmod/core';
import { BodyParserModule } from '@ditsmod/body-parser';
const moduleWithBodyParserConfig = BodyParserModule.withParams({
acceptMethods: ['POST'],
jsonOptions: { limit: '500kb', strict: false },
urlencodedOptions: { extended: true },
});
@rootModule({
imports: [
moduleWithBodyParserConfig,
// ...
],
exports: [moduleWithBodyParserConfig],
})
export class AppModule {}
Ще один варіант передачі конфігурації:
import { rootModule, Providers } from '@ditsmod/core';
import { BodyParserModule, BodyParserConfig } from '@ditsmod/body-parser';
@rootModule({
imports: [
BodyParserModule,
// ...
],
providersPerApp: new Providers()
.useValue<BodyParserConfig>(BodyParserConfig, { acceptMethods: ['POST'] }),
exports: [BodyParserModule]
})
export class AppModule {}
Отримання тіла запиту
В залежності від того, чи працює контролер в context-scoped, чи injector-scoped режимі, результат роботи інтерсептора можна отримати двома способами:
- Якщо контролер працює в режимі injector-scoped, результат можна отримати за допомогою токена
HTTP_BODY
:
import { controller, Res, inject } from '@ditsmod/core';
import { route } from '@ditsmod/rest';
import { HTTP_BODY } from '@ditsmod/body-parser';
interface Body {
one: number;
}
@controller()
export class SomeController {
@route('POST')
ok(@inject(HTTP_BODY) body: Body, res: Res) {
res.sendJson(body);
}
}
- Якщо контролер працює в режимі context-scoped, результат можна отримати з контексту:
import { controller, RequestContext } from '@ditsmod/core';
import { route } from '@ditsmod/rest';
@controller({ scope: 'ctx' })
export class SomeController {
@route('POST')
ok(ctx: RequestContext) {
ctx.sendJson(ctx.body);
}
}
Вимкнення парсера тіла запиту
Звичайно ж, перше, що можна зробити щоб перестав працювати парсер тіла запиту, це - не імпортувати у ваш модуль @ditsmod/body-parser
глобально чи локально. Також ви можете вимкнути парсер для конкретного контролера наступним чином:
import { controller } from '@ditsmod/core';
import { BodyParserConfig } from '@ditsmod/body-parser';
@controller({
providersPerRou: [
{ token: BodyParserConfig, useValue: { acceptMethods: [] } }
],
})
export class SomeController {
// ...
}
Тобто, таким чином ви передаєте пустий масив, замість дефолтного масиву ['POST', 'PUT', 'PATCH']
.
Завантаження файлів
В залежності від того, чи контролер працює в режимі injector-scope, чи context-scope, спосіб отримання парсера, та сигнатури його методів трохи відрізняються:
- Якщо контролер працює в режимі injector-scope, через DI необхідно запитати
MulterParser
, після чого можете користуватись його методами:
import { createWriteStream } from 'node:fs';
import { controller, Res } from '@ditsmod/core';
import { route } from '@ditsmod/rest';
import { MulterParsedForm, MulterParser } from '@ditsmod/body-parser';
@controller()
export class SomeController {
@route('POST', 'file-upload')
async downloadFile(res: Res, parse: MulterParser) {
const parsedForm = await parse.array('fieldName', 5);
await this.saveFiles(parsedForm);
// ...
res.send('ok');
}
protected saveFiles(parsedForm: MulterParsedForm) {
const promises: Promise<void>[] = [];
parsedForm.files.forEach((file) => {
const promise = new Promise<void>((resolve, reject) => {
const path = `uploaded-files/${file.originalName}`;
const writableStream = createWriteStream(path).on('error', reject).on('finish', resolve);
file.stream.pipe(writableStream);
});
promises.push(promise);
});
return Promise.all(promises);
}
}
- Якщо контролер працює в режимі context-scoped, через DI необхідно запитати
MulterCtxParser
, після чого можете користуватись його методами:
import { createWriteStream } from 'node:fs';
import { controller, RequestContext } from '@ditsmod/core';
import { route } from '@ditsmod/rest';
import { MulterParsedForm, MulterCtxParser } from '@ditsmod/body-parser';
@controller({ scope: 'ctx' })
export class SomeController {
constructor(protected parse: MulterCtxParser) {}
@route('POST', 'file-upload')
async downloadFile(ctx: RequestContext) {
const parsedForm = await this.parse.array(ctx, 'fieldName', 5);
await this.saveFiles(parsedForm);
// ...
ctx.rawRes.end('ok');
}
protected saveFiles(parsedForm: MulterParsedForm) {
const promises: Promise<void>[] = [];
parsedForm.files.forEach((file) => {
const promise = new Promise<void>((resolve, reject) => {
const path = `uploaded-files/${file.originalName}`;
const writableStream = createWriteStream(path).on('error', reject).on('finish', resolve);
file.stream.pipe(writableStream);
});
promises.push(promise);
});
return Promise.all(promises);
}
}
Об'єкт parsedForm
, який повертають методи парсерів, матиме чотири властивості:
textFields
міститиме об'єкт зі значеннями з текстових полів HTML-форми (якщо такі є);file
міститиме об'єкт, де зокрема зберігатиметься файл у виглядіReadable
потоку, який можна використовувати для збереження файлу.files
міститиме масив об'єктів, кожен елемент якого має тип, указаний в другому пункті.groups
міститиме об'єкт, де кожен ключ відповідає назві поля у HTML-формі, а вміст кожної властивості - це масив файлів, що має тип, указаний у третьому пункті.
За один парсинг може бути заповнено максимум дві властивості із чотирьох - це властивість textFields
і одна із властивостей: file
, files
або groups
. Яка із властивостей буде заповнюватись, залежить від використаного методу парсера.
-
метод
single
приймає єдиний файл з указаного поля форми; зверніть увагу на назви властивостей під час деструкції об'єкта (інші властивості, в даному випадку, є незаповненими):const { textFields, file } = await parse.single('fieldName');
// OR
const { textFields, file } = await parse.single(ctx, 'fieldName'); // For context-scoped. -
метод
array
може приймати декілька файлів з указаного поля форми:const { textFields, files } = await parse.array('fieldName', 5);
// OR
const { textFields, files } = await parse.array(ctx, 'fieldName', 5); // For context-scoped. -
метод
any
повертає такий самий тип даних, що і методarray
, але він приймає файли з будь-якими назвами полів форми, а також він не має параметрів для обмеження максимальної кількості файлів (вона обмежується лише загальною конфігурацією, про яку буде йти мова згодом):const { textFields, files } = await parse.any();
// OR
const { textFields, files } = await parse.any(ctx); // For context-scoped. -
метод
groups
приймає масиви файлів з указаними полями форми:const { textFields, groups } = await parse.groups([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 },
]);
// OR
const { textFields, groups } = await parse.groups(ctx, [
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 },
]); // For context-scoped. -
метод
textFields
повертає об'єкт лише з полів форми, що не маютьtype="file"
; якщо у формі будуть поля з файлами, цей метод кине помилку:const textFields = await parse.textFields();
// OR
const textFields = await parse.textFields(ctx); // For context-scoped.
MulterExtendedOptions
У модулях, де буде працювати @ditsmod/body-parser
для форм з даними у форматі multipart/form-data
, можете передавати до DI провайдер з токеном MulterExtendedOptions
. Цей клас має на дві опції більше, ніж рідний для @ts-stack/multer
клас MulterOptions
:
import { InputLogLevel, Status } from '@ditsmod/core';
import { MulterOptions } from '@ts-stack/multer';
export class MulterExtendedOptions extends MulterOptions {
errorStatus?: Status = Status.BAD_REQUEST;
errorLogLevel?: InputLogLevel = 'debug';
}
Рекомендуємо передавати провайдер з цим токеном на рівні модуля, щоб він діяв як для MulterParser
так і для MulterCtxParser
:
import { featureModule } from '@ditsmod/core';
import { BodyParserModule, MulterExtendedOptions } from '@ditsmod/body-parser';
const multerOptions: MulterExtendedOptions = { limits: { files: 20 }, errorLogLevel: 'debug' };
@featureModule({
imports: [
// ...
BodyParserModule
],
providersPerMod: [
{ token: MulterExtendedOptions, useValue: multerOptions },
],
})
export class SomeModule {}