Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix db for projects without a seed file or with integrations #10385

Merged
merged 14 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-roses-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/db": patch
---

Fixes support for integrations configuring `astro:db` and for projects that use `astro:db` but do not include a seed file.
43 changes: 5 additions & 38 deletions packages/db/src/core/integration/vite-plugin-db.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { type SQL, sql } from 'drizzle-orm';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import { normalizePath } from 'vite';
import { createLocalDatabaseClient } from '../../runtime/db-client.js';
import type { SqliteDB } from '../../runtime/index.js';
import {
SEED_DEV_FILE_NAME,
getCreateIndexQueries,
getCreateTableQuery,
} from '../../runtime/queries.js';
import { SEED_DEV_FILE_NAME } from '../../runtime/queries.js';
import { DB_PATH, RUNTIME_CONFIG_IMPORT, RUNTIME_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js';
import type { DBTables } from '../types.js';
import { type VitePlugin, getDbDirectoryUrl, getRemoteDatabaseUrl } from '../utils.js';
Expand Down Expand Up @@ -46,9 +37,6 @@ type VitePluginDBParams =

export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
const srcDirPath = normalizePath(fileURLToPath(params.srcDir));
const seedFilePaths = SEED_DEV_FILE_NAME.map((name) =>
normalizePath(fileURLToPath(new URL(name, getDbDirectoryUrl(params.root))))
);
return {
name: 'astro:db',
enforce: 'pre',
Expand All @@ -67,14 +55,6 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
return resolved.virtual;
},
async load(id) {
// Recreate tables whenever a seed file is loaded.
if (seedFilePaths.some((f) => id === f)) {
await recreateTables({
db: createLocalDatabaseClient({ dbUrl: new URL(DB_PATH, params.root).href }),
tables: params.tables.get(),
});
}

if (id !== resolved.virtual && id !== resolved.seedVirtual) return;

if (params.connectToStudio) {
Expand Down Expand Up @@ -113,7 +93,8 @@ export function getLocalVirtualModContents({
// for Vite import.meta.glob
(name) => new URL(name, getDbDirectoryUrl('file:///')).pathname
);
const resolveId = (id: string) => (id.startsWith('.') ? resolve(fileURLToPath(root), id) : id);
const resolveId = (id: string) =>
id.startsWith('.') ? normalizePath(fileURLToPath(new URL(id, root))) : id;
// Use top-level imports to correctly resolve `astro:db` within seed files.
// Dynamic imports cause a silent build failure,
// potentially because of circular module references.
Expand All @@ -138,6 +119,8 @@ export const db = createLocalDatabaseClient({ dbUrl });
${
shouldSeed
? `await seedLocal({
db,
tables: ${JSON.stringify(tables)},
userSeedGlob: import.meta.glob(${JSON.stringify(userSeedFilePaths)}, { eager: true }),
integrationSeedFunctions: [${integrationSeedImportNames.join(',')}],
});`
Expand Down Expand Up @@ -180,19 +163,3 @@ function getStringifiedCollectionExports(tables: DBTables) {
)
.join('\n');
}

const sqlite = new SQLiteAsyncDialect();

async function recreateTables({ db, tables }: { db: SqliteDB; tables: DBTables }) {
const setupQueries: SQL[] = [];
for (const [name, table] of Object.entries(tables)) {
const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`);
const createQuery = sql.raw(getCreateTableQuery(name, table));
const indexQueries = getCreateIndexQueries(name, table);
setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s)));
}
await db.batch([
db.run(sql`pragma defer_foreign_keys=true;`),
...setupQueries.map((q) => db.run(q)),
]);
}
37 changes: 1 addition & 36 deletions packages/db/src/runtime/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { LibsqlError } from '@libsql/client';
import { type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import {
Expand All @@ -10,48 +9,14 @@ import {
sqliteTable,
text,
} from 'drizzle-orm/sqlite-core';
import { SEED_DEFAULT_EXPORT_ERROR, SEED_ERROR } from '../core/errors.js';
import { type DBColumn, type DBTable } from '../core/types.js';
import { type SerializedSQL, isSerializedSQL } from './types.js';

export { sql };
export type SqliteDB = LibSQLDatabase;
export type { Table } from './types.js';
export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js';

export async function seedLocal({
// Glob all potential seed files to catch renames and deletions.
userSeedGlob,
integrationSeedFunctions: integrationSeedFunctions,
}: {
userSeedGlob: Record<string, { default?: () => Promise<void> }>;
integrationSeedFunctions: Array<() => Promise<void>>;
}) {
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));
}
throw e;
}
}
for (const seedFn of integrationSeedFunctions) {
await seedFn().catch((e) => {
if (e instanceof LibsqlError) {
throw new Error(SEED_ERROR(e.message));
}
throw e;
});
}
}
export { seedLocal } from './seed-local.js';

export function hasPrimaryKey(column: DBColumn) {
return 'primaryKey' in column.schema && !!column.schema.primaryKey;
Expand Down
58 changes: 58 additions & 0 deletions packages/db/src/runtime/seed-local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { LibsqlError } from '@libsql/client';
import { sql, type SQL } from 'drizzle-orm';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import { SEED_DEFAULT_EXPORT_ERROR, SEED_ERROR } from '../core/errors.js';
import { type DBTables } from '../core/types.js';
import { getCreateIndexQueries, getCreateTableQuery } from './queries.js';

const sqlite = new SQLiteAsyncDialect();

export async function seedLocal({
db,
tables,
// Glob all potential seed files to catch renames and deletions.
userSeedGlob,
integrationSeedFunctions,
}: {
db: LibSQLDatabase;
tables: DBTables;
userSeedGlob: Record<string, { default?: () => Promise<void> }>;
integrationSeedFunctions: Array<() => Promise<void>>;
}) {
await recreateTables({ db, tables });
const seedFunctions: Array<() => Promise<void>> = [];
const seedFilePath = Object.keys(userSeedGlob)[0];
if (seedFilePath) {
const mod = userSeedGlob[seedFilePath];
if (!mod.default) throw new Error(SEED_DEFAULT_EXPORT_ERROR(seedFilePath));
seedFunctions.push(mod.default);
}
for (const seedFn of integrationSeedFunctions) {
seedFunctions.push(seedFn);
}
for (const seed of seedFunctions) {
try {
await seed();
} catch (e) {
if (e instanceof LibsqlError) {
throw new Error(SEED_ERROR(e.message));
}
throw e;
}
}
}

async function recreateTables({ db, tables }: { db: LibSQLDatabase; tables: DBTables }) {
const setupQueries: SQL[] = [];
for (const [name, table] of Object.entries(tables)) {
const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`);
const createQuery = sql.raw(getCreateTableQuery(name, table));
const indexQueries = getCreateIndexQueries(name, table);
setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s)));
}
await db.batch([
db.run(sql`pragma defer_foreign_keys=true;`),
...setupQueries.map((q) => db.run(q)),
]);
}
8 changes: 8 additions & 0 deletions packages/db/test/fixtures/integration-only/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -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()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { menu } from './shared';
import { defineDb } from 'astro:db';

export default defineDb({
tables: {
menu,
},
});
15 changes: 15 additions & 0 deletions packages/db/test/fixtures/integration-only/integration/index.ts
Original file line number Diff line number Diff line change
@@ -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',
});
},
},
});
}
14 changes: 14 additions & 0 deletions packages/db/test/fixtures/integration-only/integration/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { asDrizzleTable } from '@astrojs/db/utils';
import { menu } from './shared';
import { db } from 'astro:db';

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' },
]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { column, defineTable } from 'astro:db';

export const menu = defineTable({
columns: {
name: column.text(),
type: column.text(),
price: column.number(),
},
});
14 changes: 14 additions & 0 deletions packages/db/test/fixtures/integration-only/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@test/db-integration-only",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/db": "workspace:*",
"astro": "workspace:*"
}
}
11 changes: 11 additions & 0 deletions packages/db/test/fixtures/integration-only/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
/// <reference path="../../.astro/db-types.d.ts" />
import { db, menu } from 'astro:db';

const menuItems = await db.select().from(menu);
---

<h2>Menu</h2>
<ul class="menu">
{menuItems.map((item) => <li>{item.name}</li>)}
</ul>
7 changes: 7 additions & 0 deletions packages/db/test/fixtures/no-seed/astro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import db from '@astrojs/db';
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
integrations: [db()],
});
12 changes: 12 additions & 0 deletions packages/db/test/fixtures/no-seed/db/config.ts
Original file line number Diff line number Diff line change
@@ -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 },
});
14 changes: 14 additions & 0 deletions packages/db/test/fixtures/no-seed/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@test/db-no-seed",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/db": "workspace:*",
"astro": "workspace:*"
}
}
21 changes: 21 additions & 0 deletions packages/db/test/fixtures/no-seed/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
/// <reference path="../../.astro/db-types.d.ts" />
import { Author, db } from 'astro:db';

await db
.insert(Author)
.values([
{ name: 'Ben' },
{ name: 'Nate' },
{ name: 'Erika' },
{ name: 'Bjorn' },
{ name: 'Sarah' },
]);

const authors = await db.select().from(Author);
---

<h2>Authors</h2>
<ul class="authors-list">
{authors.map((author) => <li>{author.name}</li>)}
</ul>
47 changes: 47 additions & 0 deletions packages/db/test/integration-only.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from '../../astro/test/test-utils.js';

describe('astro:db with only integrations, no user db config', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/integration-only/', import.meta.url),
});
});

describe('development', () => {
let devServer;
before(async () => {
devServer = await fixture.startDevServer();
});

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

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

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

it('Prints the list of menu items from integration-defined table', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerioLoad(html);

const ul = $('ul.menu');
expect(ul.children()).to.have.a.lengthOf(4);
expect(ul.children().eq(0).text()).to.equal('Pancakes');
});
});
});
Loading
Loading