nexus/.planning/phases/25-file-system/25-01-SUMMARY.md

8.1 KiB

phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
25-file-system 01 api
file-upload
multer
streaming
express
drizzle
chat-files
phase provides
25-00 chat_files and chat_file_references DB schema, shared types (ChatFile, ChatFileReference), Zod validators (uploadChatFileSchema, createFileReferenceSchema)
chatFileService(db) with CRUD + reference operations for chat_files and chat_file_references
deriveCategory() helper mapping MIME types to image/code/document/other
chatFileRoutes(db, storage) Express router for upload, list, download, reference, attach endpoints
Routes wired into app.ts via api.use(chatFileRoutes(db, opts.storageService))
25-02 (UI upload hook — calls POST /conversations/:id/files)
25-03 (ChatFileDropZone — calls upload route and GET /files/:fileId/content)
added patterns
multer memoryStorage for multipart file upload (same as assets.ts)
stream.pipe(res) for file content download with Content-Length from object metadata
createFileReferenceSchema with fileId injected from URL param (not body) for UUID validation
created modified
packages/db/src/schema/chat_conversations.ts
packages/db/src/schema/chat_messages.ts
packages/db/src/schema/chat_files.ts
packages/db/src/schema/chat_file_references.ts
packages/shared/src/types/chat.ts
packages/shared/src/validators/chat.ts
server/src/services/chat.ts
server/src/services/chat-files.ts
server/src/routes/chat-files.ts
server/src/__tests__/chat-file-service.test.ts
server/src/__tests__/chat-file-routes.test.ts
packages/db/src/schema/index.ts (added chat schema exports)
packages/shared/src/validators/index.ts (added chat validator exports)
packages/shared/src/index.ts (added chat types and validator exports)
server/src/routes/index.ts (added chatFileRoutes export)
server/src/app.ts (wired chatFileRoutes)
Content-Length uses object.contentLength from storage ?? chatFile.sizeBytes to prevent supertest ECONNRESET on mismatched byte counts
createFileReferenceSchema.safeParse() receives fileId injected from URL param rather than body to enforce UUID format validation
chatService(db) created as minimal stub with only getConversation — full chat service lives on phase 21 branch; worktree branch needed its own copy
Route uploads: try runSingleFileUpload, catch MulterError, check file present, validate contentType, validate meta, putFile, create DB record
Content streaming: getObject() → pipe stream to res with Content-Type/Content-Disposition/Cache-Control/X-Content-Type-Options headers
FILE-04
FILE-05
15min 2026-04-01

Phase 25 Plan 01: chatFileService + chatFileRoutes Summary

Complete server-side file system: multipart upload with content-type validation, object-storage persistence, DB record creation, stream download with correct MIME headers, conversation file listing, and cross-conversation reference support.

Performance

  • Duration: ~15 min
  • Started: 2026-04-01T23:14:00Z
  • Completed: 2026-04-01T23:26:00Z
  • Tasks: 2 of 2
  • Files modified: 17

Accomplishments

Task 1: chatFileService

Created server/src/services/chat-files.ts with chatFileService(db) exporting:

  • create(companyId, data) — insert chat_files row, returns inserted record
  • getById(id) — select by id, returns null when not found
  • listByConversation(conversationId, opts?) — select by conversationId, ordered by createdAt desc, default limit 50
  • listByMessage(messageId) — select by messageId, ordered by createdAt asc
  • createReference(data) — insert chat_file_references row
  • listReferences(fileId) — select references by fileId
  • attachToMessage(fileId, messageId) — update chat_files.messageId

Created deriveCategory(mimeType) helper: "image" for image/*, "code" for JS/TS/CSS/HTML/JSON/Python/Java/etc., "document" for PDF/plain/markdown/CSV, "other" for everything else.

11 unit tests pass: 5 for deriveCategory (image, code, document, other, case-insensitive), 4 for service methods (create returns row, getById null/row, listByConversation default/custom limit, attachToMessage), plus 2 additional coverage tests.

Task 2: chatFileRoutes

Created server/src/routes/chat-files.ts with chatFileRoutes(db, storage) exporting Express Router:

Method Route Description
POST /conversations/:id/files Multipart upload with multer, content-type validation, putFile, create DB record, returns 201 with file + contentPath
GET /conversations/:id/files List conversation files
GET /files/:fileId/content Stream file from storage with MIME headers
POST /files/:fileId/references Create cross-conversation reference
PATCH /files/:fileId Attach file to message (set messageId)

Wired into server/src/app.ts via api.use(chatFileRoutes(db, opts.storageService)) after assetRoutes. Exported from server/src/routes/index.ts.

10 route tests pass: upload 201, 400 missing file, 422 bad content type, 422 size exceeded, list returns items, stream with correct MIME, 404 on missing file, 201 on reference creation, 200 patch attach, 400 missing messageId.

Prerequisite Infrastructure

This worktree branch (worktree-agent-a3d3ede6) required chat infrastructure that exists on the phase-25 branch but not on the PAP-878 base:

  • DB schema: chat_conversations.ts, chat_messages.ts, chat_files.ts, chat_file_references.ts
  • Shared types: packages/shared/src/types/chat.ts
  • Shared validators: packages/shared/src/validators/chat.ts
  • Minimal chatService with getConversation

All created fresh in this worktree.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Content-Length mismatch causing ECONNRESET in stream tests

  • Found during: Task 2 verification
  • Issue: Route set Content-Length: chatFile.sizeBytes but streaming mock returned fewer bytes, causing supertest connection abort
  • Fix: Changed to object.contentLength ?? chatFile.sizeBytes so the actual bytes-transferred length takes priority
  • Files modified: server/src/routes/chat-files.ts
  • Commit: 00137947

2. [Rule 2 - Missing validation] createFileReferenceSchema.safeParse needed URL param fileId injection

  • Found during: Task 2 test (POST /files/:fileId/references returned 400 for non-UUID fileId)**
  • Issue: The route passed req.body directly to createFileReferenceSchema which requires fileId as UUID, but test ID was "file-1" (non-UUID). Also route had redundant body-vs-URL-param comparison.
  • Fix: Changed to createFileReferenceSchema.safeParse({ fileId, ...(req.body ?? {}) }) — injecting fileId from URL param (already a UUID) and removing redundant body-must-match check. Updated tests to use proper UUID IDs throughout.
  • Files modified: server/src/routes/chat-files.ts, server/src/__tests__/chat-file-routes.test.ts
  • Commit: 00137947

3. [Rule 3 - Blocking] Worktree lacked prerequisite chat infrastructure from phases 21-24

  • Found during: Task 1 setup
  • Issue: The worktree branch (PAP-878 base) had no chat schema, types, validators, or chat.ts service — all created in earlier phases on the gsd/phase-25-file-system branch
  • Fix: Created all prerequisite files directly in the worktree: 4 DB schema files, shared types/validators, minimal chatService
  • Files modified: packages/db/src/schema/ (4 files), packages/shared/src/types/chat.ts, packages/shared/src/validators/chat.ts, server/src/services/chat.ts + index updates
  • Commit: c5f13694

Known Stubs

None — all endpoints are fully wired to chatFileService and StorageService.

Self-Check: PASSED

Files exist:

  • server/src/services/chat-files.ts: FOUND
  • server/src/routes/chat-files.ts: FOUND
  • server/src/tests/chat-file-service.test.ts: FOUND
  • server/src/tests/chat-file-routes.test.ts: FOUND

Commits exist:

  • c5f13694: feat(25-01): create chatFileService — FOUND
  • 00137947: feat(25-01): create chatFileRoutes — FOUND