diff --git a/.changeset/tidy-cows-change.md b/.changeset/tidy-cows-change.md new file mode 100644 index 0000000000..21438fcbd5 --- /dev/null +++ b/.changeset/tidy-cows-change.md @@ -0,0 +1,5 @@ +--- +"@astrojs/db": patch +--- + +Fix `isDbError()` returning `false` for remote database errors. Astro will now return a `LibsqlError` in development and production. diff --git a/packages/db/src/runtime/db-client.ts b/packages/db/src/runtime/db-client.ts index d52af72fce..3bea14b0a8 100644 --- a/packages/db/src/runtime/db-client.ts +++ b/packages/db/src/runtime/db-client.ts @@ -4,7 +4,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; import { type SqliteRemoteDatabase, drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy'; import { z } from 'zod'; -import { AstroDbError, safeFetch } from './utils.js'; +import { DetailedLibsqlError, safeFetch } from './utils.js'; const isWebContainer = !!process.versions?.webcontainer; @@ -65,7 +65,10 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string const json = await res.json(); remoteResult = remoteResultSchema.parse(json); } catch (e) { - throw new AstroDbError(await getUnexpectedResponseMessage(res)); + throw new DetailedLibsqlError({ + message: await getUnexpectedResponseMessage(res), + code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, + }); } if (method === 'run') return remoteResult; @@ -107,7 +110,10 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string const json = await res.json(); remoteResults = z.array(remoteResultSchema).parse(json); } catch (e) { - throw new AstroDbError(await getUnexpectedResponseMessage(res)); + throw new DetailedLibsqlError({ + message: await getUnexpectedResponseMessage(res), + code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, + }); } let results: any[] = []; for (const [idx, rawResult] of remoteResults.entries()) { @@ -151,22 +157,25 @@ const KNOWN_ERROR_CODES = { }; const getUnexpectedResponseMessage = async (response: Response) => - `Unexpected response from remote database:\n(Status ${response.status}) ${await response.text()}`; + `Unexpected response from remote database:\n(Status ${response.status}) ${await response.clone().text()}`; -async function parseRemoteError(response: Response): Promise { +async function parseRemoteError(response: Response): Promise { let error; try { - error = errorSchema.parse(await response.json()).error; + error = errorSchema.parse(await response.clone().json()).error; } catch (e) { - return new AstroDbError(await getUnexpectedResponseMessage(response)); + return new DetailedLibsqlError({ + message: await getUnexpectedResponseMessage(response), + code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, + }); } // Strip LibSQL error prefixes - let details = - error.details?.replace(/.*SQLite error: /, '') ?? - `(Code ${error.code}) \nError querying remote database.`; + let baseDetails = error.details?.replace(/.*SQLite error: /, '') ?? 'Error querying remote database.'; + // Remove duplicated "code" in details + const details = baseDetails.slice(baseDetails.indexOf(':') + 1).trim(); 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); + return new DetailedLibsqlError({ message: details, code: error.code, hint }); } diff --git a/packages/db/src/runtime/drizzle.ts b/packages/db/src/runtime/drizzle.ts deleted file mode 100644 index d2dfa9e325..0000000000 --- a/packages/db/src/runtime/drizzle.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Drizzle utilities we expose directly from `astro:db` -export { - sql, - eq, - gt, - gte, - lt, - lte, - ne, - isNull, - isNotNull, - inArray, - notInArray, - exists, - notExists, - between, - notBetween, - like, - notIlike, - not, - asc, - desc, - and, - or, -} from 'drizzle-orm'; diff --git a/packages/db/src/runtime/utils.ts b/packages/db/src/runtime/utils.ts index 2fe837d8fa..0c2e820e0b 100644 --- a/packages/db/src/runtime/utils.ts +++ b/packages/db/src/runtime/utils.ts @@ -1,3 +1,4 @@ +import { LibsqlError } from '@libsql/client'; import { AstroError } from 'astro/errors'; const isWindows = process?.platform === 'win32'; @@ -25,6 +26,22 @@ export class AstroDbError extends AstroError { name = 'Astro DB Error'; } +export class DetailedLibsqlError extends LibsqlError { + name = 'Astro DB Error'; + hint?: string; + + constructor({ + message, + code, + hint, + rawCode, + cause, + }: { message: string; code: string; hint?: string; rawCode?: number; cause?: Error }) { + super(message, code, rawCode, cause); + this.hint = hint; + } +} + function slash(path: string) { const isExtendedLengthPath = path.startsWith('\\\\?\\'); diff --git a/packages/db/test/error-handling.test.js b/packages/db/test/error-handling.test.js index d67bd161b8..9d40507b2d 100644 --- a/packages/db/test/error-handling.test.js +++ b/packages/db/test/error-handling.test.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { loadFixture } from '../../astro/test/test-utils.js'; +import { setupRemoteDbServer } from './test-utils.js'; const foreignKeyConstraintError = 'LibsqlError: SQLITE_CONSTRAINT_FOREIGNKEY: FOREIGN KEY constraint failed'; @@ -25,18 +26,31 @@ describe('astro:db - error handling', () => { it('Raises foreign key constraint LibsqlError', async () => { const json = await fixture.fetch('/foreign-key-constraint.json').then((res) => res.json()); - expect(json.error).to.equal(foreignKeyConstraintError); + expect(json).to.deep.equal({ + message: foreignKeyConstraintError, + code: 'SQLITE_CONSTRAINT_FOREIGNKEY', + }); }); }); - describe('build', () => { + describe('build --remote', () => { + let remoteDbServer; + before(async () => { + remoteDbServer = await setupRemoteDbServer(fixture.config); await fixture.build(); }); + after(async () => { + await remoteDbServer?.stop(); + }); + it('Raises foreign key constraint LibsqlError', async () => { const json = await fixture.readFile('/foreign-key-constraint.json'); - expect(JSON.parse(json).error).to.equal(foreignKeyConstraintError); + expect(JSON.parse(json)).to.deep.equal({ + message: foreignKeyConstraintError, + code: 'SQLITE_CONSTRAINT_FOREIGNKEY', + }); }); }); }); diff --git a/packages/db/test/fixtures/error-handling/src/pages/foreign-key-constraint.json.ts b/packages/db/test/fixtures/error-handling/src/pages/foreign-key-constraint.json.ts index 8e6bad8c36..358a9a95c6 100644 --- a/packages/db/test/fixtures/error-handling/src/pages/foreign-key-constraint.json.ts +++ b/packages/db/test/fixtures/error-handling/src/pages/foreign-key-constraint.json.ts @@ -11,8 +11,8 @@ export const GET: APIRoute = async () => { }); } catch (e) { if (isDbError(e)) { - return new Response(JSON.stringify({ error: `LibsqlError: ${e.message}` })); + return new Response(JSON.stringify({ message: `LibsqlError: ${e.message}`, code: e.code })); } } - return new Response(JSON.stringify({ error: 'Did not raise expected exception' })); + return new Response(JSON.stringify({ message: 'Did not raise expected exception' })); }; diff --git a/packages/db/test/test-utils.js b/packages/db/test/test-utils.js index 8be80a879a..9f5ba6d9d8 100644 --- a/packages/db/test/test-utils.js +++ b/packages/db/test/test-utils.js @@ -1,5 +1,5 @@ import { createServer } from 'node:http'; -import { createClient } from '@libsql/client'; +import { LibsqlError, createClient } from '@libsql/client'; import { z } from 'zod'; import { cli } from '../dist/core/cli/index.js'; import { resolveDbConfig } from '../dist/core/load-file.js'; @@ -112,7 +112,10 @@ function createRemoteDbServer() { res.end( JSON.stringify({ success: false, - message: e.message, + error: { + code: e instanceof LibsqlError ? e.code : 'SQLITE_QUERY_FAILED', + details: e.message, + } }) ); }