Skip to content

feat: accept sorts as the canonical sort input key, reject unknown build and schema keys - #906

Merged
tada5hi merged 30 commits into
masterfrom
feat/sorts-vocabulary
Aug 12, 2026
Merged

feat: accept sorts as the canonical sort input key, reject unknown build and schema keys#906
tada5hi merged 30 commits into
masterfrom
feat/sorts-vocabulary

Conversation

@tada5hi

@tada5hi tada5hi commented Aug 12, 2026

Copy link
Copy Markdown
Owner

sort was the only query parameter whose AST property (Query.sorts) differed from its input key (sort), while the build type was already called SortsBuildInput and the schema classes were singular (SortSchema) where every sibling is plural (FieldsSchema, FiltersSchema, RelationsSchema).

This makes sorts the canonical spelling on every developer-authored input surface, keeps sort working as @deprecated (removal in 3.0), and folds in #905.

The URL wire parameter does not change: the query string still carries sort=-id, exactly as the filters input key is carried as filter.

What changed

Surface Before Now
defineQuery build input sort sorts canonical, sort deprecated
defineSchema options sort sorts canonical, sort deprecated
Schema property .sort .sorts canonical, .sort the identical instance
parse() input sort either spelling
ParseQueryOptions skip flag sort either spelling
typeorm EntitySchemaOptions sort sorts canonical, sort deprecated
prisma ModelSchemaOptions sort sorts canonical, sort deprecated
describe() output sort both sorts and sort
URL wire sort sort (unchanged)

Parameter gains SORTS = 'sorts'; Parameter.SORT keeps its value 'sort' and is deprecated, so parameters: ['sort'] masks, describe() readers and error payloads all keep working. Both spellings are accepted in every parameter mask.

The sort schema exports are now plural (SortsSchema, defineSortsSchema, SortsOptions, SortsOptionDefault, SortsSchemaDescription), with the singular names kept as deprecated aliases of the same values.

Supplying both spellings throws a typed KEY_AMBIGUOUS error rather than merging or silently picking a winner. The throw is unconditional, not gated by throwOnFailure, and applies on all three surfaces. An undefined side never triggers it, so a spread migration wrapper ({ sorts: props.sorts, sort: props.sort }) is safe.

Closes #905

defineQuery dropped unknown top-level keys silently, so defineQuery({ filter: ... }) (the wire spelling) produced an empty and therefore unfiltered query. Unknown top-level keys now raise a typed error with a did-you-mean pointer:

defineQuery({ filter: { active: false } })
// BuildError: The key filter is unknown. Did you mean filters?

The same guard covers defineSchema, where a mistyped key (field: instead of fields:) silently declared no allow-list at all, the more permissive direction. Suggestions cover the wire names and obvious singulars: filter to filters, page/limit/offset to pagination, include/relation to relations, field to fields.

parse() input deliberately still ignores unrelated keys: a decoded URL legitimately carries tokens and custom parameters.

Release notes

This must ship as a minor, never a patch. The two fix(core): commits make previously-working calls throw: defineQuery(someObject) carrying unrelated keys used to drop them. Do not cherry-pick 8ac9bcc7 or 1f8e337f onto a patch line.

Three observable changes worth calling out:

  1. Unknown build/schema keys now throw. Loud, immediate, first-call, with a suggestion. TypeScript does not catch the real-world case, since excess-property checking only fires on fresh literals and core: defineQuery silently ignores unknown top-level build-input keys, yielding an unfiltered query #905 is about a variable being passed, so the runtime guard is the only line of defence. Strictly better than the silent-unfiltered-data outcome it replaces, but it lands in the changelog under "Bug Fixes" and deserves a note.
  2. scope.parameter is now 'sorts' for a sort validate/validateMany hook, where it was 'sort'. Anything branching on that value needs updating.
  3. describe() output carries a new sorts key. Any downstream deep-equality snapshot of a schema description needs updating.

Apps calling a parser directly on client data now have a client-triggerable ParseError via a body carrying both sort and sorts. The documented recipe already maps ParseError to a 400, so it degrades correctly.

New public API

Parameter.SORTS, ErrorCode.KEY_UNKNOWN, ErrorCode.KEY_AMBIGUOUS, normalizeParameter, resolveAliasedKey, the five plural sort schema exports, and the sorts keys on QueryBuildInput, SchemaOptions, ParseQueryOptions, SchemaDescription, EntitySchemaOptions and ModelSchemaOptions. assertKnownInputKeys and suggestInputKey stay internal.

Verification

Sixteen commits, each with its own tests, reviewed per commit plus a whole-branch review. nx run-many -t build 11/11, npm run test 10/10 projects (2302 tests), npm run lint clean, docs site builds.

Invariants checked empirically rather than from prose:

  • encoding through both dialects, plain and schema-aware, still emits sort=-id and never sorts=
  • Parameter.SORT === 'sort' unchanged
  • every sort spelling accepted at 2.0.0 is still accepted
  • schema.sorts === schema.sort (the same instance, since extendSchemaOptions/setIndexes mutate it in place)
  • { sorts: false } and { sort: false } both skip; false is not swallowed by the definedness check
  • resolveAliasedKey reads own properties only, so an array input does not see Array.prototype.sort

Follow-up

A 3.0 issue should track removing the sort input key on every surface, Parameter.SORT, the describe() sort key and the singular class aliases. The still-singular internal vocabulary belongs in that same pass: IRootAdapter.sort, parser.parseSort(), decodeSort()/encodeSort(), SortParseError, SortParseOptions.

Summary by CodeRabbit

  • New Features

    • Introduced the canonical sorts query and schema property across the platform.
    • Added support for the deprecated sort alias for backward compatibility.
    • Added clear errors for unknown keys and conflicting sorts/sort usage.
    • URL encoding continues to use the compatible sort wire parameter.
  • Documentation

    • Updated guides, examples, and adapter documentation to consistently describe sorts.
    • Documented migration guidance, alias behavior, and new validation errors.

tada5hi added 17 commits August 12, 2026 14:11
A mistyped build-input key was dropped without a warning, so
defineQuery({ filter: ... }) produced an empty query: downstream that
means unfiltered data, not no data. Unknown keys now raise a typed
BuildError, with a did-you-mean pointing at the canonical spelling for
the URL wire names.

Closes #905
Same failure mode as #905 on the schema side: a mistyped parameter key
(field instead of fields) silently declared no allow-list at all, which
is the more permissive direction.
The enum value of SORT stays 'sort', so parameters masks, describe()
readers and error payloads are untouched. Resolver maps carry both
spellings so either one resolves identically.
sort remains accepted and is marked deprecated. Supplying both raises a
typed BuildError rather than picking a winner.
Schema.sorts and Schema.sort are the identical SortSchema instance, so
the in-place index and throwOnFailure propagation is visible through
both. describe() emits sorts and keeps sort through 2.x.
readParameter and skipParameter are the single choke points, so all
three parser dialects gain the alias at once. Unknown keys stay ignored
on the parse side: a decoded URL legitimately carries unrelated keys.
SortsSchema, defineSortsSchema, SortsOptions, SortsOptionDefault and
SortsSchemaDescription match the FieldsSchema/FiltersSchema/
RelationsSchema convention. The singular names stay exported as
deprecated aliases of the same value.
…ptions

EntitySchemaOptions and ModelSchemaOptions are developer-authored schema
input, so they take the same alias as SchemaOptions.
The wire spelling is unchanged: the URL still carries sort=-id. Encode
parameter masks accept either spelling.
Documents the sorts/sort deprecation and the new unknown-key rejection.
The URL wire parameter is unchanged.
Follow-up to the sorts/sort docs sweep: wire.md, filters.md, errors.md
and recipes/mongo-search.md named the sort sub-schema/input key in
prose or a parse-input example and were missed by the initial pass.
Wire syntax (sort=...) is unchanged.
errors.md was missing KEY_UNKNOWN and KEY_AMBIGUOUS from the
BuildError/SchemaError/ParseError code tables, introduced by the
sorts/sort migration. sort.md's Naming tip now states that supplying
both sorts and sort throws unconditionally, on defineQuery,
defineSchema and every parse()/decode() call.
…rectly

ParameterSchema<'sorts', RECORD> resolved to never since the conditional
type only matched Parameter.SORT, so ResolutionScope.for(registry,
Parameter.SORTS, schema) failed to compile even though the runtime maps
already handled both spellings. Widen the branch to match both.

resolveAliasedKey used a presence-based check (own-property, including
an explicitly undefined value), so passing both spellings with one set
to undefined (e.g. { sorts: props.sorts, sort: props.sort } from a
migration wrapper) threw KEY_AMBIGUOUS even though nothing would be
dropped. Switch to a definedness check, matching how every other build
input property is read.
… edge cases

Add a type-level regression guard so ParameterSchema<'sorts'|'sort'>
cannot silently rot back to never. Add a direct unit spec for
resolveAliasedKey covering canonical-only, alias-only, both-real
(throws), neither, canonical-undefined-with-real-alias and
both-undefined. Mirror adapter-typeorm's sorts-alias schema spec for
adapter-prisma's defineSchemaWithModel, which had no coverage of its
own alias handling. Strengthen adapter-typeorm's "reject both
spellings" assertion from a bare toThrow() to SchemaError with
ErrorCode.KEY_AMBIGUOUS.
…ask check

The expression encoder and the simple visitor each inlined the same
parameters.map((item) => normalizeParameter(item)).includes(Parameter.SORTS)
expression to test whether the sort/sorts alias is present in a
parameter mask. Extract includesParameter next to
intersectQueryParameters, and use it at both call sites.
The earlier docs sweep updated the guide pages but missed the Packages
reference section (packages/docs/packages/*.md) and the per-package npm
READMEs: sample schema/parse input, per-parameter option tables and
canonical-parameter-key lists still spelled the sort input key instead
of sorts. parser-simple/README.md's transport-agnostic paragraph was
materially wrong, not merely stale, since it listed the parser's
canonical keys with the deprecated spelling. URL wire examples (sort=),
page titles/links and plain-English prose are left untouched, since the
wire parameter is still sort.

Also make the KEY_COMBINATION_NOT_INDEXED row in guide/errors.md name
both parameters explicitly (filters, sorts) instead of a bare
non-backticked "sort", consistent with its neighbouring rows.
Finding 4's definedness relaxation (typeof input[key] !== 'undefined')
read through the prototype chain, so an array input's inherited
Array.prototype.sort satisfied the deprecated `sort` alias check:
resolveAliasedKey([], 'sorts', 'sort', ...) returned the sort method
instead of undefined, and defineQuery([] as any) threw inputInvalid
instead of returning an empty query as it did before the fix wave.
Combine the definedness check with isPropertySet so only an own,
defined property counts on either side.

Also strip trailing whitespace left over from an earlier commit in
parser/query.ts and the schema sorts-alias spec.
Copilot AI lite review requested due to automatic review settings August 12, 2026 17:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change makes sorts the canonical query and schema property. The deprecated sort alias remains supported. Ambiguous aliases and unknown top-level keys now produce typed errors. URL codecs, adapters, parsers, tests, and documentation use the updated naming.

Changes

Core API and validation

Layer / File(s) Summary
Core contracts and alias utilities
packages/core/src/constants.ts, packages/core/src/utils/*, packages/core/src/schema/parameter/sort/*, packages/core/src/errors/*
Added plural sort types and Parameter.SORTS. Added alias normalization, unknown-key validation, and KEY_UNKNOWN/KEY_AMBIGUOUS errors.
Core query and schema behavior
packages/core/src/build/*, packages/core/src/parser/*, packages/core/src/schema/*, packages/core/test/unit/*
defineQuery, parsers, and schemas accept sorts and deprecated sort. Supplying both keys raises KEY_AMBIGUOUS. Tests cover alias handling, unknown keys, descriptions, masks, and type mappings.

Adapter and codec integration

Layer / File(s) Summary
Adapters and URL codec integration
packages/adapter-prisma/src/schema/*, packages/adapter-typeorm/src/schema/*, packages/codec-url/src/*, packages/*/test/unit/sorts-alias.spec.ts
Adapter schema output and validation use sorts. URL encoding and decoding normalize both spellings and preserve the sort wire parameter.
Schema fixtures and parser coverage
packages/*/test/data/schema.ts, packages/parser-*/test/unit/sorts-alias.spec.ts
Test schemas use sorts. Expression, Mongo, and simple parsers verify canonical and deprecated sort inputs.

Documentation

Layer / File(s) Summary
Documentation and examples
README.md, packages/*/README.md, packages/docs/guide/*, packages/docs/packages/*
Examples and parameter tables use sorts. Documentation retains sort as a deprecated input alias or URL wire name where applicable. It documents unknown-key and ambiguous-key errors.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • tada5hi/rapiq#746 — Shares the sort schema and parser code affected by this alias migration.
  • tada5hi/rapiq#747 — Shares the core query-building sort API and compatibility handling.
  • tada5hi/rapiq#862 — Shares the Drizzle adapter schema and sorting configuration paths.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes a broad sort to sorts API migration across parsers, adapters, codecs, and documentation, which is not part of linked issue #905. Split the sorts canonicalization and compatibility work into a separate PR, or link issues that explicitly require those changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly names both primary changes: canonical sorts input support and rejection of unknown build and schema keys.
Linked Issues check ✅ Passed The implementation addresses #905 by rejecting unknown defineQuery and defineSchema keys with typed errors, suggestions, and regression tests.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sorts-vocabulary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/core/src/parser/index-policy.ts (1)

274-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the canonical parameter value.

Replace Parameter.SORT with Parameter.SORTS. This parser path remains an internal consumer of the deprecated alias. The replacement keeps the same SortsSchema resolution and removes a deprecated dependency before version 3.0.

Proposed fix
-    const scope = ResolutionScope.for(registry, Parameter.SORT, schema, { throwOnFailure: context.throwOnFailure });
+    const scope = ResolutionScope.for(registry, Parameter.SORTS, schema, { throwOnFailure: context.throwOnFailure });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/parser/index-policy.ts` at line 274, Update the
ResolutionScope.for call in the parser path to use the canonical Parameter.SORTS
value instead of the deprecated Parameter.SORT alias, preserving the existing
registry, schema, and throwOnFailure arguments.
packages/core/test/unit/schema/sorts-alias.spec.ts (1)

40-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover undefined alias values.

Add cases for { sorts: {...}, sort: undefined } and { sorts: undefined, sort: {...} }. Both inputs must construct a schema and select the defined value. This protects the required alias-resolution behavior.

Proposed test cases
+    it('should ignore an undefined alias value', () => {
+        const canonical = defineSchema<User>({
+            sorts: { allowed: ['id'] },
+            sort: undefined,
+        } as any);
+        const deprecated = defineSchema<User>({
+            sorts: undefined,
+            sort: { allowed: ['name'] },
+        } as any);
+
+        expect(canonical.sorts.allowed).toEqual(['id']);
+        expect(deprecated.sorts.allowed).toEqual(['name']);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/unit/schema/sorts-alias.spec.ts` around lines 40 - 54, Add
unit tests in the alias-resolution suite covering configurations with a defined
sorts value and sort: undefined, and the inverse with sort defined and sorts:
undefined. Assert each constructs successfully and uses the defined alias value,
while preserving the existing rejection behavior when both aliases are actually
defined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/parser-simple/README.md`:
- Line 33: Update the README’s URL-query example to use the wire key sort, or
explicitly label sorts as canonical parser input. Revise the parser-key
documentation around the “only” wording to state that canonical sorts is
preferred while deprecated sort remains accepted as an alias, consistent with
the existing sorts alias behavior.

---

Nitpick comments:
In `@packages/core/src/parser/index-policy.ts`:
- Line 274: Update the ResolutionScope.for call in the parser path to use the
canonical Parameter.SORTS value instead of the deprecated Parameter.SORT alias,
preserving the existing registry, schema, and throwOnFailure arguments.

In `@packages/core/test/unit/schema/sorts-alias.spec.ts`:
- Around line 40-54: Add unit tests in the alias-resolution suite covering
configurations with a defined sorts value and sort: undefined, and the inverse
with sort defined and sorts: undefined. Assert each constructs successfully and
uses the defined alias value, while preserving the existing rejection behavior
when both aliases are actually defined.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842e49e8-664c-4665-a830-28acded68247

📥 Commits

Reviewing files that changed from the base of the PR and between 63fb27a and 7c99dd7.

📒 Files selected for processing (90)
  • README.md
  • packages/adapter-drizzle/README.md
  • packages/adapter-drizzle/test/data/schema.ts
  • packages/adapter-memory/README.md
  • packages/adapter-prisma/README.md
  • packages/adapter-prisma/src/schema/assert.ts
  • packages/adapter-prisma/src/schema/module.ts
  • packages/adapter-prisma/src/schema/types.ts
  • packages/adapter-prisma/test/data/schema.ts
  • packages/adapter-prisma/test/unit/sorts-alias.spec.ts
  • packages/adapter-sql/README.md
  • packages/adapter-typeorm/README.md
  • packages/adapter-typeorm/src/schema/assert.ts
  • packages/adapter-typeorm/src/schema/module.ts
  • packages/adapter-typeorm/src/schema/types.ts
  • packages/adapter-typeorm/test/unit/schema/sorts-alias.spec.ts
  • packages/codec-url/src/decoder/module.ts
  • packages/codec-url/src/expression/encoder/module.ts
  • packages/codec-url/src/simple/encoder/visitors/module.ts
  • packages/codec-url/src/utils/encode.ts
  • packages/codec-url/test/data/schema.ts
  • packages/codec-url/test/unit/sorts-alias.spec.ts
  • packages/core/README.md
  • packages/core/src/build/module.ts
  • packages/core/src/build/types.ts
  • packages/core/src/constants.ts
  • packages/core/src/errors/build.ts
  • packages/core/src/errors/code.ts
  • packages/core/src/errors/parse.ts
  • packages/core/src/errors/schema.ts
  • packages/core/src/index.ts
  • packages/core/src/parser/index-policy.ts
  • packages/core/src/parser/parameter/sort/types.ts
  • packages/core/src/parser/parameter/validate.ts
  • packages/core/src/parser/query.ts
  • packages/core/src/parser/relation-prune.ts
  • packages/core/src/parser/types.ts
  • packages/core/src/schema/module.ts
  • packages/core/src/schema/parameter/sort/define.ts
  • packages/core/src/schema/parameter/sort/deprecated.ts
  • packages/core/src/schema/parameter/sort/index.ts
  • packages/core/src/schema/parameter/sort/schema.ts
  • packages/core/src/schema/parameter/sort/types.ts
  • packages/core/src/schema/resolver/module.ts
  • packages/core/src/schema/resolver/types.ts
  • packages/core/src/schema/types.ts
  • packages/core/src/utils/index.ts
  • packages/core/src/utils/input.ts
  • packages/core/src/utils/parameter.ts
  • packages/core/test/data/schema.ts
  • packages/core/test/unit/build/module.spec.ts
  • packages/core/test/unit/build/sorts-alias.spec.ts
  • packages/core/test/unit/build/unknown-keys.spec.ts
  • packages/core/test/unit/parser/parameter/key-validation.spec.ts
  • packages/core/test/unit/schema/describe.spec.ts
  • packages/core/test/unit/schema/sorts-alias.spec.ts
  • packages/core/test/unit/schema/sorts-naming.spec.ts
  • packages/core/test/unit/schema/unknown-keys.spec.ts
  • packages/core/test/unit/types.spec.ts
  • packages/core/test/unit/utils.spec.ts
  • packages/docs/guide/building-queries.md
  • packages/docs/guide/concepts.md
  • packages/docs/guide/errors.md
  • packages/docs/guide/filters.md
  • packages/docs/guide/index.md
  • packages/docs/guide/migration-v1.md
  • packages/docs/guide/quick-start.md
  • packages/docs/guide/recipes/express-typeorm.md
  • packages/docs/guide/recipes/frontend.md
  • packages/docs/guide/recipes/mongo-search.md
  • packages/docs/guide/schemas.md
  • packages/docs/guide/sort.md
  • packages/docs/guide/wire.md
  • packages/docs/packages/adapter-drizzle.md
  • packages/docs/packages/adapter-prisma.md
  • packages/docs/packages/adapter-typeorm.md
  • packages/docs/packages/codec-url.md
  • packages/docs/packages/core.md
  • packages/docs/packages/parser-expression.md
  • packages/docs/packages/parser-mongo.md
  • packages/docs/packages/parser-simple.md
  • packages/parser-expression/README.md
  • packages/parser-expression/test/data/schema.ts
  • packages/parser-expression/test/unit/sorts-alias.spec.ts
  • packages/parser-mongo/README.md
  • packages/parser-mongo/test/data/schema.ts
  • packages/parser-mongo/test/unit/sorts-alias.spec.ts
  • packages/parser-simple/README.md
  • packages/parser-simple/test/data/schema.ts
  • packages/parser-simple/test/unit/sorts-alias.spec.ts

Comment thread packages/parser-simple/README.md Outdated
@tada5hi

tada5hi commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

The sorts rename left two statements inaccurate. The URL-query-shaped
bullet claimed the example was the exact structure a query string
yields, but a query string carries the wire key sort, not the canonical
sorts. The transport-agnostic paragraph said the parser reads the
canonical keys only, which no longer holds now that sort is accepted as
a deprecated alias.

Reported by CodeRabbit on #906.
Schema.describe() assigned the same description object to both
output.sorts and output.sort, so a consumer mutating one mutated the
other through the shared reference. Emit sorts only.

SchemaDescription.sort is removed. The sorts-alias.spec.ts case is
converted, not deleted: it now asserts output.sorts holds the
description and output.sort is undefined. Docs corrected to no longer
document the dual key.
Two call sites still passed the deprecated Parameter.SORT member into
ResolutionScope.for, pinning the sort resolution stack to a member
scheduled for removal in 3.0. Behavior is identical today since both
spellings key the same schema and error classes, but the canonical
member belongs here.

The internal SortScope type alias in parser-simple's sorts module
follows the same change.
…se vocabulary

Apply the same canonical-plural pattern already used for SortsSchema to
the parse-layer sort vocabulary: the plural name is canonical, the
singular name stays exported as a deprecated alias of the same value
or type.

- core: SortParseError -> SortsParseError, SortParseOptions ->
  SortsParseOptions, BaseQueryParser.parseSort()/parseSortAsync() ->
  parseSorts()/parseSortsAsync() (the deprecated forms are now thin
  delegating methods).
- parser-simple: SimpleSortParser -> SimpleSortsParser,
  SortBuildInput -> SortsBuildInput, SortBuildRecordInput ->
  SortsBuildRecordInput (now exported alongside its canonical form,
  matching the sibling FieldsBuildRecordInput convention).
- codec-url: the two parser.parseSort(...) call sites in the decoder
  switch to the canonical parseSorts().

Internal references inside core and parser-simple were updated to the
canonical names. Downstream packages (parser-mongo, parser-expression)
keep extending the deprecated SimpleSortParser alias unchanged; they
resolve through it like any other consumer.
…ortsParser abstract member

BaseQueryParser.sortParser was the only abstract sub-parser member spelled
singular; its siblings are fieldsParser/filtersParser/paginationParser/
relationsParser. Rename it to sortsParser together with its three dialect
overrides, since an abstract member has no soft-deprecation path and must
move in one coordinated change.

Also rename applySortIndexPolicy to applySortsIndexPolicy and
buildSortDefaults to buildSortsDefaults (both keep a deprecated alias),
switch parser-simple's sorts module off the deprecated SortSchema/SortScope
core aliases onto SortsSchema/SortsScope, and fix stray "sort"-parameter
prose in core JSDoc to "sorts".
…ames

ExpressionSortParser and MongoSortParser parsed the sorts parameter but
were named after one member; their siblings (ExpressionFieldsParser,
MongoFieldsParser, ...) are plural. Rename both to ExpressionSortsParser
and MongoSortsParser, keep the old names as deprecated aliases, and route
both through parser-simple's canonical SimpleSortsParser base class
instead of its deprecated singular alias.
The sorts sub-adapter and its options were the only parameter area still
spelled singular: ISortAdapter, SortBaseAdapter, SortAdapter,
SortContainerOptions and SortInterpreterOptions, plus IRootAdapter.sort /
Adapter.sort, while every sibling (fields/filters/relations/pagination) is
parameter-named. QueryVisitor already assembled `this.sorts = new
SortsVisitor(adapter.sort, options)`, mixing both spellings for the same
thing.

Rename all of them to their plural form and keep deprecated aliases.
IRootAdapter now declares both `sorts` (required) and a deprecated
optional `sort`; the concrete Adapter class exposes `sorts` as the real
field and `sort` as a deprecated get/set accessor pair delegating to it,
so an external subclass assigning `this.sort = x` keeps working.

Also fix RelationInterpreterOptions to RelationsInterpreterOptions in the
same visitor layer: it configures RelationsVisitor as a whole, the same
bug in the relations area.
…sorts member

Mirrors the adapter-sql sorts rename: SortAdapter becomes SortsAdapter
(extending adapter-sql's SortsBaseAdapter, no longer the deprecated
alias), and TypeormAdapter.sort becomes TypeormAdapter.sorts. The
deprecated `sort` accessor is a full getter/setter pair delegating to
`sorts`, so an external subclass assigning `this.sort = x` keeps working.
'sort:relation' and 'sort:numeric-name' were the only sort:* holdouts;
every other featureUnsupported tag in the fleet is parameter-named plural
(filters:itself, filters:negation, filters:complexity, ...). No test or
doc pins the old strings.
decodeSort/encodeSort and the internal QueryVisitor.sort / QuerySerializer.sort
members were the only parameter-named spots still singular; their siblings
(decodeFields/encodeFields, .fields/.filters/.pagination/.relations, ...)
are plural. All four are internal (BaseURLDecoder, SimpleURLEncoder,
ExpressionURLEncoder and the visitor/serializer pair are not re-exported
from src/index.ts), so no alias is needed. The wire constant
URLParameter.SORT is untouched.
Teach the renamed canonical names (SortsParseError, SimpleSortsParser,
SortsAdapter, adapter.sorts) and fix stray singular "sort"-as-parameter
prose to "sorts" throughout the guide, package READMEs and .agents notes,
matching the parameter's own settled vocabulary. The sort= / filter[...]=
URL wire examples, the /guide/sort route and the URLParameter.SORT key
column are left untouched, since those never change.

Also drops a stale "sort tuple groups" claim from the ResolutionScope
parser-quirks list in .agents/architecture.md: tuple groups were removed
by the schema `indexes` work, so the earlier note was factually stale.
Both name the parameter itself in a list beside filters and fields, so
they take the plural form the rest of the branch established.
…tParser singular for 2.x

BaseQueryParser is an exported abstract class, so its protected abstract
sortParser member is an extension point for custom parser dialects.
Renaming it to sortsParser has no alias path (an abstract member cannot
be aliased), so an external subclass implementing sortParser would stop
compiling. release-please derives the version from commit types, so a
refactor commit carrying that break would ship silently in a minor.

The rename moves to 3.0, alongside the removal of the deprecated
aliases this branch introduced.
@tada5hi
tada5hi merged commit 671ae5a into master Aug 12, 2026
9 checks passed
tada5hi added a commit to authup/authup that referenced this pull request Aug 13, 2026
* chore(deps): update rapiq packages to v2.1.0

Bumps @rapiq/{core,codec-url,parser-mongo,adapter-memory,adapter-sql,
adapter-typeorm} from ^2.0.0 to ^2.1.0.

The release carries a third change beyond its two headline features:
tada5hi/rapiq#906 makes `sorts` the canonical spelling on every
developer-authored surface. Two consequences had to be handled.

`describe()` renamed its `sort` key to `sorts` and, unlike the build
input / schema option / Schema property surfaces, kept no deprecated
alias. That description is served verbatim as `meta.schema`, so the
rename is wire-visible for API consumers. The URL parameter is
unchanged (`?sort=-name`).

The entity collection manager decided whether a load supersedes the
retained interactive sorts by testing `'sort' in input`, so a load
carrying the now-canonical `sorts` was silently dropped. It accepts
both spellings, matched on a DEFINED value rather than key presence:
rapiq documents `{ sorts: props.sorts, sort: props.sort }` as a safe
spread wrapper, and that shape carries both keys as `undefined`, which
a presence test reads as "supplied" and would wipe the retained sorts.

`indexed-invariant.spec.ts` reported green throughout the bump while
checking nothing: it iterated `description.sort?.allowed || []`, which
the rename turned into `undefined`, so the loop never ran and the
"every allowed sort key leads an index" invariant silently stopped
being enforced. Re-pointed, and guarded against passing vacuously
again. The invariant itself still holds.

* refactor: use the canonical rapiq sorts spelling

rapiq 2.1.0 (tada5hi/rapiq#906) renamed the sort key to `sorts` across
every developer-authored surface, keeping `sort` as a deprecated alias
slated for removal in 3.0. Migrates all authored uses: the 25 entity
schema declarations plus the query build inputs in the kit components,
the admin console and the account console.

The wire parameter is deliberately untouched. A query string still
carries `?sort=-name`, so the parse-input spellings in the decode specs
stay as they are, alongside the sibling `filter` and `page` wire names.

The collection manager keeps reading the deprecated key too. That is
not authup authoring deprecated syntax, it is tolerating it from
callers: rapiq still declares `sort` on QueryBuildInput, so dropping
the branch would silently discard a consumer's sorts.

* refactor(server-core): adopt the upstream schema index assertion

`assertSchemaIndexesMatchEntity` existed because
`assertSchemaMatchesEntity` did not cover a schema's `indexes`
declaration. rapiq 2.1.0 folded that check upstream (tada5hi/rapiq#902
closed #898), and `validateEntitySchemas` already calls the upstream
assert on the line above, so the local copy was duplication.

Upstream applies the same leftmost-prefix rule over the same primary
key, unique and index structures, and is stricter in two ways: it
resolves columns by property PATH, so embedded columns compare
correctly, and it additionally rejects an index naming a column the
entity does not have. It reports a typed SchemaEntityIndexMismatchError
instead of a bare Error.

The two tests are re-pointed at the upstream function rather than
dropped, so the boot guarantee stays pinned locally.

* fix(server-core): mint one key per realm and use under concurrency

`resolveOrCreate` is check-then-act: it looks for an active key, counts
existing rows, then inserts. Nothing made that atomic, so every
concurrent caller for one (realm, use) observed zero rows and inserted
its own key.

The callers are hot paths. The token signer resolves a signature key
for EVERY issuance and the realm cipher an encryption key for every MFA
seed, so two simultaneous logins into a freshly created realm each
minted a key. A new spec reproduces it deterministically: three
concurrent resolves produced three keys.

Concurrent mints now share a single in-flight promise, and the guarded
section re-reads before inserting, because a caller's lookup may
predate a mint that has since completed and left the map.

The map lives on the adapter instance, so it is scoped to one
application and two applications in one process never share mint state.
That scoping is also why ProvisionerModule now prefers the registered
KeyStore over constructing its own adapter, falling back only in the
minimal module graphs that never register one: a second instance
carries a second map, so the startup backfill and a concurrent
realm-create request would each still mint.

Separate processes can continue to race. The duplicate is tolerable
rather than fatal, since both keys are published in JWKS and verify and
selection is deterministically ordered, so this deliberately stops
short of a distributed lock.

* test(server-core): pin mint recovery after a failed key mint

The in-flight guard would be worse than the race it fixes if a rejected
mint stayed in the map: every later caller would receive the same
rejected promise and the realm could not mint again until the process
restarted. Covers the kill-switch rejection arriving on concurrent
resolves, and that a later resolve still succeeds.

* test(server-core): check the described query blocks per schema

The vacuity guard flattened every schema's allow-list into one array, so
a single schema losing its `filters` or `sorts` block stayed invisible:
the other schemas kept the count positive while the invariant loops'
`|| []` silently skipped the one that had gone missing. It now asserts
the block per schema and reports which schema lacks it.

Non-emptiness is deliberately NOT asserted per schema. An absent
allow-list is legitimate — `clientScope` declares no sort vocabulary, so
its `sorts.allowed` is null by design and a per-schema non-empty
assertion would fail against it. A separate aggregate check keeps the
loops from running over nothing at all.

Also corrects the architecture note, which still described the sort
override as testing key presence after it moved to a defined-value test.

Reported by CodeRabbit on #3431.
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.

core: defineQuery silently ignores unknown top-level build-input keys, yielding an unfiltered query

2 participants