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