Skip to content

Your first schema

By the end of this section we'll have defined and migrated a database where we can store users and users' posts.

Defining the users table

Let's build a simple schema with a single table called users.

Change your schema.ts file with the following code:

ts
import {
  index, integer,
  primaryKey, schema, table, text, unique,
} from "@monolayer/pg/schema";

const users = table({
  columns: {
    id: integer().generatedAlwaysAsIdentity(),
    email: text().notNull(),
    name: text(),
  },
  constraints: {
    primaryKey: primaryKey(["id"]),
    unique: [unique(["email"])],
  },
  indexes: [index(["email"])],
});

export const dbSchema = schema({
  tables: {
    users,
  },
});

export type DB = typeof dbSchema.infer;

TIP

Schema definition in monolayer-pg is type-safe.

Change the unique constraint column to a non existing one and see what happens! 🤯

In this schema, we have defined a table called users with:

  • An id column as an integer, always generated identity column (auto-incremented column).
  • A email column as a non-nullable text.
  • A name column as nullable text.
  • A primary key constraint on the id column.
  • A unique constraint on the email column.
  • An index on the email column.

Defining the posts table

Now, let's define a posts table where we'll store the user's posts.

Change your schema.ts file with the following code:

ts
import { sql } from "kysely";
import {
  index, integer,
  primaryKey, schema, table, text, unique,
  boolean, foreignKey, timestampWithTimeZone
} from "@monolayer/pg/schema";
import {
  updateTimestampOnRecordUpdate,
} from "@monolayer/pg/helpers/timestamps";

const users = table({
  columns: {
    id: integer().generatedAlwaysAsIdentity(),
    email: text().notNull(),
    name: text(),
  },
  constraints: {
    primaryKey: primaryKey(["id"]),
    unique: [unique(["email"])],
  },
  indexes: [index(["email"])],
});

const posts = table({
  columns: {
    id: integer().generatedAlwaysAsIdentity(),
    title: text().notNull(),
    content: text(),
    published: boolean().default(false),
    authorId: integer(),
    createdAt: timestampWithTimeZone().notNull().default(sql`now()`),
    updatedAt: timestampWithTimeZone().notNull().default(sql`now()`),
  },
  constraints: {
    primaryKey: primaryKey(["id"]),
    foreignKeys: [
      foreignKey(["authorId"], users, ["id"])
        .deleteRule("set null")
        .updateRule("cascade"),
     ],
  },
  indexes: [index(["authorId"])],
  triggers: [updateTimestampOnRecordUpdate("updatedAt")],
});

export const dbSchema = schema({
  tables: {
    users,
    posts,
  },
});

export type DB = typeof dbSchema.infer;

The posts table what we added to the schema has:

  • An id column as an integer, always generated identity column (auto-incremented column).
  • An title column as a non-nullable text.
  • An content column as a nullable text.
  • An published column as an integer, always generated identity column (auto-incremented column).
  • An authorId column as a nullable integer.
  • An createdAt column as a timestamp with time zone, non-nullable, and with the current timestamp as default.
  • An updatedAt column as a timestamp with time zone, non-nullable, and with the current timestamp as default.
  • A primary key constraint on the id column.
  • A foreign key constraint on authorId that references users.id. When deleting the referenced user, authorId will be set to NULL. Updating users.id will update authorId.
  • An index on the authorId column.
  • A trigger that will update the updatedAt column whenever a record is updated.

INFO

The trigger will enable us to use native PostgreSQL capabilities to update the updatedAt column, no ORM needed! 🎉.

Adding an extension to the database

In order for the trigger to work, we need to add the moddatetime extension to the database. Modify the database definition in database.ts with the following code:

ts
import { defineDatabase, extension } from "@monolayer/pg/schema";
import { dbSchema } from "./schema";
import { dbSeed } from "./seed";

export default defineDatabase({
  schemas: [dbSchema],
  extensions: [extension("moddatetime")],
  camelCase: false,
  seeder: dbSeed,
});

We've added the moddatetime extension to our database definition.

Create the database

INFO

You can skip this step if the database is already created

Create the database with the monolayer-pg CLI:

bash
npx monolayer db create
text
┌  Create Database

◇  Create database hello-monolayer ✓

└  Done

Generate migrations

To update the database to reflect the schema that we have defined, we'll first generate migrations:

bash
npx monolayer migrations generate

The command will prompt you to select a migration name

txt
┌  monolayer generate

◇  Enter a name for the schema migration
│  add users and posts

●  Generated migration: monolayer/migrations/default/expand/20240725130115976-add-users-and-posts.ts

└  Done

This command will generate a migration file of your chosen name with the schema changes to apply in your project under the monolayer-pg directory.

text
🗂️ hello-monolayer (project root)
└ 📁 monolayer
  └ 📁 migrations
    └ 📁 expand
      └ 📄 ${timestamp}-${migration-name}.ts

WARNING

The folder monolayer-pg SHOULD BE added to version control.

Apply migrations

Now we'll apply the migrations to the database:

bash
npx monolayer migrations apply --phase all

The command should output something similar to the following:

text
┌  Migrate all pending migrations (expand, alter, data, contract)

◆   APPLIED  ${migration-name}

◇  Dump database structure ✓

└  Done

After running the command, the current database structure will be also dumped to a file in monolayer/dumps

text
// Generated migration file
🗂️ hello-monolayer (project root)
└ 📁 monolayer
  └ 📁 dumps
    └ 📄 structure.default.sql

You've defined a schema and applied it to a newly created the database! 🎉