Перейти до основного вмісту

@ditsmod/body-parser

У цьому модулі зроблена інтеграція з @ts-stack/body-parser (що є форком широковідомого пакета ExpressJS) та @ts-stack/multer (що є також форком широковідомого пакета ExpressJS). По-дефолту, підтримуються наступні формати даних:

  1. application/json
  2. application/x-www-form-urlencoded
  3. text/plain
  4. application/octet-stream
  5. 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 {}

Отримання тіла запиту

В залежності від того, чи є контролер одинаком чи ні, результат роботи інтерсептора можна отримати двома способами:

  1. Якщо контролер не є одинаком, результат можна отримати за допомогою токена HTTP_BODY:
import { controller, Res, route, inject } from '@ditsmod/core';
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);
}
}
  1. Якщо контролер є одинаком, результат можна отримати з контексту:
import { controller, route, SingletonRequestContext } from '@ditsmod/core';

@controller({ isSingleton: true })
export class SomeController {
@route('POST')
ok(ctx: SingletonRequestContext) {
ctx.sendJson(ctx.body);
}
}

Завантаження файлів

В залежності від того, чи є контролер одинаком чи ні, спосіб отримання парсера, та сигнатури його методів трохи відрізняються:

  1. Якщо контролер не є одинаком, через DI необхідно запитати MulterParser, після чого можете користуватись його методами:
import { createWriteStream } from 'node:fs';
import { controller, Res, route } from '@ditsmod/core';
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);
}
}
  1. Якщо контролер є одинаком, через DI необхідно запитати MulterSingletonParser, після чого можете користуватись його методами:
import { createWriteStream } from 'node:fs';
import { controller, route, SingletonRequestContext } from '@ditsmod/core';
import { MulterParsedForm, MulterSingletonParser } from '@ditsmod/body-parser';

@controller({ isSingleton: true })
export class SomeController {
constructor(protected parse: MulterSingletonParser) {}

@route('POST', 'file-upload')
async downloadFile(ctx: SingletonRequestContext) {
const parsedForm = await this.parse.array(ctx, 'fieldName', 5);
await this.saveFiles(parsedForm);
// ...
ctx.nodeRes.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, який повертають методи парсерів, матиме чотири властивості:

  1. textFields міститиме об'єкт зі значеннями з текстових полів HTML-форми (якщо такі є);
  2. file міститиме об'єкт, де зокрема зберігатиметься файл у вигляді Readable потоку, який можна використовувати для збереження файлу.
  3. files міститиме масив об'єктів, кожен елемент якого має тип, указаний в другому пункті.
  4. groups міститиме об'єкт, де кожен ключ відповідає назві поля у HTML-формі, а вміст кожної властивості - це масив файлів, що має тип, указаний у третьому пункті.

За один парсинг може бути заповнено максимум дві властивості із чотирьох - це властивість textFields і одна із властивостей: file, files або groups. Яка із властивостей буде заповнюватись, залежить від використаного методу парсера.

  • метод single приймає єдиний файл з указаного поля форми; зверніть увагу на назви властивостей під час деструкції об'єкта (інші властивості, в даному випадку, є незаповненими):

    const { textFields, file } = await parse.single('fieldName');
    // OR
    const { textFields, file } = await parse.single(ctx, 'fieldName'); // For singleton.
  • метод array може приймати декілька файлів з указаного поля форми:

    const { textFields, files } = await parse.array('fieldName', 5);
    // OR
    const { textFields, files } = await parse.array(ctx, 'fieldName', 5); // For singleton.
  • метод any повертає такий самий тип даних, що і метод array, але він приймає файли з будь-якими назвами полів форми, а також він не має параметрів для обмеження максимальної кількості файлів (вона обмежується лише загальною конфігурацією, про яку буде йти мова згодом):

    const { textFields, files } = await parse.any();
    // OR
    const { textFields, files } = await parse.any(ctx); // For singleton.
  • метод 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 singleton.
  • метод textFields повертає об'єкт лише з полів форми, що не мають type="file"; якщо у формі будуть поля з файлами, цей метод кине помилку:

    const textFields = await parse.textFields();
    // OR
    const textFields = await parse.textFields(ctx); // For singleton.

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 так і для MulterSingletonParser:

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 {}