Designing the database
The application state normalization relies on the ability to extract entities from data structure returned by the server, based on the presence of entityType and id properties. Therefore, the database schema should be designed in a way that each table includes entityType column with a fixed value representing the entity type.
In the database we have two entity types: user for the User model and task for the Task model. Weโre going to create an enum with lower-cased entity names in singular form and add it to each table as a column with a default value (ideally, it should be read-only).
Weโre also going to use prisma-zod-generatorย to generate Zod schemas from our Prisma models. This will help us to define Zod models automatically and make our server-side code much shorter.
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "./generated/client"
}
generator zod {
provider = "prisma-zod-generator"
config = "./zod-generator.config.json"
}
datasource db {
provider = "postgresql"
}
model User {
/// @zod.brand<'user'>()
id String @id @default(uuid())
/// @zod.custom.use(z.literal('user'))
entityType EntityType @default(user)
/// @zod.meta({ description: "Timestamp when the user was created", examples: ["2023-01-01T00:00:00.000Z"] })
createdAt DateTime @default(now())
/// @zod.meta({ description: "Timestamp when the user was last updated", examples: ["2023-01-01T00:00:00.000Z"] })
updatedAt DateTime @default(now()) @updatedAt
/// @zod.meta({ examples: ["John Doe"], description: "Full name of the user" })
fullName String
/// @zod.meta({ examples: ["john.doe@example.com"], description: "Email address of the user" })
email String @unique
/// @zod.meta({ examples: ["https://example.com/image.jpg"], description: "Profile image URL of the user" })
imageUrl String?
tasks Task[]
embedding Unsupported("vector(1536)")?
}
model Task {
/// @zod.brand<'task'>()
id String @id @default(uuid())
/// @zod.custom.use(z.literal('task'))
entityType EntityType @default(task)
/// @zod.meta({ description: "Timestamp when the task was created", examples: ["2023-01-01T00:00:00.000Z"] })
createdAt DateTime @default(now())
/// @zod.meta({ description: "Timestamp when the task was last updated", examples: ["2023-01-01T00:00:00.000Z"] })
updatedAt DateTime @default(now()) @updatedAt
/// @zod.meta({ examples: ["Implement authentication"], description: "Title of the task" })
title String
/// @zod.meta({ examples: ["Implement user authentication using JWT"], description: "Description of the task" })
description String
/// @zod.meta({ examples: ["TODO"], description: "Status of the task" })
status TaskStatus @default(TODO)
/// @zod.brand<'user'>().meta({ examples: ["a3bb189e-8bf9-3888-9912-ace4e6543002"], description: "ID of the user who owns the task" })
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
embedding Unsupported("vector(1536)")?
}
enum EntityType {
user
task
}
enum TaskStatus {
TODO
IN_PROGRESS
IN_REVIEW
DONE
}The code above is fetched from GitHub repository.ย
Notice the triple slashes comments. As React components and other app logic is going to work with entity IDs, we need to distinguish between IDs of different entity types. Therefore brandedย types is the best way to achieve this. Weโre also using literal types for entityType columns and define examples and descriptions for better OpenAPI documentation generation and for LLM function calling.
Thatโs it. Each time when you invoke npx prisma generate, the Zod schemas are going to be generated automatically at prisma/generated/schemas folder with all the necessary type specifics.
For easier access to the generated schemas, you can add path mapping to your tsconfig.json:
{
"compilerOptions": {
"paths": {
"@schemas/*": ["./prisma/generated/schemas/*"],
},
},
}