Skip to content

Пример реализации инфраструктурного слоя на основе @azure-net/kit

Подготовительный этап

Предполагается, что все типы были описаны в доменном слое до этапа создания репозитория.

1. Response

Определяем структуру ответов от бэкенда и реализуем по ней Response. Использовать источники данных без базовых ответов не рекомендуется, так как в любой момент API может измениться или базовый ответ от бэкенда расшириться новыми данными (meta, пагинация и так далее). Базовый класс ответа позволит нам в любой момент дополнить ключи для глобальной общей типизации по всему проекту, а также подстроить все ответы под удобную нам структуру. Поэтому крайне рекомендуется изначально создать базовый ответ, а если потребуется, то возможно создать несколько классов ответов для разных ситуаций или разных API и интеграций.

Базовый ответ рекомендуется создавать в /src/app/core/responses (также можно сгенерировать через CLI, он также использует эту папку, либо папку responses в инфраструктуре выбранного контекста).

Если мы знаем, что глобальных контекстов несколько, а API разный и с разными ответами, то создаём в каждом контексте для каждого свой в /src/app/contexts/{ContextName}/infrastructure/http/responses.

typescript
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).

typescript
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).

typescript
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.

typescript
// Данные пути и алиасы генерируются через 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

Создаём репозиторий с нужными нам методами.

typescript
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.

typescript
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),
		// Остальные репозитории...
	})
});

🎯 Ключевые особенности

  1. Типизированные запросы — полная поддержка TypeScript типов
  2. Гибкие форматы — поддержка различных форматов сериализации
  3. Перехватчики — возможность модификации запросов и ответов
  4. Обработка ошибок — унифицированная система обработки ошибок
  5. Маппинг данных — удобные инструменты для трансформации ответов
  6. Кеширование — встроенная поддержка кеширования через провайдеры

Инфраструктурный слой обеспечивает основу для взаимодействия с внешними сервисами и управления данными в приложении.