mirror of
https://github.com/withastro/astro.git
synced 2025-02-17 22:44:24 -05:00
db: expose isDbError()
utility (#10498)
* feat: expose isDbError * test: foreign key constraint error detection * fix(test); use isDbError * chore: changeset
This commit is contained in:
parent
19e42c3681
commit
f0fc78c873
10 changed files with 192 additions and 0 deletions
5
.changeset/unlucky-pumas-retire.md
Normal file
5
.changeset/unlucky-pumas-retire.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@astrojs/db": patch
|
||||
---
|
||||
|
||||
Expose `isDbError()` utility to handle database exceptions when querying.
|
|
@ -9,6 +9,7 @@ import type {
|
|||
TableConfig,
|
||||
TextColumnOpts,
|
||||
} from '../core/types.js';
|
||||
import { LibsqlError } from '@libsql/client';
|
||||
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
|
||||
|
@ -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' };
|
||||
|
|
42
packages/db/test/error-handling.test.js
Normal file
42
packages/db/test/error-handling.test.js
Normal file
|
@ -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
packages/db/test/fixtures/error-handling/astro.config.ts
vendored
Normal file
10
packages/db/test/fixtures/error-handling/astro.config.ts
vendored
Normal file
|
@ -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
packages/db/test/fixtures/error-handling/db/config.ts
vendored
Normal file
26
packages/db/test/fixtures/error-handling/db/config.ts
vendored
Normal file
|
@ -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
packages/db/test/fixtures/error-handling/db/seed.ts
vendored
Normal file
62
packages/db/test/fixtures/error-handling/db/seed.ts
vendored
Normal file
|
@ -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
packages/db/test/fixtures/error-handling/package.json
vendored
Normal file
14
packages/db/test/fixtures/error-handling/package.json
vendored
Normal file
|
@ -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:*"
|
||||
}
|
||||
}
|
18
packages/db/test/fixtures/error-handling/src/pages/foreign-key-constraint.json.ts
vendored
Normal file
18
packages/db/test/fixtures/error-handling/src/pages/foreign-key-constraint.json.ts
vendored
Normal file
|
@ -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
packages/db/virtual.d.ts
vendored
1
packages/db/virtual.d.ts
vendored
|
@ -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'];
|
||||
|
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -3921,6 +3921,15 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../../astro
|
||||
|
||||
packages/db/test/fixtures/error-handling:
|
||||
dependencies:
|
||||
'@astrojs/db':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../astro
|
||||
|
||||
packages/db/test/fixtures/integration-only:
|
||||
dependencies:
|
||||
'@astrojs/db':
|
||||
|
|
Loading…
Add table
Reference in a new issue