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:
parent
203dbe369f
commit
9c0139b86e
7 changed files with 154 additions and 82 deletions
28
packages/db/src/core/cli/commands/execute/index.ts
Normal file
28
packages/db/src/core/cli/commands/execute/index.ts
Normal 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 });
|
||||
}
|
102
packages/db/src/core/cli/commands/execute/load-file.ts
Normal file
102
packages/db/src/core/cli/commands/execute/load-file.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 <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) => {
|
||||
return `${red(`Error seeding table ${bold(tableName)}:`)}\n\n${error}`;
|
||||
};
|
||||
|
|
11
packages/db/test/fixtures/basics/db/seed.prod.ts
vendored
Normal file
11
packages/db/test/fixtures/basics/db/seed.prod.ts
vendored
Normal 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' },
|
||||
]);
|
Loading…
Add table
Reference in a new issue