zenstack-relation-naming-discovery
ZenStack Relation Naming Discovery
Problem
When working with ZenStack (enhanced Prisma), the actual relation field names on model instances don't always match what you expect from reading the schema. This causes TypeScript errors and runtime failures when accessing related data.
Common scenario: Schema defines verificationRecord VerificationRecord? but the
actual field name is verification, not verificationRecord.
Context / Trigger Conditions
Use this pattern when you encounter:
-
TypeScript errors like:
Property 'verificationRecord' does not exist on type 'User' Property 'documentsOwned' does not exist on type 'User' -
Runtime errors when accessing relations:
Cannot read property 'status' of undefined -
Implementing new queries that need to include related data and you're unsure of the exact field names
Solution
Step 1: Check the Schema Definition
Look at the schema in packages/database/zenstack/schema.zmodel (or .prisma file):
model User {
id String @id
verification VerificationRecord? // ← Note the field name here
documents Document[] // ← And here
}
model VerificationRecord {
id String @id
userId String @unique
user User @relation(fields: [userId], references: [id])
}
Key insight: The relation field name is determined by:
- Explicit name in schema:
verification VerificationRecord?→ field name isverification - Not the type name: The type
VerificationRecorddoesn't determine the field name
Step 2: Verify with Generated Types
Check the generated TypeScript types in packages/database/zenstack/models.ts:
// Search for your model
export type User = {
id: string;
verification?: VerificationRecord | null; // ← Actual field name
documents: Document[]; // ← Actual field name
}
This shows the exact field names available on instances.
Step 3: Test with a Database Query (if still unsure)
Write a simple query to see what's actually available:
const user = await db.user.findUnique({
where: { id: 'some-id' },
include: {
verification: true, // Try the suspected name
documents: true
}
});
console.log(Object.keys(user)); // See all available fields
Step 4: Update Your Code
Use the actual field names from the schema/types:
// ❌ WRONG (assumed names)
const status = instructor.verificationRecord?.status;
const docs = instructor.documentsOwned;
// ✅ CORRECT (actual schema names)
const status = instructor.verification?.status;
const docs = instructor.documents;
Verification
After using the correct field names:
- ✅ TypeScript errors disappear
- ✅ Queries execute without runtime errors
- ✅ Related data is properly accessed
Example
Scenario: Implementing admin verification UI, need to access instructor's verification record and documents.
Initial attempt (based on assumptions):
const instructor = await baseDB.user.findUnique({
where: { id: instructorId },
include: {
verificationRecord: true, // ❌ Doesn't exist
documentsOwned: true, // ❌ Doesn't exist
},
});
After schema discovery:
const instructor = await baseDB.user.findUnique({
where: { id: instructorId },
include: {
verification: true, // ✅ Correct name from schema
documents: true, // ✅ Correct name from schema
},
});
Common Patterns
One-to-One Relations
model User {
profile Profile? // ← Field name: "profile"
}
// Access: user.profile (not user.userProfile)
One-to-Many Relations
model User {
vehicles Vehicle[] // ← Field name: "vehicles"
}
// Access: user.vehicles (not user.vehiclesOwned)
Many-to-Many Relations
model User {
languages UserLanguage[] // ← Field name: "languages"
}
// Access: user.languages (not user.userLanguages)
Relations with Type Tables
model Profile {
type ProfileType @relation(fields: [typeId], references: [id])
}
// Access: profile.type (not profile.profileType)
Notes
- Schema is source of truth: The field name in the schema definition is the actual field name on instances
- Type name ≠ Field name:
verification VerificationRecord?creates a field calledverification, notverificationRecord - Prisma conventions: Prisma (and ZenStack) follow these naming rules consistently
- Generated types are reliable: Always check
models.tswhen in doubt - Include syntax matches: The
includeobject uses the same field names as schema