2.10.2 • Published 5 months ago

@ws-serenity/api-requests v2.10.2

Weekly downloads
-
License
ISC
Repository
gitlab
Last release
5 months ago

Библиотека запросов к API

Repository

About

Библиотека позволяет выполнять запросы с runtime валидацией, используя описание объекта-валидатора. Основана на runtime-validator`е.

Использование

Настройка

Для начала необходимо настроить мапперы и передать их в apiWithMappers. Библиотека содержит уже несколько стандартных мапперов. Они позволяют не создавать классы для "ручного маппинга" значений. Самостоятельно проверяя значения на указанные правила и создавая нужный тип:

import { ApiDataHandler } from '@ws-serenity/api-requests';
import axios from 'axios';
import { handleEnum } from '@ws-serenity/api-requests';

const api = axios.create({
    baseURL: 'https://some-server.com/api'
    // ...continue axios setup
});
const apiHandlers: ApiDataHandler = {
    typeHandlers: [
        // 'yyyy-MM-dd HH:mm' -> Date instance, но можно указать свой формат, передав его аргументом
        handleDateTime("yyyyy-MM-dd'T'HH:mm:ss"),
        // 'yyyy-MM-dd' -> Date instance
        handleDate(),
        // об enum см далее
        handleEnum(MeetingStatusEnum),
    ],
    keyHandlers: {}
};

const setup = apiWithMappers(api, apiHandlers);

"Под капотом" мапперы имеют следующую структуру, используя тип TypeHandler можно писать свои мапперы:

export const handleDate = (format?: string): TypeHandler<string, Date> => ({
    // тип, к которому применяем маппинг - string | number
    type: 'string',
    // правило, проверяющее, должно ли значение мапиться
    doesMatch: value => isValidDate(mapDate(value, format)),
    // непосредственно перевод из одного типа в другой
    mapper: value => mapDate(value, format),
});

setup - является функцией, которая настраивается для группы запросов на сервис.

// первым параметром передается baseUrl для контроллера
// вторым AxiosRequestConfig
// от этого объекта выполняются непосредственно запросы к api
const meetingService = setup('meeting-service');

Описание типов

interface Meeting {
    id: string;
    title: string;
    description: string | null;
    participantsCount: number;
    labels: string[];
}

// тип сущности для валидации.
// Хорошо подходит, если тип не содержит полей, которые должны быть смаплены (enum и Date)
const meetingType: Validator<Meeting> = tObjectStrict({
    id: tString(),
    title: tString(),
    description: tNullable(tString()),
    participantsCount: tNumber(),
    labels: tArray(tString())
});

если для сущности предполагается маппинг (она содержит поля типа Date или enum) МОЖНО описывать один объект валидации и один объект модели:

export type MeetingStatusType = typeof MeetingStatusEnum;

export const MeetingStatusEnum = {
    SCHEDULED: {
        value: 'SCHEDULED',
        name: 'Запланировано',
    },
    CANCELED: {
        value: 'CANCELED',
        name: 'Отменено',
    },
    DONE: {
        value: 'DONE',
        name: 'Проведено'
    }
// инструкция для TypeScript, что данный объект меняться не будет, чтобы он мог генерировать для него типы
} as const;

// Используется непосредственно в приложении
interface Meeting {
    id: string;
    dateTime: Date;
    status: MeetingStatusType[keyof MeetingStatusType];
}

const apiHandlers: ApiDataHandler = {
    typeHandlers: [
        handleDateTime(),
        handleEnum(MeetingStatusEnum),
    ],
    keyHandlers: {}
};

// Тип не является рантайм-типом. Он просто описывает данные, приходящие НЕПОСРЕДСТВЕННО с API
const meetingType = tObjectStrict({
    id: tString(),
    // мы знаем, что здесь формат будет yyyy-MM-dd HH:mm, поэтому добавляем маппер
    dateTime: tString(),
    // мы знаем, что здесь будет одно из значений: SCHEDULED, DONE, CANCELED. 
    // Оно автоматически смапится в value из MeetingStatusEnum
    status: tString(),
});

FEATURE IN PROGRESS. COMING SOON Если возникает пересечение ключей енума различных сущностей, конкретный маппер можно указать с помощью ApiDataHandler.keyHandlers для запроса

Запросы

Get

import { tNumber, tArray, tNullable, tString, tObjectStrict } from '@ws-serenity/api-requests';

// в начале указывается HTTP-метод с возвращаемым типом
const getRequest = meetingService.get<Meeting>()

// для get-метода доступны запросы на получение:
// модели:
getRequest.model(meetingType)
// пейджированного списка
getRequest.collection(meetingListType)
// массива
getRequest.array(meetingListType)

Для валидации на этом этапе необходимо передать тип, описывающий валидацию с помощью библиотеки "runtime-validator".

// для валидации указывается "тип" интерфейса, непосредственно приходящий с api! 
// Модель (class) писать не нужно! Мапперы автоматически преобразуют типы! (см выше)
getRequest.model(meetingListType);

Указание параметров

Если для запроса необходимо указать запросы, то это можно сделать с помощью последовательного вызова метода with:

modelRequest.with({ headers, body, params })
    // замыкет цепочку вызова метод from, в котором указывается последний фрагмент url, куда выполняется запрос
            .from(meetingId);

// метод with можно опустить, если параметры не требуются
modelRequest.from(meetingId);

Post

Предоставляет все те же самые методы (model, array, collection), а также дополнительный - void - не возвращающий ничего и не требующий валидации

В отличие от метода get завершает отправление запроса вызов метода to, в который аналогично передается последний фрагмент url

meetingService.post<Meeting>()
              .model(meetingType)
              .with({ body: updateMeetingDto })
              .to(`${id}/update`)

Примеры

Таким образом полное получение сущности может выглядеть следующим образом

// meetingActions.ts
// заранее настроенный конфиг для запросов под конкретный проект
import { setup } from './config/api'

// конфиг для запросов на API-сервис
const meetingActions = setup('meeting-service/internal');

export const get = (id: string) =>
    meetingActions.get<Meeting>()
                  .model(meetingType)
                  .from(id);

export const create = (dto: CreateMeetingDto) =>
    meetingActions.post<Meeting>()
                  .model(meetingType)
                  .with({ body: dto })
                  .to('create');

export const list = (params: SearchListParams) =>
    meetingActions.get<MeetingList>()
                  .array(meetingListType)
                  .with({ params })
                  .from('list');

export const setStatus = (id: string, status: MeetingStatus) =>
    meetingActions.post()
                  .void()
                  .to(`${id}/${status}`);
)

Put and Patch

meetingActions.put<Meeting>()
              .with({ body: updateMeetingDto })
              .item(id);

meetingActions.patch<Meeting>()
              .with({ body: updateMeetingDto })
              .item(id);

ApiHandlers

// простой enum, так тоже сработает. Вернет number
export enum AccountStatus {
    ENABLED,
    DISABLED,
}

const enumApiDataHandler: ApiDataHandler = {
    typeHandlers: [
        handleEnum(AccountStatus),
    ],
    keyHandlers: {},
};

// or

// можно передать и сложный объект. Больше не нужно дублировать тип, enum и frontend-сущность-описание
export const AccountStatusDisplay = {
    DISABLED: {
        value: 'DISABLED',
        name: 'Неактивен',
    },
    ENABLED: {
        value: 'ENABLED',
        name: 'Активен',
    },
} as const;

const complexEnumApiDataHandler: ApiDataHandler = {
    typeHandlers: [
        handleEnum(AccountStatusDisplay),
    ],
    keyHandlers: {},
};

Бросать ли error при возникновении ошибки с API:

env-переменная REACT_APP_UNSAFE_PROD=trueвыключает выбрасывание ошибок при валидации API в продакшене (NODE_ENV=production) env-переменная REACT_APP_UNSAFE_DEV=trueвыключает выбрасывание ошибок при валидации API в разработке (NODE_ENV=development) В противном случае ошибки валидации будут выведены в консоль

2.10.2

5 months ago

2.10.1

6 months ago

2.10.0

1 year ago

2.10.17

1 year ago

2.9.16

1 year ago

2.9.17

1 year ago

2.9.15

1 year ago

2.9.14

1 year ago

2.9.13

1 year ago

2.9.12

1 year ago

2.9.11

1 year ago

2.9.10

1 year ago

2.2.9

1 year ago

2.2.8

1 year ago

2.2.7

1 year ago

2.2.6

1 year ago

2.2.5

1 year ago

0.0.1

1 year ago

2.2.3

1 year ago

2.2.2

1 year ago

2.2.1

1 year ago

2.2.0

1 year ago

2.1.5

1 year ago

2.1.4

1 year ago

2.1.2

1 year ago

2.1.1

2 years ago

2.1.0

2 years ago

2.0.7

2 years ago

2.0.6

2 years ago

2.0.5

2 years ago

2.0.4

2 years ago

2.0.3

2 years ago

2.0.2

2 years ago

2.0.1

2 years ago

2.0.0

2 years ago

1.0.0

2 years ago