Skip to content

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.

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 relations
const 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). Returns null when 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.

Access LevelOutput
Public{ id, name, organization: { id, name } }
Authenticated{ id, name, email, organization: { id, name }, posts: [{ id, title, draft }] }
AdminAll 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.

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);
  • hasOne — Returns null when the related object is missing from the data
  • hasMany — 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 → []

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 Comment
return 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 from include
  • Resource API — Tagged views, projection methods, and custom access levels
  • Procedures — Builder API including .output()
  • Guards — Authorization and access levels