Перейти к основному содержимому

Структура модулей

Каждый модуль API следует единообразной внутренней структуре с контроллерами, репозиториями, моделями и помощниками. Понимание этой компоновки позволяет легко ориентироваться в кодовой базе и добавлять новую функциональность в любой модуль.

Перед началом работы

Структура каталогов

Модули находятся в src/modules/{name}/. Типичный модуль содержит четыре каталога:

src/modules/{name}/
├── controllers/ ← Обработчики маршрутов (эндпоинты Express)
├── repositories/ ← Уровень доступа к данным (типизированные SQL-запросы)
├── models/ ← Интерфейсы и типы TypeScript
└── helpers/ ← Бизнес-логика модуля

Например, модуль membership:

src/modules/membership/
├── controllers/
│ ├── PersonController.ts
│ ├── GroupController.ts
│ └── ...
├── repositories/
│ ├── PersonRepo.ts
│ ├── GroupRepo.ts
│ └── ...
├── models/
│ ├── Person.ts
│ ├── Group.ts
│ └── ...
└── helpers/
└── ...

Шесть основных модулей данных — membership, attendance, content, giving, messaging и doing — все следуют этой структуре. Несколько специализированных модулей (таких как reporting, который служит кросс-модульным отчётам и не владеет собственными данными) располагаются рядом с ними в src/modules/.

Одно приложение, много модулей

API — это модульный монолит: модули обозначают границы организации кода и владения данными, а не отдельные сервисы. При запуске контроллеры каждого модуля регистрируются в единый контейнер внедрения зависимостей позади одного приложения Express, поэтому весь API создаётся, работает и развёртывается как единое целое — развёртываемые функции, описанные ниже, — это всё точки входа в одно и то же приложение.

Маршруты каждого модуля находятся под префиксом URL, соответствующим имени модуля:

/membership/*    /attendance/*    /content/*
/giving/* /messaging/* /doing/*

Это держит поверхность API каждого модуля самостоятельной, пока клиенты всё ещё разговаривают с одним хостом.

Контроллеры

Контроллеры определяют маршруты API для модуля. Каждый модуль имеет свой базовый контроллер (например MembershipBaseController), который расширяет общий BaseController — сам построенный на CustomBaseController из @churchapps/apihelper. Маршруты регистрируются с декораторами Inversify.

import express from "express";
import { controller, httpGet } from "inversify-express-utils";
import { MembershipBaseController } from "./MembershipBaseController.js";
import { Permissions } from "../helpers/index.js";

@controller("/membership/people")
export class PersonController extends MembershipBaseController {

@httpGet("/recent")
public async getRecent(req: express.Request, res: express.Response): Promise<any> {
return this.actionWrapper(req, res, async (au) => {
// au = контекст аутентифицированного пользователя
if (!au.checkAccess(Permissions.people.view)) return this.json({}, 401);
return this.repos.person.loadRecent(au.churchId);
});
}
}

actionWrapper аутентифицирует запрос и гидрирует this.repos репозиториями модуля перед выполнением вашего действия.

Декораторы маршрутов

ДекораторHTTP-метод
@httpGet("/path")GET
@httpPost("/path")POST
@httpPut("/path")PUT
@httpPatch("/path")PATCH
@httpDelete("/path")DELETE

Декоратор @controller("/base") задаёт базовый путь для всех маршрутов в контроллере.

Репозитории

Репозитории обрабатывают все операции с базой данных. ORM не используется — запросы пишутся с помощью построителя запросов Kysely, типизированного против схемы базы данных модуля. db/index.ts каждого модуля открывает функцию getDb(), которая возвращает типизированный экземпляр Kysely модуля.

import { injectable } from "inversify";
import { getDb } from "../db/index.js";

@injectable()
export class PersonRepo {
public async load(churchId: string, id: string) {
return getDb().selectFrom("people").selectAll()
.where("id", "=", id)
.where("churchId", "=", churchId)
.executeTakeFirst();
}
}

Внутри контроллера репозитории модуля доступны как this.repos. Вне контроллеров получайте их через RepoManager:

const repos = await RepoManager.getRepos<Repos>("membership");
const people = await repos.person.loadAll(churchId);

Кросс-модульная коммуникация

Каждый модуль владеет своей базой данных (см. База данных), и модуль никогда не запрашивает таблицы другого модуля напрямую. Когда одному модулю нужны данные, принадлежащие другому — например, модуль doing разрешает людей из membership — он проходит через gateway владельца модуля в src/shared/modules/:

import { getMembershipModuleGateway } from "../../../shared/modules/index.js";

const people = await getMembershipModuleGateway().loadPeople(churchId, personIds);

Каждый gateway (MembershipModuleGateway, GivingModuleGateway и так далее) — это интерфейс TypeScript, определяющий ровно какие операции владельца модуля открыта для остального API. Интерфейс — это контракт: текущие реализации читают базу данных владельца модуля в процессе, но поскольку вызывающие зависят только от интерфейса, реализация может быть заменена — например, на ту, которая делает HTTP-вызовы — если модуль когда-либо будет извлечён в отдельный сервис.

Информация

Если данные, которые вам нужны, находятся в другом модуле и его gateway не открывает операцию для них, расширьте интерфейс gateway вместо того чтобы лезть в репозитории или базу данных другого модуля.

Аутентификация и авторизация

JWT-аутентификация

Все запросы аутентифицируются через JWT-токены, обрабатываемые CustomAuthProvider. Токен проверяется автоматически, и контекст аутентифицированного пользователя (au) доступен в каждом действии контроллера.

Проверка разрешений

Используйте au.checkAccess() для проверки наличия необходимого разрешения у текущего пользователя. Разрешения — это предопределённые константы, объединяющие тип содержимого и действие:

au.checkAccess(Permissions.people.view);    // Доступ на чтение
au.checkAccess(Permissions.people.edit); // Доступ на запись

Если пользователю не хватает необходимого разрешения, ответ об ошибке возвращается автоматически.

Предупреждение

Всегда вызывайте au.checkAccess() перед выполнением операций с данными. Никогда не пропускайте проверку разрешений, даже для кажущихся только для чтения эндпоинтов.

Конфигурация окружения

Класс Environment обрабатывает конфигурацию в разных окружениях:

  • Локальная разработка: Чтение из файла .env в корне проекта
  • Развёрнутые окружения: Чтение из AWS SSM Parameter Store
// Доступ к переменным окружения
const jwtSecret = Environment.jwtSecret;
const corsOrigin = Environment.corsOrigin;

Эта абстракция означает, что вашему коду не нужно знать, откуда берётся конфигурация.

Lambda-функции

При развёртывании в AWS API работает как шесть Lambda-функций:

ФункцияНазначение
webОбрабатывает все HTTP REST API-запросы
socketУправляет подключениями WebSocket для функций реального времени
timer15MinЗапланирована каждые 30 минут для уведомлений по электронной почте (имя имеет историческое значение)
timerMidnightЗапланирована ежедневно для дайджестов электронной почты и обслуживания
timerScheduledTasksЗапланирована ежедневно для выполнения автоматизаций и обработки просроченных рабочих процессов
timerWebhooksЗапланирована каждую минуту для доставки очередных исходящих вебхуков
Информация

Локально функция web работает на порту 8084, а функция socket — на порту 8087. Функции таймеров можно запускать вручную во время разработки.

Связанные статьи

  • База данных — строки подключения, скрипты схем и паттерны доступа к данным
  • Локальная установка — полное пошаговое руководство по настройке
  • ApiHelper — общая библиотека, которая предоставляет CustomBaseController и middleware аутентификации