Query

All read operations go through the same query system. This page covers every option available in findMany, findOne, findById, and count.

Note: datrix.findMany and datrix.crud.findMany are identical. datrix.raw.findMany behaves the same but skips plugin hooks (no onBeforeQuery / onAfterQuery dispatch).


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

OperatorTypesDescription
$eqallEqual
$neallNot equal
$gtnumber, dateGreater than
$gtenumber, dateGreater than or equal
$ltnumber, dateLess than
$ltenumber, dateLess than or equal
$inallValue is in array
$ninallValue is not in array
$likestringSQL LIKE pattern (% wildcard)
$ilikestringCase-insensitive LIKE
$startsWithstringStarts with string
$endsWithstringEnds with string
$containsstringContains substring
$notContainsstringDoes not contain substring
$icontainsstringCase-insensitive contains
$regexstringRegular expression match
$existsallField exists (not undefined)
$nullallField is null
$notNullallField 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 — use populate instead.


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,
		},
	},
});

where and orderBy on a populated relation only make sense for hasMany and manyToMany — a belongsTo or hasOne relation 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.