diff --git a/src/commands/data/pg/migrate.ts b/src/commands/data/pg/migrate.ts index da4aace9dc..45333531d6 100644 --- a/src/commands/data/pg/migrate.ts +++ b/src/commands/data/pg/migrate.ts @@ -30,12 +30,18 @@ export default class DataPgMigrate extends BaseCommand { static description = 'migrate an existing classic Postgres database to an Advanced database' static flags = { app: Flags.app({required: true}), + method: Flags.string({ + default: 'snapshot', + hidden: true, + options: ['snapshot', 'streaming'], + }), remote: Flags.remote(), } private advancedDatabases: Array = [] private appName: string | undefined private classicDatabases: Array = [] private extendedLevelsInfo: ExtendedPostgresLevelInfo[] | undefined + private migrationMethod: 'cdc' | 'full-load' = 'full-load' private migrationTargets: Array = [] public async createAddon(...args: Parameters): Promise { @@ -48,8 +54,9 @@ export default class DataPgMigrate extends BaseCommand { public async run(): Promise { const {flags} = await this.parse(DataPgMigrate) - const {app} = flags + const {app, method} = flags this.appName = app + this.migrationMethod = method === 'streaming' ? 'cdc' : 'full-load' ux.stdout(heredoc` @@ -200,7 +207,7 @@ export default class DataPgMigrate extends BaseCommand { ux.stdout('') ux.action.start('Configuring migration') await this.dataApi.post(`/data/postgres/v1/${targetDatabaseId}/migrations`, { - body: {source_id: sourceDatabaseId}, + body: {method: this.migrationMethod, source_id: sourceDatabaseId}, }) ux.action.stop() currentStep = '__exit' diff --git a/test/unit/commands/data/pg/migrate.unit.test.ts b/test/unit/commands/data/pg/migrate.unit.test.ts index 65cc9bf22d..f65cdf0694 100644 --- a/test/unit/commands/data/pg/migrate.unit.test.ts +++ b/test/unit/commands/data/pg/migrate.unit.test.ts @@ -328,6 +328,7 @@ describe('data:pg:migrate', function () { .get(`/data/postgres/v1/${unavailableAdvancedDbAttachment.addon.id}/info`) .reply(200, unavailableAdvancedDbInfo) .post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, { + method: 'full-load', source_id: premiumDbAttachment.addon.id, }) .reply(200, createdMigrationResponse) @@ -459,6 +460,65 @@ describe('data:pg:migrate', function () { }) }) + describe('configure a database migration with the hidden --method flag', function () { + it('sends method=cdc when --method=streaming', async function () { + const herokuApi = nock('https://api.heroku.com') + .get('/apps/myapp/addon-attachments') + .reply(200, [ + nonTargetAdvancedDbAttachment, + premiumDbAttachment, + targetAdvancedDbAttachment, + ]) + .get('/apps/myapp/addon-attachments') + .reply(200, [ + nonTargetAdvancedDbAttachment, + premiumDbAttachment, + targetAdvancedDbAttachment, + ]) + const dataApi = nock('https://api.data.heroku.com') + .get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/migrations`) + .reply(200, existentMigrationResponse) + .get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`) + .reply(404, {id: 'not_found', message: 'Add-on not found'}) + .get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/info`) + .reply(200, targetAdvancedDbInfo) + .get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/info`) + .reply(200, nonTargetAdvancedDbInfo) + .post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, { + method: 'cdc', + source_id: premiumDbAttachment.addon.id, + }) + .reply(200, createdMigrationResponse) + .get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/migrations`) + .reply(200, existentMigrationResponse) + .get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`) + .reply(200, createdMigrationResponse) + .get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/info`) + .reply(200, targetAdvancedDbInfo) + .get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/info`) + .reply(200, nonTargetAdvancedDbInfo) + + mockedStdinInput = [ + '\n', // Main menu: > Configure a database migration + '\n', // Select source database: > Premium database + '\n', // Select target database: > Non-target Advanced database + '\n', // Confirm migration configuration: > Confirm + '\n', // Main menu: > Exit + ] + + const {stderr} = await runCommand(DataPgMigrate, ['--app=myapp', '--method=streaming']) + + herokuApi.done() + dataApi.done() + expect(stderr).to.equal('Configuring migration... done\n') + }) + + it('rejects unsupported method values', async function () { + const {error} = await runCommand(DataPgMigrate, ['--app=myapp', '--method=bogus']) + expect(error?.message).to.match(/Expected --method=bogus to be one of: snapshot, streaming/) + }) + }) + describe('configure a database migration with a new target database created for the migration', function () { beforeEach(async function () { poolConfigLeaderInteractiveConfigStub.resolves({ @@ -498,6 +558,7 @@ describe('data:pg:migrate', function () { .get('/data/postgres/v1/pricing') .reply(200, pricingResponse) .post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, { + method: 'full-load', source_id: premiumDbAttachment.addon.id, }) .reply(200, createdMigrationResponse) @@ -563,6 +624,7 @@ describe('data:pg:migrate', function () { .get('/data/postgres/v1/pricing') .reply(200, pricingResponse) .post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, { + method: 'full-load', source_id: privateDbAttachment.addon.id, }) .reply(200, { @@ -634,6 +696,7 @@ describe('data:pg:migrate', function () { .get('/data/postgres/v1/pricing') .reply(200, pricingResponse) .post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, { + method: 'full-load', source_id: shieldDbAttachment.addon.id, }) .reply(200, {