mirror of
https://github.com/kamask-dev/pchelpmaster.git
synced 2026-11-11 05:49:38 +00:00
feat(shared): add pocketbase di client and domain errors
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014hvR7iG75jK5byaBBHdDLT
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fc55456286
commit
bec31ff02e
+1
-1
@@ -18,7 +18,7 @@
|
||||
| --- | --- |
|
||||
| `guide/project-structure.md` | карта репозитория: `src/app`, `pocketbase/`, `tests/`, `scripts/`; где что искать |
|
||||
| `guide/adding-a-feature.md` | пошаговый пример добавления новой сущности от миграции до экрана (на примере меток) |
|
||||
| `guide/shared.md` | общая инфраструктура: клиент PocketBase, доменные ошибки, общие UI-компоненты |
|
||||
| [guide/shared.md](guide/shared.md) | общая инфраструктура: клиент PocketBase, доменные ошибки, общие UI-компоненты |
|
||||
| `guide/auth.md` | вход, регистрация, сессия, cookie, guard'ы, работа с текущим пользователем |
|
||||
| `guide/tags.md` | метки: репозиторий, диалог управления, компонент выбора |
|
||||
| `guide/clients.md` | клиенты: телефоны, поиск, форма, точки расширения карточки |
|
||||
|
||||
+15
-10
@@ -55,7 +55,8 @@ src/app/orders/
|
||||
|
||||
| Папка | Назначение |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `shared/pocketbase/` | URL PocketBase, фабрика клиента, синхронизация cookie, маппинг ошибок |
|
||||
| `shared/pocketbase/` | URL PocketBase, фабрика клиента, синхронизация cookie |
|
||||
| `shared/errors/` | доменные ошибки и `toDomainError` |
|
||||
| `users/` | сущность User (профиль владельца аккаунта), без логики входа |
|
||||
| `auth/` | сессия: вход, регистрация, выход, текущий пользователь, guard'ы, формы входа/регистрации |
|
||||
| `layout/` | каркас приложения: шапка, навигация, меню пользователя |
|
||||
@@ -117,14 +118,18 @@ providers: [
|
||||
|
||||
## 4. Ошибки
|
||||
|
||||
`shared/pocketbase/` переводит `ClientResponseError` в доменные ошибки, которые понимает UI:
|
||||
Доменные ошибки и `toDomainError(error: unknown): Error` живут в `shared/errors/`. `toDomainError` переводит `ClientResponseError` в доменные ошибки, которые понимает UI:
|
||||
|
||||
| Доменная ошибка | Когда |
|
||||
| ------------------- | -------------------------------------------------------------------------------- |
|
||||
| `NotFoundError` | 404 (в том числе «чужая запись»: PocketBase не различает «нет» и «нет доступа») |
|
||||
| `ValidationError` | 400 с ошибками по полям: `fieldErrors: Record<string, string>` (коды PocketBase) |
|
||||
| `UnauthorizedError` | 401/403: сессия недействительна |
|
||||
| `NetworkError` | нет связи / PocketBase недоступен |
|
||||
| Доменная ошибка | Когда |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `NotFoundError` | 404 (в том числе «чужая запись»: PocketBase не различает «нет» и «нет доступа») |
|
||||
| `ValidationError` | 400 с ошибками по полям: `fieldErrors: Record<string, string>` (коды PocketBase) |
|
||||
| `UnauthorizedError` | 401/403: сессия недействительна |
|
||||
| `NetworkError` | нет связи / PocketBase недоступен (`status === 0`) или запрос прерван |
|
||||
| `InvalidCredentialsError` | неверный email или пароль; создаёт сервис аутентификации, `toDomainError` её не знает |
|
||||
| `UnknownError` | всё остальное, в том числе 400 без ошибок по полям; исходная ошибка — в `cause` |
|
||||
|
||||
Все доменные ошибки наследуют `DomainError`: ошибка, уже являющаяся `DomainError`, возвращается из `toDomainError` как есть. Исходная ошибка PocketBase сохраняется в `cause` у каждой ошибки, которую создаёт `toDomainError`.
|
||||
|
||||
UI показывает ошибки полей рядом с полями, а общие ошибки — в области уведомлений с `role="alert"`.
|
||||
|
||||
@@ -153,7 +158,7 @@ UI показывает ошибки полей рядом с полями, а
|
||||
|
||||
На сервере каждый HTTP-запрос получает **свой экземпляр Angular-приложения и свой root-инжектор**. Всё, что живёт в DI, изолировано. Опасно только то, что живёт **вне DI** — на уровне модуля, общее для всех запросов.
|
||||
|
||||
1. **Нельзя** создавать PocketBase на уровне модуля (`export const pb = new PocketBase(...)`). Экземпляр создаёт только фабрика в `shared/pocketbase/` внутри DI. ESLint-правило запрещает `new PocketBase(` во всех остальных файлах.
|
||||
1. **Нельзя** создавать PocketBase на уровне модуля (`export const pb = new PocketBase(...)`). Экземпляр создаёт только фабрика в `shared/pocketbase/` внутри DI. ESLint запрещает `new PocketBase(` и импорт класса `PocketBase` (в том числе под другим именем и через `pocketbase/*`) во всех остальных файлах; `import type` разрешён.
|
||||
2. **Нельзя** хранить пользовательские данные в переменных модуля, статических полях классов, кэшах вне DI.
|
||||
3. Сервер рендерит страницы с заголовками `Cache-Control: private, no-store` и `Vary: Cookie`: ни браузер, ни прокси не должны отдать одну страницу двум людям.
|
||||
4. На сервере используется глобальный `fetch` без общего состояния; `authStore` — свой у каждого экземпляра PocketBase (не `LocalAuthStore`, а in-memory `BaseAuthStore`).
|
||||
@@ -168,7 +173,7 @@ PocketBase SDK использует `fetch`, а не `HttpClient`, поэтом
|
||||
|
||||
- **Сервер:** `PB_URL` — переменная окружения Node-сервера, внутренний адрес PocketBase для серверного рендера (по умолчанию `http://127.0.0.1:8090`). В браузер не передаётся.
|
||||
- **Браузер:** `<window.location.origin>/pb`. Префикс `/pb` — константа `PB_PUBLIC_PATH` в `shared/pocketbase/`; если путь в nginx поменяется, меняется только она.
|
||||
- Всё это задаётся в одном месте: `shared/pocketbase/` (InjectionToken `PB_URL`, разные провайдеры для сервера и браузера).
|
||||
- Всё это задаётся в одном месте: `shared/pocketbase/`. Клиент `POCKETBASE` создаётся одной и той же фабрикой и в браузере, и на сервере (`providePocketBase()` в `app.config.ts`); различается только токен `PB_URL`: `providePocketBase()` даёт браузерное значение по умолчанию, а `providePbUrlForServer()` в `app.config.server.ts` подменяет его серверным.
|
||||
- **Разработка:** `ng serve` проксирует `/pb/*` → `PB_URL` со срезанием префикса (`proxy.conf.mjs`). E2E-тесты production-сборки используют маленький тестовый фронт-прокси, повторяющий правила nginx.
|
||||
|
||||
### Ожидаемая конфигурация nginx (пример, деплой — вне MVP)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Общая инфраструктура (`shared/`)
|
||||
|
||||
Клиент PocketBase, который можно получить только через DI, и доменные ошибки, в которые переводятся ошибки SDK.
|
||||
|
||||
## Где лежит
|
||||
|
||||
```
|
||||
src/app/shared/
|
||||
├── pocketbase/
|
||||
│ ├── pb-url.ts # PB_PUBLIC_PATH, токен PB_URL, серверный провайдер
|
||||
│ └── pocketbase-client.ts # токен POCKETBASE, фабрика, providePocketBase()
|
||||
└── errors/
|
||||
├── domain-errors.ts # DomainError (база), NotFoundError, ValidationError, ...
|
||||
└── to-domain-error.ts # toDomainError(error: unknown): Error
|
||||
```
|
||||
|
||||
## Публичный API
|
||||
|
||||
- `PB_URL` — `InjectionToken<string>`: базовый адрес PocketBase. В браузере `<origin>/pb`, на сервере `process.env['PB_URL']` или `http://127.0.0.1:8090`.
|
||||
- `PB_PUBLIC_PATH` — `'/pb'`, путь, по которому браузер обращается к PocketBase.
|
||||
- `providePbUrlForServer()` — серверный провайдер `PB_URL`; регистрируется в `app.config.server.ts` и перекрывает браузерный.
|
||||
- `POCKETBASE` — `InjectionToken<PocketBase>`: свой экземпляр на каждый инжектор приложения (на сервере — на каждый запрос). Фабрика одна и та же в браузере и на сервере.
|
||||
- `providePocketBase()` — регистрирует `POCKETBASE` и `PB_URL` по умолчанию (браузерный: `<origin>/pb`); вызывается в `app.config.ts`, поэтому действует и в браузере, и на сервере. Возвращает `EnvironmentProviders`: в `providers` компонента его подключить нельзя, иначе у поддерева был бы свой клиент с пустым `authStore`.
|
||||
- `toDomainError(error)` и классы ошибок из `domain-errors.ts` — таблица соответствий в [architecture.md, раздел 4](../architecture.md).
|
||||
|
||||
## Как пользоваться
|
||||
|
||||
```ts
|
||||
// data/ слоя фичи
|
||||
private readonly pb = inject(POCKETBASE);
|
||||
|
||||
async list(): Promise<Tag[]> {
|
||||
try {
|
||||
return (await this.pb.collection('tags').getFullList()).map(toTag);
|
||||
} catch (error) {
|
||||
throw toDomainError(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Как расширить или заменить
|
||||
|
||||
- В тестах вместо клиента подставляется `{ provide: POCKETBASE, useValue: ... }` или тестовый `PB_URL`.
|
||||
- `InvalidCredentialsError` создаёт сервис аутентификации; `toDomainError` её не знает.
|
||||
|
||||
## Тесты
|
||||
|
||||
- `to-domain-error.spec.ts`, `pocketbase-client.spec.ts` — unit.
|
||||
- `pocketbase-client.integration.spec.ts` — клиент из DI против тестового PocketBase, изоляция двух пользователей и гостя.
|
||||
- `tests/lint/eslint-rules.spec.ts` — границы ESLint, см. [testing.md](../testing.md).
|
||||
|
||||
## Подводные камни
|
||||
|
||||
- `new PocketBase(` и импорт класса `PocketBase` (в том числе из `pocketbase/*`) разрешены только в фабрике `pocketbase-client.ts` (и в `tests/`; `import type` можно везде кроме `domain/` и `ui/`); импорт `pocketbase` и `pocketbase/*` запрещён в `domain/` и `ui/`. См. [architecture.md, раздел 6](../architecture.md).
|
||||
- Новую доменную ошибку наследуйте от `DomainError`, а не от `Error`: только так `toDomainError` узнаёт её и возвращает как есть. Иначе она будет завёрнута в `UnknownError`, и UI её не распознает.
|
||||
- `authStore` — `BaseAuthStore` в памяти, не `LocalAuthStore`: на сервере иначе данные пользователей были бы общими.
|
||||
- Автоотмена запросов SDK выключена: параллельные запросы одного типа не отменяют друг друга.
|
||||
- Серверный `PB_URL` — внутренний адрес; он не попадает в браузер, браузер ходит через `/pb`. См. [architecture.md, раздел 7](../architecture.md).
|
||||
+9
-1
@@ -35,7 +35,15 @@
|
||||
|
||||
Почему цель builder'а, а не отдельный `vitest.integration.config.ts` с плагином: спеки собираются тем же компилятором Angular и с той же инициализацией TestBed, что и в `pnpm test`, без своего плагина компиляции и его настройки.
|
||||
|
||||
`tests/support/clients.ts` компилируется в двух программах — `tsconfig.tests.json` и `tsconfig.spec.json`. Поэтому в нём нет `node:*` (во второй программе нет типов Node), а `tests/support/provided-context.d.ts` с типами `inject('pbUrl')` перечислен в `include` обоих tsconfig: `references` в `tsconfig.json` программы не объединяют.
|
||||
`tests/support/clients.ts` компилируется в двух программах — `tsconfig.tests.json` и `tsconfig.spec.json`. Типы Node подключены в обеих (спеки выполняются Vitest в Node, а код в `src/` вроде серверного провайдера `PB_URL` читает `process.env`), а `tests/support/provided-context.d.ts` с типами `inject('pbUrl')` перечислен в `include` обоих tsconfig: `references` в `tsconfig.json` программы не объединяют.
|
||||
|
||||
## Тесты конфига ESLint
|
||||
|
||||
`pnpm test:lint` = `tsc -p tsconfig.tests.json && vitest run --config vitest.lint.config.mts`: чистый Vitest в Node, файлы `tests/lint/**/*.spec.ts`.
|
||||
|
||||
- Проверяют, что ESLint держит границы, от которых зависит изоляция данных при SSR: `new PocketBase(` и импорт класса `PocketBase` (в том числе под другим именем и через `pocketbase/*`) только в фабрике `shared/pocketbase/pocketbase-client.ts` (и в тестовых хелперах `tests/`), импорт `pocketbase` и `pocketbase/*` запрещён в `domain/` и `ui/`.
|
||||
- Способ: `ESLint#lintText(code, { filePath })` на коротких фрагментах кода, проверяется `ruleId` сообщений. Конфиг ESLint ломается молча (опечатка в glob, слишком широкое исключение), и `pnpm lint` при этом остаётся зелёным; тест это ловит.
|
||||
- Запускаются первым шагом в `pnpm test`, `pnpm test:api` и `pnpm test:integration`, чтобы поломка правила обнаружилась при любом прогоне. ESLint — Node-библиотека, в бандл builder'а Angular её не подключить, поэтому это отдельный набор.
|
||||
|
||||
## Контрактные тесты репозиториев
|
||||
|
||||
|
||||
Reference in New Issue
Block a user