diff --git a/docs/content/0.index.yml b/docs/content/0.index.yml index 68014038..df309efb 100644 --- a/docs/content/0.index.yml +++ b/docs/content/0.index.yml @@ -10,8 +10,8 @@ hero: light: '/images/landing/hero-light.svg' dark: '/images/landing/hero-dark.svg' headline: - label: "Blob: Presigned URLs" - to: /changelog/blob-presigned-urls + label: "Automatic Database Migrations" + to: /changelog/database-migrations icon: i-ph-arrow-right features: - name: Cloud Hosting diff --git a/docs/content/1.docs/2.features/database.md b/docs/content/1.docs/2.features/database.md index 41e17adb..e8fb6ef5 100644 --- a/docs/content/1.docs/2.features/database.md +++ b/docs/content/1.docs/2.features/database.md @@ -99,6 +99,20 @@ console.log(results) */ ``` +The method return an object that contains the results (if applicable), the success status and a meta object: + +```ts +{ + results: array | null, // [] if empty, or null if it does not apply + success: boolean, // true if the operation was successful, false otherwise + meta: { + duration: number, // duration of the operation in milliseconds + rows_read: number, // the number of rows read (scanned) by this query + rows_written: number // the number of rows written by this query + } +} +``` + ### `first()` Returns the first row of the results. This does not return metadata like the other methods. Instead, it returns the object directly. @@ -202,6 +216,8 @@ console.log(info1) */ ``` +The object returned is the same as the [`.all()`](#all) method. + ### `exec()` Executes one or more queries directly without prepared statements or parameters binding. The input can be one or multiple queries separated by \n. @@ -223,22 +239,109 @@ console.log(result) This method can have poorer performance (prepared statements can be reused in some cases) and, more importantly, is less safe. Only use this method for maintenance and one-shot tasks (for example, migration jobs). The input can be one or multiple queries separated by \n. :: -## Return Object +## Database Migrations -The methods [`.all()`](#all) and [`.batch()`](#batch) return an object that contains the results (if applicable), the success status and a meta object: +Database migrations provide version control for your database schema. They track changes and ensure consistent schema evolution across all environments through incremental updates. -```ts -{ - results: array | null, // [] if empty, or null if it does not apply - success: boolean, // true if the operation was successful, false otherwise - meta: { - duration: number, // duration of the operation in milliseconds - rows_read: number, // the number of rows read (scanned) by this query - rows_written: number // the number of rows written by this query - } -} +### Automatic Application + +SQL migrations in `server/database/migrations/*.sql` now automatically apply when you: +- Start the development server (`npx nuxt dev` or [`npx nuxt dev --remote`](/docs/getting-started/remote-storage)) +- Preview builds locally ([`npx nuxthub preview`](/changelog/nuxthub-preview)) +- Deploy via [`npx nuxthub deploy`](/docs/getting-started/deploy#nuxthub-cli) or [Cloudflare Pages CI](/docs/getting-started/deploy#cloudflare-pages-ci) + +::tip +All applied migrations are tracked in the `_hub_migrations` database table. +:: + +### Creating Migrations + +Generate a new migration file using: + +```bash [Terminal] +npx nuxthub database migrations create ``` -::callout -Read more on [Cloudflare D1 documentation](https://developers.cloudflare.com/d1/build-databases/query-databases/). +::important +Migration names must only contain alphanumeric characters and `-` (spaces are converted to `-`). :: + +Migration files are created in `server/database/migrations/`. + +```bash [Example] +> npx nuxthub database migrations create create-todos +✔ Created ./server/database/migrations/0001_create-todos.sql +``` + +After creation, add your SQL queries to modify the database schema. + + +::note{to="/docs/recipes/drizzle#npm-run-dbgenerate"} +With [Drizzle ORM](/docs/recipes/drizzle), migrations are automatically created when you run `npx drizzle-kit generate`. +:: + +### Checking Migration Status + +View pending and applied migrations across environments: + +```bash [Terminal] +# Local environment status +npx nuxthub database migrations list + +# Preview environment status +npx nuxthub database migrations list --preview + +# Production environment status +npx nuxthub database migrations list --production +``` + +```bash [Example output] +> npx nuxthub database migrations list --production +ℹ Connected to project atidone. +ℹ Using https://todos.nuxt.dev to retrieve migrations. +✔ Found 1 migration on atidone... +✅ ./server/database/migrations/0001_create-todos.sql 10/25/2024, 2:43:32 PM +🕒 ./server/database/migrations/0002_create-users.sql Pending +``` + +### Marking Migrations as Applied + +For databases with existing migrations, prevent NuxtHub from rerunning them by marking them as applied: + +```bash [Terminal] +# Mark applied in local environment +npx nuxthub database migrations mark-all-applied + +# Mark applied in preview environment +npx nuxthub database migrations mark-all-applied --preview + +# Mark applied in production environment +npx nuxthub database migrations mark-all-applied --production +``` + +::collapsible{name="self-hosting docs"} +When [self-hosting](/docs/getting-started/deploy#self-hosted), set these environment variables before running commands: :br :br + +```bash [Terminal] +NUXT_HUB_PROJECT_URL= NUXT_HUB_PROJECT_SECRET_KEY= nuxthub database migrations mark-all-applied +``` +:: + +### Migrating from Drizzle ORM + +Since NuxtHub doesn't recognize previously applied Drizzle ORM migrations (stored in `__drizzle_migrations`), it will attempt to rerun all migrations in `server/database/migrations/*.sql`. To prevent this: + +1. Mark existing migrations as applied in each environment: + + ```bash [Terminal] + # Local environment + npx nuxthub database migrations mark-all-applied + + # Preview environment + npx nuxthub database migrations mark-all-applied --preview + + # Production environment + npx nuxthub database migrations mark-all-applied --production + ``` + +2. Remove `server/plugins/database.ts` as it's no longer needed. diff --git a/docs/content/1.docs/3.recipes/1.hooks.md b/docs/content/1.docs/3.recipes/1.hooks.md index e011c1e5..7c8cf9cc 100644 --- a/docs/content/1.docs/3.recipes/1.hooks.md +++ b/docs/content/1.docs/3.recipes/1.hooks.md @@ -5,26 +5,18 @@ description: Use lifecycle hooks to stay synced with NuxtHub. ## `onHubReady()` -Use `onHubReady()` to ensure the execution of some code once NuxtHub environment bindings are set up. +Use `onHubReady()` to ensure the execution of some code once NuxtHub environment bindings are set up and database migrations are applied. ::note -`onHubReady()` is a shortcut using the [`hubHooks`](#hubhooks) object under the hood to listen to the `bindings:ready` event. +`onHubReady()` is a shortcut using the [`hubHooks`](#hubhooks) object under the hood that listens to the `bindings:ready` and `database:migrations:done` events. :: -This is useful to run database migrations inside your [server/plugins/](https://nuxt.com/docs/guide/directory-structure/server#server-plugins). - ```ts [server/plugins/migrations.ts] export default defineNitroPlugin(() => { - // Only run migrations in development + // Only run in development if (import.meta.dev) { onHubReady(async () => { - await hubDatabase().exec(` - CREATE TABLE IF NOT EXISTS todos ( - id INTEGER PRIMARY KEY, - title TEXT NOT NULL, - completed INTEGER NOT NULL DEFAULT 0 - ) - `.replace(/\n/g, '')) + console.log('NuxtHub is ready! 🚀') }) } }) @@ -40,12 +32,13 @@ The `hubHooks` object is a collection of hooks that can be used to stay synced w ```ts [Signature] export interface HubHooks { 'bindings:ready': () => any | void + 'database:migrations:done': () => any | void } ``` ### Usage -You can use the `hubHooks` object to listen to the `bindings:ready` event in your server plugins: +You can use the `hubHooks` object to listen to `HubHooks` events in your server plugins: ```ts [server/plugins/hub.ts] export default defineNitroPlugin(() => { @@ -53,10 +46,15 @@ export default defineNitroPlugin(() => { console.log('NuxtHub bindings are ready!') const db = hubDatabase() }) + // Only run in development and if the database is enabled + if (import.meta.dev) { + hubHooks.hook('database:migrations:done', () => { + console.log('Database migrations are done!') + }) + } }) ``` ::note Note that `hubHooks` is a [hookable](https://hookable.unjs.io) instance. :: - \ No newline at end of file diff --git a/docs/content/1.docs/3.recipes/2.drizzle.md b/docs/content/1.docs/3.recipes/2.drizzle.md index 1b07fef1..bf3e1412 100644 --- a/docs/content/1.docs/3.recipes/2.drizzle.md +++ b/docs/content/1.docs/3.recipes/2.drizzle.md @@ -92,35 +92,10 @@ When running the `npm run db:generate` command, `drizzle-kit` will generate the ### Migrations -We can create a server plugin to run the migrations in development automatically: - -```ts [server/plugins/migrations.ts] -import { consola } from 'consola' -import { migrate } from 'drizzle-orm/d1/migrator' - -export default defineNitroPlugin(async () => { - if (!import.meta.dev) return - - onHubReady(async () => { - await migrate(useDrizzle(), { migrationsFolder: 'server/database/migrations' }) - .then(() => { - consola.success('Database migrations done') - }) - .catch((err) => { - consola.error('Database migrations failed', err) - }) - }) -}) -``` - -::callout -Drizzle will create a `__drizzle_migrations` table in your database to keep track of the applied migrations. It will also run the migrations automatically in development mode. -:: - -To apply the migrations in staging or production, you can run the server using `npx nuxi dev --remote` command to connect your local server to the remote database, learn more about [remote storage](/docs/getting-started/remote-storage). +Migrations created with `npm run db:generate` are automatically applied during deployment, preview and when starting the development server. -::note -We are planning to update this section to leverage [Nitro Tasks](https://nitro.unjs.io/guide/tasks) instead of a server plugin in the future. +::note{to="/docs/features/database#migrations"} +Learn more about migrations. :: ### `useDrizzle()` diff --git a/docs/content/4.changelog/database-migrations.md b/docs/content/4.changelog/database-migrations.md new file mode 100644 index 00000000..dc9b699d --- /dev/null +++ b/docs/content/4.changelog/database-migrations.md @@ -0,0 +1,88 @@ +--- +title: Automatic Database Migrations +description: "Database migrations now automatically apply during development and deployment." +date: 2024-10-25 +image: '/images/changelog/database-migrations.png' +category: Core +authors: + - name: Rihan Arfan + avatar: + src: https://avatars.githubusercontent.com/u/20425781?v=4 + to: https://x.com/RihanArfan + username: RihanArfan + - name: Sebastien Chopin + avatar: + src: https://avatars.githubusercontent.com/u/904724?v=4 + to: https://x.com/atinux + username: atinux +--- + +::tip +This feature is available on both [free and pro plans](/pricing) starting with [`@nuxthub/core >= v0.8.0`](https://github.com/nuxt-hub/core/releases). +:: + +We're excited to introduce automatic [database migrations](/docs/features/database#migrations) in NuxtHub. + +### Automatic Migration Application + +SQL migrations in `server/database/migrations/*.sql` now automatically apply when you: +- Start the development server (`npx nuxt dev` or [`npx nuxt dev --remote`](/docs/getting-started/remote-storage)) +- Preview builds locally ([`npx nuxthub preview`](/changelog/nuxthub-preview)) +- Deploy via [`npx nuxthub deploy`](/docs/getting-started/deploy#nuxthub-cli) or [Cloudflare Pages CI](/docs/getting-started/deploy#cloudflare-pages-ci) + +Starting now, when you clone any of [our templates](/templates) with a database, all migrations apply automatically! + +::note{to="/docs/features/database#migrations"} +Learn more about database migrations in our **full documentation**. +:: + +## New CLI Commands + +[`nuxthub@0.7.0`](https://github.com/nuxt-hub/cli) introduces these database migration commands: + +```bash [Terminal] +# Create a new migration +npx nuxthub database migrations create + +# View migration status +npx nuxthub database migrations list + +# Mark all migrations as applied +npx nuxthub database migrations mark-all-applied +``` + +Learn more about: +- [Creating migrations](/docs/features/database#creating-migrations) +- [Checking migration status](/docs/features/database#checking-migration-status) +- [Marking migrations as applied](/docs/features/database#marking-migrations-as-applied) + +## Migrating from Existing ORMs + +::important +**Current Drizzle ORM users:** Follow these specific migration steps. +:: + +Since NuxtHub doesn't recognize previously applied Drizzle ORM migrations (stored in `__drizzle_migrations`), it will attempt to rerun all migrations in `server/database/migrations/*.sql`. To prevent this: + +1. Mark existing migrations as applied in each environment: + + ```bash [Terminal] + # Local environment + npx nuxthub database migrations mark-all-applied + + # Preview environment + npx nuxthub database migrations mark-all-applied --preview + + # Production environment + npx nuxthub database migrations mark-all-applied --production + ``` + +2. Remove `server/plugins/database.ts` as it's no longer needed. + +## Understanding Database Migrations + +Database migrations provide version control for your database schema. They track changes and ensure consistent schema evolution across all environments through incremental updates. + +::note +Implemented in [nuxt-hub/core#333](https://github.com/nuxt-hub/core/pull/333) and [nuxt-hub/cli#31](https://github.com/nuxt-hub/cli/pull/31). +:: diff --git a/docs/public/images/changelog/database-migrations.png b/docs/public/images/changelog/database-migrations.png new file mode 100644 index 00000000..53b5fb79 Binary files /dev/null and b/docs/public/images/changelog/database-migrations.png differ diff --git a/playground/server/database/migrations/0001_create-todos.sql b/playground/server/database/migrations/0001_create-todos.sql new file mode 100644 index 00000000..766c2015 --- /dev/null +++ b/playground/server/database/migrations/0001_create-todos.sql @@ -0,0 +1,7 @@ +-- Migration number: 0001 2024-10-24T00:25:12.371Z +CREATE TABLE todos ( + id integer PRIMARY KEY NOT NULL, + title text NOT NULL, + completed integer DEFAULT 0 NOT NULL, + created_at integer NOT NULL +); diff --git a/playground/server/plugins/migrations.ts b/playground/server/plugins/migrations.ts deleted file mode 100644 index 3dd52e89..00000000 --- a/playground/server/plugins/migrations.ts +++ /dev/null @@ -1,14 +0,0 @@ -export default defineNitroPlugin(() => { - if (import.meta.dev) { - onHubReady(async () => { - const db = useDrizzle() - - await db.run(sql`CREATE TABLE IF NOT EXISTS todos ( - id integer PRIMARY KEY NOT NULL, - title text NOT NULL, - completed integer DEFAULT 0 NOT NULL, - created_at integer NOT NULL - );`) - }) - } -}) diff --git a/src/features.ts b/src/features.ts index 8ce9f515..fd09dbb2 100644 --- a/src/features.ts +++ b/src/features.ts @@ -51,12 +51,14 @@ export interface HubConfig { vectorize?: HubConfig['vectorize'] } & Record } + + migrationsPath?: string } export function setupBase(nuxt: Nuxt, hub: HubConfig) { // Add Server scanning addServerScanDir(resolve('./runtime/base/server')) - addServerImportsDir(resolve('./runtime/base/server/utils')) + addServerImportsDir([resolve('./runtime/base/server/utils'), resolve('./runtime/base/server/utils/migrations')]) // Add custom tabs to Nuxt DevTools if (nuxt.options.dev) { @@ -183,7 +185,9 @@ export async function setupCache(nuxt: Nuxt) { addServerScanDir(resolve('./runtime/cache/server')) } -export function setupDatabase(_nuxt: Nuxt) { +export function setupDatabase(nuxt: Nuxt, hub: HubConfig) { + // Keep track of the path to migrations + hub.migrationsPath = join(nuxt.options.rootDir, 'server/database/migrations') // Add Server scanning addServerScanDir(resolve('./runtime/database/server')) addServerImportsDir(resolve('./runtime/database/server/utils')) diff --git a/src/module.ts b/src/module.ts index 15a931f6..fc631938 100644 --- a/src/module.ts +++ b/src/module.ts @@ -108,7 +108,7 @@ export default defineNuxtModule({ hub.blob && setupBlob(nuxt) hub.browser && await setupBrowser(nuxt) hub.cache && await setupCache(nuxt) - hub.database && setupDatabase(nuxt) + hub.database && setupDatabase(nuxt, hub as HubConfig) hub.kv && setupKV(nuxt) Object.keys(hub.vectorize!).length && setupVectorize(nuxt, hub as HubConfig) diff --git a/src/runtime/base/server/utils/hooks.ts b/src/runtime/base/server/utils/hooks.ts index 9f61d484..dbc22d98 100644 --- a/src/runtime/base/server/utils/hooks.ts +++ b/src/runtime/base/server/utils/hooks.ts @@ -1,14 +1,16 @@ import { createHooks } from 'hookable' +import { useRuntimeConfig } from '#imports' export interface HubHooks { 'bindings:ready': () => void + 'database:migrations:done': () => void } /** * Access Hub lifecycle hooks. * * @example ```ts - * hubHooks.on('bindings:ready', () => { + * hubHooks.hook('bindings:ready', () => { * console.log('Bindings are ready!') * }) * ``` @@ -28,6 +30,8 @@ export const hubHooks = createHooks() */ export function onHubReady(cb: HubHooks['bindings:ready']) { if (import.meta.dev) { + const hub = useRuntimeConfig().hub + if (hub.database) return hubHooks.hookOnce('database:migrations:done', cb) return hubHooks.hookOnce('bindings:ready', cb) } cb() diff --git a/src/runtime/database/server/plugins/migrations.dev.ts b/src/runtime/database/server/plugins/migrations.dev.ts new file mode 100644 index 00000000..5b388cf2 --- /dev/null +++ b/src/runtime/database/server/plugins/migrations.dev.ts @@ -0,0 +1,21 @@ +import { applyRemoteMigrations } from '../utils/migrations/remote' +import { hubHooks } from '../../../base/server/utils/hooks' +import { applyMigrations } from '../utils/migrations/migrations' +import { useRuntimeConfig, defineNitroPlugin } from '#imports' + +export default defineNitroPlugin(async () => { + if (!import.meta.dev) return + + const hub = useRuntimeConfig().hub + if (!hub.database) return + + hubHooks.hookOnce('bindings:ready', async () => { + if (hub.remote && hub.projectKey) { // linked to a NuxtHub project + await applyRemoteMigrations(hub) + } else { // local dev & self hosted + await applyMigrations(hub) + } + + await hubHooks.callHookParallel('database:migrations:done') + }) +}) diff --git a/src/runtime/database/server/utils/migrations/helpers.ts b/src/runtime/database/server/utils/migrations/helpers.ts new file mode 100644 index 00000000..e476643d --- /dev/null +++ b/src/runtime/database/server/utils/migrations/helpers.ts @@ -0,0 +1,52 @@ +import { createStorage } from 'unstorage' +import fsDriver from 'unstorage/drivers/fs' +import type { HubConfig } from '../../../../../features' + +export function useMigrationsStorage(hub: HubConfig) { + return createStorage({ + driver: fsDriver({ + base: hub.migrationsPath, + ignore: ['.DS_Store'] + }) + }) +} + +export async function getMigrationFiles(hub: HubConfig) { + const fileKeys = await useMigrationsStorage(hub).getKeys() + return fileKeys.filter(file => file.endsWith('.sql')) +} + +export const CreateMigrationsTableQuery = `CREATE TABLE IF NOT EXISTS _hub_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +);` + +export const AppliedMigrationsQuery = 'select "id", "name", "applied_at" from "_hub_migrations" order by "_hub_migrations"."id"' + +export function splitSqlQueries(sqlFileContent: string): string[] { + // Remove all inline comments (-- ...) + let content = sqlFileContent.replace(/--.*$/gm, '') + + // Remove all multi-line comments (/* ... */) + content = content.replace(/\/\*[\s\S]*?\*\//g, '') + + // Split by semicolons but keep them in the result + const rawQueries = content.split(/(?<=;)/) + + // Process each query + return rawQueries + .map(query => query.trim()) // Remove whitespace + .filter((query) => { + // Remove empty queries and standalone semicolons + return query !== '' && query !== ';' + }) + .map((query) => { + // Ensure each query ends with exactly one semicolon + if (!query.endsWith(';')) { + query += ';' + } + // Remove multiple semicolons at the end + return query.replace(/;+$/, ';') + }) +} diff --git a/src/runtime/database/server/utils/migrations/migrations.ts b/src/runtime/database/server/utils/migrations/migrations.ts new file mode 100644 index 00000000..a7fea6cf --- /dev/null +++ b/src/runtime/database/server/utils/migrations/migrations.ts @@ -0,0 +1,38 @@ +import log from 'consola' +import { hubDatabase } from '../database' +import type { HubConfig } from '../../../../../features' +import { AppliedMigrationsQuery, CreateMigrationsTableQuery, getMigrationFiles, splitSqlQueries, useMigrationsStorage } from './helpers' + +// Apply migrations during local development and self-hosted remote development. +// See src/utils/migrations/remote.ts for applying migrations on remote development (linked projects) and Pages CI deployments +export async function applyMigrations(hub: HubConfig) { + const migrationsStorage = useMigrationsStorage(hub) + const db = hubDatabase() + + const appliedMigrations = (await db.prepare(AppliedMigrationsQuery).all()).results + const localMigrations = (await getMigrationFiles(hub)).map(fileName => fileName.replace('.sql', '')) + const pendingMigrations = localMigrations.filter(localName => !appliedMigrations.find(({ name }) => name === localName)) + if (!pendingMigrations.length) return log.success('Database migrations up to date') + + for (const migration of pendingMigrations) { + let query = await migrationsStorage.getItem(`${migration}.sql`) + if (!query) continue + query += ` + ${CreateMigrationsTableQuery} + INSERT INTO _hub_migrations (name) values ('${migration}'); + ` + const queries = splitSqlQueries(query) + + try { + await db.batch(queries.map(q => db.prepare(q))) + } catch (error: any) { + log.error(`Failed to apply migration \`./server/database/migrations/${migration}.sql\`\n`, error?.message) + if (error?.message?.includes('already exists')) { + log.info('If your database already contains the migration, run `npx nuxthub database migrations mark-all-applied` to mark all migrations as applied.') + } + break + } + + log.success(`Database migration \`./server/database/migrations/${migration}.sql\` applied`) + } +} diff --git a/src/runtime/database/server/utils/migrations/remote.ts b/src/runtime/database/server/utils/migrations/remote.ts new file mode 100644 index 00000000..60214068 --- /dev/null +++ b/src/runtime/database/server/utils/migrations/remote.ts @@ -0,0 +1,71 @@ +import log from 'consola' +import { $fetch } from 'ofetch' +import type { HubConfig } from '../../../../../features' +import { AppliedMigrationsQuery, CreateMigrationsTableQuery, getMigrationFiles, useMigrationsStorage } from './helpers' + +export async function applyRemoteMigrations(hub: HubConfig) { + const srcStorage = useMigrationsStorage(hub) + let appliedMigrations = [] + try { + appliedMigrations = await fetchRemoteMigrations(hub) + } catch (error: any) { + log.error(`Could not fetch applied migrations: ${error.response?._data?.message}`) + return false + } + const localMigrations = (await getMigrationFiles(hub)).map(fileName => fileName.replace('.sql', '')) + const pendingMigrations = localMigrations.filter(localName => !appliedMigrations.find(({ name }) => name === localName)) + + if (!pendingMigrations.length) { + log.success('Database migrations up to date') + return true + } + + for (const migration of pendingMigrations) { + let query = await srcStorage.getItem(`${migration}.sql`) + if (!query) continue + if (query.replace(/\s$/, '').at(-1) !== ';') query += ';' // ensure previous statement ended before running next query + query += ` + ${CreateMigrationsTableQuery} + INSERT INTO _hub_migrations (name) values ('${migration}'); + ` + + try { + await queryRemoteDatabase(hub, query) + } catch (error: any) { + log.error(`Failed to apply migration \`./server/database/migrations/${migration}.sql\`: ${error.response?._data?.message}`) + if (error.response?._data?.message?.includes('already exists')) { + log.info(`To mark all migrations as already applied, run: \`npx nuxthub database migrations mark-all-applied --${hub.env}\``) + } + return false + } + + log.success(`Database migration \`./server/database/migrations/${migration}.sql\` applied`) + log.success('Database migrations up to date') + return true + } +} + +export async function queryRemoteDatabase(hub: HubConfig, query: string) { + return await $fetch, success: boolean, meta: object }>>(`/api/projects/${hub.projectKey}/database/${hub.env}/query`, { + baseURL: hub.url, + method: 'POST', + headers: { + authorization: `Bearer ${process.env.NUXT_HUB_PROJECT_DEPLOY_TOKEN || hub.userToken}` + }, + body: { query } + }) +} + +export async function createRemoteMigrationsTable(hub: HubConfig) { + await queryRemoteDatabase(hub, CreateMigrationsTableQuery) +} + +export async function fetchRemoteMigrations(hub: HubConfig) { + const res = await queryRemoteDatabase<{ id: number, name: string, applied_at: string }>(hub, AppliedMigrationsQuery).catch((error) => { + if (error.response?._data?.message.includes('no such table')) { + return [] + } + throw error + }) + return res[0]?.results ?? [] +} diff --git a/src/runtime/ready.dev.ts b/src/runtime/ready.dev.ts index 93656275..2d68a419 100644 --- a/src/runtime/ready.dev.ts +++ b/src/runtime/ready.dev.ts @@ -3,7 +3,6 @@ import { defineNitroPlugin } from '#imports' export default defineNitroPlugin(async () => { // Wait for nitro-cloudflare-dev to be ready - // @ts-except-error globalThis.__env__ not yet typed await (globalThis as any).__env__ await hubHooks.callHookParallel('bindings:ready') }) diff --git a/src/utils/build.ts b/src/utils/build.ts index b4cfca86..9c56ab9c 100644 --- a/src/utils/build.ts +++ b/src/utils/build.ts @@ -1,9 +1,10 @@ -import { writeFile } from 'node:fs/promises' +import { writeFile, cp } from 'node:fs/promises' import { logger } from '@nuxt/kit' import { join } from 'pathe' import { $fetch } from 'ofetch' import type { Nuxt } from '@nuxt/schema' import type { HubConfig } from '../features' +import { applyRemoteMigrations } from '../runtime/database/server/utils/migrations/remote' const log = logger.withTag('nuxt:hub') @@ -90,11 +91,16 @@ export function addBuildHooks(nuxt: Nuxt, hub: HubConfig) { if (e.response?._data?.message) { log.error(e.response._data.message) } else { - log.error('Failed run build:done hook on NuxtHub.', e) + log.error('Failed run compiled:done hook on NuxtHub.', e) } process.exit(1) }) + // Apply migrations + const migrationsApplied = await applyRemoteMigrations(hub) + if (!migrationsApplied) { + process.exit(1) + } }) }) } else { @@ -112,6 +118,17 @@ export function addBuildHooks(nuxt: Nuxt, hub: HubConfig) { bindings: hub.bindings } await writeFile(join(nitro.options.output.publicDir, 'hub.config.json'), JSON.stringify(hubConfig, null, 2), 'utf-8') + + if (hub.database) { + try { + await cp(join(nitro.options.rootDir, 'server/database/migrations'), join(nitro.options.output.dir, 'database/migrations'), { recursive: true }) + log.info('Database migrations included in build') + } catch (error: unknown) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + log.info('Skipping bundling database migrations - no migrations found') + } + } + } }) } }