0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

move migration logic to turso, add back sync support

This commit is contained in:
Fred K. Schott 2024-01-29 22:43:21 -08:00
parent a2d88545a4
commit 0ce327087e
2 changed files with 56 additions and 74 deletions

View file

@ -1,11 +1,12 @@
import { createClient, type InArgs, type InStatement } from '@libsql/client'; /* eslint-disable no-console */
import { createClient, type InStatement } from '@libsql/client';
import type { AstroConfig } from 'astro'; import type { AstroConfig } from 'astro';
import deepDiff from 'deep-diff'; import deepDiff from 'deep-diff';
import { type SQL, eq, sql } from 'drizzle-orm';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import type { Arguments } from 'yargs-parser';
import { appTokenError } from '../../../errors.js';
import { drizzle } from 'drizzle-orm/sqlite-proxy'; import { drizzle } from 'drizzle-orm/sqlite-proxy';
import type { Arguments } from 'yargs-parser';
import type { AstroConfigWithDB } from '../../../config.js';
import { appTokenError } from '../../../errors.js';
import { setupDbTables } from '../../../internal.js';
import { import {
createCurrentSnapshot, createCurrentSnapshot,
createEmptySnapshot, createEmptySnapshot,
@ -15,20 +16,10 @@ import {
loadMigration, loadMigration,
} from '../../../migrations.js'; } from '../../../migrations.js';
import type { DBSnapshot } from '../../../types.js'; import type { DBSnapshot } from '../../../types.js';
import { import { getAstroStudioEnv, getRemoteDatabaseUrl } from '../../../utils.js';
STUDIO_ADMIN_TABLE_ROW_ID,
adminTable,
createRemoteDatabaseClient,
getAstroStudioEnv,
getRemoteDatabaseUrl,
migrationsTable,
} from '../../../utils.js';
import { getMigrationQueries } from '../../queries.js'; import { getMigrationQueries } from '../../queries.js';
import type { AstroConfigWithDB } from '../../../config.js';
import { setupDbTables } from '../../../internal.js';
const { diff } = deepDiff; const { diff } = deepDiff;
const sqliteDialect = new SQLiteAsyncDialect();
export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) { export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) {
const isSeedData = flags.seed; const isSeedData = flags.seed;
@ -48,33 +39,27 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
console.log(calculatedDiff); console.log(calculatedDiff);
process.exit(1); process.exit(1);
} }
if (!appToken) { if (!appToken) {
// eslint-disable-next-line no-console
console.error(appTokenError); console.error(appTokenError);
process.exit(1); process.exit(1);
} }
const db = createRemoteDatabaseClient(appToken);
// Temporary: create the migration table just in case it doesn't exist
await db.run(
sql`CREATE TABLE IF NOT EXISTS ReservedAstroStudioMigrations ( name TEXT PRIMARY KEY )`
);
// get all migrations from the DB
const allRemoteMigrations = await db.select().from(migrationsTable);
// get all migrations from the filesystem // get all migrations from the filesystem
const allLocalMigrations = await getMigrations(); const allLocalMigrations = await getMigrations();
// filter to find all migrations that are in FS but not DB const { data: missingMigrations } = await prepareMigrateQuery({
const missingMigrations = allLocalMigrations.filter((migration) => { migrations: allLocalMigrations,
return !allRemoteMigrations.find((m) => m.name === migration); appToken,
}); });
// exit early if there are no migrations to push
if (missingMigrations.length === 0) { if (missingMigrations.length === 0) {
console.info('No migrations to push! Your database is up to date!'); console.info('No migrations to push! Your database is up to date!');
} else { process.exit(0);
console.log(`Pushing ${missingMigrations.length} migrations...`);
await pushSchema({ migrations: missingMigrations, appToken, isDryRun, db, currentSnapshot });
} }
// push the database schema
if (missingMigrations.length > 0) {
console.log(`Pushing ${missingMigrations.length} migrations...`);
await pushSchema({ migrations: missingMigrations, appToken, isDryRun, currentSnapshot });
}
// push the database seed data
if (isSeedData) { if (isSeedData) {
console.info('Pushing data...'); console.info('Pushing data...');
await pushData({ config, appToken, isDryRun }); await pushData({ config, appToken, isDryRun });
@ -86,13 +71,11 @@ async function pushSchema({
migrations, migrations,
appToken, appToken,
isDryRun, isDryRun,
db,
currentSnapshot, currentSnapshot,
}: { }: {
migrations: string[]; migrations: string[];
appToken: string; appToken: string;
isDryRun: boolean; isDryRun: boolean;
db: ReturnType<typeof createRemoteDatabaseClient>;
currentSnapshot: DBSnapshot; currentSnapshot: DBSnapshot;
}) { }) {
// load all missing migrations // load all missing migrations
@ -107,19 +90,11 @@ async function pushSchema({
}) })
: []; : [];
// combine all missing migrations into a single batch // combine all missing migrations into a single batch
const missingMigrationBatch = missingMigrationContents.reduce((acc, curr) => { const queries = missingMigrationContents.reduce((acc, curr) => {
return [...acc, ...curr.db]; return [...acc, ...curr.db];
}, initialMigrationBatch); }, initialMigrationBatch);
// apply the batch to the DB // apply the batch to the DB
const queries: SQL[] = missingMigrationBatch.map((q) => sql.raw(q)); await runMigrateQuery({ queries, migrations, snapshot: currentSnapshot, appToken, isDryRun });
await runBatchQuery({ queries, appToken, isDryRun });
// Update the migrations table to add all the newly run migrations
await db.insert(migrationsTable).values(migrations.map((m) => ({ name: m })));
// update the config schema in the admin table
await db
.update(adminTable)
.set({ collections: JSON.stringify(currentSnapshot) })
.where(eq(adminTable.id, STUDIO_ADMIN_TABLE_ROW_ID));
} }
async function pushData({ async function pushData({
@ -176,27 +151,32 @@ async function pushData({
}); });
} }
async function runBatchQuery({ async function runMigrateQuery({
queries: sqlQueries, queries,
migrations,
snapshot,
appToken, appToken,
isDryRun, isDryRun,
}: { }: {
queries: SQL[]; queries: string[];
migrations: string[];
snapshot: DBSnapshot;
appToken: string; appToken: string;
isDryRun?: boolean; isDryRun?: boolean;
}) { }) {
const queries = sqlQueries.map((q) => sqliteDialect.sqlToQuery(q)); const requestBody = {
const requestBody: InStatement[] = queries.map((q) => ({ snapshot,
sql: q.sql, migrations,
args: q.params as InArgs, sql: queries,
})); experimentalVersion: 1,
};
if (isDryRun) { if (isDryRun) {
console.info('[DRY RUN] Batch query:', JSON.stringify(requestBody, null, 2)); console.info('[DRY RUN] Batch query:', JSON.stringify(requestBody, null, 2));
return new Response(null, { status: 200 }); return new Response(null, { status: 200 });
} }
const url = new URL('/db/query', getRemoteDatabaseUrl()); const url = new URL('/db/migrate/run', getRemoteDatabaseUrl());
return await fetch(url, { return await fetch(url, {
method: 'POST', method: 'POST',
@ -206,3 +186,25 @@ async function runBatchQuery({
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
}); });
} }
async function prepareMigrateQuery({
migrations,
appToken,
}: {
migrations: string[];
appToken: string;
}) {
const url = new URL('/db/migrate/prepare', getRemoteDatabaseUrl());
const requestBody = {
migrations,
experimentalVersion: 1,
};
const result = await fetch(url, {
method: 'POST',
headers: new Headers({
Authorization: `Bearer ${appToken}`,
}),
body: JSON.stringify(requestBody),
});
return await result.json();
}

View file

@ -1,34 +1,14 @@
import type { AstroConfig } from 'astro'; import type { AstroConfig } from 'astro';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { loadEnv } from 'vite'; import { loadEnv } from 'vite';
import { createRemoteDatabaseClient as runtimeCreateRemoteDatabaseClient } from './utils-runtime.js'; import { createRemoteDatabaseClient as runtimeCreateRemoteDatabaseClient } from './utils-runtime.js';
export type VitePlugin = Required<AstroConfig['vite']>['plugins'][number]; export type VitePlugin = Required<AstroConfig['vite']>['plugins'][number];
export const STUDIO_ADMIN_TABLE = 'ReservedAstroStudioAdmin';
export const STUDIO_ADMIN_TABLE_ROW_ID = 'admin';
export const adminTable = sqliteTable(STUDIO_ADMIN_TABLE, {
id: text('id').primaryKey(),
collections: text('collections').notNull(),
});
export const STUDIO_MIGRATIONS_TABLE = 'ReservedAstroStudioMigrations';
export const migrationsTable = sqliteTable(STUDIO_MIGRATIONS_TABLE, {
name: text('name').primaryKey(),
});
export function getAstroStudioEnv(envMode = ''): Record<`ASTRO_STUDIO_${string}`, string> { export function getAstroStudioEnv(envMode = ''): Record<`ASTRO_STUDIO_${string}`, string> {
const env = loadEnv(envMode, process.cwd(), 'ASTRO_STUDIO_'); const env = loadEnv(envMode, process.cwd(), 'ASTRO_STUDIO_');
return env; return env;
} }
export function getStudioUrl(): string {
const env = getAstroStudioEnv();
return env.ASTRO_STUDIO_BASE_URL;
}
export function getRemoteDatabaseUrl(): string { export function getRemoteDatabaseUrl(): string {
const env = getAstroStudioEnv(); const env = getAstroStudioEnv();
return env.ASTRO_STUDIO_REMOTE_DB_URL; return env.ASTRO_STUDIO_REMOTE_DB_URL;