Skip to content

Commit d9afcb7

Browse files
committed
Adds command 'data:pg:wait' with tests
1 parent f0b17c0 commit d9afcb7

File tree

4 files changed

+353
-12
lines changed

4 files changed

+353
-12
lines changed

src/commands/data/pg/wait.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {color, pg, utils} from '@heroku/heroku-cli-util'
2+
import {HTTPError} from '@heroku/http-call'
3+
import {flags as Flags} from '@heroku-cli/command'
4+
import {Args, ux} from '@oclif/core'
5+
import tsheredoc from 'tsheredoc'
6+
7+
import BaseCommand from '../../../lib/data/baseCommand.js'
8+
import {WaitStatus} from '../../../lib/data/types.js'
9+
import notify from '../../../lib/notify.js'
10+
11+
const heredoc = tsheredoc.default
12+
13+
export default class DataPgWait extends BaseCommand {
14+
static args = {
15+
database: Args.string({
16+
description: 'database name, database attachment name, or related config var on an app',
17+
required: true,
18+
}),
19+
}
20+
21+
static description = 'show status of an operation until it\'s complete'
22+
23+
static examples = [
24+
heredoc(`
25+
# Wait for database to be available
26+
${color.command('heroku data:pg:wait DATABASE --app myapp')}
27+
`),
28+
]
29+
30+
static flags = {
31+
app: Flags.app({required: true}),
32+
'no-notify': Flags.boolean({
33+
description: 'do not show OS notification',
34+
}),
35+
remote: Flags.remote(),
36+
'wait-interval': Flags.integer({
37+
default: 5,
38+
description: 'how frequently to poll in seconds (to avoid rate limiting)',
39+
min: 1,
40+
}),
41+
}
42+
43+
public async notify(...args: Parameters<typeof notify>): Promise<void> {
44+
return notify(...args)
45+
}
46+
47+
public async run(): Promise<void> {
48+
const {args, flags} = await this.parse(DataPgWait)
49+
const {database} = args
50+
const {app, 'no-notify': noNotify, 'wait-interval': waitInterval} = flags
51+
const databaseResolver = new utils.pg.DatabaseResolver(this.heroku)
52+
const db = await databaseResolver.getAttachment(app, database)
53+
const {addon} = db
54+
55+
if (!utils.pg.isAdvancedDatabase(addon)) {
56+
ux.error(heredoc`
57+
You can only use this command on Advanced-tier databases.
58+
Run ${color.code(`heroku pg:wait ${addon.name} -a ${app}`)} instead.`)
59+
}
60+
61+
await this.waitFor(addon, waitInterval || 5, noNotify)
62+
}
63+
64+
public async wait(ms: number): Promise<void> {
65+
return new Promise(resolve => {
66+
setTimeout(resolve, ms)
67+
})
68+
}
69+
70+
private async waitFor(addon: pg.ExtendedAddonAttachment['addon'], interval: number, noNotify: boolean): Promise<void> {
71+
let status: WaitStatus = {message: null, waiting: true}
72+
let waiting = false
73+
let retries = 20
74+
const notFoundMessage = 'Waiting to provision...'
75+
76+
while (status.waiting) {
77+
try {
78+
const response = await this.dataApi.get<WaitStatus>(`/data/postgres/v1/${addon.id}/wait_status`)
79+
status = response.body
80+
} catch (error) {
81+
const httpError = error as HTTPError
82+
if (!retries || httpError.statusCode !== 404) {
83+
if (waiting) {
84+
ux.action.stop(color.red('!'))
85+
}
86+
87+
throw httpError
88+
}
89+
90+
retries--
91+
status = {message: notFoundMessage, waiting: true}
92+
}
93+
94+
if (!status.waiting) {
95+
if (waiting) {
96+
ux.action.stop(status.message || 'available')
97+
} else {
98+
ux.stdout(`${color.datastore(addon.name)} is available.`)
99+
}
100+
101+
break
102+
}
103+
104+
if (!waiting) {
105+
waiting = true
106+
ux.action.start(`Waiting for database ${color.addon(addon.name)}`, status.message || undefined)
107+
}
108+
109+
ux.action.status = status.message || undefined
110+
111+
await this.wait(interval * 1000)
112+
}
113+
114+
if (!noNotify && waiting) {
115+
this.notify('heroku data:pg:wait', `Database ${addon.name} is now available`)
116+
}
117+
}
118+
}

src/lib/data/types.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,30 +229,30 @@ export type Maintenance = {
229229
'name': string;
230230
'plan': string;
231231
'uuid'?: string;
232-
'window': string | null;
232+
'window': null | string;
233233
};
234234
'app': {
235235
'name': string;
236236
'uuid'?: string;
237237
};
238-
'completed_at': string | null;
239-
'duration_seconds': string | null;
238+
'completed_at': null | string;
239+
'duration_seconds': null | string;
240240
'method': string;
241-
'previously_scheduled_for': string | null;
241+
'previously_scheduled_for': null | string;
242242
'reason': string;
243-
'required_by': string | null;
244-
'scheduled_for': string | null;
243+
'required_by': null | string;
244+
'scheduled_for': null | string;
245245
'server_created_at': string;
246-
'started_at': string | null;
246+
'started_at': null | string;
247247
'status': MaintenanceStatus;
248-
'window': string | null;
248+
'window': null | string;
249249
}
250250

251251
export type Window = {
252-
previous_window: string | null;
253-
previously_scheduled_at: string | null;
254-
scheduled_at: string | null;
255-
window: string | null;
252+
previous_window: null | string;
253+
previously_scheduled_at: null | string;
254+
scheduled_at: null | string;
255+
window: null | string;
256256
}
257257

258258
export enum MaintenanceStatus {
@@ -263,3 +263,8 @@ export enum MaintenanceStatus {
263263
ready = 'ready',
264264
running = 'running',
265265
}
266+
267+
export type WaitStatus = {
268+
message: null | string
269+
waiting: boolean
270+
}

test/fixtures/data/pg/fixtures.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ScaleResponse,
1616
SettingsChangeResponse,
1717
SettingsResponse,
18+
WaitStatus,
1819
} from '../../../../src/lib/data/types.js'
1920

2021
export const addon: DeepRequired<Heroku.AddOn> = {
@@ -1188,3 +1189,23 @@ export const nonPostgresAddonAttachment: pg.ExtendedAddonAttachment = {
11881189
id: '0e8e72a3-7922-452e-a490-09cf45797f7e',
11891190
name: 'REDIS',
11901191
}
1192+
1193+
export const waitStatusAvailable: WaitStatus = {
1194+
message: null,
1195+
waiting: false,
1196+
}
1197+
1198+
export const waitStatusProvisioning: WaitStatus = {
1199+
message: 'Provisioning',
1200+
waiting: true,
1201+
}
1202+
1203+
export const waitStatusMigrating: WaitStatus = {
1204+
message: 'Migrating',
1205+
waiting: true,
1206+
}
1207+
1208+
export const waitStatusUpdating: WaitStatus = {
1209+
message: 'Updating',
1210+
waiting: true,
1211+
}

0 commit comments

Comments
 (0)