Nested Relations
When using Prisma’s include to fetch related data, the Resource API can recursively project nested objects and arrays. Use .hasOne() for single-object relations and .hasMany() for array relations.
Defining Nested Schemas
Section titled “Defining Nested Schemas”Define leaf schemas first, then compose parent schemas:
import { resourceSchema, resource } from '@veloxts/router';import { z } from '@veloxts/velox';
// Leaf schemas (define first)const OrgSchema = resourceSchema() .public('id', z.string()) .public('name', z.string()) .admin('taxId', z.string()) .build();
const PostSchema = resourceSchema() .public('id', z.string()) .public('title', z.string()) .authenticated('draft', z.boolean()) .build();
// Parent schema with relationsconst UserSchema = resourceSchema() .public('id', z.string()) .public('name', z.string()) .authenticated('email', z.string()) .hasOne('organization', OrgSchema, 'public') // Always included if visible .hasMany('posts', PostSchema, 'authenticated') // Only for logged-in users .admin('internalNotes', z.string()) .build();Method signatures:
.hasOne(name, nestedSchema, visibility)— Defines a single-object relation (nullable). Returnsnullwhen the related data is missing..hasMany(name, nestedSchema, visibility)— Defines an array relation. Returns[]when no related data exists.
The visibility parameter controls whether the relation is included in the output. The parent’s projection level controls what fields of the nested schema are shown.
When using custom access levels (defined via defineAccessLevels()), the visibility parameter accepts a group name from your config or an explicit array of level names:
// Group name.hasOne('author', AuthorSchema, 'internal') // group defined in access config
// Explicit array of levels.hasMany('comments', CommentSchema, ['authenticated', 'moderator', 'admin'])The default three-level builder accepts 'public', 'authenticated', or 'admin' as the visibility parameter — this is unchanged.
Projection Behavior
Section titled “Projection Behavior”| Access Level | Output |
|---|---|
| Public | { id, name, organization: { id, name } } |
| Authenticated | { id, name, email, organization: { id, name }, posts: [{ id, title, draft }] } |
| Admin | All fields, including internalNotes, organization.taxId |
The posts relation has 'authenticated' visibility, so it’s excluded at the public level. The organization relation has 'public' visibility, so it’s always included — but its taxId field (admin-only) is only visible at the admin level.
Integration with Prisma include
Section titled “Integration with Prisma include”Use Prisma’s include to fetch related data, then let the Resource API project it:
export const userProcedures = procedures('users', { getProfile: procedure() .guard(authenticated) .output(UserSchema.authenticated) .input(z.object({ id: z.string().uuid() })) .query(async ({ input, ctx }) => { return ctx.db.user.findUniqueOrThrow({ where: { id: input.id }, include: { organization: true, posts: true }, }); // Auto-projected to authenticated level }),});For manual projection:
const user = await ctx.db.user.findUnique({ where: { id }, include: { organization: true, posts: true },});return resource(user, UserSchema.authenticated);Null and Empty Handling
Section titled “Null and Empty Handling”hasOne— Returnsnullwhen the related object is missing from the datahasMany— Returns[](empty array) when no related items exist
const userWithoutOrg = { id: '1', name: 'Jane', email: 'jane@example.com' };const result = resource(userWithoutOrg, UserSchema.authenticated);// result.organization → null// result.posts → []CLI Code Generation
Section titled “CLI Code Generation”When you run velox sync, velox make resource, or velox make namespace for a model that has Prisma relations, the CLI automatically detects them and generates include: clauses in the Prisma queries:
// Generated automatically when Post has relations to User and Commentreturn ctx.db.post.findUnique({ where: { id: input.id }, include: { author: true, comments: true },});The CLI parses your schema.prisma to detect:
- hasOne relations — field type matches another model name (e.g.,
author User) - hasMany relations — array field type (e.g.,
comments Comment[]) - Back-references are skipped — fields with
@relation(fields: [...])are the inverse side and excluded frominclude
Related Content
Section titled “Related Content”- Resource API — Tagged views, projection methods, and custom access levels
- Procedures — Builder API including
.output() - Guards — Authorization and access levels