refactor: move the relation query layer out of the adapter - #38
Merged
Merged
Conversation
`col()` decided three things at once — an explicit table wins, a declared column gets the service's own table, anything else is left alone because it may already be a qualified ref or a JSON path — and it read `propertyMap` and `options.name` off the instance, so the rules were only reachable through an adapter. `qualifyColumn` takes them as parameters instead. `col()` stays as the bound convenience the 21 call sites use, and the rules are now covered by in-source tests: precedence, the `null` "already qualified" case, the empty-string fallback, no double qualification, array mapping and non-string pass-through. Also a prerequisite for moving the relation layer out: both sides need these rules, and neither should own them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Half of adapter.ts was relation resolution: 25 methods that call each
other constantly and reach outside for only four things — `options`, `app`,
`propertyMap` and `getPropertyType`. It now lives in `RelationQuery` with
those four passed in, and the file drops from 2648 to 1481 lines.
The interface got smaller than what it replaced. Composing a query used to
mean three calls in a fixed order with a Map threaded between them:
const { q, query, sortRefs } = this.applyJoins(q, params, { order })
q = this.applyWhere(q, query)
q = this.applySort(q, filters, sortRefs)
It is now two calls in any order, with normalization and the sort
references hidden:
q = relations.applyWhere(q, params.query)
q = relations.applyOrder(q, filters.$sort)
Related services are reached through an injected `lookupService` rather
than the Feathers app, so a multi-hop chain can be exercised without
registering services, running `app.setup()`, or creating a schema —
`test/relation-query.test.ts` drives the module that way, which is how the
uniqueness and fallback rules are covered now.
Building the context per call also fixes a latent inconsistency:
adapter-commons merges `params.adapter` over the options, which
`filterQuery` honoured while the relation layer read `this.options`
directly, silently ignoring a per-call `relations` or `name` override.
Pure move otherwise — all 20 SQL-shape snapshots are unchanged, so the
generated SQL is byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
feathers-kysely | db5b0fa | Commit Preview URL Branch Preview URL |
Sep 02 2026, 09:19 AM |
commit: |
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.
adapter.tswas 2648 lines, and half of it was relation resolution: 25 methods that call each other constantly and reach outside for only four things —options,app,propertyMapandgetPropertyType. That is a clean seam, so it moved.adapter.tsis now 1481 lines.Pure refactor. No behaviour change, no public API change.
The interface got smaller than what it replaced
Composing a query used to mean three calls in a fixed order with a
Mapthreaded between them:Two calls now, order-independent, with normalization and the sort references hidden behind them:
Proof that it is a pure move
All 20 SQL-shape snapshots are unchanged, so the generated SQL is byte-identical. That is what those snapshots were added for — without them, 1100 moved lines could only be backed by "tests are green".
The seam pays for itself
Related services are reached through an injected
lookupServiceinstead of the Feathers app:So a multi-hop chain can be exercised with a plain object — no registered services, no
app.setup(), no schema, no data.test/relation-query.test.tsdrives the module that way, which is how the uniqueness and fallback rules are covered now:The same case needed a dedicated test file with three app variants in #37.
A latent inconsistency fixed on the way
adapter-commonsmergesparams.adapterover the options:filterQueryhonoured that; the relation layer readthis.optionsdirectly, so a per-callrelationsornameoverride was silently ignored there. The context is now built per call, which fixes it by construction.applyWheretakes an optionalparamsfor the same reason.Commits
refactor: extract column qualification into a pure util—col()decided three things at once (an explicit table wins; a declared column gets the service's own table; anything else is left alone because it may already be a qualified ref or a JSON path) and readpropertyMap/options.nameoff the instance, so the rules were only reachable through an adapter.qualifyColumntakes them as parameters,col()stays as the bound convenience for the 21 call sites, and the rules now have in-source tests — precedence, thenull"already qualified" case, the empty-string fallback, no double qualification, array mapping, non-string pass-through. Both sides of the split need these rules and neither should own them.refactor: move the relation query layer behind its own interface— the move itself.What did not move, and why
_create,executeAndReturnandfilterQuery. Extracting it means passing the adapter in: a large interface in front of a small implementation, which is the wrong direction. It is already split into named private methods, which is most of the benefit.RelationQueryis not exported fromindex.ts. It is implementation, not public API; the tests import it by path.Testing
692 tests pass against sqlite, 815 against postgres, typecheck clean, no new lint warnings. The benchmark and the
EXPLAINcost report run unchanged.🤖 Generated with Claude Code