Skip to content

Schema updates

In this section, we will begin the process of adding articles to the app, associating them with users and displaying some raw article data on the front-end. This will involve:

  1. Updating the Prisma schema found at prisma/schema.prisma
  2. Updating the seed data generated in prisma/seed.ts to populate our database with some dummy articles
  3. Changing the file structure of the project to add ‘articles’ as well as the ‘notes’ which already exist
  4. Display some raw, unstyled article data in the /news/$category.tsx route file that we created in a previous lesson

🚀 Let’s get started! 🚀

A Prisma schema is a file that serves as a blueprint for your database, defining the structure of your data.

Open the Prisma schema for the project which can be found at prisma/schema.prisma.

In the code snippet below is a copy of the User, Note and NoteImage models from the project.

Click on the collapsed lines to expand them one at a time, and look carefully at the links between them.

Can you see how they are related to each other? 🧐

model User {
14 collapsed lines
id String @id @default(cuid())
email String @unique
username String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
image UserImage?
password Password?
notes Note[]
roles Role[]
sessions Session[]
connections Connection[]
}
model Note {
16 collapsed lines
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
ownerId String
images NoteImage[]
// non-unique foreign key
@@index([ownerId])
// This helps our order by in the user search a LOT
@@index([ownerId, updatedAt])
}
model NoteImage {
13 collapsed lines
id String @id @default(cuid())
altText String?
contentType String
blob Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade, onUpdate: Cascade)
noteId String
// non-unique foreign key
@@index([noteId])
}

Let’s take each of the models in turn:

  • A user has an id, email, username and name field. The id, email and username fields are required and unique. This means that they are all mandatory and must be unique for each user; no two users can share the same email address, for example.
  • A user can also have an image and a password. Note that the image and password fields are optional. We can tell this because they are defined with a ? at the end of the type declaration.
  • A user can have many notes, roles, sessions and connections.
model User {
id String @id @default(cuid())
email String @unique
username String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
image UserImage?
password Password?
notes Note[]
roles Role[]
sessions Session[]
connections Connection[]
}
The `User` Model
  • A note must have an id, title and content field. The id field is the primary key and is generated automatically using the cuid() function.
  • A note can have many images.
  • A note belongs to a user (the owner field). This is known as a one-to-many relationship, as a user can have many notes, but a note can only belong to one user.
  • The ownerId field is a foreign key that references the id field of the User model.
  • The ownerId field and the updatedAt field are indexed to improve performance. We can tell this because of the @@index directive at the end of the model.
model Note {
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
ownerId String
images NoteImage[]
// non-unique foreign key
@@index([ownerId])
// This helps our order by in the user search a LOT
@@index([ownerId, updatedAt])
}
The `Note` model
  • A noteImage belongs to a note. This is known as a one-to-many relationship, as a note can have many images, but an image can only belong to one note.
  • The noteId field is a foreign key that references the id field of the Note model.
  • The noteId field is indexed to improve performance. We can tell this because of the @@index directive at the end of the model.
model NoteImage {
id String @id @default(cuid())
altText String?
contentType String
blob Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade, onUpdate: Cascade)
noteId String
// non-unique foreign key
@@index([noteId])
}
The `NoteImage` model

Our goal is to add articles to the app. To do this, we need to create:

  • an Article model in the Prisma schema.
  • an ArticleImage model to store images associated with articles.

Many of the features that we need for articles are already present in the Note model.

We will use this as a blueprint for the Article model, and make the necessary changes to reflect the differences between notes and articles.

  1. Place the code below directly below the NoteImage model in your schema.prisma file.

    prisma/schema.prisma
    model NoteImage {
    13 collapsed lines
    id String @id @default(cuid())
    altText String?
    contentType String
    blob Bytes
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    note Note @relation(fields: [noteId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    noteId String
    // non-unique foreign key
    @@index([noteId])
    }
    model Article {
    id String @id @default(cuid())
    title String
    content String
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    ownerId String
    images NoteImage[]
    // non-unique foreign key
    @@index([ownerId])
    // This helps our order by in the user search a LOT
    @@index([ownerId, updatedAt])
    }
    model ArticleImage {
    id String @id @default(cuid())
    altText String?
    contentType String
    blob Bytes
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    note Note @relation(fields: [noteId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    noteId String
    // non-unique foreign key
    @@index([noteId])
    }
  2. We now need to update the existing User article. You will find this around line 13.

    Add a relationship in the User model to the Article model like so:

    prisma/schema.prisma
    model User {
    7 collapsed lines
    id String @id @default(cuid())
    email String @unique
    username String @unique
    name String?
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    image UserImage?
    password Password?
    notes Note[]
    articles Article[]
    roles Role[]
    sessions Session[]
    connections Connection[]
    }

    Next, we will update the Article model, around line 66.

    Edit the code below to reference the ArticleImage model instead of the NoteImage model.

    prisma/schema.prisma
    model Article {
    5 collapsed lines
    id String @id @default(cuid())
    title String
    content String
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    ownerId String
    images NoteImage[]
    images ArticleImage[]
    // non-unique foreign key
    @@index([ownerId])
    // This helps our order by in the user search a LOT
    @@index([ownerId, updatedAt])
    }

    Don’t worry that images ArticleImage[] is still underlined red - there’s one last change to make.

  3. The ArticleImage model is a little more complex, as it references the Article model as the parent.

    In other words, each ArticleImage belongs to a single Article that it needs to reference via a foreign key.

    We therefore need to make several changes here:

    prisma/schema.prisma
    model ArticleImage {
    id String @id @default(cuid())
    altText String?
    contentType String
    blob Bytes
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    note Note @relation(fields: [noteId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    noteId String
    article Article @relation(fields: [articleId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    articleId String
    // non-unique foreign key
    @@index([noteId])
    @@index([articleId])
    }
  4. Finally, we need to add some additional fields to the Article model to indicate whether an article is published or draft, and when it was published:

    prisma/schema.prisma
    model Article {
    id String @id @default(cuid())
    title String
    content String
    isPublished Boolean @default(false)
    publishedAt DateTime?
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    9 collapsed lines
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    ownerId String
    images ArticleImage[]
    // non-unique foreign key
    @@index([ownerId])
    // This helps our order by in the user search a LOT
    @@index([ownerId, updatedAt])
    }

Notice that the isPublished field is a Boolean type with a default value of false. Similarly, the publishedAt field is a DateTime type that is optional.

Why do you think the publishedAt field is optional, and isPublished has an initial value of false? 🤔

  • The isPublished field is a Boolean type because it can only have two values: true or false.

    It has a default value of false because an article is usually a draft when it is first created. When the article is published, the isPublished field will be set to true.

  • The publishedAt field is optional because an article might be a draft and not yet published. In this case, the publishedAt field would be null.

Click to reveal

Finally, we want to arrange the news articles into categories, such as ‘Technology’, ‘Entertainment’ and ‘Business’.

To do this, we will create an ArticleCategory model and link it to the Article model.

  1. Create an ArticleCategory model in the Prisma schema.

    This can be placed directly below the ArticleImage model in the Prisma schema.

    prisma/schema.prisma
    model ArticleImage {
    13 collapsed lines
    id String @id @default(cuid())
    altText String?
    contentType String
    blob Bytes
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    article Article @relation(fields: [articleId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    articleId String
    // non-unique foreign key
    @@index([articleId])
    }
    model ArticleCategory {
    id String @id @default(cuid())
    name String
    slug String @unique
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    articles Article[]
    }
  2. Link the Article model to the new ArticleCategory model with the code below:

    prisma/schema.prisma
    model Article {
    id String @id @default(cuid())
    title String
    content String
    isPublished Boolean @default(false)
    publishedAt DateTime?
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    ownerId String
    category ArticleCategory? @relation(fields: [categoryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
    categoryId String?
    images ArticleImage[]
    // non-unique foreign key
    @@index([ownerId])
    @@index([categoryId])
    // This helps our order by in the user search a LOT
    @@index([ownerId, updatedAt])
    }
  3. Remember to save your changes to the Prisma schema file.

Now that we’ve made all the necessary changes to the Prisma schema, we need to apply these changes to the database.

To do this, run the following command in your terminal:

Terminal window
npx prisma migrate dev --name "add-article-models"

Once the process has finished, you should see a a new migration file has appeared inside your prisma/migrations folder:

Article migration file

Let’s check the database to confirm the changes have been applied. Run the following command in your terminal:

Terminal window
npx prisma studio

This will open the Prisma Studio interface in your browser. You should see the Article, ArticleImage and ArticleCategory models at the top of your models list:

Check article migration

Currently, these tables will be empty, as we haven’t added any seed data yet. We’ll do that in the next section.

In this section, we have:

  • Updated the Prisma schema to include an Article model and an ArticleImage model.
  • Updated the User model to include an articles field that references the Article model.
  • Updated the Article model to include an isPublished, publishedAt and category field.
  • Updated the ArticleImage model to reference the Article model instead of the Note model.
  • Migrated these changes to the database.

In the next tutorial, we will make some adjustments to the project structure to reflect the changes we’ve made to the Prisma schema.