- Clarify docs/ARCHITECTURE.md link with interim pointer to docs/architecture/c4-diagrams.md until DOC-2 PR merges - Remove ./mvnw checkstyle:check — no checkstyle plugin in pom.xml; replace with ./mvnw test and ./mvnw clean package -DskipTests Refs #398 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
306 lines
12 KiB
Markdown
306 lines
12 KiB
Markdown
# Contributing to Familienarchiv
|
|
|
|
For the full collaboration rules (issue workflow, PR process, Red/Green TDD, commit conventions) see [COLLABORATING.md](./COLLABORATING.md).
|
|
For coding style see [CODESTYLE.md](./CODESTYLE.md).
|
|
For the system architecture see [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) (introduced in DOC-2; until that PR merges, see [docs/architecture/c4-diagrams.md](./docs/architecture/c4-diagrams.md)).
|
|
For domain terminology see [docs/GLOSSARY.md](./docs/GLOSSARY.md).
|
|
|
|
---
|
|
|
|
## 1. Environment setup
|
|
|
|
**Prerequisites:** Java 21 (SDKMAN), Node 24 (nvm), Docker
|
|
|
|
**Activate SDKMAN and nvm before running `java`, `mvn`, `node`, or `npm`:**
|
|
|
|
```bash
|
|
source "$HOME/.sdkman/bin/sdkman-init.sh"
|
|
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Daily development workflow
|
|
|
|
**Startup order — services must start in this sequence:**
|
|
|
|
```bash
|
|
# 1. Start PostgreSQL and MinIO
|
|
docker compose up -d db minio
|
|
|
|
# 2. Start the backend (separate terminal)
|
|
cd backend && ./mvnw spring-boot:run
|
|
|
|
# 3. Start the frontend (separate terminal)
|
|
cd frontend && npm install && npm run dev
|
|
```
|
|
|
|
> `npm install` also wires up the Husky pre-commit hook via the `prepare` script.
|
|
> Run it before your first commit, or the hook will fail to execute.
|
|
|
|
> **Do not use `docker-compose.ci.yml` locally** — it disables the bind mounts that the dev workflow depends on.
|
|
|
|
**Regenerate TypeScript types after any backend API change:**
|
|
|
|
```bash
|
|
# Backend must be running with dev profile
|
|
cd frontend && npm run generate:api
|
|
```
|
|
|
|
> ⚠️ Forgetting this step is the most common cause of "where did my TypeScript type go?" — always regenerate after changing models or endpoints.
|
|
|
|
**Test commands:**
|
|
|
|
```bash
|
|
cd backend && ./mvnw test # backend unit + slice tests
|
|
cd frontend && npm run test # Vitest unit tests
|
|
cd frontend && npm run check # svelte-check (type errors)
|
|
cd frontend && npx playwright test # Playwright e2e tests
|
|
```
|
|
|
|
**Branch naming:** `<type>/<issue-number>-<short-description>`, e.g. `feat/398-contributing`
|
|
|
|
**Commits:** one logical change per commit; reference the Gitea issue:
|
|
|
|
```
|
|
feat(person): add aliases endpoint
|
|
|
|
Closes #42
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
```
|
|
|
|
### Test-type decision matrix
|
|
|
|
| What you're testing | Test type | Tool |
|
|
|---|---|---|
|
|
| Service business logic, calculations | Unit test | JUnit + `@ExtendWith(MockitoExtension.class)` |
|
|
| HTTP contract, request validation, error codes | Controller slice test | `@WebMvcTest` |
|
|
| Server `load` function | Vitest unit | Import directly, mock `fetch` |
|
|
| Shared UI component | Vitest browser-mode | `render()` + `getByRole()` |
|
|
| Full user-facing flow, navigation, forms | E2E | Playwright |
|
|
|
|
---
|
|
|
|
## 3. Walkthrough A — Add a new domain
|
|
|
|
**Example:** adding a `citation` domain (formal references to documents).
|
|
|
|
Both the backend and frontend are organised **domain-first**. A new domain means adding a package on both sides under the same name.
|
|
|
|
### Backend
|
|
|
|
1. Create `backend/src/main/java/org/raddatz/familienarchiv/citation/`
|
|
|
|
2. Add entity, repository, service, controller, and DTOs flat in the package:
|
|
- **Entity** `Citation.java` — annotate with `@Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor`; use `@GeneratedValue(strategy = GenerationType.UUID)` for the `id` field; add `@Schema(requiredMode = REQUIRED)` on every field the backend always populates
|
|
- **Repository** `CitationRepository.java` — extends `JpaRepository<Citation, UUID>`
|
|
- **Service** `CitationService.java` — `@Service @RequiredArgsConstructor`; write methods `@Transactional`, read methods unannotated; cross-domain data goes through the other domain's service, never its repository
|
|
- **Controller** `CitationController.java` — `@RestController @RequestMapping("/api/citations")`
|
|
|
|
3. Add `@RequirePermission(Permission.WRITE_ALL)` on every `POST`, `PUT`, `PATCH`, and `DELETE` endpoint — **this is not optional**. Read-only `GET` endpoints stay unannotated.
|
|
|
|
4. Add a Flyway migration: `backend/src/main/resources/db/migration/V{n}__{description}.sql` (use the next sequential number after the highest existing one).
|
|
|
|
5. **Write failing tests before any implementation** (Red step):
|
|
- Service unit test for business logic (`@ExtendWith(MockitoExtension.class)`)
|
|
- `@WebMvcTest` slice test for each HTTP endpoint
|
|
|
|
6. Rebuild with `--spring.profiles.active=dev` and run `npm run generate:api` in `frontend/`.
|
|
|
|
### Frontend
|
|
|
|
7. Create `frontend/src/lib/citation/` — domain-specific Svelte components and TypeScript utilities go here.
|
|
|
|
8. Add routes under `frontend/src/routes/citations/` as needed.
|
|
|
|
9. Add a per-domain `README.md` in both the backend package folder and `frontend/src/lib/citation/` (per DOC-6).
|
|
|
|
### Documentation
|
|
|
|
10. Update `docs/ARCHITECTURE.md` Section 2 to include the new domain.
|
|
11. Update `docs/GLOSSARY.md` if new terms are introduced.
|
|
12. Update the ESLint boundary allow-list in `frontend/eslint.config.js` if the domain needs to import from another domain.
|
|
|
|
---
|
|
|
|
## 4. Walkthrough B — Add a new endpoint
|
|
|
|
**Example:** `POST /api/persons/{id}/aliases` — attach a name alias to an existing person.
|
|
|
|
### Red (write failing tests first)
|
|
|
|
1. Write a failing `@WebMvcTest` controller slice test:
|
|
```java
|
|
@Test
|
|
void addAlias_returns201_whenAliasCreated() { ... }
|
|
```
|
|
|
|
2. Write a failing service unit test:
|
|
```java
|
|
@Test
|
|
void addAlias_throwsNotFound_whenPersonDoesNotExist() { ... }
|
|
```
|
|
|
|
### Green (implement)
|
|
|
|
3. Add the service method in `PersonService.java`:
|
|
```java
|
|
@Transactional
|
|
public PersonNameAlias addAlias(UUID personId, PersonNameAliasDTO dto) { ... }
|
|
```
|
|
|
|
4. Add the controller method in `PersonController.java`:
|
|
```java
|
|
@PostMapping("/{id}/aliases")
|
|
@RequirePermission(Permission.WRITE_ALL)
|
|
public ResponseEntity<PersonNameAlias> addAlias(@PathVariable UUID id,
|
|
@RequestBody PersonNameAliasDTO dto) { ... }
|
|
```
|
|
`@RequirePermission(Permission.WRITE_ALL)` on every state-mutating endpoint — **not optional**.
|
|
|
|
5. Validate user-supplied inputs at the controller boundary:
|
|
```java
|
|
if (dto.name() == null || dto.name().isBlank())
|
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "name is required");
|
|
```
|
|
Validate at system boundaries; trust internal service code.
|
|
|
|
6. Use `DomainException` for domain errors:
|
|
```java
|
|
DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id)
|
|
```
|
|
If you need a new error code, add it to `ErrorCode.java`, mirror it in
|
|
`frontend/src/lib/shared/errors.ts`, and add translation keys in `messages/{de,en,es}.json`.
|
|
|
|
7. Mark every field the backend always populates with `@Schema(requiredMode = REQUIRED)` — this drives TypeScript type generation.
|
|
|
|
### Types and tests
|
|
|
|
8. Rebuild with `--spring.profiles.active=dev`, then `npm run generate:api` in `frontend/`.
|
|
|
|
> ⚠️ **Always regenerate types after any API change.** This is the #1 cause of "where did my TypeScript type go?"
|
|
|
|
9. Run the full test suite — all green before committing.
|
|
|
|
---
|
|
|
|
## 5. Walkthrough C — Add a new frontend page
|
|
|
|
**Example:** `/persons/[id]/timeline` — a chronological event timeline for one person.
|
|
|
|
### Red (write failing test first)
|
|
|
|
1. Write a failing Playwright E2E test for the user flow:
|
|
```typescript
|
|
test('timeline shows events in chronological order', async ({ page }) => {
|
|
await page.goto('/persons/1/timeline');
|
|
// assertions...
|
|
});
|
|
```
|
|
|
|
### Green (implement)
|
|
|
|
2. Create `frontend/src/routes/persons/[id]/timeline/+page.svelte`
|
|
|
|
3. Add `frontend/src/routes/persons/[id]/timeline/+page.server.ts` for the SSR load:
|
|
```typescript
|
|
import { createApiClient } from '$lib/shared/api.server';
|
|
export const load: PageServerLoad = async ({ params, fetch }) => {
|
|
const api = createApiClient(fetch);
|
|
const result = await api.GET('/api/persons/{id}', { params: { path: { id: params.id } } });
|
|
if (!result.response.ok) throw error(result.response.status, '...');
|
|
return { person: result.data! };
|
|
};
|
|
```
|
|
|
|
4. Domain-specific components (e.g. `TimelineEntry.svelte`) → `frontend/src/lib/person/`
|
|
|
|
5. Shared primitives (e.g. a generic date-range display) → `frontend/src/lib/shared/primitives/`
|
|
|
|
6. UI patterns to follow:
|
|
- Back navigation: `import BackButton from '$lib/shared/primitives/BackButton.svelte'`
|
|
- Date display: always append `T12:00:00` — `new Intl.DateTimeFormat('de-DE', …).format(new Date(val + 'T12:00:00'))` — prevents UTC off-by-one errors
|
|
- Brand colors: `brand-navy`, `brand-mint`, `brand-sand` (defined in `src/routes/layout.css`)
|
|
- Accessibility: touch targets ≥ 44 px (`min-h-[44px]`); focus rings (`focus-visible:ring-2 focus-visible:ring-brand-navy`); `aria-label` on icon-only buttons; `aria-live="polite"` on dynamic status messages
|
|
|
|
7. Add Paraglide i18n keys in `messages/de.json`, `messages/en.json`, `messages/es.json`.
|
|
|
|
8. If adding a new error code: mirror in `frontend/src/lib/shared/errors.ts` and add translation keys.
|
|
|
|
9. Make all tests green before committing.
|
|
|
|
---
|
|
|
|
## 6. Conventions reference
|
|
|
|
### Error handling
|
|
|
|
| Scenario | Pattern |
|
|
|---|---|
|
|
| Domain entity not found | `DomainException.notFound(ErrorCode.X, "…")` |
|
|
| Permission denied | `DomainException.forbidden("…")` |
|
|
| Concurrent edit conflict | `DomainException.conflict(ErrorCode.X, "…")` |
|
|
| Infrastructure failure | `DomainException.internal(ErrorCode.X, "…")` |
|
|
| Simple controller validation | `throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "…")` |
|
|
|
|
New error code: `ErrorCode.java` → `frontend/src/lib/shared/errors.ts` → `messages/{de,en,es}.json`.
|
|
|
|
### DTOs
|
|
|
|
- Input DTOs live flat in the domain package (e.g. `PersonUpdateDTO.java`)
|
|
- Responses are the entity itself — no separate response DTOs
|
|
- `@Schema(requiredMode = REQUIRED)` on every field the backend always populates
|
|
|
|
### Frontend API client
|
|
|
|
```typescript
|
|
const api = createApiClient(fetch); // from $lib/shared/api.server
|
|
const result = await api.GET('/api/persons/{id}', { params: { path: { id } } });
|
|
if (!result.response.ok) {
|
|
const code = (result.error as unknown as { code?: string })?.code;
|
|
throw error(result.response.status, getErrorMessage(code));
|
|
}
|
|
return { person: result.data! }; // non-null assertion is safe after the ok check
|
|
```
|
|
|
|
For multipart/form-data (file uploads): bypass the typed client and use raw `fetch` — the client cannot handle it.
|
|
|
|
### Date handling
|
|
|
|
| Context | Pattern |
|
|
|---|---|
|
|
| Form display | German `dd.mm.yyyy` with auto-dot insertion via `handleDateInput()` |
|
|
| Wire format | ISO 8601 via a hidden `<input type="hidden" name="documentDate" value={dateIso}>` |
|
|
| Display | `new Intl.DateTimeFormat('de-DE', …).format(new Date(val + 'T12:00:00'))` |
|
|
|
|
### Security checklist (new endpoint)
|
|
|
|
- `@RequirePermission(Permission.WRITE_ALL)` on every `POST`, `PUT`, `PATCH`, `DELETE` — required, not optional
|
|
- Validate all user-supplied inputs at the controller boundary before passing to the service
|
|
- Parameterised queries only — never interpolate user input into JPQL/SQL strings
|
|
- No raw user input in log messages — use `{}` placeholders: `log.warn("Not found: {}", id)`
|
|
- Validate content-type and size on upload endpoints before reading the stream
|
|
|
|
### Accessibility baseline (new frontend page)
|
|
|
|
- Touch targets ≥ 44 px on all interactive elements (`min-h-[44px]`)
|
|
- Focus rings on all focusable elements (`focus-visible:ring-2 focus-visible:ring-brand-navy`)
|
|
- `aria-label` on every icon-only button
|
|
- `aria-live="polite"` on dynamic status messages
|
|
- Color is never the sole status indicator
|
|
|
|
Full WCAG 2.1 AA reference: [docs/STYLEGUIDE.md](./docs/STYLEGUIDE.md).
|
|
|
|
### Lint and format
|
|
|
|
```bash
|
|
# Frontend
|
|
cd frontend && npm run lint # Prettier + ESLint check
|
|
cd frontend && npm run format # Auto-fix formatting
|
|
cd frontend && npm run check # svelte-check (type errors)
|
|
|
|
# Backend — no standalone lint tool; compilation and test runs catch style issues
|
|
cd backend && ./mvnw test # compile + test
|
|
cd backend && ./mvnw clean package -DskipTests # compile-only check
|
|
```
|