Skip to content

Commit

Permalink
db: expose isDbError() utility (#10498)
Browse files Browse the repository at this point in the history
* feat: expose isDbError

* test: foreign key constraint error detection

* fix(test); use isDbError

* chore: changeset
  • Loading branch information
bholmesdev authored Mar 20, 2024
1 parent 19e42c3 commit f0fc78c
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-pumas-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/db": patch
---

Expose `isDbError()` utility to handle database exceptions when querying.
5 changes: 5 additions & 0 deletions packages/db/src/runtime/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
TableConfig,
TextColumnOpts,
} from '../core/types.js';
import { LibsqlError } from '@libsql/client';

import type { LibSQLDatabase } from 'drizzle-orm/libsql';

Expand All @@ -24,6 +25,10 @@ function createColumn<S extends string, T extends Record<string, unknown>>(type:
};
}

export function isDbError(err: unknown): err is LibsqlError {
return err instanceof LibsqlError;
}

export const column = {
number: <T extends NumberColumnOpts>(opts: T = {} as T) => {
return createColumn('number', opts) satisfies { type: 'number' };
Expand Down
42 changes: 42 additions & 0 deletions packages/db/test/error-handling.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect } from 'chai';
import { loadFixture } from '../../astro/test/test-utils.js';

const foreignKeyConstraintError =
'LibsqlError: SQLITE_CONSTRAINT_FOREIGNKEY: FOREIGN KEY constraint failed';

describe('astro:db - error handling', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/error-handling/', import.meta.url),
});
});

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

before(async () => {
devServer = await fixture.startDevServer();
});

after(async () => {
await devServer.stop();
});

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);
});
});

describe('build', () => {
before(async () => {
await fixture.build();
});

it('Raises foreign key constraint LibsqlError', async () => {
const json = await fixture.readFile('/foreign-key-constraint.json');
expect(JSON.parse(json).error).to.equal(foreignKeyConstraintError);
});
});
});
10 changes: 10 additions & 0 deletions packages/db/test/fixtures/error-handling/astro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import db from '@astrojs/db';
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
integrations: [db()],
devToolbar: {
enabled: false,
},
});
26 changes: 26 additions & 0 deletions packages/db/test/fixtures/error-handling/db/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { column, defineDb, defineTable } from 'astro:db';

const Recipe = defineTable({
columns: {
id: column.number({ primaryKey: true }),
title: column.text(),
description: column.text(),
},
});

const Ingredient = defineTable({
columns: {
id: column.number({ primaryKey: true }),
name: column.text(),
quantity: column.number(),
recipeId: column.number(),
},
indexes: {
recipeIdx: { on: 'recipeId' },
},
foreignKeys: [{ columns: 'recipeId', references: () => [Recipe.columns.id] }],
});

export default defineDb({
tables: { Recipe, Ingredient },
});
62 changes: 62 additions & 0 deletions packages/db/test/fixtures/error-handling/db/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Ingredient, Recipe, db } from 'astro:db';

export default async function () {
const pancakes = await db
.insert(Recipe)
.values({
title: 'Pancakes',
description: 'A delicious breakfast',
})
.returning()
.get();

await db.insert(Ingredient).values([
{
name: 'Flour',
quantity: 1,
recipeId: pancakes.id,
},
{
name: 'Eggs',
quantity: 2,
recipeId: pancakes.id,
},
{
name: 'Milk',
quantity: 1,
recipeId: pancakes.id,
},
]);

const pizza = await db
.insert(Recipe)
.values({
title: 'Pizza',
description: 'A delicious dinner',
})
.returning()
.get();

await db.insert(Ingredient).values([
{
name: 'Flour',
quantity: 1,
recipeId: pizza.id,
},
{
name: 'Eggs',
quantity: 2,
recipeId: pizza.id,
},
{
name: 'Milk',
quantity: 1,
recipeId: pizza.id,
},
{
name: 'Tomato Sauce',
quantity: 1,
recipeId: pizza.id,
},
]);
}
14 changes: 14 additions & 0 deletions packages/db/test/fixtures/error-handling/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@test/error-handling",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/db": "workspace:*",
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { APIRoute } from 'astro';
import { db, Ingredient, isDbError } from 'astro:db';

export const GET: APIRoute = async () => {
try {
await db.insert(Ingredient).values({
name: 'Flour',
quantity: 1,
// Trigger foreign key constraint error
recipeId: 42,
});
} catch (e) {
if (isDbError(e)) {
return new Response(JSON.stringify({ error: `LibsqlError: ${e.message}` }));
}
}
return new Response(JSON.stringify({ error: 'Did not raise expected exception' }));
};
1 change: 1 addition & 0 deletions packages/db/virtual.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ declare module 'astro:db' {
export const column: RuntimeConfig['column'];
export const defineDb: RuntimeConfig['defineDb'];
export const defineTable: RuntimeConfig['defineTable'];
export const isDbError: RuntimeConfig['isDbError'];

export const eq: RuntimeConfig['eq'];
export const gt: RuntimeConfig['gt'];
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f0fc78c

Please sign in to comment.