Skip to content

Commit

Permalink
migrate(W-14179700): pg-v5: Upgrade pg:links:create (#2758)
Browse files Browse the repository at this point in the history
* migrate(W-14179700): pg-v5: Upgrade pg:links:create

* Update packages/cli/src/commands/pg/links/create.ts

Co-authored-by: Santiago Bosio <[email protected]>

* Moved help to description

---------

Co-authored-by: Santiago Bosio <[email protected]>
  • Loading branch information
justinwilaby and sbosio authored Apr 1, 2024
1 parent cff025d commit f8deed5
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 138 deletions.
11 changes: 5 additions & 6 deletions packages/cli/src/commands/addons/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {addonResolver} from '../../lib/addons/resolve'
import type {AddOn, Plan} from '@heroku-cli/schema'
import {HTTP} from 'http-call'
import {HerokuAPIError} from '@heroku-cli/command/lib/api-client'
import type {AddOnAttachmentWithConfigVarsAndPlan} from '../../lib/pg/types'

export default class Upgrade extends Command {
static aliases = ['addons:downgrade'];
static aliases = ['addons:downgrade']
static topic = 'addons'
static description = 'change add-on plan'
static help = 'See available plans with `heroku addons:plans SERVICE`.\n\nNote that `heroku addons:upgrade` and `heroku addons:downgrade` are the same.\nEither one can be used to change an add-on plan up or down.\n\nhttps://devcenter.heroku.com/articles/managing-add-ons'
Expand All @@ -30,9 +31,9 @@ export default class Upgrade extends Command {
// called with just one argument in the form of `heroku addons:upgrade heroku-redis:hobby`
const {addon, plan} = this.getAddonPartsFromArgs(args)

let resolvedAddon: Required<AddOn & { provision_message: string }>
let resolvedAddon: Required<AddOn> | AddOnAttachmentWithConfigVarsAndPlan
try {
resolvedAddon = await addonResolver(this.heroku, app, addon) as Required<AddOn & { provision_message: string }>
resolvedAddon = await addonResolver(this.heroku, app, addon)
} catch (error) {
if (error instanceof HerokuAPIError && error.http.statusCode === 422 && error.body.id === 'multiple_matches') {
throw new Error(this.buildApiErrorMessage(error.http.body.message, ctx))
Expand All @@ -48,9 +49,7 @@ export default class Upgrade extends Command {
ux.action.start(`Changing ${color.magenta(addonName ?? '')} on ${color.cyan(appName ?? '')} from ${color.blue(resolvedAddonPlan?.name ?? '')} to ${color.blue(updatedPlanName)}`)

try {
const patchResult: HTTP<Required<AddOn & {
provision_message: string
}>> = await this.heroku.patch(`/apps/${appName}/addons/${addonName}`,
const patchResult: HTTP<Required<AddOn>> = await this.heroku.patch(`/apps/${appName}/addons/${addonName}`,
{
body: {plan: {name: updatedPlanName}},
headers: {
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/src/commands/pg/links/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import heredoc from 'tsheredoc'
import {addonResolver} from '../../../lib/addons/resolve'
import {getAddon} from '../../../lib/pg/fetcher'
import host from '../../../lib/pg/host'
import type {AddOnAttachmentWithConfigVarsAndPlan, Link} from '../../../lib/pg/types'
import {essentialPlan} from '../../../lib/pg/util'

export default class Create extends Command {
static topic = 'pg'
static description = heredoc(`
create a link between data stores
Example:
heroku pg:links:create HEROKU_REDIS_RED HEROKU_POSTGRESQL_CERULEAN
`)

static flags = {
as: flags.string({description: 'name of link to create'}),
app: flags.app({required: true}),
remote: flags.remote(),
}

static args = {
remote: Args.string({required: true}),
database: Args.string({required: true}),
}

public async run(): Promise<void> {
const {flags, args} = await this.parse(Create)
const {app} = flags

const service = async (remoteId: string) => {
const addon = await addonResolver(this.heroku, app, remoteId)
if (!addon.plan.name.match(/^heroku-(redis|postgresql)/))
throw new Error('Remote database must be heroku-redis or heroku-postgresql')
return addon
}

const [db, target] = await Promise.all([
getAddon(this.heroku, app, args.database),
service(args.remote),
]) as [AddOnAttachmentWithConfigVarsAndPlan, AddOnAttachmentWithConfigVarsAndPlan]

if (essentialPlan(db))
throw new Error('pg:links isn\u2019t available for Essential-tier databases.')
if (essentialPlan(target))
throw new Error('pg:links isn\u2019t available for Essential-tier databases.')

ux.action.start(`Adding link from ${color.yellow(target.name)} to ${color.yellow(db.name)}`)
const {body: link} = await this.heroku.post<Link>(`/client/v11/databases/${db.id}/links`, {
body: {
target: target.name,
as: flags.as,
},
hostname: host(),
})

if (link.message) {
throw new Error(link.message)
}

ux.action.stop(`done, ${color.cyan(link.name)}`)
}
}
18 changes: 9 additions & 9 deletions packages/cli/src/lib/addons/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ const handleNotFound = function (err: { statusCode: number, body?: { resource: s
}

export const addonResolver = async (heroku: APIClient, app: string | undefined, id: string, options?: AddOnAttachment) => {
const headers = addonHeaders

const getAddon = async (addonId: string) => {
const response = await heroku.post<AddOnAttachment[]>('/actions/addons/resolve', {
headers: headers,
const response = await heroku.post<AddOnAttachmentWithConfigVarsAndPlan[]>('/actions/addons/resolve', {
headers: addonHeaders,
body: {app: null, addon: addonId, addon_service: options?.addon_service},
})
return singularize('addon', options?.namespace || '')(response?.body)
Expand Down Expand Up @@ -76,10 +74,12 @@ const attachmentHeaders: Readonly<{ Accept: string, 'Accept-Inclusion': string }
export const appAttachment = async (heroku: APIClient, app: string | undefined, id: string, options: {
addon_service?: string,
namespace?: string
} = {}): Promise<AddOnAttachment & {addon: AddOnAttachmentWithConfigVarsAndPlan}> => {
const result = await heroku.post<(AddOnAttachment & {addon: AddOnAttachmentWithConfigVarsAndPlan})[]>('/actions/addon-attachments/resolve', {
headers: attachmentHeaders, body: {app, addon_attachment: id, addon_service: options.addon_service},
})
} = {}): Promise<AddOnAttachment & { addon: AddOnAttachmentWithConfigVarsAndPlan }> => {
const result = await heroku.post<(AddOnAttachment & {
addon: AddOnAttachmentWithConfigVarsAndPlan
})[]>('/actions/addon-attachments/resolve', {
headers: attachmentHeaders, body: {app, addon_attachment: id, addon_service: options.addon_service},
})
return singularize('addon_attachment', options.namespace)(result.body)
}

Expand Down Expand Up @@ -173,7 +173,7 @@ export class AmbiguousError extends Error {
}

function singularize(type?: string | null, namespace?: string | null) {
return <T extends {namespace?: string | null, name?: string}>(matches: T[]): T => {
return <T extends { namespace?: string | null, name?: string }>(matches: T[]): T => {
if (namespace) {
matches = matches.filter(m => m.namespace === namespace)
} else if (matches.length > 1) {
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/src/lib/pg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type BackupTransfer = {
pgbackups_name: string,
},
processed_bytes: number,
schedule: {uuid: string},
schedule: { uuid: string },
started_at: string,
source_bytes: number,
succeeded: boolean,
Expand All @@ -34,8 +34,12 @@ export type BackupTransfer = {
updated_at: string,
warnings: number,
}
export type AddOnWithPlan = Required<Heroku.AddOnAttachment['addon']> & {plan: Required<Heroku.AddOn['plan']>}
export type AddOnAttachmentWithConfigVarsAndPlan = Heroku.AddOnAttachment & {
export type AddOnWithPlan = Required<Heroku.AddOnAttachment['addon']> & { plan: Required<Heroku.AddOn['plan']> }
export type AddOnAttachmentWithConfigVarsAndPlan = Required<Heroku.AddOnAttachment> & {
config_vars: Heroku.AddOn['config_vars']
addon: AddOnWithPlan
}
export type Link = {
message: string,
name: string
}
90 changes: 90 additions & 0 deletions packages/cli/test/unit/commands/pg/links/create.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {stdout, stderr} from 'stdout-stderr'
import heredoc from 'tsheredoc'
import Cmd from '../../../../../src/commands/pg/links/create'
import runCommand from '../../../../helpers/runCommand'

import {expect} from 'chai'
import * as nock from 'nock'

describe('pg:links:create', async () => {
let api: nock.Scope
let pg: nock.Scope

beforeEach(() => {
api = nock('https://api.heroku.com')
pg = nock('https://api.data.heroku.com')
})

afterEach(() => {
nock.cleanAll()
api.done()
pg.done()
})

describe('on an essential database', () => {
const addon = {
id: 2, name: 'postgres-1', plan: {name: 'heroku-postgresql:basic'},
}

it('errors when attempting to create a link', async () => {
api.post('/actions/addon-attachments/resolve', {
app: 'myapp', addon_attachment: 'heroku-postgres', addon_service: 'heroku-postgresql',
}).reply(200, [{addon}])

api.post('/actions/addons/resolve', {
app: 'myapp',
addon: 'heroku-redis',
}).reply(200, [addon])

try {
await runCommand(Cmd, [
'--app',
'myapp',
'--as',
'foobar',
'heroku-redis',
'heroku-postgres',
])
} catch (error) {
const {message} = error as { message: string }
expect(message).to.equal('pg:links isn\u2019t available for Essential-tier databases.')
}
})
})

describe('on a production database', () => {
const addon = {
id: 1, name: 'postgres-1', plan: {name: 'heroku-postgresql:standard-0'},
}

it('errors when attempting to create a link', async () => {
api.post('/actions/addon-attachments/resolve', {
app: 'myapp',
addon_attachment: 'heroku-postgres',
addon_service: 'heroku-postgresql',
}).reply(200, [{addon}])

api.post('/actions/addons/resolve', {
app: 'myapp',
addon: 'heroku-redis',
}).reply(200, [addon])

pg.post('/client/v11/databases/1/links', {target: 'postgres-1', as: 'foobar'})
.reply(200, {name: 'foobar'})

await runCommand(Cmd, [
'--app',
'myapp',
'--as',
'foobar',
'heroku-redis',
'heroku-postgres',
])
expect(stdout.output).to.equal('')
expect(stderr.output).to.equal(heredoc(`
Adding link from postgres-1 to postgres-1...
Adding link from postgres-1 to postgres-1... done, foobar
`))
})
})
})
51 changes: 0 additions & 51 deletions packages/pg-v5/commands/links/create.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/pg-v5/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ exports.commands = flatten([
require('./commands/kill'),
require('./commands/killall'),
require('./commands/links'),
require('./commands/links/create'),
require('./commands/links/destroy'),
require('./commands/locks'),
require('./commands/maintenance'),
Expand Down
68 changes: 0 additions & 68 deletions packages/pg-v5/test/unit/commands/links/create.unit.test.js

This file was deleted.

0 comments on commit f8deed5

Please sign in to comment.