Skip to content

Commit

Permalink
Fix isDbError() for remote errors (#11027)
Browse files Browse the repository at this point in the history
* fix: use LibsqlError for remote db errors

* chore: remove unused drizzle.ts

* fix(test): return expected `error` object

* fix: error detail formatting

* feat(test): error messages with remote adapter

* feat(test): add code to test body

* chore: changeset
  • Loading branch information
bholmesdev authored May 13, 2024
1 parent 296cd7e commit eb1d9a4
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-cows-change.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 20 additions & 11 deletions packages/db/src/runtime/db-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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<AstroDbError> {
async function parseRemoteError(response: Response): Promise<DetailedLibsqlError> {
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 });
}
25 changes: 0 additions & 25 deletions packages/db/src/runtime/drizzle.ts

This file was deleted.

17 changes: 17 additions & 0 deletions packages/db/src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LibsqlError } from '@libsql/client';
import { AstroError } from 'astro/errors';

const isWindows = process?.platform === 'win32';
Expand Down Expand Up @@ -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('\\\\?\\');

Expand Down
20 changes: 17 additions & 3 deletions packages/db/test/error-handling.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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' }));
};
7 changes: 5 additions & 2 deletions packages/db/test/test-utils.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
}
})
);
}
Expand Down

0 comments on commit eb1d9a4

Please sign in to comment.