Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
a346e19
chore: подчистить мёртвые цели, тест и метаданные пакета
mokevnin Aug 23, 2026
456fb2e
fix(auth): применять авторизацию по спеке, а не руками в обработчиках
mokevnin Aug 23, 2026
a2763f6
fix(auth): проверять пароль в /tokens и починить ensure()
mokevnin Aug 23, 2026
9c959ea
fix(courses): проверять владение курсом, а не только наличие токена
mokevnin Aug 23, 2026
cbf30e4
fix(db): вести таймстемпы через drizzle, а не SQL-дефолтами
mokevnin Aug 23, 2026
150e9b6
perf(test): вынести цену scrypt в окружение и снизить её в тестах
mokevnin Aug 23, 2026
ad49b93
feat(config): конфиг из окружения по схеме, security-плагины и док-ст…
mokevnin Aug 23, 2026
8928069
ci: выпускать релизы, проверять миграции и покрытие
mokevnin Aug 23, 2026
670cb48
ci: линтовать спеку, показывать ломающие правки и гонять формат до ко…
mokevnin Aug 23, 2026
8be1ea7
fix: закрыть пять 5xx и дыры контракта, найденные контрактными тестами
mokevnin Aug 23, 2026
ee28742
test: ходить в API сгенерированным из спеки клиентом
mokevnin Aug 23, 2026
3c6252a
docs: привести README и AGENTS.md в соответствие с кодом
mokevnin Aug 23, 2026
f7ce1fd
docs: не предлагать cp .env.example .env
mokevnin Aug 23, 2026
e310b8b
fix(ci): передавать oasdiff базовую спеку через workspace, а не /tmp
mokevnin Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Скопируйте в .env: `cp .env.example .env`.
# Схема проверяется на старте в plugins/env.ts — без JWT_SECRET приложение не
# поднимется.

# Минимум 32 символа. Сгенерировать: openssl rand -hex 32
JWT_SECRET=replace-me-with-at-least-32-characters

# Необязательные, значения по умолчанию в схеме.
# NODE_ENV=development
# CORS_ORIGIN=*
# RATE_LIMIT_MAX=100
43 changes: 43 additions & 0 deletions .github/workflows/contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Contract Tests

# Отдельным workflow, а не шагом в Node CI: инструмент внешний по отношению к
# проекту (python-овый schemathesis через uv), и выкинуть его должно быть
# просто — удалением одного файла и цели contract-test в Makefile.
#
# Проверяет то, чего не видят ни tsc, ни валидаторы fastify: применена ли
# авторизация, не отдаёт ли API статусы, которых нет в контракте, и не падает
# ли он в 5xx на краевых входах.

on:
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
schemathesis:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v6

- uses: actions/setup-node@v7
with:
node-version: 26.x
cache: pnpm

- uses: astral-sh/setup-uv@v7

- name: Install
run: make install

- name: Run contract tests
run: make contract-test
14 changes: 11 additions & 3 deletions .github/workflows/nodeci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
name: Node CI

# На каждое событие ровно один workflow: здесь нет release-please, поэтому
# main проверяет Node CI.
on:
push:
branches:
Expand All @@ -10,6 +8,12 @@ on:
branches:
- main

# Пуш в ветку с открытым PR запускает сборку дважды. Прошлый прогон отменяется,
# кроме main: там история сборок нужна целиком.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
build:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -45,5 +49,9 @@ jobs:
# руками.
- name: Check generated code
run: make generate-check
# Схема без миграции — молчаливая поломка: код ждёт колонку, которой в
# базе не появится.
- name: Check migrations
run: make migration-check
- name: Run tests
run: make test
run: make test-coverage
39 changes: 39 additions & 0 deletions .github/workflows/openapi-diff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: OpenAPI Diff

# Информационный шаг: показывает в логе PR, что изменилось в контракте и есть
# ли ломающие правки. Сборку намеренно не роняет — v1 нигде не опубликован, и
# жёсткий гейт обещал бы совместимость, которой репозиторий не даёт. Когда
# спека станет публичной, достаточно убрать continue-on-error.
on:
pull_request:
branches:
- main

permissions:
contents: read

jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

# Файл кладётся в рабочую копию, а не в /tmp: oasdiff запускается
# docker-действием, и внутрь контейнера монтируется только workspace —
# из /tmp он базовую спеку не увидит.
- name: Extract the base version of the spec
run: |
mkdir -p .oasdiff
git show "origin/${{ github.base_ref }}:tsp-output/@typespec/openapi3/openapi.v1.json" \
> .oasdiff/base.json \
|| cp tsp-output/@typespec/openapi3/openapi.v1.json .oasdiff/base.json
test -s .oasdiff/base.json

- name: Report breaking changes
continue-on-error: true
uses: oasdiff/oasdiff-action/breaking@main
with:
base: .oasdiff/base.json
revision: tsp-output/@typespec/openapi3/openapi.v1.json
4 changes: 2 additions & 2 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: PR Title

# Заголовок PR обязан быть conventional commit: при squash-мерже он становится
# сообщением коммита, а по нему release-please определяет разряд версии.
# Отдельные коммиты ветки намеренно не проверяются.
# сообщением коммита, а по нему release-please (см. release-please.yml)
# определяет разряд версии. Отдельные коммиты ветки намеренно не проверяются.
on:
pull_request_target:
types:
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Release Please

# Вторая половина того, что уже настроено: pr-title.yml требует conventional
# commit в заголовке PR именно затем, чтобы по нему определялся разряд версии.
# Без этого workflow заголовки проверялись, но ничего не выпускали.
on:
push:
branches:
- main

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ profile*
*clinic*
*flamegraph*
.env

# кеш schemathesis (make contract-test)
.schemathesis

# базовая спека для oasdiff в CI
.oasdiff
78 changes: 52 additions & 26 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,65 @@

## Project Structure & Module Organization

- `app.js`: Fastify entry; autoloads `plugins/` and `routes/`.
- `routes/`: HTTP handlers.
- `plugins/`: App plugins (JWT auth, DB, sensible errors, response validation, route glue).
- `db/`: Drizzle ORM schema and seeds; `drizzle/` holds generated migrations.
- `lib/`: Utilities and test data builders.
- `test/`: Node tests; helpers in `test/helper.js`, route specs in `test/routes/*.test.js`.
- `types/`: ts types
- `main.tsp`, `tsp-output/`: TypeSpec and generated OpenAPI/handler typings.
- `app.ts`: Fastify entry; автозагружает `plugins/`, а маршруты регистрирует
`fastify-openapi-glue` по спеке. Здесь же обработчик ошибок (RFC 9457) и
securityHandlers.
- `main.tsp`, `tsp-output/`: контракт на TypeSpec и сгенерированный из него
OpenAPI. Источник истины для всего остального.
- `types/handlers/`: сгенерированное из OpenAPI — типы обработчиков, zod-схемы
и клиент. Руками не правится.
- `routes/`: обработчики; `routes/index.ts` собирает их в полный
`RouteHandlers`, поэтому забытая операция — ошибка компиляции.
- `plugins/`: конфиг (`env`), база, JWT, security-плагины, документация.
- `db/`: схема Drizzle, сиды и публичные проекции; `drizzle/` — миграции.
- `validators/`, `rules/`, `policies/`: бизнес-валидация, правила уровня базы,
права доступа.
- `lib/`: утилиты, хеширование паролей, фабрики тестовых данных.
- `test/`: спеки; бутстрап в `test/helper.ts`.
- `scripts/`: contract-test.sh.

## Build, Test, and Development Commands
- `npm run dev`: Start Fastify with watch on http://localhost:3000.
- `npm start`: Start in production mode.
- `npm test` or `make test`: Run Node tests (`node --test`).
- `make lint` / `make lint-fix`: Lint and autofix.
- `make check-types`: Type-check with `tsc` (JS + d.ts).
- `make generate-types`: Compile TypeSpec and generate Fastify handler types.
- `make migration-generate`: Generate Drizzle migrations.
- `make mock`: Serve mocked API from generated OpenAPI.

- `make install` — установка.
- `make dev` — запуск с перезагрузкой на http://localhost:3000. Требует `.env`
(см. `.env.example`).
- `make test` / `make test-coverage` — vitest, второй с порогами покрытия.
- `make lint` / `make lint-fix` — oxlint, tsc, формат и линт спеки.
- `make generate-types` — OpenAPI из TypeSpec и всё сгенерированное из него.
- `make generate-check` — сгенерированное не разошлось со спекой.
- `make migration-generate` / `make migration-check` — миграции и проверка, что
схема не менялась без миграции.
- `make contract-test` — schemathesis по спеке (нужен uv).
- `make mock` — мок-сервер по OpenAPI.

## Coding Style & Naming Conventions
- **Modules**: ESM only (`type: module`). Prefer named exports; keep JSDoc types consistent with `types/`.
- **Formatting**: 2-space indent, no semicolons; follow ESLint + `@stylistic` rules. Run `make lint` before committing.
- **Files**: Group endpoints by resource in `routes/api/` (e.g., `routes/api/books.js`). Co-locate validators and serializers by domain when present.

- **Модули**: ESM (`type: module`), TypeScript, импорты с расширением `.ts`.
- **Формат**: oxfmt, линт oxlint. `make lint` перед коммитом; lefthook гоняет
формат и линт по staged-файлам автоматически.
- **Контракт первичен**: новое поле, статус или операция сначала появляются в
`main.tsp`, потом `make generate-types`, потом код. Обратный порядок ловится
в CI.

## Testing Guidelines
- **Framework**: Node built-in `node:test` with `app.inject()`; see `test/helper.js` for server bootstrap.
- **Naming**: Place specs under `test/routes/` as `*.test.js` (e.g., `test/routes/users.test.js`).
- **Scope**: Add success tests for each new/changed route. No coverage gate enforced.

- **Фреймворк**: vitest. Спеки в `test/**/*.test.ts`.
- **Клиент**: корректные запросы — через сгенерированный клиент
(`buildClient()`), нарушающие контракт — через `app.inject()` (`build()`):
клиент типизирован по спеке и выразить их не даёт.
- **Покрытие**: пороги в `vitest.config.ts`, проверяются в CI.
- **Контрактные тесты**: `make contract-test` генерирует запросы из OpenAPI —
им найдены почти все 5xx, которые здесь исправлены.

## Commit & Pull Request Guidelines
- **Commits**: Use clear, imperative messages (optionally Conventional Commits). Reference issues when applicable.
- **PRs**: Provide purpose, summary, linked issues, test plan, and example requests/responses (curl or HTTPie). Keep diffs focused.

- **Коммиты**: conventional commits.
- **PR**: заголовок обязан быть conventional commit — по нему release-please
определяет разряд версии (проверяется в `pr-title.yml`).

## Security & Configuration Tips
- **Secrets**: Move JWT secret to env (e.g., `JWT_SECRET`) rather than hardcoding; use `.env` locally and never commit secrets.
- **DB**: Current DB is in-memory SQLite (`plugins/drizzle.js`). Switch to file/real DB for persistence before production.

- **Секреты**: конфиг проверяется схемой в `plugins/env.ts`. Без `JWT_SECRET`
от 32 символов приложение не поднимается. `.env` не коммитится.
- **База**: in-memory SQLite (`plugins/drizzle.ts`), пересоздаётся при каждом
запуске. Для постоянного хранения нужен файл или настоящая СУБД.
46 changes: 40 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,25 +1,55 @@
test:
pnpm test

test-coverage:
pnpm exec vitest run --coverage

dev:
pnpm run dev

check-types:
pnpm exec tsc

# ncu, а не npx: пакет стоит в devDependencies, и npx при его отсутствии молча
# тянул бы из сети другую версию. Обычно обновления приносит dependabot — цель
# нужна, когда хочется обновиться сразу и локально.
deps-update:
npx ncu -u
pnpm exec ncu -u

# Таблица маршрутов целиком: их регистрирует glue по спеке, отдельного файла
# с маршрутами нет — печатать нужно приложение.
routes:
pnpm exec fastify print-routes routes/api/users.js
pnpm exec fastify print-routes app.ts

migration-generate:
pnpm exec drizzle-kit generate

# Схема без миграции — молчаливая поломка: код ждёт колонку, которой в базе не
# появится. Цель ловит это, перегенерировав и проверив, что ничего нового не
# возникло. В generate-check миграции не входят намеренно: там речь про
# сгенерированное из спеки, а миграцию автор создаёт осознанно.
#
# check вызывается флагами, а не через конфиг: читая drizzle.config.ts, он
# принимает dialect за параметр AWS Data API и падает (drizzle-kit 0.31).
migration-check:
pnpm exec drizzle-kit check --dialect sqlite --out ./drizzle
pnpm exec drizzle-kit generate
@test -z "$$(git status --porcelain drizzle)" || { \
echo "Схема изменилась без миграции — запустите make migration-generate:"; \
git status --porcelain drizzle; \
exit 1; \
}

lint:
pnpm --silent run lint
pnpm exec tsc
pnpm --silent run format:check
$(MAKE) lint-openapi

# Линт контракта: правила и причины отключений — в redocly.yaml. Гоняется по
# сгенерированному, а не по main.tsp: проверять надо то, что видит клиент.
lint-openapi:
pnpm exec redocly lint tsp-output/@typespec/openapi3/openapi.v1.json

lint-fix:
pnpm --silent run lint:fix
Expand All @@ -44,12 +74,16 @@ generate-types: generate-openapi generate-openapi-ts-types
generate-check: generate-types
git diff --exit-code -- tsp-output types/handlers

# Контрактные тесты поверх спеки: см. комментарий в самом скрипте.
contract-test:
./scripts/contract-test.sh

mock:
pnpm exec prism mock ./tsp-output/@typespec/openapi3/openapi.v1.json

tsp-build:

.PHONY: test routes

install:
pnpm install

.PHONY: install test dev check-types deps-update routes migration-generate \
lint lint-fix generate-openapi generate-openapi-ts-types generate-types \
generate-check migration-check mock test-coverage lint-openapi contract-test
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,37 @@ REST API на [Fastify](https://fastify.dev/), собранный «по-взр
- **База через [Drizzle](https://orm.drizzle.team/)**: схема в `db/schema.ts`,
миграции генерируются по ней.
- **Валидация входа** отдельным слоем в `validators/`, а не внутри обработчика.
- **Авторизация тоже из спеки.** `@useAuth` в контракте применяет
`fastify-openapi-glue` через securityHandlers, а не `jwtVerify()` в каждом
обработчике — забыть его негде.
- **Спека проверяется снаружи.** `make contract-test` натравливает
[schemathesis](https://schemathesis.readthedocs.io/) на поднятое приложение:
тот генерирует запросы из OpenAPI и ловит то, что не видят ни tsc, ни
валидаторы — незадокументированные статусы, 5xx на краевых входах и
неприменённую авторизацию.

## Запуск

```bash
make install
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env # см. .env.example
make dev
make test
```

Документация — на http://localhost:3000/docs, сама спека — на `/openapi.json`.

Полезное:

```bash
make routes # список маршрутов
make migration-generate # миграция по изменённой схеме
make migration-check # схема не менялась без миграции
make generate-types # OpenAPI и типы из TypeSpec
make generate-check # проверить, что сгенерированное закоммичено
make lint-openapi # линт контракта
make contract-test # schemathesis по спеке (нужен uv)
make test-coverage # тесты с порогами покрытия
make mock # поднять мок-сервер по OpenAPI
```

Expand Down
Loading
Loading