Query API
The runtime API is model-based and fully typed from the schema.
Model clients
Given:
const orm = createOrm({
schema: authSchema,
driver: createMemoryDriver(),
});you get model clients such as:
orm.user;
orm.profile;
orm.session;
orm.account;Each model client supports:
findUniquefindOnefindManyfindFirstcountcreatecreateManyupdateupdateManyupsertdeletedeleteMany
findUnique
const user = await orm.user.findUnique({
where: { email: "ada@farminglabs.dev" },
select: {
id: true,
email: true,
},
});Use findUnique when the query should resolve to at most one record, such as
email, id, token, or another unique lookup.
That includes declared compound unique keys.
const account = await orm.account.findUnique({
where: {
provider: "github",
accountId: "gh_ada",
},
select: {
userId: true,
},
});For findUnique(...), the where object should match exactly one unique key:
- an
idfield - a single
.unique()field - or a declared model-level compound unique constraint
findOne
const user = await orm.user.findOne({
where: {
emailVerified: true,
},
orderBy: {
createdAt: "asc",
},
select: {
id: true,
email: true,
},
});findOne is the simple ergonomic name for "give me the first matching record".
It is useful for library code that does not want to expose Prisma-flavored
method names to its own consumers.
findFirst
const user = await orm.user.findFirst({
where: { email: "ada@farminglabs.dev" },
select: {
id: true,
email: true,
},
});Returns one typed record or null.
findFirst is still available for teams that prefer that naming. In the current
runtime it behaves the same as findOne.
findMany
const sessions = await orm.session.findMany({
where: {
token: { contains: "session" },
},
orderBy: {
expiresAt: "desc",
},
take: 10,
skip: 0,
select: {
id: true,
token: true,
expiresAt: true,
},
});Returns an array of typed records.
count
const sessionCount = await orm.session.count({
where: {
userId: "user_1",
},
});Returns the number of matching rows without loading the full records.
create
const user = await orm.user.create({
data: {
name: "Grace Hopper",
email: "grace@farminglabs.dev",
},
select: {
id: true,
email: true,
createdAt: true,
},
});Generated/default fields are filled by the driver where supported. In the memory driver:
id()generates an iddefaultNow()usesnew Date().default(...)applies the literal default value
createMany
const createdSessions = await orm.session.createMany({
data: [
{
userId: "user_1",
token: "token-1",
expiresAt: new Date("2026-01-01T00:00:00.000Z"),
},
{
userId: "user_1",
token: "token-2",
expiresAt: new Date("2026-02-01T00:00:00.000Z"),
},
],
select: {
token: true,
},
});Returns the created records, narrowed by select if provided.
update
const updated = await orm.user.update({
where: { email: "grace@farminglabs.dev" },
data: {
emailVerified: true,
},
select: {
id: true,
emailVerified: true,
},
});Returns the updated record or null.
updateMany
const updatedCount = await orm.session.updateMany({
where: {
userId: "user_1",
},
data: {
expiresAt: new Date("2027-01-01T00:00:00.000Z"),
},
});Returns the number of updated rows.
upsert
const session = await orm.session.upsert({
where: {
token: "session-token",
},
create: {
userId: "user_1",
token: "session-token",
expiresAt: new Date("2027-01-01T00:00:00.000Z"),
},
update: {
expiresAt: new Date("2027-01-01T00:00:00.000Z"),
},
select: {
token: true,
expiresAt: true,
},
});This is especially useful for auth-style operations such as rotating sessions, refreshing tokens, or syncing provider accounts.
It also supports compound unique keys:
const account = await orm.account.upsert({
where: {
provider: "github",
accountId: "gh_ada",
},
create: {
userId: "user_1",
provider: "github",
accountId: "gh_ada",
},
update: {
userId: "user_2",
},
select: {
provider: true,
accountId: true,
userId: true,
},
});Integer and JSON filters
Integer fields participate in the normal comparison operators:
const activeUsers = await orm.user.findMany({
where: {
loginCount: {
gte: 2,
},
},
select: {
email: true,
loginCount: true,
},
});JSON fields can be written and read like any other scalar, and equality filters can use the raw JSON value directly:
const matchingAccounts = await orm.account.findMany({
where: {
metadata: {
plan: "oss",
scopes: ["repo:read", "repo:write"],
flags: {
sync: true,
},
},
},
select: {
provider: true,
metadata: true,
},
});This is especially useful for auth and plugin data where you want a stable typed contract in library code, but the underlying app may be using Prisma, Drizzle, Kysely, direct SQL, MongoDB, or Mongoose.
For compound upserts:
- every field in
wheremust belong to one declared unique key createmust agree with thewherevalues for those fieldsupdatecannot change the conflict fields themselves
Enum, bigint, and decimal values
Enums behave like constrained strings in the unified API:
const premiumUsers = await orm.user.findMany({
where: {
tier: "pro",
},
select: {
email: true,
tier: true,
},
});Bigints use real JavaScript bigint values and support the normal comparison
operators:
const quotaUsers = await orm.user.findMany({
where: {
quota: {
gte: 1024n,
},
},
select: {
email: true,
quota: true,
},
});Decimals stay as strings so precision is preserved across runtimes:
const updatedAccount = await orm.account.update({
where: {
provider: "github",
accountId: "gh_ada",
},
data: {
balance: "19.95",
},
select: {
balance: true,
},
});Decimal outputs are normalized strings, so values like "12.50" may reload as
"12.5" after a round-trip.
delete
const deleted = await orm.session.delete({
where: { token: "session-token" },
});Returns the number of deleted rows.
deleteMany
const deletedCount = await orm.session.deleteMany({
where: {
userId: "user_1",
},
});Returns the number of deleted rows across the whole match set.
select and type inference
The select object is one of the most important parts of the runtime API.
const user = await orm.user.findFirst({
where: { email: "ada@farminglabs.dev" },
select: {
id: true,
name: true,
profile: {
select: {
bio: true,
},
},
sessions: {
select: {
token: true,
},
},
},
});That means the result type is narrowed to exactly the selected fields and relations.
Relation selection
Relations can be selected with:
truefor default scalar selection on the target- a nested object with
select,where,orderBy,take, andskip
Example
const user = await orm.user.findFirst({
where: { email: "ada@farminglabs.dev" },
select: {
id: true,
sessions: {
where: {
token: { contains: "session" },
},
orderBy: {
expiresAt: "desc",
},
take: 5,
select: {
token: true,
expiresAt: true,
},
},
},
});where filters
Shorthand equality
where: {
email: "ada@farminglabs.dev";
}Operator objects
where: {
email: { eq: "ada@farminglabs.dev" },
token: { contains: "session" },
}Supported operators
For strings:
eqcontainsinnot
For comparable values such as strings and dates:
eqgtgteltlteinnot
Logical operators
where: {
AND: [{ emailVerified: true }],
OR: [
{ name: { contains: "Ada" } },
{ email: { contains: "@farminglabs.dev" } },
],
NOT: { email: { contains: "@spam.test" } },
}Sorting and paging
const users = await orm.user.findMany({
orderBy: {
createdAt: "desc",
},
skip: 20,
take: 20,
});Transactions
await orm.transaction(async (tx) => {
const user = await tx.user.create({
data: {
name: "Linus",
email: "linus@farminglabs.dev",
},
select: {
id: true,
},
});
await tx.profile.create({
data: {
userId: user.id,
bio: "Kernel and schema pragmatist.",
},
});
});Batch workflows
const [user, sessionCount, accountCount] = await orm.batch([
(tx) =>
tx.user.findUnique({
where: { id: "user_1" },
select: {
id: true,
email: true,
},
}),
(tx) =>
tx.session.count({
where: { userId: "user_1" },
}),
(tx) =>
tx.account.count({
where: { userId: "user_1" },
}),
] as const);batch is helpful when a library wants one grouped storage call but still wants
to keep each internal query simple and typed.
Auth-style storage example
const normalizeEmail = (email: string) => email.trim().toLowerCase();
export function createAuthStore(orm: typeof authOrm) {
return {
findUserByEmail(email: string) {
return orm.user.findUnique({
where: {
email: normalizeEmail(email),
},
select: {
id: true,
name: true,
email: true,
profile: {
select: {
bio: true,
},
},
accounts: {
select: {
provider: true,
accountId: true,
},
},
sessions: {
select: {
token: true,
expiresAt: true,
},
orderBy: {
expiresAt: "desc",
},
},
},
});
},
createOAuthUser(input: { name: string; email: string; provider: string; accountId: string }) {
return orm.transaction(async (tx) => {
const user = await tx.user.create({
data: {
name: input.name,
email: normalizeEmail(input.email),
},
select: {
id: true,
email: true,
},
});
const account = await tx.account.create({
data: {
userId: user.id,
provider: input.provider,
accountId: input.accountId,
},
select: {
provider: true,
accountId: true,
},
});
return { user, account };
});
},
rotateSession(input: { userId: string; token: string; expiresAt: Date }) {
return orm.session.upsert({
where: {
token: input.token,
},
create: {
userId: input.userId,
token: input.token,
expiresAt: input.expiresAt,
},
update: {
expiresAt: input.expiresAt,
},
select: {
token: true,
expiresAt: true,
},
});
},
invalidateUserSessions(userId: string) {
return orm.session.deleteMany({
where: {
userId,
},
});
},
};
}If the storage layer normalizes emails, normalize them on write as well as on
lookup. A lookup-only toLowerCase() can miss legacy or mixed-case rows.
This is the main point of the runtime contract: an auth package can write its storage layer once and then plug that same logic into whichever runtime driver the app eventually uses.
Design goals of the query API
- model-first instead of stringly-typed command objects
- typed
selectresult narrowing - generic enough to support many driver implementations
- simple enough to explain in library docs
Related pages
How is this guide?