Build

Querying & CRUD

The generated CRUD methods, where operators, pagination, returning, soft deletes, and transactions.

7b. Querying & the CRUD service

Every model registered in a module's service gets a full CRUD accessor — service.<model>.find(...), service.<model>.create(...), and so on (see Modules & services for how accessors are generated). This chapter is the reference for those methods and their options.

The methods

MethodReturnsNotes
find(options)T | nullfirst match
findMany(options)T[]list with paging/sorting
findById(id, options?)T | nullshorthand for a primary-key lookup
findOne(where, options?)T | nullshorthand for find({ where })
create({ data, returning? })Tvalidates against the generated zod schema
createMany({ data: [...], returning? })T[]bulk insert
upsert({ data, onConflict, updateColumns?, set?, returning? })Tinsert-or-update on a conflict target
upsertMany({ data: [...], onConflict, ... })T[]bulk upsert
update({ where, data, returning? })T[]updates all matches
updateOne({ where, data, returning? })T | nullupdates the first match
delete({ where, returning?, cascade? })numberhard delete; returns row count
softDelete({ where, returning?, cascade? })T[]sets deleted_at (needs .softDelete() on the model)
restore({ where, returning? })T[]clears deleted_at
count({ where?, withDeleted? })number
exists({ where, withDeleted? })boolean

Find options

interface FindOptions {
  select?: string[];       // project specific columns
  where?: WhereClause;     // filters (see below)
  orderBy?: Array<{
    column: string;
    direction?: "ASC" | "DESC";
    nulls?: "NULLS FIRST" | "NULLS LAST";
  }>;
  skip?: number;           // offset
  take?: number;           // limit — capped at 1000 (MAX_PAGE_SIZE)
  include?: string[];      // eager-load relations declared on the model
  withDeleted?: boolean;   // include soft-deleted rows
}
const admins = await users.user.findMany({
  where: { role: "admin", createdAt: { gte: since } },
  orderBy: [{ column: "createdAt", direction: "DESC" }],
  select: ["id", "email"],
  take: 50,
});

Where clauses

A where value is either a plain value (equality) or an operator object:

OperatorMeaning
eq / neqequals / not equals
gt / gte / lt / ltecomparisons
like / ilikeSQL LIKE (case-sensitive / insensitive)
in / notInvalue in list
isNull: true / isNotNull: truenull checks
between: [a, b]inclusive range
await posts.post.findMany({
  where: {
    title: { ilike: "%damat%" },
    status: { in: ["published", "featured"] },
    deletedBy: { isNull: true },
  },
});

Returning and projections

Writes accept returning: [...] to control which columns come back — you saw this in Workflows, where a step returns only what the next step needs:

const user = await users.user.create({
  data: { email },
  returning: ["id", "email"],
});

Soft deletes

If a model declares .softDelete(), every read automatically filters deleted_at IS NULL. Pass withDeleted: true to see through it, and use restore() to bring rows back. cascade: true on delete/softDelete follows the model's relations.

Transactions

Any sequence of service calls can run atomically:

await this.transaction(async () => {
  const user = await this.user.create({ data: { email } });
  await this.account.create({ data: { userId: user.id, provider } });
  return user;
}, { isolationLevel: "SERIALIZABLE" });   // options are optional

TransactionOptions supports isolationLevel ("READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SERIALIZABLE"), readOnly, and deferrable. The callback's throw rolls the whole transaction back.

Validation

create, update, and upsert validate data against the zod schemas generated from your models (damat codegen) before touching the database — invalid payloads throw a validation error instead of producing a DB error.