Skip to content
5 changes: 5 additions & 0 deletions .changeset/happy-rooms-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/db': minor
---

Add --db-app-token CLI flag to astro db execute, push, query, and verify commands
8 changes: 4 additions & 4 deletions packages/astro/test/custom-404-implicit-rerouting.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ for (const caseNumber of [1, 2, 3, 4, 5]) {
});

// sanity check
it('dev server handles normal requests', { timeout: 1000 }, async () => {
it('dev server handles normal requests', { timeout: 3000 }, async () => {
const response = await fixture.fetch('/');
assert.equal(response.status, 200);
});

// IMPORTANT: never skip
it('dev server stays responsive', { timeout: 1000 }, async () => {
it('dev server stays responsive', { timeout: 3000 }, async () => {
const response = await fixture.fetch('/alvsibdlvjks');
assert.equal(response.status, 404);
});
Expand All @@ -52,15 +52,15 @@ for (const caseNumber of [1, 2, 3, 4, 5]) {
});

// sanity check
it('prod server handles normal requests', { timeout: 1000 }, async () => {
it('prod server handles normal requests', { timeout: 3000 }, async () => {
const response = await app.render(new Request('https://example.com/'));
assert.equal(response.status, 200);
});

// IMPORTANT: never skip
it(
'prod server stays responsive for case number ' + caseNumber,
{ timeout: 1000 },
{ timeout: 3000 },
async () => {
const response = await app.render(new Request('https://example.com/alvsibdlvjks'));
assert.equal(response.status, 404);
Expand Down
5 changes: 3 additions & 2 deletions packages/db/src/core/cli/commands/execute/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '../../../integration/vite-plugin-db.js';
import { bundleFile, importBundledFile } from '../../../load-file.js';
import type { DBConfig } from '../../../types.js';
import { getRemoteDatabaseInfo } from '../../../utils.js';
import { getRemoteDatabaseInfo, resolveDbAppToken } from '../../../utils.js';

export async function cmd({
astroConfig,
Expand All @@ -41,9 +41,10 @@ export async function cmd({
let virtualModContents: string;
if (flags.remote) {
const dbInfo = getRemoteDatabaseInfo();
const appToken = resolveDbAppToken(flags, dbInfo.token);
virtualModContents = getRemoteVirtualModContents({
tables: dbConfig.tables ?? {},
appToken: flags.token ?? dbInfo.token,
appToken,
isBuild: false,
output: 'server',
localExecution: true,
Expand Down
11 changes: 8 additions & 3 deletions packages/db/src/core/cli/commands/push/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import type { Arguments } from 'yargs-parser';
import { MIGRATION_VERSION } from '../../../consts.js';
import { createClient } from '../../../db-client/libsql-node.js';
import type { DBConfig, DBSnapshot } from '../../../types.js';
import { getRemoteDatabaseInfo, type RemoteDatabaseInfo } from '../../../utils.js';
import {
getRemoteDatabaseInfo,
type RemoteDatabaseInfo,
resolveDbAppToken,
} from '../../../utils.js';
import {
createCurrentSnapshot,
createEmptySnapshot,
Expand All @@ -25,7 +29,8 @@ export async function cmd({
const isDryRun = flags.dryRun;
const isForceReset = flags.forceReset;
const dbInfo = getRemoteDatabaseInfo();
const productionSnapshot = await getProductionCurrentSnapshot(dbInfo);
const appToken = resolveDbAppToken(flags, dbInfo.token);
const productionSnapshot = await getProductionCurrentSnapshot({ ...dbInfo, token: appToken });
const currentSnapshot = createCurrentSnapshot(dbConfig);
const isFromScratch = !productionSnapshot;
const { queries: migrationQueries, confirmations } = await getMigrationQueries({
Expand Down Expand Up @@ -67,7 +72,7 @@ export async function cmd({
await pushSchema({
statements: migrationQueries,
dbInfo,
appToken: flags.token ?? dbInfo.token,
appToken,
isDryRun,
currentSnapshot: currentSnapshot,
});
Expand Down
5 changes: 3 additions & 2 deletions packages/db/src/core/cli/commands/shell/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createClient as createLocalDatabaseClient } from '../../../db-client/li
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';
import { getAstroEnv, getRemoteDatabaseInfo, resolveDbAppToken } from '../../../utils.js';

export async function cmd({
flags,
Expand All @@ -24,7 +24,8 @@ export async function cmd({
}
const dbInfo = getRemoteDatabaseInfo();
if (flags.remote) {
const db = createRemoteDatabaseClient(dbInfo);
const appToken = resolveDbAppToken(flags, dbInfo.token);
const db = createRemoteDatabaseClient({ ...dbInfo, token: appToken });
const result = await db.run(sql.raw(query));
console.log(result);
} else {
Expand Down
5 changes: 3 additions & 2 deletions packages/db/src/core/cli/commands/verify/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AstroConfig } from 'astro';
import type { Arguments } from 'yargs-parser';
import type { DBConfig } from '../../../types.js';
import { getRemoteDatabaseInfo } from '../../../utils.js';
import { getRemoteDatabaseInfo, resolveDbAppToken } from '../../../utils.js';
import {
createCurrentSnapshot,
createEmptySnapshot,
Expand All @@ -20,7 +20,8 @@ export async function cmd({
}) {
const isJson = flags.json;
const dbInfo = getRemoteDatabaseInfo();
const productionSnapshot = await getProductionCurrentSnapshot(dbInfo);
const appToken = resolveDbAppToken(flags, dbInfo.token);
const productionSnapshot = await getProductionCurrentSnapshot({ ...dbInfo, token: appToken });
const currentSnapshot = createCurrentSnapshot(dbConfig);
const { queries: migrationQueries, confirmations } = await getMigrationQueries({
oldSnapshot: productionSnapshot || createEmptySnapshot(),
Expand Down
12 changes: 12 additions & 0 deletions packages/db/src/core/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function cli({
// Most commands are `astro db foo`, but for now login/logout
// are also handled by this package, so first check if this is a db command.
const command = args[2] === 'db' ? args[3] : args[2];
validateDbAppTokenFlag(command, flags);
const { dbConfig } = await resolveDbConfig(astroConfig);

switch (command) {
Expand Down Expand Up @@ -68,3 +69,14 @@ export async function cli({
}
}
}

function validateDbAppTokenFlag(command: string | undefined, flags: Arguments) {
if (command !== 'execute' && command !== 'push' && command !== 'verify' && command !== 'shell') return;

const dbAppToken = (flags as Arguments & { dbAppToken?: unknown }).dbAppToken;
if (dbAppToken == null) return;
if (typeof dbAppToken !== 'string') {
console.error(`Invalid value for --db-app-token; expected a string.`);
process.exit(1);
}
}
19 changes: 19 additions & 0 deletions packages/db/src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AstroConfig, AstroIntegration } from 'astro';
import { loadEnv } from 'vite';
import type { Arguments } from 'yargs-parser';
import './types.js';

export type VitePlugin = Required<AstroConfig['vite']>['plugins'][number];
Expand All @@ -23,6 +24,24 @@ export function getRemoteDatabaseInfo(): RemoteDatabaseInfo {
};
}

export function resolveDbAppToken(
flags: Arguments,
envToken: string,
): string;
export function resolveDbAppToken(
flags: Arguments,
envToken: string | undefined,
): string | undefined;
export function resolveDbAppToken(
flags: Arguments,
envToken: string | undefined,
): string | undefined {
const dbAppToken = (flags as Arguments & { dbAppToken?: unknown }).dbAppToken;
if (typeof dbAppToken === 'string') return dbAppToken;

return envToken;
}

export function getDbDirectoryUrl(root: URL | string) {
return new URL('db/', root);
}
Expand Down
28 changes: 28 additions & 0 deletions packages/db/test/basics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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 { resolveDbAppToken } from '../dist/core/utils.js';
import { clearEnvironment, setupRemoteDb } from './test-utils.js';

describe('astro:db', () => {
Expand Down Expand Up @@ -200,4 +201,31 @@ describe('astro:db', () => {
assert.equal(ul.children().length, 5);
});
});

describe('cli --db-app-token', () => {
it('Seeds remote database with --db-app-token flag set and without ASTRO_DB_APP_TOKEN env being set', async () => {
clearEnvironment();
assert.equal(process.env.ASTRO_DB_APP_TOKEN, undefined);

const remoteDbServer = await setupRemoteDb(fixture.config, { useDbAppTokenFlag: true });
try {
assert.equal(process.env.ASTRO_DB_APP_TOKEN, undefined);
} finally {
await remoteDbServer.stop();
}
assert.equal(process.env.ASTRO_DB_APP_TOKEN, undefined);
});
});

describe('Precedence for --db-app-token and ASTRO_DB_APP_TOKEN handled correctly', () => {
it('prefers --db-app-token over `ASTRO_DB_APP_TOKEN`', () => {
const flags = /** @type {any} */ ({ _: [], dbAppToken: 'from-flag' });
assert.equal(resolveDbAppToken(flags, 'from-env'), 'from-flag');
});

it('falls back to ASTRO_DB_APP_TOKEN if no flags set', () => {
const flags = /** @type {any} */ ({ _: [] });
assert.equal(resolveDbAppToken(flags, 'from-env'), 'from-env');
});
});
});
34 changes: 34 additions & 0 deletions packages/db/test/error-handling.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { loadFixture } from '../../astro/test/test-utils.js';
import { cli } from '../dist/core/cli/index.js';
import { setupRemoteDb } from './test-utils.js';

const foreignKeyConstraintError =
Expand All @@ -14,6 +15,39 @@ describe('astro:db - error handling', () => {
});
});

it('Errors on invalid --db-app-token input', async () => {
const originalExit = process.exit;
const originalError = console.error;
/** @type {string[]} */
const errorMessages = [];
console.error = (...args) => {
errorMessages.push(args.map(String).join(' '));
};
process.exit = (code) => {
throw new Error(`EXIT_${code}`);
};

try {
await cli({
config: fixture.config,
flags: {
_: [undefined, 'astro', 'db', 'verify'],
dbAppToken: true,
},
});
assert.fail('Expected command to exit');
} catch (err) {
assert.match(String(err), /EXIT_1/);
assert.ok(
errorMessages.some((m) => m.includes('Invalid value for --db-app-token')),
`Expected error output to mention invalid --db-app-token, got: ${errorMessages.join('\n')}`,
);
} finally {
process.exit = originalExit;
console.error = originalError;
}
});

describe('development', () => {
let devServer;

Expand Down
7 changes: 5 additions & 2 deletions packages/db/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ const isWindows = process.platform === 'win32';
/**
* @param {import('astro').AstroConfig} astroConfig
*/
export async function setupRemoteDb(astroConfig) {
export async function setupRemoteDb(astroConfig, options = {}) {
const url = isWindows
? new URL(`./.astro/${Date.now()}.db`, astroConfig.root)
: new URL(`./${Date.now()}.db`, astroConfig.root);
const token = 'foo';
process.env.ASTRO_DB_REMOTE_URL = url.toString();
process.env.ASTRO_DB_APP_TOKEN = token;
if (!options.useDbAppTokenFlag) {
process.env.ASTRO_DB_APP_TOKEN = token;
}
process.env.ASTRO_INTERNAL_TEST_REMOTE = true;

if (isWindows) {
Expand Down Expand Up @@ -47,6 +49,7 @@ export async function setupRemoteDb(astroConfig) {
flags: {
_: [undefined, 'astro', 'db', 'execute', 'db/seed.ts'],
remote: true,
...(options.useDbAppTokenFlag ? { dbAppToken: token } : {}),
},
});

Expand Down
Loading