mirror of
https://github.com/withastro/astro.git
synced 2025-02-03 22:29:08 -05:00
add primaryKey support
This commit is contained in:
parent
65696de832
commit
7c786276f5
8 changed files with 144 additions and 296 deletions
|
@ -7,6 +7,7 @@ import type { Arguments } from 'yargs-parser';
|
|||
import { appTokenError } from '../../../errors.js';
|
||||
import { collectionToTable, createLocalDatabaseClient } from '../../../internal.js';
|
||||
import {
|
||||
createEmptySnapshot,
|
||||
getMigrations,
|
||||
initializeFromMigrations,
|
||||
loadInitialSnapshot,
|
||||
|
@ -98,8 +99,8 @@ async function pushSchema({
|
|||
// create a migration for the initial snapshot, if needed
|
||||
const initialMigrationBatch = initialSnapshot
|
||||
? await getMigrationQueries({
|
||||
oldCollections: {},
|
||||
newCollections: await loadInitialSnapshot(),
|
||||
oldSnapshot: createEmptySnapshot(),
|
||||
newSnapshot: await loadInitialSnapshot(),
|
||||
})
|
||||
: [];
|
||||
// combine all missing migrations into a single batch
|
||||
|
|
|
@ -3,6 +3,7 @@ import deepDiff from 'deep-diff';
|
|||
import { writeFile } from 'fs/promises';
|
||||
import type { Arguments } from 'yargs-parser';
|
||||
import {
|
||||
createCurrentSnapshot,
|
||||
getMigrations,
|
||||
initializeFromMigrations,
|
||||
initializeMigrationsDirectory,
|
||||
|
@ -11,7 +12,7 @@ import { getMigrationQueries } from '../../queries.js';
|
|||
const { diff } = deepDiff;
|
||||
|
||||
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
|
||||
const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
|
||||
const currentSnapshot = createCurrentSnapshot(config);
|
||||
const allMigrationFiles = await getMigrations();
|
||||
if (allMigrationFiles.length === 0) {
|
||||
await initializeMigrationsDirectory(currentSnapshot);
|
||||
|
@ -27,8 +28,8 @@ export async function cmd({ config }: { config: AstroConfig; flags: Arguments })
|
|||
}
|
||||
|
||||
const migrationQueries = await getMigrationQueries({
|
||||
oldCollections: prevSnapshot,
|
||||
newCollections: currentSnapshot,
|
||||
oldSnapshot: prevSnapshot,
|
||||
newSnapshot: currentSnapshot,
|
||||
});
|
||||
|
||||
const largestNumber = allMigrationFiles.reduce((acc, curr) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { AstroConfig } from 'astro';
|
|||
import deepDiff from 'deep-diff';
|
||||
import type { Arguments } from 'yargs-parser';
|
||||
import { getMigrations, initializeFromMigrations } from '../../../migrations.js';
|
||||
const { diff, applyChange } = deepDiff;
|
||||
const { diff } = deepDiff;
|
||||
|
||||
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
|
||||
const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
|
||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
|||
DBCollections,
|
||||
DBField,
|
||||
DBFields,
|
||||
DBSnapshot,
|
||||
DateField,
|
||||
FieldType,
|
||||
JsonField,
|
||||
|
@ -25,18 +26,17 @@ interface PromptResponses {
|
|||
}
|
||||
|
||||
export async function getMigrationQueries({
|
||||
oldCollections,
|
||||
newCollections,
|
||||
oldSnapshot,
|
||||
newSnapshot,
|
||||
promptResponses,
|
||||
}: {
|
||||
oldCollections: DBCollections;
|
||||
newCollections: DBCollections;
|
||||
oldSnapshot: DBSnapshot;
|
||||
newSnapshot: DBSnapshot;
|
||||
promptResponses?: PromptResponses;
|
||||
}) {
|
||||
}): Promise<string[]> {
|
||||
const queries: string[] = [];
|
||||
let added = getAddedCollections(oldCollections, newCollections);
|
||||
let dropped = getDroppedCollections(oldCollections, newCollections);
|
||||
|
||||
let added = getAddedCollections(oldSnapshot, newSnapshot);
|
||||
let dropped = getDroppedCollections(oldSnapshot, newSnapshot);
|
||||
if (!isEmpty(added) && !isEmpty(dropped)) {
|
||||
const resolved = await resolveCollectionRenames(
|
||||
added,
|
||||
|
@ -62,8 +62,8 @@ export async function getMigrationQueries({
|
|||
queries.push(dropQuery);
|
||||
}
|
||||
|
||||
for (const [collectionName, newCollection] of Object.entries(newCollections)) {
|
||||
const oldCollection = oldCollections[collectionName];
|
||||
for (const [collectionName, newCollection] of Object.entries(newSnapshot.schema)) {
|
||||
const oldCollection = oldSnapshot.schema[collectionName];
|
||||
if (!oldCollection) continue;
|
||||
const collectionChangeQueries = await getCollectionChangeQueries({
|
||||
collectionName,
|
||||
|
@ -130,12 +130,9 @@ export async function getCollectionChangeQueries({
|
|||
)} field ${color.blue(color.bold(fieldName))}. ${color.red(
|
||||
'This will delete all existing data in the collection!'
|
||||
)} We recommend setting a default value to avoid data loss.`,
|
||||
'updated-required': `Changing ${color.blue(color.bold(collectionName))} field ${color.blue(
|
||||
'added-unique': `Adding unique ${color.blue(color.bold(collectionName))} field ${color.blue(
|
||||
color.bold(fieldName)
|
||||
)} to required. ${color.red('This will delete all existing data in the collection!')}`,
|
||||
'updated-unique': `Changing ${color.blue(color.bold(collectionName))} field ${color.blue(
|
||||
color.bold(fieldName)
|
||||
)} to unique. ${color.red('This will delete all existing data in the collection!')}`,
|
||||
)}. ${color.red('This will delete all existing data in the collection!')}`,
|
||||
'updated-type': `Changing the type of ${color.blue(
|
||||
color.bold(collectionName)
|
||||
)} field ${color.blue(color.bold(fieldName))}. ${color.red(
|
||||
|
@ -159,11 +156,22 @@ export async function getCollectionChangeQueries({
|
|||
}
|
||||
}
|
||||
|
||||
const addedPrimaryKey = Object.entries(added).find(
|
||||
([, field]) => hasPrimaryKey(field)
|
||||
);
|
||||
const droppedPrimaryKey = Object.entries(dropped).find(
|
||||
([, field]) => hasPrimaryKey(field)
|
||||
);
|
||||
const updatedPrimaryKey = Object.entries(updated).find(
|
||||
([, field]) =>
|
||||
(hasPrimaryKey(field.old) || hasPrimaryKey(field.new))
|
||||
);
|
||||
const recreateTableQueries = getRecreateTableQueries({
|
||||
unescCollectionName: collectionName,
|
||||
unescapedCollectionName: collectionName,
|
||||
newCollection,
|
||||
added,
|
||||
hasDataLoss: dataLossCheck.dataLoss,
|
||||
migrateHiddenPrimaryKey: !addedPrimaryKey && !droppedPrimaryKey && !updatedPrimaryKey,
|
||||
});
|
||||
queries.push(...recreateTableQueries);
|
||||
return queries;
|
||||
|
@ -288,25 +296,31 @@ async function resolveCollectionRenames(
|
|||
return { added, dropped, renamed };
|
||||
}
|
||||
|
||||
function getAddedCollections(oldCollections: DBCollections, newCollections: DBCollections) {
|
||||
function getAddedCollections(
|
||||
oldCollections: DBSnapshot,
|
||||
newCollections: DBSnapshot
|
||||
): DBCollections {
|
||||
const added: DBCollections = {};
|
||||
for (const [key, newCollection] of Object.entries(newCollections)) {
|
||||
if (!(key in oldCollections)) added[key] = newCollection;
|
||||
for (const [key, newCollection] of Object.entries(newCollections.schema)) {
|
||||
if (!(key in oldCollections.schema)) added[key] = newCollection;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
function getDroppedCollections(oldCollections: DBCollections, newCollections: DBCollections) {
|
||||
function getDroppedCollections(
|
||||
oldCollections: DBSnapshot,
|
||||
newCollections: DBSnapshot
|
||||
): DBCollections {
|
||||
const dropped: DBCollections = {};
|
||||
for (const [key, oldCollection] of Object.entries(oldCollections)) {
|
||||
if (!(key in newCollections)) dropped[key] = oldCollection;
|
||||
for (const [key, oldCollection] of Object.entries(oldCollections.schema)) {
|
||||
if (!(key in newCollections.schema)) dropped[key] = oldCollection;
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
function getFieldRenameQueries(unescCollectionName: string, renamed: Renamed): string[] {
|
||||
function getFieldRenameQueries(unescapedCollectionName: string, renamed: Renamed): string[] {
|
||||
const queries: string[] = [];
|
||||
const collectionName = sqlite.escapeName(unescCollectionName);
|
||||
const collectionName = sqlite.escapeName(unescapedCollectionName);
|
||||
|
||||
for (const { from, to } of renamed) {
|
||||
const q = `ALTER TABLE ${collectionName} RENAME COLUMN ${sqlite.escapeName(
|
||||
|
@ -323,12 +337,12 @@ function getFieldRenameQueries(unescCollectionName: string, renamed: Renamed): s
|
|||
* `canUseAlterTableAddColumn` and `canAlterTableDropColumn` checks!
|
||||
*/
|
||||
function getAlterTableQueries(
|
||||
unescCollectionName: string,
|
||||
unescapedCollectionName: string,
|
||||
added: DBFields,
|
||||
dropped: DBFields
|
||||
): string[] {
|
||||
const queries: string[] = [];
|
||||
const collectionName = sqlite.escapeName(unescCollectionName);
|
||||
const collectionName = sqlite.escapeName(unescapedCollectionName);
|
||||
|
||||
for (const [unescFieldName, field] of Object.entries(added)) {
|
||||
const fieldName = sqlite.escapeName(unescFieldName);
|
||||
|
@ -350,40 +364,52 @@ function getAlterTableQueries(
|
|||
}
|
||||
|
||||
function getRecreateTableQueries({
|
||||
unescCollectionName,
|
||||
unescapedCollectionName,
|
||||
newCollection,
|
||||
added,
|
||||
hasDataLoss,
|
||||
migrateHiddenPrimaryKey,
|
||||
}: {
|
||||
unescCollectionName: string;
|
||||
unescapedCollectionName: string;
|
||||
newCollection: DBCollection;
|
||||
added: Record<string, DBField>;
|
||||
hasDataLoss: boolean;
|
||||
migrateHiddenPrimaryKey: boolean;
|
||||
}): string[] {
|
||||
const unescTempName = `${unescCollectionName}_${genTempTableName()}`;
|
||||
const unescTempName = `${unescapedCollectionName}_${genTempTableName()}`;
|
||||
const tempName = sqlite.escapeName(unescTempName);
|
||||
const collectionName = sqlite.escapeName(unescCollectionName);
|
||||
const queries = [getCreateTableQuery(unescTempName, newCollection)];
|
||||
const collectionName = sqlite.escapeName(unescapedCollectionName);
|
||||
|
||||
if (!hasDataLoss) {
|
||||
const newColumns = ['id', ...Object.keys(newCollection.fields)];
|
||||
const originalColumns = newColumns.filter((i) => !(i in added));
|
||||
const escapedColumns = originalColumns.map((c) => sqlite.escapeName(c)).join(', ');
|
||||
|
||||
queries.push(
|
||||
`INSERT INTO ${tempName} (${escapedColumns}) SELECT ${escapedColumns} FROM ${collectionName}`
|
||||
);
|
||||
if (hasDataLoss) {
|
||||
return [`DROP TABLE ${collectionName}`, getCreateTableQuery(collectionName, newCollection)];
|
||||
}
|
||||
const newColumns = [...Object.keys(newCollection.fields)];
|
||||
if (migrateHiddenPrimaryKey) {
|
||||
newColumns.unshift('_id');
|
||||
}
|
||||
const escapedColumns = newColumns
|
||||
.filter((i) => !(i in added))
|
||||
.map((c) => sqlite.escapeName(c))
|
||||
.join(', ');
|
||||
|
||||
queries.push(`DROP TABLE ${collectionName}`);
|
||||
queries.push(`ALTER TABLE ${tempName} RENAME TO ${collectionName}`);
|
||||
return queries;
|
||||
return [
|
||||
getCreateTableQuery(unescTempName, newCollection),
|
||||
`INSERT INTO ${tempName} (${escapedColumns}) SELECT ${escapedColumns} FROM ${collectionName}`,
|
||||
`DROP TABLE ${collectionName}`,
|
||||
`ALTER TABLE ${tempName} RENAME TO ${collectionName}`,
|
||||
];
|
||||
}
|
||||
|
||||
export function getCreateTableQuery(collectionName: string, collection: DBCollection) {
|
||||
let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
|
||||
|
||||
const colQueries = ['"id" text PRIMARY KEY'];
|
||||
const colQueries = [];
|
||||
const colHasPrimaryKey = Object.entries(collection.fields).find(
|
||||
([, field]) => hasPrimaryKey(field)
|
||||
);
|
||||
if (!colHasPrimaryKey) {
|
||||
colQueries.push('_id INTEGER PRIMARY KEY AUTOINCREMENT');
|
||||
}
|
||||
for (const [columnName, column] of Object.entries(collection.fields)) {
|
||||
const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
|
||||
column.type
|
||||
|
@ -395,16 +421,19 @@ export function getCreateTableQuery(collectionName: string, collection: DBCollec
|
|||
return query;
|
||||
}
|
||||
|
||||
function getModifiers(columnName: string, column: DBField) {
|
||||
function getModifiers(fieldName: string, field: DBField) {
|
||||
let modifiers = '';
|
||||
if (!column.optional) {
|
||||
if (hasPrimaryKey(field)) {
|
||||
modifiers += ' PRIMARY KEY';
|
||||
}
|
||||
if (!field.optional) {
|
||||
modifiers += ' NOT NULL';
|
||||
}
|
||||
if (column.unique) {
|
||||
if (field.unique) {
|
||||
modifiers += ' UNIQUE';
|
||||
}
|
||||
if (hasDefault(column)) {
|
||||
modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`;
|
||||
if (hasDefault(field)) {
|
||||
modifiers += ` DEFAULT ${getDefaultValueSql(fieldName, field)}`;
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
@ -431,19 +460,24 @@ function isEmpty(obj: Record<string, unknown>) {
|
|||
*
|
||||
* @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column
|
||||
*/
|
||||
function canAlterTableAddColumn(column: DBField) {
|
||||
if (column.unique) return false;
|
||||
if (hasRuntimeDefault(column)) return false;
|
||||
if (!column.optional && !hasDefault(column)) return false;
|
||||
function canAlterTableAddColumn(field: DBField) {
|
||||
if (field.unique) return false;
|
||||
if (hasRuntimeDefault(field)) return false;
|
||||
if (!field.optional && !hasDefault(field)) return false;
|
||||
if (hasPrimaryKey(field)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function canAlterTableDropColumn(column: DBField) {
|
||||
if (column.unique) return false;
|
||||
function canAlterTableDropColumn(field: DBField) {
|
||||
if (field.unique) return false;
|
||||
if (hasPrimaryKey(field)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
type DataLossReason = 'added-required' | 'updated-required' | 'updated-unique' | 'updated-type';
|
||||
type DataLossReason =
|
||||
| 'added-required'
|
||||
| 'added-unique'
|
||||
| 'updated-type';
|
||||
type DataLossResponse =
|
||||
| { dataLoss: false }
|
||||
| { dataLoss: true; fieldName: string; reason: DataLossReason };
|
||||
|
@ -453,19 +487,20 @@ function canRecreateTableWithoutDataLoss(
|
|||
updated: UpdatedFields
|
||||
): DataLossResponse {
|
||||
for (const [fieldName, a] of Object.entries(added)) {
|
||||
if (!a.optional && !hasDefault(a))
|
||||
if (hasPrimaryKey(a) && a.type !== 'number') {
|
||||
return { dataLoss: true, fieldName, reason: 'added-required' };
|
||||
}
|
||||
if (!a.optional && !hasDefault(a)) {
|
||||
return { dataLoss: true, fieldName, reason: 'added-required' };
|
||||
}
|
||||
if (a.unique) {
|
||||
return { dataLoss: true, fieldName, reason: 'added-unique' };
|
||||
}
|
||||
}
|
||||
for (const [fieldName, u] of Object.entries(updated)) {
|
||||
// Cannot respect default value when changing to NOT NULL
|
||||
// TODO: use UPDATE query to set default where IS NULL
|
||||
// @see https://dataschool.com/learn-sql/how-to-replace-nulls-with-0s/
|
||||
if (u.old.optional && !u.new.optional)
|
||||
return { dataLoss: true, fieldName, reason: 'updated-required' };
|
||||
if (!u.old.unique && u.new.unique)
|
||||
return { dataLoss: true, fieldName, reason: 'updated-unique' };
|
||||
if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new))
|
||||
if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new)) {
|
||||
return { dataLoss: true, fieldName, reason: 'updated-type' };
|
||||
}
|
||||
}
|
||||
return { dataLoss: false };
|
||||
}
|
||||
|
@ -535,9 +570,19 @@ type DBFieldWithDefault =
|
|||
| WithDefaultDefined<BooleanField>
|
||||
| WithDefaultDefined<JsonField>;
|
||||
|
||||
function hasPrimaryKey(field: DBField) {
|
||||
return 'primaryKey' in field && !!field.primaryKey;
|
||||
}
|
||||
|
||||
// Type narrowing the default fails on union types, so use a type guard
|
||||
function hasDefault(field: DBField): field is DBFieldWithDefault {
|
||||
return field.default !== undefined;
|
||||
if (field.default !== undefined) {
|
||||
return true;
|
||||
}
|
||||
if (hasPrimaryKey(field) && field.type === 'number') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault {
|
||||
|
@ -549,7 +594,7 @@ function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): str
|
|||
case 'boolean':
|
||||
return column.default ? 'TRUE' : 'FALSE';
|
||||
case 'number':
|
||||
return `${column.default}`;
|
||||
return `${column.default || 'AUTOINCREMENT'}`;
|
||||
case 'text':
|
||||
return sqlite.escapeString(column.default);
|
||||
case 'date':
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
// import type { DBCollections } from '../../types.js';
|
||||
// import yargs from 'yargs-parser';
|
||||
// import { appTokenError } from '../../errors.js';
|
||||
// import {
|
||||
// getAstroStudioEnv,
|
||||
// getStudioUrl,
|
||||
// getRemoteDatabaseUrl,
|
||||
// isAppTokenValid,
|
||||
// } from '../../utils.js';
|
||||
// import { migrate } from './migrate.js';
|
||||
// import { collectionToTable, createLocalDatabaseClient } from '../../internal.js';
|
||||
// import type { Query } from 'drizzle-orm';
|
||||
// import type { InArgs, InStatement } from '@libsql/client';
|
||||
|
||||
// export async function sync({ collections }: { collections: DBCollections }) {
|
||||
// const args = yargs(process.argv.slice(3), {
|
||||
// string: ['dry-run', 'seed'],
|
||||
// });
|
||||
// const isDryRun = 'dry-run' in args;
|
||||
// const shouldSeed = 'seed' in args;
|
||||
// const remoteDbUrl = getRemoteDatabaseUrl();
|
||||
|
||||
// const appToken = getAstroStudioEnv().ASTRO_STUDIO_APP_TOKEN;
|
||||
// if (!appToken || !(await isAppTokenValid({ remoteDbUrl, appToken }))) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error(appTokenError);
|
||||
// process.exit(1);
|
||||
// }
|
||||
|
||||
// try {
|
||||
// await setSyncStatus({ status: 'RUNNING', remoteDbUrl, appToken });
|
||||
// await migrate({ collections, isDryRun, appToken });
|
||||
// await setSyncStatus({ status: 'SUCCESS', remoteDbUrl, appToken });
|
||||
// if (shouldSeed) {
|
||||
// await tempDataPush({ collections, appToken, isDryRun });
|
||||
// }
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.info('Sync complete 🔄');
|
||||
// } catch (e) {
|
||||
// await setSyncStatus({ status: 'FAILED', remoteDbUrl, appToken });
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error(e);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /** TODO: refine with migration changes */
|
||||
// async function tempDataPush({
|
||||
// collections,
|
||||
// appToken,
|
||||
// isDryRun,
|
||||
// }: {
|
||||
// collections: DBCollections;
|
||||
// appToken: string;
|
||||
// isDryRun?: boolean;
|
||||
// }) {
|
||||
// const db = await createLocalDatabaseClient({ collections, dbUrl: ':memory:', seeding: true });
|
||||
// const queries: Query[] = [];
|
||||
|
||||
// for (const [name, collection] of Object.entries(collections)) {
|
||||
// if (collection.writable || !collection.data) continue;
|
||||
// const table = collectionToTable(name, collection);
|
||||
// const insert = db.insert(table).values(await collection.data());
|
||||
|
||||
// queries.push(insert.toSQL());
|
||||
// }
|
||||
// const url = new URL('/db/query', getRemoteDatabaseUrl());
|
||||
// const requestBody: InStatement[] = queries.map((q) => ({
|
||||
// sql: q.sql,
|
||||
// args: q.params as InArgs,
|
||||
// }));
|
||||
|
||||
// if (isDryRun) {
|
||||
// console.info('[DRY RUN] Batch data seed:', JSON.stringify(requestBody, null, 2));
|
||||
// return new Response(null, { status: 200 });
|
||||
// }
|
||||
|
||||
// return await fetch(url, {
|
||||
// method: 'POST',
|
||||
// headers: new Headers({
|
||||
// Authorization: `Bearer ${appToken}`,
|
||||
// }),
|
||||
// body: JSON.stringify(requestBody),
|
||||
// });
|
||||
// }
|
||||
|
||||
// async function setSyncStatus({
|
||||
// remoteDbUrl,
|
||||
// appToken,
|
||||
// status,
|
||||
// }: {
|
||||
// remoteDbUrl: string;
|
||||
// appToken: string;
|
||||
// status: 'RUNNING' | 'FAILED' | 'SUCCESS';
|
||||
// }) {
|
||||
// const syncStatusUrl = new URL('/api/rest/sync-status', getStudioUrl());
|
||||
// syncStatusUrl.searchParams.set('workerUrl', remoteDbUrl);
|
||||
// syncStatusUrl.searchParams.set('status', status);
|
||||
|
||||
// const response = await fetch(syncStatusUrl, {
|
||||
// method: 'PATCH',
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${appToken}`,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!response.ok) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error('Unexpected problem completing sync.');
|
||||
// process.exit(1);
|
||||
// }
|
||||
// }
|
|
@ -1,105 +0,0 @@
|
|||
// import type { InArgs, InStatement } from '@libsql/client';
|
||||
// import {
|
||||
// STUDIO_ADMIN_TABLE_ROW_ID,
|
||||
// adminTable,
|
||||
// } from './admin.js';
|
||||
// import { type SQL, eq, sql } from 'drizzle-orm';
|
||||
// import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
|
||||
// import { red } from 'kleur/colors';
|
||||
// import { collectionsSchema, type DBCollections } from '../../types.js';
|
||||
// import { getRemoteDatabaseUrl } from '../../utils.js';
|
||||
// import { authenticationError, unexpectedAstroAdminError } from '../../errors.js';
|
||||
// import { createLocalDatabaseClient } from './remote-db.js';
|
||||
// import { getMigrationQueries } from './queries.js';
|
||||
|
||||
// const sqliteDialect = new SQLiteAsyncDialect();
|
||||
|
||||
// export async function migrate({
|
||||
// collections: newCollections,
|
||||
// appToken,
|
||||
// isDryRun,
|
||||
// }: {
|
||||
// collections: DBCollections;
|
||||
// appToken: string;
|
||||
// isDryRun?: boolean;
|
||||
// }) {
|
||||
// const db = createLocalDatabaseClient(appToken);
|
||||
// const adminEntry = await db
|
||||
// .select({ collections: adminTable.collections })
|
||||
// .from(adminTable)
|
||||
// .get();
|
||||
|
||||
// if (!adminEntry) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error(
|
||||
// `${red(
|
||||
// '⚠️ Unexpected error syncing collections.',
|
||||
// )} You may need to initialize a new project with \`studio init\`.`,
|
||||
// );
|
||||
// process.exit(1);
|
||||
// }
|
||||
|
||||
// if (JSON.stringify(newCollections) === adminEntry.collections) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.info('Collections already up to date');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const oldCollections = collectionsSchema.parse(
|
||||
// adminEntry.collections ? JSON.parse(adminEntry.collections) : {},
|
||||
// );
|
||||
// const queries: SQL[] = [];
|
||||
// const migrationQueries = await getMigrationQueries({ oldCollections, newCollections });
|
||||
// queries.push(...migrationQueries.map((q) => sql.raw(q)));
|
||||
|
||||
// const updateCollectionsJson = db
|
||||
// .update(adminTable)
|
||||
// .set({ collections: JSON.stringify(newCollections) })
|
||||
// .where(eq(adminTable.id, STUDIO_ADMIN_TABLE_ROW_ID))
|
||||
// .getSQL();
|
||||
// queries.push(updateCollectionsJson);
|
||||
|
||||
// const res = await runBatchQuery({ queries, appToken, isDryRun });
|
||||
// if (!res.ok) {
|
||||
// if (res.status === 401) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error(authenticationError);
|
||||
// process.exit(1);
|
||||
// }
|
||||
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error(unexpectedAstroAdminError);
|
||||
// process.exit(1);
|
||||
// }
|
||||
// }
|
||||
|
||||
// async function runBatchQuery({
|
||||
// queries: sqlQueries,
|
||||
// appToken,
|
||||
// isDryRun,
|
||||
// }: {
|
||||
// queries: SQL[];
|
||||
// appToken: string;
|
||||
// isDryRun?: boolean;
|
||||
// }) {
|
||||
// const queries = sqlQueries.map((q) => sqliteDialect.sqlToQuery(q));
|
||||
// const requestBody: InStatement[] = queries.map((q) => ({
|
||||
// sql: q.sql,
|
||||
// args: q.params as InArgs,
|
||||
// }));
|
||||
|
||||
// if (isDryRun) {
|
||||
// console.info('[DRY RUN] Batch query:', JSON.stringify(requestBody, null, 2));
|
||||
// return new Response(null, { status: 200 });
|
||||
// }
|
||||
|
||||
// const url = new URL('/db/query', getRemoteDatabaseUrl());
|
||||
|
||||
// return await fetch(url, {
|
||||
// method: 'POST',
|
||||
// headers: new Headers({
|
||||
// Authorization: `Bearer ${appToken}`,
|
||||
// }),
|
||||
// body: JSON.stringify(requestBody),
|
||||
// });
|
||||
// }
|
|
@ -1,6 +1,7 @@
|
|||
import deepDiff from 'deep-diff';
|
||||
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
|
||||
import type { DBCollections } from './types.js';
|
||||
import type { DBSnapshot } from './types.js';
|
||||
import type { AstroConfig } from 'astro';
|
||||
const { applyChange } = deepDiff;
|
||||
|
||||
export async function getMigrations(): Promise<string[]> {
|
||||
|
@ -17,18 +18,20 @@ export async function loadMigration(migration: string): Promise<{ diff: any[]; d
|
|||
return JSON.parse(await readFile(`./migrations/${migration}`, 'utf-8'));
|
||||
}
|
||||
|
||||
export async function loadInitialSnapshot(): Promise<DBCollections> {
|
||||
return JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8'));
|
||||
export async function loadInitialSnapshot(): Promise<DBSnapshot> {
|
||||
const snapshot = JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8'));
|
||||
if (!snapshot.version) {
|
||||
return { version: 2, meta: {}, schema: snapshot };
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export async function initializeMigrationsDirectory(currentSnapshot: unknown) {
|
||||
export async function initializeMigrationsDirectory(currentSnapshot: DBSnapshot) {
|
||||
await mkdir('./migrations', { recursive: true });
|
||||
await writeFile('./migrations/0000_snapshot.json', JSON.stringify(currentSnapshot, undefined, 2));
|
||||
}
|
||||
|
||||
export async function initializeFromMigrations(
|
||||
allMigrationFiles: string[]
|
||||
): Promise<DBCollections> {
|
||||
export async function initializeFromMigrations(allMigrationFiles: string[]): Promise<DBSnapshot> {
|
||||
const prevSnapshot = await loadInitialSnapshot();
|
||||
for (const migration of allMigrationFiles) {
|
||||
if (migration === '0000_snapshot.json') continue;
|
||||
|
@ -39,3 +42,11 @@ export async function initializeFromMigrations(
|
|||
}
|
||||
return prevSnapshot;
|
||||
}
|
||||
|
||||
export function createCurrentSnapshot(config: AstroConfig): DBSnapshot {
|
||||
const schema = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
|
||||
return { version: 2, meta: {}, schema };
|
||||
}
|
||||
export function createEmptySnapshot(): DBSnapshot {
|
||||
return { version: 2, meta: {}, schema: {} };
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@ const booleanFieldSchema = baseFieldSchema.extend({
|
|||
const numberFieldSchema = baseFieldSchema.extend({
|
||||
type: z.literal('number'),
|
||||
default: z.number().optional(),
|
||||
primaryKey: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const textFieldSchema = baseFieldSchema.extend({
|
||||
type: z.literal('text'),
|
||||
multiline: z.boolean().optional(),
|
||||
default: z.string().optional(),
|
||||
primaryKey: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const dateFieldSchema = baseFieldSchema.extend({
|
||||
|
@ -95,6 +97,11 @@ export type DBCollection = z.infer<
|
|||
typeof readableCollectionSchema | typeof writableCollectionSchema
|
||||
>;
|
||||
export type DBCollections = Record<string, DBCollection>;
|
||||
export type DBSnapshot = {
|
||||
version: number;
|
||||
meta: Record<string, never>;
|
||||
schema: Record<string, DBCollection>;
|
||||
};
|
||||
export type ReadableDBCollection = z.infer<typeof readableCollectionSchema>;
|
||||
export type WritableDBCollection = z.infer<typeof writableCollectionSchema>;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue