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"',