Skip to content

Commit 5ee14c8

Browse files
committed
feat: add CLI commands and improve migrations DX
1 parent 3d383cf commit 5ee14c8

File tree

17 files changed

+227
-42
lines changed

17 files changed

+227
-42
lines changed

cli/commands/database.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { defineCommand } from 'citty'
22
import generate from './database/generate.mjs'
33
import migrate from './database/migrate.mjs'
4+
import markAsMigrated from './database/mark-as-migrated.mjs'
5+
import drop from './database/drop.mjs'
46

57
export default defineCommand({
68
meta: {
@@ -10,6 +12,8 @@ export default defineCommand({
1012
},
1113
subCommands: {
1214
generate,
13-
migrate
15+
migrate,
16+
'mark-as-migrated': markAsMigrated,
17+
drop
1418
}
1519
})

cli/commands/database/drop.mjs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { defineCommand } from 'citty'
2+
import { consola } from 'consola'
3+
import { execa } from 'execa'
4+
import { readFile } from 'node:fs/promises'
5+
import { join } from 'pathe'
6+
import { createDrizzleClient } from '../../../dist/module.mjs'
7+
import { sql } from 'drizzle-orm'
8+
9+
export default defineCommand({
10+
meta: {
11+
name: 'drop',
12+
description: 'Drop a table from the database.'
13+
},
14+
args: {
15+
table: {
16+
type: 'positional',
17+
description: 'The name of the table to drop.',
18+
required: true
19+
},
20+
cwd: {
21+
type: 'option',
22+
description: 'The directory to run the command in.',
23+
required: false
24+
},
25+
verbose: {
26+
alias: 'v',
27+
type: 'boolean',
28+
description: 'Show verbose output.',
29+
required: false
30+
}
31+
},
32+
async run({ args }) {
33+
if (args.verbose) {
34+
consola.level = 'debug'
35+
}
36+
const cwd = args.cwd || process.cwd()
37+
consola.info('Preparing database configuration...')
38+
await execa({
39+
stdout: 'pipe',
40+
preferLocal: true,
41+
cwd
42+
})`nuxt prepare`
43+
const hubConfig = JSON.parse(await readFile(join(cwd, '.nuxt/hub/database/config.json'), 'utf-8'))
44+
consola.info(`Database dialect: \`${hubConfig.database.dialect}\``)
45+
const db = await createDrizzleClient(hubConfig.database)
46+
const execute = hubConfig.database.dialect === 'sqlite' ? 'run' : 'execute'
47+
await db[execute](sql.raw(`DROP TABLE IF EXISTS "${args.table}";`))
48+
consola.success(`Table \`${args.table}\` dropped successfully.`)
49+
}
50+
})
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { defineCommand } from 'citty'
2+
import { consola } from 'consola'
3+
import { execa } from 'execa'
4+
import { readFile } from 'node:fs/promises'
5+
import { join } from 'pathe'
6+
import { createDrizzleClient, getDatabaseMigrationFiles, AppliedDatabaseMigrationsQuery } from '../../../dist/module.mjs'
7+
import { sql } from 'drizzle-orm'
8+
9+
export default defineCommand({
10+
meta: {
11+
name: 'mark-as-migrated',
12+
description: 'Mark local database migration(s) as applied to the database.'
13+
},
14+
args: {
15+
name: {
16+
type: 'positional',
17+
description: 'The name of the migration to mark as applied.',
18+
required: false
19+
},
20+
cwd: {
21+
type: 'option',
22+
description: 'The directory to run the command in.',
23+
required: false
24+
},
25+
verbose: {
26+
alias: 'v',
27+
type: 'boolean',
28+
description: 'Show verbose output.',
29+
required: false
30+
}
31+
},
32+
async run({ args }) {
33+
if (args.verbose) {
34+
consola.level = 'debug'
35+
}
36+
const cwd = args.cwd || process.cwd()
37+
consola.info('Ensuring database schema is generated...')
38+
await execa({
39+
stdout: 'pipe',
40+
preferLocal: true,
41+
cwd
42+
})`nuxt prepare`
43+
const hubConfig = JSON.parse(await readFile(join(cwd, '.nuxt/hub/database/config.json'), 'utf-8'))
44+
consola.info(`Database dialect: \`${hubConfig.database.dialect}\``)
45+
const localMigrations = await getDatabaseMigrationFiles(hubConfig)
46+
if (localMigrations.length === 0) {
47+
consola.info('No local migrations found.')
48+
return
49+
}
50+
consola.info(`Found \`${localMigrations.length}\` local migration${localMigrations.length === 1 ? '' : 's'}`)
51+
consola.debug(`Local migrations:\n${localMigrations.map(migration => `- ${migration.name}`).join('\n')}`)
52+
if (args.name && !localMigrations.find(migration => migration.name === args.name)) {
53+
consola.error(`Local migration \`${args.name}\` not found.`)
54+
process.exit(1)
55+
}
56+
const db = await createDrizzleClient(hubConfig.database)
57+
const execute = hubConfig.database.dialect === 'sqlite' ? 'run' : 'execute'
58+
const { rows: appliedMigrations } = await db[execute](sql.raw(AppliedDatabaseMigrationsQuery))
59+
consola.info(`Database has \`${appliedMigrations.length}\` applied migration${appliedMigrations.length === 1 ? '' : 's'}`)
60+
consola.debug(`Applied migrations:\n${appliedMigrations.map(migration => `- ${migration.name} (\`${migration.applied_at}\`)`).join('\n')}`)
61+
// If a specific migration is provided, check if it is already applied
62+
if (args.name && appliedMigrations.find(appliedMigration => appliedMigration.name === args.name)) {
63+
consola.success(`Local migration \`${args.name}\` is already applied.`)
64+
return
65+
}
66+
// If a specific migration is provided, mark it as applied
67+
if (args.name) {
68+
await db[execute](sql.raw(`INSERT INTO "_hub_migrations" (name) values ('${args.name}');`))
69+
consola.success(`Local migration \`${args.name}\` marked as applied.`)
70+
return
71+
}
72+
// If no specific migration is provided, mark all pending migrations as applied
73+
const pendingMigrations = localMigrations.filter(migration => !appliedMigrations.find(appliedMigration => appliedMigration.name === migration.name))
74+
if (pendingMigrations.length === 0) {
75+
consola.success('All migrations are already applied.')
76+
return
77+
}
78+
consola.info(`Found \`${pendingMigrations.length}\` pending migration${pendingMigrations.length === 1 ? '' : 's'}`)
79+
let migrationsMarkedAsApplied = 0
80+
for (const migration of pendingMigrations) {
81+
const confirmed = await consola.prompt(`Mark migration \`${migration.name}\` as applied?`, {
82+
type: 'confirm',
83+
default: true,
84+
cancel: 'null'
85+
})
86+
if (!confirmed) {
87+
consola.info(`Migration \`${migration.name}\` skipped.`)
88+
continue
89+
}
90+
await db[execute](sql.raw(`INSERT INTO "_hub_migrations" (name) values ('${migration.name}');`))
91+
consola.success(`Migration \`${migration.name}\` marked as applied.`)
92+
migrationsMarkedAsApplied++
93+
}
94+
if (migrationsMarkedAsApplied === 0) {
95+
consola.info('No migrations marked as applied.')
96+
return
97+
}
98+
consola.success(`${migrationsMarkedAsApplied} migration${migrationsMarkedAsApplied === 1 ? '' : 's'} marked as applied.`)
99+
}
100+
})

cli/commands/database/migrate.mjs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { consola } from 'consola'
33
import { execa } from 'execa'
44
import { readFile } from 'node:fs/promises'
55
import { join } from 'pathe'
6-
import { applyDatabaseMigrations, createDrizzleClient } from '../../../dist/module.mjs'
6+
import { applyDatabaseMigrations, applyDatabaseQueries, createDrizzleClient } from '../../../dist/module.mjs'
77

88
export default defineCommand({
99
meta: {
@@ -15,9 +15,18 @@ export default defineCommand({
1515
type: 'option',
1616
description: 'The directory to run the command in.',
1717
required: false
18+
},
19+
verbose: {
20+
alias: 'v',
21+
type: 'boolean',
22+
description: 'Show verbose output.',
23+
required: false
1824
}
1925
},
2026
async run({ args }) {
27+
if (args.verbose) {
28+
process.env.CONSOLA_LEVEL = 'debug'
29+
}
2130
const cwd = args.cwd || process.cwd()
2231
consola.info('Ensuring database schema is generated...')
2332
await execa({
@@ -28,7 +37,13 @@ export default defineCommand({
2837
consola.info('Applying database migrations...')
2938
const hubConfig = JSON.parse(await readFile(join(cwd, '.nuxt/hub/database/config.json'), 'utf-8'))
3039
const db = await createDrizzleClient(hubConfig.database)
31-
await applyDatabaseMigrations(hubConfig, db)
32-
consola.success('Database migrations applied successfully.')
40+
const migrationsApplied = await applyDatabaseMigrations(hubConfig, db)
41+
if (migrationsApplied === false) {
42+
process.exit(1)
43+
}
44+
const queriesApplied = await applyDatabaseQueries(hubConfig, db)
45+
if (queriesApplied === false) {
46+
process.exit(1)
47+
}
3348
}
3449
})
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { int, mysqlTable, text, timestamp } from "drizzle-orm/mysql-core";
1+
import { int, mysqlTable, text, timestamp } from 'drizzle-orm/mysql-core'
22

33
export const pages = mysqlTable('pages', {
44
id: int().primaryKey(),
55
title: text().notNull(),
66
body: text().notNull(),
77
createdAt: timestamp().notNull().defaultNow(),
8-
updatedAt: timestamp().notNull().defaultNow(),
8+
updatedAt: timestamp().notNull().defaultNow()
99
})
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
1+
import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
22

33
export const pages = pgTable('pages', {
44
id: integer().primaryKey(),
55
title: text().notNull(),
66
body: text().notNull(),
77
createdAt: timestamp().notNull().defaultNow(),
8-
updatedAt: timestamp().notNull().defaultNow(),
8+
updatedAt: timestamp().notNull().defaultNow()
99
})
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { sql } from "drizzle-orm";
2-
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
1+
import { sql } from 'drizzle-orm'
2+
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
33

44
export const pages = sqliteTable('pages', {
55
id: integer().primaryKey(),
66
title: text().notNull(),
77
body: text().notNull(),
88
createdAt: integer({ mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
9-
updatedAt: integer({ mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
9+
updatedAt: integer({ mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)
1010
})
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { db, schema } from "hub:database"
1+
import { db } from 'hub:database'
22

33
export default eventHandler(async () => {
4-
54
// List todos for the current user
65
return await db.query.todos.findMany()
76
})

playground/server/api/todos/index.post.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { db, schema } from "hub:database"
1+
import { db, schema } from 'hub:database'
22

33
export default eventHandler(async (event) => {
44
const { title } = await readValidatedBody(event, z.object({
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// import { pages } from 'hub:database:schema'
2-
import { integer, pgTable, text, timestamp, serial } from "drizzle-orm/pg-core";
2+
import { integer, pgTable, text, timestamp, serial } from 'drizzle-orm/pg-core'
33

44
export const comments = pgTable('comments', {
55
id: serial().primaryKey(),
66
// pageId: integer().references(() => pages.id),
77
pageId: integer().notNull(),
88
content: text().notNull(),
99
createdAt: timestamp().notNull().defaultNow(),
10-
updatedAt: timestamp().notNull().defaultNow(),
10+
updatedAt: timestamp().notNull().defaultNow()
1111
})

0 commit comments

Comments
 (0)