npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ws-serenity/api-requests

v2.10.2

Published

Based on axios library to perform API requests

Downloads

268

Readme

Библиотека запросов к 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) В противном случае ошибки валидации будут выведены в консоль