Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions .claude/commands/audit-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Аудит Zod-схем (DTO) на соответствие Prisma-модели. Аргументы: $ARGUMENTS

## Контекст

Частая проблема: поле в Prisma nullable (`String?`), а в Zod Create-схеме только `.optional()` без `.nullable()`. Это приводит к ошибке валидации, когда клиент отправляет `null`.

Обратная проблема: поле required в Prisma (без `?` и без `@default`), но в Zod помечено `.optional()` — Prisma упадёт при INSERT с неясной ошибкой.

## Шаги

### 1. Определи проект
Prisma схема: `backend/src/prisma/schema.prisma`. DTO: `backend/src/modules/**/*.dto.ts`.

### 2. Прочитай Prisma-схему
Для каждой модели составь карту полей:
- **Nullable:** `Type?` → поле nullable
- **Required:** `Type` без `@default` → поле обязательно при INSERT
- **Default:** `@default(...)` → поле необязательно при INSERT
- **Relation:** `@relation(...)` → пропустить

### 3. Прочитай все DTO-файлы
Для каждой Create/Update схемы проверь каждое поле:

**Категория 1 — Prisma nullable, Zod не nullable:**
Поле `Type?` в Prisma, но в Zod только `.optional()` без `.nullable()`.
Клиент может отправить `null`, Zod отклонит → 400 Bad Request.
Фикс: заменить `.optional()` на `.nullable().optional()`.

**Категория 2 — Prisma required, Zod optional:**
Поле обязательно в БД (`Type` без `?` и без `@default`), но Zod пропускает `undefined`.
Риск: Prisma упадёт при INSERT.
Фикс: убрать `.optional()` или добавить `.default()`.

**Категория 3 — Update-схемы:**
В Update-схемах все поля `.optional()`.
Nullable в Prisma → `.nullable().optional()` (`.partial()` не добавляет `.nullable()` автоматически).

### 4. Составь отчёт

Для каждого нарушения:
```
Файл: backend/src/modules/tasks/tasks.dto.ts
Схема: createTaskDto
Поле: assigneeId
Prisma: String? (nullable)
Zod: z.string().uuid().optional() ← НАРУШЕНИЕ категория 1
Фикс: z.string().uuid().nullable().optional()
```

Если нарушений нет — сообщить "Все схемы соответствуют Prisma-модели ✅".

### 5. Примени фиксы
Исправь все нарушения категорий 1 и 2.
Для nullable полей: всегда `.nullable()` стоит ПЕРЕД `.optional()`.
132 changes: 132 additions & 0 deletions .claude/commands/design-doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
Создай design-doc и spec для задачи перед началом реализации. Аргументы: $ARGUMENTS

## Использование
```
/design-doc "Расширение RBAC до per-feature permissions"
/design-doc gap-22 "Настройки пространства"
```

## Контекст
Design-doc + spec — обязательные артефакты перед реализацией задачи с >3 файлами или новой сущностью/паттерном. Создаются ДО написания кода. Design-doc фиксирует архитектурные решения и живёт в `docs/design/`. Spec описывает поведение в BDD + контракты в SDD и живёт в `specs/`.

## Шаги

### 1. Изучи контекст
- Найди связанные файлы через `gitnexus_query({ query: "{тема}" })`
- Прочитай существующие specs в `specs/existing/` для понимания масштаба
- Если указан gap-номер — прочитай `specs/gaps/gap-{N}-*.md`

### 2. Создай design-doc

Файл: `docs/design/{slug}.md`

```markdown
# Design Doc: {Название}

**Дата:** {дата}
**Статус:** draft
**Spec:** [specs/{path}](../../specs/{path})

## Цель
Одно предложение: что пользователь/система получит в результате.

## Инвентаризация данных
ВСЕ места где затронутая сущность читается или пишется:
- `backend/src/modules/{module}/{file}.service.ts` — что делает
- `backend/src/modules/{module}/{file}.router.ts` — какие эндпоинты
- `frontend/src/api/{file}.ts` — какие вызовы
- `frontend/src/components/{Component}.tsx` — что рендерит

## Карта компонентов
Кто потребляет изменяемые данные (upstream от изменения).

## Модель данных
Prisma-изменения: новые модели, поля, enum-значения, миграции.

## API контракт
Новые или изменённые эндпоинты (метод, путь, DTO shape).

## Стыки
Какие существующие файлы нужно менять и почему.

## Риски и blast radius
Результат `gitnexus_impact()` на ключевые символы.
Что может неожиданно сломаться.

## Решения
Принятые архитектурные решения и их обоснование.
Альтернативы которые рассматривались и почему отклонены.
```

### 3. Создай spec

Для gap-задачи: `specs/gaps/gap-{N}-{slug}.md`
Для новой фичи без gap-номера: `specs/existing/{N}-{slug}.md` со статусом `draft`

```markdown
---
id: {slug}
type: gap-feat | gap-fix | existing
priority: P0 | P1 | P2 | P3
status: draft
design-doc: docs/design/{slug}.md
---

# Spec: {Название}

## Intent
{Одна фраза — ЧТО и ЗАЧЕМ}

## BDD Scenarios

\`\`\`gherkin
Feature: {Название}

Background:
Given {предусловие}

Scenario: {Happy path — основной сценарий}
Given {условие}
When {действие}
Then {результат}

Scenario: {Edge case}
Given {условие}
When {действие}
Then {результат}

Scenario: {Негативный / ошибка}
Given {условие}
When {действие}
Then {ожидаемая ошибка}
\`\`\`

## SDD Contracts

\`\`\`typescript
// Интерфейсы, DTO, API shape — минимально необходимое для реализации
// Пример:
// interface CreateLabelDto { name: string; color: string; workspaceId: string; }
// POST /api/workspaces/:wid/labels → LabelDto
// GET /api/workspaces/:wid/labels → { items: LabelDto[]; total: number }
\`\`\`

## Scope
- {Что входит}

## Out of Scope
- {Что не входит}

## Constraints
- {Технические ограничения, RBAC-правила, существующие паттерны}

## Acceptance Criteria
- [ ] {Проверяемое условие}
- [ ] API: `{method} {path}` → `{ожидаемый ответ}`
- [ ] UI: {действие} → {результат}
```

### 4. Добавь перекрёстные ссылки
- В design-doc уже есть ссылка на spec (шаг 2)
- В spec frontmatter: `design-doc: docs/design/{slug}.md`
- Обнови `specs/README.md` — добавь новый spec в таблицу приоритетов
144 changes: 144 additions & 0 deletions .claude/commands/new-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
Создай новый API-эндпоинт по стандартам проекта. Аргументы: $ARGUMENTS

## Использование
```
/new-api attachments POST
/new-api workspaces/:wid/milestones CRUD
/new-api admin/reports GET
```

## Шаги

### 1. Разбери аргументы
- **Путь:** `tasks/:id/attachments` → модуль `attachments`
- **Методы:** `GET`, `POST`, `PATCH`, `DELETE` или `CRUD` (= GET список + POST + GET один + PATCH + DELETE)
- Если аргументы не указаны — спроси путь и методы

### 2. Impact analysis
```
gitnexus_impact({ target: "{смежный сервис или модуль}", direction: "upstream" })
```
Убедиться что новый эндпоинт не конфликтует с существующими маршрутами.

### 3. Создай Zod DTO

Файл: `backend/src/modules/{module}/{module}.dto.ts`

```typescript
import { z } from 'zod';
import { registry } from '../../shared/openapi/registry.js';

export const create{Entity}Dto = registry.register(
'Create{Entity}',
z.object({
// обязательные поля — без .optional()
name: z.string().min(1).max(255),
// nullable поля из Prisma (Type?) — .nullable().optional()
description: z.string().nullable().optional(),
// FK-поля — .string().uuid()
workspaceId: z.string().uuid(),
})
);

export const update{Entity}Dto = registry.register(
'Update{Entity}',
// .partial() делает все поля .optional(), но не добавляет .nullable()
// для nullable полей Prisma — добавь .nullable() явно
z.object({
name: z.string().min(1).max(255).optional(),
description: z.string().nullable().optional(),
})
);

export const {entity}FiltersDto = z.object({
limit: z.coerce.number().int().min(1).max(100).default(20).optional(),
offset: z.coerce.number().int().min(0).default(0).optional(),
});

export type Create{Entity}Dto = z.infer<typeof create{Entity}Dto>;
export type Update{Entity}Dto = z.infer<typeof update{Entity}Dto>;
```

**Правила DTO:**
- Nullable в Prisma (`Type?`) → `.nullable().optional()` в Zod — всегда
- Update-схемы: все поля `.optional()`, nullable остаются `.nullable().optional()`
- ВСЕГДА оборачивать через `registry.register()` для OpenAPI генерации

### 4. Зарегистрируй пути в OpenAPI

Файл: `backend/src/shared/openapi/routes/{module}.ts` (создай если нет)

```typescript
import { registry } from '../registry.js';
import { create{Entity}Dto } from '../../modules/{module}/{module}.dto.js';

registry.registerPath({
method: 'post',
path: '/api/{path}',
summary: 'Создать {сущность}',
tags: ['{Module}'],
request: {
body: { content: { 'application/json': { schema: create{Entity}Dto } } },
},
responses: {
201: { description: 'Создано' },
400: { description: 'Ошибка валидации' },
401: { description: 'Не авторизован' },
403: { description: 'Нет прав' },
},
});
```

Импортируй новый файл в `backend/src/shared/openapi/registry.ts`.

### 5. Создай роутер

Файл: `backend/src/modules/{module}/{module}.router.ts`

```typescript
import { Router } from 'express';
import { authenticate } from '../../shared/middleware/auth.js';
import { validate } from '../../shared/middleware/validate.js';
import { asyncHandler, authHandler } from '../../shared/utils/async-handler.js';
import { create{Entity}Dto, {entity}FiltersDto } from './{module}.dto.js';
import * as service from './{module}.service.js';

const router = Router({ mergeParams: true });
router.use(authenticate);

router.get('/', validate({entity}FiltersDto, 'query'), authHandler(async (req, res) => {
res.json(await service.list{Entities}(req.user!.userId, req.query as never));
}));

router.post('/', validate(create{Entity}Dto), authHandler(async (req, res) => {
res.status(201).json(await service.create{Entity}(req.user!.userId, req.body));
}));

export { router as {module}Router };
```

### 6. Смонтируй в app.ts
```typescript
import { {module}Router } from './modules/{module}/{module}.router.js';
app.use('/api/{path}', {module}Router);
```

### 7. Напиши тесты

Файл: `backend/src/__tests__/{module}.test.ts`

Минимальный набор сценариев:
- POST → 201 + тело ответа
- POST с невалидными данными → 400 + `details`
- GET список → 200 + массив
- GET несуществующего → 404
- Без токена → 401
- Без прав → 403 (если применимо)

### 8. Чеклист
- [ ] DTO зарегистрирован через `registry.register()`
- [ ] OpenAPI пути зарегистрированы в `routes/{module}.ts`
- [ ] Файл роутов импортирован в `registry.ts`
- [ ] Роутер смонтирован в `app.ts`
- [ ] Тесты написаны
- [ ] `npm run check:rbac` — зелёный
Loading
Loading