Core concepts

Resources and operations

Every operation family and variant Aventara offers on a Resource, what each selects by and returns, which are not offered, with an example of each.

Read as Markdown

A Resource is a thing your API serves: one per Prisma model, keyed by the model name as written (User, Post, Category). A Resource has fields (scalars with a type and the filters, ordering and writes each supports; relations, one or many, pointing at another Resource), identifiers (the unique selectors, such as id or email, that unique operations accept) and operations.

Operations come from a fixed set of five families, each with variants. The meaning of an operation is set by the protocol; configuration can switch an operation off for a Resource or a layer, never redefine it. Only what the contract advertises exists.

The examples use the generated client on this schema: a User with email (unique), name?, role, createdAt and many posts; a Post with title, views, published, meta (JSON) and an author; and a Category.

Families and variants

Familyfirstuniquemanycountone
findyesyesyesyes
createyesyesyes
updateyesyesyesyes
deleteyesyesnot offeredyes
upsertyes
VariantSelects byReturns
firsta general where, with an optional orderBy that decides whichthe record, or null when nothing matches
uniquean identifierthe record; a miss is an error (A2003)
manya general wherea list (possibly empty)
counta general wherea number
oneone data objectthe created record

Reads and writes both take an optional projection: select (exactly these fields) or include (default fields plus these relations), never both. count variants take none. See Results and typing for what the types do with it, and Querying for the full argument tables.

find

Code
// first: a miss is null, not an error
const maybe = await avClient.User.find.first({ where: { email: "nobody@example.com" } });   // null

// unique: by identifier; a miss throws AvNotFoundError (A2003)
const ada = await avClient.User.find.unique({
  where: { email: "ada@example.com" },
  select: ["id", "email", "role"],
});
// { id: 1, email: "ada@example.com", role: "USER" }

// many: a list
const admins = await avClient.User.find.many({
  where: { role: "ADMIN" },
  orderBy: { id: "desc" },
  limit: 20,
  select: ["id", "email"],
});

// count: a number
const total = await avClient.User.find.count({ where: { role: "USER" } });   // 2

create

Code
// one
const user = await avClient.User.create.one({
  data: { email: "ada@example.com", name: "Ada", passwordHash: "..." },
  select: ["id", "email", "createdAt"],
});
// { id: 1, email: "ada@example.com", createdAt: Date }

// many: returns the created records
const created = await avClient.User.create.many({
  data: [
    { email: "b@example.com", passwordHash: "..." },
    { email: "c@example.com", passwordHash: "...", role: "ADMIN" },
  ],
  select: ["id", "email"],
});
// [{ id: 2, email: "b@example.com" }, { id: 3, email: "c@example.com" }]

// count: creates and returns only how many
const n = await avClient.Category.create.count({ data: [{ name: "news" }, { name: "tips" }] });   // 2

A relation can be written inline with a nested directive:

Code
await avClient.Post.create.one({
  data: { title: "Hello", author: { $connect: { id: 1 } }, category: { $connect: { name: "news" } } },
});
await avClient.User.create.one({
  data: { email: "n@example.com", passwordHash: "...", posts: { $create: { title: "first post" } } },
  select: ["id", { posts: { select: ["title"] } }],
});
// { id: 6, posts: [{ title: "first post" }] }

update

Code
// first: a miss is null. `orderBy` decides which match is "first"
await avClient.User.update.first({
  where: { role: "USER" }, orderBy: { id: "asc" },
  data: { name: "First user" }, select: ["id", "name"],
});
// { id: 1, name: "First user" }

// unique: by identifier; a miss is AvNotFoundError
await avClient.User.update.unique({ where: { id: 1 }, data: { name: "Ada L." }, select: ["id", "name"] });

// many: updates every match, returns the updated records
await avClient.Post.update.many({
  where: { published: true },
  data: { views: { $increment: 1 } },
  select: ["id", "views"],
});
// [{ id: 1, views: 1 }]

// count: returns how many were updated
const updated = await avClient.Post.update.count({ where: { published: true }, data: { views: { $multiply: 2 } } });   // 1

A plain value in data assigns. An object with a $ key acts on the current value: $increment, $decrement, $multiply, $divide on numbers, $push on lists where the database allows it.

delete

Code
// first: deletes the first match, returns it, or null
await avClient.Category.delete.first({ where: { name: "tips" }, select: ["id", "name"] });   // { id: 2, name: "tips" }

// unique: by identifier; a miss is AvNotFoundError
await avClient.Category.delete.unique({ where: { name: "news" } });

// count: deletes every match, returns how many
const removed = await avClient.Category.delete.count({ where: { name: "zzz" } });            // 0

upsert

upsert.unique finds a record by identifier, updates it if it exists, and creates it otherwise:

Code
await avClient.User.upsert.unique({
  where: { email: "d@example.com" },
  create: { email: "d@example.com", passwordHash: "..." },
  update: { name: "D" },
  select: ["id", "email", "name"],
});
// { id: 4, email: "d@example.com", name: null }

What is not offered, and why

Not offeredWhy, and what to use
delete.manyThe Prisma 7 adapter cannot return the deleted rows, and Aventara does not emulate it. Use delete.count to delete a filtered set.
A to-many relation or $count at the top level of the projection of update.many or create.manyAt the top level these variants project scalars and to-one relations only; a to-many nested inside an included to-one relation is allowed (Bulk variants differ). The request fails with A2004 (V1008: Relation "posts" is not available for select.; include is refused the same way, while a to-one relation such as author works in either). Read to-many relations with a separate find.

Two behaviors to keep in mind:

  • update.first and delete.first run as a read followed by a write inside one transaction. The record written is one the read selected, or nothing is written and you get A2014. If correctness depends on the record still being the first match at the instant of the write, select it yourself and use update.unique or delete.unique. Not offered with @prisma/adapter-d1, which has no transactions.
  • Whether a variant is available is decided in two steps: the adapter advertises what it can perform faithfully, then exposure can switch some off per layer. The result is in the contract:
Code
"operations": {
  "create": { "count": {"write":"bulkWrite"}, "many": {"projection":"bulkWrite","write":"bulkWrite"}, "one": true },
  "delete": { "count": true, "first": true, "unique": true },
  "find":   { "count": true, "first": true, "many": true, "unique": true },
  "update": { "count": {"write":"bulkWrite"}, "first": true, "many": {"projection":"bulkWrite","write":"bulkWrite"}, "unique": true },
  "upsert": { "unique": true }
}

What a create requires

Each Resource's contract entry also says what a create cannot do without, as the adapter read it off your schema:

Code
"createRequirements": {
  "required": ["title"],
  "oneOf": [["author", "authorId"]],
  "parentSupplies": { "User": { "posts": ["author", "authorId"] } }
}
  • required: fields every create must write (columns that are not optional and have no default).
  • oneOf: each required relation, followed by the field or fields holding its reference. A create writes the relation or all of those fields, never both.
  • parentSupplies: what a create nested through a parent's relation gets from the parent. A Post created in User.posts needs neither author nor authorId.

A layer's contract lists only what that layer lets a caller write: when exposure hides authorId, the group becomes "required": ["author", "title"]; when it hides author, ["authorId", "title"]. A field with a compute.create stage is left out. A layer that can still create the Resource cannot leave a requirement unwritable on create (a required field hidden or with its create exempted, or a relation hidden with any field that holds its reference) unless a compute.create fills it: startup refuses it with COMPILER_HIDDEN_REQUIRED_FIELD. A create.many or create.count record writes no relation, so there a oneOf group means its fields: each record needs authorId, and a layer that keeps those variants cannot hide authorId either, even with author exposed. A Resource that requires nothing has no createRequirements. Like the rest of the contract, it is part of the contract hash. The generated client and the server-side calls are typed by it (Relations and nested writes).

How relations are paired

Each relation field also says how it is paired, as the adapter read it off your schema:

Code
"author": { "kind": "relation", "target": "User", "cardinality": "one", "nullable": false, "inverse": "posts", "referenceFields": ["authorId"], "capabilities": { … } }
"posts":  { "kind": "relation", "target": "Post", "cardinality": "many", "nullable": false, "inverse": "author", "capabilities": { … } }
  • inverse: the target's relation field at the other end (User.posts and Post.author; a self-relation's two ends name each other). A layer's contract names it only while that layer shows the other end.
  • referenceFields: the scalar fields of this Resource that hold the relation's reference, in the order the database pairs them with the target's key (["authorId"]; a composite reference lists every field). Present on optional relations as on required ones; absent on the end that holds no reference (User.posts). A layer's contract keeps the ones it shows and leaves the member out when it shows none.

Inside a record written through a relation, that relation's other end and its reference fields are what the enclosing write supplies: a post created in User.posts cannot write author or authorId, and one created in Category.posts cannot write category or categoryId (A2004, V1012; the generated client's types leave them out). Both members are part of the contract hash.

Success codes

Family and variantCodeHTTP
find.*A1000 (A1001 when a first finds nothing)200
create.oneA1002201
create.many, create.countA1003201
update.first, update.uniqueA1004200
update.many, update.countA1005200
delete.first, delete.uniqueA1006200
delete.countA1007200
upsert.uniqueA1008200

See the HTTP protocol for the failure codes.

See also