mirror of
https://github.com/withastro/astro.git
synced 2025-01-27 22:19:04 -05:00
db: Better error messages when querying remote (#10636)
* feat: clear error messages on remote db error * refactor: use AstroDbError for correct error name * refactor: errorMessage -> responseMessage * chore: changeset * fix: revert seed file change * fix: format seed errors as AstroDbError * fix: correctly log eager seed errors
This commit is contained in:
parent
17badaf55c
commit
504d15d772
6 changed files with 69 additions and 32 deletions
5
.changeset/plenty-lobsters-design.md
Normal file
5
.changeset/plenty-lobsters-design.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@astrojs/db": patch
|
||||
---
|
||||
|
||||
Detailed error messages for remote database exceptions.
|
|
@ -2,7 +2,6 @@ import { existsSync } from 'fs';
|
|||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { AstroConfig, AstroIntegration } from 'astro';
|
||||
import { AstroError } from 'astro/errors';
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import { blue, yellow } from 'kleur/colors';
|
||||
import { loadEnv } from 'vite';
|
||||
|
@ -16,6 +15,7 @@ import { fileURLIntegration } from './file-url.js';
|
|||
import { typegenInternal } from './typegen.js';
|
||||
import { type LateSeedFiles, type LateTables, resolved, vitePluginDb } from './vite-plugin-db.js';
|
||||
import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js';
|
||||
import { AstroDbError } from '../../utils.js';
|
||||
|
||||
function astroDBIntegration(): AstroIntegration {
|
||||
let connectToStudio = false;
|
||||
|
@ -145,10 +145,17 @@ function astroDBIntegration(): AstroIntegration {
|
|||
seedInFlight = true;
|
||||
const mod = server.moduleGraph.getModuleById(resolved.seedVirtual);
|
||||
if (mod) server.moduleGraph.invalidateModule(mod);
|
||||
server.ssrLoadModule(resolved.seedVirtual).then(() => {
|
||||
seedInFlight = false;
|
||||
logger.info('Seeded database.');
|
||||
});
|
||||
server
|
||||
.ssrLoadModule(resolved.seedVirtual)
|
||||
.then(() => {
|
||||
logger.info('Seeded database.');
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error(e instanceof Error ? e.message : String(e));
|
||||
})
|
||||
.finally(() => {
|
||||
seedInFlight = false;
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
@ -161,7 +168,7 @@ function astroDBIntegration(): AstroIntegration {
|
|||
const message = `Attempting to build without the --remote flag or the ASTRO_DATABASE_FILE environment variable defined. You probably want to pass --remote to astro build.`;
|
||||
const hint =
|
||||
'Learn more connecting to Studio: https://docs.astro.build/en/guides/astro-db/#connect-to-astro-studio';
|
||||
throw new AstroError(message, hint);
|
||||
throw new AstroDbError(message, hint);
|
||||
}
|
||||
|
||||
logger.info('database: ' + (connectToStudio ? yellow('remote') : blue('local database.')));
|
||||
|
|
|
@ -5,6 +5,7 @@ import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql';
|
|||
import { type SqliteRemoteDatabase, drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy';
|
||||
import { z } from 'zod';
|
||||
import { safeFetch } from './utils.js';
|
||||
import { AstroDbError } from '../utils.js';
|
||||
|
||||
const isWebContainer = !!process.versions?.webcontainer;
|
||||
|
||||
|
@ -55,10 +56,8 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string
|
|||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
(response) => {
|
||||
throw new Error(
|
||||
`Failed to execute query.\nQuery: ${sql}\nFull error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
async (response) => {
|
||||
throw await parseRemoteError(response);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -67,11 +66,7 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string
|
|||
const json = await res.json();
|
||||
remoteResult = remoteResultSchema.parse(json);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to execute query.\nQuery: ${sql}\nFull error: Unexpected JSON response. ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`
|
||||
);
|
||||
throw new AstroDbError(await getUnexpectedResponseMessage(res));
|
||||
}
|
||||
|
||||
if (method === 'run') return remoteResult;
|
||||
|
@ -103,10 +98,8 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string
|
|||
},
|
||||
body: JSON.stringify(stmts),
|
||||
},
|
||||
(response) => {
|
||||
throw new Error(
|
||||
`Failed to execute batch queries.\nFull error: ${response.status} ${response.statusText}}`
|
||||
);
|
||||
async (response) => {
|
||||
throw await parseRemoteError(response);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -115,11 +108,7 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string
|
|||
const json = await res.json();
|
||||
remoteResults = z.array(remoteResultSchema).parse(json);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to execute batch queries.\nFull error: Unexpected JSON response. ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`
|
||||
);
|
||||
throw new AstroDbError(await getUnexpectedResponseMessage(res));
|
||||
}
|
||||
let results: any[] = [];
|
||||
for (const [idx, rawResult] of remoteResults.entries()) {
|
||||
|
@ -149,3 +138,36 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string
|
|||
applyTransactionNotSupported(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
const errorSchema = z.object({
|
||||
success: z.boolean(),
|
||||
error: z.object({
|
||||
code: z.string(),
|
||||
details: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const KNOWN_ERROR_CODES = {
|
||||
SQL_QUERY_FAILED: 'SQL_QUERY_FAILED',
|
||||
};
|
||||
|
||||
const getUnexpectedResponseMessage = async (response: Response) =>
|
||||
`Unexpected response from remote database:\n(Status ${response.status}) ${await response.text()}`;
|
||||
|
||||
async function parseRemoteError(response: Response): Promise<AstroDbError> {
|
||||
let error;
|
||||
try {
|
||||
error = errorSchema.parse(await response.json()).error;
|
||||
} catch (e) {
|
||||
return new AstroDbError(await getUnexpectedResponseMessage(response));
|
||||
}
|
||||
// Strip LibSQL error prefixes
|
||||
let details =
|
||||
error.details?.replace(/.*SQLite error: /, '') ??
|
||||
`(Code ${error.code}) \nError querying remote database.`;
|
||||
let hint = `See the Astro DB guide for query and push instructions: https://docs.astro.build/en/guides/astro-db/#query-your-database`;
|
||||
if (error.code === KNOWN_ERROR_CODES.SQL_QUERY_FAILED && details.includes('no such table')) {
|
||||
hint = `Did you run \`astro db push\` to push your latest table schemas?`;
|
||||
}
|
||||
return new AstroDbError(details, hint);
|
||||
}
|
||||
|
|
|
@ -24,10 +24,6 @@ export const REFERENCE_DNE_ERROR = (columnName: string) => {
|
|||
)} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
|
||||
};
|
||||
|
||||
export const SEED_ERROR = (error: string) => {
|
||||
return `${red(`Error while seeding database:`)}\n\n${error}`;
|
||||
};
|
||||
|
||||
export const SEED_DEFAULT_EXPORT_ERROR = (fileName: string) => {
|
||||
return SEED_ERROR(`Missing default function export in ${bold(fileName)}`);
|
||||
return `Missing default function export in ${bold(fileName)}`;
|
||||
};
|
||||
|
|
|
@ -3,8 +3,9 @@ import { type SQL, sql } from 'drizzle-orm';
|
|||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
|
||||
import { type DBTables } from '../core/types.js';
|
||||
import { SEED_DEFAULT_EXPORT_ERROR, SEED_ERROR } from './errors.js';
|
||||
import { SEED_DEFAULT_EXPORT_ERROR } from './errors.js';
|
||||
import { getCreateIndexQueries, getCreateTableQuery } from './queries.js';
|
||||
import { AstroDbError } from '../utils.js';
|
||||
|
||||
const sqlite = new SQLiteAsyncDialect();
|
||||
|
||||
|
@ -25,7 +26,7 @@ export async function seedLocal({
|
|||
const seedFilePath = Object.keys(userSeedGlob)[0];
|
||||
if (seedFilePath) {
|
||||
const mod = userSeedGlob[seedFilePath];
|
||||
if (!mod.default) throw new Error(SEED_DEFAULT_EXPORT_ERROR(seedFilePath));
|
||||
if (!mod.default) throw new AstroDbError(SEED_DEFAULT_EXPORT_ERROR(seedFilePath));
|
||||
seedFunctions.push(mod.default);
|
||||
}
|
||||
for (const seedFn of integrationSeedFunctions) {
|
||||
|
@ -36,7 +37,7 @@ export async function seedLocal({
|
|||
await seed();
|
||||
} catch (e) {
|
||||
if (e instanceof LibsqlError) {
|
||||
throw new Error(SEED_ERROR(e.message));
|
||||
throw new AstroDbError(`Failed to seed database:\n${e.message}`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -1,2 +1,8 @@
|
|||
import { AstroError } from 'astro/errors';
|
||||
|
||||
export { defineDbIntegration } from './core/utils.js';
|
||||
export { asDrizzleTable } from './runtime/index.js';
|
||||
|
||||
export class AstroDbError extends AstroError {
|
||||
name = 'Astro DB Error';
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue