Skip to content

Release v2.2.6 - #27

Merged
kodjunkie merged 16 commits into
masterfrom
release/v2.2.6
Jul 31, 2026
Merged

Release v2.2.6#27
kodjunkie merged 16 commits into
masterfrom
release/v2.2.6

Conversation

@kodjunkie

Copy link
Copy Markdown
Owner

A patch release. It fixes cursor-mode pagination's handling of a route's default sort and closes every security advisory reported since 2.2.5.

Fixed

  • Cursor-mode getMany now falls back to a route's @Crud({ query: { sort } }) default when a request omits ?sort=, matching how offset mode has always resolved sort. Applies to @nestjs-crud/typeorm, @nestjs-crud/drizzle, @nestjs-crud/mikro-orm, and @nestjs-crud/prisma. A route default that declares two or more sort fields still returns 400, now with a message naming the field count and noting it came from the route configuration. A request with no sort anywhere still returns 400, with a message describing both failure cases instead of reporting a bare field count of zero.
  • In @nestjs-crud/drizzle, @nestjs-crud/mikro-orm, and @nestjs-crud/prisma, the first cursor page now always orders by the sort field plus a primary-key tie-breaker. Before this fix, the first page could return ties in unspecified order, risking skipped or duplicated rows once pagination continued.

Security

This release closes every dependabot advisory reported since 2.2.5: 56 advisories, all closed through dependency and lockfile updates, none through dismissal.

  • The most severe was a critical archive-extraction denial-of-service in build tooling, affecting both the root workspace and the example app. Neither issue reached the installed dependency closure of a published package.
  • One item carries forward: the @nestjs-crud/typeorm peer range on typeorm still admits older releases for consumers who pin low. Tightening that range is planned peer-floor work for the next minor.

kodjunkie added 16 commits July 30, 2026 20:27
Cursor-mode getMany rejected requests with no ?sort= even when the route
declared a single-field @crud({ query: { sort } }) default, because the
guard validated only client-parsed sort before the offset composer's
client-then-default fallback ever ran. Add a shared resolveCursorSort
helper in @nestjs-crud/core/cursor that mirrors the offset resolution
order (client sort, then route default, then neither) and wire it into
TypeOrmCrudService.doGetManyCursor ahead of every other cursor guard.
Multi-field defaults and no-sort-anywhere still 400, with messages naming
the responsible origin instead of a bare field count.

Adds a fixture route and an integration cell proving a defaulted sort
reaches a live 200 with a cursor that decodes to the declared field.
Unit specs for the cursor default-sort fallback helper — client sort,
route-default fallback (single and multi-field), no-sort-anywhere,
empty-array-as-absent on both sides, and the locked error-message prefix
with no legacy count-suffix wording. No database dependency; 100% branch
coverage on the helper.
Adds the remaining cursor 400-path integration cells: a multi-field route
default with no ?sort= still 400s naming the route default as the origin;
no sort anywhere (neither client nor route) 400s naming both remedies with
no legacy count-suffix wording; and a mismatch-guard regression replaying
a defaulted-sort cursor against an explicit differing ?sort=. The two
pre-existing 400 cells (multi-field client sort, missing limit) are
untouched.
doGetManyCursor now calls the shared resolveCursorSort helper (client
?sort= first, else the route's @crud({ query: { sort } }) default)
instead of validating only the client-parsed sort before the
single-sort-field guard. A route declaring a single-field default now
returns a live keyset page with no ?sort= on the request; a multi-field
default and the no-sort-anywhere case still 400 with origin-naming
messages instead of the old misleading "got: 0" count.

Adds two fixture routes (users-cursor-default-sort,
users-cursor-multi-sort) and three integration cells to
packages/drizzle/test/cursor.spec.ts; all 15 pre-existing cells pass
unchanged.
doGetManyCursor now calls the shared resolveCursorSort helper (client
?sort= first, else the route's @crud({ query: { sort } }) default)
instead of validating only the client-parsed sort before the
single-sort-field guard. A route declaring a single-field default now
returns a live keyset page with no ?sort= on the request; a multi-field
default and the no-sort-anywhere case still 400 with origin-naming
messages instead of the old misleading "got: 0" count. The fresh
per-call EntityManager access pattern is untouched.

Adds two fixture routes (users-cursor-default-sort,
users-cursor-multi-sort) and three integration cells to
packages/mikro-orm/test/cursor.spec.ts; all 15 pre-existing cells pass
unchanged under the ESM test runner.
PrismaQueryComposer.applyCursor returned early when no cursor was
decoded, so a route-declared default sort on cursor mode's first page
never reached ORDER BY and rows came back in arbitrary database order.
The sort-field allowlist check also only ran on non-first pages.

Restructure applyCursor so the allowlist check and the ORDER BY
assignment (with primary-key tie-breaker) run on every cursor request,
including the first page. Only the keyset WHERE composition stays
gated on a decoded cursor, since there is no prior page position to
resume from on a first page.
PrismaCrudService.doGetManyCursor validated only the client-parsed
sort array, so cursor-mode requests with no ?sort= against a route
declaring a @crud({ query: { sort } }) default always 400'd — even
when a single-field default existed to fall back to. Offset mode
already resolves this fallback; cursor mode did not.

Wire the shared resolveCursorSort helper (introduced for TypeORM in a
prior commit) into doGetManyCursor before every other cursor guard,
replacing the inline client-only check. A route-declared single-field
default is now honored the same way a client-supplied sort is; a
multi-field default or no sort anywhere still 400s with a message
naming the origin instead of the previous misleading count.

Add two fixture routes (/users-cursor-default-sort,
/users-cursor-multi-sort) and four integration cells covering the
200 default-sort path, first-page ordering, the multi-field-default
400, and the no-sort-anywhere 400.
Add a Sort resolution section to the cursor pagination wiki page,
placed right after Setup. Covers the client-then-route-default
resolution order, the two multi-field 400 cases, the no-sort 400
case, and a table mapping each condition to its exact message text.
… and CLAUDE.md

Add an Unreleased/Fixed entry to the root CHANGELOG and every
adapter package CHANGELOG (core, typeorm, drizzle, mikro-orm,
prisma) describing the cursor sort-resolution fix, plus a second
Prisma-specific entry for the first-cursor-page ORDER BY fix.
Rewrite the cursor bullet in CLAUDE.md's Key patterns list to
state the resolution order instead of stale 400-condition wording.
…zzle and MikroORM

DrizzleQueryComposer.applyCursor and MikroOrmQueryComposer.applyCursor
skipped the sort-field allowlist check and the ORDER BY assignment
(sort field + primary-key tie-breaker) whenever no cursor was decoded,
i.e. on every cursor-mode first page. A tie-prone sort field could then
return rows in unspecified order on that first page, letting later
keyset pages silently skip or duplicate rows. Ports the same
restructuring already applied to PrismaQueryComposer.applyCursor this
phase: the allowlist check and ORDER BY now run unconditionally, and
only the keyset WHERE stays gated on a decoded cursor.

Adds a first-page ordering regression cell to each adapter's cursor
spec that asserts the ORDER BY SQL fragment received by the driver
connection, not just response row order — the latter false-passes
because small LIMIT-bounded unordered scans often incidentally return
primary-key order anyway.
Closes 39 open advisories against the root yarn.lock via a dedupe-first
fix ladder: yarn dedupe + yarn up -R for every package whose parent range
already admits the patched version, and a resolutions entry only where a
parent hard-pins an exact version outside every declared range (lerna's
js-yaml). Ten packages moved purely via re-resolution; nine hono/
@hono/node-server alerts closed because the package left the tree
entirely (a stale @prisma/dev-era transitive), which also let two dead
resolutions entries get deleted. The existing resolutions block was
audited in place: three stale floors bumped (tar, plus two lerna-scoped
tar entries), one dead global tar entry removed, one dead hono entry and
one dead @hono/node-server entry removed, and the fast-uri floor raised
to the current patched version. No new resolutions forced a version
outside its declaring parent's own range, and no direct dependency
range or peerDependencies changed.
Bump every demo dependency to a current caret range (no exact pins),
staying inside each package's declared peer cap, and regenerate the
lockfile. Closes all 17 demo-lockfile dependabot advisories, including
the critical node-tar decompression chain and the long-standing uuid
advisory (now satisfied by typeorm's refreshed uuid dependency).

typescript stays on the 5.x line after 6.x/7.x both failed to
compile/install cleanly; @types/node capped at the 24.x line to match
CI's Node version, with the README's Prerequisites floor raised to
match.
Closes 56 open dependabot alerts accumulated since 2.2.5, all via
dependency and lockfile updates with zero dismissals. No advisory
reached the installed dependency closure of a published package;
the one residual concern (typeorm's wide peer range) carries forward
as existing peer-floor work already planned for the next minor.
@kodjunkie
kodjunkie merged commit 2dbc8f4 into master Jul 31, 2026
12 checks passed
@github-actions

Copy link
Copy Markdown

Packages with version v2.2.6 have been released

@kodjunkie
kodjunkie deleted the release/v2.2.6 branch July 31, 2026 11:35
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