Skip to content

Commit

Permalink
Add migration tests and docs (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
G4brym authored Nov 9, 2024
1 parent 01f77bb commit 90f487d
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ nav:
- basic-queries.md
- modular-selects.md
- utilities.md
- migrations.md
- Advanced Queries:
- advanced-queries/fields.md
- advanced-queries/join.md
Expand Down
139 changes: 139 additions & 0 deletions docs/pages/migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
## Managing Durable Objects Migrations

In order to automatically manage migrations inside Durable Objects, just apply run the apply method inside the constructor

```ts
import { DurableObject } from 'cloudflare:workers'
import { DOQB } from '../src'
import { Env } from './bindings'

export const migrations: Migration[] = [
{
name: '100000000000000_add_logs_table.sql',
sql: `
create table logs
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);`,
},
]

export class TestDO extends DurableObject {
constructor(state: DurableObjectState, env: Env) {
super(state, env)

void this.ctx.blockConcurrencyWhile(async () => {
const qb = new DOQB(this.ctx.storage.sql)
qb.migrations({ migrations }).apply()
})
}
}
```

Having this code inside the constructor will automatically apply new migrations when you update your worker.


## Methods

#### `migrations()`

```typescript
qb.migrations(options: MigrationOptions): Migrations
```
- **Parameters:**

- `options: MigrationOptions` - An object containing migrations and optional table name.

- `migrations: Array<Migration>` - An array of migration objects to be applied.

- `tableName?: string` - The name of the table to store migration records, defaults to 'migrations'.

#### `initialize()`

```typescript
initialize(): void
```
- **Description:**

- Initializes the migration table if it doesn't exist. Creates a table named according to `_tableName` or `migrations` if non is set, with columns for `id`, `name`, and `applied_at`.

#### `getApplied()`

```typescript
getApplied(): Array<MigrationEntry>
```
- **Description:**

- Fetches all migrations that have been applied from the database.

- **Returns:** An array of `MigrationEntry` objects representing applied migrations.

#### `getUnapplied()`

```typescript
getUnapplied(): Array<Migration>
```
- **Description:**

- Compares the list of all migrations with those that have been applied to determine which ones remain unapplied.

- **Returns:** An array of `Migration` objects that have not yet been applied.

#### `apply()`

```typescript
apply(): Array<Migration>
```
- **Description:**

- Applies all unapplied migrations by executing their SQL statements and logging the migration to the migration table.

- **Returns:** An array of `Migration` objects that were applied during this call.

### Type Definitions

#### MigrationEntry

```typescript
type MigrationEntry = {
id: number
name: string
applied_at: Date
}
```
- **Fields:**
- `id`: The unique identifier for each migration entry.
- `name`: The name of the migration.
- `applied_at`: The timestamp when the migration was applied.
#### Migration
```typescript
type Migration = {
name: string
sql: string
}
```
- **Fields:**
- `name`: The name of the migration.
- `sql`: The SQL command to execute for this migration.
#### MigrationOptions
```typescript
type MigrationOptions = {
migrations: Array<Migration>
tableName?: string
}
```
- **Fields:**
- `migrations`: An array of migration objects.
- `tableName`: Optional name for the migrations table.
1 change: 1 addition & 0 deletions tests/bindings.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type Env = {
DB: D1Database
TEST_DO: DurableObjectNamespace
}

declare module 'cloudflare:test' {
Expand Down
22 changes: 22 additions & 0 deletions tests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DurableObject } from 'cloudflare:workers'
import { DOQB } from '../src'
import { Env } from './bindings'
import { migrations } from './integration/migrations-do.test'

export class TestDO extends DurableObject {
constructor(state: DurableObjectState, env: Env) {
super(state, env)

void this.ctx.blockConcurrencyWhile(async () => {
const qb = new DOQB(this.ctx.storage.sql)

qb.migrations({ migrations }).apply()
})
}
}

export default {
async fetch(request: Request, env: Env) {
return new Response('test')
},
}
File renamed without changes.
102 changes: 102 additions & 0 deletions tests/integration/migrations-do.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { env, runInDurableObject } from 'cloudflare:test'
import { describe, expect, it } from 'vitest'
import { D1QB, DOQB, Migration } from '../../src'

export const migrations: Migration[] = [
{
name: '100000000000000_add_logs_table.sql',
sql: `
create table logs
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);`,
},
]

describe('Migrations', () => {
it('initialize', async () => {
const id = env.TEST_DO.idFromName('test')
const stub = env.TEST_DO.get(id)

await runInDurableObject(stub, async (_instance, state) => {
// Initialize is called inside DO constructor

expect(
Array.from(
state.storage.sql.exec(`SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name not in ('_cf_KV', 'sqlite_sequence', '_cf_METADATA')`)
)
).toEqual([
{
name: 'migrations',
},
{
name: 'logs',
},
])
})
})

it('apply', async () => {
const id = env.TEST_DO.idFromName('test')
const stub = env.TEST_DO.get(id)

await runInDurableObject(stub, async (_instance, state) => {
// Initialize is called inside DO constructor

expect(
Array.from(
state.storage.sql.exec(`SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name not in ('_cf_KV', 'sqlite_sequence', '_cf_METADATA')`)
)
).toEqual([
{
name: 'migrations',
},
{
name: 'logs',
},
])

const qb = new DOQB(state.storage.sql)
const applyResp2 = qb.migrations({ migrations }).apply()
expect(applyResp2.length).toEqual(0)
})
})

it('incremental migrations', async () => {
const id = env.TEST_DO.idFromName('test')
const stub = env.TEST_DO.get(id)

await runInDurableObject(stub, async (_instance, state) => {
// Initialize is called inside DO constructor

const updatedMigrations = [
...migrations,
{
name: '100000000000001_add_second_table.sql',
sql: `
create table logs_two
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);`,
},
]

const qb = new DOQB(state.storage.sql)
const applyResp2 = qb.migrations({ migrations: updatedMigrations }).apply()

expect(applyResp2.length).toEqual(1)
expect(applyResp2[0]?.name).toEqual('100000000000001_add_second_table.sql')

const applyResp3 = qb.migrations({ migrations: updatedMigrations }).apply()
expect(applyResp3.length).toEqual(0)
})
})
})
3 changes: 3 additions & 0 deletions tests/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: {
configPath: './wrangler.toml',
},
miniflare: {
compatibilityFlags: ['nodejs_compat'],
d1Databases: {
Expand Down
11 changes: 11 additions & 0 deletions tests/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name = "test"
main = "index.ts"
compatibility_date = "2024-11-09"

[[durable_objects.bindings]]
name = "TEST_DO"
class_name = "TestDO"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["TestDO"]

0 comments on commit 90f487d

Please sign in to comment.