Skip to content

Commit 90f487d

Browse files
authored
Add migration tests and docs (#76)
1 parent 01f77bb commit 90f487d

File tree

8 files changed

+279
-0
lines changed

8 files changed

+279
-0
lines changed

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ nav:
3838
- basic-queries.md
3939
- modular-selects.md
4040
- utilities.md
41+
- migrations.md
4142
- Advanced Queries:
4243
- advanced-queries/fields.md
4344
- advanced-queries/join.md

docs/pages/migrations.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
## Managing Durable Objects Migrations
2+
3+
In order to automatically manage migrations inside Durable Objects, just apply run the apply method inside the constructor
4+
5+
```ts
6+
import { DurableObject } from 'cloudflare:workers'
7+
import { DOQB } from '../src'
8+
import { Env } from './bindings'
9+
10+
export const migrations: Migration[] = [
11+
{
12+
name: '100000000000000_add_logs_table.sql',
13+
sql: `
14+
create table logs
15+
(
16+
id INTEGER PRIMARY KEY AUTOINCREMENT,
17+
name TEXT NOT NULL
18+
);`,
19+
},
20+
]
21+
22+
export class TestDO extends DurableObject {
23+
constructor(state: DurableObjectState, env: Env) {
24+
super(state, env)
25+
26+
void this.ctx.blockConcurrencyWhile(async () => {
27+
const qb = new DOQB(this.ctx.storage.sql)
28+
qb.migrations({ migrations }).apply()
29+
})
30+
}
31+
}
32+
```
33+
34+
Having this code inside the constructor will automatically apply new migrations when you update your worker.
35+
36+
37+
## Methods
38+
39+
#### `migrations()`
40+
41+
```typescript
42+
qb.migrations(options: MigrationOptions): Migrations
43+
```
44+
- **Parameters:**
45+
46+
- `options: MigrationOptions` - An object containing migrations and optional table name.
47+
48+
- `migrations: Array<Migration>` - An array of migration objects to be applied.
49+
50+
- `tableName?: string` - The name of the table to store migration records, defaults to 'migrations'.
51+
52+
#### `initialize()`
53+
54+
```typescript
55+
initialize(): void
56+
```
57+
- **Description:**
58+
59+
- 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`.
60+
61+
#### `getApplied()`
62+
63+
```typescript
64+
getApplied(): Array<MigrationEntry>
65+
```
66+
- **Description:**
67+
68+
- Fetches all migrations that have been applied from the database.
69+
70+
- **Returns:** An array of `MigrationEntry` objects representing applied migrations.
71+
72+
#### `getUnapplied()`
73+
74+
```typescript
75+
getUnapplied(): Array<Migration>
76+
```
77+
- **Description:**
78+
79+
- Compares the list of all migrations with those that have been applied to determine which ones remain unapplied.
80+
81+
- **Returns:** An array of `Migration` objects that have not yet been applied.
82+
83+
#### `apply()`
84+
85+
```typescript
86+
apply(): Array<Migration>
87+
```
88+
- **Description:**
89+
90+
- Applies all unapplied migrations by executing their SQL statements and logging the migration to the migration table.
91+
92+
- **Returns:** An array of `Migration` objects that were applied during this call.
93+
94+
### Type Definitions
95+
96+
#### MigrationEntry
97+
98+
```typescript
99+
type MigrationEntry = {
100+
id: number
101+
name: string
102+
applied_at: Date
103+
}
104+
```
105+
- **Fields:**
106+
107+
- `id`: The unique identifier for each migration entry.
108+
109+
- `name`: The name of the migration.
110+
111+
- `applied_at`: The timestamp when the migration was applied.
112+
113+
#### Migration
114+
115+
```typescript
116+
type Migration = {
117+
name: string
118+
sql: string
119+
}
120+
```
121+
- **Fields:**
122+
123+
- `name`: The name of the migration.
124+
125+
- `sql`: The SQL command to execute for this migration.
126+
127+
#### MigrationOptions
128+
129+
```typescript
130+
type MigrationOptions = {
131+
migrations: Array<Migration>
132+
tableName?: string
133+
}
134+
```
135+
- **Fields:**
136+
137+
- `migrations`: An array of migration objects.
138+
139+
- `tableName`: Optional name for the migrations table.

tests/bindings.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export type Env = {
22
DB: D1Database
3+
TEST_DO: DurableObjectNamespace
34
}
45

56
declare module 'cloudflare:test' {

tests/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { DurableObject } from 'cloudflare:workers'
2+
import { DOQB } from '../src'
3+
import { Env } from './bindings'
4+
import { migrations } from './integration/migrations-do.test'
5+
6+
export class TestDO extends DurableObject {
7+
constructor(state: DurableObjectState, env: Env) {
8+
super(state, env)
9+
10+
void this.ctx.blockConcurrencyWhile(async () => {
11+
const qb = new DOQB(this.ctx.storage.sql)
12+
13+
qb.migrations({ migrations }).apply()
14+
})
15+
}
16+
}
17+
18+
export default {
19+
async fetch(request: Request, env: Env) {
20+
return new Response('test')
21+
},
22+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { env, runInDurableObject } from 'cloudflare:test'
2+
import { describe, expect, it } from 'vitest'
3+
import { D1QB, DOQB, Migration } from '../../src'
4+
5+
export const migrations: Migration[] = [
6+
{
7+
name: '100000000000000_add_logs_table.sql',
8+
sql: `
9+
create table logs
10+
(
11+
id INTEGER PRIMARY KEY AUTOINCREMENT,
12+
name TEXT NOT NULL
13+
);`,
14+
},
15+
]
16+
17+
describe('Migrations', () => {
18+
it('initialize', async () => {
19+
const id = env.TEST_DO.idFromName('test')
20+
const stub = env.TEST_DO.get(id)
21+
22+
await runInDurableObject(stub, async (_instance, state) => {
23+
// Initialize is called inside DO constructor
24+
25+
expect(
26+
Array.from(
27+
state.storage.sql.exec(`SELECT name
28+
FROM sqlite_master
29+
WHERE type = 'table'
30+
AND name not in ('_cf_KV', 'sqlite_sequence', '_cf_METADATA')`)
31+
)
32+
).toEqual([
33+
{
34+
name: 'migrations',
35+
},
36+
{
37+
name: 'logs',
38+
},
39+
])
40+
})
41+
})
42+
43+
it('apply', async () => {
44+
const id = env.TEST_DO.idFromName('test')
45+
const stub = env.TEST_DO.get(id)
46+
47+
await runInDurableObject(stub, async (_instance, state) => {
48+
// Initialize is called inside DO constructor
49+
50+
expect(
51+
Array.from(
52+
state.storage.sql.exec(`SELECT name
53+
FROM sqlite_master
54+
WHERE type = 'table'
55+
AND name not in ('_cf_KV', 'sqlite_sequence', '_cf_METADATA')`)
56+
)
57+
).toEqual([
58+
{
59+
name: 'migrations',
60+
},
61+
{
62+
name: 'logs',
63+
},
64+
])
65+
66+
const qb = new DOQB(state.storage.sql)
67+
const applyResp2 = qb.migrations({ migrations }).apply()
68+
expect(applyResp2.length).toEqual(0)
69+
})
70+
})
71+
72+
it('incremental migrations', async () => {
73+
const id = env.TEST_DO.idFromName('test')
74+
const stub = env.TEST_DO.get(id)
75+
76+
await runInDurableObject(stub, async (_instance, state) => {
77+
// Initialize is called inside DO constructor
78+
79+
const updatedMigrations = [
80+
...migrations,
81+
{
82+
name: '100000000000001_add_second_table.sql',
83+
sql: `
84+
create table logs_two
85+
(
86+
id INTEGER PRIMARY KEY AUTOINCREMENT,
87+
name TEXT NOT NULL
88+
);`,
89+
},
90+
]
91+
92+
const qb = new DOQB(state.storage.sql)
93+
const applyResp2 = qb.migrations({ migrations: updatedMigrations }).apply()
94+
95+
expect(applyResp2.length).toEqual(1)
96+
expect(applyResp2[0]?.name).toEqual('100000000000001_add_second_table.sql')
97+
98+
const applyResp3 = qb.migrations({ migrations: updatedMigrations }).apply()
99+
expect(applyResp3.length).toEqual(0)
100+
})
101+
})
102+
})

tests/vitest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export default defineWorkersConfig({
44
test: {
55
poolOptions: {
66
workers: {
7+
wrangler: {
8+
configPath: './wrangler.toml',
9+
},
710
miniflare: {
811
compatibilityFlags: ['nodejs_compat'],
912
d1Databases: {

tests/wrangler.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name = "test"
2+
main = "index.ts"
3+
compatibility_date = "2024-11-09"
4+
5+
[[durable_objects.bindings]]
6+
name = "TEST_DO"
7+
class_name = "TestDO"
8+
9+
[[migrations]]
10+
tag = "v1"
11+
new_sqlite_classes = ["TestDO"]

0 commit comments

Comments
 (0)