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:
parent
a2d88545a4
commit
0ce327087e
2 changed files with 56 additions and 74 deletions
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue