diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts new file mode 100644 index 0000000000..13e066fb74 --- /dev/null +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -0,0 +1,28 @@ +import type { AstroConfig } from 'astro'; +import type { Arguments } from 'yargs-parser'; +import path from 'node:path'; +import { MISSING_EXECUTE_PATH_ERROR, FILE_NOT_FOUND_ERROR } from '../../../errors.js'; +import { pathToFileURL } from 'node:url'; +import { existsSync } from 'node:fs'; +import { getManagedAppTokenOrExit } from '../../../tokens.js'; +import { tablesSchema } from '../../../types.js'; + +export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) { + const appToken = await getManagedAppTokenOrExit(flags.token); + const tables = tablesSchema.parse(config.db?.tables ?? {}); + + const filePath = flags._[4]; + if (typeof filePath !== 'string') { + console.error(MISSING_EXECUTE_PATH_ERROR); + process.exit(1); + } + + const fileUrl = pathToFileURL(path.join(process.cwd(), filePath)); + if (!existsSync(fileUrl)) { + console.error(FILE_NOT_FOUND_ERROR(filePath)); + process.exit(1); + } + + const { executeFile } = await import('./load-file.js'); + await executeFile({ fileUrl, tables, appToken: appToken.token }); +} diff --git a/packages/db/src/core/cli/commands/execute/load-file.ts b/packages/db/src/core/cli/commands/execute/load-file.ts new file mode 100644 index 0000000000..9412755de8 --- /dev/null +++ b/packages/db/src/core/cli/commands/execute/load-file.ts @@ -0,0 +1,102 @@ +import { build as esbuild } from 'esbuild'; +import { VIRTUAL_MODULE_ID } from '../../../consts.js'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { getStudioVirtualModContents } from '../../../integration/vite-plugin-db.js'; +import type { DBTables } from '../../../types.js'; +import { writeFile, unlink } from 'node:fs/promises'; + +export async function executeFile({ + fileUrl, + tables, + appToken, +}: { + fileUrl: URL; + tables: DBTables; + appToken: string; +}): Promise<{ default?: unknown } | undefined> { + const { code } = await bundleFile({ fileUrl, tables, appToken }); + // Executable files use top-level await. Importing will run the file. + return await importBundledFile(code); +} + +/** + * Bundle config file to support `.ts` files. Simplified fork from Vite's `bundleConfigFile` + * function: + * + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961 + */ +async function bundleFile({ + fileUrl, + tables, + appToken, +}: { + fileUrl: URL; + tables: DBTables; + appToken: string; +}): Promise<{ code: string }> { + const result = await esbuild({ + absWorkingDir: process.cwd(), + entryPoints: [fileURLToPath(fileUrl)], + outfile: 'out.js', + packages: 'external', + write: false, + target: ['node16'], + platform: 'node', + bundle: true, + format: 'esm', + sourcemap: 'inline', + metafile: true, + define: { + 'import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL': 'undefined', + }, + plugins: [ + { + name: 'resolve-astro-db', + setup(build) { + build.onResolve({ filter: /^astro:db$/ }, ({ path }) => { + return { path, namespace: VIRTUAL_MODULE_ID }; + }); + build.onLoad({ namespace: VIRTUAL_MODULE_ID, filter: /.*/ }, () => { + return { + contents: getStudioVirtualModContents({ tables, appToken }), + // Needed to resolve runtime dependencies + resolveDir: process.cwd(), + }; + }); + }, + }, + ], + }); + + const file = result.outputFiles[0]; + if (!file) { + throw new Error(`Unexpected: no output file`); + } + + return { + code: file.text, + }; +} + +/** + * Forked from Vite config loader, replacing CJS-based path concat with ESM only + * + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L1074 + */ +async function importBundledFile(code: string): Promise<{ default?: unknown }> { + // Write it to disk, load it with native Node ESM, then delete the file. + const tmpFileUrl = new URL( + `studio.seed.timestamp-${Date.now()}.mjs`, + pathToFileURL(process.cwd()) + ); + await writeFile(tmpFileUrl, code); + try { + return await import(tmpFileUrl.pathname); + } finally { + try { + await unlink(tmpFileUrl); + } catch { + // already removed if this function is called twice simultaneously + } + } +} diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index e67ba72a70..a96b6ed5cc 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -1,14 +1,10 @@ -import { createClient, type InStatement } from '@libsql/client'; +import { type InStatement } from '@libsql/client'; import type { AstroConfig } from 'astro'; -import { drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy'; -import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; -import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { red } from 'kleur/colors'; import prompts from 'prompts'; import type { Arguments } from 'yargs-parser'; -import { recreateTables } from '../../../../runtime/queries.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; -import { tablesSchema, type AstroConfigWithDB, type DBSnapshot } from '../../../types.js'; +import { type DBSnapshot } from '../../../types.js'; import { getRemoteDatabaseUrl } from '../../../utils.js'; import { getMigrationQueries } from '../../migration-queries.js'; import { @@ -68,9 +64,6 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum currentSnapshot: migration.currentSnapshot, }); } - // push the database seed data - console.info('Pushing data...'); - await pushData({ config, appToken: appToken.token, isDryRun }); // cleanup and exit await appToken.destroy(); console.info('Push complete!'); @@ -130,68 +123,6 @@ async function pushSchema({ await runMigrateQuery({ queries, migrations, snapshot: currentSnapshot, appToken, isDryRun }); } -const sqlite = new SQLiteAsyncDialect(); - -async function pushData({ - config, - appToken, - isDryRun, -}: { - config: AstroConfigWithDB; - appToken: string; - isDryRun?: boolean; -}) { - const queries: InStatement[] = []; - // TODO: replace with pure remote client? - // if (config.db?.data) { - // const libsqlclient = createclient({ url: ':memory:' }); - // // stand up tables locally to mirror inserts. - // // needed to generate return values. - // await recreatetables({ - // db: drizzlelibsql(libsqlclient), - // tables: tablesschema.parse(config.db.tables ?? {}), - // }); - - // // use proxy to trace all queries to queue up in a batch. - // const db = await drizzleproxy(async (sqlquery, params, method) => { - // const stmt: instatement = { sql: sqlquery, args: params }; - // queries.push(stmt); - // // use in-memory database to generate results for `returning()`. - // const { rows } = await libsqlclient.execute(stmt); - // const rowvalues: unknown[][] = []; - // for (const row of rows) { - // if (row != null && typeof row === 'object') { - // rowvalues.push(object.values(row)); - // } - // } - // if (method === 'get') { - // return { rows: rowvalues[0] }; - // } - // return { rows: rowvalues }; - // }); - // await seedData({ - // db, - // mode: 'build', - // data: config.db.data, - // }); - // } - - const url = new URL('/db/query', getRemoteDatabaseUrl()); - - if (isDryRun) { - console.info('[DRY RUN] Batch data seed:', JSON.stringify(queries, null, 2)); - return new Response(null, { status: 200 }); - } - - return await fetch(url, { - method: 'POST', - headers: new Headers({ - Authorization: `Bearer ${appToken}`, - }), - body: JSON.stringify(queries), - }); -} - async function runMigrateQuery({ queries: baseQueries, migrations, diff --git a/packages/db/src/core/cli/index.ts b/packages/db/src/core/cli/index.ts index 56a5667a67..37579e18c9 100644 --- a/packages/db/src/core/cli/index.ts +++ b/packages/db/src/core/cli/index.ts @@ -1,6 +1,5 @@ import type { AstroConfig } from 'astro'; import type { Arguments } from 'yargs-parser'; -import { STUDIO_CONFIG_MISSING_CLI_ERROR } from '../errors.js'; export async function cli({ flags, config }: { flags: Arguments; config: AstroConfig }) { const args = flags._ as string[]; @@ -8,11 +7,6 @@ export async function cli({ flags, config }: { flags: Arguments; config: AstroCo // are also handled by this package, so first check if this is a db command. const command = args[2] === 'db' ? args[3] : args[2]; - if (!config.db?.studio) { - console.log(STUDIO_CONFIG_MISSING_CLI_ERROR); - process.exit(1); - } - switch (command) { case 'shell': { const { cmd } = await import('./commands/shell/index.js'); @@ -31,6 +25,10 @@ export async function cli({ flags, config }: { flags: Arguments; config: AstroCo const { cmd } = await import('./commands/verify/index.js'); return await cmd({ config, flags }); } + case 'execute': { + const { cmd } = await import('./commands/execute/index.js'); + return await cmd({ config, flags }); + } case 'login': { const { cmd } = await import('./commands/login/index.js'); return await cmd({ config, flags }); diff --git a/packages/db/src/core/errors.ts b/packages/db/src/core/errors.ts index 18ea3013f9..8ce3a45e77 100644 --- a/packages/db/src/core/errors.ts +++ b/packages/db/src/core/errors.ts @@ -16,15 +16,17 @@ export const UNSAFE_DISABLE_STUDIO_WARNING = `${yellow( Redeploying your app may result in wiping away your database. I hope you know what you are doing.\n`; -export const STUDIO_CONFIG_MISSING_CLI_ERROR = `${red('▶ This command requires Astro Studio.')} - - Visit ${cyan('https://astro.build/studio')} to create your account - and set ${green('studio: true')} in your astro.config.mjs file to enable Studio.\n`; - export const MIGRATIONS_NOT_INITIALIZED = `${yellow( '▶ No migrations found!' )}\n\n To scaffold your migrations folder, run\n ${cyan('astro db sync')}\n`; +export const MISSING_EXECUTE_PATH_ERROR = `${red( + '▶ No file path provided.' +)} Provide a path by running ${cyan('astro db execute ')}\n`; + +export const FILE_NOT_FOUND_ERROR = (path: string) => + `${red('▶ File not found:')} ${bold(path)}\n`; + export const SEED_ERROR = (tableName: string, error: string) => { return `${red(`Error seeding table ${bold(tableName)}:`)}\n\n${error}`; }; diff --git a/packages/db/test/fixtures/basics/db/seed.ts b/packages/db/test/fixtures/basics/db/seed.dev.ts similarity index 100% rename from packages/db/test/fixtures/basics/db/seed.ts rename to packages/db/test/fixtures/basics/db/seed.dev.ts diff --git a/packages/db/test/fixtures/basics/db/seed.prod.ts b/packages/db/test/fixtures/basics/db/seed.prod.ts new file mode 100644 index 0000000000..e2a191fd48 --- /dev/null +++ b/packages/db/test/fixtures/basics/db/seed.prod.ts @@ -0,0 +1,11 @@ +import { db, Author } from 'astro:db'; + +await db + .insert(Author) + .values([ + { name: 'Ben' }, + { name: 'Nate' }, + { name: 'Erika' }, + { name: 'Bjorn' }, + { name: 'Sarah' }, + ]);