diff --git a/packages/db/src/cli/index.ts b/packages/db/src/cli/index.ts index fa7903d6a4..e5a0d70068 100644 --- a/packages/db/src/cli/index.ts +++ b/packages/db/src/cli/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { red } from 'kleur/colors'; -import { astroConfigWithDBValidator } from '../config.js'; +import { astroConfigWithDbSchema } from '../config.js'; import { errorMap } from '../error-map.js'; import { loadAstroConfig } from '../load-astro-config.js'; @@ -26,14 +26,14 @@ async function main() { async function getAstroConfigOrExit(root: string = process.cwd()) { const astroConfig = await loadAstroConfig(root); - const parsed = astroConfigWithDBValidator.safeParse(astroConfig, { errorMap }); + const parsed = astroConfigWithDbSchema.safeParse(astroConfig, { errorMap }); if (parsed.success) { return parsed.data; } // eslint-disable-next-line no-console console.error( red('⚠️ Invalid studio config. Check your astro config file\n') + - parsed.error.issues.map((i) => i.message).join('\n'), + parsed.error.issues.map((i) => i.message).join('\n') ); process.exit(0); } diff --git a/packages/db/src/config.ts b/packages/db/src/config.ts index 94a8c769f7..cc8baa4564 100644 --- a/packages/db/src/config.ts +++ b/packages/db/src/config.ts @@ -10,32 +10,40 @@ import { } from './types.js'; import { z } from 'zod'; -export const adjustedConfigSchema = z.object({ +export const dbConfigSchema = z.object({ + studio: z.boolean().optional(), collections: collectionsSchema.optional(), }); -export type DBUserConfig = z.input; +export type DBUserConfig = z.input; -export const astroConfigWithDBValidator = z.object({ - db: adjustedConfigSchema.optional(), +export const astroConfigWithDbSchema = z.object({ + db: dbConfigSchema.optional(), }); -type CollectionConfig['fields'], Writable extends boolean> = - Writable extends true ? ( - { +type CollectionConfig< + TFields extends z.input['fields'], + Writable extends boolean, +> = Writable extends true + ? { fields: TFields; // TODO: type inference based on field type. Just `any` for now. - seed?: Writable extends false ? never : () => Array & { id?: string }>; + seed?: Writable extends false + ? never + : () => Array & { id?: string }>; } - ) : ( - { + : { fields: TFields; // TODO: type inference based on field type. Just `any` for now. - data?: Writable extends true ? never : () => Array & { id?: string }>; - } - ) + data?: Writable extends true + ? never + : () => Array & { id?: string }>; + }; -type ResolvedCollectionConfig['fields'], Writable extends boolean> = CollectionConfig & { +type ResolvedCollectionConfig< + TFields extends z.input['fields'], + Writable extends boolean, +> = CollectionConfig & { writable: Writable; }; @@ -44,20 +52,20 @@ export function defineCollection { return { ...userConfig, - writable: false + writable: false, }; } -export function defineWritableCollection['fields']>( - userConfig: CollectionConfig -): ResolvedCollectionConfig { +export function defineWritableCollection< + TFields extends z.input['fields'], +>(userConfig: CollectionConfig): ResolvedCollectionConfig { return { ...userConfig, - writable: true + writable: true, }; } -export type AstroConfigWithDB = z.infer; +export type AstroConfigWithDB = z.infer; type FieldOpts = Omit; diff --git a/packages/db/src/consts.ts b/packages/db/src/consts.ts index f472201869..8e64ca4027 100644 --- a/packages/db/src/consts.ts +++ b/packages/db/src/consts.ts @@ -1,4 +1,3 @@ - import { readFileSync } from 'node:fs'; export const PACKAGE_NAME = JSON.parse( @@ -13,6 +12,6 @@ export const DB_TYPES_FILE = 'db-types.d.ts'; export const VIRTUAL_MODULE_ID = 'astro:db'; // TODO: copy DB to build for serverless -export function getDbUrl(root: URL) { +export function getLocalDbUrl(root: URL) { return new URL('.astro/content.db', root); } diff --git a/packages/db/src/integration.ts b/packages/db/src/integration.ts index bca3e2b79a..d9384ad69e 100644 --- a/packages/db/src/integration.ts +++ b/packages/db/src/integration.ts @@ -2,11 +2,13 @@ import type { AstroIntegration } from 'astro'; import { vitePluginDb } from './vite-plugin-db.js'; import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js'; import { typegen } from './typegen.js'; -import { collectionsSchema } from './types.js'; import { existsSync } from 'fs'; import { rm } from 'fs/promises'; -import { getDbUrl } from './consts.js'; +import { getLocalDbUrl } from './consts.js'; import { createDb, setupDbTables } from './internal.js'; +import { astroConfigWithDbSchema } from './config.js'; +import { getAstroStudioEnv, type VitePlugin } from './utils.js'; +import { appTokenError } from './errors.js'; export function integration(): AstroIntegration { return { @@ -17,27 +19,33 @@ export function integration(): AstroIntegration { // TODO: refine where we load collections // @matthewp: may want to load collections by path at runtime - const collections = collectionsSchema.parse(config.db?.collections ?? {}); - const dbUrl = getDbUrl(config.root); - if (existsSync(dbUrl)) { - await rm(dbUrl); + const configWithDb = astroConfigWithDbSchema.parse(config); + const collections = configWithDb.db?.collections ?? {}; + const studio = configWithDb.db?.studio ?? false; + + let dbPlugin: VitePlugin; + if (studio && command === 'build') { + const appToken = getAstroStudioEnv().ASTRO_STUDIO_APP_TOKEN; + if (!appToken) { + logger.error(appTokenError); + process.exit(0); + } + dbPlugin = vitePluginDb({ connectToStudio: true, collections, appToken }); + } else { + const dbUrl = getLocalDbUrl(config.root).href; + if (existsSync(dbUrl)) { + await rm(dbUrl); + } + const db = await createDb({ collections, dbUrl, seeding: true }); + await setupDbTables({ db, collections, logger }); + logger.info('Collections set up 🚀'); + + dbPlugin = vitePluginDb({ connectToStudio: false, collections, dbUrl }); } - const db = await createDb({ collections, dbUrl: dbUrl.href, seeding: true }); - await setupDbTables({ db, collections, logger }); - logger.info('Collections set up 🚀'); updateConfig({ vite: { - plugins: [ - // TODO: figure out when vite.Plugin doesn't line up with these types - // @ts-ignore - vitePluginDb({ - collections, - root: config.root, - }), - // @ts-ignore - vitePluginInjectEnvTs(config), - ], + plugins: [dbPlugin, vitePluginInjectEnvTs(config)], }, }); await typegen({ collections, root: config.root }); diff --git a/packages/db/src/internal.ts b/packages/db/src/internal.ts index b8cef7c6bb..38814b02f3 100644 --- a/packages/db/src/internal.ts +++ b/packages/db/src/internal.ts @@ -2,7 +2,6 @@ import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; import { createClient } from '@libsql/client'; import { type ReadableDBCollection, - type WritableDBCollection, type BooleanField, type DBCollection, type DBCollections, @@ -49,12 +48,18 @@ function checkIfModificationIsAllowed(collections: DBCollections, Table: SQLiteT // This totally works, don't worry about it const tableName = (Table as any)[(SQLiteTable as any).Symbol.Name]; const collection = collections[tableName]; - if(collection.writable) { + if (collection.writable) { throw new Error(`The [${tableName}] collection is read-only.`); } } -export async function createDb({ collections, dbUrl, seeding }: { +export { createDb as createStudioDb } from './cli/sync/remote-db.js'; + +export async function createDb({ + collections, + dbUrl, + seeding, +}: { dbUrl: string; collections: DBCollections; seeding: boolean; @@ -62,13 +67,9 @@ export async function createDb({ collections, dbUrl, seeding }: { const client = createClient({ url: dbUrl }); const db = drizzle(client); - if(seeding) return db; + if (seeding) return db; - const { - insert: drizzleInsert, - update: drizzleUpdate, - delete: drizzleDelete - } = db; + const { insert: drizzleInsert, update: drizzleUpdate, delete: drizzleDelete } = db; return Object.assign(db, { insert(Table: SQLiteTable) { //console.log('Table info...', Table._); diff --git a/packages/db/src/vite-plugin-db.ts b/packages/db/src/vite-plugin-db.ts index f18cf578a6..f48bbb8d70 100644 --- a/packages/db/src/vite-plugin-db.ts +++ b/packages/db/src/vite-plugin-db.ts @@ -1,17 +1,22 @@ -import { existsSync } from 'node:fs'; -import { DRIZZLE_MOD_IMPORT, INTERNAL_MOD_IMPORT, VIRTUAL_MODULE_ID, getDbUrl } from './consts.js'; +import { DRIZZLE_MOD_IMPORT, INTERNAL_MOD_IMPORT, VIRTUAL_MODULE_ID } from './consts.js'; import type { DBCollections } from './types.js'; -import type { Plugin as VitePlugin } from 'vite'; +import type { VitePlugin } from './utils.js'; const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID; -export function vitePluginDb({ - collections, - root, -}: { - collections: DBCollections; - root: URL; -}): VitePlugin { +export function vitePluginDb( + params: + | { + connectToStudio: false; + collections: DBCollections; + dbUrl: string; + } + | { + connectToStudio: true; + collections: DBCollections; + appToken: string; + } +): VitePlugin { return { name: 'astro:db', enforce: 'pre', @@ -22,19 +27,22 @@ export function vitePluginDb({ }, load(id) { if (id !== resolvedVirtualModuleId) return; - return getVirtualModContents({ collections, root }); + + if (params.connectToStudio) { + return getStudioVirtualModContents(params); + } + return getVirtualModContents(params); }, }; } export function getVirtualModContents({ collections, - root, + dbUrl, }: { collections: DBCollections; - root: URL; + dbUrl: string; }) { - const dbUrl = getDbUrl(root).href; return ` import { collectionToTable, createDb } from ${INTERNAL_MOD_IMPORT}; @@ -43,12 +51,32 @@ export const db = await createDb(${JSON.stringify({ dbUrl, seeding: false, })}); + export * from ${DRIZZLE_MOD_IMPORT}; ${getStringifiedCollectionExports(collections)} `; } +export function getStudioVirtualModContents({ + collections, + appToken, +}: { + collections: DBCollections; + appToken: string; +}) { + return ` +import {collectionToTable, createStudioDb} from ${INTERNAL_MOD_IMPORT}; + +export const db = await createStudioDb(${JSON.stringify({ + appToken, + })}); +export * from ${DRIZZLE_MOD_IMPORT}; + +${getStringifiedCollectionExports(collections)} + `; +} + function getStringifiedCollectionExports(collections: DBCollections) { return Object.entries(collections) .map( diff --git a/packages/db/src/vite-plugin-inject-env-ts.ts b/packages/db/src/vite-plugin-inject-env-ts.ts index b85cebd9bf..373b85f6ee 100644 --- a/packages/db/src/vite-plugin-inject-env-ts.ts +++ b/packages/db/src/vite-plugin-inject-env-ts.ts @@ -4,8 +4,8 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { bold, cyan } from 'kleur/colors'; import { normalizePath } from 'vite'; -import type { Plugin as VitePlugin } from 'vite'; import { DB_TYPES_FILE } from './consts.js'; +import type { VitePlugin } from './utils.js'; export function vitePluginInjectEnvTs({ srcDir, root }: { srcDir: URL; root: URL }): VitePlugin { return {