Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: Add sample code to use Kysely in Deno & manage migrations #1011

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions site/docs/migrations/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "Migrations",
"position": 4,
"link": {
"type": "generated-index",
"description": "Manage database schema migrations with Kysely."
}
}
238 changes: 238 additions & 0 deletions site/docs/migrations/deno.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
---
sidebar_position: 4
---

# Deno

An example of the code in this guide can be found at [this repo](https://github.com/Sleepful/deno-kysely-migrations).

To manage migrations we need a few things:

- A way to create new migrations.
- A way to run our migrations.

This guide will show how to do this in Deno.

Assume the following configuration in `deno.json`:

```ts
{
"tasks": {
"dev": "deno run --watch main.ts",
"migrate": "deno run -A ./tasks/db_migrate.ts",
"new_migration": "deno run -A ./tasks/new_migration.ts",
},
"imports": {
"$std/": "https://deno.land/std/",
"kysely": "npm:kysely",
"kysely-postgres-js": "npm:kysely-postgres-js",
"postgres": "https://deno.land/x/postgresjs/mod.js",
}
}
```

Lets put a script that creates our migrations, we can run this script with:
```
$ deno new_migration <your_migration_name>
```
The script will be located at `tasks/new_migration.ts` and it will look like this:

```ts
// tasks/new_migration.ts
import { parseArgs } from "$std/cli/parse_args.ts";

// This is a Deno task, that means that CWD (Deno.cwd) is the root dir of the project,
// remember this when looking at the relative paths below.

// Make sure there is this `migrations` dir in root of project
const migrationDir = "./migrations";

const isMigrationFile = (filename: string): boolean => {
const regex = /.*migration.ts$/;
return regex.test(filename);
};

const files = [];
for await (const f of Deno.readDir(migrationDir)) {
f.isFile && isMigrationFile(f.name) && files.push(f);
}

const filename = (idx: number, filename: string) => {
const YYYYmmDD = new Date().toISOString().split("T")[0];
// pad the index of the migration to 3 digits because
// the migrations will be executed in alphanumerical order
const paddedIdx = String(idx).padStart(3, "0");
return `${paddedIdx}-${YYYYmmDD}-${filename}.migration.ts`;
};

const firstCLIarg = parseArgs(Deno.args)?._[0] as string ?? null;

if (!firstCLIarg) {
console.error("You must pass-in the name of the migration file as an arg");
Deno.exit(1);
}

// make sure this is file is present in the project
const templateText = await Deno.readTextFile(
"./tasks/new_migration/migration_template.ts",
);

const migrationFilename = filename(files.length, firstCLIarg);

console.log(`Creating migration:\n\nmigrations/${migrationFilename}\n`);

await Deno.writeTextFile(`./migrations/${migrationFilename}`, templateText);

console.log("Done!");
```

This script uses a template file for our migrations, lets create that too at `tasks/new_migration/migration_template.ts`:

```ts
// tasks/new_migration/migration_template.ts
import { Kysely, sql } from "kysely";

export async function up(db: Kysely<any>): Promise<void> {
// Migration code
}

export async function down(db: Kysely<any>): Promise<void> {
// Migration code
}
```

Finally, lets create the script that will run our migrations. We will use this script like this:
```
$ deno migrate
$ deno migrate down
```
The script will be located at `tasks/db_migrate.ts` and will look like this:
```ts
// tasks/db_migrate.ts
import {
Kysely,
type Migration,
type MigrationProvider,
Migrator,
} from "kysely";
import { PostgresJSDialect } from "kysely-postgres-js";
import postgres from "postgres";
import * as Path from "$std/path/mod.ts";
import { parseArgs } from "$std/cli/parse_args.ts";

const databaseConfig = {
postgres: postgres({
database: "my_database_name",
host: "localhost",
max: 10,
port: 5432,
user: "postgres",
}),
};

// Migration files must be found in the `migrationDir`.
// and the migration files must follow name convention:
// number-<filename>.migration.ts
// where `number` identifies the order in which migrations are to be run.
//
// To create a new migration file use:
// deno task new_migration <name-of-your-migration-file>

const migrationDir = "./migrations";

const allowUnorderedMigrations = false;

// Documentation to understand why the code in this file looks as it does:
// Kysely docs: https://www.kysely.dev/docs/migrations/deno
// Example provider: https://github.com/kysely-org/kysely/blob/6f913552/src/migration/file-migration-provider.ts#L20

interface Database {}

const db = new Kysely<Database>({
dialect: new PostgresJSDialect(databaseConfig),
});

export interface FileMigrationProviderProps {
migrationDir: string;
}

class DenoMigrationProvider implements MigrationProvider {
readonly #props: FileMigrationProviderProps;

constructor(props: FileMigrationProviderProps) {
this.#props = props;
}

isMigrationFile = (filename: string): boolean => {
const regex = /.*migration.ts$/;
return regex.test(filename);
};

async getMigrations(): Promise<Record<string, Migration>> {
const files: Deno.DirEntry[] = [];
for await (const f of Deno.readDir(this.#props.migrationDir)) {
f.isFile && this.isMigrationFile(f.name) && files.push(f);
}

const migrations: Record<string, Migration> = {};

for (const f of files) {
const filePath = Path.join(Deno.cwd(), this.#props.migrationDir, f.name);
const migration = await import(filePath);
const migrationKey = f.name.match(/(\d+-.*).migration.ts/)![1];
migrations[migrationKey] = migration;
}

return migrations;
}
}

const migrator = new Migrator({
db,
provider: new DenoMigrationProvider({ migrationDir }),
allowUnorderedMigrations,
});

const firstCLIarg = parseArgs(Deno.args)?._[0] as string ?? null;

const migrate = () => {
if (firstCLIarg == "down") {
return migrator.migrateDown();
}
return migrator.migrateToLatest();
};

const { error, results } = await migrate();
results?.forEach((it) => {
if (it.status === "Success") {
console.log(`Migration "${it.migrationName}" was executed successfully`);
} else if (it.status === "Error") {
console.error(`Failed to execute migration "${it.migrationName}"`);
}
});

if (error) {
console.error("Failed to migrate");
console.error(error);
Deno.exit(1);
}

await db.destroy();

Deno.exit(0);
```

Remember to adjust `databaseConfig` so that it connects to your database.

Now you can create a migration with:
```
deno task new_migration <name>
```
Then apply it to your database with:
```
deno task migrate
```
If you want to undo your last migration just use:
```
deno task migrate down
```
95 changes: 13 additions & 82 deletions site/docs/migrations.mdx → site/docs/migrations/overview.mdx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
---
sidebar_position: 4
sidebar_position: 1
---

# Migrations
# Overview

To run migrations in kysely, you need two things:
- Migration files.
- A configured [Migrator](https://kysely-org.github.io/kysely-apidoc/classes/Migrator.html) to run its methods.

## Migration files

Migration files should look like this:

Expand All @@ -24,6 +30,11 @@ Migrations should never depend on the current code of your app because they need

Migrations can use the `Kysely.schema` module to modify the schema. Migrations can also run normal queries to modify data.

An example migration file for:

- [PostgreSQL](./postgresql-migration.mdx)
- [SQLite](./sqlite-migration.mdx)

## Execution order

Migrations will be run in the alpha-numeric order of your migration names. An excellent way to name your migrations is to prefix them with an ISO 8601 date string.
Expand All @@ -46,86 +57,6 @@ const migrator = new Migrator({

You don't need to store your migrations as separate files if you don't want to. You can easily implement your own MigrationProvider and give it to the Migrator class when you instantiate one.

## PostgreSQL migration example

```ts
import { Kysely, sql } from 'kysely'

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('person')
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('first_name', 'varchar', (col) => col.notNull())
.addColumn('last_name', 'varchar')
.addColumn('gender', 'varchar(50)', (col) => col.notNull())
.addColumn('created_at', 'timestamp', (col) =>
col.defaultTo(sql`now()`).notNull()
)
.execute()

await db.schema
.createTable('pet')
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('name', 'varchar', (col) => col.notNull().unique())
.addColumn('owner_id', 'integer', (col) =>
col.references('person.id').onDelete('cascade').notNull()
)
.addColumn('species', 'varchar', (col) => col.notNull())
.execute()

await db.schema
.createIndex('pet_owner_id_index')
.on('pet')
.column('owner_id')
.execute()
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('pet').execute()
await db.schema.dropTable('person').execute()
}
```

## SQLite migration example

```ts
import { Kysely, sql } from 'kysely'

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('person')
.addColumn('id', 'integer', (col) => col.primaryKey())
.addColumn('first_name', 'text', (col) => col.notNull())
.addColumn('last_name', 'text')
.addColumn('gender', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) =>
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
)
.execute()

await db.schema
.createTable('pet')
.addColumn('id', 'integer', (col) => col.primaryKey())
.addColumn('name', 'text', (col) => col.notNull().unique())
.addColumn('owner_id', 'integer', (col) =>
col.references('person.id').onDelete('cascade').notNull()
)
.addColumn('species', 'text', (col) => col.notNull())
.execute()

await db.schema
.createIndex('pet_owner_id_index')
.on('pet')
.column('owner_id')
.execute()
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('pet').execute()
await db.schema.dropTable('person').execute()
}
```

## Running migrations

You can then use
Expand Down
Loading
Loading