diff --git a/.changeset/empty-spoons-kiss.md b/.changeset/empty-spoons-kiss.md new file mode 100644 index 000000000000..8bb114ef6ff9 --- /dev/null +++ b/.changeset/empty-spoons-kiss.md @@ -0,0 +1,5 @@ +--- +'@astrojs/db': patch +--- + +Fixes mixed environment variable for app token when using DB commands with libSQL remote. diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts index c6c11cdbb6b9..9a5a1b8e2f90 100644 --- a/packages/db/src/core/cli/commands/execute/index.ts +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -1,5 +1,4 @@ import { existsSync } from 'node:fs'; -import { getManagedAppTokenOrExit } from '@astrojs/studio'; import { LibsqlError } from '@libsql/client'; import type { AstroConfig } from 'astro'; import { green } from 'kleur/colors'; @@ -16,6 +15,7 @@ import { } from '../../../integration/vite-plugin-db.js'; import { bundleFile, importBundledFile } from '../../../load-file.js'; import type { DBConfig } from '../../../types.js'; +import { getManagedRemoteToken } from '../../../utils.js'; export async function cmd({ astroConfig, @@ -40,7 +40,7 @@ export async function cmd({ let virtualModContents: string; if (flags.remote) { - const appToken = await getManagedAppTokenOrExit(flags.token); + const appToken = await getManagedRemoteToken(flags.token); virtualModContents = getStudioVirtualModContents({ tables: dbConfig.tables ?? {}, appToken: appToken.token, diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index 0efa7ebcbf71..0b7b32bf9498 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -1,4 +1,3 @@ -import { getManagedAppTokenOrExit } from '@astrojs/studio'; import type { AstroConfig } from 'astro'; import { sql } from 'drizzle-orm'; import prompts from 'prompts'; @@ -7,7 +6,7 @@ import { createRemoteDatabaseClient } from '../../../../runtime/index.js'; import { safeFetch } from '../../../../runtime/utils.js'; import { MIGRATION_VERSION } from '../../../consts.js'; import type { DBConfig, DBSnapshot } from '../../../types.js'; -import { type Result, getRemoteDatabaseInfo } from '../../../utils.js'; +import { type RemoteDatabaseInfo, type Result, getManagedRemoteToken, getRemoteDatabaseInfo } from '../../../utils.js'; import { createCurrentSnapshot, createEmptySnapshot, @@ -26,8 +25,12 @@ export async function cmd({ }) { const isDryRun = flags.dryRun; const isForceReset = flags.forceReset; - const appToken = await getManagedAppTokenOrExit(flags.token); - const productionSnapshot = await getProductionCurrentSnapshot({ appToken: appToken.token }); + const dbInfo = getRemoteDatabaseInfo(); + const appToken = await getManagedRemoteToken(flags.token, dbInfo); + const productionSnapshot = await getProductionCurrentSnapshot({ + dbInfo, + appToken: appToken.token, + }); const currentSnapshot = createCurrentSnapshot(dbConfig); const isFromScratch = !productionSnapshot; const { queries: migrationQueries, confirmations } = await getMigrationQueries({ @@ -68,6 +71,7 @@ export async function cmd({ console.log(`Pushing database schema updates...`); await pushSchema({ statements: migrationQueries, + dbInfo, appToken: appToken.token, isDryRun, currentSnapshot: currentSnapshot, @@ -80,11 +84,13 @@ export async function cmd({ async function pushSchema({ statements, + dbInfo, appToken, isDryRun, currentSnapshot, }: { statements: string[]; + dbInfo: RemoteDatabaseInfo; appToken: string; isDryRun: boolean; currentSnapshot: DBSnapshot; @@ -99,8 +105,6 @@ async function pushSchema({ return new Response(null, { status: 200 }); } - const dbInfo = getRemoteDatabaseInfo(); - return dbInfo.type === 'studio' ? pushToStudio(requestBody, appToken, dbInfo.url) : pushToDb(requestBody, appToken, dbInfo.url); diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 0b5c2c2f06e4..8237fdc1be38 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -1,4 +1,3 @@ -import { getManagedAppTokenOrExit } from '@astrojs/studio'; import type { AstroConfig } from 'astro'; import { sql } from 'drizzle-orm'; import type { Arguments } from 'yargs-parser'; @@ -10,7 +9,7 @@ import { normalizeDatabaseUrl } from '../../../../runtime/index.js'; import { DB_PATH } from '../../../consts.js'; import { SHELL_QUERY_MISSING_ERROR } from '../../../errors.js'; import type { DBConfigInput } from '../../../types.js'; -import { getAstroEnv, getRemoteDatabaseInfo } from '../../../utils.js'; +import { getAstroEnv, getManagedRemoteToken, getRemoteDatabaseInfo } from '../../../utils.js'; export async function cmd({ flags, @@ -27,7 +26,7 @@ export async function cmd({ } const dbInfo = getRemoteDatabaseInfo(); if (flags.remote) { - const appToken = await getManagedAppTokenOrExit(flags.token); + const appToken = await getManagedRemoteToken(flags.token, dbInfo); const db = createRemoteDatabaseClient({ dbType: dbInfo.type, remoteUrl: dbInfo.url, diff --git a/packages/db/src/core/cli/commands/verify/index.ts b/packages/db/src/core/cli/commands/verify/index.ts index 950fb661567c..d535fba1feb9 100644 --- a/packages/db/src/core/cli/commands/verify/index.ts +++ b/packages/db/src/core/cli/commands/verify/index.ts @@ -1,4 +1,3 @@ -import { getManagedAppTokenOrExit } from '@astrojs/studio'; import type { AstroConfig } from 'astro'; import type { Arguments } from 'yargs-parser'; import type { DBConfig } from '../../../types.js'; @@ -9,6 +8,7 @@ import { getMigrationQueries, getProductionCurrentSnapshot, } from '../../migration-queries.js'; +import { getManagedRemoteToken, getRemoteDatabaseInfo } from '../../../utils.js'; export async function cmd({ dbConfig, @@ -19,8 +19,12 @@ export async function cmd({ flags: Arguments; }) { const isJson = flags.json; - const appToken = await getManagedAppTokenOrExit(flags.token); - const productionSnapshot = await getProductionCurrentSnapshot({ appToken: appToken.token }); + const dbInfo = getRemoteDatabaseInfo(); + const appToken = await getManagedRemoteToken(flags.token, dbInfo); + const productionSnapshot = await getProductionCurrentSnapshot({ + dbInfo, + appToken: appToken.token, + }); const currentSnapshot = createCurrentSnapshot(dbConfig); const { queries: migrationQueries, confirmations } = await getMigrationQueries({ oldSnapshot: productionSnapshot || createEmptySnapshot(), diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index 5c4a23557df2..6f0e0a5e26cf 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -35,7 +35,7 @@ import type { ResolvedIndexes, TextColumn, } from '../types.js'; -import { type Result, getRemoteDatabaseInfo } from '../utils.js'; +import type { RemoteDatabaseInfo, Result } from '../utils.js'; const sqlite = new SQLiteAsyncDialect(); const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); @@ -425,13 +425,12 @@ function hasRuntimeDefault(column: DBColumn): column is DBColumnWithDefault { } export function getProductionCurrentSnapshot(options: { + dbInfo: RemoteDatabaseInfo; appToken: string; }): Promise { - const dbInfo = getRemoteDatabaseInfo(); - - return dbInfo.type === 'studio' - ? getStudioCurrentSnapshot(options.appToken, dbInfo.url) - : getDbCurrentSnapshot(options.appToken, dbInfo.url); + return options.dbInfo.type === 'studio' + ? getStudioCurrentSnapshot(options.appToken, options.dbInfo.url) + : getDbCurrentSnapshot(options.appToken, options.dbInfo.url); } async function getDbCurrentSnapshot( diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index 263fa250a1b9..1b1eeec8e1fc 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { type ManagedAppToken, getManagedAppTokenOrExit } from '@astrojs/studio'; +import type { ManagedAppToken } from '@astrojs/studio'; import { LibsqlError } from '@libsql/client'; import type { AstroConfig, AstroIntegration } from 'astro'; import { blue, yellow } from 'kleur/colors'; @@ -20,7 +20,7 @@ import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js'; import { EXEC_DEFAULT_EXPORT_ERROR, EXEC_ERROR } from '../errors.js'; import { resolveDbConfig } from '../load-file.js'; import { SEED_DEV_FILE_NAME } from '../queries.js'; -import { type VitePlugin, getDbDirectoryUrl } from '../utils.js'; +import { type VitePlugin, getDbDirectoryUrl, getManagedRemoteToken } from '../utils.js'; import { fileURLIntegration } from './file-url.js'; import { getDtsContent } from './typegen.js'; import { @@ -32,7 +32,7 @@ import { } from './vite-plugin-db.js'; function astroDBIntegration(): AstroIntegration { - let connectToStudio = false; + let connectToRemote = false; let configFileDependencies: string[] = []; let root: URL; let appToken: ManagedAppToken | undefined; @@ -72,12 +72,12 @@ function astroDBIntegration(): AstroIntegration { let dbPlugin: VitePlugin | undefined = undefined; const args = parseArgs(process.argv.slice(3)); - connectToStudio = process.env.ASTRO_INTERNAL_TEST_REMOTE || args['remote']; + connectToRemote = process.env.ASTRO_INTERNAL_TEST_REMOTE || args['remote']; - if (connectToStudio) { - appToken = await getManagedAppTokenOrExit(); + if (connectToRemote) { + appToken = await getManagedRemoteToken(); dbPlugin = vitePluginDb({ - connectToStudio, + connectToStudio: connectToRemote, appToken: appToken.token, tables, root: config.root, @@ -116,7 +116,7 @@ function astroDBIntegration(): AstroIntegration { configFileDependencies = dependencies; const localDbUrl = new URL(DB_PATH, config.root); - if (!connectToStudio && !existsSync(localDbUrl)) { + if (!connectToRemote && !existsSync(localDbUrl)) { await mkdir(dirname(fileURLToPath(localDbUrl)), { recursive: true }); await writeFile(localDbUrl, ''); } @@ -143,9 +143,9 @@ function astroDBIntegration(): AstroIntegration { // Wait for dev server log before showing "connected". setTimeout(() => { logger.info( - connectToStudio ? 'Connected to remote database.' : 'New local database created.', + connectToRemote ? 'Connected to remote database.' : 'New local database created.', ); - if (connectToStudio) return; + if (connectToRemote) return; const localSeedPaths = SEED_DEV_FILE_NAME.map( (name) => new URL(name, getDbDirectoryUrl(root)), @@ -160,7 +160,7 @@ function astroDBIntegration(): AstroIntegration { }, 'astro:build:start': async ({ logger }) => { if ( - !connectToStudio && + !connectToRemote && !databaseFileEnvDefined() && (output === 'server' || output === 'hybrid') ) { @@ -170,7 +170,7 @@ function astroDBIntegration(): AstroIntegration { throw new AstroDbError(message, hint); } - logger.info('database: ' + (connectToStudio ? yellow('remote') : blue('local database.'))); + logger.info('database: ' + (connectToRemote ? yellow('remote') : blue('local database.'))); }, 'astro:build:setup': async ({ vite }) => { tempViteServer = await getTempViteServer({ viteConfig: vite }); @@ -178,7 +178,7 @@ function astroDBIntegration(): AstroIntegration { await executeSeedFile({ fileUrl, viteServer: tempViteServer! }); }; }, - 'astro:build:done': async ({}) => { + 'astro:build:done': async ({ }) => { await appToken?.destroy(); await tempViteServer?.close(); }, diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts index cf3e375354cc..53732c5db3c1 100644 --- a/packages/db/src/core/utils.ts +++ b/packages/db/src/core/utils.ts @@ -1,4 +1,4 @@ -import { getAstroStudioEnv } from '@astrojs/studio'; +import { getAstroStudioEnv, getManagedAppTokenOrExit, type ManagedAppToken } from '@astrojs/studio'; import type { AstroConfig, AstroIntegration } from 'astro'; import { loadEnv } from 'vite'; import './types.js'; @@ -37,6 +37,22 @@ export function getRemoteDatabaseInfo(): RemoteDatabaseInfo { }; } +export function getManagedRemoteToken(token?: string, dbInfo?: RemoteDatabaseInfo): Promise { + dbInfo ??= getRemoteDatabaseInfo(); + + if (dbInfo.type === 'studio') { + return getManagedAppTokenOrExit(token) + } + + const astroEnv = getAstroEnv(); + + return Promise.resolve({ + token: token ?? astroEnv.ASTRO_DB_APP_TOKEN, + renew: () => Promise.resolve(), + destroy: () => Promise.resolve(), + }); +} + export function getDbDirectoryUrl(root: URL | string) { return new URL('db/', root); } diff --git a/packages/db/test/basics.test.js b/packages/db/test/basics.test.js index c85880bef678..8622740eac8d 100644 --- a/packages/db/test/basics.test.js +++ b/packages/db/test/basics.test.js @@ -3,7 +3,7 @@ import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import testAdapter from '../../astro/test/test-adapter.js'; import { loadFixture } from '../../astro/test/test-utils.js'; -import { setupRemoteDbServer } from './test-utils.js'; +import { clearEnvironment, setupRemoteDbServer } from './test-utils.js'; describe('astro:db', () => { let fixture; @@ -19,6 +19,7 @@ describe('astro:db', () => { let devServer; before(async () => { + clearEnvironment(); devServer = await fixture.startDevServer(); }); @@ -98,6 +99,7 @@ describe('astro:db', () => { let remoteDbServer; before(async () => { + clearEnvironment(); remoteDbServer = await setupRemoteDbServer(fixture.config); devServer = await fixture.startDevServer(); }); @@ -178,6 +180,7 @@ describe('astro:db', () => { let remoteDbServer; before(async () => { + clearEnvironment(); process.env.ASTRO_STUDIO_APP_TOKEN = 'some token'; remoteDbServer = await setupRemoteDbServer(fixture.config); await fixture.build(); diff --git a/packages/db/test/static-remote.test.js b/packages/db/test/static-remote.test.js index ddfe735e5540..bbe71539c065 100644 --- a/packages/db/test/static-remote.test.js +++ b/packages/db/test/static-remote.test.js @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import { loadFixture } from '../../astro/test/test-utils.js'; -import { setupRemoteDbServer } from './test-utils.js'; +import { clearEnvironment, setupRemoteDbServer } from './test-utils.js'; describe('astro:db', () => { let fixture; @@ -44,6 +44,7 @@ describe('astro:db', () => { let remoteDbServer; before(async () => { + clearEnvironment(); process.env.ASTRO_DB_REMOTE_URL = `memory:`; await fixture.build(); }); diff --git a/packages/db/test/test-utils.js b/packages/db/test/test-utils.js index 3ad5d3c14230..bf66cdb1ce2c 100644 --- a/packages/db/test/test-utils.js +++ b/packages/db/test/test-utils.js @@ -64,6 +64,18 @@ export async function setupRemoteDbServer(astroConfig) { }; } +/** + * Clears the environment variables related to Astro DB and Astro Studio. + */ +export function clearEnvironment() { + const keys = Array.from(Object.keys(process.env)); + for (const key of keys) { + if (key.startsWith('ASTRO_DB_') || key.startsWith('ASTRO_STUDIO_')) { + delete process.env[key]; + } + } +} + function createRemoteDbServer() { const dbClient = createClient({ url: ':memory:', diff --git a/packages/db/test/unit/remote-info.test.js b/packages/db/test/unit/remote-info.test.js new file mode 100644 index 000000000000..909feddd7c54 --- /dev/null +++ b/packages/db/test/unit/remote-info.test.js @@ -0,0 +1,117 @@ +import test, { after, beforeEach, describe } from "node:test"; +import assert from "node:assert"; +import { getRemoteDatabaseInfo, getManagedRemoteToken } from '../../dist/core/utils.js'; +import { clearEnvironment } from "../test-utils.js"; + +describe('RemoteDatabaseInfo', () => { + beforeEach(() => { + clearEnvironment(); + }); + + test('default remote info', () => { + const dbInfo = getRemoteDatabaseInfo(); + + assert.deepEqual(dbInfo, { + type: 'studio', + url: 'https://db.services.astro.build', + }); + }); + + test('configured Astro Studio remote', () => { + process.env.ASTRO_STUDIO_REMOTE_DB_URL = 'https://studio.astro.build'; + const dbInfo = getRemoteDatabaseInfo(); + + assert.deepEqual(dbInfo, { + type: 'studio', + url: 'https://studio.astro.build', + }); + }); + + test('configured libSQL remote', () => { + process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted'; + const dbInfo = getRemoteDatabaseInfo(); + + assert.deepEqual(dbInfo, { + type: 'libsql', + url: 'libsql://libsql.self.hosted', + }); + }); + + test('configured both libSQL and Studio remote', () => { + process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted'; + process.env.ASTRO_STUDIO_REMOTE_DB_URL = 'https://studio.astro.build'; + const dbInfo = getRemoteDatabaseInfo(); + + assert.deepEqual(dbInfo, { + type: 'studio', + url: 'https://studio.astro.build', + }); + }); +}); + +describe('RemoteManagedToken', () => { + // Avoid conflicts with other tests + beforeEach(() => { + clearEnvironment(); + process.env.ASTRO_STUDIO_APP_TOKEN = 'studio token' + process.env.ASTRO_DB_APP_TOKEN = 'db token' + }); + after(() => { clearEnvironment(); }); + + test('given token for default remote', async () => { + const { token } = await getManagedRemoteToken('given token'); + assert.equal(token, 'given token'); + }); + + test('token for default remote', async () => { + const { token } = await getManagedRemoteToken(); + + assert.equal(token, 'studio token'); + }); + + test('given token for configured Astro Studio remote', async () => { + process.env.ASTRO_STUDIO_REMOTE_DB_URL = 'https://studio.astro.build'; + const { token } = await getManagedRemoteToken('given token'); + assert.equal(token, 'given token'); + }); + + test('token for configured Astro Studio remote', async () => { + process.env.ASTRO_STUDIO_REMOTE_DB_URL = 'https://studio.astro.build'; + const { token } = await getManagedRemoteToken(); + + assert.equal(token, 'studio token'); + }); + + test('given token for configured libSQL remote', async () => { + process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted'; + const { token } = await getManagedRemoteToken('given token'); + assert.equal(token, 'given token'); + }); + + test('token for configured libSQL remote', async () => { + process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted'; + const { token } = await getManagedRemoteToken(); + + assert.equal(token, 'db token'); + }); + + test('token for given Astro Studio remote', async () => { + process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted'; + const { token } = await getManagedRemoteToken(undefined, { + type: 'studio', + url: 'https://studio.astro.build', + }); + + assert.equal(token, 'studio token'); + }); + + test('token for given libSQL remote', async () => { + process.env.ASTRO_STUDIO_REMOTE_URL = 'libsql://libsql.self.hosted'; + const { token } = await getManagedRemoteToken(undefined, { + type: 'libsql', + url: 'libsql://libsql.self.hosted', + }); + + assert.equal(token, 'db token'); + }); +});