diff --git a/claude.md b/claude.md index eff087d..e0c3ef1 100644 --- a/claude.md +++ b/claude.md @@ -1,4 +1,4 @@ -# AI-Assisted Architecture Guardrails (Copy-Paste Identical) +# AI-Assisted Architecture Guardrails (Case-Service Architecture) This repo will be developed by different AI tools across a group. Keep the architecture clean and predictable by following these conventions every time you add or change code. @@ -6,209 +6,105 @@ This repo will be developed by different AI tools across a group. Keep the archi ## Goals (what we optimize for) -1. Security first: authorization and data-access controls must be correct by construction. -2. Clear layering: domain logic is independent of Spring/Web/DB/S3. +1. Security first: authorization and data-access controls must be correct. +2. Clear layering: separation of concerns between DTOs, Services, and Entities. 3. Auditability: every important activity produces an audit record. -4. Real-time updates: changes are emitted as events and delivered to clients. +4. Transactional Integrity: business operations are wrapped in SQL transactions. --- ## Project Stack - Java 25 -- Spring Boot + Maven +- Spring Boot (Web + Data JPA) + Maven +- H2 Database (or SQL compatible) - JUnit tests -- Docker (local dev + integration) -- Server-side templates: Thymeleaf or JTE-templates +- Thymeleaf templates -## Target Clean Architecture (Spring Boot) +--- + +## Core Architectural Components -Use a consistent package split under your base package (`org.example.projektarendehantering`). +For every feature (e.g., "Case"), we maintain a consistent set of components: -### Recommended package layout +### 1. `*Controller` (Presentation Layer) +- **Role:** Handles incoming HTTP requests and interacts with the frontend. +- **Location:** `...presentation.rest` +- **Responsibility:** Translates between HTTP payloads and DTOs. Thin logic only. +- **Injected with:** `*Service`. -- `...domain` - - Pure domain model: entities/aggregates, value objects, domain services, domain policies - - No Spring annotations - - No direct JDBC/JPA/S3/Web dependencies +### 2. `*DTO` (Data Transfer Object) +- **Role:** Temporary objects for frontend interaction. +- **Location:** `...presentation.dto` +- **Responsibility:** Placeholder for data before it is converted to an entity or returned to the client. -- `...application` - - Use cases (application services) that orchestrate domain + ports - - Permission checks happen here (not only in controllers) - - Defines port interfaces (e.g. `EventPublisher`, `CaseRepository`, `FileStorage`) +### 3. `*Entity` (Infrastructure/Persistence Layer) +- **Role:** The data object written to the database. +- **Location:** `...infrastructure.persistence` +- **Responsibility:** JPA-mapped object representing the database schema. -- `...presentation` - - Controllers (REST), WebSocket handlers, request/response DTOs - - Translates between HTTP/Web payloads and application commands/queries - - Keep controllers thin: no heavy logic, no direct persistence/S3 calls +### 4. `*Mapper` (Application Layer) +- **Role:** Utility for object conversion. +- **Location:** `...application.service` +- **Responsibility:** Mapping `DTO -> Entity` and `Entity -> DTO`. -- `...infrastructure` - - Adapters that implement application ports - - Persistence (JPA repositories, migrations) - - S3-compatible storage client/adapter - - Real-time delivery adapter (WebSocket/SSE) - - Security adapter integrating with Spring Security - - Framework-specific configuration +### 5. `*Service` (Application Layer) +- **Role:** The core business logic handler. +- **Location:** `...application.service` +- **Responsibility:** Handles SQL transactions (using `@Transactional`). +- **Injected with:** `*Repository` and `*Mapper`. -- `...common` (optional but recommended) - - Cross-cutting domain-independent utilities: IDs, error types, time abstraction, shared DTO base types +### 6. `*Repository` (Infrastructure Layer) +- **Role:** Database access. +- **Location:** `...infrastructure.persistence` +- **Responsibility:** Extends `JpaRepository` for SQL operations. -### Folder structure (mirrors packages) +--- -Maintain the following in `src/main/java`: +## Package layout ```text src/main/java/ org/example/projektarendehantering/ - ProjektArendehanteringApplication.java - common/ - domain/ + common/ (Cross-cutting utilities) + domain/ (Core business logic / legacy domain models) application/ + service/ (Services, Mappers) + ports/ (Interface boundaries) presentation/ + rest/ (Controllers) + dto/ (DTOs) + web/ (UI Controllers) infrastructure/ + persistence/ (Entities, Repositories) + config/ (Spring Config) ``` -Maintain the following in `src/test/java`: - -```text -src/test/java/ - org/example/projektarendehantering/ - domain/ (unit tests) - application/ (use-case tests with mocks/fakes) - infrastructure/(adapter tests with testcontainers or fakes) - presentation/ (controller tests) -``` - ---- - -## Naming conventions (enforce consistency) - -1. Use-case classes: suffix with `UseCase` or `Service` (pick one style and stick to it). -2. Port interfaces (application boundary): name them as nouns + suffix `Port`. - - Example: `FileStoragePort`, `CaseEventPublisherPort` -3. Adapter implementations (infrastructure): suffix with `*Adapter` or `*JpaRepository` as appropriate. - - Example: `S3FileStorageAdapter implements FileStoragePort` -4. Controller classes: suffix with `*Controller`. -5. DTOs: - - Incoming: `*Request` - - Outgoing: `*Response` -6. Domain objects: - - Entities: nouns (e.g. `Case`) - - Value objects: `*Id`, `*Name`, `*Policy`, etc. - ---- - -## Security & Authorization (central requirement) - -### Where checks must happen - -- Authorization must be enforced in the `application` layer. -- Controllers must not assume the user is allowed; they pass commands/queries to application services, which verify permissions. - -### How to structure authorization - -1. Define role/user concepts in `domain` (or `common` if shared). -2. Create an application-level authorization component (port + implementation). - - Example ports: - - `CurrentUserPort` (who am I?) - - `AuthorizationPolicyPort` (am I allowed?) -3. Permission logic lives in domain/application policies, not spread across controllers. - -### Audit on security-sensitive actions - -- If access is denied, decide whether to audit it (at least log internally). -- If access is granted and data is returned (e.g., file download), audit the event. - ---- - -## File handling & S3 (strict access) - -### Recommended approach - -1. Store only file metadata + S3 key in the database (not the file bytes). -2. For download: - - Application service authorizes the user for that specific case + document. - - Infrastructure adapter streams from S3 only after successful authorization. -3. For upload: - - Application service authorizes the user for the target case. - - Infrastructure adapter uploads bytes and returns the stored S3 key + metadata. - -### Ports - -Create ports in `application`: -- `FileStoragePort` (upload/download/delete) -- `DocumentMetadataRepositoryPort` (persist metadata + link to cases) - -Implement in `infrastructure`: -- `S3FileStorageAdapter` (S3-compatible client) - --- -## Logging & Audit Trail (transparency) - -Every important activity must create an audit event. - -### Define an audit event model - -In `domain`, define: -- `AuditEvent` (type, timestamp, actor, target identifiers, details) - -### Persist and publish audit +## Naming conventions -In `application`: -- Use an application port like `AuditLogPort` to persist audit events -- Optionally also publish to the real-time system - -In `infrastructure`: -- Implement persistence (`AuditLogJpaAdapter`, etc.) -- Optionally forward to external log/stream +1. **Controllers:** Suffix with `Controller` (e.g., `CaseController`). +2. **Services:** Suffix with `Service` (e.g., `CaseService`). +3. **Mappers:** Suffix with `Mapper` (e.g., `CaseMapper`). +4. **DTOs:** Suffix with `DTO` (e.g., `CaseDTO`). +5. **Entities:** Suffix with `Entity` (e.g., `CaseEntity`). +6. **Repositories:** Suffix with `Repository` (e.g., `CaseRepository`). --- -## Real-time updates (comments, changes, lifecycle events) - -### Event-driven pattern - -1. Application services create domain events (or audit events that double as domain events). -2. Application publishes events via an application port. -3. Infrastructure delivery adapter sends them to clients. - -Example ports: -- `CaseEventPublisherPort` (publish lifecycle/comment/file-activity events) - -Example delivery adapters: -- `WebSocketCaseEventsAdapter` -- `SseCaseEventsAdapter` (if using SSE) +## Security & Authorization ---- - -## AI Tool Usage Rules (how teammates should prompt/code) - -1. When you propose code, explain: - - which layer you touched (`domain`/`application`/`presentation`/`infrastructure`) - - what port/interface boundary is used - - where authorization and audit are enforced -2. Never add Spring annotations in `domain`. -3. Never call S3 or DB directly from `presentation`. Use application ports/use cases. -4. Prefer interfaces in `application`; implement them in `infrastructure`. -5. Add tests: - - domain unit tests for pure behavior - - application tests for use-case orchestration (mock ports) - - integration/adapters tests for S3/JPA/WebSocket only when needed -6. Keep configuration: - - in `src/main/resources` (application properties) - - framework wiring in `infrastructure` config classes +- Authorization must be enforced in the `Service` layer or via Spring Security. +- Controllers should not perform heavy logic; they delegate to Services which verify permissions. --- -## Initial module checklist (when we start implementing) - -At minimum, we will eventually add: - -1. `domain`: Case lifecycle model + permissions concepts -2. `application`: use cases for create/follow/assign/update/close; plus ports for files, audit, events, auth context -3. `infrastructure`: S3 adapter, persistence adapters, auth adapter, event delivery adapter -4. `presentation`: REST endpoints + DTOs + WebSocket endpoints - -Start small: add one vertical slice (e.g., create case + audit + real-time notification) and repeat. +## AI Tool Usage Rules +1. **Always use DTOs** for public API communication. +2. **Never expose Entities** directly to the web layer. +3. **Use Mappers** to handle the translation between layers. +4. **Ensure @Transactional** is used on Service methods that modify data. +5. **Verify changes** with `mvnw compile` before finishing. +6. ** DO NOT TOUCH pom.xml, docker-compose or application.properties. diff --git a/cursor.md b/cursor.md index eff087d..5b6e415 100644 --- a/cursor.md +++ b/cursor.md @@ -1,4 +1,4 @@ -# AI-Assisted Architecture Guardrails (Copy-Paste Identical) +# AI-Assisted Architecture Guardrails (Case-Service Architecture) This repo will be developed by different AI tools across a group. Keep the architecture clean and predictable by following these conventions every time you add or change code. @@ -6,209 +6,105 @@ This repo will be developed by different AI tools across a group. Keep the archi ## Goals (what we optimize for) -1. Security first: authorization and data-access controls must be correct by construction. -2. Clear layering: domain logic is independent of Spring/Web/DB/S3. +1. Security first: authorization and data-access controls must be correct. +2. Clear layering: separation of concerns between DTOs, Services, and Entities. 3. Auditability: every important activity produces an audit record. -4. Real-time updates: changes are emitted as events and delivered to clients. +4. Transactional Integrity: business operations are wrapped in SQL transactions. --- ## Project Stack - Java 25 -- Spring Boot + Maven +- Spring Boot (Web + Data JPA) + Maven +- H2 Database (or SQL compatible) - JUnit tests -- Docker (local dev + integration) -- Server-side templates: Thymeleaf or JTE-templates +- Thymeleaf templates -## Target Clean Architecture (Spring Boot) +--- + +## Core Architectural Components -Use a consistent package split under your base package (`org.example.projektarendehantering`). +For every feature (e.g., "Case"), we maintain a consistent set of components: -### Recommended package layout +### 1. `*Controller` (Presentation Layer) +- **Role:** Handles incoming HTTP requests and interacts with the frontend. +- **Location:** `...presentation.rest` +- **Responsibility:** Translates between HTTP payloads and DTOs. Thin logic only. +- **Injected with:** `*Service`. -- `...domain` - - Pure domain model: entities/aggregates, value objects, domain services, domain policies - - No Spring annotations - - No direct JDBC/JPA/S3/Web dependencies +### 2. `*DTO` (Data Transfer Object) +- **Role:** Temporary objects for frontend interaction. +- **Location:** `...presentation.dto` +- **Responsibility:** Placeholder for data before it is converted to an entity or returned to the client. -- `...application` - - Use cases (application services) that orchestrate domain + ports - - Permission checks happen here (not only in controllers) - - Defines port interfaces (e.g. `EventPublisher`, `CaseRepository`, `FileStorage`) +### 3. `*Entity` (Infrastructure/Persistence Layer) +- **Role:** The data object written to the database. +- **Location:** `...infrastructure.persistence` +- **Responsibility:** JPA-mapped object representing the database schema. -- `...presentation` - - Controllers (REST), WebSocket handlers, request/response DTOs - - Translates between HTTP/Web payloads and application commands/queries - - Keep controllers thin: no heavy logic, no direct persistence/S3 calls +### 4. `*Mapper` (Application Layer) +- **Role:** Utility for object conversion. +- **Location:** `...application.service` +- **Responsibility:** Mapping `DTO -> Entity` and `Entity -> DTO`. -- `...infrastructure` - - Adapters that implement application ports - - Persistence (JPA repositories, migrations) - - S3-compatible storage client/adapter - - Real-time delivery adapter (WebSocket/SSE) - - Security adapter integrating with Spring Security - - Framework-specific configuration +### 5. `*Service` (Application Layer) +- **Role:** The core business logic handler. +- **Location:** `...application.service` +- **Responsibility:** Handles SQL transactions (using `@Transactional`). +- **Injected with:** `*Repository` and `*Mapper`. -- `...common` (optional but recommended) - - Cross-cutting domain-independent utilities: IDs, error types, time abstraction, shared DTO base types +### 6. `*Repository` (Infrastructure Layer) +- **Role:** Database access. +- **Location:** `...infrastructure.persistence` +- **Responsibility:** Extends `JpaRepository` for SQL operations. -### Folder structure (mirrors packages) +--- -Maintain the following in `src/main/java`: +## Package layout ```text src/main/java/ org/example/projektarendehantering/ - ProjektArendehanteringApplication.java - common/ - domain/ + common/ (Cross-cutting utilities) + domain/ (Core business logic / legacy domain models) application/ + service/ (Services, Mappers) + ports/ (Interface boundaries) presentation/ + rest/ (Controllers) + dto/ (DTOs) + web/ (UI Controllers) infrastructure/ + persistence/ (Entities, Repositories) + config/ (Spring Config) ``` -Maintain the following in `src/test/java`: - -```text -src/test/java/ - org/example/projektarendehantering/ - domain/ (unit tests) - application/ (use-case tests with mocks/fakes) - infrastructure/(adapter tests with testcontainers or fakes) - presentation/ (controller tests) -``` - ---- - -## Naming conventions (enforce consistency) - -1. Use-case classes: suffix with `UseCase` or `Service` (pick one style and stick to it). -2. Port interfaces (application boundary): name them as nouns + suffix `Port`. - - Example: `FileStoragePort`, `CaseEventPublisherPort` -3. Adapter implementations (infrastructure): suffix with `*Adapter` or `*JpaRepository` as appropriate. - - Example: `S3FileStorageAdapter implements FileStoragePort` -4. Controller classes: suffix with `*Controller`. -5. DTOs: - - Incoming: `*Request` - - Outgoing: `*Response` -6. Domain objects: - - Entities: nouns (e.g. `Case`) - - Value objects: `*Id`, `*Name`, `*Policy`, etc. - ---- - -## Security & Authorization (central requirement) - -### Where checks must happen - -- Authorization must be enforced in the `application` layer. -- Controllers must not assume the user is allowed; they pass commands/queries to application services, which verify permissions. - -### How to structure authorization - -1. Define role/user concepts in `domain` (or `common` if shared). -2. Create an application-level authorization component (port + implementation). - - Example ports: - - `CurrentUserPort` (who am I?) - - `AuthorizationPolicyPort` (am I allowed?) -3. Permission logic lives in domain/application policies, not spread across controllers. - -### Audit on security-sensitive actions - -- If access is denied, decide whether to audit it (at least log internally). -- If access is granted and data is returned (e.g., file download), audit the event. - ---- - -## File handling & S3 (strict access) - -### Recommended approach - -1. Store only file metadata + S3 key in the database (not the file bytes). -2. For download: - - Application service authorizes the user for that specific case + document. - - Infrastructure adapter streams from S3 only after successful authorization. -3. For upload: - - Application service authorizes the user for the target case. - - Infrastructure adapter uploads bytes and returns the stored S3 key + metadata. - -### Ports - -Create ports in `application`: -- `FileStoragePort` (upload/download/delete) -- `DocumentMetadataRepositoryPort` (persist metadata + link to cases) - -Implement in `infrastructure`: -- `S3FileStorageAdapter` (S3-compatible client) - --- -## Logging & Audit Trail (transparency) - -Every important activity must create an audit event. - -### Define an audit event model - -In `domain`, define: -- `AuditEvent` (type, timestamp, actor, target identifiers, details) - -### Persist and publish audit +## Naming conventions -In `application`: -- Use an application port like `AuditLogPort` to persist audit events -- Optionally also publish to the real-time system - -In `infrastructure`: -- Implement persistence (`AuditLogJpaAdapter`, etc.) -- Optionally forward to external log/stream +1. **Controllers:** Suffix with `Controller` (e.g., `CaseController`). +2. **Services:** Suffix with `Service` (e.g., `CaseService`). +3. **Mappers:** Suffix with `Mapper` (e.g., `CaseMapper`). +4. **DTOs:** Suffix with `DTO` (e.g., `CaseDTO`). +5. **Entities:** Suffix with `Entity` (e.g., `CaseEntity`). +6. **Repositories:** Suffix with `Repository` (e.g., `CaseRepository`). --- -## Real-time updates (comments, changes, lifecycle events) - -### Event-driven pattern - -1. Application services create domain events (or audit events that double as domain events). -2. Application publishes events via an application port. -3. Infrastructure delivery adapter sends them to clients. - -Example ports: -- `CaseEventPublisherPort` (publish lifecycle/comment/file-activity events) - -Example delivery adapters: -- `WebSocketCaseEventsAdapter` -- `SseCaseEventsAdapter` (if using SSE) +## Security & Authorization ---- - -## AI Tool Usage Rules (how teammates should prompt/code) - -1. When you propose code, explain: - - which layer you touched (`domain`/`application`/`presentation`/`infrastructure`) - - what port/interface boundary is used - - where authorization and audit are enforced -2. Never add Spring annotations in `domain`. -3. Never call S3 or DB directly from `presentation`. Use application ports/use cases. -4. Prefer interfaces in `application`; implement them in `infrastructure`. -5. Add tests: - - domain unit tests for pure behavior - - application tests for use-case orchestration (mock ports) - - integration/adapters tests for S3/JPA/WebSocket only when needed -6. Keep configuration: - - in `src/main/resources` (application properties) - - framework wiring in `infrastructure` config classes +- Authorization must be enforced in the `Service` layer or via Spring Security. +- Controllers should not perform heavy logic; they delegate to Services which verify permissions. --- -## Initial module checklist (when we start implementing) - -At minimum, we will eventually add: - -1. `domain`: Case lifecycle model + permissions concepts -2. `application`: use cases for create/follow/assign/update/close; plus ports for files, audit, events, auth context -3. `infrastructure`: S3 adapter, persistence adapters, auth adapter, event delivery adapter -4. `presentation`: REST endpoints + DTOs + WebSocket endpoints - -Start small: add one vertical slice (e.g., create case + audit + real-time notification) and repeat. +## AI Tool Usage Rules +1. **Always use DTOs** for public API communication. +2. **Never expose Entities** directly to the web layer. +3. **Use Mappers** to handle the translation between layers. +4. **Ensure @Transactional** is used on Service methods that modify data. +5. **Verify changes** with `mvnw compile` before finishing. +6. 6. ** DO NOT TOUCH pom.xml, docker-compose or application.properties. diff --git a/docker-compose.yml b/docker-compose.yml index 4b4db37..5f378e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,3 +20,4 @@ services: volumes: postgres_data: + diff --git a/gemini.md b/gemini.md index eff087d..e0c3ef1 100644 --- a/gemini.md +++ b/gemini.md @@ -1,4 +1,4 @@ -# AI-Assisted Architecture Guardrails (Copy-Paste Identical) +# AI-Assisted Architecture Guardrails (Case-Service Architecture) This repo will be developed by different AI tools across a group. Keep the architecture clean and predictable by following these conventions every time you add or change code. @@ -6,209 +6,105 @@ This repo will be developed by different AI tools across a group. Keep the archi ## Goals (what we optimize for) -1. Security first: authorization and data-access controls must be correct by construction. -2. Clear layering: domain logic is independent of Spring/Web/DB/S3. +1. Security first: authorization and data-access controls must be correct. +2. Clear layering: separation of concerns between DTOs, Services, and Entities. 3. Auditability: every important activity produces an audit record. -4. Real-time updates: changes are emitted as events and delivered to clients. +4. Transactional Integrity: business operations are wrapped in SQL transactions. --- ## Project Stack - Java 25 -- Spring Boot + Maven +- Spring Boot (Web + Data JPA) + Maven +- H2 Database (or SQL compatible) - JUnit tests -- Docker (local dev + integration) -- Server-side templates: Thymeleaf or JTE-templates +- Thymeleaf templates -## Target Clean Architecture (Spring Boot) +--- + +## Core Architectural Components -Use a consistent package split under your base package (`org.example.projektarendehantering`). +For every feature (e.g., "Case"), we maintain a consistent set of components: -### Recommended package layout +### 1. `*Controller` (Presentation Layer) +- **Role:** Handles incoming HTTP requests and interacts with the frontend. +- **Location:** `...presentation.rest` +- **Responsibility:** Translates between HTTP payloads and DTOs. Thin logic only. +- **Injected with:** `*Service`. -- `...domain` - - Pure domain model: entities/aggregates, value objects, domain services, domain policies - - No Spring annotations - - No direct JDBC/JPA/S3/Web dependencies +### 2. `*DTO` (Data Transfer Object) +- **Role:** Temporary objects for frontend interaction. +- **Location:** `...presentation.dto` +- **Responsibility:** Placeholder for data before it is converted to an entity or returned to the client. -- `...application` - - Use cases (application services) that orchestrate domain + ports - - Permission checks happen here (not only in controllers) - - Defines port interfaces (e.g. `EventPublisher`, `CaseRepository`, `FileStorage`) +### 3. `*Entity` (Infrastructure/Persistence Layer) +- **Role:** The data object written to the database. +- **Location:** `...infrastructure.persistence` +- **Responsibility:** JPA-mapped object representing the database schema. -- `...presentation` - - Controllers (REST), WebSocket handlers, request/response DTOs - - Translates between HTTP/Web payloads and application commands/queries - - Keep controllers thin: no heavy logic, no direct persistence/S3 calls +### 4. `*Mapper` (Application Layer) +- **Role:** Utility for object conversion. +- **Location:** `...application.service` +- **Responsibility:** Mapping `DTO -> Entity` and `Entity -> DTO`. -- `...infrastructure` - - Adapters that implement application ports - - Persistence (JPA repositories, migrations) - - S3-compatible storage client/adapter - - Real-time delivery adapter (WebSocket/SSE) - - Security adapter integrating with Spring Security - - Framework-specific configuration +### 5. `*Service` (Application Layer) +- **Role:** The core business logic handler. +- **Location:** `...application.service` +- **Responsibility:** Handles SQL transactions (using `@Transactional`). +- **Injected with:** `*Repository` and `*Mapper`. -- `...common` (optional but recommended) - - Cross-cutting domain-independent utilities: IDs, error types, time abstraction, shared DTO base types +### 6. `*Repository` (Infrastructure Layer) +- **Role:** Database access. +- **Location:** `...infrastructure.persistence` +- **Responsibility:** Extends `JpaRepository` for SQL operations. -### Folder structure (mirrors packages) +--- -Maintain the following in `src/main/java`: +## Package layout ```text src/main/java/ org/example/projektarendehantering/ - ProjektArendehanteringApplication.java - common/ - domain/ + common/ (Cross-cutting utilities) + domain/ (Core business logic / legacy domain models) application/ + service/ (Services, Mappers) + ports/ (Interface boundaries) presentation/ + rest/ (Controllers) + dto/ (DTOs) + web/ (UI Controllers) infrastructure/ + persistence/ (Entities, Repositories) + config/ (Spring Config) ``` -Maintain the following in `src/test/java`: - -```text -src/test/java/ - org/example/projektarendehantering/ - domain/ (unit tests) - application/ (use-case tests with mocks/fakes) - infrastructure/(adapter tests with testcontainers or fakes) - presentation/ (controller tests) -``` - ---- - -## Naming conventions (enforce consistency) - -1. Use-case classes: suffix with `UseCase` or `Service` (pick one style and stick to it). -2. Port interfaces (application boundary): name them as nouns + suffix `Port`. - - Example: `FileStoragePort`, `CaseEventPublisherPort` -3. Adapter implementations (infrastructure): suffix with `*Adapter` or `*JpaRepository` as appropriate. - - Example: `S3FileStorageAdapter implements FileStoragePort` -4. Controller classes: suffix with `*Controller`. -5. DTOs: - - Incoming: `*Request` - - Outgoing: `*Response` -6. Domain objects: - - Entities: nouns (e.g. `Case`) - - Value objects: `*Id`, `*Name`, `*Policy`, etc. - ---- - -## Security & Authorization (central requirement) - -### Where checks must happen - -- Authorization must be enforced in the `application` layer. -- Controllers must not assume the user is allowed; they pass commands/queries to application services, which verify permissions. - -### How to structure authorization - -1. Define role/user concepts in `domain` (or `common` if shared). -2. Create an application-level authorization component (port + implementation). - - Example ports: - - `CurrentUserPort` (who am I?) - - `AuthorizationPolicyPort` (am I allowed?) -3. Permission logic lives in domain/application policies, not spread across controllers. - -### Audit on security-sensitive actions - -- If access is denied, decide whether to audit it (at least log internally). -- If access is granted and data is returned (e.g., file download), audit the event. - ---- - -## File handling & S3 (strict access) - -### Recommended approach - -1. Store only file metadata + S3 key in the database (not the file bytes). -2. For download: - - Application service authorizes the user for that specific case + document. - - Infrastructure adapter streams from S3 only after successful authorization. -3. For upload: - - Application service authorizes the user for the target case. - - Infrastructure adapter uploads bytes and returns the stored S3 key + metadata. - -### Ports - -Create ports in `application`: -- `FileStoragePort` (upload/download/delete) -- `DocumentMetadataRepositoryPort` (persist metadata + link to cases) - -Implement in `infrastructure`: -- `S3FileStorageAdapter` (S3-compatible client) - --- -## Logging & Audit Trail (transparency) - -Every important activity must create an audit event. - -### Define an audit event model - -In `domain`, define: -- `AuditEvent` (type, timestamp, actor, target identifiers, details) - -### Persist and publish audit +## Naming conventions -In `application`: -- Use an application port like `AuditLogPort` to persist audit events -- Optionally also publish to the real-time system - -In `infrastructure`: -- Implement persistence (`AuditLogJpaAdapter`, etc.) -- Optionally forward to external log/stream +1. **Controllers:** Suffix with `Controller` (e.g., `CaseController`). +2. **Services:** Suffix with `Service` (e.g., `CaseService`). +3. **Mappers:** Suffix with `Mapper` (e.g., `CaseMapper`). +4. **DTOs:** Suffix with `DTO` (e.g., `CaseDTO`). +5. **Entities:** Suffix with `Entity` (e.g., `CaseEntity`). +6. **Repositories:** Suffix with `Repository` (e.g., `CaseRepository`). --- -## Real-time updates (comments, changes, lifecycle events) - -### Event-driven pattern - -1. Application services create domain events (or audit events that double as domain events). -2. Application publishes events via an application port. -3. Infrastructure delivery adapter sends them to clients. - -Example ports: -- `CaseEventPublisherPort` (publish lifecycle/comment/file-activity events) - -Example delivery adapters: -- `WebSocketCaseEventsAdapter` -- `SseCaseEventsAdapter` (if using SSE) +## Security & Authorization ---- - -## AI Tool Usage Rules (how teammates should prompt/code) - -1. When you propose code, explain: - - which layer you touched (`domain`/`application`/`presentation`/`infrastructure`) - - what port/interface boundary is used - - where authorization and audit are enforced -2. Never add Spring annotations in `domain`. -3. Never call S3 or DB directly from `presentation`. Use application ports/use cases. -4. Prefer interfaces in `application`; implement them in `infrastructure`. -5. Add tests: - - domain unit tests for pure behavior - - application tests for use-case orchestration (mock ports) - - integration/adapters tests for S3/JPA/WebSocket only when needed -6. Keep configuration: - - in `src/main/resources` (application properties) - - framework wiring in `infrastructure` config classes +- Authorization must be enforced in the `Service` layer or via Spring Security. +- Controllers should not perform heavy logic; they delegate to Services which verify permissions. --- -## Initial module checklist (when we start implementing) - -At minimum, we will eventually add: - -1. `domain`: Case lifecycle model + permissions concepts -2. `application`: use cases for create/follow/assign/update/close; plus ports for files, audit, events, auth context -3. `infrastructure`: S3 adapter, persistence adapters, auth adapter, event delivery adapter -4. `presentation`: REST endpoints + DTOs + WebSocket endpoints - -Start small: add one vertical slice (e.g., create case + audit + real-time notification) and repeat. +## AI Tool Usage Rules +1. **Always use DTOs** for public API communication. +2. **Never expose Entities** directly to the web layer. +3. **Use Mappers** to handle the translation between layers. +4. **Ensure @Transactional** is used on Service methods that modify data. +5. **Verify changes** with `mvnw compile` before finishing. +6. ** DO NOT TOUCH pom.xml, docker-compose or application.properties. diff --git a/pom.xml b/pom.xml index ab78dcc..9e5ce89 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,11 @@ spring-security-test test + + com.h2database + h2 + test + org.thymeleaf.extras thymeleaf-extras-springsecurity6 @@ -106,4 +111,4 @@ - + \ No newline at end of file diff --git a/src/main/java/org/example/projektarendehantering/application/ports/CaseEventPublisherPort.java b/src/main/java/org/example/projektarendehantering/application/ports/CaseEventPublisherPort.java index 1f7c9de..091f25f 100644 --- a/src/main/java/org/example/projektarendehantering/application/ports/CaseEventPublisherPort.java +++ b/src/main/java/org/example/projektarendehantering/application/ports/CaseEventPublisherPort.java @@ -7,3 +7,4 @@ public interface CaseEventPublisherPort { void publishCaseEvent(CaseEvent event); } +// Kommentar för att kunna pusha igen \ No newline at end of file diff --git a/src/main/java/org/example/projektarendehantering/application/service/CaseMapper.java b/src/main/java/org/example/projektarendehantering/application/service/CaseMapper.java new file mode 100644 index 0000000..eec79f9 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/application/service/CaseMapper.java @@ -0,0 +1,31 @@ +package org.example.projektarendehantering.application.service; + +import org.example.projektarendehantering.infrastructure.persistence.CaseEntity; +import org.example.projektarendehantering.presentation.dto.CaseDTO; +import org.springframework.stereotype.Component; + +@Component +public class CaseMapper { + + public CaseDTO toDTO(CaseEntity entity) { + if (entity == null) return null; + return new CaseDTO( + entity.getId(), + entity.getStatus(), + entity.getTitle(), + entity.getDescription(), + entity.getCreatedAt() + ); + } + + public CaseEntity toEntity(CaseDTO dto) { + if (dto == null) return null; + CaseEntity entity = new CaseEntity(); + entity.setId(dto.getId()); + entity.setStatus(dto.getStatus()); + entity.setTitle(dto.getTitle()); + entity.setDescription(dto.getDescription()); + entity.setCreatedAt(dto.getCreatedAt()); + return entity; + } +} diff --git a/src/main/java/org/example/projektarendehantering/application/service/CaseService.java b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java new file mode 100644 index 0000000..ffd69e3 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java @@ -0,0 +1,50 @@ +package org.example.projektarendehantering.application.service; + +import org.example.projektarendehantering.infrastructure.persistence.CaseEntity; +import org.example.projektarendehantering.infrastructure.persistence.CaseRepository; +import org.example.projektarendehantering.presentation.dto.CaseDTO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class CaseService { + + private final CaseRepository caseRepository; + private final CaseMapper caseMapper; + + public CaseService(CaseRepository caseRepository, CaseMapper caseMapper) { + this.caseRepository = caseRepository; + this.caseMapper = caseMapper; + } + + @Transactional + public CaseDTO createCase(CaseDTO caseDTO) { + CaseEntity entity = caseMapper.toEntity(caseDTO); + if (entity.getStatus() == null) { + entity.setStatus("OPEN"); + } + if (entity.getCreatedAt() == null) { + entity.setCreatedAt(Instant.now()); + } + CaseEntity savedEntity = caseRepository.save(entity); + return caseMapper.toDTO(savedEntity); + } + + @Transactional(readOnly = true) + public Optional getCase(UUID id) { + return caseRepository.findById(id).map(caseMapper::toDTO); + } + + @Transactional(readOnly = true) + public List getAllCases() { + return caseRepository.findAll().stream() + .map(caseMapper::toDTO) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseCommand.java b/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseCommand.java deleted file mode 100644 index 5998775..0000000 --- a/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.example.projektarendehantering.application.usecase; - -import java.util.Objects; - -public record CreateCaseCommand(String title, String description) { - - public CreateCaseCommand { - Objects.requireNonNull(title, "title"); - Objects.requireNonNull(description, "description"); - } -} - diff --git a/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseResult.java b/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseResult.java deleted file mode 100644 index 86bc60e..0000000 --- a/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseResult.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.projektarendehantering.application.usecase; - -import org.example.projektarendehantering.domain.CaseId; - -public record CreateCaseResult(CaseId caseId) { -} - diff --git a/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseUseCase.java b/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseUseCase.java deleted file mode 100644 index a19f71b..0000000 --- a/src/main/java/org/example/projektarendehantering/application/usecase/CreateCaseUseCase.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.example.projektarendehantering.application.usecase; - -import org.example.projektarendehantering.application.ports.AuditLogPort; -import org.example.projektarendehantering.application.ports.CaseEventPublisherPort; -import org.example.projektarendehantering.application.ports.CaseRepositoryPort; -import org.example.projektarendehantering.application.ports.CurrentUserPort; -import org.example.projektarendehantering.domain.AuditEvent; -import org.example.projektarendehantering.domain.Case; -import org.example.projektarendehantering.domain.CaseEvent; -import org.example.projektarendehantering.domain.CaseId; -import org.example.projektarendehantering.domain.CasePermissions; -import org.example.projektarendehantering.common.Actor; -import org.springframework.stereotype.Service; - -@Service -public class CreateCaseUseCase { - - private final CurrentUserPort currentUserPort; - private final CaseRepositoryPort caseRepositoryPort; - private final AuditLogPort auditLogPort; - private final CaseEventPublisherPort caseEventPublisherPort; - - public CreateCaseUseCase( - CurrentUserPort currentUserPort, - CaseRepositoryPort caseRepositoryPort, - AuditLogPort auditLogPort, - CaseEventPublisherPort caseEventPublisherPort - ) { - this.currentUserPort = currentUserPort; - this.caseRepositoryPort = caseRepositoryPort; - this.auditLogPort = auditLogPort; - this.caseEventPublisherPort = caseEventPublisherPort; - } - - public CreateCaseResult execute(CreateCaseCommand command) { - Actor actor = currentUserPort.currentUser(); - CasePermissions.assertCanCreate(actor); - - Case caseToCreate = Case.create(actor, command.title(), command.description()); - CaseId caseId = caseToCreate.id(); - caseRepositoryPort.save(caseToCreate); - - AuditEvent auditEvent = AuditEvent.caseCreated(actor, caseId, command.title(), command.description()); - auditLogPort.append(auditEvent); - - CaseEvent caseEvent = CaseEvent.caseCreated(caseId); - caseEventPublisherPort.publishCaseEvent(caseEvent); - - return new CreateCaseResult(caseId); - } -} - diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseEntity.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseEntity.java new file mode 100644 index 0000000..2d57c78 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseEntity.java @@ -0,0 +1,49 @@ +package org.example.projektarendehantering.infrastructure.persistence; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "cases") +public class CaseEntity { + + @Id + private UUID id; + private String status; + private UUID ownerId; + private String title; + private String description; + private Instant createdAt; + + public CaseEntity() {} + + public CaseEntity(UUID id, String status, UUID ownerId, String title, String description, Instant createdAt) { + this.id = id; + this.status = status; + this.ownerId = ownerId; + this.title = title; + this.description = description; + this.createdAt = createdAt; + } + + public UUID getId() { return id; } + public void setId(UUID id) { this.id = id; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public UUID getOwnerId() { return ownerId; } + public void setOwnerId(UUID ownerId) { this.ownerId = ownerId; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } +} diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseRepository.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseRepository.java new file mode 100644 index 0000000..0eb62c0 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseRepository.java @@ -0,0 +1,7 @@ +package org.example.projektarendehantering.infrastructure.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.UUID; + +public interface CaseRepository extends JpaRepository { +} diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/InMemoryCaseRepositoryAdapter.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/InMemoryCaseRepositoryAdapter.java index 83bb9ae..8a0ede4 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/InMemoryCaseRepositoryAdapter.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/InMemoryCaseRepositoryAdapter.java @@ -3,22 +3,159 @@ import org.example.projektarendehantering.application.ports.CaseRepositoryPort; import org.example.projektarendehantering.domain.Case; import org.example.projektarendehantering.domain.CaseId; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.stereotype.Repository; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; @Repository public class InMemoryCaseRepositoryAdapter implements CaseRepositoryPort { - private final ConcurrentMap cases = new ConcurrentHashMap<>(); + private final ConcurrentMap domainCases = new ConcurrentHashMap<>(); + private final ConcurrentMap entities = new ConcurrentHashMap<>(); + // Old Port implementation (for compatibility) @Override public Case save(Case caseToSave) { Objects.requireNonNull(caseToSave, "caseToSave"); - cases.put(caseToSave.id(), caseToSave); + domainCases.put(caseToSave.id(), caseToSave); return caseToSave; } -} + // New Repository implementation (keeping the methods but not implementing CaseRepository) + public CaseEntity save(CaseEntity entity) { + Objects.requireNonNull(entity, "entity"); + if (entity.getId() == null) { + entity.setId(UUID.randomUUID()); + } + entities.put(entity.getId(), entity); + return entity; + } + + public Optional findById(UUID id) { + return Optional.ofNullable(entities.get(id)); + } + + public boolean existsById(UUID uuid) { + return false; + } + + public List saveAll(Iterable entities) { + return List.of(); + } + + public List findAll() { + return new ArrayList<>(entities.values()); + } + + public List findAllById(Iterable uuids) { + return List.of(); + } + + public long count() { + return 0; + } + + public void deleteById(UUID uuid) { + + } + + public void delete(CaseEntity entity) { + + } + + public void deleteAllById(Iterable uuids) { + + } + + public void deleteAll(Iterable entities) { + + } + + public void deleteAll() { + + } + + public void flush() { + + } + + public S saveAndFlush(S entity) { + return null; + } + + public List saveAllAndFlush(Iterable entities) { + return List.of(); + } + + public void deleteAllInBatch(Iterable entities) { + + } + + public void deleteAllByIdInBatch(Iterable uuids) { + + } + + public void deleteAllInBatch() { + + } + + public CaseEntity getOne(UUID uuid) { + return null; + } + + public CaseEntity getById(UUID uuid) { + return null; + } + + public CaseEntity getReferenceById(UUID uuid) { + return null; + } + + public Optional findOne(Example example) { + return Optional.empty(); + } + + public List findAll(Example example) { + return List.of(); + } + + public List findAll(Example example, Sort sort) { + return List.of(); + } + + public Page findAll(Example example, Pageable pageable) { + return null; + } + + public long count(Example example) { + return 0; + } + + public boolean exists(Example example) { + return false; + } + + public R findBy(Example example, Function, R> queryFunction) { + return null; + } + + public List findAll(Sort sort) { + return List.of(); + } + + public Page findAll(Pageable pageable) { + return null; + } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/CaseDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/CaseDTO.java new file mode 100644 index 0000000..050fcb9 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/CaseDTO.java @@ -0,0 +1,38 @@ +package org.example.projektarendehantering.presentation.dto; + +import java.time.Instant; +import java.util.UUID; + +public class CaseDTO { + + private UUID id; + private String status; + private String title; + private String description; + private Instant createdAt; + + public CaseDTO() {} + + public CaseDTO(UUID id, String status, String title, String description, Instant createdAt) { + this.id = id; + this.status = status; + this.title = title; + this.description = description; + this.createdAt = createdAt; + } + + public UUID getId() { return id; } + public void setId(UUID id) { this.id = id; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/rest/CaseController.java b/src/main/java/org/example/projektarendehantering/presentation/rest/CaseController.java new file mode 100644 index 0000000..9ffa43e --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/presentation/rest/CaseController.java @@ -0,0 +1,39 @@ +package org.example.projektarendehantering.presentation.rest; + +import jakarta.validation.Valid; +import org.example.projektarendehantering.application.service.CaseService; +import org.example.projektarendehantering.presentation.dto.CaseDTO; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/cases") +public class CaseController { + + private final CaseService caseService; + + public CaseController(CaseService caseService) { + this.caseService = caseService; + } + + @PostMapping + public ResponseEntity createCase(@RequestBody @Valid CaseDTO caseDTO) { + CaseDTO created = caseService.createCase(caseDTO); + return ResponseEntity.ok(created); + } + + @GetMapping("/{id}") + public ResponseEntity getCase(@PathVariable UUID id) { + return caseService.getCase(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping + public ResponseEntity> getAllCases() { + return ResponseEntity.ok(caseService.getAllCases()); + } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/rest/CaseRestController.java b/src/main/java/org/example/projektarendehantering/presentation/rest/CaseRestController.java deleted file mode 100644 index 40e7e7e..0000000 --- a/src/main/java/org/example/projektarendehantering/presentation/rest/CaseRestController.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.example.projektarendehantering.presentation.rest; - -import jakarta.validation.Valid; -import org.example.projektarendehantering.application.usecase.CreateCaseCommand; -import org.example.projektarendehantering.application.usecase.CreateCaseResult; -import org.example.projektarendehantering.application.usecase.CreateCaseUseCase; -import org.example.projektarendehantering.presentation.dto.CreateCaseRequest; -import org.example.projektarendehantering.presentation.dto.CreateCaseResponse; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/cases") -public class CaseRestController { - - private final CreateCaseUseCase createCaseUseCase; - - public CaseRestController(CreateCaseUseCase createCaseUseCase) { - this.createCaseUseCase = createCaseUseCase; - } - - @PostMapping( - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public CreateCaseResponse createCase(@RequestBody @Valid CreateCaseRequest request) { - CreateCaseCommand command = new CreateCaseCommand(request.title(), request.description()); - CreateCaseResult result = createCaseUseCase.execute(command); - return new CreateCaseResponse(result.caseId().value().toString()); - } -} - diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..c2ea3e9 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,17 @@ +spring.application.name=Projekt-arendehantering-test + +# Use H2 in-memory database for tests +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +# Create schema on startup +spring.jpa.hibernate.ddl-auto=create-drop + +# Show SQL in logs for easier debugging during tests +spring.jpa.show-sql=true + +# Disable Docker Compose auto-configuration during tests to avoid connection errors in CI +spring.docker.compose.enabled=false