refactor(api): V2 response envelope, snake_case schema, named views#2729
Merged
Conversation
|
This pull request exceeds GitHub's diff limits and cannot be scanned. GitHub Limits:
Recommendations:
|
Innei
added a commit
that referenced
this pull request
May 20, 2026
Squash PR #2729 into one commit before rebasing onto latest master.
da6a054 to
87e6381
Compare
Innei
added a commit
that referenced
this pull request
May 20, 2026
Squash PR #2729 into one commit before rebasing onto latest master.
87e6381 to
fa634ec
Compare
Innei
added a commit
that referenced
this pull request
May 20, 2026
Squash PR #2729 into one commit before rebasing onto latest master.
fa634ec to
aefbbf1
Compare
Innei
added a commit
that referenced
this pull request
May 20, 2026
Squash PR #2729 into one commit before rebasing onto latest master.
aefbbf1 to
9d42b19
Compare
Squash PR #2729 into one commit before rebasing onto latest master.
9d42b19 to
5543aa4
Compare
- target augment module by package name so type augmentations survive rolldown chunking - legacy entry imports HTTPClient via root, ensuring controllers attach to returned client - release: bump @mx-space/api-client to v5.0.2-next.1
…ot entry bump to v5.0.2-next.2
| // runs camelcaseKeys on the raw snake_case wire payload before it reaches this | ||
| // adapter. So `is_liked` arrives as `isLiked`, `source_lang` as `sourceLang`, etc. | ||
|
|
||
| const stripPath = (p: string) => p.split('?')[0].replace(/\/+$/, '') || '/' |
|
|
||
| const NEW_PORT = process.env.NEW_PORT ?? '2333' | ||
| const OLD_PORT = process.env.OLD_PORT ?? '3333' | ||
| const filter = process.argv[2] ? new RegExp(process.argv[2], 'i') : null |
Previously post/page/note detail handlers only attached translation meta when the article had actually been translated. V1 consumers expect the default fields (is_translated, source_lang, available_translations) to always be present so they can render the language picker / source lang badge without conditional checks. Also widen ArticleTranslationSchema.source_lang and target_lang to allow null so PostgreSQL rows that lack a language column still validate.
The legacy adapter now reconstructs the V1 response shape from V2 raw
payloads on a per-endpoint basis:
- Flattens meta.{translation,interaction,insights,enrichments,related}
back into items (camelCase, matching default transformResponse output)
- Renames pagination fields (page/totalPages → currentPage/totalPage)
and synthesizes hasNextPage/hasPrevPage
- Wraps NoteModel detail responses as { data, next, prev } again
- Hoists comment.getByRefId readers/pagination back to the top level
- Strips text/content from aggregate.getTop list items (V2 server omits
bodies for SSR payload size; V1 didn't, this aligns at the adapter)
- Restores singleFileSizeMb (V2 snake-case emits single_file_size_m_b
which camelcase upper-cases to singleFileSizeMB)
- Other endpoint-specific tweaks: comment threads, note middle list,
note topic list, activity presence
Also fix the fetch adaptor losing the Response status getter when
shallow-copying via Object.assign, which caused error handlers to fall
back to status 500 instead of the real HTTP code.
Adds packages/api-client/scripts/smoke-diff.mjs — a parity harness that
hits both ends of a V2/V1 setup with the legacy adapter applied and
deep-diffs the normalized payloads. Used to drive this fix to 0 diffs
across Yohaku's 52-endpoint call surface.
bump @mx-space/api-client to v5.0.2-next.3
f7944bc to
9b2363f
Compare
…ith BasicPagerDto/createPagerSchema Removes the legacy BizException / BusinessException / ErrorCodeEnum + CannotFindException / BanInDemoExcpetion / NoContentCanBeModifiedException dual-track in favor of a single AppErrorCode enum, AppErrorPayloadMap, and createAppException factory. All 50+ throw sites and 13 test files migrated. ~/common/exceptions/* and ~/constants/error-code.constant.ts are deleted. Removes the legacy PagerSchema / PagerDto / PagerInput (with V1 select / state / db_query / camelCase sort fields). The new pager API exposes BasicPagerSchema/BasicPagerDto/BasicPagerInput for sort-less endpoints and createPagerSchema(sortKeys) for sortable endpoints; 27 controllers and schemas migrated. sort_by/sort_order are snake_case on the wire and typed via z.enum(sortKeys). Also folds in the earlier ultrareview fixes: - note.controller.ts /notes/list/:id empty-fallback double-wrap fix - check-controller-response-envelope.ts now flags any literal whose top keys include `data` (catches the bug above) - CLAUDE.md envelope description corrected (Symbol-based detection) - snakeKey boundary-aware (articleURL -> article_url, HTMLContent -> html_content) - AppExceptionFilter deduped (logServerError, handleThrottle helpers); log/Bark/throttle copy in English - BypassCaseTransform JSDoc spells out subtree verbatim semantics All AppErrorCode messages are in English. ENRICHMENT_BROWSER_MODE_REQUIRED and ENRICHMENT_SCREENSHOT_DISABLED return 409 to match contract tests. Verification: tsc --noEmit clean, vitest 1163 passed / 0 failed / 3 pending.
| @Get('/:type') | ||
| @Auth() | ||
| async getTypes(@Query() query: PagerDto, @Param() params: FileUploadDto) { | ||
| async getTypes( |
- file: add CommentUploadsListQueryDto so /comment-uploads/list coerces flat ?page=&size= query into numbers; raw @query() previously echoed strings into withMeta(...).pagination(...) and tripped ResponseMetaSchema - markdown: cast meta.slug to string in generateArchive — notes set meta.slug to nid (number) and .concat threw a TypeError on export - say: expose missing GET/:id, POST, PUT/:id, DELETE/:id routes; repository already had the methods, controller now wires them so admin's manage-says CRUD works - errors: NOT_FOUND now carries an optional `id` detail to match the per-resource error payloads (used by the new say routes) - api-client: probe both axios-style and ofetch-style envelopes when extracting body + $meta; covered by new test
- AggregateController.getSiteMetadata / getAggregateData / getTop now
accept a CacheableOptions arg ({ next, cache }) so server callers can
pass Next.js cache hints without dropping to .proxy.* raw path access.
- getAggregateData also accepts `lang` to push the locale into the
query string (previously consumers had to plumb it through
ofetch-level langStorage or .proxy.get({ params: { lang } })).
- RequestOptions exposes typed `next?: NextFetchRequestConfig` and
`cache?: RequestCache` fields, declared structurally so the SDK does
not pull `next` as a dependency. Adapters that don't recognise them
(axios, umi) silently drop the keys.
Single-file shim (`src/domain/envelope-compat.ts`) that normalizes wire bodies
to the V3 shape before reaching schema decoding, error mapping, and renderer
views. Lets the CLI work against either a V2 or V3 mx-core deployment.
- `normalizeSuccessBody`: lifts V2 root `pagination` under `meta.pagination`.
- `normalizeErrorBody`: unwraps V3 `{ error: { code, message } }` and flattens
V2 `{ message: string | string[], code? }` to one tuple.
- `mapHttpStatusToError`: prefers V3 SCREAMING_SNAKE codes
(`<RESOURCE>_NOT_FOUND`, `VALIDATION_FAILED`) over status-derived tags when
present, so `POST_NOT_FOUND` correctly maps to `ResourceNotFound`.
- `--verbose` prints the detected wire version once per `ApiService`.
View cleanup: `post`/`comment` list renderers now read `meta.pagination` only;
the V2 root-`pagination` read path is dead post-adapter.
Removal is grep-driven via `// COMPAT:envelope` markers and a deletion
checklist in `envelope-compat.ts`'s JSDoc. Design doc:
`packages/cli/docs/specs/2026-05-21-v2-v3-envelope-compat-layer.md`.
Tests: 22 new unit tests for the adapter, V3-fixture branches added to
`cli-post-list` and `cli-error-envelope` integration suites. Full suite:
637/637 passing.
Align response meta with the camelCase-internal / snake_case-wire convention enforced by `ResponseInterceptorV2`. Snake_case keys (`total_pages`, `is_translated`, `source_lang`, `target_lang`, `content_format`, `available_translations`, `is_liked`, `like_count`, `read_count`, `has_in_locale`) leaked into Zod schemas, the meta builder, the translation helper, and every controller call site — forcing manual snake_case in business code and double-converting through the interceptor. - meta.types: schemas now declare camelCase keys - meta-builder: drop `total_pages` alias on LegacyPaginationLike - helper.translation: buildArticleTranslationMeta emits camelCase - 12 controllers (note, post, page, draft, reader, webhook, say, file, snippet, topic, project, category, aggregate): builder call sites switched to camelCase - unit specs updated; wire shape unchanged (contract specs pass) - drop dead dep snakecase-keys
Translate all Chinese in comments, logger calls, error messages, thrown exceptions, Zod descriptions, email subjects/bodies, and other user-facing strings across apps/core/src. Preserved (intentional): - AI prompt example fixtures in modules/ai/ai.prompts.ts - i18n locale data (LABELS.zh/ja) in mx-space enrichment provider - Spam-detection dictionaries (block-keywords.json, meaningless-words.json) - Test fixtures
ResponseInterceptorV2 handles camelCase→snake_case conversion at the wire boundary. Controllers were duplicating that work with hand-rolled key renames and migration-era `?? c.snake_case` fallbacks. Removed across aggregate/comment/enrichment/note/subscribe controllers; wire shape is unchanged because the interceptor still emits snake_case.
Signed-off-by: Innei <tukon479@gmail.com>
…anslation meta Collapse repeated translateArticleList + isTranslated filter + EntryTranslation map construction boilerplate from five controllers (post, page, note, category, aggregate) into a single helper on TranslationService. Drops three `as any` casts in the meta path; pulls sourceLang from translationMeta so it is actually populated.
Flatten common/response/ to wire-format core only (envelope.types, meta.types, meta-builder, case-transform). Move out NestJS-typed files to their matching common/ subdirectory so the layout matches the rest of common/: - bypass-case-transform.decorator.ts → common/decorators/ - raw-response.decorator.ts → common/decorators/ - error.types.ts → common/errors/exception.types.ts - app-exception.filter.ts → common/filters/ - response.interceptor.ts → common/interceptors/ Updates 54 importers across src and test, plus CLAUDE.md path references.
…asses it through
The legacy adapter's transformLegacyData assumed defaultGetDataFromResponse
had already peeled the V3 `{ data, meta }` envelope, so per-endpoint rules
read fields off the inner object. When a consumer ships an identity
getDataFromResponse (common with ofetch stacks that already deliver the
parsed body), the envelope survives and rules like aggregateTopRule look
for `notes`/`posts` on the wrapper, find nothing, and emit an output
shaped like the envelope instead of the inner data. Apps using getTop(5)
then crash with `notes is not iterable`.
destructureData only collapses single-key `{ data }` wrappers, so the
two-key `{ data, meta }` envelope slipped through whenever meta was
populated (e.g. `/aggregate/top?lang=en` returning a translation map).
Strip the V3 envelope explicitly at the top of transformLegacyData so
rules see the same inner shape regardless of how the consumer hands the
payload over. Adds a regression test that drives the adapter with an
identity getDataFromResponse to lock the behavior in.
Restores V1 behaviour where ?lang=xx produces a response with translated content directly in `data` (title/text/content/topic.name/mood/weather etc.), with `meta.translation.<id>.article` reduced to slim metadata (is_translated/source_lang/target_lang/translated_at/model/ available_translations) and no duplicated content fields.
Adopt all codex review findings: strict ArticleTranslationSchema, missing V1 endpoints (/notes/:id, /notes/list/:id, /posts/:id, /pages/:id, /search/:type, /activity/last-year/publication, /aggregate/stat/top-articles), /pages/slug/:slug rename, builder reads translatedAt/model from translationMeta, paired content/contentFormat overwrite, lookup-group wording, mandatory translation-before-enrichment pipeline order, translated-items-only meta policy on list endpoints, refId-keyed activity reading rule for legacy adapter, smoke-diff script declaration, existing adapter test updates, category /:query tag=true branch row. Keep /notes/topics/:id topic.* coverage as an intentional expansion beyond V1.
- List envelope example now omits an untranslated item, matching the "translated items only" policy. - Cached-title meta paragraph rewritten: sub-entity entries never emit meta; article-row title translations may emit slim metadata when available. - Pseudocode `if (translatedContent)` → `if (translatedContent != null)` so empty-string translated bodies are still considered overwrites. - Post detail table column renamed to "Overwritten in response"; moved related[].title into the meta.related lane explicitly. - "9 affected controllers" → "8 affected controllers" with names listed. - New "Nested-array adapter rules" section closes the V1-wire parity gap on aggregate / activity / category nested lists.
Drop translated-content fields (title/text/subtitle/summary/tags/content/ contentFormat) from meta.translation.<id>.article — those values now overwrite their counterparts in `data` in place per the in-place overwrite design. ArticleTranslationSchema becomes .strict() so any controller still emitting the removed fields fails fast at MetaObjectBuilder.build() rather than silently stripping the keys. Drop EntryTranslationSchema.fields — entry-driven translations also overwrite data in place. Tests assert that supplying any removed field causes ArticleTranslationSchema.safeParse() to return success: false (not a stripped result).
buildArticleTranslationMeta now returns only slim metadata
(isTranslated/sourceLang/targetLang/translatedAt/model/availableTranslations)
matching the slim ArticleTranslationSchema. translatedAt/model/targetLang
are read from result.translationMeta.* with targetLang falling back to
the lang argument.
Add applyArticleTranslationInPlace that overwrites article body fields
on the target object in place — uses nullish checks so empty-string
overwrites apply, and pairs content with contentFormat so the latter
never flips without the former.
Add applyTranslationEntriesInPlace for dotted-path overwrites driven by
entityMaps (lookup by an idField on the parent) or dictMaps (lookup by
the current source value). Powers note mood/weather, topic.*, and
category.name translations.
collectArticleTranslations now returns { results, meta }: results is the
per-id TranslationResult for downstream applyArticleTranslationInPlace,
meta is the slim metadata map suitable for MetaObjectBuilder.translation.
Remove getTopicTranslationFields — its meta.translation.<id>.fields
output no longer exists; callers will switch to
translationEntryService.getTranslationsBatch + applyTranslationEntriesInPlace.
Fix a latent sourceLang propagation bug in translateArticleList where
the value was nested in translationMeta but not surfaced at the result
top level.
…ules
Slim buildTranslationFlat: drop the article-or-translation fallback,
remove fields handling, and reduce translationMeta to sourceLang/
targetLang/translatedAt/model — translated display fields (title/text/
content/...) now arrive inline in data, so V1-shape consumers no longer
read them from translationMeta.
Add flattenNestedArrays helper and dedicated rules for endpoints that
return item arrays at non-data keys: aggregate /latest /timeline,
activity /rooms /recent /reading/{top,rank} /last-year/publication, and
aggregate /top (composed with the existing body-strip behaviour). Each
rule flattens meta.translation entries onto the items so V1 consumers
keep getting translationMeta per item.
readingRankRule synthesizes id from refId before flatten and strips it
after, so refId-keyed items match meta.translation entries.
Declare smoke:diff npm script (the script file already existed but had
no package entry). Bump version to 5.0.2-next.9.
Update existing adapter tests to assert the slim translationMeta and
that translated display values live in data; add new tests for
activity/rooms and activity/reading/top.
note.controller: every detail/list endpoint now overwrites article body
fields, mood/weather (dict), and topic.{name,introduce,description}
(entity) directly on data — including next/prev adjacents — and emits
only the slim translation metadata in meta.translation. One batched
TranslationEntryService.getTranslationsBatch call per request covers
all topic entity ids and mood/weather source values across current +
next + prev or all list items. Translation runs before
enrichmentService.attachEnrichments so link cards extract from the
translated body.
aggregate.controller: /top, /latest (combined + separate), /timeline,
/stat/top-articles now use collectArticleTranslations + in-place
applyArticleTranslationInPlace per item, plus a single
getTranslationsBatch for dict (note.mood/weather) and entity
(posts[].category.name on timeline). Translated-items-only meta.
Test infrastructure: translation.mock now returns {results, meta} from
collectArticleTranslations and exposes translationEntryProvider for
controllers that inject TranslationEntryService. Several contract spec
files register translationEntryProvider.
C1, C3, C4, C5, C6, C8 controllers (post/page/topic/category/search/
activity) follow in a subsequent commit; their work in the parallel
dispatch did not persist to the main worktree and is being redone.
Apply the in-place article + entry translation pattern to post, page,
topic, category, search, and activity. Each affected endpoint:
- Translates article body fields (title/text/content/contentFormat/
subtitle/summary/tags) in place on data, never duplicating them into
meta.
- Translates sub-entity fields (topic.{name,introduce,description},
category.name, note.mood/weather) in place via a single batched
TranslationEntryService.getTranslationsBatch call per request.
- Emits meta.translation.<id>.article in slim metadata form
(isTranslated/sourceLang/targetLang/translatedAt/model/
availableTranslations) for detail endpoints (always) or for
translated items only (lists/aggregate/activity).
- Runs all translation steps before enrichmentService.attachEnrichments
so URL extraction sees the translated body.
post: getById (auth detail) gains @lang() + the full detail pipeline;
getByCateAndSlug + getPaginate batch category.name entity lookups
alongside article translation.
page: getPageBySlug + getById + getPagesSummary translate
title/text/subtitle/content/contentFormat in place.
topic: /all + /:id + /slug/:slug overwrite name/introduce/description
via entity entries; no meta.translation emission. TopicModule gains
forwardRef(AiModule) for TranslationEntryService DI.
category: GET / (by-type + ids branch), GET /:query regular branch, and
the tag=true branch each handle category.name + child post titles.
CategoryModule gains forwardRef(AiModule).
search: GET / and GET /:type translate data[].category.name on post
items only; no meta.translation emission.
activity: /rooms flattens all object-type buckets into one translation
batch; /reading/{top,rank} use refId as the meta key (matching the new
legacy adapter readingRankRule); /recent and /last-year/publication
aggregate translation across like/comment/post/note buckets and emit
slim per-item meta.
This consolidates the controller portion of the V3 in-place translation
redesign (commits 553f91e, c6eb940, d17bde5, 73e7ae6 build the
foundation; this commit completes the rest).
AggregateController now injects TranslationEntryService; AggregateModule imports AiModule via forwardRef so DI resolves at boot. CategoryController's Promise.resolve fallback for getTranslationsBatch now types itself as EntryMaps so TranslationEntryKeyPath flows through correctly.
The note/post/page views' readable + llm modes had a source_lang and translated row that has been silently empty since the V3 API envelope moved translation metadata out of the document body and into meta.translation.<id>.article. Restore parity by introducing pickArticleTranslationMeta in Renderer/content.ts and threading the article meta into collectFields as a fallback source. Applies to: - mxs note view (detail readable + llm) - mxs post view (detail readable + llm) and post list readable - mxs page view (detail readable + llm) The XML envelope mode is unchanged — it never emitted these fields. Tests cover all four view paths plus the per-row list fallback.
Merge the standalone RawResponse decorator into the HTTPDecorators object alongside Idempotence/SkipLogging. Drops raw-response.decorator.ts and rewrites 23 controllers to use @HTTPDecorators.RawResponse.
Drops redundant `const X = await this.service.foo(); return X` blocks in favor of `return this.service.foo()` across 17 controllers, and strips async on the now-await-free one-liners.
Catches the cases missed by the first sweep where the awaited call spans multiple lines (analyze, draft, enrichment, ai-insights, ai-summary, translation-entry).
Adds a global RequestCaseNormalizationPipe that runs ahead of the zod
validation pipe and folds incoming query/param keys (deep) and body
keys (top-level only, to preserve freeform JSON like meta/socialIds/AI
messages) to camelCase. Both ?sort_by= and ?sortBy= now reach the
controller as sortBy, restoring camelCase end-to-end inside core
without losing snake_case wire ergonomics.
- New: src/common/request/{case-transform,case-normalization.pipe}.ts
- Reverts snake_case keys in pager/draft/file/activity/markdown schemas
- Updates post/note/file/activity/markdown controllers to camelCase
destructuring (drops the sort_by: sortBy aliasing)
- Wires the pipe into bootstrap and the e2e test harness
- Tests: unit (camelKey, pipe) + e2e (snake/camel query parity, body
freeform preservation)
Per project convention, organize ~/common by NestJS artifact type (decorators/filters/guards/interceptors/middlewares/pipes) rather than domain (request/response). Drops common/request/ in favor of the existing pipes layout.
sortOrder was a hard z.enum(['asc','desc']) after the V2 refactor, so existing clients still sending ?sortOrder=1 / -1 (the pre-V2 wire form) got a 422. The case-normalization pipe handles key casing, not value shapes — value-level compat needs its own preprocess. Reshape zSortOrder in ~/common/zod/primitives.ts to coerce the legacy numeric/string forms to 'asc' / 'desc', keep core typed as the enum, and route createPagerSchema + DraftPagerSchema through it. Adds parametrized tests covering all four wire shapes plus the rejection case.
…e into envelope
generic Array path no-pagination case used to return a bare array, dropping
the V3 `{data:[...]}` envelope. Callers throughout host apps destructure
`.data` off list responses (matching the controller method's typed return),
so the bare-array shape made `.data` undefined at runtime — crashed
useInfiniteQuery getNextPageParam on /thinking, broke /timeline data, etc.
Add an /aggregate/timeline rule that re-wraps `{posts,notes}` into envelope
to match the typed `getTimeline` return as well.
V3 backends only accept `createdAt`/`modifiedAt` as `sortBy` enum values; V1 callers still pass `created`/`modified`. The legacy client now intercepts request envelopes (covering GET via attach-request's $$get and POST/PUT/PATCH/DELETE via the same boundary) and rewrites these aliases before the host adapter sends them — keeping host code untouched during migration. Also bumps version to 5.0.2-next.10.
…ware When `x-skip-translation: 1` is set, restrict the lang-resolution chain to the explicit `x-lang` header only. Implicit sources (NEXT_LOCALE cookie, Accept-Language) are skipped so admin callers, which always send the flag, never receive auto-translated responses driven by the browser locale. Non-admin callers are unaffected.
Innei
added a commit
that referenced
this pull request
May 26, 2026
V2 server returns `{ presence, readers }` (renamed from V1 `{ data, readers }`
in #2729) but the client still read `payload.data`, producing an empty map.
Also drop the outer `...camelcaseKeys(spread)` which deep-recursed into the
presence subtree and silently mangled identity keys like `owner_xyz` → `ownerXyz`.
Tests updated to the new envelope shape.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
src/common/response/*(envelope/meta/error types,MetaObjectBuilder,ResponseInterceptorV2,AppExceptionFilter,@RawResponse),src/common/views/view.types.ts(parseView),createPagerSchemafactory.packages/db-schemaand rippled through ~32 repositories. The 6 Better Auth tables (readers,accounts,sessions,apiKeys,passkeys,verifications) keep camelCase props fordrizzleAdaptercompatibility.{ data, meta? }envelope, named*.views.tsZod views,MetaObjectBuilder, andAppExceptionsubclasses.JSONTransformInterceptor, legacyResponseInterceptor,translation-entry.interceptor,@TranslateFields, andsrc/utils/case.util.ts(with itssnakeCaseKeyshelper); removed theBypassalias; generic exceptions migrated toAppException;ResponseInterceptorV2+AppExceptionFilterwired as globalAPP_INTERCEPTOR/APP_FILTER.Breaking changes
{ data, meta? }on success and{ error: { code, message, details? } }on failure. Existing V1 consumers must either upgrade to V2-aware client code or pin@mx-space/api-clientto the version that ships the legacy response adapter (this PR includespackages/api-client/legacy/response-adapter.tscovering the in-tree consumers).created_at,is_published,category_id, etc. The Drizzle TS code remains camelCase;ResponseInterceptorV2converts at the wire boundary.createPagerSchemaand drop the following V1 query parameters with no direct replacement:select— use named*.views.tsZod views (e.g.?view=card) instead.state— query by domain-specific filters (e.g.?is_published=…) instead.db_query— removed permanently; raw query passthrough will not return.sortBy/sortOrder— renamed tosort_by/sort_orderon the wire.HTTP_ERROR,VALIDATION_FAILED,RATE_LIMITED,INTERNAL_ERROR) plus per-domain SCREAMING_SNAKE codes viaAppErrorCode. Existing numericErrorCodeEnumvalues still flow throughBizExceptionand surface as their string names on the wire.Notable deviations
drizzleAdapter) — a design-spec gap resolved here.AppErrorCode(new, SCREAMING_SNAKE strings) and legacyErrorCodeEnum(numeric, used byBizException) coexist by design — generic plumbing moved toAppException, business codes still go throughBizException. A follow-up issue can unify them.Review fixes folded into this PR
note.controller.ts/notes/list/:idempty-fallback double-wrap. The no-currentDocumentbranch returned{ data: [], size: 0 }, whichResponseInterceptorV2wraps to{ data: { data: [], size: 0 } }(Symbol-marked envelopes only, by design). Replaced withwithMeta([], builder.build()). The contract test forArray.isArray(body.data)would have caught this had the fixture exercised the empty path.scripts/check-controller-response-envelope.tsnow flags any controller return literal whose top-level keys includedata(previously only{ data }and{ data, meta }were flagged).Symbol-based detection, and explicitly warns that a literal{ data, ... }will be double-wrapped.snakeKeyboundary-aware.articleURL->article_url,HTMLContent->html_content(was_a_r_t_i_c_l_e__u_r_l). Added a unit test for acronym boundaries.AppExceptionFilterdeduped. Triplestatus >= 500block factored intologServerError(exception); throttle handling intohandleThrottle(ip, url).@BypassCaseTransformJSDoc spells out that the matched subtree is emitted verbatim regardless of depth.PagerDtodeprecation expanded to enumerate the V1 fields dropped in V2 (select,state,db_query,sortBy/sortOrderrename).Verification
208 files changed, +6125 / -3381. Typecheck clean, lint clean, full Vitest suite 1088 passed / 3 skipped / 0 failed.