From db874e508ec80b53385e0be384b8cf4925a91b94 Mon Sep 17 00:00:00 2001
From: bholmesdev <hey@bholmes.dev>
Date: Tue, 6 Feb 2024 13:48:41 -0500
Subject: [PATCH] fix: add back promptResponse injection

---
 packages/db/src/core/cli/migration-queries.ts | 136 +++++++++++-------
 packages/db/test/unit/field-queries.test.js   | 105 ++++++--------
 2 files changed, 128 insertions(+), 113 deletions(-)

diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts
index ea8559ac82..87aab6603c 100644
--- a/packages/db/src/core/cli/migration-queries.ts
+++ b/packages/db/src/core/cli/migration-queries.ts
@@ -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 {
diff --git a/packages/db/test/unit/field-queries.test.js b/packages/db/test/unit/field-queries.test.js
index 3a2e800729..ca9d641470 100644
--- a/packages/db/test/unit/field-queries.test.js
+++ b/packages/db/test/unit/field-queries.test.js
@@ -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"',