diff --git a/src/presets/nuxthub.ts b/src/presets/nuxthub.ts index 270c1afe2..77bd3055d 100644 --- a/src/presets/nuxthub.ts +++ b/src/presets/nuxthub.ts @@ -5,10 +5,33 @@ import { provider } from 'std-env' import { logger } from '../utils/dev' import { definePreset } from '../utils/preset' import type { Nuxt } from 'nuxt/schema' -import type { LibSQLDatabaseConfig, PGliteDatabaseConfig, SqliteDatabaseConfig } from '~/dist/module.mjs' +import type { D1DatabaseConfig, LibSQLDatabaseConfig, PGliteDatabaseConfig, PostgreSQLDatabaseConfig, SqliteDatabaseConfig } from '~/dist/module.mjs' import cloudflarePreset from './cloudflare' import nodePreset from './node' +type ContentDatabaseConfig = D1DatabaseConfig | SqliteDatabaseConfig | PostgreSQLDatabaseConfig | LibSQLDatabaseConfig | PGliteDatabaseConfig + +// Map the resolved NuxtHub database config (`runtimeConfig.hub.db`) to a Nuxt Content database config +export function hubDatabaseToContentDatabase(hubDb: { driver: string, connection?: { url?: string, [key: string]: unknown } }): ContentDatabaseConfig | undefined { + if (hubDb.driver === 'd1') { + return { type: 'd1', bindingName: 'DB' } + } + if (['node-postgres', 'postgres-js', 'neon-http', 'postgres', 'postgresql'].includes(hubDb.driver)) { + return typeof hubDb.connection?.url === 'string' && hubDb.connection.url ? { type: 'postgresql', url: hubDb.connection.url } : undefined + } + if (['sqlite', 'better-sqlite3'].includes(hubDb.driver)) { + if (typeof hubDb.connection?.filename === 'string' && hubDb.connection.filename) { + return { type: 'sqlite', filename: hubDb.connection.filename } + } + const filename = typeof hubDb.connection?.url === 'string' ? hubDb.connection.url.replace(/^file:/, '') : '' + return filename ? { type: 'sqlite', filename } : undefined + } + if (['libsql', 'pglite'].includes(hubDb.driver)) { + return { type: hubDb.driver, ...hubDb.connection } as unknown as ContentDatabaseConfig + } + return undefined +} + export default definePreset({ name: 'nuxthub', async setup(options, nuxt, config) { @@ -21,15 +44,14 @@ export default definePreset({ if (nuxtOptions.hub?.database === true) { options.database ||= { type: 'd1', bindingName: 'DB' } } - else if (typeof nuxtOptions.hub?.db === 'string' && typeof hubDb === 'object') { - if (hubDb.driver === 'd1') { - options.database ||= { type: 'd1', bindingName: 'DB' } - } - else if (hubDb.driver === 'node-postgres') { - options.database ||= { type: 'postgresql', url: hubDb.connection.url as string } + // NuxtHub >= 0.10, `hub.db` can be a string or an object + else if (nuxtOptions.hub?.db && typeof hubDb === 'object' && !options.database) { + const database = hubDatabaseToContentDatabase(hubDb) + if (database) { + options.database = database } else { - options.database ||= { type: hubDb.driver as 'sqlite' | 'postgresql' | 'postgres' | 'libsql' | 'pglite', ...hubDb.connection } as unknown as SqliteDatabaseConfig | LibSQLDatabaseConfig | PGliteDatabaseConfig + logger.warn(`Nuxt Content cannot use the NuxtHub \`${hubDb.driver}\` database configuration, using the default database instead.`) } } } @@ -62,16 +84,10 @@ export default definePreset({ nitroConfig.runtimeConfig!.content!.database = { type: 'd1', bindingName: 'DB' } } } - else if (typeof nuxt.options.hub?.db === 'string' && typeof hubConfig.db === 'object') { - const hubDb = hubConfig.db as unknown as { driver: string, connection: object } - if (hubDb.driver === 'd1') { - nitroConfig.runtimeConfig!.content!.database ||= { type: 'd1', bindingName: 'DB' } - } - else if (hubDb.driver === 'node-postgres') { - nitroConfig.runtimeConfig!.content!.database ||= { type: 'postgresql', ...hubDb.connection } - } - else { - nitroConfig.runtimeConfig!.content!.database ||= { type: hubDb.driver, ...hubDb.connection } + else if (nuxt.options.hub?.db && typeof hubConfig.db === 'object') { + const database = hubDatabaseToContentDatabase(hubConfig.db as unknown as { driver: string, connection?: { url?: string } }) + if (database) { + nitroConfig.runtimeConfig!.content!.database ||= database } } diff --git a/test/unit/nuxthubPreset.test.ts b/test/unit/nuxthubPreset.test.ts new file mode 100644 index 000000000..dbed9f862 --- /dev/null +++ b/test/unit/nuxthubPreset.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, test, vi } from 'vitest' +import type { Nuxt } from '@nuxt/schema' +import type { Resolver } from '@nuxt/kit' +import type { NitroConfig } from 'nitropack' +import nuxthubPreset, { hubDatabaseToContentDatabase } from '../../src/presets/nuxthub' +import type { ModuleOptions } from '../../src/types/module' +import type { Manifest } from '../../src/types/manifest' + +// `addTemplate` and `addServerHandler` require a live Nuxt context +vi.mock('@nuxt/kit', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + addTemplate: vi.fn(() => ({ dst: '' })), + addServerHandler: vi.fn(), + } +}) + +const resolver = { resolve: (p: string) => p } as unknown as Resolver +const manifest = { collections: [], dump: {} } as unknown as Manifest +const opts = { resolver, manifest } + +function createNuxt(hub: Record, runtimeHub: Record): Nuxt { + return { + options: { + dev: false, + hub, + nitro: { preset: 'node-server' }, + runtimeConfig: { hub: runtimeHub }, + }, + } as unknown as Nuxt +} + +describe('hubDatabaseToContentDatabase', () => { + test('maps d1 driver to d1 database', () => { + expect(hubDatabaseToContentDatabase({ driver: 'd1' })).toEqual({ type: 'd1', bindingName: 'DB' }) + }) + + test('maps postgres drivers to postgresql database', () => { + for (const driver of ['node-postgres', 'postgres-js', 'neon-http', 'postgres', 'postgresql']) { + expect(hubDatabaseToContentDatabase({ driver, connection: { url: 'postgres://localhost' } })) + .toEqual({ type: 'postgresql', url: 'postgres://localhost' }) + } + }) + + test('maps sqlite drivers to sqlite database', () => { + for (const driver of ['sqlite', 'better-sqlite3']) { + expect(hubDatabaseToContentDatabase({ driver, connection: { url: 'file:.data/hub/db/sqlite.db' } })) + .toEqual({ type: 'sqlite', filename: '.data/hub/db/sqlite.db' }) + } + }) + + test('keeps an explicit sqlite filename', () => { + expect(hubDatabaseToContentDatabase({ driver: 'sqlite', connection: { filename: './contents.sqlite' } })) + .toEqual({ type: 'sqlite', filename: './contents.sqlite' }) + }) + + test('maps libsql driver with its connection', () => { + expect(hubDatabaseToContentDatabase({ driver: 'libsql', connection: { url: 'file:.data/hub/db/sqlite.db' } })) + .toEqual({ type: 'libsql', url: 'file:.data/hub/db/sqlite.db' }) + }) + + test('maps pglite driver with its connection', () => { + expect(hubDatabaseToContentDatabase({ driver: 'pglite', connection: { dataDir: '.data/hub/db/pglite' } })) + .toEqual({ type: 'pglite', dataDir: '.data/hub/db/pglite' }) + }) + + test('returns undefined for unsupported drivers', () => { + expect(hubDatabaseToContentDatabase({ driver: 'mysql2', connection: { uri: 'mysql://localhost' } })).toBeUndefined() + }) + + test('returns undefined when required connection values are missing', () => { + expect(hubDatabaseToContentDatabase({ driver: 'postgres-js' })).toBeUndefined() + expect(hubDatabaseToContentDatabase({ driver: 'postgres-js', connection: { url: '' } })).toBeUndefined() + expect(hubDatabaseToContentDatabase({ driver: 'sqlite' })).toBeUndefined() + expect(hubDatabaseToContentDatabase({ driver: 'sqlite', connection: {} })).toBeUndefined() + }) + + test('returns undefined for non-string connection values', () => { + expect(hubDatabaseToContentDatabase({ driver: 'postgres-js', connection: { url: 123 as unknown as string } })).toBeUndefined() + expect(hubDatabaseToContentDatabase({ driver: 'sqlite', connection: { filename: true, url: 123 as unknown as string } })).toBeUndefined() + expect(hubDatabaseToContentDatabase({ driver: 'sqlite', connection: { filename: true, url: 'file:.data/hub/db/sqlite.db' } })) + .toEqual({ type: 'sqlite', filename: '.data/hub/db/sqlite.db' }) + }) +}) + +describe('nuxthub preset setup', () => { + const resolvedSqliteDb = { driver: 'libsql', connection: { url: 'file:.data/hub/db/sqlite.db' } } + + test('maps the string form of hub.db', async () => { + const options = {} as ModuleOptions + await nuxthubPreset.setup!(options, createNuxt({ db: 'sqlite' }, { db: resolvedSqliteDb }), opts) + + expect(options.database).toEqual({ type: 'libsql', url: 'file:.data/hub/db/sqlite.db' }) + }) + + test('maps the object form of hub.db', async () => { + const options = {} as ModuleOptions + await nuxthubPreset.setup!(options, createNuxt({ db: { dialect: 'sqlite', applyMigrationsDuringBuild: false } }, { db: resolvedSqliteDb }), opts) + + expect(options.database).toEqual({ type: 'libsql', url: 'file:.data/hub/db/sqlite.db' }) + }) + + test('does not override an explicitly configured database', async () => { + const options = { database: { type: 'libsql', url: 'file:/tmp/sqlite.db' } } as ModuleOptions + await nuxthubPreset.setup!(options, createNuxt({ db: { dialect: 'sqlite' } }, { db: resolvedSqliteDb }), opts) + + expect(options.database).toEqual({ type: 'libsql', url: 'file:/tmp/sqlite.db' }) + }) + + test('leaves the database unset for unsupported drivers', async () => { + const options = {} as ModuleOptions + await nuxthubPreset.setup!(options, createNuxt({ db: { dialect: 'mysql' } }, { db: { driver: 'mysql2', connection: { uri: 'mysql://localhost' } } }), opts) + + expect(options.database).toBeUndefined() + }) + + test('uses d1 with NuxtHub <= 0.9', async () => { + const options = {} as ModuleOptions + await nuxthubPreset.setup!(options, createNuxt({ database: true }, { database: true }), opts) + + expect(options.database).toEqual({ type: 'd1', bindingName: 'DB' }) + }) +}) + +describe('nuxthub preset setupNitro', () => { + test('maps the object form of hub.db and rewrites local libsql to /tmp', async () => { + const nuxt = createNuxt( + { db: { dialect: 'sqlite', applyMigrationsDuringBuild: false } }, + { db: { driver: 'libsql', connection: { url: 'file:.data/hub/db/sqlite.db' }, applyMigrationsDuringBuild: false } }, + ) + const nitroConfig = { runtimeConfig: { content: {} }, rootDir: '/' } as unknown as NitroConfig + await nuxthubPreset.setupNitro(nitroConfig, { ...opts, moduleOptions: {} as ModuleOptions, nuxt }) + + expect(nitroConfig.runtimeConfig!.content!.database).toEqual({ type: 'libsql', url: 'file:/tmp/sqlite.db' }) + expect(nitroConfig.runtimeConfig!.content!.integrityCheck).toBe(true) + }) + + test('maps the string form of hub.db to the same database', async () => { + const nuxt = createNuxt( + { db: 'sqlite' }, + { db: { driver: 'libsql', connection: { url: 'file:.data/hub/db/sqlite.db' }, applyMigrationsDuringBuild: false } }, + ) + const nitroConfig = { runtimeConfig: { content: {} }, rootDir: '/' } as unknown as NitroConfig + await nuxthubPreset.setupNitro(nitroConfig, { ...opts, moduleOptions: {} as ModuleOptions, nuxt }) + + expect(nitroConfig.runtimeConfig!.content!.database).toEqual({ type: 'libsql', url: 'file:/tmp/sqlite.db' }) + expect(nitroConfig.runtimeConfig!.content!.integrityCheck).toBe(true) + }) +})