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

@ditsmod/openapi

Щоб створити документацію за специфікацією OpenAPI, можна використовувати модуль @ditsmod/openapi.

Встановлення

npm i @ditsmod/openapi

Створення документації

Щоб створювати окремі маршрути, користуйтесь декоратором oasRoute, в якому четвертим або третім параметром (якщо немає ґардів) йде так званий Operation Object:

import { controller } from '@ditsmod/core';
import { oasRoute } from '@ditsmod/openapi';

@controller()
export class SomeController {
// ...
@oasRoute('GET', 'users/:username', {
parameters: [
{
name: 'username',
in: 'path',
required: true,
description: 'Username of the profile to get',
schema: {
type: 'string',
},
},
],
})
async getSome() {
// ...
}
}

Ditsmod має хорошу підтримку TypeScript-моделей для OpenAPI v3.1.0, зокрема й Operation Object, але вручну прописувати весь Operation Object прямо в коді до кожного роуту - не обов'язково. Краще скористатись хелперами, які за вас згенерують необхідний код, та ще й зменшать кількість помилок за рахунок ще кращої пітримки TypeScript. Ditsmod має декілька таких хелперів: getParams(), getContent(), Parameters, Content. Усі вони імпортуються з модуля @ditsmod/openapi.

Передача параметрів Operation Object

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

import { controller } from '@ditsmod/core';
import { oasRoute, getParams } from '@ditsmod/openapi';

@controller()
export class SomeController {
// ...
@oasRoute('GET', 'users/:username', {
parameters: getParams('path', true, 'username'),
})
async getSome() {
// ...
}
}

Тут не вистачає ще вказання типу даних для параметра username та його опису. Рекомендуємо використовувати TypeScript-клас у якості моделі, щоб потім можна було на нього посилатись за допомогою хелперів, які вміють читати його метадані і повертати готові JSON-об'єкти.

Створення TypeScript-моделей

В наступному прикладі показано модель з трьома параметрами:

import { property } from '@ditsmod/openapi';

class Params {
@property({ description: 'Username of the profile to get.' })
username: string;

@property({ minimum: 1, maximum: 100, description: 'Page number.' })
page: number;

@property()
hasName: boolean;
}

Як бачите, щоб закріпити метадані за моделлю, використовується декоратор @property(), куди ви можете передавати першим аргументом Schema Object.

Зверніть увагу, що в даному разі властивість type не прописується у метаданих, оскільки указані тут типи автоматично читаються хелперами. Щоправда не усі наявні у TypeScript типи можуть читатись. Наприклад, хелпери не зможуть автоматично побачити який тип масиву ви передаєте. Це саме стосується enum. Також хелпери не бачать чи є властивість об'єкта опціональною чи ні.

Тип масиву чи enum можна передати другим параметром в декоратор @property():

import { property } from '@ditsmod/openapi';

enum NumberEnum {
one,
two,
three,
}

class Params {
@property({}, { enum: NumberEnum })
property1: NumberEnum;

@property({}, { array: String })
property2: string[];

@property({}, { array: [String, Number] })
property3: (string | number)[];

@property({}, { array: [[String]] }) // Масив в масиві
property4: string[][];
}

Посилання одних моделей на інші добре читаються. В наступному прикладі Model2 має посилання на Model1:

import { property } from '@ditsmod/openapi';

export class Model1 {
@property()
property1: string;
}

export class Model2 {
@property()
model1: Model1;

@property({}, Model1)
arrModel1: Model1[];
}

Використання TypeScript-моделей

Хелпер getParams() дозволяє використовувати моделі, і якщо ви зробите помилку у назві параметра, TypeScript скаже вам про це:

import { controller } from '@ditsmod/core';
import { oasRoute, getParams } from '@ditsmod/openapi';

import { Params } from './params.js';

@controller()
export class SomeController {
// ...
@oasRoute('GET', '', {
parameters: getParams('path', true, Params, 'username'),
})
async getSome() {
// ...
}
}

Тут Params - це клас, який використовується у якості моделі параметрів.

Але хелпер getParams() не призначений щоб його використовували одночасно для обов'язкових та необов'язкових параметрів. Також через нього не можна передавати опис параметрів, який відрізняється від опису параметрів у моделі параметрів. Для таких цілей можна скористатись іншим хелпером - Parameters:

import { controller } from '@ditsmod/core';
import { oasRoute, Parameters } from '@ditsmod/openapi';

import { Params } from './params.js';

@controller()
export class SomeController {
// ...
@oasRoute('GET', '', {
parameters: new Parameters()
.required('path', Params, 'username')
.optional('query', Params, 'page', 'hasName')
.getParams(),
})
async getSome() {
// ...
}
}

requestBody та responses content

Моделі даних також використовуються щоб описати контент requestBody, але тут є одна невелика відмінність у порівнянні з параметрами. По дефолту, усі властивості моделі є необов'язковими, і щоб позначити певну властивість обов'язковою, необхідно скористатись константою REQUIRED:

import { property, REQUIRED } from '@ditsmod/openapi';

class Model1 {
@property()
property1: string;
@property({ [REQUIRED]: true })
property2: number;
}

Якщо дана модель буде використовуватись для опису requestBody, то property2 в ній буде обов'язковою. Але якщо цю модель використовувати для опису параметрів, маркер REQUIRED буде ігноруватись:

class SomeController {
// ...
@oasRoute('GET', 'users', {
parameters: getParams('query', false, Model1, 'property2'),
})
async getSome() {
// ...
}
}

Для опису контента в requestBody та responses існує також хелпер getContent():

import { controller, Status } from '@ditsmod/core';
import { oasRoute, getContent } from '@ditsmod/openapi';

import { SomeModel } from '#models/some.js';

@controller()
export class SomeController {
// ...
@oasRoute('POST', '', {
requestBody: {
description: 'All properties are taken from Model1.',
content: getContent({ mediaType: 'application/json', model: Model1 }),
},
})
async postSome() {
// ...
}
}

Хелпер getContent() приймає скорочену версію даних, коли потрібно описати єдиний варіант mediaType. Якщо ж вам потрібно описати більшу кількість mediaType, можна скористатись класом Content:

import { controller, Status } from '@ditsmod/core';
import { oasRoute, Content } from '@ditsmod/openapi';

import { SomeModel } from '#models/some.js';

@controller()
export class SomeController {
// ...
@oasRoute('GET', '', {
responses: {
[Status.OK]: {
description: 'Опис контенту із даним статусом',
content: new Content()
.set({ mediaType: 'application/xml', model: SomeModel })
.set({ mediaType: 'application/json', model: SomeModel })
.get(),
},
},
})
async getSome() {
// ...
}
}

OpenAPI опції на рівні модуля

Теги та параметри можна передавати на рівні модуля:

import { OasOptions } from '@ditsmod/openapi';

@featureModule({
// ...
extensionsMeta: {
oasOptions: {
tags: ['i18n'],
paratemers: new Parameters()
.optional('query', Params, 'lcl')
.describe('Internalization')
.getParams(),
} as OasOptions,
},
})
export class I18nModule {}

Хелпери, що повертають цілий Operation Object

У попередніх прикладах були показані хелпери, що повертають частини Operation Object, але, звичайно ж, ви можете створити власні хелпери, які повертають цілі Operation Object. Один із прикладів використання таких хелперів показаний в репозиторії RealWorld.

Спеціальний декоратор для ґардів

Модуль @ditsmod/openapi має спеціальний декоратор oasGuard, що дозволяє закріпити метадані OpenAPI за ґардами:

import { CanActivate } from '@ditsmod/core';
import { oasGuard } from '@ditsmod/openapi';

@oasGuard({
tags: ['withBasicAuth'],
securitySchemeObject: {
type: 'http',
scheme: 'basic',
description:
'Enter username: `demo`, password: `p@55w0rd`. For more info see ' +
'[Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)',
},
responses: {
[Status.UNAUTHORIZED]: {
$ref: '#/components/responses/UnauthorizedError',
},
},
})
export class BasicGuard implements CanActivate {
// ...
}

На даний момент декоратор oasGuard приймає наступний тип даних:

interface OasGuardMetadata {
securitySchemeObject: XSecuritySchemeObject;
responses?: XResponsesObject;
tags?: string[];
}

Де securitySchemeObject має тип Security Scheme Object, а responses - Responses Object.

Використовуються такі ґарди точно так само, як і "звичайні" ґарди:

import { controller } from '@ditsmod/core';
import { oasRoute } from '@ditsmod/openapi';

@controller()
export class SomeController {
// ...
@oasRoute('GET', 'users/:username', [BasicGuard])
async getSome() {
// ...
}
}