Skip to content

refactor: service layers for documents, projects, tabular, chat, user and workflows (ADR) - #42

Open
amal66 wants to merge 1 commit into
upstream-mainfrom
upstream-pr/service-layers
Open

refactor: service layers for documents, projects, tabular, chat, user and workflows (ADR)#42
amal66 wants to merge 1 commit into
upstream-mainfrom
upstream-pr/service-layers

Conversation

@amal66

@amal66 amal66 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Design (ADR)

Context. The backend's route files are monoliths: routes/tabular.ts (1,837 lines), routes/documents.ts (1,464), routes/user.ts (1,132), routes/projects.ts (1,027) each interleave HTTP parsing, auth/access checks, storage IO, and business logic inline in the handlers (the fork's equivalent of the 3,900-line chatTools file was already split into lib/chat/ upstream; the route layer is the remaining monolith). Handlers can't be unit-tested without a live HTTP stack, and every change touches a file with a dozen unrelated endpoints.

Decision. Decompose each route file into a module under src/modules/<domain>/: thin *.routes.ts (HTTP concerns: parse, validate, map typed results onto status codes) delegating to *.service.ts functions (business logic + data access, explicit db parameter, typed results, never touch req/res). Larger domains split the service behind a facade (documents.service.ts re-exports access/upload/versions/download/edits; tabular splits reviews/extract/generate/chats; user splits profile/mfa/apiKeys/mcp/account/export). index.ts is split into app.ts (exported express app) + index.ts (listen/signals), which is what makes the ported supertest integration suites possible — tests-first was the fork's discipline for this refactor and the tests ride along.

Consequences. Reviewability (each concern is a few-hundred-line file), testability (93 tests in this PR exercise routes with all IO mocked), a stable seam for future work. Cost: many more files, and a big one-time diff.

Alternatives considered. Status quo (rejected: the monoliths are where changes go to be risky); partial split of only the worst files (rejected: two conventions coexisting indefinitely).

Summary

Ports the amal66/mike fork's service-layer architecture refactor onto upstream's backend/ layout: routes/*.ts monoliths are replaced by src/modules/{chat,project-chat,projects,documents,tabular,user,workflows,library,downloads,case-law}/, plus the supporting libs the modules are written against and the module test suites.

Changes

  • src/modules/ — 10 domain modules (routes + services), replacing 10 deleted src/routes/ files. Endpoint surface is unchanged (verified by extracting method+path pairs from both trees; only deliberate omissions differ, see Provenance).
  • src/app.ts + slim src/index.ts — app/process split; central error handler; lib/asyncErrors patches express so thrown async errors reach it.
  • Supporting libs: lib/http (sendError/parseBody), lib/logger (pino with request-id/trace mixin) + lib/observability/requestContext, middleware/httpLogger + middleware/requestContext, lib/sseHeartbeat, lib/pdfjs; lib/upload gains magic-byte validation (hasMagicBytes) called by the upload routes.
  • Tests: 7 supertest integration suites (src/__tests__/integration/) + tabular.extractDoc unit test — 93 tests, all IO mocked. tsconfig.json excludes test files so npm run build output is unchanged (same exclude the test-harness PR adds; duplicated so this branch builds green standalone).
  • Deps: pino, pino-http, @opentelemetry/api (logger; no OTel SDK — the mixin no-ops without one), zod 3→4 (module validation schemas use v4 APIs; upstream src/ has no zod call sites), dev: pino-pretty, supertest, @types/supertest.

Why

This is the fork's architecture refactor offered upstream as design-review material: it makes the backend testable at the route level and turns the monoliths into per-domain modules with a uniform routes/service contract.

Testing

  • npm install + npm run build (tsc) green from a clean checkout of this branch (node_modules removed first).
  • With the vitest test-harness branch (upstream-pr/test-harness) merged locally (not part of this PR): npm test9 files, 105 tests passed (93 from this branch: health, chat, project-chat, projects, documents-upload, tabular, user integration suites + tabular.extractDoc unit; 12 from the harness's own tests).
  • NOT run: a live server against a real Supabase/storage stack; no e2e. The fork's supabase-backed integration tests were not ported (they need live credentials).

Provenance

All added lines are mechanical ports of amal66/mike@origin/main (b3166dd): path moves (apps/api/srcbackend/src), import rewrites, and omission of fork-only feature hunks. Exceptions, each deliberate:

  • Omitted fork-only features (hunks removed; the flag-gated inline fallbacks the fork kept are what remains): BullMQ job queues (conversion/embedding enqueues in documents, the async tabular extraction path + reconnectable GET /:reviewId/generate/stream view; tabular.generateStream.ts not ported), orgs/multi-tenant RBAC (org_id columns and getOrgRole/getPersonalOrgId/resolveContentOrgId/listUserOrgIds call sites), message credits (chat + project-chat reserve/refund), DMS connectors (user.dms.ts + its routes), Word add-in documentContext + spotlight nonce-fencing, demo-model fallback and air-gap assertModelAvailable gating, docx redline summaries in tabular extraction, workflow pack import/export (workflowFormat.ts + 2 endpoints), /ready probe, Sentry/metrics/OTel init, secretGuard, and the orgs + guest-auth modules.
  • Schema alignment: upstream dropped documents.filename; inserts seed of filename/org_id removed and two dead select("filename") calls deleted. Project PATCH management check maps the fork's org-aware access.canManage to upstream's owner-only access.isOwner.
  • Type-level adaptations (upstream's typed supabase client vs the fork's any-typed one): ApiKeyProvider param type in user.apiKeys.ts, one as unknown as DocRow cast for a dynamic select, applyKindFilter generic loosened in library.service.ts (TS2589 otherwise).
  • app.ts keeps upstream's own limiter/CORS/JSON-limit lines rather than the fork's lib/env-based versions (fork's typed-env module not ported).
  • Tests adapted: lib/env / lib/credits mocks removed; credit-limit, org-access, /ready, and invalid-bearer-token cases dropped with the features they exercised.
  • Deps the fork ships: pino/pino-http/pino-pretty/@opentelemetry/api/supertest/zod@4 as listed above.

Credits & prior art

🤖 Generated with Claude Code

https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC

Port of the amal66/mike fork's backend architecture refactor onto the
upstream layout: the monolithic backend/src/routes/*.ts files are replaced
by per-domain modules under backend/src/modules/, each split into thin
route handlers (HTTP concerns only) and service functions (business logic
+ data access, taking an explicit db client and returning typed results).

- modules: chat, project-chat, projects, documents (access/upload/
  versions/download/edits facade), tabular (reviews/extract/generate/
  chats), user (profile/mfa/apiKeys/mcp/account/export), workflows,
  library, downloads, case-law
- src/index.ts split into app.ts (exported express app, testable with
  supertest) + index.ts (process concerns: listen, signals, crash exits)
- supporting libs the modules are written against: lib/http (sendError/
  parseBody), lib/logger (pino + request-id correlation), lib/asyncErrors,
  lib/sseHeartbeat, lib/pdfjs, middleware/httpLogger + requestContext;
  lib/upload gains magic-byte validation used by the upload routes
- integration tests (supertest against the exported app, all IO mocked)
  and unit tests ported alongside the modules; tsconfig excludes test
  files from the build so tsc output is unchanged
- deps: pino/pino-http/@opentelemetry/api (logger), zod 3 -> 4 (module
  validation schemas), supertest (dev)

Fork-only features NOT ported (hunks omitted): BullMQ job queues, orgs/
multi-tenant RBAC, message credits, RAG/embeddings, DMS connectors,
Word add-in document context + prompt spotlighting, demo-model fallback,
air-gap model gating, docx redline extraction, workflow pack import/
export, /ready probe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
amal66 added a commit that referenced this pull request Aug 6, 2026
…ice layers

WHY THIS MATTERS
The backend's route files were monoliths — routes/tabular.ts (1,649
lines), routes/documents.ts (1,504), routes/projects.ts (1,139),
routes/user.ts (1,132) — each interleaving HTTP parsing, auth checks,
storage IO, and business logic inline in a dozen unrelated handlers.
In a monolith, every change lands in a giant file where the blast
radius is unclear, business logic can only be exercised through a live
HTTP stack, and a new contributor cannot tell which lines are "the
endpoint" and which are "the feature". For an open-source repo this is
the difference between a drive-by contributor shipping a fix and giving
up: small files with one concern each are reviewable; 1,600-line route
files are not.

WHAT IS A SERVICE LAYER
A service layer separates WHAT the application does from HOW it is
reached. The route (HTTP layer) owns request parsing, validation, and
mapping results onto status codes; the service owns business logic and
data access, takes its database handle as an explicit parameter, returns
typed results (discriminated unions like { ok: false, kind: "not_found" }
instead of writing to `res`), and never touches the HTTP request or
response. That inversion is what makes logic unit-testable (call the
function with a fake db — no server needed), reusable (the async
extraction worker calls the exact same functions the SSE route calls),
and safe to change (the compiler knows every result shape a route must
handle).

HOW IT WORKS
- src/routes/*.ts (11 files, 7,853 lines) is replaced by
  src/modules/<domain>/ — chat, project-chat, projects, documents,
  tabular, user, workflows, library, downloads, case-law, models —
  each a thin <name>.routes.ts plus <name>.service.ts. Large domains
  split the service into topic files behind a named-re-export facade
  (documents: access/upload/versions/download/edits; projects:
  crud/folders/documents/chats; user: profile/mfa/apiKeys/mcp/account/
  export; tabular: reviews/rows/extract/extractRow/generate/
  generateStream/chats) so intra-module helpers cannot leak.
- lib/tabular/* (from the durable-queues change this builds on) moves
  into modules/tabular/ — the domain's extraction core, row loaders and
  route layer now live together; src/lib/ keeps only cross-domain
  infrastructure (storage, llm, chat, queue, access...).
- Streaming endpoints keep their SSE loops in the routes file; only
  their non-streaming prepare/persist logic moved into services —
  streaming lifetime and client-abort handling are HTTP concerns.
- Pure motion, verified three ways: the endpoint inventory
  (method+path multiset, 67 endpoints) is byte-identical before and
  after; tsc is clean; the full suite — 510 tests, including the 11
  route-level integration suites that exercise the real express app —
  passes unchanged. Handler bodies moved verbatim; the only rewrites
  are the mechanical seam (res.status(...) inside moved code became
  typed returns mapped back to the identical status/JSON in the route).
- DRY within domains only: helpers duplicated across handlers in the
  same domain (shared_with normalization in projects, the doc-access
  guard sequence in documents, findSystemWorkflow) now have one copy in
  their service; similar-but-not-identical code was left alone rather
  than force-merged.
- Zero new dependencies. No logging framework, no validation framework,
  no observability hooks — organization only, so the diff is reviewable
  as motion and each future concern can be its own decision.

Re-derived against this branch's code from the fork's service-layer
refactor (#42, running in the amal66 fork), whose module
boundaries and routes/service contract this follows; the fork's
pino/OTel/zod adoption was deliberately NOT ported to keep this
dependency-free pure motion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant