# Results and typing [#results-and-typing] The generated client types every result from the arguments you pass, not from the model. A `select` of two fields gives a record with exactly two fields; add a relation and the relation's shape follows its own `select`. The types are derived from the same contract the server enforces, so what compiles is what the server accepts. ## select: only the selected fields exist [#select-only-the-selected-fields-exist] ```ts const users = await avClient.User.find.many({ select: ["id", "email"] }); // readonly { readonly id: number; readonly email: string }[] users[0].email; // string users[0].name; // error TS2339: Property 'name' does not exist on type ... ``` With neither `select` nor `include`, a record has every readable scalar field and no relations. Scalars map to their natural types: `Int` to `number`, a nullable `String?` to `string | null`, an enum to the union of its values (`"USER" | "ADMIN"`), `DateTime` to `Date`, `Boolean` to `boolean`. ```ts const ada = await avClient.User.find.unique({ where: { id: 1 } }); // { readonly id: number; readonly email: string; readonly name: string | null; // readonly role: "USER" | "ADMIN"; readonly createdAt: Date } ``` Your editor shows these plain types too. Hover a result and each field reads as its value type (`name: string | null`, `role: "USER" | "ADMIN"`, `published: boolean`, `publishedAt: Date | null`), and an included relation as its fields. A declaration file your project emits (`"declaration": true`) writes the same short types, so a service whose methods return results without annotations stays small. This holds for `framework.application`, `framework.client` and `framework.serverClient` results and for generated clients. A field the client layer hides or makes write-only is not in these types, and is not a valid `select` entry; an operator a field does not offer is not a valid filter either: ```ts await avClient.User.find.many({ select: ["passwordHash"] }); // compile error ``` ## include and nested select [#include-and-nested-select] `include` adds relations to the default scalars. `select` can nest a relation with its own `select`, `where`, `orderBy` and paging: ```ts const withPosts = await avClient.User.find.unique({ where: { id: 1 }, include: ["posts"] }); // default User fields plus: readonly posts: readonly { id, title, body, views, published, meta, authorId }[] const posts = await avClient.Post.find.many({ select: ["id", "title", { author: { select: ["id", "name"] } }], limit: 10, }); // readonly { readonly id: number; readonly title: string; // readonly author: { readonly id: number; readonly name: string | null } }[] ``` A to-one relation is an object (or `null` when the relation is optional); a to-many relation is a readonly array. A relation named as a bare string uses the target's default projection; a customized one uses object form. ## $count [#count] `$count` is a reducer on a to-many relation, not a field. It changes the shape of that relation's result: | Nested `select` | Result type | | --------------------------- | --------------------------------------------------------------------- | | `["id", "title"]` | `readonly { id; title }[]` | | `["$count"]` | `{ readonly count: number }` | | `["id", "title", "$count"]` | `{ readonly data: readonly { id; title }[]; readonly count: number }` | ```ts const users = await avClient.User.find.many({ select: ["id", { posts: { select: ["id", "title", "$count"] } }], }); // readonly { readonly id: number; // readonly posts: { readonly data: readonly { readonly id: number; readonly title: string }[]; // readonly count: number } }[] // at runtime: [{ id: 1, posts: { data: [{ id: 1, title: "Hello" }], count: 1 } }, { id: 2, posts: { data: [], count: 0 } }] const counts = await avClient.User.find.many({ select: ["id", { posts: { select: ["$count"] } }], limit: 1 }); // [{ id: 1, posts: { count: 1 } }] ``` When the nested relation has a `where` and paging, `count` is the number of matching rows before `limit` and `offset`, and `data` is the page. ## $defaults [#defaults] `"$defaults"` in a `select` stands for the level's default projection, so the type is the default fields plus whatever else the list names, all present: | `select` | Result type | | ------------------------ | -------------------------------------- | | `["$defaults"]` | the default fields | | `["$defaults", "email"]` | the default fields and `email: string` | | `["id", "email"]` | `{ id; email }` only | A field that a [policy](/framework/docs/policies#governed-fields-and-defaults) governs is not in the default fields. It is typed as present, never optional or nullable, wherever a `select` names it. A request that may not read it is refused (`A4002`) instead of returning a gap. ## Results are readonly [#results-are-readonly] Every result type is `readonly`, arrays included, so a returned record cannot be mutated in place. Copy it into a new object, or type a variable as `readonly User[]`: ```ts users[0].id = 5; // error TS2540: Cannot assign to 'id' because it is a read-only property. users.push(users[0]); // error TS2339: Property 'push' does not exist on type 'readonly ...[]'. const list: readonly User[] = await avClient.User.find.many({}); // `User` is a named type from the generated client ``` ## first returns null, unique throws, count is a number [#first-returns-null-unique-throws-count-is-a-number] | Call | Result | When nothing matches | | ------------------------------------------------------------ | ------------------- | ---------------------------------- | | `find.first`, `update.first`, `delete.first` | `Record \| null` | `null` | | `find.unique`, `update.unique`, `delete.unique` | `Record` | throws `AvNotFoundError` (`A2003`) | | `find.many` | `readonly Record[]` | `[]` | | `find.count`, `create.count`, `update.count`, `delete.count` | `number` | `0` | | `create.one`, `upsert.unique` | `Record` | not applicable | | `create.many`, `update.many` | `readonly Record[]` | `[]` | The type system follows: a `first` result must be checked before use, a `unique` result need not. ```ts const first = await avClient.User.find.first({ select: ["id"] }); first.id; // error TS18047: 'first' is possibly 'null'. first?.id; // number | undefined const n: number = await avClient.User.find.count(); ``` ## Failures are typed errors [#failures-are-typed-errors] The client resolves to the operation's `data` and throws on every failure. Branch on the error class or its `code`, never on the message: | Class | Typical codes | | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `AvValidationError` | `A2004`, `A2007`, `A2009` (with `cause.issues`, each with a `V` code and a `path`) | | `AvNotFoundError` | `A2003` | | `AvConflictError` | `A2008`, `A2013`, `A2014` | | `AvContractMismatchError` | `A2005` | | `AvAuthError` | `A4000`, `A4001`, `A4002` | | `AvInternalError` | `A3000`, `A3001`, `A3002`, `A3003`, `A3004` | | `AvProtocolError` | `A2000`, `A2001`, `A2002`, `A2006`, `A2010`, `A2011`, `A2012` (a request the server could not route or interpret) | | `AvTransportError` | no response, or a response that is not a framework envelope (not an `AvError`; it has no `code`) | ```ts import avClient, { AvConflictError } from "./api/AvClient"; try { await avClient.User.create.one({ data: { email: "ada@example.com", passwordHash: "..." } }); } catch (e) { if (e instanceof AvConflictError) { e.code; // "A2008": the email already exists } } ``` ## On the server, the same results and errors [#on-the-server-the-same-results-and-errors] Server-side calls through `framework.application.*`, `framework.client.*`, `framework.serverClient.*` and `framework.transaction` settle exactly like the client: they resolve the typed data and throw the same error classes, exported from `@aventara/core` (`import { AvConflictError } from "@aventara/core"`). See [Server-side usage](/framework/docs/server-side-usage#errors). The types of `framework.application` and `framework.client` follow your [exposure](/framework/docs/exposure) layer by layer, exactly as the generated client's do. A hidden field is not in the types. A field whose `select`, `filter`, `order`, `create` or `update` capability you took away is refused in that position and left out of the result types, and a `where` offers each field only the filter operators it allows (inside `not` too), so an operator the server would refuse with `V1006` is a type error. A removed operation is not on the object. `framework.client` reads the root's exemptions with the `client` overlay's over them; `framework.application` the root's with the `application` overlay's, so a field hidden at the root is hidden on `framework.application` too. `framework.appTx` and `framework.clientTx` follow the same rules. ```ts const n = await framework.application.User.find.count({}); // number ``` ## See also [#see-also] * [Resources and operations](/framework/docs/resources-and-operations) * [Querying](/framework/docs/querying) * [Frontend client](/framework/docs/client-errors) * [HTTP protocol](/framework/docs/http-protocol#codes) * Guides: [Relations and nested writes](/framework/docs/relations-and-nested-writes), [Enums and scalar types](/framework/docs/enums-and-scalars), [Counting](/framework/docs/counting)