A social knowledge tool for researchers built on ATProto
45
fork

Configure Feed

Select the types of activity you want to include in your feed.

formatting and linting

+241 -66
+25 -2
docs/plan/IMPLEMENTATION_COMPLETE.md
··· 7 7 ### ✅ What Was Implemented 8 8 9 9 **Infrastructure (100% Complete)** 10 + 10 11 - ✅ Created `@semble/types` npm workspace package 11 12 - ✅ Configured npm workspaces in root package.json 12 13 - ✅ Set up TypeScript compilation with proper paths ··· 14 15 - ✅ Built and compiled successfully 15 16 16 17 **Shared Types Package** (`src/types/src/api/`) 18 + 17 19 - ✅ `common.ts` - User, Pagination, Sorting base types 18 20 - ✅ `requests.ts` - All API request types (30+ types) 19 21 - ✅ `responses.ts` - All API response types (30+ types) ··· 21 23 - ✅ Compiles cleanly with TypeScript 22 24 23 25 **Backend Migration (100% Complete)** 26 + 24 27 - ✅ **All 8 card query use cases** migrated to `@semble/types` 25 28 - GetCollectionsForUrlUseCase 26 29 - GetGlobalFeedUseCase ··· 36 39 - ✅ Removed old DTO directories 37 40 38 41 **Frontend Migration (100% Complete)** 42 + 39 43 - ✅ ApiClient.ts imports from `@semble/types` 40 44 - ✅ All client files updated (QueryClient, CardClient, etc.) 41 45 - ✅ Removed old `src/webapp/api-client/types/` directory ··· 158 162 ## 🔧 Development Workflow 159 163 160 164 ### Starting Development 165 + 161 166 ```bash 162 167 # Terminal 1: Watch and rebuild types on changes 163 168 npm run dev:types ··· 170 175 ``` 171 176 172 177 ### Making Type Changes 178 + 173 179 1. Edit files in `src/types/src/api/` 174 180 2. Types package auto-rebuilds (if dev:types is running) 175 181 3. Both backend and frontend see changes immediately 176 182 4. TypeScript catches any mismatches 177 183 178 184 ### Example: Adding a New Endpoint 185 + 179 186 ```typescript 180 187 // 1. Add types to src/types/src/api/requests.ts 181 188 export interface CreateCommentRequest { ··· 218 225 ## 📚 Reference Implementations 219 226 220 227 ### Example Use Case 228 + 221 229 `src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts` 230 + 222 231 ```typescript 223 232 import { GetCollectionsForUrlResponse, Collection } from '@semble/types'; 224 233 ··· 231 240 232 241 return ok({ 233 242 collections: enrichedCollections, 234 - pagination: { /* ... */ }, 235 - sorting: { /* ... */ }, 243 + pagination: { 244 + /* ... */ 245 + }, 246 + sorting: { 247 + /* ... */ 248 + }, 236 249 }); 237 250 } 238 251 } 239 252 ``` 240 253 241 254 ### Example Controller with Zod 255 + 242 256 `src/modules/cards/infrastructure/http/controllers/GetCollectionsForUrlController.ts` 257 + 243 258 ```typescript 244 259 import { z } from 'zod'; 245 260 import { GetCollectionsForUrlResponse } from '@semble/types'; ··· 264 279 ``` 265 280 266 281 ### Example Frontend Usage 282 + 267 283 `src/webapp/api-client/ApiClient.ts` 284 + 268 285 ```typescript 269 286 import { GetCollectionsForUrlParams, GetCollectionsForUrlResponse } from '@semble/types'; 270 287 ··· 281 298 ## 🎯 Future Enhancements (Optional) 282 299 283 300 ### Short Term 301 + 284 302 - [ ] Add Zod validation to remaining 23 controllers 285 303 - [ ] Create shared Zod utility schemas for pagination/sorting 286 304 - [ ] Add request/response logging middleware 287 305 288 306 ### Medium Term 307 + 289 308 - [ ] Generate OpenAPI spec from Zod schemas + types 290 309 - [ ] Create API documentation from types 291 310 - [ ] Add integration tests using shared types 292 311 - [ ] Runtime response validation in development mode 293 312 294 313 ### Long Term 314 + 295 315 - [ ] Type versioning strategy for breaking changes 296 316 - [ ] Generate client SDKs for mobile apps 297 317 - [ ] Publish types to private npm registry ··· 323 343 ## 📝 Key Files Modified 324 344 325 345 ### Created 346 + 326 347 - ✅ `src/types/` - Entire @semble/types package 327 348 - ✅ `docs/plan/shared_type_unification.md` - Implementation plan 328 349 - ✅ `docs/shared_types_implementation_status.md` - Status tracking 329 350 - ✅ `IMPLEMENTATION_COMPLETE.md` - This file 330 351 331 352 ### Modified 353 + 332 354 - ✅ `package.json` - Added workspaces, @semble/types dependency, zod 333 355 - ✅ `src/webapp/package.json` - Added @semble/types dependency 334 356 - ✅ `tsconfig.json` - Added paths for @semble/types ··· 340 362 - ✅ All webapp client files - Import from @semble/types 341 363 342 364 ### Deleted 365 + 343 366 - ✅ `src/modules/cards/application/dtos/` - Moved to @semble/types 344 367 - ✅ `src/modules/user/application/dtos/` - Moved to @semble/types 345 368 - ✅ `src/webapp/api-client/types/` - Moved to @semble/types
+127 -26
docs/plan/shared_type_unification.md
··· 9 9 **TL;DR**: You already have the important mapper (Domain → Application), and adding another layer (Application → Infrastructure) would just be ceremony when the types are identical. 10 10 11 11 **You're already doing this (correct DDD):** 12 + 12 13 ``` 13 14 Domain Entity → Use Case (maps to DTO) → Controller (passes through) → Frontend 14 15 ↓ ↓ ↓ ··· 18 19 **The shared types represent the Application Layer**, not Infrastructure. Both backend controllers and frontend clients consume the same Application Layer contract - this is **textbook Ports & Adapters pattern**. 19 20 20 21 **When you WOULD need mappers:** 22 + 21 23 - API versioning (supporting v1 and v2) 22 24 - Multiple protocols (REST + GraphQL + gRPC) 23 25 - Public API (hiding internal structures) ··· 32 34 ### What Exists Today 33 35 34 36 **Backend DTOs** (`src/modules/cards/application/dtos/`): 37 + 35 38 - `UserProfileDTO`, `UrlCardDTO`, `NoteCardDTO`, `CollectionDTO` 36 39 - `PaginationDTO`, `CardSortingDTO`, `CollectionSortingDTO` 37 40 - `FeedItemDTO` 38 41 - These are used as return types from Use Cases 39 42 40 43 **Frontend Types** (`src/webapp/api-client/types/`): 44 + 41 45 - `requests.ts` - Request parameter interfaces 42 46 - `responses.ts` - Response type interfaces (User, UrlCard, Collection, etc.) 43 47 - Nearly identical to backend DTOs (already unified in recent work) 44 48 45 49 **Current Flow**: 50 + 46 51 ``` 47 52 Domain Model → Use Case (returns DTO) → Controller (returns DTO as-is) → Frontend (expects matching type) 48 53 ``` ··· 58 63 ### DDD Layered Architecture Review 59 64 60 65 **Classic DDD Layers:** 66 + 61 67 1. **Domain Layer**: Entities, Value Objects, Domain Services, Domain Events 62 68 2. **Application Layer**: Use Cases, DTOs, Application Services 63 69 3. **Infrastructure Layer**: Controllers, Repositories, External Services ··· 126 132 ``` 127 133 128 134 **When you need this:** 135 + 129 136 - ✅ Public API that needs versioning independent of domain 130 137 - ✅ Domain model significantly different from API representation 131 138 - ✅ Multiple API formats (REST, GraphQL, gRPC) from same domain ··· 133 140 - ✅ Large team with separate domain/API teams 134 141 135 142 **Drawbacks:** 143 + 136 144 - ❌ High ceremony when DTO ≈ HTTP Response 137 145 - ❌ Boilerplate mapper code 138 146 - ❌ Slower iteration velocity ··· 168 176 ``` 169 177 170 178 **When this is appropriate:** 179 + 171 180 - ✅ Monorepo with tight frontend/backend coupling 172 181 - ✅ DTO and HTTP Response are identical (or nearly so) 173 182 - ✅ Private/internal API (not public third-party API) ··· 177 186 **Key insight:** The shared types represent the **Application Layer contract**, not Infrastructure. Both the backend Use Case and frontend are clients of this application layer contract. 178 187 179 188 **Drawbacks:** 189 + 180 190 - ⚠️ Harder to version API independently 181 191 - ⚠️ Frontend sees application layer types (but this may be fine) 182 192 ··· 193 203 ``` 194 204 195 205 **This is an anti-pattern** because: 206 + 196 207 - ❌ Use Cases depend on Infrastructure (breaks DDD layering) 197 208 - ❌ Application layer coupled to HTTP representation 198 209 - ❌ Can't reuse Use Cases for non-HTTP interfaces ··· 202 213 **Why this is the right choice for your codebase:** 203 214 204 215 1. **You're already mapping Domain → Application:** 216 + 205 217 ```typescript 206 218 // GetCollectionsForUrlUseCase.ts - lines 122-151 207 219 const enrichedCollections: CollectionDTO[] = await Promise.all( ··· 224 236 }) 225 237 ); 226 238 ``` 239 + 227 240 This is **proper DDD** - the Use Case orchestrates domain objects and produces DTOs. 228 241 229 242 2. **Your DTOs ARE your HTTP responses:** ··· 251 264 Add a separate HTTP Response layer if: 252 265 253 266 1. **API Versioning:** 267 + 254 268 ```typescript 255 269 // v1: { id, name } 256 270 // v2: { id, title } // renamed field ··· 262 276 ``` 263 277 264 278 2. **Different Representations:** 279 + 265 280 ```typescript 266 281 // REST API: flat structure 267 282 { collectionId: '123', authorId: '456' } ··· 271 286 ``` 272 287 273 288 3. **Hide Internal Details:** 289 + 274 290 ```typescript 275 291 // DTO has internal fields 276 292 interface CollectionDTO { 277 293 id: string; 278 294 name: string; 279 - internalAuditLog: AuditEntry[]; // don't expose 295 + internalAuditLog: AuditEntry[]; // don't expose 280 296 } 281 297 282 298 // HTTP response filters ··· 301 317 // These are Application Layer DTOs, not Infrastructure types 302 318 303 319 // common.ts - Core domain concepts 304 - export interface User { /* ... */ } 305 - export interface Collection { /* ... */ } 320 + export interface User { 321 + /* ... */ 322 + } 323 + export interface Collection { 324 + /* ... */ 325 + } 306 326 307 327 // requests.ts - Use Case inputs 308 - export interface GetCollectionsParams { /* ... */ } 328 + export interface GetCollectionsParams { 329 + /* ... */ 330 + } 309 331 310 332 // responses.ts - Use Case outputs 311 - export interface GetCollectionsResponse { /* ... */ } 333 + export interface GetCollectionsResponse { 334 + /* ... */ 335 + } 312 336 ``` 313 337 314 338 **Use Cases depend on these types:** 339 + 315 340 ```typescript 316 341 import { GetCollectionsResponse } from '@semble/types'; 317 342 ··· 323 348 ``` 324 349 325 350 **Controllers are thin pipes:** 351 + 326 352 ```typescript 327 353 import { GetCollectionsResponse } from '@semble/types'; 328 354 ··· 335 361 ``` 336 362 337 363 **Frontend consumes Application Layer contract:** 364 + 338 365 ```typescript 339 366 import { GetCollectionsResponse } from '@semble/types'; 340 367 ··· 348 375 ### Summary 349 376 350 377 **What we're doing:** 378 + 351 379 - ✅ Sharing **Application Layer** types between backend and frontend 352 380 - ✅ Use Cases map Domain → Application DTO (proper DDD) 353 381 - ✅ Controllers validate HTTP and pass through DTOs 354 382 - ✅ Single source of truth for application contracts 355 383 356 384 **What we're NOT doing:** 385 + 357 386 - ❌ Skipping Domain → Application mapping (we do this!) 358 387 - ❌ Letting infrastructure dictate application types 359 388 - ❌ Breaking DDD layer dependencies 360 389 361 390 **This is pragmatic DDD because:** 391 + 362 392 - Domain layer remains pure ✅ 363 393 - Application layer defines contracts ✅ 364 394 - Infrastructure depends on Application (correct direction) ✅ ··· 371 401 ### Three Levels of Validation 372 402 373 403 #### 1. Controller-Level Validation (Infrastructure Layer) 404 + 374 405 **Purpose**: Validate HTTP request structure and types 375 406 **Implementation**: Use Zod schemas at controller entry points 376 407 **Examples**: 408 + 377 409 - Is `url` parameter present and a string? 378 410 - Is `page` a positive integer? 379 411 - Are required fields in request body present? ··· 404 436 ``` 405 437 406 438 #### 2. Use Case-Level Validation (Application Layer) 439 + 407 440 **Purpose**: Validate business rules and create domain objects 408 441 **Implementation**: Use domain value objects (already doing this!) 409 442 **Examples**: 443 + 410 444 - `URL.create(query.url)` - validates URL format using domain rules 411 445 - `CardId.createFromString(id)` - validates ID format 412 446 - Business logic validation (e.g., "user can only have 50 collections") ··· 416 450 This layer is **already correct** in the codebase! 417 451 418 452 #### 3. Domain-Level Validation (Domain Layer) 453 + 419 454 **Purpose**: Enforce invariants and domain rules 420 455 **Implementation**: Value Objects and Entity constructors 421 456 **Examples**: 457 + 422 458 - `URL` value object validates URL format 423 459 - `CollectionName` might enforce length limits 424 460 - `Email` value object validates email format ··· 480 516 ### Type Organization 481 517 482 518 **`src/types/src/api/common.ts`** - Shared domain concepts: 519 + 483 520 ```typescript 484 521 export interface User { 485 522 id: string; ··· 515 552 ``` 516 553 517 554 **`src/types/src/api/requests.ts`** - API request types: 555 + 518 556 ```typescript 519 557 // Base interfaces 520 558 export interface PaginationParams { ··· 528 566 } 529 567 530 568 // Query parameter interfaces 531 - export interface GetCollectionsForUrlParams extends PaginationParams, SortingParams { 569 + export interface GetCollectionsForUrlParams 570 + extends PaginationParams, 571 + SortingParams { 532 572 url: string; 533 573 } 534 574 ··· 543 583 ``` 544 584 545 585 **`src/types/src/api/responses.ts`** - API response types: 586 + 546 587 ```typescript 547 588 import { User, Pagination, CardSorting, CollectionSorting } from './common'; 548 589 ··· 594 635 ### Phase 0: Prerequisites 595 636 596 637 **Install Zod for validation:** 638 + 597 639 ```bash 598 640 npm install zod 599 641 ``` ··· 603 645 #### 1.1 Configure Workspace Root 604 646 605 647 Update `package.json`: 648 + 606 649 ```json 607 650 { 608 651 "name": "semble", 609 - "workspaces": [ 610 - "src/types", 611 - "src/webapp" 612 - ], 652 + "workspaces": ["src/types", "src/webapp"], 613 653 "scripts": { 614 654 "build:types": "npm run build --workspace=@semble/types", 615 655 "dev:types": "npm run dev --workspace=@semble/types", ··· 621 661 #### 1.2 Create Types Package 622 662 623 663 Create `src/types/package.json`: 664 + 624 665 ```json 625 666 { 626 667 "name": "@semble/types", ··· 641 682 ``` 642 683 643 684 Create `src/types/tsconfig.json`: 685 + 644 686 ```json 645 687 { 646 688 "compilerOptions": { ··· 674 716 #### 2.1 Add Dependency 675 717 676 718 Update root `package.json`: 719 + 677 720 ```json 678 721 { 679 722 "dependencies": { ··· 690 733 **Example: `GetCollectionsForUrlUseCase.ts`** 691 734 692 735 Before: 736 + 693 737 ```typescript 694 738 import { CollectionDTO, PaginationDTO, CollectionSortingDTO } from '../../dtos'; 695 739 ··· 701 745 ``` 702 746 703 747 After: 748 + 704 749 ```typescript 705 750 import { GetCollectionsForUrlResponse } from '@semble/types'; 706 751 ··· 718 763 **Example: `GetCollectionsForUrlController.ts`** 719 764 720 765 Before: 766 + 721 767 ```typescript 722 768 async executeImpl(req: Request, res: Response): Promise<any> { 723 769 const { url } = req.query; ··· 729 775 ``` 730 776 731 777 After: 778 + 732 779 ```typescript 733 780 import { z } from 'zod'; 734 781 import { GetCollectionsForUrlParams, GetCollectionsForUrlResponse } from '@semble/types'; ··· 771 818 #### 2.4 Remove Old DTOs 772 819 773 820 Once all use cases and controllers are updated: 821 + 774 822 ```bash 775 823 rm -rf src/modules/cards/application/dtos/ 776 824 rm -rf src/modules/user/application/dtos/ ··· 781 829 #### 3.1 Add Dependency 782 830 783 831 Update `src/webapp/package.json`: 832 + 784 833 ```json 785 834 { 786 835 "dependencies": { ··· 796 845 **`src/webapp/api-client/ApiClient.ts`** 797 846 798 847 Before: 848 + 799 849 ```typescript 800 850 import type { 801 851 GetCollectionsForUrlParams, ··· 804 854 ``` 805 855 806 856 After: 857 + 807 858 ```typescript 808 859 import type { 809 860 GetCollectionsForUrlParams, ··· 822 873 #### 4.1 Add Development Scripts 823 874 824 875 Update root `package.json`: 876 + 825 877 ```json 826 878 { 827 879 "scripts": { ··· 838 890 #### 4.2 Update Backend tsconfig.json 839 891 840 892 Ensure backend can resolve workspace packages: 893 + 841 894 ```json 842 895 { 843 896 "compilerOptions": { ··· 851 904 ### Phase 5: Testing & Validation 852 905 853 906 #### 5.1 Type Checking 907 + 854 908 ```bash 855 909 npm run type-check # Check backend 856 910 npm run type-check --workspace=@semble/webapp # Check frontend 857 911 ``` 858 912 859 913 #### 5.2 Build Everything 914 + 860 915 ```bash 861 916 npm run build:all 862 917 ``` 863 918 864 919 #### 5.3 Runtime Testing 920 + 865 921 - Start dev servers: `npm run dev` 866 922 - Test each modified endpoint 867 923 - Verify request validation works (try invalid requests) ··· 870 926 ## Migration Checklist 871 927 872 928 ### Phase 1: Shared Types Package ✅ 929 + 873 930 - [ ] Update root `package.json` with workspaces configuration 874 931 - [ ] Create `src/types/package.json` 875 932 - [ ] Create `src/types/tsconfig.json` ··· 882 939 - [ ] Verify build output in `src/types/dist/` 883 940 884 941 ### Phase 2: Backend Migration ✅ 942 + 885 943 - [ ] Install zod: `npm install zod` 886 944 - [ ] Add `@semble/types` to root dependencies 887 945 - [ ] Run `npm install` to link workspace ··· 898 956 - [ ] Fix any type errors 899 957 900 958 ### Phase 3: Frontend Migration ✅ 959 + 901 960 - [ ] Add `@semble/types` to `src/webapp/package.json` 902 961 - [ ] Run `npm install` in webapp 903 962 - [ ] Update `src/webapp/api-client/ApiClient.ts` imports ··· 908 967 - [ ] Fix any type errors 909 968 910 969 ### Phase 4: Development Workflow ✅ 970 + 911 971 - [ ] Add dev scripts to root package.json 912 972 - [ ] Test concurrent development: `npm run dev` 913 973 - [ ] Make a test change to shared types ··· 915 975 - [ ] Test build pipeline: `npm run build:all` 916 976 917 977 ### Phase 5: Testing ✅ 978 + 918 979 - [ ] Backend tests pass 919 980 - [ ] Frontend tests pass 920 981 - [ ] Manual testing of key endpoints: ··· 931 992 ### High Priority (Core Queries) 932 993 933 994 **Use Cases:** 995 + 934 996 - `src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts` 935 997 - `src/modules/cards/application/useCases/queries/GetCollectionsUseCase.ts` 936 998 - `src/modules/cards/application/useCases/queries/GetLibrariesForCardUseCase.ts` ··· 941 1003 - `src/modules/feeds/application/useCases/queries/GetGlobalFeedUseCase.ts` 942 1004 943 1005 **Controllers:** 1006 + 944 1007 - All controllers in `src/modules/cards/infrastructure/http/controllers/` 945 1008 - `src/modules/feeds/infrastructure/http/controllers/GetGlobalFeedController.ts` 946 1009 947 1010 **Frontend:** 1011 + 948 1012 - `src/webapp/api-client/ApiClient.ts` 949 1013 - All files in `src/webapp/api-client/clients/` 950 1014 951 1015 ### Medium Priority (Commands) 952 1016 953 1017 **Use Cases:** 1018 + 954 1019 - `src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts` 955 1020 - `src/modules/cards/application/useCases/commands/CreateCollectionUseCase.ts` 956 1021 - ... (other command use cases) ··· 964 1029 ## Best Practices 965 1030 966 1031 ### Type Naming Conventions 1032 + 967 1033 - **Requests**: `{Verb}{Resource}Request` (e.g., `AddUrlToLibraryRequest`) 968 1034 - **Params**: `Get{Resource}Params` (e.g., `GetCollectionsForUrlParams`) 969 1035 - **Responses**: `{Verb}{Resource}Response` (e.g., `GetCollectionsForUrlResponse`) ··· 972 1038 ### Validation Patterns 973 1039 974 1040 **Controller validation (structure & types):** 1041 + 975 1042 ```typescript 976 1043 const schema = z.object({ 977 1044 requiredString: z.string().min(1), ··· 986 1053 ``` 987 1054 988 1055 **Use case validation (business rules):** 1056 + 989 1057 ```typescript 990 1058 const urlResult = URL.create(params.url); 991 1059 if (urlResult.isErr()) { ··· 994 1062 ``` 995 1063 996 1064 ### Development Workflow 1065 + 997 1066 1. **Always run types in watch mode** during development: `npm run dev:types` 998 1067 2. **Make type changes first** before implementing features 999 1068 3. **Type-check frequently**: `npm run type-check` ··· 1004 1073 ### What We're NOT Doing (And Why That's OK) 1005 1074 1006 1075 ❌ **Separate DTO and HTTP Response mapping layer**: 1076 + 1007 1077 - Reason: Application DTOs and HTTP responses are identical 1008 1078 - Reality: Controllers would just be identity mappers (pointless ceremony) 1009 1079 - When to add: If you need API versioning, different client representations, or hide internal fields 1010 1080 - See "When Would You Need Mappers?" section above for specific scenarios 1011 1081 1012 1082 ❌ **Runtime validation of Use Case outputs**: 1083 + 1013 1084 - Reason: TypeScript compilation guarantees response shape from Use Cases 1014 1085 - Alternative: Could add Zod validation of DTO construction, but not needed initially 1015 1086 - When to add: If you have bugs where Use Cases return malformed DTOs 1016 1087 1017 1088 ❌ **OpenAPI schema generation**: 1089 + 1018 1090 - Reason: Can add later if needed (Zod schemas make this easy) 1019 1091 - Benefit: Shared types + Zod schemas = future OpenAPI generation is trivial 1020 1092 - When to add: When you want API documentation, client SDK generation, or contract testing 1021 1093 1022 1094 ❌ **Separate versioning of types package**: 1095 + 1023 1096 - Reason: Monorepo with synchronized deploys (frontend/backend always in sync) 1024 1097 - When to add: If you publish a public API or have multiple clients on different versions 1025 1098 - Note: For now, Git commits provide version history 1026 1099 1027 1100 ❌ **Multiple API representations (REST, GraphQL, gRPC)**: 1101 + 1028 1102 - Reason: Only building REST API currently 1029 1103 - When to add: When you need GraphQL, gRPC, or other protocols (then add mappers) 1030 1104 ··· 1049 1123 ✅ **Type safety across boundaries**: Shared Application contracts prevent drift 1050 1124 1051 1125 The key insight: **Sharing Application Layer types between backend and frontend is not a DDD violation** when: 1126 + 1052 1127 1. Both are clients of the same Application Layer 1053 1128 2. They deploy together (monorepo) 1054 1129 3. The Application DTOs appropriately abstract the Domain Model ··· 1056 1131 ## Troubleshooting 1057 1132 1058 1133 ### "Cannot find module '@semble/types'" 1134 + 1059 1135 ```bash 1060 1136 # From root 1061 1137 npm install ··· 1063 1139 ``` 1064 1140 1065 1141 ### Types package not updating 1142 + 1066 1143 ```bash 1067 1144 # Restart types watch mode 1068 1145 npm run dev:types 1069 1146 ``` 1070 1147 1071 1148 ### Import path errors in IDE 1149 + 1072 1150 - Restart TypeScript server in your IDE 1073 1151 - Check `tsconfig.json` paths configuration 1074 1152 - Verify `node_modules/@semble/types` symlink exists 1075 1153 1076 1154 ### Validation errors not showing 1155 + 1077 1156 - Check Zod schema matches the expected request shape 1078 1157 - Use `.safeParse()` not `.parse()` to get detailed errors 1079 1158 - Log `validation.error.format()` for debugging ··· 1081 1160 ## Future Enhancements 1082 1161 1083 1162 ### Short Term 1163 + 1084 1164 - [ ] Add Zod schemas for all controllers 1085 1165 - [ ] Create shared Zod utilities for common patterns (pagination, sorting) 1086 1166 - [ ] Add request/response logging middleware 1087 1167 1088 1168 ### Medium Term 1169 + 1089 1170 - [ ] Generate OpenAPI spec from shared types 1090 1171 - [ ] Create API documentation from types 1091 1172 - [ ] Add integration tests using shared types 1092 1173 - [ ] Create type-safe test factories 1093 1174 1094 1175 ### Long Term 1176 + 1095 1177 - [ ] Publish types package to private npm registry 1096 1178 - [ ] Implement breaking change detection (type diff checks) 1097 1179 - [ ] Add runtime response validation in development mode ··· 1102 1184 ### What We Preserved (Proper DDD) 1103 1185 1104 1186 ✅ **Domain Layer Purity**: 1187 + 1105 1188 - Domain entities, value objects, and domain services have no external dependencies 1106 1189 - Domain models encapsulate business logic and invariants 1107 1190 - Domain layer doesn't know about HTTP, DTOs, or frontend 1108 1191 1109 1192 ✅ **Application Layer Orchestration**: 1193 + 1110 1194 - Use Cases orchestrate domain objects to fulfill application requirements 1111 1195 - Use Cases map rich domain models → simple DTOs (proper anti-corruption layer) 1112 1196 - DTOs hide domain complexity from external consumers 1113 1197 - Application layer defines the contract for consumers (backend and frontend) 1114 1198 1115 1199 ✅ **Infrastructure Layer Dependency Direction**: 1200 + 1116 1201 - Controllers depend on Application types (correct: Infrastructure → Application) 1117 1202 - Controllers do NOT define types that Application depends on (would be wrong) 1118 1203 - Infrastructure implements Application contracts, not the other way around 1119 1204 1120 1205 ✅ **Three-Tier Validation Hierarchy**: 1206 + 1121 1207 - **Domain**: Invariants enforced in value objects and entities 1122 1208 - **Application**: Business rules validated in use cases 1123 1209 - **Infrastructure**: HTTP request structure validated in controllers 1124 1210 1125 1211 ✅ **Separation of Concerns**: 1212 + 1126 1213 - Domain logic in entities/value objects 1127 1214 - Application logic in use cases 1128 1215 - HTTP transport details in controllers ··· 1131 1218 ### What's Actually Happening (Not a Compromise) 1132 1219 1133 1220 ✅ **Shared Application Layer Types**: 1221 + 1134 1222 - `@semble/types` represents the **Application Layer contract** 1135 1223 - Backend Use Cases produce these contracts 1136 1224 - Frontend API Client consumes these contracts 1137 1225 - Both are clients of the Application Layer (this is correct DDD!) 1138 1226 1139 1227 ✅ **Controllers as Thin Adapters**: 1228 + 1140 1229 - Controllers adapt HTTP → Application Layer (parse request, call use case) 1141 1230 - Controllers adapt Application Layer → HTTP (serialize DTO to JSON) 1142 1231 - No additional mapping needed when DTO shape = HTTP response shape ··· 1145 1234 ### Why This Is Good DDD (Not Just "OK for Startups") 1146 1235 1147 1236 **From Eric Evans / Martin Fowler:** 1237 + 1148 1238 - DTOs exist to cross architectural boundaries ✅ (we do this) 1149 1239 - Application Layer should be independent of delivery mechanism ✅ (it is) 1150 1240 - Infrastructure should depend on Application, not vice versa ✅ (correct) 1151 1241 - Shared kernel is acceptable when bounded contexts align ✅ (monorepo, single app) 1152 1242 1153 1243 **From Hexagonal Architecture (Ports & Adapters):** 1244 + 1154 1245 - Application Layer defines "ports" (interfaces/contracts) ✅ (`@semble/types`) 1155 1246 - Infrastructure provides "adapters" (controllers, API clients) ✅ (thin HTTP adapters) 1156 1247 - Multiple adapters can implement same port ✅ (backend controller, frontend client) 1157 1248 - This is the **textbook pattern**! 1158 1249 1159 1250 **When you'd need an additional mapping layer:** 1251 + 1160 1252 1. **API Versioning**: Supporting v1 and v2 simultaneously 1161 1253 2. **Different Protocols**: REST + GraphQL + gRPC from same Application Layer 1162 1254 3. **Public API**: Need to hide internal structures ··· 1198 1290 The quality of this architecture depends on whether your DTOs properly abstract the domain: 1199 1291 1200 1292 ✅ **Good DTO Design** (what you have): 1293 + 1201 1294 ```typescript 1202 1295 // Domain: Rich model with behavior 1203 1296 class Collection { 1204 - collectionId: CollectionId; // Value object 1205 - name: CollectionName; // Value object with validation 1206 - authorId: UserId; // Value object 1207 - description?: CollectionDescription; // Value object 1208 - cardIds: CardId[]; // List of value objects 1209 - addCard(cardId: CardId) { /* logic */ } 1210 - removeCard(cardId: CardId) { /* logic */ } 1297 + collectionId: CollectionId; // Value object 1298 + name: CollectionName; // Value object with validation 1299 + authorId: UserId; // Value object 1300 + description?: CollectionDescription; // Value object 1301 + cardIds: CardId[]; // List of value objects 1302 + addCard(cardId: CardId) { 1303 + /* logic */ 1304 + } 1305 + removeCard(cardId: CardId) { 1306 + /* logic */ 1307 + } 1211 1308 } 1212 1309 1213 1310 // DTO: Simple data structure for transfer 1214 1311 interface Collection { 1215 - id: string; // Unwrapped value 1216 - name: string; // Unwrapped value 1217 - author: User; // Enriched relationship 1218 - description?: string; // Unwrapped value 1219 - cardCount: number; // Computed property 1220 - createdAt: string; // ISO string 1221 - updatedAt: string; // ISO string 1312 + id: string; // Unwrapped value 1313 + name: string; // Unwrapped value 1314 + author: User; // Enriched relationship 1315 + description?: string; // Unwrapped value 1316 + cardCount: number; // Computed property 1317 + createdAt: string; // ISO string 1318 + updatedAt: string; // ISO string 1222 1319 } 1223 1320 ``` 1224 1321 1225 1322 This is **good separation**: 1323 + 1226 1324 - Domain has rich behavior, validation, invariants 1227 1325 - DTO is simple, serializable, consumer-friendly 1228 1326 - Use Case does the mapping (proper layer responsibility) 1229 1327 1230 1328 ❌ **Bad DTO Design** (anti-pattern): 1329 + 1231 1330 ```typescript 1232 1331 // Exposing domain internals 1233 1332 interface CollectionDTO { 1234 - collectionId: CollectionId; // WRONG: exposing domain value object 1235 - _aggregateVersion: number; // WRONG: exposing internal details 1333 + collectionId: CollectionId; // WRONG: exposing domain value object 1334 + _aggregateVersion: number; // WRONG: exposing internal details 1236 1335 domainEvents: DomainEvent[]; // WRONG: leaking domain events 1237 1336 } 1238 1337 ``` ··· 1402 1501 ``` 1403 1502 1404 1503 ### Key Changes 1504 + 1405 1505 1. ✅ Single `Collection` type (not duplicated) 1406 1506 2. ✅ Single `GetCollectionsForUrlResponse` type 1407 1507 3. ✅ Controller validates with Zod schema ··· 1422 1522 - **Stays simple** and avoids overengineering 1423 1523 1424 1524 The result is a **barebones but reliable** type system that: 1525 + 1425 1526 - Catches errors at compile time 1426 1527 - Validates requests at runtime 1427 1528 - Maintains a single source of truth
+30 -16
docs/shared_types.md
··· 54 54 { 55 55 "name": "annos", 56 56 "version": "1.0.0", 57 - "workspaces": [ 58 - "src/types", 59 - "src/webapp", 60 - "." 61 - ], 57 + "workspaces": ["src/types", "src/webapp", "."], 62 58 "scripts": { 63 59 "build:types": "npm run build --workspace=@annos/types", 64 60 "dev:types": "npm run dev --workspace=@annos/types", ··· 80 76 "description": "Shared TypeScript types for Annos API", 81 77 "main": "dist/index.js", 82 78 "types": "dist/index.d.ts", 83 - "files": [ 84 - "dist/**/*" 85 - ], 79 + "files": ["dist/**/*"], 86 80 "scripts": { 87 81 "build": "tsc", 88 82 "dev": "tsc --watch", ··· 125 119 { 126 120 "name": "@annos/webapp", 127 121 "dependencies": { 128 - "@annos/types": "workspace:*", 122 + "@annos/types": "workspace:*" 129 123 // ... existing dependencies 130 124 } 131 125 } ··· 138 132 Move and organize existing webapp types into the shared package: 139 133 140 134 **src/types/src/api/common.ts:** 135 + 141 136 ```typescript 142 137 export interface User { 143 138 id: string; ··· 173 168 ``` 174 169 175 170 **src/types/src/api/requests.ts:** 171 + 176 172 ```typescript 177 173 // Copy all request types from src/webapp/api-client/types/requests.ts 178 174 export interface PaginationParams { ··· 189 185 ``` 190 186 191 187 **src/types/src/api/responses.ts:** 188 + 192 189 ```typescript 193 - import { User, Pagination, CardSorting, CollectionSorting, FeedPagination } from './common'; 190 + import { 191 + User, 192 + Pagination, 193 + CardSorting, 194 + CollectionSorting, 195 + FeedPagination, 196 + } from './common'; 194 197 195 198 // Copy all response types from src/webapp/api-client/types/responses.ts 196 199 export interface UrlCard { ··· 204 207 ``` 205 208 206 209 **src/types/src/api/index.ts:** 210 + 207 211 ```typescript 208 212 export * from './common'; 209 213 export * from './requests'; ··· 211 215 ``` 212 216 213 217 **src/types/src/index.ts:** 218 + 214 219 ```typescript 215 220 export * from './api'; 216 221 ``` ··· 242 247 } from './types/responses'; 243 248 244 249 // NEW: 245 - import type { 246 - GetUrlCardsResponse, 247 - AddUrlToLibraryRequest, 248 - } from '@annos/types'; 250 + import type { GetUrlCardsResponse, AddUrlToLibraryRequest } from '@annos/types'; 249 251 ``` 250 252 251 253 #### Step 3.3: Remove Old Type Files ··· 276 278 277 279 export class GetUrlCardsUseCase { 278 280 async execute( 279 - query: GetUrlCardsQuery 280 - ): Promise<Result<GetUrlCardsResponse, ValidationError | AppError.UnexpectedError>> { 281 + query: GetUrlCardsQuery, 282 + ): Promise< 283 + Result<GetUrlCardsResponse, ValidationError | AppError.UnexpectedError> 284 + > { 281 285 // Implementation must return GetUrlCardsResponse type 282 286 return ok({ 283 287 cards: enrichedCards, ··· 356 360 Add Zod schemas for runtime validation: 357 361 358 362 **src/types/src/validation/index.ts:** 363 + 359 364 ```typescript 360 365 import { z } from 'zod'; 361 366 ··· 385 390 ## Migration Checklist 386 391 387 392 ### Phase 1: Infrastructure ✅ 393 + 388 394 - [ ] Update root `package.json` with workspaces 389 395 - [ ] Create `src/types/package.json` 390 396 - [ ] Create `src/types/tsconfig.json` ··· 392 398 - [ ] Run `npm install` to setup workspace 393 399 394 400 ### Phase 2: Type Migration ✅ 401 + 395 402 - [ ] Create `src/types/src/api/common.ts` 396 403 - [ ] Create `src/types/src/api/requests.ts` 397 404 - [ ] Create `src/types/src/api/responses.ts` ··· 400 407 - [ ] Build shared types: `npm run build:types` 401 408 402 409 ### Phase 3: Frontend Migration ✅ 410 + 403 411 - [ ] Update all imports in webapp to use `@annos/types` 404 412 - [ ] Remove old type files: `rm -rf src/webapp/api-client/types/` 405 413 - [ ] Test webapp compilation: `npm run type-check --workspace=@annos/webapp` 406 414 407 415 ### Phase 4: Backend Migration ✅ 416 + 408 417 - [ ] Add shared types dependency to root package 409 418 - [ ] Update use cases to import and return shared types 410 419 - [ ] Update controllers to use shared types 411 420 - [ ] Test backend compilation: `npm run type-check` 412 421 413 422 ### Phase 5: Development Setup ✅ 423 + 414 424 - [ ] Add development scripts to root package.json 415 425 - [ ] Test concurrent development: `npm run dev` 416 426 - [ ] Verify hot reload works for type changes 417 427 418 428 ### Phase 6: Validation ✅ 429 + 419 430 - [ ] Run full type check across all packages 420 431 - [ ] Test API endpoints return correct types 421 432 - [ ] Add runtime validation (optional) ··· 424 435 ## Best Practices 425 436 426 437 ### Type Naming Conventions 438 + 427 439 - **Requests**: `{Action}{Resource}Request` (e.g., `GetUrlCardsRequest`) 428 440 - **Responses**: `{Action}{Resource}Response` (e.g., `GetUrlCardsResponse`) 429 441 - **Common types**: Descriptive names (e.g., `User`, `Pagination`) 430 442 431 443 ### Development Workflow 444 + 432 445 1. **Always run shared types in watch mode** during development 433 446 2. **Make type changes first** before implementing features 434 447 3. **Use TypeScript strict mode** to catch issues early 435 448 4. **Version shared types** when making breaking changes 436 449 437 450 ### Error Handling 451 + 438 452 - Define consistent error response types 439 453 - Use discriminated unions for different error types 440 454 - Include error codes and messages in shared types
+18
docs/shared_types_implementation_status.md
··· 3 3 ## ✅ Successfully Completed 4 4 5 5 ### Infrastructure Setup 6 + 6 7 - ✅ Created `@semble/types` package at `src/types/` 7 8 - ✅ Configured npm workspaces in root `package.json` 8 9 - ✅ Set up TypeScript compilation for types package ··· 11 12 - ✅ Installed zod for validation (version 3.22.4) 12 13 13 14 ### Shared Types Package (`src/types/src/api/`) 15 + 14 16 - ✅ `common.ts` - User, Pagination, Sorting interfaces 15 17 - ✅ `requests.ts` - All request parameter types 16 18 - ✅ `responses.ts` - All response types 17 19 - ✅ Built successfully with TypeScript 18 20 19 21 ### Backend Migration (Cards/Feeds Modules) 22 + 20 23 - ✅ **GetCollectionsForUrlUseCase** - Uses `GetCollectionsForUrlResponse` from `@semble/types` 21 24 - ✅ **GetGlobalFeedUseCase** - Uses `GetGlobalFeedResponse` from `@semble/types` 22 25 - ✅ **GetCollectionsForUrlController** - Added Zod validation schema ··· 24 27 - ✅ Removed old `src/modules/cards/application/dtos/` directory 25 28 26 29 ### Frontend Migration 30 + 27 31 - ✅ **ApiClient.ts** - Imports all types from `@semble/types` 28 32 - ✅ All client files (`QueryClient`, `CardClient`, etc.) - Updated to import from `@semble/types` 29 33 - ✅ Removed old `src/webapp/api-client/types/` directory ··· 31 35 ## ⚠️ Remaining Work (Not Critical for Core Functionality) 32 36 33 37 ### Use Cases to Migrate 38 + 34 39 These use cases still import from old `../../dtos` and need migration: 35 40 36 41 **Cards Module:** 42 + 37 43 - `GetCollectionsUseCase.ts` 38 44 - `GetLibrariesForCardUseCase.ts` 39 45 - `GetLibrariesForUrlUseCase.ts` ··· 42 48 - `GetUrlStatusForMyLibraryUseCase.ts` 43 49 44 50 **Pattern to follow:** 51 + 45 52 ```typescript 46 53 // OLD 47 54 import { CollectionDTO, PaginationDTO } from '../../dtos'; ··· 52 59 ``` 53 60 54 61 ### User Module DTOs 62 + 55 63 The following files reference deleted User DTOs and need new types added to `@semble/types`: 56 64 57 65 **Missing types to add:** 66 + 58 67 - `OAuthCallbackDTO` (used by OAuth flows) 59 68 - `TokenDTO` (used by token services) 60 69 - `UserDTO` (used by user mappers) 61 70 - `LoginWithAppPasswordDTO` (used by login use case) 62 71 63 72 **Affected files:** 73 + 64 74 - `src/modules/atproto/infrastructure/services/AtProtoOAuthProcessor.ts` 65 75 - `src/modules/atproto/infrastructure/services/FakeAtProtoOAuthProcessor.ts` 66 76 - `src/modules/user/application/mappers/UserMap.ts` ··· 73 83 Add these types to `src/types/src/api/responses.ts` or create a new `auth.ts` file. 74 84 75 85 ### Controllers to Add Zod Validation 86 + 76 87 All other controllers in `src/modules/cards/infrastructure/http/controllers/` should follow the same pattern: 77 88 78 89 ```typescript ··· 94 105 ## 🎯 What's Working Right Now 95 106 96 107 ### End-to-End Type Safety 108 + 97 109 1. **Frontend** → API request using types from `@semble/types` 98 110 2. **Controller** → Validates request with Zod schema 99 111 3. **Use Case** → Returns response typed as `@semble/types` interface ··· 101 113 5. **Frontend** → Receives response with full type safety 102 114 103 115 ### Example Flow (Working) 116 + 104 117 ``` 105 118 Frontend: getCollectionsForUrl(params: GetCollectionsForUrlParams) 106 119 ··· 112 125 ``` 113 126 114 127 ### Compile Status 128 + 115 129 - **Frontend**: ✅ Should compile (needs verification with `npm run type-check --workspace=@semble/webapp`) 116 130 - **Backend**: ⚠️ 20 type errors (all in non-migrated use cases and user module) 117 131 - **Types Package**: ✅ Compiles successfully ··· 121 135 ### For Each Remaining Use Case 122 136 123 137 1. **Update imports:** 138 + 124 139 ```typescript 125 140 // Replace 126 141 import { SomeDTO } from '../../dtos'; ··· 129 144 ``` 130 145 131 146 2. **Update return type:** 147 + 132 148 ```typescript 133 149 // Replace 134 150 export interface GetSomeResult { ··· 144 160 ### For User Module 145 161 146 162 1. **Add missing types to `@semble/types`:** 163 + 147 164 ```typescript 148 165 // src/types/src/api/auth.ts 149 166 export interface TokenResponse { ··· 209 226 ## 📚 Reference Implementation 210 227 211 228 See these files for the pattern to follow: 229 + 212 230 - **Use Case**: `src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts` 213 231 - **Controller**: `src/modules/cards/infrastructure/http/controllers/GetCollectionsForUrlController.ts` 214 232 - **Frontend**: `src/webapp/api-client/ApiClient.ts`
+5 -1
src/modules/cards/application/useCases/queries/GetCollectionsUseCase.ts
··· 8 8 import { IProfileService } from 'src/modules/cards/domain/services/IProfileService'; 9 9 import { DIDOrHandle } from 'src/modules/atproto/domain/DIDOrHandle'; 10 10 import { IIdentityResolutionService } from 'src/modules/atproto/domain/services/IIdentityResolutionService'; 11 - import { CollectionDTO, PaginationDTO, CollectionSortingDTO } from '@semble/types'; 11 + import { 12 + CollectionDTO, 13 + PaginationDTO, 14 + CollectionSortingDTO, 15 + } from '@semble/types'; 12 16 13 17 export interface GetCollectionsQuery { 14 18 curatorId: string;
+1 -3
src/modules/cards/infrastructure/http/controllers/GetCollectionsForUrlController.ts
··· 13 13 url: z.string().min(1, 'URL is required'), 14 14 page: z.coerce.number().int().positive().optional(), 15 15 limit: z.coerce.number().int().positive().max(100).optional(), 16 - sortBy: z 17 - .enum(['name', 'createdAt', 'updatedAt', 'cardCount']) 18 - .optional(), 16 + sortBy: z.enum(['name', 'createdAt', 'updatedAt', 'cardCount']).optional(), 19 17 sortOrder: z.enum(['asc', 'desc']).optional(), 20 18 }); 21 19
+7 -1
src/types/src/api/responses.ts
··· 1 - import { User, Pagination, CardSorting, CollectionSorting, FeedPagination } from './common'; 1 + import { 2 + User, 3 + Pagination, 4 + CardSorting, 5 + CollectionSorting, 6 + FeedPagination, 7 + } from './common'; 2 8 3 9 // Command response types 4 10 export interface AddUrlToLibraryResponse {
+4 -1
src/webapp/components/UrlCardForm.tsx
··· 59 59 // Get existing collections for this card (filtered by current user) 60 60 const existingCollections = useMemo(() => { 61 61 if (!existingCardCollections || !userId) return []; 62 - return existingCardCollections.map((c: any) => ({ ...c, authorId: userId })); 62 + return existingCardCollections.map((c: any) => ({ 63 + ...c, 64 + authorId: userId, 65 + })); 63 66 }, [existingCardCollections, userId]); 64 67 65 68 const handleSubmit = async (e: React.FormEvent) => {
+5 -1
src/webapp/features/cards/components/addCardToModal/CardToBeAddedPreview.tsx
··· 12 12 Tooltip, 13 13 } from '@mantine/core'; 14 14 import Link from 'next/link'; 15 - import { GetUrlStatusForMyLibraryResponse, UrlCard, Collection } from '@/api-client'; 15 + import { 16 + GetUrlStatusForMyLibraryResponse, 17 + UrlCard, 18 + Collection, 19 + } from '@/api-client'; 16 20 import { BiCollection } from 'react-icons/bi'; 17 21 import { LuLibrary } from 'react-icons/lu'; 18 22 import { getDomain } from '@/lib/utils/link';
+15 -13
src/webapp/features/feeds/components/feedActivityStatus/FeedActivityStatus.tsx
··· 36 36 ) : ( 37 37 <Fragment> 38 38 added to{' '} 39 - {displayedCollections.map((collection: Collection, index: number) => ( 40 - <span key={collection.id}> 41 - <Anchor 42 - component={Link} 43 - href={`/profile/${collection.author.handle}/collections/${getRecordKey(collection.uri!)}`} 44 - c="grape" 45 - fw={500} 46 - > 47 - {collection.name} 48 - </Anchor> 49 - {index < displayedCollections.length - 1 ? ', ' : ''} 50 - </span> 51 - ))} 39 + {displayedCollections.map( 40 + (collection: Collection, index: number) => ( 41 + <span key={collection.id}> 42 + <Anchor 43 + component={Link} 44 + href={`/profile/${collection.author.handle}/collections/${getRecordKey(collection.uri!)}`} 45 + c="grape" 46 + fw={500} 47 + > 48 + {collection.name} 49 + </Anchor> 50 + {index < displayedCollections.length - 1 ? ', ' : ''} 51 + </span> 52 + ), 53 + )} 52 54 {remainingCount > 0 && 53 55 ` and ${remainingCount} other collection${remainingCount > 1 ? 's' : ''}`} 54 56 </Fragment>
+3 -1
src/webapp/features/semble/components/SembleHeader/SembleHeader.tsx
··· 55 55 </Stack> 56 56 {metadata.description && ( 57 57 <Spoiler showLabel={'Read more'} hideLabel={'See less'}> 58 - <Text c="gray" fw={500}>{metadata.description}</Text> 58 + <Text c="gray" fw={500}> 59 + {metadata.description} 60 + </Text> 59 61 </Spoiler> 60 62 )} 61 63 </Stack>
+1 -1
src/webapp/features/semble/components/addedByCard/AddedByCard.tsx
··· 1 - import type { GetLibrariesForUrlResponse } from '@/api-client/types'; 1 + import { GetLibrariesForUrlResponse } from '@/api-client'; 2 2 import { getRelativeTime } from '@/lib/utils/time'; 3 3 import { Avatar, Card, Group, Stack, Text } from '@mantine/core'; 4 4 import Link from 'next/link';