Skip to content

Commit ae451b1

Browse files
committed
feat: add hidden --method flag to data:pg:migrate
Allows internal teams to opt in to CDC/streaming migrations without surfacing the option to customers. Accepts 'snapshot' (default, mapped to full-load) or 'streaming' (mapped to cdc) and is sent in the configure-migration POST body.
1 parent 355113e commit ae451b1

2 files changed

Lines changed: 72 additions & 2 deletions

File tree

src/commands/data/pg/migrate.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ export default class DataPgMigrate extends BaseCommand {
3030
static description = 'migrate an existing classic Postgres database to an Advanced database'
3131
static flags = {
3232
app: Flags.app({required: true}),
33+
method: Flags.string({
34+
default: 'snapshot',
35+
hidden: true,
36+
options: ['snapshot', 'streaming'],
37+
}),
3338
remote: Flags.remote(),
3439
}
3540
private advancedDatabases: Array<pg.ExtendedAddonAttachment['addon'] & {attachment_names?: string[], info?: InfoResponse}> = []
3641
private appName: string | undefined
3742
private classicDatabases: Array<pg.ExtendedAddonAttachment['addon'] & {attachment_names?: string[]}> = []
3843
private extendedLevelsInfo: ExtendedPostgresLevelInfo[] | undefined
44+
private migrationMethod: 'cdc' | 'full-load' = 'full-load'
3945
private migrationTargets: Array<MigrationResponse> = []
4046

4147
public async createAddon(...args: Parameters<typeof createAddon>): Promise<Heroku.AddOn> {
@@ -48,8 +54,9 @@ export default class DataPgMigrate extends BaseCommand {
4854

4955
public async run(): Promise<void> {
5056
const {flags} = await this.parse(DataPgMigrate)
51-
const {app} = flags
57+
const {app, method} = flags
5258
this.appName = app
59+
this.migrationMethod = method === 'streaming' ? 'cdc' : 'full-load'
5360

5461
ux.stdout(heredoc`
5562
@@ -200,7 +207,7 @@ export default class DataPgMigrate extends BaseCommand {
200207
ux.stdout('')
201208
ux.action.start('Configuring migration')
202209
await this.dataApi.post<MigrationResponse>(`/data/postgres/v1/${targetDatabaseId}/migrations`, {
203-
body: {source_id: sourceDatabaseId},
210+
body: {method: this.migrationMethod, source_id: sourceDatabaseId},
204211
})
205212
ux.action.stop()
206213
currentStep = '__exit'

test/unit/commands/data/pg/migrate.unit.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ describe('data:pg:migrate', function () {
328328
.get(`/data/postgres/v1/${unavailableAdvancedDbAttachment.addon.id}/info`)
329329
.reply(200, unavailableAdvancedDbInfo)
330330
.post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, {
331+
method: 'full-load',
331332
source_id: premiumDbAttachment.addon.id,
332333
})
333334
.reply(200, createdMigrationResponse)
@@ -459,6 +460,65 @@ describe('data:pg:migrate', function () {
459460
})
460461
})
461462

463+
describe('configure a database migration with the hidden --method flag', function () {
464+
it('sends method=cdc when --method=streaming', async function () {
465+
const herokuApi = nock('https://api.heroku.com')
466+
.get('/apps/myapp/addon-attachments')
467+
.reply(200, [
468+
nonTargetAdvancedDbAttachment,
469+
premiumDbAttachment,
470+
targetAdvancedDbAttachment,
471+
])
472+
.get('/apps/myapp/addon-attachments')
473+
.reply(200, [
474+
nonTargetAdvancedDbAttachment,
475+
premiumDbAttachment,
476+
targetAdvancedDbAttachment,
477+
])
478+
const dataApi = nock('https://api.data.heroku.com')
479+
.get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/migrations`)
480+
.reply(200, existentMigrationResponse)
481+
.get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`)
482+
.reply(404, {id: 'not_found', message: 'Add-on not found'})
483+
.get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/info`)
484+
.reply(200, targetAdvancedDbInfo)
485+
.get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/info`)
486+
.reply(200, nonTargetAdvancedDbInfo)
487+
.post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, {
488+
method: 'cdc',
489+
source_id: premiumDbAttachment.addon.id,
490+
})
491+
.reply(200, createdMigrationResponse)
492+
.get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/migrations`)
493+
.reply(200, existentMigrationResponse)
494+
.get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`)
495+
.reply(200, createdMigrationResponse)
496+
.get(`/data/postgres/v1/${targetAdvancedDbAttachment.addon.id}/info`)
497+
.reply(200, targetAdvancedDbInfo)
498+
.get(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/info`)
499+
.reply(200, nonTargetAdvancedDbInfo)
500+
501+
mockedStdinInput = [
502+
'\n', // Main menu: > Configure a database migration
503+
'\n', // Select source database: > Premium database
504+
'\n', // Select target database: > Non-target Advanced database
505+
'\n', // Confirm migration configuration: > Confirm
506+
'\n', // Main menu: > Exit
507+
]
508+
509+
const {stderr} = await runCommand(DataPgMigrate, ['--app=myapp', '--method=streaming'])
510+
511+
herokuApi.done()
512+
dataApi.done()
513+
expect(stderr).to.equal('Configuring migration... done\n')
514+
})
515+
516+
it('rejects unsupported method values', async function () {
517+
const {error} = await runCommand(DataPgMigrate, ['--app=myapp', '--method=bogus'])
518+
expect(error?.message).to.match(/Expected --method=bogus to be one of: snapshot, streaming/)
519+
})
520+
})
521+
462522
describe('configure a database migration with a new target database created for the migration', function () {
463523
beforeEach(async function () {
464524
poolConfigLeaderInteractiveConfigStub.resolves({
@@ -498,6 +558,7 @@ describe('data:pg:migrate', function () {
498558
.get('/data/postgres/v1/pricing')
499559
.reply(200, pricingResponse)
500560
.post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, {
561+
method: 'full-load',
501562
source_id: premiumDbAttachment.addon.id,
502563
})
503564
.reply(200, createdMigrationResponse)
@@ -563,6 +624,7 @@ describe('data:pg:migrate', function () {
563624
.get('/data/postgres/v1/pricing')
564625
.reply(200, pricingResponse)
565626
.post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, {
627+
method: 'full-load',
566628
source_id: privateDbAttachment.addon.id,
567629
})
568630
.reply(200, {
@@ -634,6 +696,7 @@ describe('data:pg:migrate', function () {
634696
.get('/data/postgres/v1/pricing')
635697
.reply(200, pricingResponse)
636698
.post(`/data/postgres/v1/${nonTargetAdvancedDbAttachment.addon.id}/migrations`, {
699+
method: 'full-load',
637700
source_id: shieldDbAttachment.addon.id,
638701
})
639702
.reply(200, {

0 commit comments

Comments
 (0)