NestJS: Known Limitations
Relation row limits ($limit)
When you use $limit to cap relation rows, the strategy depends on what fields are in the nested select.
Scalar fields only — SQL-level limiting
If your relation select contains only scalar columns (no computed or derived fields), Relayer uses ROW_NUMBER() in SQL:
SELECT "id", "content", "post_id", "author_id"FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY "post_id") as "__rn" FROM "comments" WHERE "post_id" IN (1, 2, 3)) "__sub"WHERE "__rn" <= 5This is efficient — the database handles the per-parent limiting, only the needed rows are returned.
Computed or derived fields — JS-level limiting
When the relation select includes computed fields (@Entity.computed) or derived fields (@Entity.derived), SQL-level limiting is not possible — these fields require Drizzle’s query builder with custom SQL expressions that can’t be wrapped in a ROW_NUMBER() subquery.
In this case, Relayer falls back to loading all matching rows from the database and slicing per-parent in JavaScript:
// All comments loaded, then sliced to 5 per post in JSselect: { comments: { $limit: 5, id: true, content: true, upperContent: true } // ^^^^^^^^^^^^ // computed field -> JS fallback}Performance impact: For large datasets (thousands of child records per parent), the JS fallback loads all rows into memory before slicing. This can cause:
- High memory usage
- Slow response times
- Database load from transferring unnecessary rows
Recommendations:
- Avoid computed/derived fields in limited relation selects when working with large datasets
- Use a nested resource endpoint instead:
GET /posts/5/comments?limit=5runs a direct query with SQL LIMIT - Set
defaultRelationLimitglobally to prevent accidental full-table loads
SQLite
SQLite (better-sqlite3) always uses JS-level limiting regardless of field types, because the driver lacks async execute() support for raw SQL.
Cursor pagination
Cursor pagination (pagination: 'cursor_UNSTABLE') has a known issue with timestamp precision.
The problem
PostgreSQL stores timestamps with microsecond precision (6 decimal places):
2025-01-15 10:30:00.157432JavaScript Date only supports millisecond precision (3 decimal places):
2025-01-15T10:30:00.157Z (lost: 432)When the cursor stores a Date value and uses it for equality comparison on the next page request, the comparison can fail:
-- Cursor value (from JS Date, ms precision)WHERE "created_at" = '2025-01-15 10:30:00.157000'
-- Actual value in PG (μs precision)-- '2025-01-15 10:30:00.157432'-- These are NOT equal -> rows skippedWhen it matters
This only affects cursor pagination when:
- Sorting by a timestamp field (e.g.,
createdAt) - Multiple records share the same millisecond-precision timestamp
- The cursor lands exactly on such a record
In practice, the ID tiebreaker (always added automatically) makes this rare — two records must have the exact same millisecond timestamp AND be adjacent in sort order.
When it doesn’t matter
- Sorting by numeric or string fields (IDs, titles, etc.)
- Timestamps with
timestamp(3)precision in your schema (max 3 decimal places = ms precision) - Tables where records rarely share the same millisecond timestamp
Workaround
Add a computed field that extracts the timestamp as a text string with full precision, and sort by it:
const PostBase = createRelayerEntity(schema, 'posts');
export class PostEntity extends PostBase { @PostBase.computed({ resolve: ({ table, sql }) => sql`${table.createdAt}::text`, }) createdAtRaw!: string;}defaults: { orderBy: { field: 'createdAtRaw', order: 'desc' },}String comparison preserves full PostgreSQL precision. The cursor stores and compares strings, avoiding the JS Date precision loss entirely.
Future fix
This will be resolved when cursor logic moves to Relayer core (ORM level), where it has access to Drizzle’s sql template and can extract cursor values with full database precision.
Entity types and relations
Entity classes created with createRelayerEntity(schema, 'posts') include scalar and computed/derived fields but not relation fields by default.
To get full relation-aware types (with autocomplete for entity.comments, entity.author.fullName, etc.), use the entity map pattern with TEntities generic:
// Define entity mapexport const entities = { users: UserEntity, posts: PostEntity, comments: CommentEntity };export type EM = typeof entities;
// Service with relation-aware typesclass PostsService extends RelayerService<PostEntity, EM> { ... }
// Model<PostEntity, EM> includes: id, title, ..., author, comments (with nested types)Model<TEntity, TEntities> resolves relation fields automatically from the Drizzle schema. This works in services, hooks, dto mappers, and controller config (via @CrudController<PostEntity, EM>).
Roadmap
- Stable cursor pagination (requires
@relayerjs/drizzlepatch) - Swagger for API documentation
- API endpoints for linking m2m, one2m relations
- Better integration with Relayer context object