Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export default defineNuxtConfig({
tasks: true,
},
modules: ['nitro-graphql'],
serverAssets: [{
baseName: 'migrations',
dir: './database/migrations', // Relative to Nitro srcDir (server/)
}],
graphql: {
framework: 'graphql-yoga',
codegen: {
Expand Down
22 changes: 22 additions & 0 deletions server/api/cron/migrate.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { MigrationResult } from '../../utils/migrate'

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()

// Security check - require CRON_SECRET for production
const authHeader = getHeader(event, 'authorization')
if (config.cronSecret && authHeader !== `Bearer ${config.cronSecret}`) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}

const { result } = await runTask('db:migrate') as { result: MigrationResult }

if (!result.success) {
throw createError({
statusCode: 500,
message: result.message,
})
}

return { success: true, result }
})
32 changes: 32 additions & 0 deletions server/plugins/00.database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { runMigrations } from '../utils/migrate'

export default defineNitroPlugin(async () => {
// Skip in test environment
if (process.env.NODE_ENV === 'test') {
console.log('[Database] Skipping migrations in test environment')
return
}

// Skip if AUTO_MIGRATE is explicitly disabled
if (process.env.AUTO_MIGRATE === 'false') {
console.log('[Database] Auto-migration disabled')
return
}

const result = await runMigrations()

if (result.success) {
if (result.applied.length > 0) {
for (const migration of result.applied) {
console.log(`[Database] Applied migration: ${migration}`)
}
console.log(`[Database] ${result.applied.length} migration(s) applied successfully`)
}
else {
console.log('[Database] All migrations already applied')
}
}
else {
console.error('[Database] Migration failed:', result.message)
}
})
12 changes: 12 additions & 0 deletions server/tasks/db/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { runMigrations } from '../../utils/migrate'

export default defineTask({
meta: {
name: 'db:migrate',
description: 'Run database migrations',
},
async run() {
const result = await runMigrations()
return { result }
},
})
93 changes: 93 additions & 0 deletions server/utils/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'

interface MigrationJournal {
version: string
dialect: string
entries: {
idx: number
version: string
when: number
tag: string
breakpoints: boolean
}[]
}

export interface MigrationResult {
success: boolean
message: string
applied: string[]
}

export async function runMigrations(): Promise<MigrationResult> {
const connectionString = process.env.DATABASE_URL || 'postgresql://localhost:5432/nitroping'
const client = postgres(connectionString, { max: 1 })
const db = drizzle(client)

const applied: string[] = []

try {
const storage = useStorage('assets:migrations')

const journal = await storage.getItem<MigrationJournal>('meta/_journal.json')
if (!journal || !journal.entries) {
await client.end()
return { success: true, message: 'No migrations found', applied: [] }
}

await db.execute(sql`
CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint
)
`)

const existingMigrations = await db.execute<{ hash: string }>(sql`SELECT hash FROM "__drizzle_migrations"`)
const appliedHashes = new Set(existingMigrations.map(r => r.hash))

for (const entry of journal.entries) {
if (appliedHashes.has(entry.tag)) {
continue
}

const sqlContent = await storage.getItem<string>(`${entry.tag}.sql`)
if (!sqlContent) {
applied.push(`[SKIP] ${entry.tag} - file not found`)
continue
}

const statements = sqlContent.split('--> statement-breakpoint')
for (const stmt of statements) {
const trimmed = stmt.trim()
if (trimmed) {
await db.execute(sql.raw(trimmed))
}
}

await db.execute(sql`
INSERT INTO "__drizzle_migrations" (hash, created_at)
VALUES (${entry.tag}, ${Date.now()})
`)

applied.push(entry.tag)
}

return {
success: true,
message: applied.length > 0 ? `Applied ${applied.length} migration(s)` : 'All migrations already applied',
applied,
}
}
catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Migration failed',
applied,
}
}
finally {
await client.end()
}
}