From a66455f55815d8919a8c923547e1774f932247ea Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 09:28:28 -0700 Subject: [PATCH 01/35] feat(db): add support for non-node libsql client --- .changeset/full-dingos-repeat.md | 17 ++++++++++ packages/db/src/core/integration/index.ts | 28 +++++++++++++++-- .../db/src/core/integration/vite-plugin-db.ts | 5 +++ packages/db/src/runtime/db-client.ts | 31 +++++++++++++++++-- 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 .changeset/full-dingos-repeat.md diff --git a/.changeset/full-dingos-repeat.md b/.changeset/full-dingos-repeat.md new file mode 100644 index 000000000000..4d34a9116c40 --- /dev/null +++ b/.changeset/full-dingos-repeat.md @@ -0,0 +1,17 @@ +--- +'@astrojs/db': minor +--- + +Adds support for environments such as Cloudflare or Deno that require a non-node based libsql client. + +To utilize this new feature, you must add the following to your Astro Db config. This will enable the usage of the alterative LibSQL web driver. In most cases this should only be needed on Cloudflare or Deno type environments, and using the default mode `node` will be enough for normal usage. + +```ts +import db from '@astrojs/db'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [db({ mode: 'web' })], +}); +``` \ No newline at end of file diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index adaccda24470..d833545155a5 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -13,6 +13,7 @@ import { type ViteDevServer, } from 'vite'; import parseArgs from 'yargs-parser'; +import { z } from 'zod'; import { AstroDbError, isDbError } from '../../runtime/utils.js'; import { CONFIG_FILE_NAMES, DB_PATH, VIRTUAL_MODULE_ID } from '../consts.js'; import { EXEC_DEFAULT_EXPORT_ERROR, EXEC_ERROR } from '../errors.js'; @@ -28,7 +29,27 @@ import { vitePluginDb, } from './vite-plugin-db.js'; -function astroDBIntegration(): AstroIntegration { +const astroDBConfigSchema = z + .object({ + /** + * Sets the mode of the underlying `@libsql/client` connection. + * + * In most cases, the default 'node' mode is sufficient. On platforms like Cloudflare, or Deno, you may need to set this to 'web'. + * + * @default 'node' + */ + mode: z + .union([z.literal('node'), z.literal('web')]) + .optional() + .default('node'), + }) + .optional() + .default({}); + +export type AstroDBConfig = z.infer; + +function astroDBIntegration(options?: AstroDBConfig): AstroIntegration { + const resolvedConfig = astroDBConfigSchema.parse(options); let connectToRemote = false; let configFileDependencies: string[] = []; let root: URL; @@ -78,6 +99,7 @@ function astroDBIntegration(): AstroIntegration { srcDir: config.srcDir, output: config.output, seedHandler, + mode: resolvedConfig.mode, }); } else { dbPlugin = vitePluginDb({ @@ -182,8 +204,8 @@ function databaseFileEnvDefined() { return env.ASTRO_DATABASE_FILE != null || process.env.ASTRO_DATABASE_FILE != null; } -export function integration(): AstroIntegration[] { - return [astroDBIntegration(), fileURLIntegration()]; +export function integration(options?: AstroDBConfig): AstroIntegration[] { + return [astroDBIntegration(options), fileURLIntegration()]; } async function executeSeedFile({ diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 7394ab046f12..808520820287 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -51,6 +51,7 @@ type VitePluginDBParams = root: URL; output: AstroConfig['output']; seedHandler: SeedHandler; + mode: 'node' | 'web'; }; export function vitePluginDb(params: VitePluginDBParams): VitePlugin { @@ -77,6 +78,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { tables: params.tables.get(), isBuild: command === 'build', output: params.output, + mode: params.mode, }); } @@ -137,11 +139,13 @@ export function getRemoteVirtualModContents({ appToken, isBuild, output, + mode }: { tables: DBTables; appToken: string; isBuild: boolean; output: AstroConfig['output']; + mode: 'node' | 'web'; }) { const dbInfo = getRemoteDatabaseInfo(); @@ -176,6 +180,7 @@ import {asDrizzleTable, createRemoteDatabaseClient} from ${RUNTIME_IMPORT}; export const db = await createRemoteDatabaseClient({ url: ${dbUrlArg()}, token: ${appTokenArg()}, + mode: ${JSON.stringify(mode)}, }); export * from ${RUNTIME_VIRTUAL_IMPORT}; diff --git a/packages/db/src/runtime/db-client.ts b/packages/db/src/runtime/db-client.ts index 35cdd6e51138..4d4eb79cc43a 100644 --- a/packages/db/src/runtime/db-client.ts +++ b/packages/db/src/runtime/db-client.ts @@ -1,6 +1,7 @@ import { createClient, type Config as LibSQLConfig } from '@libsql/client'; -import type { LibSQLDatabase } from 'drizzle-orm/libsql'; -import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; +import { createClient as createClientWeb } from '@libsql/client/web'; +import { drizzle as drizzleLibsql, type LibSQLDatabase } from 'drizzle-orm/libsql'; +import { drizzle as drizzleLibsqlWeb } from 'drizzle-orm/libsql/web'; const isWebContainer = !!process.versions?.webcontainer; @@ -18,12 +19,18 @@ export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQL type RemoteDbClientOptions = { token: string; url: string | URL; + mode: 'node' | 'web'; }; export function createRemoteDatabaseClient(options: RemoteDbClientOptions) { const url = new URL(options.url); - return createRemoteLibSQLClient(options.token, url, options.url.toString()); + switch (options.mode) { + case 'web': + return createRemoteLibSQLWebClient(options.token, url); + case 'node': + return createRemoteLibSQLClient(options.token, url, options.url.toString()); + } } // this function parses the options from a `Record` @@ -69,3 +76,21 @@ function createRemoteLibSQLClient(authToken: string, dbURL: URL, rawUrl: string) const client = createClient({ ...parseOpts(options), url, authToken }); return drizzleLibsql(client); } + +function createRemoteLibSQLWebClient(authToken: string, dbURL: URL) { + const options: Record = Object.fromEntries(dbURL.searchParams.entries()); + dbURL.search = ''; + + let url = dbURL.toString(); + + const supportedProtocols = ['http:', 'https:', 'libsql:']; + + if (!supportedProtocols.includes(dbURL.protocol)) { + throw new Error( + `Unsupported protocol "${dbURL.protocol}" for libSQL web client. Supported protocols are: ${supportedProtocols.join(', ')}.` + ); + } + + const client = createClientWeb({ ...parseOpts(options), url, authToken }); + return drizzleLibsqlWeb(client); +} From 2ddea20c84a9902b249915a9e080d97525f690eb Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 09:42:25 -0700 Subject: [PATCH 02/35] feat(db): update remote database info retrieval to include mode parameter --- packages/db/src/core/cli/commands/execute/index.ts | 4 +++- packages/db/src/core/cli/commands/push/index.ts | 3 ++- packages/db/src/core/cli/commands/shell/index.ts | 2 +- packages/db/src/core/cli/commands/verify/index.ts | 2 +- packages/db/src/core/cli/migration-queries.ts | 1 + packages/db/src/core/integration/index.ts | 3 ++- packages/db/src/core/integration/vite-plugin-db.ts | 9 ++++++--- packages/db/src/core/utils.ts | 4 +++- 8 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts index 140f5f74bd13..1d894e840e3d 100644 --- a/packages/db/src/core/cli/commands/execute/index.ts +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -40,17 +40,19 @@ export async function cmd({ let virtualModContents: string; if (flags.remote) { - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo('node'); virtualModContents = getRemoteVirtualModContents({ tables: dbConfig.tables ?? {}, appToken: flags.token ?? dbInfo.token, isBuild: false, output: 'server', + mode: 'node' }); } else { virtualModContents = getLocalVirtualModContents({ tables: dbConfig.tables ?? {}, root: astroConfig.root, + mode: 'node' }); } const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl }); diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index d1755a56d4ef..e333273fdcd0 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -24,7 +24,7 @@ export async function cmd({ }) { const isDryRun = flags.dryRun; const isForceReset = flags.forceReset; - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo('node'); const productionSnapshot = await getProductionCurrentSnapshot(dbInfo); const currentSnapshot = createCurrentSnapshot(dbConfig); const isFromScratch = !productionSnapshot; @@ -111,6 +111,7 @@ async function pushToDb(requestBody: RequestBody, appToken: string, remoteUrl: s const client = createRemoteDatabaseClient({ token: appToken, url: remoteUrl, + mode: 'node', }); await client.run(sql`create table if not exists _astro_db_snapshot ( diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 941bf2296e8d..5746967c8803 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -24,7 +24,7 @@ export async function cmd({ console.error(SHELL_QUERY_MISSING_ERROR); process.exit(1); } - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo('node'); if (flags.remote) { const db = createRemoteDatabaseClient(dbInfo); const result = await db.run(sql.raw(query)); diff --git a/packages/db/src/core/cli/commands/verify/index.ts b/packages/db/src/core/cli/commands/verify/index.ts index ae9c776090ca..e37b19ca3948 100644 --- a/packages/db/src/core/cli/commands/verify/index.ts +++ b/packages/db/src/core/cli/commands/verify/index.ts @@ -19,7 +19,7 @@ export async function cmd({ flags: Arguments; }) { const isJson = flags.json; - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo('node'); const productionSnapshot = await getProductionCurrentSnapshot(dbInfo); const currentSnapshot = createCurrentSnapshot(dbConfig); const { queries: migrationQueries, confirmations } = await getMigrationQueries({ diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index d0299df80c0e..32e0e44cfa2c 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -437,6 +437,7 @@ async function getDbCurrentSnapshot( const client = createRemoteDatabaseClient({ token: appToken, url: remoteUrl, + mode: 'node', }); try { diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index d833545155a5..c2e622ec5800 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -93,7 +93,7 @@ function astroDBIntegration(options?: AstroDBConfig): AstroIntegration { if (connectToRemote) { dbPlugin = vitePluginDb({ connectToRemote, - appToken: getRemoteDatabaseInfo().token, + appToken: getRemoteDatabaseInfo(resolvedConfig.mode).token, tables, root: config.root, srcDir: config.srcDir, @@ -111,6 +111,7 @@ function astroDBIntegration(options?: AstroDBConfig): AstroIntegration { output: config.output, logger, seedHandler, + mode: resolvedConfig.mode, }); } diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 808520820287..99e6855ca9ac 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -42,6 +42,7 @@ type VitePluginDBParams = logger?: AstroIntegrationLogger; output: AstroConfig['output']; seedHandler: SeedHandler; + mode: 'node' | 'web'; } | { connectToRemote: true; @@ -89,6 +90,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { return getLocalVirtualModContents({ root: params.root, tables: params.tables.get(), + mode: 'node', }); } @@ -110,6 +112,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { return getLocalVirtualModContents({ root: params.root, tables: params.tables.get(), + mode: params.mode, }); }, }; @@ -119,9 +122,9 @@ export function getConfigVirtualModContents() { return `export * from ${RUNTIME_VIRTUAL_IMPORT}`; } -export function getLocalVirtualModContents({ tables, root }: { tables: DBTables; root: URL }) { +export function getLocalVirtualModContents({ tables, root, mode }: { tables: DBTables; root: URL; mode: 'node' | 'web' }) { const { ASTRO_DATABASE_FILE } = getAstroEnv(); - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo(mode); const dbUrl = new URL(DB_PATH, root); return ` import { asDrizzleTable, createLocalDatabaseClient, normalizeDatabaseUrl } from ${RUNTIME_IMPORT}; @@ -147,7 +150,7 @@ export function getRemoteVirtualModContents({ output: AstroConfig['output']; mode: 'node' | 'web'; }) { - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo(mode); function appTokenArg() { if (isBuild) { diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts index 1a58ddcab646..2944234cb396 100644 --- a/packages/db/src/core/utils.ts +++ b/packages/db/src/core/utils.ts @@ -12,14 +12,16 @@ export function getAstroEnv(envMode = ''): Record<`ASTRO_${string}`, string> { export type RemoteDatabaseInfo = { url: string; token: string; + mode: 'node' | 'web'; }; -export function getRemoteDatabaseInfo(): RemoteDatabaseInfo { +export function getRemoteDatabaseInfo(mode: 'node' | 'web'): RemoteDatabaseInfo { const astroEnv = getAstroEnv(); return { url: astroEnv.ASTRO_DB_REMOTE_URL, token: astroEnv.ASTRO_DB_APP_TOKEN, + mode, }; } From 4311aa6b6723d94762c4429daec439142a8ee550 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 09:48:51 -0700 Subject: [PATCH 03/35] feat(db): export AstroDBConfig type alongside integration from core integration --- packages/db/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 9d7b176fae28..d96fd260932a 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,3 +1,3 @@ export { cli } from './core/cli/index.js'; -export { integration as default } from './core/integration/index.js'; +export { type AstroDBConfig, integration as default } from './core/integration/index.js'; export type { TableConfig } from './core/types.js'; From 6ad1ac8a5d95c9126c713295e3ac40f4d2112182 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 10:12:29 -0700 Subject: [PATCH 04/35] test(db): ensure mode parameter is passed to getRemoteDatabaseInfo in tests --- packages/db/test/unit/remote-info.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/db/test/unit/remote-info.test.js b/packages/db/test/unit/remote-info.test.js index 4940f2e4a283..f87bdb089c8f 100644 --- a/packages/db/test/unit/remote-info.test.js +++ b/packages/db/test/unit/remote-info.test.js @@ -9,22 +9,24 @@ describe('RemoteDatabaseInfo', () => { }); test('default remote info', () => { - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo('node'); assert.deepEqual(dbInfo, { url: undefined, token: undefined, + mode: 'node', }); }); test('configured libSQL remote', () => { process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted'; process.env.ASTRO_DB_APP_TOKEN = 'foo'; - const dbInfo = getRemoteDatabaseInfo(); + const dbInfo = getRemoteDatabaseInfo('node'); assert.deepEqual(dbInfo, { url: 'libsql://libsql.self.hosted', token: 'foo', + mode: 'node' }); }); }); From e34acea6942c52663413a6696574b9a2e9ca5c48 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 20:11:20 -0700 Subject: [PATCH 05/35] feat(db): refactor database client handling and remove legacy code --- packages/db/package.json | 4 + .../db/src/core/cli/commands/execute/index.ts | 6 +- .../db/src/core/cli/commands/push/index.ts | 7 +- .../db/src/core/cli/commands/shell/index.ts | 12 +-- .../db/src/core/cli/commands/verify/index.ts | 2 +- packages/db/src/core/cli/migration-queries.ts | 6 +- packages/db/src/core/consts.ts | 8 ++ .../db/src/core/db-client/libsql-local.ts | 15 +++ packages/db/src/core/db-client/libsql-node.ts | 41 ++++++++ packages/db/src/core/db-client/libsql-web.ts | 29 ++++++ packages/db/src/core/integration/index.ts | 12 ++- .../core/integration/vite-plugin-db-client.ts | 47 +++++++++ .../db/src/core/integration/vite-plugin-db.ts | 52 ++++++---- packages/db/src/core/utils.ts | 4 +- packages/db/src/db-client.d.ts | 4 + packages/db/src/runtime/db-client.ts | 96 ------------------- packages/db/src/runtime/index.ts | 2 +- packages/db/src/runtime/utils.ts | 17 +++- packages/db/test/unit/db-client.test.js | 2 +- 19 files changed, 224 insertions(+), 142 deletions(-) create mode 100644 packages/db/src/core/db-client/libsql-local.ts create mode 100644 packages/db/src/core/db-client/libsql-node.ts create mode 100644 packages/db/src/core/db-client/libsql-web.ts create mode 100644 packages/db/src/core/integration/vite-plugin-db-client.ts create mode 100644 packages/db/src/db-client.d.ts delete mode 100644 packages/db/src/runtime/db-client.ts diff --git a/packages/db/package.json b/packages/db/package.json index 8fab9173ad2a..5c154da36fd0 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -27,6 +27,10 @@ "types": "./dist/runtime/index.d.ts", "default": "./dist/runtime/index.js" }, + "./db-client/*": { + "types": "./dist/core/db-client/*.d.ts", + "default": "./dist/core/db-client/*.js" + }, "./dist/runtime/virtual.js": { "default": "./dist/runtime/virtual.js" }, diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts index 1d894e840e3d..e519c6784da2 100644 --- a/packages/db/src/core/cli/commands/execute/index.ts +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -40,19 +40,19 @@ export async function cmd({ let virtualModContents: string; if (flags.remote) { - const dbInfo = getRemoteDatabaseInfo('node'); + const dbInfo = getRemoteDatabaseInfo(); virtualModContents = getRemoteVirtualModContents({ tables: dbConfig.tables ?? {}, appToken: flags.token ?? dbInfo.token, isBuild: false, output: 'server', - mode: 'node' + __execute: true, }); } else { virtualModContents = getLocalVirtualModContents({ tables: dbConfig.tables ?? {}, root: astroConfig.root, - mode: 'node' + __execute: true, }); } const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl }); diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index e333273fdcd0..2d443bb9cdc9 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -2,8 +2,8 @@ import type { AstroConfig } from 'astro'; import { sql } from 'drizzle-orm'; import prompts from 'prompts'; import type { Arguments } from 'yargs-parser'; -import { createRemoteDatabaseClient } from '../../../../runtime/index.js'; import { MIGRATION_VERSION } from '../../../consts.js'; +import { createRemoteLibSQLClient } from '../../../db-client/libsql-node.js'; import type { DBConfig, DBSnapshot } from '../../../types.js'; import { getRemoteDatabaseInfo, type RemoteDatabaseInfo } from '../../../utils.js'; import { @@ -24,7 +24,7 @@ export async function cmd({ }) { const isDryRun = flags.dryRun; const isForceReset = flags.forceReset; - const dbInfo = getRemoteDatabaseInfo('node'); + const dbInfo = getRemoteDatabaseInfo(); const productionSnapshot = await getProductionCurrentSnapshot(dbInfo); const currentSnapshot = createCurrentSnapshot(dbConfig); const isFromScratch = !productionSnapshot; @@ -108,10 +108,9 @@ type RequestBody = { }; async function pushToDb(requestBody: RequestBody, appToken: string, remoteUrl: string) { - const client = createRemoteDatabaseClient({ + const client = createRemoteLibSQLClient({ token: appToken, url: remoteUrl, - mode: 'node', }); await client.run(sql`create table if not exists _astro_db_snapshot ( diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 5746967c8803..c5b0f7a21201 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -1,12 +1,10 @@ import type { AstroConfig } from 'astro'; import { sql } from 'drizzle-orm'; import type { Arguments } from 'yargs-parser'; -import { - createLocalDatabaseClient, - createRemoteDatabaseClient, -} from '../../../../runtime/db-client.js'; import { normalizeDatabaseUrl } from '../../../../runtime/index.js'; import { DB_PATH } from '../../../consts.js'; +import { createLocalDatabaseClient } from '../../../db-client/libsql-local.js'; +import { createRemoteLibSQLClient } from '../../../db-client/libsql-node.js'; import { SHELL_QUERY_MISSING_ERROR } from '../../../errors.js'; import type { DBConfigInput } from '../../../types.js'; import { getAstroEnv, getRemoteDatabaseInfo } from '../../../utils.js'; @@ -24,9 +22,9 @@ export async function cmd({ console.error(SHELL_QUERY_MISSING_ERROR); process.exit(1); } - const dbInfo = getRemoteDatabaseInfo('node'); + const dbInfo = getRemoteDatabaseInfo(); if (flags.remote) { - const db = createRemoteDatabaseClient(dbInfo); + const db = createRemoteLibSQLClient(dbInfo); const result = await db.run(sql.raw(query)); console.log(result); } else { @@ -35,7 +33,7 @@ export async function cmd({ ASTRO_DATABASE_FILE, new URL(DB_PATH, astroConfig.root).href, ); - const db = createLocalDatabaseClient({ dbUrl }); + const db = createLocalDatabaseClient({ url: dbUrl }); const result = await db.run(sql.raw(query)); console.log(result); } diff --git a/packages/db/src/core/cli/commands/verify/index.ts b/packages/db/src/core/cli/commands/verify/index.ts index e37b19ca3948..ae9c776090ca 100644 --- a/packages/db/src/core/cli/commands/verify/index.ts +++ b/packages/db/src/core/cli/commands/verify/index.ts @@ -19,7 +19,7 @@ export async function cmd({ flags: Arguments; }) { const isJson = flags.json; - const dbInfo = getRemoteDatabaseInfo('node'); + const dbInfo = getRemoteDatabaseInfo(); const productionSnapshot = await getProductionCurrentSnapshot(dbInfo); const currentSnapshot = createCurrentSnapshot(dbConfig); const { queries: migrationQueries, confirmations } = await getMigrationQueries({ diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index 32e0e44cfa2c..f81f82b7c792 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -4,10 +4,11 @@ import { sql } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import * as color from 'kleur/colors'; import { customAlphabet } from 'nanoid'; -import { createRemoteDatabaseClient, hasPrimaryKey } from '../../runtime/index.js'; +import { hasPrimaryKey } from '../../runtime/index.js'; import { isSerializedSQL } from '../../runtime/types.js'; import { isDbError } from '../../runtime/utils.js'; import { MIGRATION_VERSION } from '../consts.js'; +import { createRemoteLibSQLClient } from '../db-client/libsql-node.js'; import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from '../errors.js'; import { getCreateIndexQueries, @@ -434,10 +435,9 @@ async function getDbCurrentSnapshot( appToken: string, remoteUrl: string, ): Promise { - const client = createRemoteDatabaseClient({ + const client = createRemoteLibSQLClient({ token: appToken, url: remoteUrl, - mode: 'node', }); try { diff --git a/packages/db/src/core/consts.ts b/packages/db/src/core/consts.ts index 8b8ccaf2d442..57fdcfa3fcc7 100644 --- a/packages/db/src/core/consts.ts +++ b/packages/db/src/core/consts.ts @@ -15,3 +15,11 @@ export const DB_PATH = '.astro/content.db'; export const CONFIG_FILE_NAMES = ['config.ts', 'config.js', 'config.mts', 'config.mjs']; export const MIGRATION_VERSION = '2024-03-12'; + +export const VIRTUAL_CLIENT_MODULE_ID = 'virtual:astro:db-client'; + +export const DB_CLIENTS = { + node: '@astro/db/db-client/libsql-node.js', + web: '@astro/db/db-client/libsql-web.js', + local: '@astro/db/db-client/libsql-local.js', +}; diff --git a/packages/db/src/core/db-client/libsql-local.ts b/packages/db/src/core/db-client/libsql-local.ts new file mode 100644 index 000000000000..731fdae2b600 --- /dev/null +++ b/packages/db/src/core/db-client/libsql-local.ts @@ -0,0 +1,15 @@ +import { createClient } from '@libsql/client'; +import { drizzle as drizzleLibsql, type LibSQLDatabase } from 'drizzle-orm/libsql'; + +const isWebContainer = !!process.versions?.webcontainer; + +type LocalDbClientOptions = { + url: string; +}; + +export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQLDatabase { + const url = isWebContainer ? 'file:content.db' : options.url; + const client = createClient({ url }); + const db = drizzleLibsql(client); + return db; +} diff --git a/packages/db/src/core/db-client/libsql-node.ts b/packages/db/src/core/db-client/libsql-node.ts new file mode 100644 index 000000000000..3ab7bb1b646f --- /dev/null +++ b/packages/db/src/core/db-client/libsql-node.ts @@ -0,0 +1,41 @@ +import { createClient } from '@libsql/client'; +import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; +import { parseOpts } from '../../runtime/utils.js'; + +type RemoteDbClientOptions = { + token: string; + url: string; +}; + +export function createRemoteLibSQLClient(opts: RemoteDbClientOptions) { + const { token, url } = opts; + + const parsedUrl = new URL(url); + + const options: Record = Object.fromEntries(parsedUrl.searchParams.entries()); + parsedUrl.search = ''; + + let dbURL = parsedUrl.toString(); + if (parsedUrl.protocol === 'memory:') { + // libSQL expects a special string in place of a URL + // for in-memory DBs. + dbURL = ':memory:'; + } else if ( + parsedUrl.protocol === 'file:' && + parsedUrl.pathname.startsWith('/') && + !dbURL.startsWith('file:/') + ) { + // libSQL accepts relative and absolute file URLs + // for local DBs. This doesn't match the URL specification. + // Parsing `file:some.db` and `file:/some.db` should yield + // the same result, but libSQL interprets the former as + // a relative path, and the latter as an absolute path. + // This detects when such a conversion happened during parsing + // and undoes it so that the URL given to libSQL is the + // same as given by the user. + dbURL = 'file:' + parsedUrl.pathname.substring(1); + } + + const client = createClient({ ...parseOpts(options), url: dbURL, authToken: token }); + return drizzleLibsql(client); +} diff --git a/packages/db/src/core/db-client/libsql-web.ts b/packages/db/src/core/db-client/libsql-web.ts new file mode 100644 index 000000000000..62ec97a5fb6c --- /dev/null +++ b/packages/db/src/core/db-client/libsql-web.ts @@ -0,0 +1,29 @@ +import { createClient } from '@libsql/client/web'; +import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql/web'; +import { parseOpts } from '../../runtime/utils.js'; + +type RemoteDbClientOptions = { + token: string; + url: string; +}; + +export function createRemoteLibSQLClient(opts: RemoteDbClientOptions) { + const { token, url } = opts; + + const parsedUrl = new URL(url); + const options: Record = Object.fromEntries(parsedUrl.searchParams.entries()); + parsedUrl.search = ''; + + let dbURL = parsedUrl.toString(); + + const supportedProtocols = ['http:', 'https:', 'libsql:']; + + if (!supportedProtocols.includes(parsedUrl.protocol)) { + throw new Error( + `Unsupported protocol "${parsedUrl.protocol}" for libSQL web client. Supported protocols are: ${supportedProtocols.join(', ')}.`, + ); + } + + const client = createClient({ ...parseOpts(options), url: dbURL, authToken: token }); + return drizzleLibsql(client); +} diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index c2e622ec5800..2947a9650de3 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -28,6 +28,7 @@ import { type SeedHandler, vitePluginDb, } from './vite-plugin-db.js'; +import { vitePluginDbClient } from './vite-plugin-db-client.js'; const astroDBConfigSchema = z .object({ @@ -90,16 +91,20 @@ function astroDBIntegration(options?: AstroDBConfig): AstroIntegration { const args = parseArgs(process.argv.slice(3)); connectToRemote = process.env.ASTRO_INTERNAL_TEST_REMOTE || args['remote']; + const dbClientPlugin = vitePluginDbClient({ + connectToRemote, + mode: resolvedConfig.mode, + }); + if (connectToRemote) { dbPlugin = vitePluginDb({ connectToRemote, - appToken: getRemoteDatabaseInfo(resolvedConfig.mode).token, + appToken: getRemoteDatabaseInfo().token, tables, root: config.root, srcDir: config.srcDir, output: config.output, seedHandler, - mode: resolvedConfig.mode, }); } else { dbPlugin = vitePluginDb({ @@ -111,14 +116,13 @@ function astroDBIntegration(options?: AstroDBConfig): AstroIntegration { output: config.output, logger, seedHandler, - mode: resolvedConfig.mode, }); } updateConfig({ vite: { assetsInclude: [DB_PATH], - plugins: [dbPlugin], + plugins: [dbClientPlugin, dbPlugin], }, }); }, diff --git a/packages/db/src/core/integration/vite-plugin-db-client.ts b/packages/db/src/core/integration/vite-plugin-db-client.ts new file mode 100644 index 000000000000..b47717143b53 --- /dev/null +++ b/packages/db/src/core/integration/vite-plugin-db-client.ts @@ -0,0 +1,47 @@ +import { DB_CLIENTS, VIRTUAL_CLIENT_MODULE_ID } from '../consts.js'; +import type { VitePlugin } from '../utils.js'; + +type VitePluginDBClientParams = { + connectToRemote: boolean; + mode: 'node' | 'web'; +}; + +function getRemoteClientModule(mode: 'node' | 'web') { + switch (mode) { + case 'web': { + return `export { createRemoteLibSQLClient as createClient } from '${DB_CLIENTS.web}';`; + } + default: + return `export { createRemoteLibSQLClient as createClient } from '${DB_CLIENTS.node}';`; + } +} + +function getLocalClientModule(mode: 'node' | 'web') { + switch (mode) { + default: + return `export { createLocalDatabaseClient as createClient } from '${DB_CLIENTS.local}';`; + } +} + +const resolved = '\0' + VIRTUAL_CLIENT_MODULE_ID; + +export function vitePluginDbClient(params: VitePluginDBClientParams): VitePlugin { + return { + name: 'virtual:astro:db-client', + enforce: 'pre', + async resolveId(id) { + if (id !== VIRTUAL_CLIENT_MODULE_ID) return; + return resolved; + }, + async load(id) { + if (id !== resolved) return; + + switch (params.connectToRemote) { + case true: + return getRemoteClientModule(params.mode); + case false: + return getLocalClientModule(params.mode); + } + }, + }; +} diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 99e6855ca9ac..745b0190e12b 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -3,9 +3,9 @@ import { fileURLToPath } from 'node:url'; import type { AstroConfig, AstroIntegrationLogger } from 'astro'; import { type SQL, sql } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; -import { createLocalDatabaseClient } from '../../runtime/db-client.js'; import { normalizeDatabaseUrl } from '../../runtime/index.js'; -import { DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js'; +import { DB_CLIENTS, DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js'; +import { createLocalDatabaseClient } from '../db-client/libsql-local.js'; import { getResolvedFileUrl } from '../load-file.js'; import { getCreateIndexQueries, getCreateTableQuery, SEED_DEV_FILE_NAME } from '../queries.js'; import type { DBTables } from '../types.js'; @@ -42,7 +42,6 @@ type VitePluginDBParams = logger?: AstroIntegrationLogger; output: AstroConfig['output']; seedHandler: SeedHandler; - mode: 'node' | 'web'; } | { connectToRemote: true; @@ -52,7 +51,6 @@ type VitePluginDBParams = root: URL; output: AstroConfig['output']; seedHandler: SeedHandler; - mode: 'node' | 'web'; }; export function vitePluginDb(params: VitePluginDBParams): VitePlugin { @@ -79,7 +77,6 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { tables: params.tables.get(), isBuild: command === 'build', output: params.output, - mode: params.mode, }); } @@ -90,7 +87,6 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { return getLocalVirtualModContents({ root: params.root, tables: params.tables.get(), - mode: 'node', }); } @@ -112,7 +108,6 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { return getLocalVirtualModContents({ root: params.root, tables: params.tables.get(), - mode: params.mode, }); }, }; @@ -122,15 +117,30 @@ export function getConfigVirtualModContents() { return `export * from ${RUNTIME_VIRTUAL_IMPORT}`; } -export function getLocalVirtualModContents({ tables, root, mode }: { tables: DBTables; root: URL; mode: 'node' | 'web' }) { +export function getLocalVirtualModContents({ + tables, + root, + __execute = false, +}: { + tables: DBTables; + root: URL; + __execute?: boolean; +}) { const { ASTRO_DATABASE_FILE } = getAstroEnv(); - const dbInfo = getRemoteDatabaseInfo(mode); const dbUrl = new URL(DB_PATH, root); + + // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. + const clientImport = __execute + ? `import { createClient } from '${DB_CLIENTS.node}';` + : `import { createClient } from '${RUNTIME_IMPORT}';`; + return ` -import { asDrizzleTable, createLocalDatabaseClient, normalizeDatabaseUrl } from ${RUNTIME_IMPORT}; +import { asDrizzleTable, normalizeDatabaseUrl } from ${RUNTIME_IMPORT}; + +${clientImport} const dbUrl = normalizeDatabaseUrl(${JSON.stringify(ASTRO_DATABASE_FILE)}, ${JSON.stringify(dbUrl)}); -export const db = createLocalDatabaseClient({ dbUrl, enableTransactions: ${dbInfo.url === 'libsql'} }); +export const db = createClient({ url: dbUrl }); export * from ${RUNTIME_VIRTUAL_IMPORT}; @@ -142,15 +152,15 @@ export function getRemoteVirtualModContents({ appToken, isBuild, output, - mode + __execute = false, // Used for execute command }: { tables: DBTables; appToken: string; isBuild: boolean; output: AstroConfig['output']; - mode: 'node' | 'web'; + __execute?: boolean; }) { - const dbInfo = getRemoteDatabaseInfo(mode); + const dbInfo = getRemoteDatabaseInfo(); function appTokenArg() { if (isBuild) { @@ -177,13 +187,19 @@ export function getRemoteVirtualModContents({ } } + // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. + const clientImport = __execute + ? `import { createClient } from '${DB_CLIENTS.node}';` + : `import { createClient } from '${RUNTIME_IMPORT}';`; + return ` -import {asDrizzleTable, createRemoteDatabaseClient} from ${RUNTIME_IMPORT}; +import {asDrizzleTable} from ${RUNTIME_IMPORT}; + +${clientImport} -export const db = await createRemoteDatabaseClient({ +export const db = await createClient({ url: ${dbUrlArg()}, token: ${appTokenArg()}, - mode: ${JSON.stringify(mode)}, }); export * from ${RUNTIME_VIRTUAL_IMPORT}; @@ -208,7 +224,7 @@ const sqlite = new SQLiteAsyncDialect(); async function recreateTables({ tables, root }: { tables: LateTables; root: URL }) { const { ASTRO_DATABASE_FILE } = getAstroEnv(); const dbUrl = normalizeDatabaseUrl(ASTRO_DATABASE_FILE, new URL(DB_PATH, root).href); - const db = createLocalDatabaseClient({ dbUrl }); + const db = createLocalDatabaseClient({ url: dbUrl }); const setupQueries: SQL[] = []; for (const [name, table] of Object.entries(tables.get() ?? {})) { const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`); diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts index 2944234cb396..1a58ddcab646 100644 --- a/packages/db/src/core/utils.ts +++ b/packages/db/src/core/utils.ts @@ -12,16 +12,14 @@ export function getAstroEnv(envMode = ''): Record<`ASTRO_${string}`, string> { export type RemoteDatabaseInfo = { url: string; token: string; - mode: 'node' | 'web'; }; -export function getRemoteDatabaseInfo(mode: 'node' | 'web'): RemoteDatabaseInfo { +export function getRemoteDatabaseInfo(): RemoteDatabaseInfo { const astroEnv = getAstroEnv(); return { url: astroEnv.ASTRO_DB_REMOTE_URL, token: astroEnv.ASTRO_DB_APP_TOKEN, - mode, }; } diff --git a/packages/db/src/db-client.d.ts b/packages/db/src/db-client.d.ts new file mode 100644 index 000000000000..7709b30f6dfc --- /dev/null +++ b/packages/db/src/db-client.d.ts @@ -0,0 +1,4 @@ +declare module 'virtual:astro:db-client' { + export const createClient: typeof import('./core/db-client/libsql-node.ts').createRemoteLibSQLClient + +} \ No newline at end of file diff --git a/packages/db/src/runtime/db-client.ts b/packages/db/src/runtime/db-client.ts deleted file mode 100644 index 4d4eb79cc43a..000000000000 --- a/packages/db/src/runtime/db-client.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { createClient, type Config as LibSQLConfig } from '@libsql/client'; -import { createClient as createClientWeb } from '@libsql/client/web'; -import { drizzle as drizzleLibsql, type LibSQLDatabase } from 'drizzle-orm/libsql'; -import { drizzle as drizzleLibsqlWeb } from 'drizzle-orm/libsql/web'; - -const isWebContainer = !!process.versions?.webcontainer; - -type LocalDbClientOptions = { - dbUrl: string; -}; - -export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQLDatabase { - const url = isWebContainer ? 'file:content.db' : options.dbUrl; - const client = createClient({ url }); - const db = drizzleLibsql(client); - return db; -} - -type RemoteDbClientOptions = { - token: string; - url: string | URL; - mode: 'node' | 'web'; -}; - -export function createRemoteDatabaseClient(options: RemoteDbClientOptions) { - const url = new URL(options.url); - - switch (options.mode) { - case 'web': - return createRemoteLibSQLWebClient(options.token, url); - case 'node': - return createRemoteLibSQLClient(options.token, url, options.url.toString()); - } -} - -// this function parses the options from a `Record` -// provided from the object conversion of the searchParams and properly -// verifies that the Config object is providing the correct types. -// without this, there is runtime errors due to incorrect values -export function parseOpts(config: Record): Partial { - return { - ...config, - ...(config.syncInterval ? { syncInterval: parseInt(config.syncInterval) } : {}), - ...('readYourWrites' in config ? { readYourWrites: config.readYourWrites !== 'false' } : {}), - ...('offline' in config ? { offline: config.offline !== 'false' } : {}), - ...('tls' in config ? { tls: config.tls !== 'false' } : {}), - ...(config.concurrency ? { concurrency: parseInt(config.concurrency) } : {}), - }; -} - -function createRemoteLibSQLClient(authToken: string, dbURL: URL, rawUrl: string) { - const options: Record = Object.fromEntries(dbURL.searchParams.entries()); - dbURL.search = ''; - - let url = dbURL.toString(); - if (dbURL.protocol === 'memory:') { - // libSQL expects a special string in place of a URL - // for in-memory DBs. - url = ':memory:'; - } else if ( - dbURL.protocol === 'file:' && - dbURL.pathname.startsWith('/') && - !rawUrl.startsWith('file:/') - ) { - // libSQL accepts relative and absolute file URLs - // for local DBs. This doesn't match the URL specification. - // Parsing `file:some.db` and `file:/some.db` should yield - // the same result, but libSQL interprets the former as - // a relative path, and the latter as an absolute path. - // This detects when such a conversion happened during parsing - // and undoes it so that the URL given to libSQL is the - // same as given by the user. - url = 'file:' + dbURL.pathname.substring(1); - } - - const client = createClient({ ...parseOpts(options), url, authToken }); - return drizzleLibsql(client); -} - -function createRemoteLibSQLWebClient(authToken: string, dbURL: URL) { - const options: Record = Object.fromEntries(dbURL.searchParams.entries()); - dbURL.search = ''; - - let url = dbURL.toString(); - - const supportedProtocols = ['http:', 'https:', 'libsql:']; - - if (!supportedProtocols.includes(dbURL.protocol)) { - throw new Error( - `Unsupported protocol "${dbURL.protocol}" for libSQL web client. Supported protocols are: ${supportedProtocols.join(', ')}.` - ); - } - - const client = createClientWeb({ ...parseOpts(options), url, authToken }); - return drizzleLibsqlWeb(client); -} diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 2ea7916d6dd2..9309e6b3676f 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -13,7 +13,7 @@ import type { DBColumn, DBTable } from '../core/types.js'; import { isSerializedSQL, type SerializedSQL } from './types.js'; import { pathToFileURL } from './utils.js'; export type Database = LibSQLDatabase; -export { createLocalDatabaseClient, createRemoteDatabaseClient } from './db-client.js'; +export { createClient } from 'virtual:astro:db-client'; export type { Table } from './types.js'; export function hasPrimaryKey(column: DBColumn) { diff --git a/packages/db/src/runtime/utils.ts b/packages/db/src/runtime/utils.ts index af3fc66ec381..735f30bec27c 100644 --- a/packages/db/src/runtime/utils.ts +++ b/packages/db/src/runtime/utils.ts @@ -1,4 +1,4 @@ -import { LibsqlError } from '@libsql/client'; +import { type Config as LibSQLConfig, LibsqlError } from '@libsql/client'; import { AstroError } from 'astro/errors'; const isWindows = process?.platform === 'win32'; @@ -34,3 +34,18 @@ export function pathToFileURL(path: string): URL { // Unix is easy return new URL('file://' + path); } + +// this function parses the options from a `Record` +// provided from the object conversion of the searchParams and properly +// verifies that the Config object is providing the correct types. +// without this, there is runtime errors due to incorrect values +export function parseOpts(config: Record): Partial { + return { + ...config, + ...(config.syncInterval ? { syncInterval: parseInt(config.syncInterval) } : {}), + ...('readYourWrites' in config ? { readYourWrites: config.readYourWrites !== 'false' } : {}), + ...('offline' in config ? { offline: config.offline !== 'false' } : {}), + ...('tls' in config ? { tls: config.tls !== 'false' } : {}), + ...(config.concurrency ? { concurrency: parseInt(config.concurrency) } : {}), + }; +} diff --git a/packages/db/test/unit/db-client.test.js b/packages/db/test/unit/db-client.test.js index 22df2610e49a..08c2b920fd2b 100644 --- a/packages/db/test/unit/db-client.test.js +++ b/packages/db/test/unit/db-client.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert'; import test, { describe } from 'node:test'; -import { parseOpts } from '../../dist/runtime/db-client.js'; +import { parseOpts } from '../../dist/runtime/utils.js'; describe('db client config', () => { test('parse config options from URL (docs example url)', () => { From 12a4379c75f52a44c6acd59e0f69fec6897b600f Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 20:12:26 -0700 Subject: [PATCH 06/35] test(db): remove mode parameter from getRemoteDatabaseInfo assertions --- packages/db/test/unit/remote-info.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/db/test/unit/remote-info.test.js b/packages/db/test/unit/remote-info.test.js index f87bdb089c8f..f21e8568af39 100644 --- a/packages/db/test/unit/remote-info.test.js +++ b/packages/db/test/unit/remote-info.test.js @@ -9,12 +9,11 @@ describe('RemoteDatabaseInfo', () => { }); test('default remote info', () => { - const dbInfo = getRemoteDatabaseInfo('node'); + const dbInfo = getRemoteDatabaseInfo(); assert.deepEqual(dbInfo, { url: undefined, token: undefined, - mode: 'node', }); }); @@ -26,7 +25,6 @@ describe('RemoteDatabaseInfo', () => { assert.deepEqual(dbInfo, { url: 'libsql://libsql.self.hosted', token: 'foo', - mode: 'node' }); }); }); From d9bd4b132d7e5c2143feb52f499ae2505a53de4e Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 20:15:02 -0700 Subject: [PATCH 07/35] test(db): remove mode parameter from getRemoteDatabaseInfo call in tests --- packages/db/test/unit/remote-info.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/test/unit/remote-info.test.js b/packages/db/test/unit/remote-info.test.js index f21e8568af39..4940f2e4a283 100644 --- a/packages/db/test/unit/remote-info.test.js +++ b/packages/db/test/unit/remote-info.test.js @@ -20,7 +20,7 @@ describe('RemoteDatabaseInfo', () => { test('configured libSQL remote', () => { process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted'; process.env.ASTRO_DB_APP_TOKEN = 'foo'; - const dbInfo = getRemoteDatabaseInfo('node'); + const dbInfo = getRemoteDatabaseInfo(); assert.deepEqual(dbInfo, { url: 'libsql://libsql.self.hosted', From 322b7cf92fa1c7954212004c31fc07e704907c80 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 20:20:26 -0700 Subject: [PATCH 08/35] fix(db): ensure correct handling of 'node' mode in client module functions --- packages/db/src/core/integration/vite-plugin-db-client.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/db/src/core/integration/vite-plugin-db-client.ts b/packages/db/src/core/integration/vite-plugin-db-client.ts index b47717143b53..5e9150b51ce3 100644 --- a/packages/db/src/core/integration/vite-plugin-db-client.ts +++ b/packages/db/src/core/integration/vite-plugin-db-client.ts @@ -11,6 +11,7 @@ function getRemoteClientModule(mode: 'node' | 'web') { case 'web': { return `export { createRemoteLibSQLClient as createClient } from '${DB_CLIENTS.web}';`; } + case 'node': default: return `export { createRemoteLibSQLClient as createClient } from '${DB_CLIENTS.node}';`; } @@ -18,6 +19,8 @@ function getRemoteClientModule(mode: 'node' | 'web') { function getLocalClientModule(mode: 'node' | 'web') { switch (mode) { + case 'node': + case 'web': default: return `export { createLocalDatabaseClient as createClient } from '${DB_CLIENTS.local}';`; } From 475c62432d8052da6d62ae62bad54b088e1ef6c3 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 20:44:29 -0700 Subject: [PATCH 09/35] refactor(db): consolidate utility functions by moving hasPrimaryKey to utils --- packages/db/src/core/cli/migration-queries.ts | 3 +-- packages/db/src/runtime/index.ts | 6 +----- packages/db/src/runtime/utils.ts | 5 +++++ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index f81f82b7c792..ab4eb83252d1 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -4,9 +4,8 @@ import { sql } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import * as color from 'kleur/colors'; import { customAlphabet } from 'nanoid'; -import { hasPrimaryKey } from '../../runtime/index.js'; import { isSerializedSQL } from '../../runtime/types.js'; -import { isDbError } from '../../runtime/utils.js'; +import { hasPrimaryKey, isDbError } from '../../runtime/utils.js'; import { MIGRATION_VERSION } from '../consts.js'; import { createRemoteLibSQLClient } from '../db-client/libsql-node.js'; import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from '../errors.js'; diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 9309e6b3676f..2132e24811b7 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -11,15 +11,11 @@ import { } from 'drizzle-orm/sqlite-core'; import type { DBColumn, DBTable } from '../core/types.js'; import { isSerializedSQL, type SerializedSQL } from './types.js'; -import { pathToFileURL } from './utils.js'; +import { hasPrimaryKey, pathToFileURL } from './utils.js'; export type Database = LibSQLDatabase; export { createClient } from 'virtual:astro:db-client'; export type { Table } from './types.js'; -export function hasPrimaryKey(column: DBColumn) { - return 'primaryKey' in column.schema && !!column.schema.primaryKey; -} - // Taken from: // https://stackoverflow.com/questions/52869695/check-if-a-date-string-is-in-iso-and-utc-format const isISODateString = (str: string) => /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str); diff --git a/packages/db/src/runtime/utils.ts b/packages/db/src/runtime/utils.ts index 735f30bec27c..55f55451e2a0 100644 --- a/packages/db/src/runtime/utils.ts +++ b/packages/db/src/runtime/utils.ts @@ -1,5 +1,10 @@ import { type Config as LibSQLConfig, LibsqlError } from '@libsql/client'; import { AstroError } from 'astro/errors'; +import type { DBColumn } from '../core/types.js'; + +export function hasPrimaryKey(column: DBColumn) { + return 'primaryKey' in column.schema && !!column.schema.primaryKey; +} const isWindows = process?.platform === 'win32'; From 604648eddebe7eb0b1a414378386c4fd4b20e957 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 20:46:53 -0700 Subject: [PATCH 10/35] fix(db): re-export hasPrimaryKey from utils for consistency --- packages/db/src/runtime/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 2132e24811b7..76ddd08f0356 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -15,6 +15,7 @@ import { hasPrimaryKey, pathToFileURL } from './utils.js'; export type Database = LibSQLDatabase; export { createClient } from 'virtual:astro:db-client'; export type { Table } from './types.js'; +export { hasPrimaryKey } from './utils.js'; // Taken from: // https://stackoverflow.com/questions/52869695/check-if-a-date-string-is-in-iso-and-utc-format From 4187ecc4b2ed963566928b17741edf12c1a0140c Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 21:06:11 -0700 Subject: [PATCH 11/35] fix(db): update import path for hasPrimaryKey to utils for consistency --- packages/db/src/core/queries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts index cb483d4ef3fa..f772f802501e 100644 --- a/packages/db/src/core/queries.ts +++ b/packages/db/src/core/queries.ts @@ -7,8 +7,8 @@ import { FOREIGN_KEY_REFERENCES_LENGTH_ERROR, REFERENCE_DNE_ERROR, } from '../runtime/errors.js'; -import { hasPrimaryKey } from '../runtime/index.js'; import { isSerializedSQL } from '../runtime/types.js'; +import { hasPrimaryKey } from '../runtime/utils.js'; import type { BooleanColumn, ColumnType, From f3572c815c5d9d672ecda6e0fcff0d772ad61148 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 21:26:29 -0700 Subject: [PATCH 12/35] fix(db): update client import to use VIRTUAL_CLIENT_MODULE_ID for consistency --- packages/db/src/core/integration/vite-plugin-db.ts | 6 +++--- packages/db/src/runtime/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 745b0190e12b..43c87427bedd 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -4,7 +4,7 @@ import type { AstroConfig, AstroIntegrationLogger } from 'astro'; import { type SQL, sql } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { normalizeDatabaseUrl } from '../../runtime/index.js'; -import { DB_CLIENTS, DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js'; +import { DB_CLIENTS, DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_CLIENT_MODULE_ID, VIRTUAL_MODULE_ID } from '../consts.js'; import { createLocalDatabaseClient } from '../db-client/libsql-local.js'; import { getResolvedFileUrl } from '../load-file.js'; import { getCreateIndexQueries, getCreateTableQuery, SEED_DEV_FILE_NAME } from '../queries.js'; @@ -132,7 +132,7 @@ export function getLocalVirtualModContents({ // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. const clientImport = __execute ? `import { createClient } from '${DB_CLIENTS.node}';` - : `import { createClient } from '${RUNTIME_IMPORT}';`; + : `import { createClient } from '${VIRTUAL_CLIENT_MODULE_ID}';`; return ` import { asDrizzleTable, normalizeDatabaseUrl } from ${RUNTIME_IMPORT}; @@ -190,7 +190,7 @@ export function getRemoteVirtualModContents({ // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. const clientImport = __execute ? `import { createClient } from '${DB_CLIENTS.node}';` - : `import { createClient } from '${RUNTIME_IMPORT}';`; + : `import { createClient } from '${VIRTUAL_CLIENT_MODULE_ID}';`; return ` import {asDrizzleTable} from ${RUNTIME_IMPORT}; diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 76ddd08f0356..6188cf8d4ca2 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -13,9 +13,9 @@ import type { DBColumn, DBTable } from '../core/types.js'; import { isSerializedSQL, type SerializedSQL } from './types.js'; import { hasPrimaryKey, pathToFileURL } from './utils.js'; export type Database = LibSQLDatabase; -export { createClient } from 'virtual:astro:db-client'; export type { Table } from './types.js'; -export { hasPrimaryKey } from './utils.js'; + +// export { hasPrimaryKey } from './utils.js'; // Taken from: // https://stackoverflow.com/questions/52869695/check-if-a-date-string-is-in-iso-and-utc-format From be17d1e591a16f2219e698df37885cdc8bc333aa Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 21:27:26 -0700 Subject: [PATCH 13/35] fix(db): remove commented export of hasPrimaryKey for clarity --- packages/db/src/runtime/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 6188cf8d4ca2..dca3cacef069 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -14,8 +14,7 @@ import { isSerializedSQL, type SerializedSQL } from './types.js'; import { hasPrimaryKey, pathToFileURL } from './utils.js'; export type Database = LibSQLDatabase; export type { Table } from './types.js'; - -// export { hasPrimaryKey } from './utils.js'; +export { hasPrimaryKey } from './utils.js'; // Taken from: // https://stackoverflow.com/questions/52869695/check-if-a-date-string-is-in-iso-and-utc-format From 2a4f73714cc0cfdcf2e81e55e0b25ac8c7d91eaf Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 22:26:53 -0700 Subject: [PATCH 14/35] fix(db): update DB_CLIENTS paths to use '@astrojs' for consistency fix(db): ensure local client module is returned in default case --- packages/db/src/core/consts.ts | 6 +++--- packages/db/src/core/integration/vite-plugin-db-client.ts | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/db/src/core/consts.ts b/packages/db/src/core/consts.ts index 57fdcfa3fcc7..c3cc1f0eeb69 100644 --- a/packages/db/src/core/consts.ts +++ b/packages/db/src/core/consts.ts @@ -19,7 +19,7 @@ export const MIGRATION_VERSION = '2024-03-12'; export const VIRTUAL_CLIENT_MODULE_ID = 'virtual:astro:db-client'; export const DB_CLIENTS = { - node: '@astro/db/db-client/libsql-node.js', - web: '@astro/db/db-client/libsql-web.js', - local: '@astro/db/db-client/libsql-local.js', + node: '@astrojs/db/db-client/libsql-node.js', + web: '@astrojs/db/db-client/libsql-web.js', + local: '@astrojs/db/db-client/libsql-local.js', }; diff --git a/packages/db/src/core/integration/vite-plugin-db-client.ts b/packages/db/src/core/integration/vite-plugin-db-client.ts index 5e9150b51ce3..e757540ae755 100644 --- a/packages/db/src/core/integration/vite-plugin-db-client.ts +++ b/packages/db/src/core/integration/vite-plugin-db-client.ts @@ -43,7 +43,9 @@ export function vitePluginDbClient(params: VitePluginDBClientParams): VitePlugin case true: return getRemoteClientModule(params.mode); case false: - return getLocalClientModule(params.mode); + default: + // Local client is always available, even if not used. + return getLocalClientModule(params.mode); } }, }; From 4b661ab7385ff450e647437cf9c8ecf62eb4ab33 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 22:27:33 -0700 Subject: [PATCH 15/35] fix(db): update DB_CLIENTS paths to use PACKAGE_NAME for consistency --- packages/db/src/core/consts.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/db/src/core/consts.ts b/packages/db/src/core/consts.ts index c3cc1f0eeb69..7cb3cf565cc2 100644 --- a/packages/db/src/core/consts.ts +++ b/packages/db/src/core/consts.ts @@ -19,7 +19,7 @@ export const MIGRATION_VERSION = '2024-03-12'; export const VIRTUAL_CLIENT_MODULE_ID = 'virtual:astro:db-client'; export const DB_CLIENTS = { - node: '@astrojs/db/db-client/libsql-node.js', - web: '@astrojs/db/db-client/libsql-web.js', - local: '@astrojs/db/db-client/libsql-local.js', + node: `${PACKAGE_NAME}/db-client/libsql-node.js`, + web: `${PACKAGE_NAME}/db-client/libsql-web.js`, + local: `${PACKAGE_NAME}/db-client/libsql-local.js`, }; From d00fc284613ba7a8e68e4b956bdb48b07c3f1218 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 22:49:37 -0700 Subject: [PATCH 16/35] fix(db): simplify db-client export paths for consistency --- packages/db/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/db/package.json b/packages/db/package.json index 5c154da36fd0..bd5dae8a6885 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -27,10 +27,7 @@ "types": "./dist/runtime/index.d.ts", "default": "./dist/runtime/index.js" }, - "./db-client/*": { - "types": "./dist/core/db-client/*.d.ts", - "default": "./dist/core/db-client/*.js" - }, + "./db-client/*": "./dist/core/db-client/*", "./dist/runtime/virtual.js": { "default": "./dist/runtime/virtual.js" }, From 5b2c0568e7cb4834558571533caa8f4de7d2dadf Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 23:12:26 -0700 Subject: [PATCH 17/35] fix(db): unify client creation function names across db-client modules --- packages/db/src/core/db-client/libsql-local.ts | 6 +++--- packages/db/src/core/db-client/libsql-node.ts | 6 +++--- packages/db/src/core/db-client/libsql-web.ts | 6 +++--- packages/db/src/core/integration/vite-plugin-db-client.ts | 6 +++--- packages/db/src/core/integration/vite-plugin-db.ts | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/db/src/core/db-client/libsql-local.ts b/packages/db/src/core/db-client/libsql-local.ts index 731fdae2b600..8d8f6ad3df30 100644 --- a/packages/db/src/core/db-client/libsql-local.ts +++ b/packages/db/src/core/db-client/libsql-local.ts @@ -1,4 +1,4 @@ -import { createClient } from '@libsql/client'; +import { createClient as createLibsqlClient } from '@libsql/client'; import { drizzle as drizzleLibsql, type LibSQLDatabase } from 'drizzle-orm/libsql'; const isWebContainer = !!process.versions?.webcontainer; @@ -7,9 +7,9 @@ type LocalDbClientOptions = { url: string; }; -export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQLDatabase { +export function createClient(options: LocalDbClientOptions): LibSQLDatabase { const url = isWebContainer ? 'file:content.db' : options.url; - const client = createClient({ url }); + const client = createLibsqlClient({ url }); const db = drizzleLibsql(client); return db; } diff --git a/packages/db/src/core/db-client/libsql-node.ts b/packages/db/src/core/db-client/libsql-node.ts index 3ab7bb1b646f..9f3fb1f34411 100644 --- a/packages/db/src/core/db-client/libsql-node.ts +++ b/packages/db/src/core/db-client/libsql-node.ts @@ -1,4 +1,4 @@ -import { createClient } from '@libsql/client'; +import { createClient as createLibsqlClient } from '@libsql/client'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; import { parseOpts } from '../../runtime/utils.js'; @@ -7,7 +7,7 @@ type RemoteDbClientOptions = { url: string; }; -export function createRemoteLibSQLClient(opts: RemoteDbClientOptions) { +export function createClient(opts: RemoteDbClientOptions) { const { token, url } = opts; const parsedUrl = new URL(url); @@ -36,6 +36,6 @@ export function createRemoteLibSQLClient(opts: RemoteDbClientOptions) { dbURL = 'file:' + parsedUrl.pathname.substring(1); } - const client = createClient({ ...parseOpts(options), url: dbURL, authToken: token }); + const client = createLibsqlClient({ ...parseOpts(options), url: dbURL, authToken: token }); return drizzleLibsql(client); } diff --git a/packages/db/src/core/db-client/libsql-web.ts b/packages/db/src/core/db-client/libsql-web.ts index 62ec97a5fb6c..be6afd124a08 100644 --- a/packages/db/src/core/db-client/libsql-web.ts +++ b/packages/db/src/core/db-client/libsql-web.ts @@ -1,4 +1,4 @@ -import { createClient } from '@libsql/client/web'; +import { createClient as createLibsqlClient } from '@libsql/client/web'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql/web'; import { parseOpts } from '../../runtime/utils.js'; @@ -7,7 +7,7 @@ type RemoteDbClientOptions = { url: string; }; -export function createRemoteLibSQLClient(opts: RemoteDbClientOptions) { +export function createClient(opts: RemoteDbClientOptions) { const { token, url } = opts; const parsedUrl = new URL(url); @@ -24,6 +24,6 @@ export function createRemoteLibSQLClient(opts: RemoteDbClientOptions) { ); } - const client = createClient({ ...parseOpts(options), url: dbURL, authToken: token }); + const client = createLibsqlClient({ ...parseOpts(options), url: dbURL, authToken: token }); return drizzleLibsql(client); } diff --git a/packages/db/src/core/integration/vite-plugin-db-client.ts b/packages/db/src/core/integration/vite-plugin-db-client.ts index e757540ae755..5a1c86824da9 100644 --- a/packages/db/src/core/integration/vite-plugin-db-client.ts +++ b/packages/db/src/core/integration/vite-plugin-db-client.ts @@ -9,11 +9,11 @@ type VitePluginDBClientParams = { function getRemoteClientModule(mode: 'node' | 'web') { switch (mode) { case 'web': { - return `export { createRemoteLibSQLClient as createClient } from '${DB_CLIENTS.web}';`; + return `export { createClient } from '${DB_CLIENTS.web}';`; } case 'node': default: - return `export { createRemoteLibSQLClient as createClient } from '${DB_CLIENTS.node}';`; + return `export { createClient } from '${DB_CLIENTS.node}';`; } } @@ -22,7 +22,7 @@ function getLocalClientModule(mode: 'node' | 'web') { case 'node': case 'web': default: - return `export { createLocalDatabaseClient as createClient } from '${DB_CLIENTS.local}';`; + return `export { createClient } from '${DB_CLIENTS.local}';`; } } diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 43c87427bedd..95a833f53de7 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -5,7 +5,7 @@ import { type SQL, sql } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { normalizeDatabaseUrl } from '../../runtime/index.js'; import { DB_CLIENTS, DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_CLIENT_MODULE_ID, VIRTUAL_MODULE_ID } from '../consts.js'; -import { createLocalDatabaseClient } from '../db-client/libsql-local.js'; +import { createClient } from '../db-client/libsql-local.js'; import { getResolvedFileUrl } from '../load-file.js'; import { getCreateIndexQueries, getCreateTableQuery, SEED_DEV_FILE_NAME } from '../queries.js'; import type { DBTables } from '../types.js'; @@ -224,7 +224,7 @@ const sqlite = new SQLiteAsyncDialect(); async function recreateTables({ tables, root }: { tables: LateTables; root: URL }) { const { ASTRO_DATABASE_FILE } = getAstroEnv(); const dbUrl = normalizeDatabaseUrl(ASTRO_DATABASE_FILE, new URL(DB_PATH, root).href); - const db = createLocalDatabaseClient({ url: dbUrl }); + const db = createClient({ url: dbUrl }); const setupQueries: SQL[] = []; for (const [name, table] of Object.entries(tables.get() ?? {})) { const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`); From 3bc6eac4917557f84a2d9940e36742ece0647fce Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 23:13:40 -0700 Subject: [PATCH 18/35] fix(db): standardize client creation function names across db-client modules --- packages/db/src/core/cli/commands/push/index.ts | 4 ++-- packages/db/src/core/cli/commands/shell/index.ts | 4 ++-- packages/db/src/core/cli/migration-queries.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index 2d443bb9cdc9..663b648119cf 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -3,7 +3,7 @@ import { sql } from 'drizzle-orm'; import prompts from 'prompts'; import type { Arguments } from 'yargs-parser'; import { MIGRATION_VERSION } from '../../../consts.js'; -import { createRemoteLibSQLClient } from '../../../db-client/libsql-node.js'; +import { createClient } from '../../../db-client/libsql-node.js'; import type { DBConfig, DBSnapshot } from '../../../types.js'; import { getRemoteDatabaseInfo, type RemoteDatabaseInfo } from '../../../utils.js'; import { @@ -108,7 +108,7 @@ type RequestBody = { }; async function pushToDb(requestBody: RequestBody, appToken: string, remoteUrl: string) { - const client = createRemoteLibSQLClient({ + const client = createClient({ token: appToken, url: remoteUrl, }); diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index c5b0f7a21201..7ac42f990809 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -3,8 +3,8 @@ import { sql } from 'drizzle-orm'; import type { Arguments } from 'yargs-parser'; import { normalizeDatabaseUrl } from '../../../../runtime/index.js'; import { DB_PATH } from '../../../consts.js'; -import { createLocalDatabaseClient } from '../../../db-client/libsql-local.js'; -import { createRemoteLibSQLClient } from '../../../db-client/libsql-node.js'; +import { createClient as createLocalDatabaseClient } from '../../../db-client/libsql-local.js'; +import { createClient as createRemoteLibSQLClient } from '../../../db-client/libsql-node.js'; import { SHELL_QUERY_MISSING_ERROR } from '../../../errors.js'; import type { DBConfigInput } from '../../../types.js'; import { getAstroEnv, getRemoteDatabaseInfo } from '../../../utils.js'; diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index ab4eb83252d1..5b662a3f3d22 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -7,7 +7,7 @@ import { customAlphabet } from 'nanoid'; import { isSerializedSQL } from '../../runtime/types.js'; import { hasPrimaryKey, isDbError } from '../../runtime/utils.js'; import { MIGRATION_VERSION } from '../consts.js'; -import { createRemoteLibSQLClient } from '../db-client/libsql-node.js'; +import { createClient } from '../db-client/libsql-node.js'; import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from '../errors.js'; import { getCreateIndexQueries, @@ -434,7 +434,7 @@ async function getDbCurrentSnapshot( appToken: string, remoteUrl: string, ): Promise { - const client = createRemoteLibSQLClient({ + const client = createClient({ token: appToken, url: remoteUrl, }); From 24a49b226cd6c89b6326f38dab543636d5354ed1 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 23:14:13 -0700 Subject: [PATCH 19/35] fix(db): rename remote database client import for consistency --- packages/db/src/core/cli/commands/shell/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 7ac42f990809..a4b61863c92e 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -4,7 +4,7 @@ import type { Arguments } from 'yargs-parser'; import { normalizeDatabaseUrl } from '../../../../runtime/index.js'; import { DB_PATH } from '../../../consts.js'; import { createClient as createLocalDatabaseClient } from '../../../db-client/libsql-local.js'; -import { createClient as createRemoteLibSQLClient } from '../../../db-client/libsql-node.js'; +import { createClient as createRemoteDatabaseClient } from '../../../db-client/libsql-node.js'; import { SHELL_QUERY_MISSING_ERROR } from '../../../errors.js'; import type { DBConfigInput } from '../../../types.js'; import { getAstroEnv, getRemoteDatabaseInfo } from '../../../utils.js'; @@ -24,7 +24,7 @@ export async function cmd({ } const dbInfo = getRemoteDatabaseInfo(); if (flags.remote) { - const db = createRemoteLibSQLClient(dbInfo); + const db = createRemoteDatabaseClient(dbInfo); const result = await db.run(sql.raw(query)); console.log(result); } else { From 92516ee7705e0ae0eab22e7c8bd352ad0517181f Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Fri, 8 Aug 2025 23:42:24 -0700 Subject: [PATCH 20/35] fix(db): change const to let for parsedUrl in createClient function --- packages/db/src/core/db-client/libsql-node.ts | 2 +- packages/db/src/core/db-client/libsql-web.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/db/src/core/db-client/libsql-node.ts b/packages/db/src/core/db-client/libsql-node.ts index 9f3fb1f34411..22238dabcf8c 100644 --- a/packages/db/src/core/db-client/libsql-node.ts +++ b/packages/db/src/core/db-client/libsql-node.ts @@ -10,7 +10,7 @@ type RemoteDbClientOptions = { export function createClient(opts: RemoteDbClientOptions) { const { token, url } = opts; - const parsedUrl = new URL(url); + let parsedUrl = new URL(url); const options: Record = Object.fromEntries(parsedUrl.searchParams.entries()); parsedUrl.search = ''; diff --git a/packages/db/src/core/db-client/libsql-web.ts b/packages/db/src/core/db-client/libsql-web.ts index be6afd124a08..7a7bb9c38ed1 100644 --- a/packages/db/src/core/db-client/libsql-web.ts +++ b/packages/db/src/core/db-client/libsql-web.ts @@ -10,7 +10,7 @@ type RemoteDbClientOptions = { export function createClient(opts: RemoteDbClientOptions) { const { token, url } = opts; - const parsedUrl = new URL(url); + let parsedUrl = new URL(url); const options: Record = Object.fromEntries(parsedUrl.searchParams.entries()); parsedUrl.search = ''; From c9e55927089fb580b72239fd87d88ae5543784b0 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 00:13:24 -0700 Subject: [PATCH 21/35] fix(db): update variable naming for consistency in createClient function --- packages/db/src/core/db-client/libsql-node.ts | 14 +++++++------- packages/db/src/core/db-client/libsql-web.ts | 10 ++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/db/src/core/db-client/libsql-node.ts b/packages/db/src/core/db-client/libsql-node.ts index 22238dabcf8c..05ee67690c37 100644 --- a/packages/db/src/core/db-client/libsql-node.ts +++ b/packages/db/src/core/db-client/libsql-node.ts @@ -8,22 +8,22 @@ type RemoteDbClientOptions = { }; export function createClient(opts: RemoteDbClientOptions) { - const { token, url } = opts; + const { token, url: rawUrl } = opts; - let parsedUrl = new URL(url); + let parsedUrl = new URL(rawUrl); const options: Record = Object.fromEntries(parsedUrl.searchParams.entries()); parsedUrl.search = ''; - let dbURL = parsedUrl.toString(); + let url = parsedUrl.toString(); if (parsedUrl.protocol === 'memory:') { // libSQL expects a special string in place of a URL // for in-memory DBs. - dbURL = ':memory:'; + url = ':memory:'; } else if ( parsedUrl.protocol === 'file:' && parsedUrl.pathname.startsWith('/') && - !dbURL.startsWith('file:/') + !url.startsWith('file:/') ) { // libSQL accepts relative and absolute file URLs // for local DBs. This doesn't match the URL specification. @@ -33,9 +33,9 @@ export function createClient(opts: RemoteDbClientOptions) { // This detects when such a conversion happened during parsing // and undoes it so that the URL given to libSQL is the // same as given by the user. - dbURL = 'file:' + parsedUrl.pathname.substring(1); + url = 'file:' + parsedUrl.pathname.substring(1); } - const client = createLibsqlClient({ ...parseOpts(options), url: dbURL, authToken: token }); + const client = createLibsqlClient({ ...parseOpts(options), url, authToken: token }); return drizzleLibsql(client); } diff --git a/packages/db/src/core/db-client/libsql-web.ts b/packages/db/src/core/db-client/libsql-web.ts index 7a7bb9c38ed1..6b72bb720b5f 100644 --- a/packages/db/src/core/db-client/libsql-web.ts +++ b/packages/db/src/core/db-client/libsql-web.ts @@ -8,13 +8,15 @@ type RemoteDbClientOptions = { }; export function createClient(opts: RemoteDbClientOptions) { - const { token, url } = opts; + const { token, url: rawUrl } = opts; - let parsedUrl = new URL(url); + let parsedUrl = new URL(rawUrl); + const options: Record = Object.fromEntries(parsedUrl.searchParams.entries()); + parsedUrl.search = ''; - let dbURL = parsedUrl.toString(); + let url = parsedUrl.toString(); const supportedProtocols = ['http:', 'https:', 'libsql:']; @@ -24,6 +26,6 @@ export function createClient(opts: RemoteDbClientOptions) { ); } - const client = createLibsqlClient({ ...parseOpts(options), url: dbURL, authToken: token }); + const client = createLibsqlClient({ ...parseOpts(options), url, authToken: token }); return drizzleLibsql(client); } From 736b2d2076bbbfe954a5521657fb1629a49230ef Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 00:42:54 -0700 Subject: [PATCH 22/35] fix(test): add logging for prodDbPath and ASTRO_DB_REMOTE_URL in libsql-remote tests --- packages/db/test/libsql-remote.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/db/test/libsql-remote.test.js b/packages/db/test/libsql-remote.test.js index 089db50f0f4e..ead68e8e7e82 100644 --- a/packages/db/test/libsql-remote.test.js +++ b/packages/db/test/libsql-remote.test.js @@ -54,6 +54,11 @@ describe('astro:db local database', () => { // Remove the file if it exists to avoid conflict between test runs await rm(prodDbPath, { force: true }); + console.log('============='); + console.log('prodDbPath', prodDbPath); + console.log('ASTRO_DB_REMOTE_URL', `file:${prodDbPath}`); + console.log('============='); + process.env.ASTRO_INTERNAL_TEST_REMOTE = true; process.env.ASTRO_DB_REMOTE_URL = `file:${prodDbPath}`; await fixture.build(); From 03892a86202cacbd91292c3d85bbe734cfaeb379 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 01:05:36 -0700 Subject: [PATCH 23/35] fix(test): add logging for absoluteFileUrl in libsql-remote tests --- packages/db/test/libsql-remote.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/db/test/libsql-remote.test.js b/packages/db/test/libsql-remote.test.js index ead68e8e7e82..6f07d652bf8a 100644 --- a/packages/db/test/libsql-remote.test.js +++ b/packages/db/test/libsql-remote.test.js @@ -55,6 +55,7 @@ describe('astro:db local database', () => { await rm(prodDbPath, { force: true }); console.log('============='); + console.log('absoluteFileUrl', absoluteFileUrl); console.log('prodDbPath', prodDbPath); console.log('ASTRO_DB_REMOTE_URL', `file:${prodDbPath}`); console.log('============='); From 95bf69738ac171b6750a0f7585b8a76e639ee278 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 01:33:38 -0700 Subject: [PATCH 24/35] fix(db): correct variable reference for rawUrl in createClient function --- packages/db/src/core/db-client/libsql-node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/src/core/db-client/libsql-node.ts b/packages/db/src/core/db-client/libsql-node.ts index 05ee67690c37..560ce7a7e5f9 100644 --- a/packages/db/src/core/db-client/libsql-node.ts +++ b/packages/db/src/core/db-client/libsql-node.ts @@ -23,7 +23,7 @@ export function createClient(opts: RemoteDbClientOptions) { } else if ( parsedUrl.protocol === 'file:' && parsedUrl.pathname.startsWith('/') && - !url.startsWith('file:/') + !rawUrl.startsWith('file:/') ) { // libSQL accepts relative and absolute file URLs // for local DBs. This doesn't match the URL specification. From 95dcef00e0a79be6b75bbc67ea8bbbcb9f94c172 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 02:06:37 -0700 Subject: [PATCH 25/35] fix(test): remove debug logging for prodDbPath in libsql-remote tests --- packages/db/test/libsql-remote.test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/db/test/libsql-remote.test.js b/packages/db/test/libsql-remote.test.js index 6f07d652bf8a..089db50f0f4e 100644 --- a/packages/db/test/libsql-remote.test.js +++ b/packages/db/test/libsql-remote.test.js @@ -54,12 +54,6 @@ describe('astro:db local database', () => { // Remove the file if it exists to avoid conflict between test runs await rm(prodDbPath, { force: true }); - console.log('============='); - console.log('absoluteFileUrl', absoluteFileUrl); - console.log('prodDbPath', prodDbPath); - console.log('ASTRO_DB_REMOTE_URL', `file:${prodDbPath}`); - console.log('============='); - process.env.ASTRO_INTERNAL_TEST_REMOTE = true; process.env.ASTRO_DB_REMOTE_URL = `file:${prodDbPath}`; await fixture.build(); From d7795ebe6cba2602beda0cc5d4e9db30f85b1246 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 05:07:26 -0700 Subject: [PATCH 26/35] fix(db): rename __execute to localExecution for clarity in virtual module loading --- packages/db/src/core/cli/commands/execute/index.ts | 4 ++-- .../src/core/integration/vite-plugin-db-client.ts | 3 +-- packages/db/src/core/integration/vite-plugin-db.ts | 14 ++++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts index e519c6784da2..0664e642d602 100644 --- a/packages/db/src/core/cli/commands/execute/index.ts +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -46,13 +46,13 @@ export async function cmd({ appToken: flags.token ?? dbInfo.token, isBuild: false, output: 'server', - __execute: true, + localExecution: true, }); } else { virtualModContents = getLocalVirtualModContents({ tables: dbConfig.tables ?? {}, root: astroConfig.root, - __execute: true, + localExecution: true, }); } const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl }); diff --git a/packages/db/src/core/integration/vite-plugin-db-client.ts b/packages/db/src/core/integration/vite-plugin-db-client.ts index 5a1c86824da9..3e47842fb385 100644 --- a/packages/db/src/core/integration/vite-plugin-db-client.ts +++ b/packages/db/src/core/integration/vite-plugin-db-client.ts @@ -8,9 +8,8 @@ type VitePluginDBClientParams = { function getRemoteClientModule(mode: 'node' | 'web') { switch (mode) { - case 'web': { + case 'web': return `export { createClient } from '${DB_CLIENTS.web}';`; - } case 'node': default: return `export { createClient } from '${DB_CLIENTS.node}';`; diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 95a833f53de7..566debc4ca7d 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -120,17 +120,18 @@ export function getConfigVirtualModContents() { export function getLocalVirtualModContents({ tables, root, - __execute = false, + localExecution = false, }: { tables: DBTables; root: URL; - __execute?: boolean; + // Request module to be loaded immediately in process + localExecution?: boolean; }) { const { ASTRO_DATABASE_FILE } = getAstroEnv(); const dbUrl = new URL(DB_PATH, root); // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. - const clientImport = __execute + const clientImport = localExecution ? `import { createClient } from '${DB_CLIENTS.node}';` : `import { createClient } from '${VIRTUAL_CLIENT_MODULE_ID}';`; @@ -152,13 +153,14 @@ export function getRemoteVirtualModContents({ appToken, isBuild, output, - __execute = false, // Used for execute command + localExecution = false, // Used for execute command }: { tables: DBTables; appToken: string; isBuild: boolean; output: AstroConfig['output']; - __execute?: boolean; + // Request module to be loaded immediately in process + localExecution?: boolean; }) { const dbInfo = getRemoteDatabaseInfo(); @@ -188,7 +190,7 @@ export function getRemoteVirtualModContents({ } // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. - const clientImport = __execute + const clientImport = localExecution ? `import { createClient } from '${DB_CLIENTS.node}';` : `import { createClient } from '${VIRTUAL_CLIENT_MODULE_ID}';`; From 8e4e5c5ea0be9a324b0210af9110d5b21e8962a5 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 05:24:31 -0700 Subject: [PATCH 27/35] refactor(db): replace parseOpts with parseLibSQLConfig for improved configuration handling --- packages/db/src/core/db-client/libsql-node.ts | 6 ++- packages/db/src/core/db-client/libsql-web.ts | 8 +-- packages/db/src/core/db-client/utils.ts | 49 +++++++++++++++++++ packages/db/src/runtime/utils.ts | 19 +------ packages/db/test/unit/db-client.test.js | 14 ++++-- 5 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 packages/db/src/core/db-client/utils.ts diff --git a/packages/db/src/core/db-client/libsql-node.ts b/packages/db/src/core/db-client/libsql-node.ts index 560ce7a7e5f9..d634e1cd9930 100644 --- a/packages/db/src/core/db-client/libsql-node.ts +++ b/packages/db/src/core/db-client/libsql-node.ts @@ -1,6 +1,6 @@ import { createClient as createLibsqlClient } from '@libsql/client'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; -import { parseOpts } from '../../runtime/utils.js'; +import { parseLibSQLConfig } from './utils.js'; type RemoteDbClientOptions = { token: string; @@ -36,6 +36,8 @@ export function createClient(opts: RemoteDbClientOptions) { url = 'file:' + parsedUrl.pathname.substring(1); } - const client = createLibsqlClient({ ...parseOpts(options), url, authToken: token }); + const libSQLOptions = parseLibSQLConfig({ ...options, url, authToken: token }); + + const client = createLibsqlClient(libSQLOptions); return drizzleLibsql(client); } diff --git a/packages/db/src/core/db-client/libsql-web.ts b/packages/db/src/core/db-client/libsql-web.ts index 6b72bb720b5f..11dd5bf29fb4 100644 --- a/packages/db/src/core/db-client/libsql-web.ts +++ b/packages/db/src/core/db-client/libsql-web.ts @@ -1,6 +1,6 @@ import { createClient as createLibsqlClient } from '@libsql/client/web'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql/web'; -import { parseOpts } from '../../runtime/utils.js'; +import { parseLibSQLConfig } from './utils.js'; type RemoteDbClientOptions = { token: string; @@ -11,7 +11,7 @@ export function createClient(opts: RemoteDbClientOptions) { const { token, url: rawUrl } = opts; let parsedUrl = new URL(rawUrl); - + const options: Record = Object.fromEntries(parsedUrl.searchParams.entries()); parsedUrl.search = ''; @@ -26,6 +26,8 @@ export function createClient(opts: RemoteDbClientOptions) { ); } - const client = createLibsqlClient({ ...parseOpts(options), url, authToken: token }); + const libSQLOptions = parseLibSQLConfig({ ...options, url, authToken: token }); + + const client = createLibsqlClient(libSQLOptions); return drizzleLibsql(client); } diff --git a/packages/db/src/core/db-client/utils.ts b/packages/db/src/core/db-client/utils.ts new file mode 100644 index 000000000000..bdd3d496b3ac --- /dev/null +++ b/packages/db/src/core/db-client/utils.ts @@ -0,0 +1,49 @@ +import type { Config as LibSQLConfig } from '@libsql/client'; +import z from 'zod'; + +const rawLibSQLOptions = z.record(z.string()); + +const parseNumber = (value: string) => z.coerce.number().parse(value); +const parseBoolean = (value: string) => z.coerce.boolean().parse(value); + +const libSQLConfigTransformed = rawLibSQLOptions.transform((raw) => { + // Ensure the URL is always present + const parsed: LibSQLConfig = { + url: raw.url, + }; + + // Optional fields + for (const [key, value] of Object.entries(raw)) { + switch (key) { + case 'syncInterval': + case 'concurrency': + parsed[key] = parseNumber(value); + break; + case 'readYourWrites': + case 'offline': + case 'tls': + parsed[key] = parseBoolean(value); + break; + case 'authToken': + case 'encryptionKey': + case 'syncUrl': + parsed[key] = value; + default: + throw new Error(`Unsupported LibSQL config option: ${key}`); + } + } + + // Return the parsed config + return parsed; +}); + +export const parseLibSQLConfig = (config: Record): LibSQLConfig => { + try { + return libSQLConfigTransformed.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Invalid LibSQL config: ${error.errors.map(e => e.message).join(', ')}`); + } + throw error; + } +} \ No newline at end of file diff --git a/packages/db/src/runtime/utils.ts b/packages/db/src/runtime/utils.ts index 55f55451e2a0..9af3f37570d5 100644 --- a/packages/db/src/runtime/utils.ts +++ b/packages/db/src/runtime/utils.ts @@ -1,4 +1,4 @@ -import { type Config as LibSQLConfig, LibsqlError } from '@libsql/client'; +import { LibsqlError } from '@libsql/client'; import { AstroError } from 'astro/errors'; import type { DBColumn } from '../core/types.js'; @@ -38,19 +38,4 @@ export function pathToFileURL(path: string): URL { // Unix is easy return new URL('file://' + path); -} - -// this function parses the options from a `Record` -// provided from the object conversion of the searchParams and properly -// verifies that the Config object is providing the correct types. -// without this, there is runtime errors due to incorrect values -export function parseOpts(config: Record): Partial { - return { - ...config, - ...(config.syncInterval ? { syncInterval: parseInt(config.syncInterval) } : {}), - ...('readYourWrites' in config ? { readYourWrites: config.readYourWrites !== 'false' } : {}), - ...('offline' in config ? { offline: config.offline !== 'false' } : {}), - ...('tls' in config ? { tls: config.tls !== 'false' } : {}), - ...(config.concurrency ? { concurrency: parseInt(config.concurrency) } : {}), - }; -} +} \ No newline at end of file diff --git a/packages/db/test/unit/db-client.test.js b/packages/db/test/unit/db-client.test.js index 08c2b920fd2b..f82399a0cc17 100644 --- a/packages/db/test/unit/db-client.test.js +++ b/packages/db/test/unit/db-client.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert'; import test, { describe } from 'node:test'; -import { parseOpts } from '../../dist/runtime/utils.js'; +import { parseLibSQLConfig } from '../../dist/core/db-client/utils.js'; describe('db client config', () => { test('parse config options from URL (docs example url)', () => { @@ -9,9 +9,10 @@ describe('db client config', () => { ); const options = Object.fromEntries(remoteURLToParse.searchParams.entries()); - const config = parseOpts(options); + const config = parseLibSQLConfig(options); assert.deepEqual(config, { + url: 'file://local-copy.db', encryptionKey: 'your-encryption-key', syncInterval: 60, syncUrl: 'libsql://your.server.io', @@ -22,9 +23,10 @@ describe('db client config', () => { const remoteURLToParse = new URL('file://local-copy.db?readYourWrites&offline&tls'); const options = Object.fromEntries(remoteURLToParse.searchParams.entries()); - const config = parseOpts(options); + const config = parseLibSQLConfig(options); assert.deepEqual(config, { + url: 'file://local-copy.db', readYourWrites: true, offline: true, tls: true, @@ -37,9 +39,10 @@ describe('db client config', () => { ); const options = Object.fromEntries(remoteURLToParse.searchParams.entries()); - const config = parseOpts(options); + const config = parseLibSQLConfig(options); assert.deepEqual(config, { + url: 'file://local-copy.db', readYourWrites: true, offline: true, tls: true, @@ -50,9 +53,10 @@ describe('db client config', () => { const remoteURLToParse = new URL('file://local-copy.db?syncInterval=60&concurrency=2'); const options = Object.fromEntries(remoteURLToParse.searchParams.entries()); - const config = parseOpts(options); + const config = parseLibSQLConfig(options); assert.deepEqual(config, { + url: 'file://local-copy.db', syncInterval: 60, concurrency: 2, }); From 400ea3919dc8649d43c7ca1b4027b557dee6a58f Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 05:26:39 -0700 Subject: [PATCH 28/35] fix(db): prevent redundant assignment for 'url' in LibSQL config parsing --- packages/db/src/core/db-client/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/db/src/core/db-client/utils.ts b/packages/db/src/core/db-client/utils.ts index bdd3d496b3ac..018afc7d2727 100644 --- a/packages/db/src/core/db-client/utils.ts +++ b/packages/db/src/core/db-client/utils.ts @@ -28,6 +28,10 @@ const libSQLConfigTransformed = rawLibSQLOptions.transform((raw) => { case 'encryptionKey': case 'syncUrl': parsed[key] = value; + break; + case 'url': + // Already handled above, no need to reassign + break; default: throw new Error(`Unsupported LibSQL config option: ${key}`); } From cd848b6197c84df0abd9dda2c68a991ec5fe79df Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 05:48:46 -0700 Subject: [PATCH 29/35] refactor(db): streamline libSQL configuration handling in createClient functions --- packages/db/src/core/db-client/libsql-node.ts | 4 ++-- packages/db/src/core/db-client/libsql-web.ts | 4 ++-- packages/db/src/core/db-client/utils.ts | 11 ++--------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/db/src/core/db-client/libsql-node.ts b/packages/db/src/core/db-client/libsql-node.ts index d634e1cd9930..1653d4047564 100644 --- a/packages/db/src/core/db-client/libsql-node.ts +++ b/packages/db/src/core/db-client/libsql-node.ts @@ -36,8 +36,8 @@ export function createClient(opts: RemoteDbClientOptions) { url = 'file:' + parsedUrl.pathname.substring(1); } - const libSQLOptions = parseLibSQLConfig({ ...options, url, authToken: token }); + const libSQLOptions = parseLibSQLConfig(options); - const client = createLibsqlClient(libSQLOptions); + const client = createLibsqlClient({ ...libSQLOptions, url, authToken: token }); return drizzleLibsql(client); } diff --git a/packages/db/src/core/db-client/libsql-web.ts b/packages/db/src/core/db-client/libsql-web.ts index 11dd5bf29fb4..fce032f61e60 100644 --- a/packages/db/src/core/db-client/libsql-web.ts +++ b/packages/db/src/core/db-client/libsql-web.ts @@ -26,8 +26,8 @@ export function createClient(opts: RemoteDbClientOptions) { ); } - const libSQLOptions = parseLibSQLConfig({ ...options, url, authToken: token }); + const libSQLOptions = parseLibSQLConfig(options); - const client = createLibsqlClient(libSQLOptions); + const client = createLibsqlClient({ ...libSQLOptions, url, authToken: token }); return drizzleLibsql(client); } diff --git a/packages/db/src/core/db-client/utils.ts b/packages/db/src/core/db-client/utils.ts index 018afc7d2727..c54891c2cb58 100644 --- a/packages/db/src/core/db-client/utils.ts +++ b/packages/db/src/core/db-client/utils.ts @@ -8,9 +8,7 @@ const parseBoolean = (value: string) => z.coerce.boolean().parse(value); const libSQLConfigTransformed = rawLibSQLOptions.transform((raw) => { // Ensure the URL is always present - const parsed: LibSQLConfig = { - url: raw.url, - }; + const parsed: Partial = {}; // Optional fields for (const [key, value] of Object.entries(raw)) { @@ -28,12 +26,7 @@ const libSQLConfigTransformed = rawLibSQLOptions.transform((raw) => { case 'encryptionKey': case 'syncUrl': parsed[key] = value; - break; - case 'url': - // Already handled above, no need to reassign break; - default: - throw new Error(`Unsupported LibSQL config option: ${key}`); } } @@ -41,7 +34,7 @@ const libSQLConfigTransformed = rawLibSQLOptions.transform((raw) => { return parsed; }); -export const parseLibSQLConfig = (config: Record): LibSQLConfig => { +export const parseLibSQLConfig = (config: Record): Partial => { try { return libSQLConfigTransformed.parse(config); } catch (error) { From 910081a68903f48d003beb699b6bb5f99079673a Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 06:13:44 -0700 Subject: [PATCH 30/35] fix(test): remove redundant 'url' field from config assertions in db-client tests --- packages/db/test/unit/db-client.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/db/test/unit/db-client.test.js b/packages/db/test/unit/db-client.test.js index f82399a0cc17..c1c27ce97a91 100644 --- a/packages/db/test/unit/db-client.test.js +++ b/packages/db/test/unit/db-client.test.js @@ -12,7 +12,6 @@ describe('db client config', () => { const config = parseLibSQLConfig(options); assert.deepEqual(config, { - url: 'file://local-copy.db', encryptionKey: 'your-encryption-key', syncInterval: 60, syncUrl: 'libsql://your.server.io', @@ -26,7 +25,6 @@ describe('db client config', () => { const config = parseLibSQLConfig(options); assert.deepEqual(config, { - url: 'file://local-copy.db', readYourWrites: true, offline: true, tls: true, @@ -42,7 +40,6 @@ describe('db client config', () => { const config = parseLibSQLConfig(options); assert.deepEqual(config, { - url: 'file://local-copy.db', readYourWrites: true, offline: true, tls: true, @@ -56,7 +53,6 @@ describe('db client config', () => { const config = parseLibSQLConfig(options); assert.deepEqual(config, { - url: 'file://local-copy.db', syncInterval: 60, concurrency: 2, }); From 89153ece33f2c8d6409027ac9857067f46167ca7 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sat, 9 Aug 2025 06:41:38 -0700 Subject: [PATCH 31/35] refactor(db): enhance boolean parsing logic in LibSQL config handling --- packages/db/src/core/db-client/utils.ts | 47 +++++++++++++++---------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/db/src/core/db-client/utils.ts b/packages/db/src/core/db-client/utils.ts index c54891c2cb58..7c4d127266c7 100644 --- a/packages/db/src/core/db-client/utils.ts +++ b/packages/db/src/core/db-client/utils.ts @@ -6,11 +6,22 @@ const rawLibSQLOptions = z.record(z.string()); const parseNumber = (value: string) => z.coerce.number().parse(value); const parseBoolean = (value: string) => z.coerce.boolean().parse(value); +const booleanValues = ['true', 'false']; + +// parse a value that should be a boolean, but could be a valueless variable) +// e.g. 'file://local-copy.db?readYourWrites' & 'file://local-copy.db?readYourWrites=true' should be parsed as true +const parseOptionalBoolean = (value: string) => { + if (booleanValues.includes(value)) { + return parseBoolean(value); + } + return true; // If the value is not explicitly 'true' or 'false', assume it's true (valueless variable) +}; + const libSQLConfigTransformed = rawLibSQLOptions.transform((raw) => { - // Ensure the URL is always present + // Ensure the URL is always present const parsed: Partial = {}; - // Optional fields + // Optional fields for (const [key, value] of Object.entries(raw)) { switch (key) { case 'syncInterval': @@ -20,27 +31,27 @@ const libSQLConfigTransformed = rawLibSQLOptions.transform((raw) => { case 'readYourWrites': case 'offline': case 'tls': - parsed[key] = parseBoolean(value); + parsed[key] = parseOptionalBoolean(value); break; - case 'authToken': - case 'encryptionKey': - case 'syncUrl': + case 'authToken': + case 'encryptionKey': + case 'syncUrl': parsed[key] = value; - break; + break; } } - // Return the parsed config - return parsed; + // Return the parsed config + return parsed; }); export const parseLibSQLConfig = (config: Record): Partial => { - try { - return libSQLConfigTransformed.parse(config); - } catch (error) { - if (error instanceof z.ZodError) { - throw new Error(`Invalid LibSQL config: ${error.errors.map(e => e.message).join(', ')}`); - } - throw error; - } -} \ No newline at end of file + try { + return libSQLConfigTransformed.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Invalid LibSQL config: ${error.errors.map((e) => e.message).join(', ')}`); + } + throw error; + } +}; From f0311bcea016f084d490c82c3df0790869e757bf Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Mon, 11 Aug 2025 05:13:00 -0700 Subject: [PATCH 32/35] Apply suggestions from code review Thank god for the save by the docs queen Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/full-dingos-repeat.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.changeset/full-dingos-repeat.md b/.changeset/full-dingos-repeat.md index 4d34a9116c40..38f3942a0878 100644 --- a/.changeset/full-dingos-repeat.md +++ b/.changeset/full-dingos-repeat.md @@ -2,9 +2,12 @@ '@astrojs/db': minor --- -Adds support for environments such as Cloudflare or Deno that require a non-node based libsql client. +Adds a new libSQL web driver to support environments that require a non-Node.js libSQL client such as Cloudflare or Deno. Also adds a new `mode` configuration option to allow you to set your client connection type: `node` (default) or `web`. -To utilize this new feature, you must add the following to your Astro Db config. This will enable the usage of the alterative LibSQL web driver. In most cases this should only be needed on Cloudflare or Deno type environments, and using the default mode `node` will be enough for normal usage. + +The default db `node` driver mode is identical to the previous AstroDB functionality. No changes have been made to how AstroDB works in Node.js environments, and this is still the integration's default behavior. If you are currently using AstroDB, no changes to your project code are required and setting a `mode` is not required. + +However, if you have previously been unable to use AstroDB because you required a non-Node.js libSQL client, you can now install and configure the libSQL web driver by setting `mode: 'web'` in your `db` configuration: ```ts import db from '@astrojs/db'; @@ -14,4 +17,6 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ integrations: [db({ mode: 'web' })], }); -``` \ No newline at end of file +``` + +For more information, see the [`@astrojs/db` documentation](https://docs.astro.build/en/guides/integrations-guide/db/). \ No newline at end of file From 9fde9ecc0a9ab46a6072651cfdcb95f21fd22441 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Mon, 11 Aug 2025 06:20:32 -0700 Subject: [PATCH 33/35] Apply suggestions from code review Co-authored-by: Emanuele Stoppa --- packages/db/src/core/db-client/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/src/core/db-client/utils.ts b/packages/db/src/core/db-client/utils.ts index 7c4d127266c7..69e56e4ee8c5 100644 --- a/packages/db/src/core/db-client/utils.ts +++ b/packages/db/src/core/db-client/utils.ts @@ -8,7 +8,7 @@ const parseBoolean = (value: string) => z.coerce.boolean().parse(value); const booleanValues = ['true', 'false']; -// parse a value that should be a boolean, but could be a valueless variable) +// parse a value that should be a boolean, but could be a valueless variable: // e.g. 'file://local-copy.db?readYourWrites' & 'file://local-copy.db?readYourWrites=true' should be parsed as true const parseOptionalBoolean = (value: string) => { if (booleanValues.includes(value)) { From 5604a6f6d351627fa22f00e3039ad92e7e932981 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Mon, 11 Aug 2025 06:28:19 -0700 Subject: [PATCH 34/35] Refactor localExecution handling in vitePluginDb and related functions --- .../db/src/core/integration/vite-plugin-db.ts | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 566debc4ca7d..1e6bc96088b0 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -4,7 +4,14 @@ import type { AstroConfig, AstroIntegrationLogger } from 'astro'; import { type SQL, sql } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { normalizeDatabaseUrl } from '../../runtime/index.js'; -import { DB_CLIENTS, DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_CLIENT_MODULE_ID, VIRTUAL_MODULE_ID } from '../consts.js'; +import { + DB_CLIENTS, + DB_PATH, + RUNTIME_IMPORT, + RUNTIME_VIRTUAL_IMPORT, + VIRTUAL_CLIENT_MODULE_ID, + VIRTUAL_MODULE_ID, +} from '../consts.js'; import { createClient } from '../db-client/libsql-local.js'; import { getResolvedFileUrl } from '../load-file.js'; import { getCreateIndexQueries, getCreateTableQuery, SEED_DEV_FILE_NAME } from '../queries.js'; @@ -77,6 +84,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { tables: params.tables.get(), isBuild: command === 'build', output: params.output, + localExecution: false, }); } @@ -87,6 +95,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { return getLocalVirtualModContents({ root: params.root, tables: params.tables.get(), + localExecution: false, }); } @@ -108,6 +117,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { return getLocalVirtualModContents({ root: params.root, tables: params.tables.get(), + localExecution: false, }); }, }; @@ -117,23 +127,43 @@ export function getConfigVirtualModContents() { return `export * from ${RUNTIME_VIRTUAL_IMPORT}`; } +/** + * Get the module import for the DB client. + * This is used to pick which module to import based on whether + * the DB client is being used by the CLI, or in the Astro runtime. + * + * This is important for the `astro db execute` command to work correctly. + * + * @param localExecution - Whether the DB client is being used in a local execution context (e.g. CLI commands). + * @returns The module import string for the DB client. + */ +function getDBModule(localExecution: boolean) { + return localExecution + ? `import { createClient } from '${DB_CLIENTS.node}';` + : `import { createClient } from '${VIRTUAL_CLIENT_MODULE_ID}';`; +} + export function getLocalVirtualModContents({ tables, root, - localExecution = false, + localExecution, }: { tables: DBTables; root: URL; - // Request module to be loaded immediately in process - localExecution?: boolean; + /** + * Used for the execute command to import the client directly. + * In other cases, we use the runtime only vite virtual module. + * + * This is used to ensure that the client is imported correctly + * when executing commands like `astro db execute`. + */ + localExecution: boolean; }) { const { ASTRO_DATABASE_FILE } = getAstroEnv(); const dbUrl = new URL(DB_PATH, root); // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. - const clientImport = localExecution - ? `import { createClient } from '${DB_CLIENTS.node}';` - : `import { createClient } from '${VIRTUAL_CLIENT_MODULE_ID}';`; + const clientImport = getDBModule(localExecution); return ` import { asDrizzleTable, normalizeDatabaseUrl } from ${RUNTIME_IMPORT}; @@ -153,14 +183,20 @@ export function getRemoteVirtualModContents({ appToken, isBuild, output, - localExecution = false, // Used for execute command + localExecution, }: { tables: DBTables; appToken: string; isBuild: boolean; output: AstroConfig['output']; - // Request module to be loaded immediately in process - localExecution?: boolean; + /** + * Used for the execute command to import the client directly. + * In other cases, we use the runtime only vite virtual module. + * + * This is used to ensure that the client is imported correctly + * when executing commands like `astro db execute`. + */ + localExecution: boolean; }) { const dbInfo = getRemoteDatabaseInfo(); @@ -190,9 +226,7 @@ export function getRemoteVirtualModContents({ } // If this is for the execute command, we need to import the client directly instead of using the runtime only virtual module. - const clientImport = localExecution - ? `import { createClient } from '${DB_CLIENTS.node}';` - : `import { createClient } from '${VIRTUAL_CLIENT_MODULE_ID}';`; + const clientImport = getDBModule(localExecution); return ` import {asDrizzleTable} from ${RUNTIME_IMPORT}; From c45022efb2cadad2b2aa451627f11497527b99e8 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 24 Aug 2025 20:01:23 -0700 Subject: [PATCH 35/35] Update .changeset/full-dingos-repeat.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/full-dingos-repeat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/full-dingos-repeat.md b/.changeset/full-dingos-repeat.md index 38f3942a0878..215ebe1e3bf5 100644 --- a/.changeset/full-dingos-repeat.md +++ b/.changeset/full-dingos-repeat.md @@ -19,4 +19,4 @@ export default defineConfig({ }); ``` -For more information, see the [`@astrojs/db` documentation](https://docs.astro.build/en/guides/integrations-guide/db/). \ No newline at end of file +For more information, see the [`@astrojs/db` documentation](https://docs.astro.build/en/guides/integrations-guide/db/#mode). \ No newline at end of file