Структура модулей
Каждый модуль API следует единообразной внутренней структуре с контроллерами, репозиториями, моделями и помощниками. Понимание этой компоновки позволяет легко ориентироваться в кодовой базе и добавлять новую функциональность в любой модуль.
Перед началом работы
- Настройте 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 аутентификации