Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@heroku/heroku-fetch": "^0.1.1-beta",
"@heroku/http-call": "^5.5.1",
"@heroku/mcp-server": "^1.2.5",
"@heroku/sdk": "^0.6.4",
"@heroku/sdk": "^0.7.0",
"@heroku/socksv5": "^0.0.9",
"@heroku/types": "^5.2.0",
"@inquirer/prompts": "^8.7",
Expand Down
19 changes: 9 additions & 10 deletions src/commands/pg/backups/cancel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {Command, flags} from '@heroku-cli/command'
import {utils} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {Args, ux} from '@oclif/core'

import backupsFactory from '../../../lib/pg/backups.js'
import {BackupTransfer} from '../../../lib/pg/types.js'
import * as pgBackups from '../../../lib/pg/backups.js'

export default class Cancel extends Command {
static args = {
Expand All @@ -20,27 +19,27 @@ export default class Cancel extends Command {
const {args, flags} = await this.parse(Cancel)
const {app} = flags
const {backup_id} = args
const pgbackups = backupsFactory(app, this.heroku)
const {data} = new HerokuSDK()

let transfer: BackupTransfer | undefined
let transfer

if (backup_id) {
const num = await pgbackups.num(backup_id)
const num = await pgBackups.num(backup_id, app, data)
if (!num) {
ux.error(`Invalid Backup: ${backup_id}`)
}

({body: transfer} = await this.heroku.get<BackupTransfer>(`/client/v11/apps/${app}/transfers/${num}`, {hostname: utils.pg.host()}))
transfer = await data.transfer.infoByApp(app, String(num), {})
} else {
const {body: transfers} = await this.heroku.get<BackupTransfer[]>(`/client/v11/apps/${app}/transfers`, {hostname: utils.pg.host()})
const transfers = await data.transfer.listByApp(app)
transfer = transfers
.sort((a, b) => b.created_at.localeCompare(a.created_at))
.find(t => !t.finished_at)
}

if (transfer) {
ux.action.start(`Cancelling ${pgbackups.name(transfer)}`)
await this.heroku.post(`/client/v11/apps/${app}/transfers/${transfer.uuid}/actions/cancel`, {hostname: utils.pg.host()})
ux.action.start(`Cancelling ${pgBackups.name(transfer)}`)
await data.transfer.cancel(app, transfer.uuid)
ux.action.stop()
} else {
ux.error('No active backups/transfers')
Expand Down
11 changes: 6 additions & 5 deletions src/commands/pg/backups/delete.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Command, flags} from '@heroku-cli/command'
import {color, utils} from '@heroku/heroku-cli-util'
import {color} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {Args, ux} from '@oclif/core'

import ConfirmCommand from '../../../lib/confirm-command.js'
import backupsFactory from '../../../lib/pg/backups.js'
import * as pgBackups from '../../../lib/pg/backups.js'

export default class Delete extends Command {
static args = {
Expand All @@ -24,17 +25,17 @@ export default class Delete extends Command {
const {args, flags} = await this.parse(Delete)
const {app, confirm} = flags
const {backup_id} = args
const pgbackups = backupsFactory(app, this.heroku)
const {data} = new HerokuSDK()

await new ConfirmCommand().confirm(app, confirm)
ux.action.start(`Deleting backup ${color.cyan(backup_id)} on ${color.app(app)}`)

const num = await pgbackups.num(backup_id)
const num = await pgBackups.num(backup_id, app, data)
if (!num) {
throw new Error(`Invalid Backup: ${backup_id}`)
}

await this.heroku.delete(`/client/v11/apps/${app}/transfers/${num}`, {hostname: utils.pg.host()})
await data.transfer.deleteByApp(app, String(num))
ux.action.stop()
}
}
14 changes: 7 additions & 7 deletions src/commands/pg/backups/download.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {Command, flags} from '@heroku-cli/command'
import {color, utils} from '@heroku/heroku-cli-util'
import {color} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {Args, ux} from '@oclif/core'
import fs from 'fs-extra'

import type {BackupTransfer, PublicUrlResponse} from '../../../lib/pg/types.js'

import pgBackupsApi from '../../../lib/pg/backups.js'
import * as pgBackups from '../../../lib/pg/backups.js'
import download from '../../../lib/pg/download.js'

export default class Download extends Command {
Expand All @@ -25,14 +24,15 @@ export default class Download extends Command {
const {backup_id} = args
const {app} = flags
const output = flags.output || defaultFilename()
const {data} = new HerokuSDK()
let num
ux.action.start(`Getting backup from ${color.app(app)}`)
if (backup_id) {
num = await pgBackupsApi(app, this.heroku).num(backup_id)
num = await pgBackups.num(backup_id, app, data)
if (!num)
throw new Error(`Invalid Backup: ${backup_id}`)
} else {
const {body: transfers} = await this.heroku.get<BackupTransfer[]>(`/client/v11/apps/${app}/transfers`, {hostname: utils.pg.host()})
const transfers = await data.transfer.listByApp(app)
const lastBackup = transfers
.filter(t => t.succeeded && t.to_type === 'gof3r')
.sort((a, b) => b.created_at.localeCompare(a.created_at))[0]
Expand All @@ -42,7 +42,7 @@ export default class Download extends Command {
}

ux.action.status = `fetching url of #${num}`
const {body: info} = await this.heroku.post<PublicUrlResponse>(`/client/v11/apps/${app}/transfers/${num}/actions/public-url`, {hostname: utils.pg.host()})
const info = await data.transfer.publicUrl(app, String(num), {})

ux.action.stop(`done, #${num}`)
await download(info.url, output, {progress: true})
Expand Down
64 changes: 32 additions & 32 deletions src/commands/pg/backups/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type {TransferInfoByAppResult, TransferListByAppResult} from '@heroku/types/data'

import {Command, flags} from '@heroku-cli/command'
import {color, hux, utils} from '@heroku/heroku-cli-util'
import {color, hux} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {transferExtensions} from '@heroku/sdk/extensions/data'
import {ux} from '@oclif/core/ux'

import type {BackupTransfer} from '../../../lib/pg/types.js'
import * as pgBackups from '../../../lib/pg/backups.js'

import backupsFactory from '../../../lib/pg/backups.js'
// hux.table needs Record<string, unknown>; the SDK's interface-based TransferInfoByAppResult has no implicit index signature.
type TransferRow = Record<string, unknown> & TransferInfoByAppResult

export default class Index extends Command {
static description = 'list database backups'
Expand All @@ -27,38 +30,37 @@ export default class Index extends Command {
const {flags: {app}} = await this.parse(Index)

const {data} = new HerokuSDK({extensions: [transferExtensions]})
const transfers = await data.transfer.listByApp(app) as BackupTransfer[]
const transfers = await data.transfer.listByApp(app)
// NOTE that the sort order is descending
transfers.sort((transferA, transferB) => transferB.created_at.localeCompare(transferA.created_at))

this.displayBackups(transfers, app)
this.displayRestores(transfers, app)
this.displayCopies(transfers, app)
this.displayBackups(transfers)
this.displayRestores(transfers)
this.displayCopies(transfers)
}

private displayBackups(transfers: BackupTransfer[], app: string) {
private displayBackups(transfers: TransferListByAppResult) {
const backups = transfers.filter(backupTransfer => backupTransfer.from_type === 'pg_dump' && backupTransfer.to_type === 'gof3r')
const pgbackups = backupsFactory(app, this.heroku)
hux.styledHeader('Backups')
if (backups.length === 0) {
ux.stdout(`No backups. Capture one with ${color.code('heroku pg:backups:capture')}`)
} else {
/* eslint-disable perfectionist/sort-objects */
hux.table<BackupTransfer>(backups, {
hux.table<TransferRow>(backups as TransferRow[], {
ID: {
get: (transfer: BackupTransfer) => color.name(pgbackups.name(transfer)),
get: (transfer: TransferInfoByAppResult) => color.name(pgBackups.name(transfer)),
},
'Created at': {
get: (transfer: BackupTransfer) => transfer.created_at,
get: (transfer: TransferInfoByAppResult) => transfer.created_at,
},
Status: {
get: (transfer: BackupTransfer) => pgbackups.status(transfer),
get: (transfer: TransferInfoByAppResult) => pgBackups.status(transfer),
},
Size: {
get: (transfer: BackupTransfer) => pgbackups.filesize(transfer.processed_bytes),
get: (transfer: TransferInfoByAppResult) => pgBackups.filesize(transfer.processed_bytes),
},
Database: {
get: (transfer: BackupTransfer) => color.datastore(transfer.from_name) || 'UNKNOWN',
get: (transfer: TransferInfoByAppResult) => transfer.from_name ? color.datastore(transfer.from_name) : 'UNKNOWN',
},
})
/* eslint-enable perfectionist/sort-objects */
Expand All @@ -67,32 +69,31 @@ export default class Index extends Command {
ux.stdout()
}

private displayCopies(transfers: BackupTransfer[], app: string) {
const pgbackups = backupsFactory(app, this.heroku)
private displayCopies(transfers: TransferListByAppResult) {
const copies = transfers.filter(t => t.from_type === 'pg_dump' && t.to_type === 'pg_restore').slice(0, 10)
hux.styledHeader('Copies')
if (copies.length === 0) {
ux.stdout(`No copies found. Use ${color.code('heroku pg:copy')} to copy a database to another`)
} else {
/* eslint-disable perfectionist/sort-objects */
hux.table(copies, {
hux.table<TransferRow>(copies as TransferRow[], {
ID: {
get: (transfer: BackupTransfer) => color.name(pgbackups.name(transfer)),
get: (transfer: TransferInfoByAppResult) => color.name(pgBackups.name(transfer)),
},
'Started at': {
get: (transfer: BackupTransfer) => transfer.created_at,
get: (transfer: TransferInfoByAppResult) => transfer.created_at,
},
Status: {
get: (transfer: BackupTransfer) => pgbackups.status(transfer),
get: (transfer: TransferInfoByAppResult) => pgBackups.status(transfer),
},
Size: {
get: (transfer: BackupTransfer) => pgbackups.filesize(transfer.processed_bytes),
get: (transfer: TransferInfoByAppResult) => pgBackups.filesize(transfer.processed_bytes),
},
From: {
get: (transfer: BackupTransfer) => color.datastore(transfer.from_name) || color.inactive('UNKNOWN'),
get: (transfer: TransferInfoByAppResult) => transfer.from_name ? color.datastore(transfer.from_name) : color.inactive('UNKNOWN'),
},
To: {
get: (transfer: BackupTransfer) => color.datastore(transfer.to_name) || color.inactive('UNKNOWN'),
get: (transfer: TransferInfoByAppResult) => transfer.to_name ? color.datastore(transfer.to_name) : color.inactive('UNKNOWN'),
},
})
}
Expand All @@ -101,31 +102,30 @@ export default class Index extends Command {
ux.stdout()
}

private displayRestores(transfers: BackupTransfer[], app: string) {
private displayRestores(transfers: TransferListByAppResult) {
const restores = transfers
.filter(t => t.from_type !== 'pg_dump' && t.to_type === 'pg_restore')
.slice(0, 10) // first 10 only
const pgbackups = backupsFactory(app, this.heroku)
hux.styledHeader('Restores')
if (restores.length === 0) {
ux.stdout(`No restores found. Use ${color.code('heroku pg:backups:restore')} to restore a backup`)
} else {
/* eslint-disable perfectionist/sort-objects */
hux.table(restores, {
hux.table<TransferRow>(restores as TransferRow[], {
ID: {
get: (transfer: BackupTransfer) => color.name(pgbackups.name(transfer)),
get: (transfer: TransferInfoByAppResult) => color.name(pgBackups.name(transfer)),
},
'Started at': {
get: (transfer: BackupTransfer) => transfer.created_at,
get: (transfer: TransferInfoByAppResult) => transfer.created_at,
},
Status: {
get: (transfer: BackupTransfer) => pgbackups.status(transfer),
get: (transfer: TransferInfoByAppResult) => pgBackups.status(transfer),
},
Size: {
get: (transfer: BackupTransfer) => pgbackups.filesize(transfer.processed_bytes),
get: (transfer: TransferInfoByAppResult) => pgBackups.filesize(transfer.processed_bytes),
},
Database: {
get: (transfer: BackupTransfer) => color.datastore(transfer.to_name) || 'UNKNOWN',
get: (transfer: TransferInfoByAppResult) => transfer.to_name ? color.datastore(transfer.to_name) : 'UNKNOWN',
},
})
/* eslint-enable perfectionist/sort-objects */
Expand Down
Loading
Loading