0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-17 23:11:29 -05:00

feat(db-cli): clean up CLI logging, support --json flag for astro db verify, extract shared logic to a utility

This commit is contained in:
Nate Moore 2024-02-09 08:51:46 -06:00
parent 1f719ffb6d
commit ed4294abbe
5 changed files with 168 additions and 85 deletions

View file

@ -1,6 +1,5 @@
import { createClient, type InStatement } from '@libsql/client';
import type { AstroConfig } from 'astro';
import deepDiff from 'deep-diff';
import { drizzle } from 'drizzle-orm/sqlite-proxy';
import { red } from 'kleur/colors';
import prompts from 'prompts';
@ -11,44 +10,56 @@ import type { AstroConfigWithDB, DBSnapshot } from '../../../types.js';
import { getRemoteDatabaseUrl } from '../../../utils.js';
import { getMigrationQueries } from '../../migration-queries.js';
import {
createCurrentSnapshot,
createEmptySnapshot,
getMigrations,
initializeFromMigrations,
getMigrationStatus,
loadInitialSnapshot,
loadMigration,
MIGRATION_NEEDED,
MIGRATIONS_NOT_INITIALIZED,
MIGRATIONS_UP_TO_DATE,
} from '../../migrations.js';
const { diff } = deepDiff;
import { MISSING_SESSION_ID_ERROR } from '../../../errors.js';
export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) {
const isSeedData = flags.seed;
const isDryRun = flags.dryRun;
const appToken = await getManagedAppTokenOrExit(flags.token);
const currentSnapshot = createCurrentSnapshot(config);
const allMigrationFiles = await getMigrations();
if (allMigrationFiles.length === 0) {
console.log('Project not yet initialized!');
process.exit(1);
}
const prevSnapshot = await initializeFromMigrations(allMigrationFiles);
const calculatedDiff = diff(prevSnapshot, currentSnapshot);
if (calculatedDiff) {
console.log('Changes detected!');
console.log(calculatedDiff);
const migration = await getMigrationStatus(config);
if (migration.state === 'no-migrations-found') {
console.log(MIGRATIONS_NOT_INITIALIZED)
process.exit(1);
} else if (migration.state === 'ahead') {
console.log(MIGRATION_NEEDED);
process.exit(1);
}
// get all migrations from the filesystem
const allLocalMigrations = await getMigrations();
const { data: missingMigrations } = await prepareMigrateQuery({
migrations: allLocalMigrations,
appToken: appToken.token,
});
let missingMigrations: string[] = [];
try {
const { data } = await prepareMigrateQuery({
migrations: allLocalMigrations,
appToken: appToken.token,
})
missingMigrations = data;
} catch (e) {
if (e instanceof Error) {
if (e.message.startsWith('{')) {
const { error: { code } = { code: "" } } = JSON.parse(e.message);
if (code === 'TOKEN_UNAUTHORIZED') {
console.error(MISSING_SESSION_ID_ERROR);
}
process.exit(1);
}
}
console.error(e);
process.exit(1);
}
// exit early if there are no migrations to push
if (missingMigrations.length === 0) {
console.info('No migrations to push! Your database is up to date!');
console.log(MIGRATIONS_UP_TO_DATE);
process.exit(0);
}
// push the database schema
@ -58,7 +69,7 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
migrations: missingMigrations,
appToken: appToken.token,
isDryRun,
currentSnapshot,
currentSnapshot: migration.currentSnapshot,
});
}
// push the database seed data

View file

@ -1,57 +1,43 @@
import type { AstroConfig } from 'astro';
import deepDiff from 'deep-diff';
import { writeFile } from 'fs/promises';
import type { Arguments } from 'yargs-parser';
import { writeFile } from 'node:fs/promises';
import {
createCurrentSnapshot,
getMigrations,
initializeFromMigrations,
MIGRATIONS_CREATED,
MIGRATIONS_UP_TO_DATE,
getMigrationStatus,
initializeMigrationsDirectory,
} from '../../migrations.js';
import { getMigrationQueries } from '../../migration-queries.js';
import prompts from 'prompts';
import { bgRed, bold, red, reset } from 'kleur/colors';
const { diff } = deepDiff;
import { bgRed, red, reset } from 'kleur/colors';
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
const currentSnapshot = createCurrentSnapshot(config);
const allMigrationFiles = await getMigrations();
if (allMigrationFiles.length === 0) {
await initializeMigrationsDirectory(currentSnapshot);
console.log('Project initialized!');
return;
}
const prevSnapshot = await initializeFromMigrations(allMigrationFiles);
const calculatedDiff = diff(prevSnapshot, currentSnapshot);
if (!calculatedDiff) {
console.log('No changes detected!');
const migration = await getMigrationStatus(config);
if (migration.state === 'no-migrations-found') {
await initializeMigrationsDirectory(migration.currentSnapshot);
console.log(MIGRATIONS_CREATED);
return;
} else if (migration.state === 'up-to-date') {
console.log(MIGRATIONS_UP_TO_DATE);
return;
}
const { oldSnapshot, newSnapshot, newFilename, diff } = migration;
const { queries: migrationQueries, confirmations } = await getMigrationQueries({
oldSnapshot: prevSnapshot,
newSnapshot: currentSnapshot,
oldSnapshot,
newSnapshot
});
// Warn the user about any changes that lead to data-loss.
// When the user runs `db push`, they will be prompted to confirm these changes.
confirmations.map((message) => console.log(bgRed(' !!! ') + ' ' + red(message)));
// Generate the new migration filename by calculating the largest number.
const largestNumber = allMigrationFiles.reduce((acc, curr) => {
const num = parseInt(curr.split('_')[0]);
return num > acc ? num : acc;
}, 0);
const migrationFileContent = {
diff: calculatedDiff,
diff,
db: migrationQueries,
// TODO(fks): Encode the relevant data, instead of the raw message.
// This will give `db push` more control over the formatting of the message.
confirm: confirmations.map(c => reset(c)),
};
const migrationFileName = `./migrations/${String(largestNumber + 1).padStart(
4,
'0'
)}_migration.json`;
const migrationFileName = `./migrations/${newFilename}`;
await writeFile(migrationFileName, JSON.stringify(migrationFileContent, undefined, 2));
console.log(migrationFileName + ' created!');
}

View file

@ -1,23 +1,28 @@
import type { AstroConfig } from 'astro';
import deepDiff from 'deep-diff';
import type { Arguments } from 'yargs-parser';
import { getMigrations, initializeFromMigrations } from '../../migrations.js';
const { diff } = deepDiff;
import { getMigrationStatus, MIGRATION_NEEDED, MIGRATIONS_NOT_INITIALIZED, MIGRATIONS_UP_TO_DATE } from '../../migrations.js';
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
const allMigrationFiles = await getMigrations();
if (allMigrationFiles.length === 0) {
console.log('Project not yet initialized!');
process.exit(1);
export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) {
const status = await getMigrationStatus(config);
const { state } = status;
if (flags.json) {
console.log(JSON.stringify(status));
process.exit(state === 'up-to-date' ? 0 : 1);
}
const prevSnapshot = await initializeFromMigrations(allMigrationFiles);
const calculatedDiff = diff(prevSnapshot, currentSnapshot);
if (calculatedDiff) {
console.log('Changes detected!');
process.exit(1);
switch (state) {
case 'no-migrations-found': {
console.log(MIGRATIONS_NOT_INITIALIZED);
process.exit(1);
}
case 'ahead': {
console.log(MIGRATION_NEEDED);
process.exit(1);
}
case 'up-to-date': {
console.log(MIGRATIONS_UP_TO_DATE);
return;
}
}
console.log('No changes detected.');
return;
}

View file

@ -2,7 +2,75 @@ import deepDiff from 'deep-diff';
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
import type { DBSnapshot } from '../types.js';
import type { AstroConfig } from 'astro';
const { applyChange } = deepDiff;
import { cyan, green, yellow } from 'kleur/colors';
const { applyChange, diff: generateDiff } = deepDiff;
export type MigrationStatus = {
state: 'no-migrations-found'
currentSnapshot: DBSnapshot
} | {
state: 'ahead',
oldSnapshot: DBSnapshot,
newSnapshot: DBSnapshot,
diff: deepDiff.Diff<DBSnapshot, DBSnapshot>[],
newFilename: string,
summary: string,
} | {
state: 'up-to-date',
currentSnapshot: DBSnapshot
}
export async function getMigrationStatus(config: AstroConfig): Promise<MigrationStatus> {
const currentSnapshot = createCurrentSnapshot(config);
const allMigrationFiles = await getMigrations();
if (allMigrationFiles.length === 0) {
return {
state: 'no-migrations-found',
currentSnapshot
}
}
const previousSnapshot = await initializeFromMigrations(allMigrationFiles);
const diff = generateDiff(previousSnapshot, currentSnapshot);
if (diff) {
const n = getNewMigrationNumber(allMigrationFiles);
const newFilename = `${String(n + 1).padStart(4, '0')}_migration.json`;
return {
state: 'ahead',
oldSnapshot: previousSnapshot,
newSnapshot: currentSnapshot,
diff,
newFilename,
summary: generateDiffSummary(diff),
}
}
return {
state: 'up-to-date',
currentSnapshot
}
}
export const MIGRATIONS_CREATED = `${green('■ Migrations initialized!')}\n\n To execute your migrations, run\n ${cyan('astro db push')}`
export const MIGRATIONS_UP_TO_DATE = `${green('■ No migrations needed!')}\n\n Your data is all up to date.\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 MIGRATION_NEEDED = `${yellow('▶ Changes detected!')}\n\n To create the necessary migration file, run\n ${cyan('astro db sync')}\n`
function generateDiffSummary(diff: deepDiff.Diff<DBSnapshot, DBSnapshot>[]) {
// TODO: human readable summary
return JSON.stringify(diff, null, 2);
}
function getNewMigrationNumber(allMigrationFiles: string[]): number {
const len = allMigrationFiles.length - 1;
return allMigrationFiles.reduce((acc, curr) => {
const num = Number.parseInt(curr.split('_')[0] ?? len, 10);
return num > acc ? num : acc;
}, 0);
}
export async function getMigrations(): Promise<string[]> {
const migrationFiles = await readdir('./migrations').catch((err) => {

View file

@ -1,19 +1,32 @@
import { cyan, bold, red } from 'kleur/colors';
import { cyan, bold, red, green, yellow } from 'kleur/colors';
export const MISSING_SESSION_ID_ERROR = `${red(
'⚠️ Login required.'
)} Run ${bold('astro db login')} to authenticate with Astro Studio.`;
'▶ Login required!'
)}
To authenticate with Astro Studio, run
${cyan('astro db login')}\n`;
export const MISSING_PROJECT_ID_ERROR = `${red(
'⚠️ Directory not linked.'
)} Run ${bold('astro db link')} to link this directory to an Astro Studio project.`;
' Directory not linked.'
)}
export const STUDIO_CONFIG_MISSING_WRITABLE_COLLECTIONS_ERROR = (collectionName: string) =>
red(`⚠️ Writable collection ${bold(collectionName)} requires Astro Studio.`) +
` Visit ${cyan('https://astro.build/studio')} to create your account` +
` and then set ${bold('studio: true')} in your astro.config.js file to enable.`;
To link this directory to an Astro Studio project, run
${cyan('astro db link')}\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 then set ${bold('studio: true')} in your astro.config.js file to enable.`;
export const STUDIO_CONFIG_MISSING_WRITABLE_COLLECTIONS_ERROR = (collectionName: string) => `${
red(`▶ Writable collection ${bold(collectionName)} 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 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`