Query
All read operations go through the same query system. This page covers every option
available in findMany, findOne, findById, and count.
Note:
datrix.findManyanddatrix.crud.findManyare identical.datrix.raw.findManybehaves the same but skips plugin hooks (noonBeforeQuery/onAfterQuerydispatch).
where
Filter records by field values. Supports direct values, comparison operators, logical operators, and nested relation conditions.
Direct value (shorthand for $eq)
// These are equivalent
await datrix.findMany("user", { where: { role: "admin" } });
await datrix.findMany("user", { where: { role: { $eq: "admin" } } });
Comparison operators
| Operator | Types | Description |
|---|---|---|
$eq | all | Equal |
$ne | all | Not equal |
$gt | number, date | Greater than |
$gte | number, date | Greater than or equal |
$lt | number, date | Less than |
$lte | number, date | Less than or equal |
$in | all | Value is in array |
$nin | all | Value is not in array |
$like | string | SQL LIKE pattern (% wildcard) |
$ilike | string | Case-insensitive LIKE |
$startsWith | string | Starts with string |
$endsWith | string | Ends with string |
$contains | string | Contains substring |
$notContains | string | Does not contain substring |
$icontains | string | Case-insensitive contains |
$regex | string | Regular expression match |
$exists | all | Field exists (not undefined) |
$null | all | Field is null |
$notNull | all | Field is not null |
await datrix.findMany("user", {
where: {
age: { $gte: 18, $lte: 65 },
email: { $like: "%@example.com" },
role: { $in: ["admin", "editor"] },
deletedAt: { $null: true },
},
});
Logical operators
await datrix.findMany("post", {
where: {
$and: [
{ published: true },
{ createdAt: { $gte: new Date("2024-01-01") } },
],
},
});
await datrix.findMany("user", {
where: {
$or: [{ role: "admin" }, { role: "editor" }],
},
});
await datrix.findMany("user", {
where: {
$not: { role: "banned" },
},
});
Nested relation WHERE
Filter by fields on related records without a separate query:
// Posts where the author is verified
await datrix.findMany("post", {
where: {
author: { verified: true },
},
});
// Deep nesting
await datrix.findMany("post", {
where: {
author: {
company: {
country: { name: { $eq: "Turkey" } },
},
},
},
});
The query builder validates that all field names exist in the schema before the query reaches the database. Unknown fields throw an error at query time, not at runtime.
For writing relation fields in create and update operations, see Relations.
select
Choose which fields to return. By default all non-hidden, non-relation fields are returned.
// Array of fields
await datrix.findMany("user", {
select: ["id", "name", "email"],
});
// Single field
await datrix.findOne("user", { id: 1 }, { select: "name" });
// All fields (default behavior)
await datrix.findMany("user", { select: "*" });
Relation fields cannot appear in
select— usepopulateinstead.
populate
Load related records alongside the main record.
Populate all relations
await datrix.findMany("post", { populate: "*" });
await datrix.findMany("post", { populate: true });
Populate specific relations
// Array
await datrix.findMany("post", {
populate: ["author", "tags"],
});
// Object — relation name as key, `true` to include
await datrix.findMany("post", {
populate: { author: true, tags: true },
});
Populate with options
Each relation can have its own select, where, orderBy, limit, offset, and nested populate:
await datrix.findMany("post", {
populate: {
author: {
select: ["id", "name", "avatar"],
},
comments: {
where: { isApproved: true },
orderBy: [{ field: "createdAt", direction: "desc" }],
limit: 5,
offset: 0,
populate: {
author: { select: ["id", "name"] },
},
},
tags: {
limit: 10,
},
},
});
whereandorderByon a populated relation only make sense for hasMany and manyToMany — abelongsToorhasOnerelation always returns a single record.
orderBy
Three formats are supported:
// Full format — explicit, supports nulls
await datrix.findMany("user", {
orderBy: [
{ field: "createdAt", direction: "desc", nulls: "last" },
{ field: "name", direction: "asc" },
],
});
// Object shortcut — single field
await datrix.findMany("user", {
orderBy: { createdAt: "desc" },
});
// String array — multiple fields, - prefix = desc
await datrix.findMany("user", {
orderBy: ["-createdAt", "name"], // createdAt DESC, name ASC
});
limit and offset
// Page 2 with 20 items per page
await datrix.findMany("user", {
limit: 20,
offset: 20,
});
Type-safe queries
The recommended way to get model types is via the CLI — it reads your initialized Datrix instance and generates types for all registered schemas:
$datrix generate types
Output is written to types/generated.ts by default. The generated file looks like this:
// This file is auto-generated by datrix generate types
// Do not edit manually - regenerate with: datrix generate types
import type {
DatrixEntry,
RelationBelongsTo,
RelationHasOne,
RelationHasMany,
RelationManyToMany,
} from '@datrix/core'
// ─────────────────────────────────────────
// User (table: users)
// relations: posts → hasMany(post)
// ─────────────────────────────────────────
export interface UserBase extends DatrixEntry {
name: string
email: string
role?: 'admin' | 'editor' | 'user'
}
export interface UserRelation {
posts?: RelationHasMany<Post>
}
export type User = UserBase & UserRelation
export type CreateUserInput = Omit<UserBase, keyof DatrixEntry> & UserRelationUpdate
export type UpdateUserInput = Partial<Omit<UserBase, keyof DatrixEntry>> & UserRelationUpdate
With generated types you get full autocomplete and compile-time checks:
import type { User } from './types/generated'
const users = await datrix.findMany<User>('user', {
where: {
role: { $in: ['admin', 'editor'] }, // type-checked
posts: { title: { $like: '%datrix%' } } // nested relation WHERE
},
select: ['id', 'name'], // autocomplete on User fields
populate: {
posts: { select: ['id', 'title'] } // type-safe populate
}
})
See CLI: generate types for output path and options.