diff --git a/packages/db/src/integration.ts b/packages/db/src/integration.ts index cfc292f27f..bca3e2b79a 100644 --- a/packages/db/src/integration.ts +++ b/packages/db/src/integration.ts @@ -22,7 +22,7 @@ export function integration(): AstroIntegration { if (existsSync(dbUrl)) { await rm(dbUrl); } - const db = await createDb({ dbUrl: dbUrl.href }); + const db = await createDb({ collections, dbUrl: dbUrl.href, seeding: true }); await setupDbTables({ db, collections, logger }); logger.info('Collections set up 🚀'); diff --git a/packages/db/src/internal.ts b/packages/db/src/internal.ts index 3b5b6693e7..9e80df52a6 100644 --- a/packages/db/src/internal.ts +++ b/packages/db/src/internal.ts @@ -1,18 +1,19 @@ import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; import { createClient } from '@libsql/client'; -import type { - BooleanField, - DBCollection, - DBCollections, - DBField, - DateField, - FieldType, - JsonField, - NumberField, - TextField, +import { + collectionSchema, + type BooleanField, + type DBCollection, + type DBCollections, + type DBField, + type DateField, + type FieldType, + type JsonField, + type NumberField, + type TextField, } from './types.js'; import { type LibSQLDatabase, drizzle } from 'drizzle-orm/libsql'; -import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; +import { SQLiteAsyncDialect, SQLiteTable } from 'drizzle-orm/sqlite-core'; import { bold } from 'kleur/colors'; import { type SQL, type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm'; import { @@ -39,10 +40,45 @@ export type { const sqlite = new SQLiteAsyncDialect(); -export async function createDb({ dbUrl }: { dbUrl: string }) { +function checkIfModificationIsAllowed(collections: DBCollections, Table: SQLiteTable) { + // This totally works, don't worry about it + const tableName = (Table as any)[(SQLiteTable as any).Symbol.Name]; + const collection = collections[tableName]; + if(collection.source === 'readable') { + throw new Error(`The [${tableName}] collection is read-only.`); + } +} + +export async function createDb({ collections, dbUrl, seeding }: { + dbUrl: string; + collections: DBCollections; + seeding: boolean; +}) { const client = createClient({ url: dbUrl }); const db = drizzle(client); - return db; + + if(seeding) return db; + + const { + insert: drizzleInsert, + update: drizzleUpdate, + delete: drizzleDelete + } = db; + return Object.assign(db, { + insert(Table: SQLiteTable) { + //console.log('Table info...', Table._); + checkIfModificationIsAllowed(collections, Table); + return drizzleInsert.call(this, Table); + }, + update(Table: SQLiteTable) { + checkIfModificationIsAllowed(collections, Table); + return drizzleUpdate.call(this, Table); + }, + delete(Table: SQLiteTable) { + checkIfModificationIsAllowed(collections, Table); + return drizzleDelete.call(this, Table); + }, + }); } export async function setupDbTables({ @@ -217,6 +253,7 @@ export function collectionToTable( } const table = sqliteTable(name, columns); + return table; } diff --git a/packages/db/src/vite-plugin-db.ts b/packages/db/src/vite-plugin-db.ts index fd3647bc58..f18cf578a6 100644 --- a/packages/db/src/vite-plugin-db.ts +++ b/packages/db/src/vite-plugin-db.ts @@ -35,14 +35,13 @@ export function getVirtualModContents({ root: URL; }) { const dbUrl = getDbUrl(root).href; - const shouldSetUpDb = !existsSync(getDbUrl(root)); return ` import { collectionToTable, createDb } from ${INTERNAL_MOD_IMPORT}; export const db = await createDb(${JSON.stringify({ collections, dbUrl, - createTables: shouldSetUpDb, + seeding: false, })}); export * from ${DRIZZLE_MOD_IMPORT}; diff --git a/packages/db/test/basics.test.js b/packages/db/test/basics.test.js index 54cd38e737..4b145a18a6 100644 --- a/packages/db/test/basics.test.js +++ b/packages/db/test/basics.test.js @@ -1,27 +1,53 @@ import { expect } from 'chai'; import { load as cheerioLoad } from 'cheerio'; import { loadFixture } from '../../astro/test/test-utils.js'; +import testAdapter from '../../astro/test/test-adapter.js'; describe('astro:db', () => { let fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/basics/', import.meta.url), + output: 'server', + adapter: testAdapter() }); }); - describe('build', () => { + describe('production', () => { before(async () => { await fixture.build(); }); it('Prints the list of authors', async () => { - const html = await fixture.readFile('/index.html'); + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/'); + const res = await app.render(request); + const html = await res.text(); const $ = cheerioLoad(html); const ul = $('ul'); expect(ul.children()).to.have.a.lengthOf(5); expect(ul.children().eq(0).text()).to.equal('Ben'); }); + + it('Errors when inserting to a readonly collection', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/insert-into-readonly'); + const res = await app.render(request); + const html = await res.text(); + const $ = cheerioLoad(html); + + expect($('#error').text()).to.equal('The [Author] collection is read-only.'); + }); + + it('Does not error when inserting into writable collection', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/insert-into-writable'); + const res = await app.render(request); + const html = await res.text(); + const $ = cheerioLoad(html); + + expect($('#error').text()).to.equal(''); + }); }); }); diff --git a/packages/db/test/fixtures/basics/astro.config.ts b/packages/db/test/fixtures/basics/astro.config.ts index 41fb2cf4e4..7b54863bf9 100644 --- a/packages/db/test/fixtures/basics/astro.config.ts +++ b/packages/db/test/fixtures/basics/astro.config.ts @@ -5,7 +5,7 @@ const Author = defineCollection({ fields: { name: field.text(), }, - source: 'local', + source: 'readable', data() { return [ { @@ -25,13 +25,27 @@ const Author = defineCollection({ }, ] } -}) +}); + +const Themes = defineCollection({ + fields: { + name: field.text(), + }, + source: 'writable', + data() { + return [ + { + name: 'One', + }, + ] + } +}); // https://astro.build/config export default defineConfig({ integrations: [db()], db: { - collections: { Author }, + collections: { Author, Themes }, } }); diff --git a/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro b/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro new file mode 100644 index 0000000000..ef4bc3a693 --- /dev/null +++ b/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro @@ -0,0 +1,14 @@ +--- +import { Author, db } from 'astro:db'; + +const authors = await db.select().from(Author); + +let error: any = {}; +try { + db.insert(Author).values({ name: 'Person A' }) +} catch(err) { + error = err; +} +--- + +