Skip to content

refactor(api): V2 response envelope, snake_case schema, named views#2729

Open
Innei wants to merge 31 commits into
masterfrom
refactor/v2-api-response
Open

refactor(api): V2 response envelope, snake_case schema, named views#2729
Innei wants to merge 31 commits into
masterfrom
refactor/v2-api-response

Conversation

@Innei
Copy link
Copy Markdown
Member

@Innei Innei commented May 15, 2026

Summary

Breaking 4-phase refactor of the mx-core HTTP response layer per docs/superpowers/specs/2026-05-15-v2-api-response-design.md.

Every successful JSON response is now { data, meta? }; every error is { error: { code, message, details? } }.

Phases

  • Phase 0 — envelope infrastructure. src/common/response/* (envelope/meta/error types, MetaObjectBuilder, ResponseInterceptorV2, AppExceptionFilter, @RawResponse), src/common/views/view.types.ts (parseView), createPagerSchema factory.
  • Phase 1 — snake_case at the schema layer. Drizzle column TS prop names renamed to snake_case across packages/db-schema and rippled through ~32 repositories. The 6 Better Auth tables (readers, accounts, sessions, apiKeys, passkeys, verifications) keep camelCase props for drizzleAdapter compatibility.
  • Phase 2 — per-module migration. All ~45 modules migrated to the { data, meta? } envelope, named *.views.ts Zod views, MetaObjectBuilder, and AppException subclasses.
  • Phase 3 — cleanup. Deleted JSONTransformInterceptor, legacy ResponseInterceptor, translation-entry.interceptor, @TranslateFields; removed the Bypass alias; generic exceptions migrated to AppException; ResponseInterceptorV2 + AppExceptionFilter wired as global APP_INTERCEPTOR / APP_FILTER.

Notable deviations

  • Better Auth tables excluded from the snake_case rename (renaming their Drizzle props breaks drizzleAdapter) — a design-spec gap resolved here.
  • src/utils/case.util.ts retains an explicit snakeCaseKeys helper, called directly in ~13 controllers for responses whose source shape is still camelCase (notably enrichment screenshot/quota fields). The spec's hard requirements hold (uniform envelope, no global case-conversion interceptor); a follow-up could push those shapes into the views/repositories.

Verification

208 files changed, +6125 / -3381. Typecheck clean, lint clean, full Vitest suite 1088 passed / 3 skipped / 0 failed.

Innei added 16 commits May 15, 2026 23:12
Add src/common/response (envelope/meta/error types, MetaObjectBuilder,
ResponseInterceptorV2, AppExceptionFilter, @RawResponse) and
src/common/views (parseView). Add createPagerSchema factory to
pager.dto.ts. Additive only; not yet wired into the global pipeline.
41 unit tests; typecheck and lint clean.
Rename Drizzle column TS property names and ripple through apps/core
repositories. Better Auth tables (readers, accounts, sessions, apiKeys,
passkeys, verifications) keep camelCase props for drizzle-adapter
compatibility. SQL column names unchanged; no migration needed.
@ResponseV2() now composes SetMetadata (legacy interceptors skip),
UseInterceptors(ResponseInterceptorV2), and UseFilters(AppExceptionFilter).
Migrated controllers opt in with this one decorator; un-migrated
controllers keep the legacy pipeline untouched.
Apply @ResponseV2() to all controllers in ai (and all sub-controllers:
ai-agent, ai-insights, ai-summary, ai-task, ai-translation including
translation-entry, ai-writer), enrichment, analyze, activity, and link
modules.

- Add ai.exceptions.ts, enrichment.exceptions.ts, link.exceptions.ts
  with AppException subclasses replacing BizException/HttpException usage
- Add @RawResponse on SSE/streaming endpoints in ai-agent, ai-insights,
  ai-summary, and ai-translation controllers
- Wrap all controller return values in { data } / { data, meta } envelopes
- Add RESPONSE_V2_METADATA constant to system.constant.ts (required by
  v2-controller.decorator.ts)
- Update contract tests and e2e specs to assert { data } envelope shape
  and AppException error envelope { error: { code, message } }
Migrate feed, pageproxy, render, sitemap, snippet-route, snippet,
serverless, health, server-time, update, debug, dependency, cron-task,
backup, poll, markdown, option (base + email), and app.controller.ts
to the V2 response layer.

- Add @ResponseV2() to each controller class
- Replace @HTTPDecorators.Bypass with @RawResponse on genuinely
  non-JSON methods (RSS/XML, HTML render, binary streams, SSE,
  user-defined function outputs)
- Remove Bypass from JSON-returning endpoints (health /ping,
  dependency /graph, cron-task list, backup list, poll state,
  option /form-schema, app /uptime)
- Migrate poll controller from class-level Bypass to @ResponseV2()
  with all endpoints returning plain JSON envelopes
- Fix snippet.controller list methods to return {data, meta:{pagination}}
  instead of legacy spread shape; update two tests accordingly
- Fix missing RESPONSE_V2_METADATA export in system.constant.ts
- Add missing isDev import in app.controller.ts
Update 7 test files to match the V2 response envelope ({ data, meta? })
and new AppException subclasses replacing BizException/CannotFindException.
Fix snake_case DB column references in all batch-3 repository files (auth,
file-reference, meta-preset, owner, project, say, subscribe, webhook) that
were authored against a pre-rename base. Typecheck 0 errors, lint clean,
tests 0 failed.
Delete json-transform.interceptor, legacy response.interceptor,
translation-entry.interceptor, translate-fields.decorator, and
case.util. Promote ResponseInterceptorV2 as the single global
APP_INTERCEPTOR. Remove UseInterceptors from @ResponseV2() since
the interceptor now runs globally. Fix activity.controller.ts to
build presence response with plain objects instead of snakecaseKeys.
Rewrite CannotFindException (NOT_FOUND/404), BanInDemoExcpetion
(DEMO_FORBIDDEN/403), NoContentCanBeModifiedException
(NO_CONTENT_MODIFIABLE/400), and BusinessException (enum key code)
to extend AppException with stable string codes.

Promote AppExceptionFilter as the single global APP_FILTER, replacing
AllExceptionsFilter. Fold in AllExceptionsFilter's side effects: Bark
push on 429, EventBusEvents.SystemException broadcast on 5xx,
uncaughtException/unhandledRejection process hooks. Services are
@optional so the filter still instantiates without DI in unit tests.
Introduce snakeCaseKeys utility and apply it explicitly at every controller
boundary so HTTP responses emit snake_case keys without relying on a global
interceptor. Covers aggregate, category, comment, draft, enrichment, link,
note, page, post, recently, snippet, subscribe, and topic controllers.
@safedep
Copy link
Copy Markdown

safedep Bot commented May 15, 2026

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

No dependency changes detected. Nothing to scan.

View complete scan results →

This report is generated by SafeDep Github App

Innei added 13 commits May 16, 2026 03:11
- getDataFromResponse unwraps the { data, meta? } envelope; folds
  meta.pagination into PaginateResult
- expose raw response meta via a non-enumerable $meta accessor
- parse the { error: { code, message, details? } } error body;
  RequestError gains string `code` and `details`
- Pager becomes { page, size, total, totalPages }; add ResponseMeta,
  InteractionMeta, ArticleTranslation, EntryTranslation, RelatedRef
- drop the removed ?select= param from post/note/page; retire SelectFields
- unwrap { data: X } list/random returns to bare X
- update test mocks/assertions to the V2 envelope
…onse layer

The Drizzle schema column TS property names are renamed from snake_case
back to camelCase (DB column name strings unchanged). Business code —
repositories, services, domain types — is camelCase throughout; the
ResponseInterceptorV2 converts the response to snake_case at the wire
boundary via transformResponseCase, so the external contract is
unchanged.

- db-schema package: all 7 schema files use camelCase column properties
- repositories/services: snake_case column access renamed to camelCase
- SayRow domain type aligned to camelCase
- ResponseInterceptorV2: guard bypass-path metadata to an array
- test fixtures: Drizzle insert/select keys renamed to camelCase
…terceptor

ResponseInterceptorV2 now converts every response to snake_case at the
wire boundary, so the ad-hoc snakeCaseKeys() calls in 13 controllers are
redundant. Remove them and delete the now-unused case.util helper.
The schema-layer snake_case decision (design spec §2) was reversed: code
is camelCase end to end and ResponseInterceptorV2 converts to snake_case
at the wire boundary. Update §2, the goals list, the §7 RawResponse
note, the migration reference, and the project CLAUDE.md API rules;
document @BypassCaseTransform.
V2 note-detail responses return `data` as a flat NoteModel with `next`/
`prev` siblings, matching post detail — not the V1 `{ data, next, prev }`
wrapper. Retype NoteWrappedPayload accordingly; bump to 5.0.1.
Innei added 2 commits May 18, 2026 00:33
Unify aggregate/category/topic/note controllers on the V2 meta.translation
contract: emit id-keyed translation maps instead of mutating translated
titles into data. Taxonomy names (category/topic) resolve via the
entry-translation subsystem rather than the article pipeline.

- aggregate: buildTitleTranslationMap replaces inline title mutation
- category: category.name via getEntityTranslations
- topic: /topics/all gains @lang(), topic fields via translation entries
- note: note-detail meta carries the embedded topic translation
- add TranslationService.getTopicTranslationFields shared helper
- type translation maps as Map<string, EntryTranslation>
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