0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-10 23:01:26 -05:00

fix: add back promptResponse injection

This commit is contained in:
bholmesdev 2024-02-06 13:48:41 -05:00
parent 76721dd193
commit db874e508e
2 changed files with 128 additions and 113 deletions

View file

@ -29,19 +29,29 @@ import { hasPrimaryKey } from '../../runtime/index.js';
const sqlite = new SQLiteAsyncDialect();
const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
/** Dependency injected for unit testing */
type AmbiguityResponses = {
collectionRenames: Record<string, string>;
fieldRenames: {
[collectionName: string]: Record<string, string>;
};
};
export async function getMigrationQueries({
oldSnapshot,
newSnapshot,
ambiguityResponses,
}: {
oldSnapshot: DBSnapshot;
newSnapshot: DBSnapshot;
ambiguityResponses?: AmbiguityResponses;
}): Promise<{ queries: string[]; confirmations: string[] }> {
const queries: string[] = [];
const confirmations: string[] = [];
let added = getAddedCollections(oldSnapshot, newSnapshot);
let dropped = getDroppedCollections(oldSnapshot, newSnapshot);
if (!isEmpty(added) && !isEmpty(dropped)) {
const resolved = await resolveCollectionRenames(added, dropped);
const resolved = await resolveCollectionRenames(added, dropped, ambiguityResponses);
added = resolved.added;
dropped = resolved.dropped;
for (const { from, to } of resolved.renamed) {
@ -80,10 +90,12 @@ export async function getCollectionChangeQueries({
collectionName,
oldCollection,
newCollection,
ambiguityResponses,
}: {
collectionName: string;
oldCollection: DBCollection;
newCollection: DBCollection;
ambiguityResponses?: AmbiguityResponses;
}): Promise<{ queries: string[]; confirmations: string[] }> {
const queries: string[] = [];
const confirmations: string[] = [];
@ -102,7 +114,7 @@ export async function getCollectionChangeQueries({
};
}
if (!isEmpty(added) && !isEmpty(dropped)) {
const resolved = await resolveFieldRenames(collectionName, added, dropped);
const resolved = await resolveFieldRenames(collectionName, added, dropped, ambiguityResponses);
added = resolved.added;
dropped = resolved.dropped;
queries.push(...getFieldRenameQueries(collectionName, resolved.renamed));
@ -127,9 +139,21 @@ export async function getCollectionChangeQueries({
if (dataLossCheck.dataLoss) {
const { reason, fieldName } = dataLossCheck;
const reasonMsgs: Record<DataLossReason, string> = {
'added-required': `New field ${color.bold(collectionName + '.' + fieldName)} is required with no default value.\nThis requires deleting existing data in the ${color.bold(collectionName)} collection.`,
'added-unique': `New field ${color.bold(collectionName + '.' + fieldName)} is marked as unique.\nThis requires deleting existing data in the ${color.bold(collectionName)} collection.`,
'updated-type': `Updated field ${color.bold(collectionName + '.' + fieldName)} cannot convert data to new field data type.\nThis requires deleting existing data in the ${color.bold(collectionName)} collection.`,
'added-required': `New field ${color.bold(
collectionName + '.' + fieldName
)} is required with no default value.\nThis requires deleting existing data in the ${color.bold(
collectionName
)} collection.`,
'added-unique': `New field ${color.bold(
collectionName + '.' + fieldName
)} is marked as unique.\nThis requires deleting existing data in the ${color.bold(
collectionName
)} collection.`,
'updated-type': `Updated field ${color.bold(
collectionName + '.' + fieldName
)} cannot convert data to new field data type.\nThis requires deleting existing data in the ${color.bold(
collectionName
)} collection.`,
};
confirmations.push(reasonMsgs[reason]);
}
@ -180,35 +204,42 @@ type Renamed = Array<{ from: string; to: string }>;
async function resolveFieldRenames(
collectionName: string,
mightAdd: DBFields,
mightDrop: DBFields
mightDrop: DBFields,
ambiguityResponses?: AmbiguityResponses
): Promise<{ added: DBFields; dropped: DBFields; renamed: Renamed }> {
const added: DBFields = {};
const dropped: DBFields = {};
const renamed: Renamed = [];
for (const [fieldName, field] of Object.entries(mightAdd)) {
const { oldFieldName } = (await prompts(
{
type: 'select',
name: 'oldFieldName',
message:
'New field ' +
color.blue(color.bold(`${collectionName}.${fieldName}`)) +
' detected. Was this renamed from an existing field?',
choices: [
{ title: 'New field (not renamed from existing)', value: '__NEW__' },
...Object.keys(mightDrop)
.filter((key) => !(key in renamed))
.map((key) => ({ title: key, value: key })),
],
},
{
onCancel: () => {
process.exit(1);
let oldFieldName = ambiguityResponses
? ambiguityResponses.fieldRenames[collectionName]?.[fieldName] ?? '__NEW__'
: undefined;
if (!oldFieldName) {
const res = await prompts(
{
type: 'select',
name: 'fieldName',
message:
'New field ' +
color.blue(color.bold(`${collectionName}.${fieldName}`)) +
' detected. Was this renamed from an existing field?',
choices: [
{ title: 'New field (not renamed from existing)', value: '__NEW__' },
...Object.keys(mightDrop)
.filter((key) => !(key in renamed))
.map((key) => ({ title: key, value: key })),
],
},
}
)) as { oldFieldName: string };
// Handle their response
{
onCancel: () => {
process.exit(1);
},
}
);
oldFieldName = res.fieldName as string;
}
if (oldFieldName === '__NEW__') {
added[fieldName] = field;
} else {
@ -226,35 +257,42 @@ async function resolveFieldRenames(
async function resolveCollectionRenames(
mightAdd: DBCollections,
mightDrop: DBCollections
mightDrop: DBCollections,
ambiguityResponses?: AmbiguityResponses
): Promise<{ added: DBCollections; dropped: DBCollections; renamed: Renamed }> {
const added: DBCollections = {};
const dropped: DBCollections = {};
const renamed: Renamed = [];
for (const [collectionName, collection] of Object.entries(mightAdd)) {
const { oldCollectionName } = (await prompts(
{
type: 'select',
name: 'oldCollectionName',
message:
'New collection ' +
color.blue(color.bold(collectionName)) +
' detected. Was this renamed from an existing collection?',
choices: [
{ title: 'New collection (not renamed from existing)', value: '__NEW__' },
...Object.keys(mightDrop)
.filter((key) => !(key in renamed))
.map((key) => ({ title: key, value: key })),
],
},
{
onCancel: () => {
process.exit(1);
let oldCollectionName = ambiguityResponses
? ambiguityResponses.collectionRenames[collectionName] ?? '__NEW__'
: undefined;
if (!oldCollectionName) {
const res = await prompts(
{
type: 'select',
name: 'collectionName',
message:
'New collection ' +
color.blue(color.bold(collectionName)) +
' detected. Was this renamed from an existing collection?',
choices: [
{ title: 'New collection (not renamed from existing)', value: '__NEW__' },
...Object.keys(mightDrop)
.filter((key) => !(key in renamed))
.map((key) => ({ title: key, value: key })),
],
},
}
)) as { oldCollectionName: string };
// Handle their response
{
onCancel: () => {
process.exit(1);
},
}
);
oldCollectionName = res.collectionName as string;
}
if (oldCollectionName === '__NEW__') {
added[collectionName] = collection;
} else {

View file

@ -5,54 +5,46 @@ import {
getMigrationQueries,
} from '../../dist/core/cli/migration-queries.js';
import { getCreateTableQuery } from '../../dist/core/queries.js';
import { field, collectionSchema } from '../../dist/core/types.js';
import { field, defineCollection } from '../../dist/core/types.js';
const COLLECTION_NAME = 'Users';
const userInitial = collectionSchema.parse({
const userInitial = defineCollection({
fields: {
name: field.text(),
age: field.number(),
email: field.text({ unique: true }),
mi: field.text({ optional: true }),
},
writable: false,
});
const defaultPromptResponse = {
allowDataLoss: false,
fieldRenames: new Proxy(
{},
{
get: () => false,
}
),
collectionRenames: new Proxy(
{},
{
get: () => false,
}
),
const defaultAmbiguityResponses = {
collectionRenames: {},
fieldRenames: {},
};
function userChangeQueries(oldCollection, newCollection, promptResponses = defaultPromptResponse) {
function userChangeQueries(
oldCollection,
newCollection,
ambiguityResponses = defaultAmbiguityResponses
) {
return getCollectionChangeQueries({
collectionName: COLLECTION_NAME,
oldCollection,
newCollection,
promptResponses,
ambiguityResponses,
});
}
function configChangeQueries(
oldCollections,
newCollections,
promptResponses = defaultPromptResponse
ambiguityResponses = defaultAmbiguityResponses
) {
return getMigrationQueries({
oldSnapshot: { schema: oldCollections, experimentalVersion: 1 },
newSnapshot: { schema: newCollections, experimentalVersion: 1 },
promptResponses,
ambiguityResponses,
});
}
@ -61,21 +53,21 @@ describe('field queries', () => {
it('should be empty when collections are the same', async () => {
const oldCollections = { [COLLECTION_NAME]: userInitial };
const newCollections = { [COLLECTION_NAME]: userInitial };
const queries = await configChangeQueries(oldCollections, newCollections);
const { queries } = await configChangeQueries(oldCollections, newCollections);
expect(queries).to.deep.equal([]);
});
it('should create table for new collections', async () => {
const oldCollections = {};
const newCollections = { [COLLECTION_NAME]: userInitial };
const queries = await configChangeQueries(oldCollections, newCollections);
const { queries } = await configChangeQueries(oldCollections, newCollections);
expect(queries).to.deep.equal([getCreateTableQuery(COLLECTION_NAME, userInitial)]);
});
it('should drop table for removed collections', async () => {
const oldCollections = { [COLLECTION_NAME]: userInitial };
const newCollections = {};
const queries = await configChangeQueries(oldCollections, newCollections);
const { queries } = await configChangeQueries(oldCollections, newCollections);
expect(queries).to.deep.equal([`DROP TABLE "${COLLECTION_NAME}"`]);
});
@ -83,8 +75,8 @@ describe('field queries', () => {
const rename = 'Peeps';
const oldCollections = { [COLLECTION_NAME]: userInitial };
const newCollections = { [rename]: userInitial };
const queries = await configChangeQueries(oldCollections, newCollections, {
...defaultPromptResponse,
const { queries } = await configChangeQueries(oldCollections, newCollections, {
...defaultAmbiguityResponses,
collectionRenames: { [rename]: COLLECTION_NAME },
});
expect(queries).to.deep.equal([`ALTER TABLE "${COLLECTION_NAME}" RENAME TO "${rename}"`]);
@ -93,26 +85,26 @@ describe('field queries', () => {
describe('getCollectionChangeQueries', () => {
it('should be empty when collections are the same', async () => {
const queries = await userChangeQueries(userInitial, userInitial);
const { queries } = await userChangeQueries(userInitial, userInitial);
expect(queries).to.deep.equal([]);
});
it('should be empty when type updated to same underlying SQL type', async () => {
const blogInitial = collectionSchema.parse({
const blogInitial = defineCollection({
...userInitial,
fields: {
title: field.text(),
draft: field.boolean(),
},
});
const blogFinal = collectionSchema.parse({
const blogFinal = defineCollection({
...userInitial,
fields: {
...blogInitial.fields,
draft: field.number(),
},
});
const queries = await userChangeQueries(blogInitial, blogFinal);
const { queries } = await userChangeQueries(blogInitial, blogFinal);
expect(queries).to.deep.equal([]);
});
@ -127,9 +119,9 @@ describe('field queries', () => {
userFinal.fields.middleInitial = userFinal.fields.mi;
delete userFinal.fields.mi;
const queries = await userChangeQueries(userInitial, userFinal, {
...defaultPromptResponse,
fieldRenames: { middleInitial: 'mi' },
const { queries } = await userChangeQueries(userInitial, userFinal, {
collectionRenames: {},
fieldRenames: { [COLLECTION_NAME]: { middleInitial: 'mi' } },
});
expect(queries).to.deep.equal([
`ALTER TABLE "${COLLECTION_NAME}" RENAME COLUMN "mi" TO "middleInitial"`,
@ -147,10 +139,7 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal, {
...defaultPromptResponse,
allowDataLoss: true,
});
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.deep.equal([
'DROP TABLE "Users"',
@ -167,10 +156,7 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal, {
...defaultPromptResponse,
allowDataLoss: true,
});
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.deep.equal([
'DROP TABLE "Users"',
@ -189,10 +175,7 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal, {
...defaultPromptResponse,
allowDataLoss: true,
});
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.have.lengthOf(4);
const tempTableName = getTempTableName(queries[0]);
@ -214,7 +197,7 @@ describe('field queries', () => {
};
delete userFinal.fields.email;
const queries = await userChangeQueries(userInitial, userFinal);
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.have.lengthOf(4);
const tempTableName = getTempTableName(queries[0]);
@ -228,7 +211,7 @@ describe('field queries', () => {
});
it('when updating to a runtime default', async () => {
const initial = collectionSchema.parse({
const initial = defineCollection({
...userInitial,
fields: {
...userInitial.fields,
@ -236,15 +219,15 @@ describe('field queries', () => {
},
});
const userFinal = {
...userInitial,
const userFinal = defineCollection({
...initial,
fields: {
...initial.fields,
age: field.date({ default: 'now' }),
},
};
});
const queries = await userChangeQueries(initial, userFinal);
const { queries } = await userChangeQueries(initial, userFinal);
expect(queries).to.have.lengthOf(4);
const tempTableName = getTempTableName(queries[0]);
@ -265,7 +248,7 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal);
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.have.lengthOf(4);
const tempTableName = getTempTableName(queries[0]);
@ -294,10 +277,7 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal, {
...defaultPromptResponse,
allowDataLoss: true,
});
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.have.lengthOf(4);
@ -320,10 +300,7 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal, {
...defaultPromptResponse,
allowDataLoss: true,
});
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.have.lengthOf(4);
const tempTableName = getTempTableName(queries[0]);
@ -347,13 +324,13 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal);
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.deep.equal(['ALTER TABLE "Users" ADD COLUMN "birthday" text']);
});
it('when adding a required field with default', async () => {
const defaultDate = new Date('2023-01-01');
const userFinal = collectionSchema.parse({
const userFinal = defineCollection({
...userInitial,
fields: {
...userInitial.fields,
@ -361,7 +338,7 @@ describe('field queries', () => {
},
});
const queries = await userChangeQueries(userInitial, userFinal);
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.deep.equal([
`ALTER TABLE "Users" ADD COLUMN "birthday" text NOT NULL DEFAULT '${defaultDate.toISOString()}'`,
]);
@ -378,7 +355,7 @@ describe('field queries', () => {
},
};
const queries = await userChangeQueries(userInitial, userFinal);
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.deep.equal([
'ALTER TABLE "Users" DROP COLUMN "age"',
'ALTER TABLE "Users" DROP COLUMN "mi"',