0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-02-24 22:46:02 -05:00

feat: db execute command

This commit is contained in:
bholmesdev 2024-02-27 13:10:15 -05:00
parent 203dbe369f
commit 9c0139b86e
7 changed files with 154 additions and 82 deletions

View file

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

View file

@ -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
}
}
}

View file

@ -1,14 +1,10 @@
import { createClient, type InStatement } from '@libsql/client'; import { type InStatement } from '@libsql/client';
import type { AstroConfig } from 'astro'; 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 { red } from 'kleur/colors';
import prompts from 'prompts'; import prompts from 'prompts';
import type { Arguments } from 'yargs-parser'; import type { Arguments } from 'yargs-parser';
import { recreateTables } from '../../../../runtime/queries.js';
import { getManagedAppTokenOrExit } from '../../../tokens.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 { getRemoteDatabaseUrl } from '../../../utils.js';
import { getMigrationQueries } from '../../migration-queries.js'; import { getMigrationQueries } from '../../migration-queries.js';
import { import {
@ -68,9 +64,6 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
currentSnapshot: migration.currentSnapshot, currentSnapshot: migration.currentSnapshot,
}); });
} }
// push the database seed data
console.info('Pushing data...');
await pushData({ config, appToken: appToken.token, isDryRun });
// cleanup and exit // cleanup and exit
await appToken.destroy(); await appToken.destroy();
console.info('Push complete!'); console.info('Push complete!');
@ -130,68 +123,6 @@ async function pushSchema({
await runMigrateQuery({ queries, migrations, snapshot: currentSnapshot, appToken, isDryRun }); 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({ async function runMigrateQuery({
queries: baseQueries, queries: baseQueries,
migrations, migrations,

View file

@ -1,6 +1,5 @@
import type { AstroConfig } from 'astro'; import type { AstroConfig } from 'astro';
import type { Arguments } from 'yargs-parser'; 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 }) { export async function cli({ flags, config }: { flags: Arguments; config: AstroConfig }) {
const args = flags._ as string[]; 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. // 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 command = args[2] === 'db' ? args[3] : args[2];
if (!config.db?.studio) {
console.log(STUDIO_CONFIG_MISSING_CLI_ERROR);
process.exit(1);
}
switch (command) { switch (command) {
case 'shell': { case 'shell': {
const { cmd } = await import('./commands/shell/index.js'); 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'); const { cmd } = await import('./commands/verify/index.js');
return await cmd({ config, flags }); return await cmd({ config, flags });
} }
case 'execute': {
const { cmd } = await import('./commands/execute/index.js');
return await cmd({ config, flags });
}
case 'login': { case 'login': {
const { cmd } = await import('./commands/login/index.js'); const { cmd } = await import('./commands/login/index.js');
return await cmd({ config, flags }); return await cmd({ config, flags });

View file

@ -16,15 +16,17 @@ export const UNSAFE_DISABLE_STUDIO_WARNING = `${yellow(
Redeploying your app may result in wiping away your database. Redeploying your app may result in wiping away your database.
I hope you know what you are doing.\n`; 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( export const MIGRATIONS_NOT_INITIALIZED = `${yellow(
'▶ No migrations found!' '▶ No migrations found!'
)}\n\n To scaffold your migrations folder, run\n ${cyan('astro db sync')}\n`; )}\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 <path>')}\n`;
export const FILE_NOT_FOUND_ERROR = (path: string) =>
`${red('▶ File not found:')} ${bold(path)}\n`;
export const SEED_ERROR = (tableName: string, error: string) => { export const SEED_ERROR = (tableName: string, error: string) => {
return `${red(`Error seeding table ${bold(tableName)}:`)}\n\n${error}`; return `${red(`Error seeding table ${bold(tableName)}:`)}\n\n${error}`;
}; };

View file

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