summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/purple-poets-sin.md58
-rw-r--r--packages/db/src/core/cli/commands/execute/index.ts1
-rw-r--r--packages/db/src/core/cli/index.ts7
-rw-r--r--packages/db/src/core/errors.ts10
-rw-r--r--packages/db/src/core/integration/index.ts22
-rw-r--r--packages/db/src/core/integration/vite-plugin-db.ts20
-rw-r--r--packages/db/src/core/load-file.ts94
-rw-r--r--packages/db/src/core/types.ts12
-rw-r--r--packages/db/src/core/utils.ts7
-rw-r--r--packages/db/src/runtime/index.ts40
-rw-r--r--packages/db/src/utils.ts1
-rw-r--r--packages/db/test/fixtures/integrations/astro.config.mjs8
-rw-r--r--packages/db/test/fixtures/integrations/db/config.ts12
-rw-r--r--packages/db/test/fixtures/integrations/db/seed.ts13
-rw-r--r--packages/db/test/fixtures/integrations/integration/config.ts8
-rw-r--r--packages/db/test/fixtures/integrations/integration/index.ts15
-rw-r--r--packages/db/test/fixtures/integrations/integration/seed.ts14
-rw-r--r--packages/db/test/fixtures/integrations/integration/shared.ts9
-rw-r--r--packages/db/test/fixtures/integrations/package.json14
-rw-r--r--packages/db/test/fixtures/integrations/src/pages/index.astro17
-rw-r--r--packages/db/test/integrations.test.js48
-rw-r--r--pnpm-lock.yaml9
22 files changed, 401 insertions, 38 deletions
diff --git a/.changeset/purple-poets-sin.md b/.changeset/purple-poets-sin.md
new file mode 100644
index 000000000..0496e3a2d
--- /dev/null
+++ b/.changeset/purple-poets-sin.md
@@ -0,0 +1,58 @@
+---
+"@astrojs/db": minor
+---
+
+Adds support for integrations providing `astro:db` configuration and seed files, using the new `astro:db:setup` hook.
+
+To get TypeScript support for the `astro:db:setup` hook, wrap your integration object in the `defineDbIntegration()` utility:
+
+```js
+import { defineDbIntegration } from '@astrojs/db/utils';
+
+export default function MyDbIntegration() {
+ return defineDbIntegration({
+ name: 'my-astro-db-powered-integration',
+ hooks: {
+ 'astro:db:setup': ({ extendDb }) => {
+ extendDb({
+ configEntrypoint: '@astronaut/my-package/config',
+ seedEntrypoint: '@astronaut/my-package/seed',
+ });
+ },
+ },
+ });
+}
+```
+
+Use the `extendDb` method to register additional `astro:db` config and seed files.
+
+Integration config and seed files follow the same format as their user-defined equivalents. However, often while working on integrations, you may not be able to benefit from Astro’s generated table types exported from `astro:db`. For full type safety and autocompletion support, use the `asDrizzleTable()` utility to wrap your table definitions in the seed file.
+
+```js
+// config.ts
+import { defineTable, column } from 'astro:db';
+
+export const Pets = defineTable({
+ columns: {
+ name: column.text(),
+ age: column.number(),
+ },
+});
+```
+
+```js
+// seed.ts
+import { asDrizzleTable } from '@astrojs/db/utils';
+import { db } from 'astro:db';
+import { Pets } from './config';
+
+export default async function() {
+ // Convert the Pets table into a format ready for querying.
+ const typeSafePets = asDrizzleTable('Pets', Pets);
+
+ await db.insert(typeSafePets).values([
+ { name: 'Palomita', age: 7 },
+ { name: 'Pan', age: 3.5 },
+ ]);
+}
+```
diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts
index b1aa50cc8..d9bfbaf91 100644
--- a/packages/db/src/core/cli/commands/execute/index.ts
+++ b/packages/db/src/core/cli/commands/execute/index.ts
@@ -43,6 +43,7 @@ export async function cmd({
tables: dbConfig.tables ?? {},
root: astroConfig.root,
shouldSeed: false,
+ seedFiles: [],
});
}
const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl });
diff --git a/packages/db/src/core/cli/index.ts b/packages/db/src/core/cli/index.ts
index 85dbdd38a..0e9d5636f 100644
--- a/packages/db/src/core/cli/index.ts
+++ b/packages/db/src/core/cli/index.ts
@@ -1,7 +1,6 @@
import type { AstroConfig } from 'astro';
import type { Arguments } from 'yargs-parser';
-import { loadDbConfigFile } from '../load-file.js';
-import { dbConfigSchema } from '../types.js';
+import { resolveDbConfig } from '../load-file.js';
export async function cli({
flags,
@@ -14,9 +13,7 @@ export async function cli({
// Most commands are `astro db foo`, but for now login/logout
// are also handled by this package, so first check if this is a db command.
const command = args[2] === 'db' ? args[3] : args[2];
- const { mod } = await loadDbConfigFile(astroConfig.root);
- // TODO: parseConfigOrExit()
- const dbConfig = dbConfigSchema.parse(mod?.default ?? {});
+ const { dbConfig } = await resolveDbConfig(astroConfig);
switch (command) {
case 'shell': {
diff --git a/packages/db/src/core/errors.ts b/packages/db/src/core/errors.ts
index 4ff477219..0724a74e9 100644
--- a/packages/db/src/core/errors.ts
+++ b/packages/db/src/core/errors.ts
@@ -71,3 +71,13 @@ export const FOREIGN_KEY_REFERENCES_EMPTY_ERROR = (tableName: string) => {
tableName
)} is misconfigured. \`references\` array cannot be empty.`;
};
+
+export const INTEGRATION_TABLE_CONFLICT_ERROR = (
+ integrationName: string,
+ tableName: string,
+ isUserConflict: boolean
+) => {
+ return red('▶ Conflicting table name in integration ' + bold(integrationName)) + isUserConflict
+ ? `\n A user-defined table named ${bold(tableName)} already exists`
+ : `\n Another integration already added a table named ${bold(tableName)}`;
+};
diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts
index d8c0168b4..26bcd393f 100644
--- a/packages/db/src/core/integration/index.ts
+++ b/packages/db/src/core/integration/index.ts
@@ -6,14 +6,12 @@ import { mkdir, rm, writeFile } from 'fs/promises';
import { blue, yellow } from 'kleur/colors';
import parseArgs from 'yargs-parser';
import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js';
-import { loadDbConfigFile } from '../load-file.js';
+import { resolveDbConfig } from '../load-file.js';
import { type ManagedAppToken, getManagedAppTokenOrExit } from '../tokens.js';
-import { type DBConfig, dbConfigSchema } from '../types.js';
import { type VitePlugin, getDbDirectoryUrl } from '../utils.js';
-import { errorMap } from './error-map.js';
import { fileURLIntegration } from './file-url.js';
import { typegen } from './typegen.js';
-import { type LateTables, vitePluginDb } from './vite-plugin-db.js';
+import { type LateTables, vitePluginDb, type LateSeedFiles } from './vite-plugin-db.js';
import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js';
function astroDBIntegration(): AstroIntegration {
@@ -21,7 +19,6 @@ function astroDBIntegration(): AstroIntegration {
let configFileDependencies: string[] = [];
let root: URL;
let appToken: ManagedAppToken | undefined;
- let dbConfig: DBConfig;
// Make table loading "late" to pass to plugins from `config:setup`,
// but load during `config:done` to wait for integrations to settle.
@@ -30,6 +27,11 @@ function astroDBIntegration(): AstroIntegration {
throw new Error('[astro:db] INTERNAL Tables not loaded yet');
},
};
+ let seedFiles: LateSeedFiles = {
+ get() {
+ throw new Error('[astro:db] INTERNAL Seed files not loaded yet');
+ },
+ };
let command: 'dev' | 'build' | 'preview';
return {
name: 'astro:db',
@@ -57,6 +59,7 @@ function astroDBIntegration(): AstroIntegration {
dbPlugin = vitePluginDb({
connectToStudio: false,
tables,
+ seedFiles,
root: config.root,
srcDir: config.srcDir,
});
@@ -74,13 +77,10 @@ function astroDBIntegration(): AstroIntegration {
// TODO: refine where we load tables
// @matthewp: may want to load tables by path at runtime
- const { mod, dependencies } = await loadDbConfigFile(config.root);
+ const { dbConfig, dependencies, integrationSeedPaths } = await resolveDbConfig(config);
+ tables.get = () => dbConfig.tables;
+ seedFiles.get = () => integrationSeedPaths;
configFileDependencies = dependencies;
- dbConfig = dbConfigSchema.parse(mod?.default ?? {}, {
- errorMap,
- });
- // TODO: resolve integrations here?
- tables.get = () => dbConfig.tables ?? {};
if (!connectToStudio) {
const dbUrl = new URL(DB_PATH, config.root);
diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts
index 6ac3ac5e4..af62c2aac 100644
--- a/packages/db/src/core/integration/vite-plugin-db.ts
+++ b/packages/db/src/core/integration/vite-plugin-db.ts
@@ -1,3 +1,4 @@
+import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { type SQL, sql } from 'drizzle-orm';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
@@ -23,11 +24,15 @@ const resolved = {
export type LateTables = {
get: () => DBTables;
};
+export type LateSeedFiles = {
+ get: () => Array<string | URL>;
+};
type VitePluginDBParams =
| {
connectToStudio: false;
tables: LateTables;
+ seedFiles: LateSeedFiles;
srcDir: URL;
root: URL;
}
@@ -81,6 +86,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
return getLocalVirtualModContents({
root: params.root,
tables: params.tables.get(),
+ seedFiles: params.seedFiles.get(),
shouldSeed: id === resolved.seedVirtual,
});
},
@@ -94,17 +100,26 @@ export function getConfigVirtualModContents() {
export function getLocalVirtualModContents({
tables,
root,
+ seedFiles,
shouldSeed,
}: {
tables: DBTables;
+ seedFiles: Array<string | URL>;
root: URL;
shouldSeed: boolean;
}) {
- const seedFilePaths = SEED_DEV_FILE_NAME.map(
+ const userSeedFilePaths = SEED_DEV_FILE_NAME.map(
// Format as /db/[name].ts
// for Vite import.meta.glob
(name) => new URL(name, getDbDirectoryUrl('file:///')).pathname
);
+ const resolveId = (id: string) => (id.startsWith('.') ? resolve(fileURLToPath(root), id) : id);
+ const integrationSeedFilePaths = seedFiles.map((pathOrUrl) =>
+ typeof pathOrUrl === 'string' ? resolveId(pathOrUrl) : pathOrUrl.pathname
+ );
+ const integrationSeedImports = integrationSeedFilePaths.map(
+ (filePath) => `() => import(${JSON.stringify(filePath)})`
+ );
const dbUrl = new URL(DB_PATH, root);
return `
@@ -117,7 +132,8 @@ export const db = createLocalDatabaseClient({ dbUrl });
${
shouldSeed
? `await seedLocal({
- fileGlob: import.meta.glob(${JSON.stringify(seedFilePaths)}, { eager: true }),
+ userSeedGlob: import.meta.glob(${JSON.stringify(userSeedFilePaths)}, { eager: true }),
+ integrationSeedImports: [${integrationSeedImports.join(',')}],
});`
: ''
}
diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts
index f31749c18..987c0143d 100644
--- a/packages/db/src/core/load-file.ts
+++ b/packages/db/src/core/load-file.ts
@@ -1,12 +1,74 @@
+import type { AstroConfig, AstroIntegration } from 'astro';
+import { build as esbuild } from 'esbuild';
import { existsSync } from 'node:fs';
import { unlink, writeFile } from 'node:fs/promises';
-import { fileURLToPath } from 'node:url';
-import { build as esbuild } from 'esbuild';
+import { createRequire } from 'node:module';
+import { fileURLToPath, pathToFileURL } from 'node:url';
import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js';
+import { INTEGRATION_TABLE_CONFLICT_ERROR } from './errors.js';
+import { errorMap } from './integration/error-map.js';
import { getConfigVirtualModContents } from './integration/vite-plugin-db.js';
+import { dbConfigSchema, type AstroDbIntegration } from './types.js';
import { getDbDirectoryUrl } from './utils.js';
-export async function loadDbConfigFile(
+const isDbIntegration = (integration: AstroIntegration): integration is AstroDbIntegration =>
+ 'astro:db:setup' in integration.hooks;
+
+/**
+ * Load a user’s `astro:db` configuration file and additional configuration files provided by integrations.
+ */
+export async function resolveDbConfig({ root, integrations }: AstroConfig) {
+ const { mod, dependencies } = await loadUserConfigFile(root);
+ const userDbConfig = dbConfigSchema.parse(mod?.default ?? {}, { errorMap });
+ /** Resolved `astro:db` config including tables provided by integrations. */
+ const dbConfig = { tables: userDbConfig.tables ?? {} };
+
+ // Collect additional config and seed files from integrations.
+ const integrationDbConfigPaths: Array<{ name: string; configEntrypoint: string | URL }> = [];
+ const integrationSeedPaths: Array<string | URL> = [];
+ for (const integration of integrations) {
+ if (!isDbIntegration(integration)) continue;
+ const { name, hooks } = integration;
+ if (hooks['astro:db:setup']) {
+ hooks['astro:db:setup']({
+ extendDb({ configEntrypoint, seedEntrypoint }) {
+ if (configEntrypoint) {
+ integrationDbConfigPaths.push({ name, configEntrypoint });
+ }
+ if (seedEntrypoint) {
+ integrationSeedPaths.push(seedEntrypoint);
+ }
+ },
+ });
+ }
+ }
+ for (const { name, configEntrypoint } of integrationDbConfigPaths) {
+ // TODO: config file dependencies are not tracked for integrations for now.
+ const loadedConfig = await loadIntegrationConfigFile(root, configEntrypoint);
+ const integrationDbConfig = dbConfigSchema.parse(loadedConfig.mod?.default ?? {}, {
+ errorMap,
+ });
+ for (const key in integrationDbConfig.tables) {
+ if (key in dbConfig.tables) {
+ const isUserConflict = key in (userDbConfig.tables ?? {});
+ throw new Error(INTEGRATION_TABLE_CONFLICT_ERROR(name, key, isUserConflict));
+ } else {
+ dbConfig.tables[key] = integrationDbConfig.tables[key];
+ }
+ }
+ }
+
+ return {
+ /** Resolved `astro:db` config, including tables added by integrations. */
+ dbConfig,
+ /** Dependencies imported into the user config file. */
+ dependencies,
+ /** Additional `astro:db` seed file paths provided by integrations. */
+ integrationSeedPaths,
+ };
+}
+
+async function loadUserConfigFile(
root: URL
): Promise<{ mod: { default?: unknown } | undefined; dependencies: string[] }> {
let configFileUrl: URL | undefined;
@@ -16,13 +78,35 @@ export async function loadDbConfigFile(
configFileUrl = fileUrl;
}
}
- if (!configFileUrl) {
+ return await loadAndBundleDbConfigFile({ root, fileUrl: configFileUrl });
+}
+
+async function loadIntegrationConfigFile(root: URL, filePathOrUrl: string | URL) {
+ let fileUrl: URL;
+ if (typeof filePathOrUrl === 'string') {
+ const { resolve } = createRequire(root);
+ const resolvedFilePath = resolve(filePathOrUrl);
+ fileUrl = pathToFileURL(resolvedFilePath);
+ } else {
+ fileUrl = filePathOrUrl;
+ }
+ return await loadAndBundleDbConfigFile({ root, fileUrl });
+}
+
+async function loadAndBundleDbConfigFile({
+ root,
+ fileUrl,
+}: {
+ root: URL;
+ fileUrl: URL | undefined;
+}): Promise<{ mod: { default?: unknown } | undefined; dependencies: string[] }> {
+ if (!fileUrl) {
return { mod: undefined, dependencies: [] };
}
const { code, dependencies } = await bundleFile({
virtualModContents: getConfigVirtualModContents(),
root,
- fileUrl: configFileUrl,
+ fileUrl,
});
return {
mod: await importBundledFile({ code, root }),
diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts
index a88f75df9..8d46cbfec 100644
--- a/packages/db/src/core/types.ts
+++ b/packages/db/src/core/types.ts
@@ -3,6 +3,7 @@ import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import { type ZodTypeDef, z } from 'zod';
import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js';
import { errorMap } from './integration/error-map.js';
+import type { AstroIntegration } from 'astro';
export type MaybePromise<T> = T | Promise<T>;
export type MaybeArray<T> = T | T[];
@@ -271,3 +272,14 @@ export type ResolvedCollectionConfig<TColumns extends ColumnsConfig = ColumnsCon
// since Omit collapses our union type on primary key.
export type NumberColumnOpts = z.input<typeof numberColumnOptsSchema>;
export type TextColumnOpts = z.input<typeof textColumnOptsSchema>;
+
+export type AstroDbIntegration = AstroIntegration & {
+ hooks: {
+ 'astro:db:setup'?: (options: {
+ extendDb: (options: {
+ configEntrypoint?: URL | string;
+ seedEntrypoint?: URL | string;
+ }) => void;
+ }) => void | Promise<void>;
+ };
+};
diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts
index d57e3f660..2fe5af282 100644
--- a/packages/db/src/core/utils.ts
+++ b/packages/db/src/core/utils.ts
@@ -1,5 +1,6 @@
-import type { AstroConfig } from 'astro';
+import type { AstroConfig, AstroIntegration } from 'astro';
import { loadEnv } from 'vite';
+import type { AstroDbIntegration } from './types.js';
export type VitePlugin = Required<AstroConfig['vite']>['plugins'][number];
@@ -21,3 +22,7 @@ export function getAstroStudioUrl(): string {
export function getDbDirectoryUrl(root: URL | string) {
return new URL('db/', root);
}
+
+export function defineDbIntegration(integration: AstroDbIntegration): AstroIntegration {
+ return integration;
+}
diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts
index a7952e148..45b46df75 100644
--- a/packages/db/src/runtime/index.ts
+++ b/packages/db/src/runtime/index.ts
@@ -21,24 +21,36 @@ export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-clie
export async function seedLocal({
// Glob all potential seed files to catch renames and deletions.
- fileGlob,
+ userSeedGlob,
+ integrationSeedImports,
}: {
- fileGlob: Record<string, { default?: () => Promise<void> }>;
+ userSeedGlob: Record<string, { default?: () => Promise<void> }>;
+ integrationSeedImports: Array<() => Promise<{ default: () => Promise<void> }>>;
}) {
- const seedFilePath = Object.keys(fileGlob)[0];
- if (!seedFilePath) return;
- const mod = fileGlob[seedFilePath];
+ const seedFilePath = Object.keys(userSeedGlob)[0];
+ if (seedFilePath) {
+ const mod = userSeedGlob[seedFilePath];
- if (!mod.default) {
- throw new Error(SEED_DEFAULT_EXPORT_ERROR(seedFilePath));
- }
- try {
- await mod.default();
- } catch (e) {
- if (e instanceof LibsqlError) {
- throw new Error(SEED_ERROR(e.message));
+ if (!mod.default) {
+ throw new Error(SEED_DEFAULT_EXPORT_ERROR(seedFilePath));
+ }
+ try {
+ await mod.default();
+ } catch (e) {
+ if (e instanceof LibsqlError) {
+ throw new Error(SEED_ERROR(e.message));
+ }
+ throw e;
}
- throw e;
+ }
+ for (const importModule of integrationSeedImports) {
+ const mod = await importModule();
+ await mod.default().catch((e) => {
+ if (e instanceof LibsqlError) {
+ throw new Error(SEED_ERROR(e.message));
+ }
+ throw e;
+ });
}
}
diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts
index 0b4c31832..4e1a18685 100644
--- a/packages/db/src/utils.ts
+++ b/packages/db/src/utils.ts
@@ -1 +1,2 @@
+export { defineDbIntegration } from './core/utils.js';
export { asDrizzleTable } from './runtime/index.js';
diff --git a/packages/db/test/fixtures/integrations/astro.config.mjs b/packages/db/test/fixtures/integrations/astro.config.mjs
new file mode 100644
index 000000000..23f52739e
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/astro.config.mjs
@@ -0,0 +1,8 @@
+import db from '@astrojs/db';
+import { defineConfig } from 'astro/config';
+import testIntegration from './integration';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [db(), testIntegration()],
+});
diff --git a/packages/db/test/fixtures/integrations/db/config.ts b/packages/db/test/fixtures/integrations/db/config.ts
new file mode 100644
index 000000000..a581d1279
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/db/config.ts
@@ -0,0 +1,12 @@
+import { column, defineDB, defineTable } from 'astro:db';
+
+const Author = defineTable({
+ columns: {
+ name: column.text(),
+ age2: column.number({ optional: true }),
+ },
+});
+
+export default defineDB({
+ tables: { Author },
+});
diff --git a/packages/db/test/fixtures/integrations/db/seed.ts b/packages/db/test/fixtures/integrations/db/seed.ts
new file mode 100644
index 000000000..56ffb5668
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/db/seed.ts
@@ -0,0 +1,13 @@
+import { Author, db } from 'astro:db';
+
+export default async () => {
+ await db
+ .insert(Author)
+ .values([
+ { name: 'Ben' },
+ { name: 'Nate' },
+ { name: 'Erika' },
+ { name: 'Bjorn' },
+ { name: 'Sarah' },
+ ]);
+};
diff --git a/packages/db/test/fixtures/integrations/integration/config.ts b/packages/db/test/fixtures/integrations/integration/config.ts
new file mode 100644
index 000000000..00b8123c4
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/integration/config.ts
@@ -0,0 +1,8 @@
+import { defineDB } from 'astro:db';
+import { menu } from './shared';
+
+export default defineDB({
+ tables: {
+ menu,
+ },
+});
diff --git a/packages/db/test/fixtures/integrations/integration/index.ts b/packages/db/test/fixtures/integrations/integration/index.ts
new file mode 100644
index 000000000..b249cc253
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/integration/index.ts
@@ -0,0 +1,15 @@
+import { defineDbIntegration } from '@astrojs/db/utils';
+
+export default function testIntegration() {
+ return defineDbIntegration({
+ name: 'db-test-integration',
+ hooks: {
+ 'astro:db:setup'({ extendDb }) {
+ extendDb({
+ configEntrypoint: './integration/config.ts',
+ seedEntrypoint: './integration/seed.ts',
+ });
+ },
+ },
+ });
+}
diff --git a/packages/db/test/fixtures/integrations/integration/seed.ts b/packages/db/test/fixtures/integrations/integration/seed.ts
new file mode 100644
index 000000000..cf10d6657
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/integration/seed.ts
@@ -0,0 +1,14 @@
+import { asDrizzleTable } from '@astrojs/db/utils';
+import { db } from 'astro:db';
+import { menu } from './shared';
+
+export default async function () {
+ const table = asDrizzleTable('menu', menu);
+
+ await db.insert(table).values([
+ { name: 'Pancakes', price: 9.5, type: 'Breakfast' },
+ { name: 'French Toast', price: 11.25, type: 'Breakfast' },
+ { name: 'Coffee', price: 3, type: 'Beverages' },
+ { name: 'Cappuccino', price: 4.5, type: 'Beverages' },
+ ]);
+}
diff --git a/packages/db/test/fixtures/integrations/integration/shared.ts b/packages/db/test/fixtures/integrations/integration/shared.ts
new file mode 100644
index 000000000..b4f5243de
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/integration/shared.ts
@@ -0,0 +1,9 @@
+import { defineTable, column } from 'astro:db';
+
+export const menu = defineTable({
+ columns: {
+ name: column.text(),
+ type: column.text(),
+ price: column.number(),
+ },
+});
diff --git a/packages/db/test/fixtures/integrations/package.json b/packages/db/test/fixtures/integrations/package.json
new file mode 100644
index 000000000..1bb17a8c7
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@test/db-integration",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview"
+ },
+ "dependencies": {
+ "@astrojs/db": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/db/test/fixtures/integrations/src/pages/index.astro b/packages/db/test/fixtures/integrations/src/pages/index.astro
new file mode 100644
index 000000000..3e9c30ef7
--- /dev/null
+++ b/packages/db/test/fixtures/integrations/src/pages/index.astro
@@ -0,0 +1,17 @@
+---
+/// <reference path="../../.astro/db-types.d.ts" />
+import { Author, db, menu } from 'astro:db';
+
+const authors = await db.select().from(Author);
+const menuItems = await db.select().from(menu);
+---
+
+<h2>Authors</h2>
+<ul class="authors-list">
+ {authors.map((author) => <li>{author.name}</li>)}
+</ul>
+
+<h2>Menu</h2>
+<ul class="menu">
+ {menuItems.map((item) => <li>{item.name}</li>)}
+</ul>
diff --git a/packages/db/test/integrations.test.js b/packages/db/test/integrations.test.js
new file mode 100644
index 000000000..c2f12109c
--- /dev/null
+++ b/packages/db/test/integrations.test.js
@@ -0,0 +1,48 @@
+import { expect } from 'chai';
+import { load as cheerioLoad } from 'cheerio';
+import { loadFixture } from '../../astro/test/test-utils.js';
+
+describe('astro:db with integrations', () => {
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/integrations/', import.meta.url),
+ });
+ });
+
+ // Note(bholmesdev): Use in-memory db to avoid
+ // Multiple dev servers trying to unlink and remount
+ // the same database file.
+ process.env.TEST_IN_MEMORY_DB = 'true';
+ describe('development', () => {
+ let devServer;
+
+ before(async () => {
+ console.log('starting dev server');
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ process.env.TEST_IN_MEMORY_DB = undefined;
+ });
+
+ it('Prints the list of authors from user-defined table', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ const ul = $('.authors-list');
+ expect(ul.children()).to.have.a.lengthOf(5);
+ expect(ul.children().eq(0).text()).to.equal('Ben');
+ });
+
+ it('Prints the list of menu items from integration-defined table', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ const ul = $('ul.menu');
+ expect(ul.children()).to.have.a.lengthOf(4);
+ expect(ul.children().eq(0).text()).to.equal('Pancakes');
+ });
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 082cbbf7e..7fc969592 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3900,6 +3900,15 @@ importers:
specifier: workspace:*
version: link:../../../../astro
+ packages/db/test/fixtures/integrations:
+ dependencies:
+ '@astrojs/db':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: workspace:*
+ version: link:../../../../astro
+
packages/db/test/fixtures/recipes:
dependencies:
'@astrojs/db':