# Query API
URL: /docs/runtime/query-api
LLM index: /llms.txt
Description: Typed query and mutation methods, nested select, filters, sorting, paging, transactions, and auth-style workflows.

# Query API

The runtime API is model-based and fully typed from the schema.

## Model clients

Given:

```ts
const orm = createOrm({
  schema: authSchema,
  driver: createMemoryDriver(),
});
```

you get model clients such as:

```ts
orm.user;
orm.profile;
orm.session;
orm.account;
```

Each model client supports:

- `findUnique`
- `findOne`
- `findMany`
- `findFirst`
- `count`
- `create`
- `createMany`
- `update`
- `updateMany`
- `upsert`
- `delete`
- `deleteMany`

## `findUnique`

```ts
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.

```ts
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 `id` field
- a single `.unique()` field
- or a declared model-level compound unique constraint

## `findOne`

```ts
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`

```ts
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`

```ts
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`

```ts
const sessionCount = await orm.session.count({
  where: {
    userId: "user_1",
  },
});
```

Returns the number of matching rows without loading the full records.

## `create`

```ts
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 id
- `defaultNow()` uses `new Date()`
- `.default(...)` applies the literal default value

## `createMany`

```ts
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`

```ts
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`

```ts
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`

```ts
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:

```ts
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:

```ts
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:

```ts
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 `where` must belong to one declared unique key
- `create` must agree with the `where` values for those fields
- `update` cannot change the conflict fields themselves

## Enum, bigint, and decimal values

Enums behave like constrained strings in the unified API:

```ts
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:

```ts
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:

```ts
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`

```ts
const deleted = await orm.session.delete({
  where: { token: "session-token" },
});
```

Returns the number of deleted rows.

## `deleteMany`

```ts
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.

```ts
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:

- `true` for default scalar selection on the target
- a nested object with `select`, `where`, `orderBy`, `take`, and `skip`

### Example

```ts
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

```ts
where: {
  email: "ada@farminglabs.dev";
}
```

### Operator objects

```ts
where: {
  email: { eq: "ada@farminglabs.dev" },
  token: { contains: "session" },
}
```

### Supported operators

For strings:

- `eq`
- `contains`
- `in`
- `not`

For comparable values such as strings and dates:

- `eq`
- `gt`
- `gte`
- `lt`
- `lte`
- `in`
- `not`

### Logical operators

```ts
where: {
  AND: [{ emailVerified: true }],
  OR: [
    { name: { contains: "Ada" } },
    { email: { contains: "@farminglabs.dev" } },
  ],
  NOT: { email: { contains: "@spam.test" } },
}
```

## Sorting and paging

```ts
const users = await orm.user.findMany({
  orderBy: {
    createdAt: "desc",
  },
  skip: 20,
  take: 20,
});
```

## Transactions

```ts
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

```ts
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

```ts
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 `select` result narrowing
- generic enough to support many driver implementations
- simple enough to explain in library docs

## Related pages

- [Runtime overview](/docs/runtime)
- [Memory driver](/docs/runtime/memory-driver)
- [Schema relations](/docs/schema/relations)