diff --git a/template/mock-backend/AGENTS.md b/template/mock-backend/AGENTS.md new file mode 100644 index 0000000..9110330 --- /dev/null +++ b/template/mock-backend/AGENTS.md @@ -0,0 +1,167 @@ +# MSW Mocks – Agent Guide + +This document captures the conventions for adding and evolving the MSW handlers in this repo. Follow these guidelines to keep mocks predictable, type-safe, and realistic. + +Related docs +- `../mock-data/AGENTS.md` — mock dataset structure, generators and presets. +- `../AGENTS.md` — project overview and how mocks integrate in dev/tests. +- `../mock-data/data-mocks/mock-data.small.json` — A small and representative example of the dataset that is passed to the generate functions. + +## Core Principles + +- Type safety first: use the http method generics for Params, RequestBody, ResponseBody but not Path since this is inferred. +- Never read cookies directly: always call `auth(request.headers)` or `auth(request.headers, 'admin')`. +- Prefer early auth failure: use `auth(request.headers, 'admin')` on admin-only routes to avoid manual checks. +- Keep responses realistic: return 404 for missing resources and 403 for RBAC violations, and add small `serverDelay(...)`. +- Mutations can and should update the in-memory dataset in-place, including reverse indices. +- Use absolute URLs for paths: `http://localhost:3000/api/v1/...`. +- You can throw error responses like 403 with custom bodies instead of returning them to avoid type errors. + +The code for the api layer that is consuming the mock API is defined in src/api. + +## Imports You’ll Typically Need + +- Dataset and types + - `import type { Dataset } from '@/mock-data/dataset'` + - Domain types from `src/types/* +- Helpers + - `import { serverDelay } from '@/mock-backend/utils'` + - `import type { AuthFunction } from '@/mock-backend/utils'` + +## Handler Shape and Exports + +- Group related handlers in a `getXxxHandlers(dataset, auth, addDelay)` function that returns a plain object. +- Export `xxxHandlers(dataset, auth, addDelay)` as an array: `Object.values(getXxxHandlers(...))`. + +Example skeleton: + +``` +function getWidgetHandlers(dataset: Dataset, auth: AuthFunction, addDelay: boolean) { + return { + list: http.get( + 'http://localhost:3000/api/v1/widgets', + async ({ request }) => { + auth(request.headers, 'admin'); + const data = Object.values(dataset.widgets); + await serverDelay(data.length, addDelay); + return HttpResponse.json(data); + }, + ), + }; +} + +export const widgetHandlers = (dataset: Dataset, auth: AuthFunction, addDelay: boolean) => + Object.values(getWidgetHandlers(dataset, auth, addDelay)); +``` + +## Generics and Types + +Always annotate the handler with generics: `http.METHOD(...)`. +Do not pass the path as a 4th generic argument — it is inferred from the first string parameter. Keep only the first three generic parameters typed. + +Examples for a "Things" Endpoint: + +- List +``` +http.get( + 'http://localhost:3000/api/v1/things', + async ({ request }) => { /* auth(request.headers, 'admin') ... */ }, +) +``` + +- Mine +``` +http.get( + 'http://localhost:3000/api/v1/things/mine', + async ({ request }) => { /* auth(request.headers) ... */ }, +) +``` + +- Detail +``` +http.get<{ id: string }, never, ThingRead>( + 'http://localhost:3000/api/v1/things/:id', + async ({ request, params }) => { /* ... */ }, +) +``` + +- Create +``` +http.post( + 'http://localhost:3000/api/v1/things', + async ({ request }) => { /* ... */ }, +) +``` + +- Update +``` +http.put<{ id: string }, Thing & { readonly id: string }, ThingRead>( + 'http://localhost:3000/api/v1/things/:id', + async ({ request, params }) => { /* ... */ }, +) +``` + +- Delete +``` +http.delete<{ id: string }, never>( + 'http://localhost:3000/api/v1/things/:id', + async ({ request, params }) => { /* ... */ }, +) +``` + +## Auth and RBAC + +- Use `auth(request.headers)` for routes accessible to both admin and end-users. RBAC checks (e.g., owner vs non-owner) must be handled within the handler. +- Use `auth(request.headers, 'admin')` for admin-only routes. This fails early with a standardized error — no extra checks needed. +- Do not access cookies directly. The `auth` helper returns a Session with `user.role` and `user.id`. +- If mocks are active, the Auth token that was returned from Entra ID is replaced with a serialized hero user object. This is then extracted in the auth() function. + +Owner-check example (admin-or-owner): + +``` +const session = auth(request.headers); +const isAdmin = session.user.role === 'admin'; +const userId = session.user.id!; +const isOwner = userId === thing.ownerId +if (!isAdmin && !isOwner) { + return HttpResponse.json({ detail: 'Not authorized' }, { status: 403, statusText: 'Forbidden' }); +} +``` + +## Errors and Delays + +- Not found: `return HttpResponse.json(null, { status: 404, statusText: 'Not Found' })`. +- Unauthorized: `403` for RBAC failures inside handlers. Admin-only endpoints use `auth(request.headers, 'admin')` to fail early. +- Add small artificial latency using `serverDelay(sizeOr1, addDelay)`. + +## Dataset Mutations + +Mutations should update the dataset in-place. Maintain reverse indices to keep data consistent. + +- Creating a Thing (example entity) + - Add to `dataset.things[id]`. + - Push `id` into owner arrays: `dataset.users_things[owner_id]`. + +- Updating a Thing + - Merge fields and update `updated_at`/`updated_by`. + - If owners change, remove previous links and add new ones in `dataset.users_things`. + +- Deleting a Thing + - Remove id from owners in `dataset.users_things`. + +## Path Conventions + +- Always use absolute URL strings as the path argument (e.g., `'http://localhost:3000/api/v1/...')`. Do not pass the path as a generic type parameter; it’s inferred from the first argument. + +## Do and Don’t + +- Do: + - Keep handlers concise and deterministic. + - Use the types from `src/types` and keep response shapes identical to the real API. + - Group related handlers and export as arrays. +- Don’t: + - Read cookies directly or from `document.cookie` in these handlers. + - Return shapes that differ from the real API. + +--- + diff --git a/template/mock-data/AGENTS.md b/template/mock-data/AGENTS.md new file mode 100644 index 0000000..f4a2f63 --- /dev/null +++ b/template/mock-data/AGENTS.md @@ -0,0 +1,128 @@ +# Mocks – Agent Guide + +This guide explains how the mock dataset under `mock-data/` is structured, how to extend it for new entities/fields, and how to keep it deterministic and aligned with the real API. These mocks power both MSW handlers (dev and tests) and the optional JSON snapshot CLI. + +Related docs +- `../mock-backend/AGENTS.md` — MSW handler conventions and RBAC patterns. +- `../AGENTS.md` — project overview and how mocks are used. +- `./README.md` — ERD and API Models; authoritative schema reference for mocks. + +See the root AGENTS.md for the role of mocks in local dev and tests. This document focuses on how to create and maintain the generators themselves. + +## Goals + +- Deterministic, realistic sample data with Faker. +- Type-safe and aligned with `src/types/*` shapes returned by the real API. +- Referential integrity between entities and reverse indices preserved. +- Simple knobs to control dataset size, seed, and reference date. + +## Folder Overview + +- `src/generators/config.ts`: dataset presets and the `DatasetConfig` type. Control size, seeding, and reference date. +- `src/generators/dataset.ts`: `buildDataset(config)` orchestrates end-to-end data creation and assembles indices. +- `src/generators`: pure, per-entity generators. +- `facts`: static catalogs (hero users, model names, endpoint bases) used across generations. +- `types.ts`: mock-local types not present in `src/types/*` (e.g., `EntraUser`). +- `utils.ts`: small helpers (e.g., `today()` zeroed to midnight for stable dates). +- `cli/generate-data.ts`: CLI to write JSON snapshots under `mocks/data-mocks/` for manual review. +- `data-mocks/`: optional generated snapshots, not used by the app at runtime. + +## Quick Start + +- Build a dataset in code (used by MSW): + - `import { buildDataset, datasetConfigs } from '@/mock-data'` then `const { data } = buildDataset(datasetConfigs.medium)`. +- Run dev server with MSW-based mocks: `npm run dev:mocks`. +- Generate a JSON snapshot (manual inspection only): + - `npm run generate:small` + - or `npx tsx mock-data/cli/generate-data.ts -d medium -s 42 -o mock-data/data-mocks/mock-data..json` + +## Determinism and Seeding + +- `buildDataset(config)` uses Faker with a `Randomizer` and optional `seed`. Provide one for reproducible datasets. +- `refDate` (in presets) anchors all time-dependent Faker calls for stability across runs. +- If you need purely random runs, omit both `seed` and `refDate` and pass a custom `randomizer` if needed. + +## Dataset Shape and Indices + +`Dataset` (in `dataset.ts`) stores flat maps plus reverse indices for joins: + +- Entities: `Thing` . +- Reverse indices: + - `users_things: Record` + +When mutating or extending the dataset, always keep the indices in sync. MSW handlers rely on these to serve list/detail endpoints efficiently. + +## Generators – Authoring Guidelines + +- Keep generators pure: they should only use the provided `faker` and arguments to return a typed value. +- Use domain read types from `src/types/*` for outputs (e.g., `ThingRead`) where possible. Otherwise, create custom types in `mock-data/types.ts`. +- Respect API invariants and optionals: + - Example: optional entries should sometimes be `undefined`. +- Use `UniqueEnforcer` (already wired in `dataset.ts`) for fields that must be unique. +- Timestamps: prefer `created_at` <= `updated_at` <= `now/refDate` patterns. Use `faker.date.between({ from, to })` to keep order valid. +- Cross-entity coherence: generate IDs first, then derive relationships. + +## Orchestration – `buildDataset` + +- Users: creates two “hero” users from `static.ts` (`admin`, `user`) and fills the remainder with Faker. +- TODO: Add more hints here. + +If you add new entities or relationships, follow the same pattern: create base catalogs in `static.ts` (if needed), generate entities, then compute derived relationships and reverse indices in a single pass to avoid inconsistencies. + +## Presets – `config.ts` + +- `DatasetConfig` supports: + - `seed` and `refDate` for determinism; or a custom `randomizer`. + - Size knobs: Tune these only when necessary for performance or demo fidelity. +- When changing presets, ensure MSW dev config still behaves well (see `msw/dev-handlers.ts` which uses `datasetConfigs.medium`). + +## Extending the Mocks – New Entity Checklist + +1. Types first + - Add zod/TS types under `src/types/.ts` (Read/Write shapes). + - Reuse API-level types for generator outputs (e.g., `Read`). +2. Generator + - Implement `generate({ faker, ...deps })` in `mocks/generators.ts`. + - Ensure required foreign keys are passed in or computable. +3. Static catalogs (optional) + - If the entity has a fixed base set (like model names or endpoint bases), add a list to `mock-data/src/facts/static.ts`. +4. Dataset assembly + - Extend `Dataset` in `mock-data/src/generators/dataset.ts` with new maps and necessary reverse indices. + - Update `buildDataset` to generate, link, and index the new entity consistently. +5. MSW handlers + - Add or update handlers in `msw/*.ts` to expose list/detail/mutations using the new dataset fields. See `msw/AGENTS.md` for conventions. +6. CLI output (optional) + - Snapshots automatically include anything in `data`. No CLI changes needed unless you add CLI flags. + +## Maintaining and Evolving Fields + +- Add a field to an entity + - Update `src/types/*` first; adjust generators to produce realistic values. + - If the field is relational, update `Dataset` maps/indices and the assembly logic. + - Update MSW handlers to read/write the field on mutations. +- Rename/remove a field + - Change `src/types/*`, fix generators and any code in `dataset.ts` referencing it. + - Consider a transitional period where generators output both old and new data only if MSW/test code requires it; otherwise, prefer a straight rename. +- Optional booleans/dates + - Randomize presence with a clear probability (e.g., `faker.datatype.boolean({ probability: 0.3 })` for optional dates) to surface varied UI states. + +## Quality and Performance + +- Keep functions small and side-effect free; orchestration goes in `buildDataset`. +- Be mindful of the `full` preset size; it is for stress testing, not for default dev. +- Avoid heavy computations in per-entity generators; precompute sets/lists in `buildDataset` and pass them down. + +## Common Pitfalls + +- Forgetting reverse index updates when adding relationships (leads to empty lists in MSW routes). +- Using ad-hoc shapes instead of `src/types/*` (breaks type safety and handler consistency). +- Reading current time directly; prefer `faker.setDefaultRefDate(refDate)` and `today()` for stable scenarios. +- Returning fields that only exist in write payloads or server-only flows (e.g., secret key material). + +## Examples + +Add examples here + +--- + +For handler conventions and RBAC patterns, see `mock-backend/AGENTS.md`. For how mocks integrate into the app and tests, see the root `AGENTS.md` (sections 2.3–2.5).