Пример реализации инфраструктурного слоя на основе @azure-net/kit
Подготовительный этап
Предполагается, что все типы были описаны в доменном слое до этапа создания репозитория.
1. Response
Определяем структуру ответов от бэкенда и реализуем по ней Response. Использовать источники данных без базовых ответов не рекомендуется, так как в любой момент API может измениться или базовый ответ от бэкенда расшириться новыми данными (meta, пагинация и так далее). Базовый класс ответа позволит нам в любой момент дополнить ключи для глобальной общей типизации по всему проекту, а также подстроить все ответы под удобную нам структуру. Поэтому крайне рекомендуется изначально создать базовый ответ, а если потребуется, то возможно создать несколько классов ответов для разных ситуаций или разных API и интеграций.
Базовый ответ рекомендуется создавать в /src/app/core/responses (также можно сгенерировать через CLI, он также использует эту папку, либо папку responses в инфраструктуре выбранного контекста).
Если мы знаем, что глобальных контекстов несколько, а API разный и с разными ответами, то создаём в каждом контексте для каждого свой в /src/app/contexts/{ContextName}/infrastructure/http/responses.
import { ResponseBuilder } from '@azure-net/kit/infra';
export interface IBackendApiDataSourceResponse<T = unknown> {
data: T;
success: boolean;
message: string;
}
export class AzureNetApiResponse<TData = unknown, TMeta = unknown> extends ResponseBuilder<TData, TMeta, IBackendApiDataSourceResponse<TData>> {
// Распакуем data чтобы постоянно не таскать `data.data`
override unwrapData(data: IBackendApiDataSourceResponse<TData>): TData {
return data.data;
}
// Представим ситуацию, что на бэкенде Yii2, который из коробки отдаёт пагинацию в заголовках (ключи в мете при вызове метода автотипизируются)
paginate() {
return this.addMeta({ page: Number(this.response.headers.page), total: Number(this.response.headers.total) });
}
}2. Datasource
Источник данных нам нужен для того, чтобы использовать переданный в него httpService с базовым URL, queryBuilder, который поможет нам обрабатывать query, и для того, чтобы определить метод, в котором мы будем использовать наш response. В названии метода ограничений нет, но он должен использовать createRawRequest.
Базовый источник данных рекомендуется создавать в /src/app/core/datasources (также можно сгенерировать через CLI, он также использует эту папку, либо папку datasources в инфраструктуре выбранного контекста).
Как и с response, мы можем создавать различные источники данных в различных контекстах (/src/app/contexts/{ContextName}/infrastructure/http/datasources).
import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
import { AzureNetApiResponse, type IBackendApiDataSourceResponse } from '../responses/AzureNetApiResponse';
export class AzureNetHttpDatasource extends BaseHttpDatasource {
async createRequest<T>(callback: CreateRequestCallbackType<IBackendApiDataSourceResponse<T>>) {
return new AzureNetApiResponse<T, unknown>(await this.createRawRequest<IBackendApiDataSourceResponse<T>>(callback));
}
}3. Provider for datasource
Теперь у нас есть готовая база для того, чтобы делать запросы, и мы должны создать провайдер, чтобы предоставить доступ к источнику данных другим провайдерам.
Provider для источника данных, если он один, рекомендуется создавать в /src/app/core/providers. В случае различных источников данных под разные контексты создаём в контекстах (/src/app/contexts/{ContextName}/infrastructure/providers).
import { createBoundaryProvider, UniversalCookie } from '@azure-net/kit';
import { HttpService } from '@azure-net/kit/infra';
// Алиас и экспорт сгенерированы с помощью CLI
import { AzureNetRestDatasource } from '$core';
export const DatasourceProvider = createBoundaryProvider('CoreDatasourceProvider', {
register: () => ({
AzureNetRestDatasource: () =>
new AzureNetRestDatasource({
http: new HttpService({
baseUrl: `https://api.example.ru`,
onRequest: (options) => {
// отправляем заголовок авторизации если токен в куке есть с каждым запросом
const token = UniversalCookie.get('token');
if (token) {
options.headers = { ...options.headers, Authorization: `Bearer ${token}` };
}
}
})
// Если мы не передаем QueryBuilder, то будет использоваться стандартный со стандартными настройками
})
})
});4. DTO (Необязательно)
Если у нас случай, когда наша доменная модель отличается от того, что прилетает к нам с бэка, а нам в приложении надо по-своему, то мы можем создать DTO и привести всё к нужному нам формату. Количество DTO не ограничено, рекомендуется создавать папки с названиями агрегатов внутри папки с DTO на всякий случай /src/app/contexts/{ContextName}/infrastructure/http/dto.
// Данные пути и алиасы генерируются через CLI, вы свободны выбирать, если не используете CLI
import type {IUser, IUserFromBack} from '${ContextName}/domain/user'; // IUserFromBack папка ports, IUser папка model
class UserDTO extends DTOMapper<IUser> {
id: number;
name: string;
last_name: string;
// представим ситуацию, что мы везде выводим только full_name и не хотим постоянно писать одно и то же в коде, а с бэка летит first_name и last_name
full_name: string;
constructor(data: IUserFromBack) {
super();
this.id = data.id;
this.name = data.first_name;
this.last_name = data.last_name;
this.full_name = this.getFullName();
}
getFullName() {
return `${this.name} ${this.last_name}`;
}
}5. Repository
Создаём репозиторий с нужными нам методами.
import { AzureNetRestDatasource } from '$core';
import { UserDTO } from '../dto/user';
import type { ILoginRequest, ILoginResponse } from '${ContextName}/domain/auth'; // папка ports
import type { IUserFromBack } from '${ContextName}/domain/user'; // папка ports
export class AuthRepository {
constructor(private azureNetRestDatasource: AzureNetRestDatasource) {}
public async login(request: ILoginRequest) {
return this.azureNetRestDatasource
.createRequest<ILoginResponse>(({ http }) => http.post('/auth/login', { json: request }))
.then((res) => res.getData());
}
// То, что вернёт метод, автотипизируется как Promise<IUser>
public async current() {
return this.azureNetRestDatasource.createRequest<IUserFromBack>(({ http }) => http.get('/auth/current')).then((res) => res.mapUsing(UserDTO).getData());
}
public async logout() {
return this.azureNetRestDatasource.createRequest<void>(({ http }) => http.post('/auth/logout')).then((res) => res.getData());
}
}6. Provider for infrastructure
Последним этапом создаём провайдер для инфраструктуры, инжектим в него DatasourceProvider и регистрируем репозиторий, чтобы предоставить его в application слой. После этого этапа мы можем работать в application слое с этим репозиторием, как только заинжектим в ApplicationProvider наш InfrastructureProvider.
import { createBoundaryProvider } from '@azure-net/kit';
import { DatasourceProvider } from '$core';
import { AuthRepository } from '../http/repositories';
export const InfrastructureProvider = createBoundaryProvider('{ContextName}InfrastructureProvider', {
dependsOn: { DatasourceProvider },
register: ({ DatasourceProvider }) => ({
AuthRepository: () => new AuthRepository(DatasourceProvider.AzureNetRestDatasource),
// Остальные репозитории...
})
});🎯 Ключевые особенности
- Типизированные запросы — полная поддержка TypeScript типов
- Гибкие форматы — поддержка различных форматов сериализации
- Перехватчики — возможность модификации запросов и ответов
- Обработка ошибок — унифицированная система обработки ошибок
- Маппинг данных — удобные инструменты для трансформации ответов
- Кеширование — встроенная поддержка кеширования через провайдеры
Инфраструктурный слой обеспечивает основу для взаимодействия с внешними сервисами и управления данными в приложении.