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:
parent
1f719ffb6d
commit
ed4294abbe
5 changed files with 168 additions and 85 deletions
|
@ -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
|
||||
|
|
|
@ -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!');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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`
|
||||
|
|
Loading…
Add table
Reference in a new issue