1 | 'use strict'
|
2 |
|
3 | const co = require('co')
|
4 | const bastion = require('./bastion')
|
5 | const debug = require('./debug')
|
6 |
|
7 | function handlePsqlError (reject, psql) {
|
8 | psql.on('error', (err) => {
|
9 | if (err.code === 'ENOENT') {
|
10 | reject(`The local psql command could not be located. For help installing psql, see https://devcenter.heroku.com/articles/heroku-postgresql#local-setup`)
|
11 | } else {
|
12 | reject(err)
|
13 | }
|
14 | })
|
15 | }
|
16 |
|
17 | function execPsql (query, dbEnv) {
|
18 | const { spawn } = require('child_process')
|
19 | return new Promise((resolve, reject) => {
|
20 | let result = ''
|
21 | debug('Running query: %s', query.trim())
|
22 | let psql = spawn('psql', ['-c', query], { env: dbEnv, encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'inherit' ] })
|
23 | psql.stdout.on('data', function (data) {
|
24 | result += data.toString()
|
25 | })
|
26 | psql.on('close', function (code) {
|
27 | resolve(result)
|
28 | })
|
29 | handlePsqlError(reject, psql)
|
30 | })
|
31 | }
|
32 |
|
33 | function execPsqlWithFile (file, dbEnv) {
|
34 | const { spawn } = require('child_process')
|
35 | return new Promise((resolve, reject) => {
|
36 | let result = ''
|
37 | debug('Running sql file: %s', file.trim())
|
38 | let psql = spawn('psql', ['-f', file], { env: dbEnv, encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'inherit' ] })
|
39 | psql.stdout.on('data', function (data) {
|
40 | result += data.toString()
|
41 | })
|
42 | psql.on('close', function (code) {
|
43 | resolve(result)
|
44 | })
|
45 | handlePsqlError(reject, psql)
|
46 | })
|
47 | }
|
48 |
|
49 | function psqlInteractive (dbEnv, prompt) {
|
50 | const { spawn } = require('child_process')
|
51 | return new Promise((resolve, reject) => {
|
52 | let psqlArgs = ['--set', `PROMPT1=${prompt}`, '--set', `PROMPT2=${prompt}`]
|
53 | let psqlHistoryPath = process.env.HEROKU_PSQL_HISTORY
|
54 | if (psqlHistoryPath) {
|
55 | const fs = require('fs')
|
56 | const path = require('path')
|
57 | if (fs.existsSync(psqlHistoryPath) && fs.statSync(psqlHistoryPath).isDirectory()) {
|
58 | let appLogFile = `${psqlHistoryPath}/${prompt.split(':')[0]}`
|
59 | debug('Logging psql history to %s', appLogFile)
|
60 | psqlArgs = psqlArgs.concat(['--set', `HISTFILE=${appLogFile}`])
|
61 | } else if (fs.existsSync(path.dirname(psqlHistoryPath))) {
|
62 | debug('Logging psql history to %s', psqlHistoryPath)
|
63 | psqlArgs = psqlArgs.concat(['--set', `HISTFILE=${psqlHistoryPath}`])
|
64 | } else {
|
65 | const cli = require('heroku-cli-util')
|
66 | cli.warn(`HEROKU_PSQL_HISTORY is set but is not a valid path (${psqlHistoryPath})`)
|
67 | }
|
68 | }
|
69 |
|
70 | let psql = spawn('psql', psqlArgs, { env: dbEnv, stdio: 'inherit' })
|
71 | handlePsqlError(reject, psql)
|
72 | psql.on('close', (data) => {
|
73 | resolve()
|
74 | })
|
75 | })
|
76 | }
|
77 |
|
78 | function handleSignals () {
|
79 | process.removeAllListeners('SIGINT')
|
80 | process.on('SIGINT', () => {})
|
81 | }
|
82 |
|
83 | function * exec (db, query) {
|
84 | handleSignals()
|
85 | let configs = bastion.getConfigs(db)
|
86 |
|
87 | yield bastion.sshTunnel(db, configs.dbTunnelConfig)
|
88 | return yield execPsql(query, configs.dbEnv)
|
89 | }
|
90 |
|
91 | async function execFile (db, file) {
|
92 | handleSignals()
|
93 | let configs = bastion.getConfigs(db)
|
94 |
|
95 | await bastion.sshTunnel(db, configs.dbTunnelConfig)
|
96 | return execPsqlWithFile(file, configs.dbEnv)
|
97 | }
|
98 |
|
99 | function * interactive (db) {
|
100 | let name = db.attachment.name
|
101 | let prompt = `${db.attachment.app.name}::${name}%R%# `
|
102 | handleSignals()
|
103 | let configs = bastion.getConfigs(db)
|
104 | configs.dbEnv.PGAPPNAME = 'psql interactive'
|
105 |
|
106 | yield bastion.sshTunnel(db, configs.dbTunnelConfig)
|
107 | return yield psqlInteractive(configs.dbEnv, prompt)
|
108 | }
|
109 |
|
110 | module.exports = {
|
111 | exec: co.wrap(exec),
|
112 | execFile: execFile,
|
113 | interactive: co.wrap(interactive)
|
114 | }
|