@heroku-cli/heroku-connect-plugin 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +206 -0
- package/README.md +93 -0
- package/commands/connect/db-set.js +69 -0
- package/commands/connect/diagnose.js +108 -0
- package/commands/connect/export.js +35 -0
- package/commands/connect/import.js +30 -0
- package/commands/connect/info.js +55 -0
- package/commands/connect/mapping-delete.js +33 -0
- package/commands/connect/mapping-diagnose.js +33 -0
- package/commands/connect/mapping-reload.js +31 -0
- package/commands/connect/mapping-state.js +26 -0
- package/commands/connect/mapping-write-errors.js +34 -0
- package/commands/connect/pause.js +24 -0
- package/commands/connect/recover.js +25 -0
- package/commands/connect/resume.js +24 -0
- package/commands/connect/sf-auth.js +75 -0
- package/commands/connect/state.js +34 -0
- package/commands/connect/write-errors.js +27 -0
- package/commands/connect-events/db-set.js +68 -0
- package/commands/connect-events/info.js +57 -0
- package/commands/connect-events/pause.js +24 -0
- package/commands/connect-events/recover.js +25 -0
- package/commands/connect-events/resume.js +24 -0
- package/commands/connect-events/sf-auth.js +75 -0
- package/commands/connect-events/state.js +34 -0
- package/commands/connect-events/stream-create.js +32 -0
- package/commands/connect-events/stream-delete.js +33 -0
- package/commands/connect-events/stream-state.js +26 -0
- package/index.js +50 -0
- package/lib/clients/connect.js +29 -0
- package/lib/clients/discovery.js +32 -0
- package/lib/connect/api.js +181 -0
- package/package.json +42 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const api = require('../../lib/connect/api.js')
|
|
3
|
+
const cli = require('@heroku/heroku-cli-util')
|
|
4
|
+
const co = require('co')
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
topic: 'connect-events:stream',
|
|
8
|
+
command: 'delete',
|
|
9
|
+
description: 'Delete an existing stream',
|
|
10
|
+
help: 'Delete an existing stream',
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'stream' }
|
|
13
|
+
],
|
|
14
|
+
flags: [
|
|
15
|
+
{ name: 'resource', description: 'specific connection resource name', hasValue: true },
|
|
16
|
+
{ name: 'confirm', hasValue: true }
|
|
17
|
+
],
|
|
18
|
+
needsApp: true,
|
|
19
|
+
needsAuth: true,
|
|
20
|
+
run: cli.command(co.wrap(function * (context, heroku) {
|
|
21
|
+
yield cli.confirmApp(context.app, context.flags.confirm)
|
|
22
|
+
|
|
23
|
+
yield cli.action('deleting stream', co(function * () {
|
|
24
|
+
const connection = yield api.withConnection(context, heroku, api.ADDON_TYPE_EVENTS)
|
|
25
|
+
context.region = connection.region_url
|
|
26
|
+
const stream = yield api.withStream(context, connection, context.args.stream)
|
|
27
|
+
const response = yield api.request(context, 'DELETE', `/api/v3/streams/${stream.id}`)
|
|
28
|
+
if (response.status !== 204) {
|
|
29
|
+
throw new Error(response.data.message || 'unknown error')
|
|
30
|
+
}
|
|
31
|
+
}))
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const api = require('../../lib/connect/api.js')
|
|
3
|
+
const cli = require('@heroku/heroku-cli-util')
|
|
4
|
+
const co = require('co')
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
topic: 'connect-events:stream',
|
|
8
|
+
command: 'state',
|
|
9
|
+
description: 'return a stream state',
|
|
10
|
+
help: 'return a stream state',
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'stream' }
|
|
13
|
+
],
|
|
14
|
+
flags: [
|
|
15
|
+
{ name: 'resource', description: 'specific connection resource name', hasValue: true }
|
|
16
|
+
],
|
|
17
|
+
needsApp: true,
|
|
18
|
+
needsAuth: true,
|
|
19
|
+
run: cli.command(co.wrap(function * (context, heroku) {
|
|
20
|
+
const connection = yield api.withConnection(context, heroku, api.ADDON_TYPE_EVENTS)
|
|
21
|
+
context.region = connection.region_url
|
|
22
|
+
const stream = yield api.withStream(context, connection, context.args.stream)
|
|
23
|
+
|
|
24
|
+
cli.log(stream.state)
|
|
25
|
+
}))
|
|
26
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
exports.topics = [
|
|
3
|
+
{
|
|
4
|
+
name: 'connect',
|
|
5
|
+
description: 'manage connections for Heroku Connect'
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: 'connect:mapping',
|
|
9
|
+
description: 'manage mappings on a Heroku Connect addon'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'connect-events',
|
|
13
|
+
description: 'manage connections for Heroku Connect Events Pilot'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'connect-events:stream',
|
|
17
|
+
description: 'manage mappings on a Heroku Connect Events Pilot addon'
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
exports.commands = [
|
|
22
|
+
require('./commands/connect/info'),
|
|
23
|
+
require('./commands/connect/state'),
|
|
24
|
+
require('./commands/connect/import'),
|
|
25
|
+
require('./commands/connect/export'),
|
|
26
|
+
require('./commands/connect/pause'),
|
|
27
|
+
require('./commands/connect/resume'),
|
|
28
|
+
require('./commands/connect/recover'),
|
|
29
|
+
require('./commands/connect/sf-auth'),
|
|
30
|
+
require('./commands/connect/db-set'),
|
|
31
|
+
require('./commands/connect/diagnose'),
|
|
32
|
+
require('./commands/connect/mapping-state'),
|
|
33
|
+
require('./commands/connect/mapping-delete'),
|
|
34
|
+
require('./commands/connect/mapping-reload'),
|
|
35
|
+
require('./commands/connect/mapping-diagnose'),
|
|
36
|
+
require('./commands/connect/mapping-write-errors'),
|
|
37
|
+
require('./commands/connect/write-errors'),
|
|
38
|
+
|
|
39
|
+
// Connect Events
|
|
40
|
+
require('./commands/connect-events/info'),
|
|
41
|
+
require('./commands/connect-events/state'),
|
|
42
|
+
require('./commands/connect-events/pause'),
|
|
43
|
+
require('./commands/connect-events/resume'),
|
|
44
|
+
require('./commands/connect-events/recover'),
|
|
45
|
+
require('./commands/connect-events/sf-auth'),
|
|
46
|
+
require('./commands/connect-events/db-set'),
|
|
47
|
+
require('./commands/connect-events/stream-state'),
|
|
48
|
+
require('./commands/connect-events/stream-delete'),
|
|
49
|
+
require('./commands/connect-events/stream-create')
|
|
50
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
|
|
3
|
+
class ConnectClient {
|
|
4
|
+
constructor (context) {
|
|
5
|
+
this.client = axios.create({
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
Authorization: `Bearer ${context.auth.password}`,
|
|
9
|
+
'Heroku-Client': 'cli'
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getDetails (connection) {
|
|
15
|
+
const detailUrl = connection.detail_url
|
|
16
|
+
return this.client.get(`${detailUrl}?deep=true`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
request ({ baseURL, method, url, data }) {
|
|
20
|
+
return this.client({
|
|
21
|
+
baseURL,
|
|
22
|
+
method,
|
|
23
|
+
url,
|
|
24
|
+
data
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
exports.ConnectClient = ConnectClient
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
const baseURL = process.env.CONNECT_DISCOVERY_SERVER || (process.env.CONNECT_ADDON === 'connectqa'
|
|
3
|
+
? 'https://hc-central-qa.herokai.com'
|
|
4
|
+
: 'https://hc-central.heroku.com')
|
|
5
|
+
|
|
6
|
+
class DiscoveryClient {
|
|
7
|
+
constructor (context) {
|
|
8
|
+
this.client = axios.create({
|
|
9
|
+
baseURL,
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
Authorization: `Bearer ${context.auth.password}`,
|
|
13
|
+
'Heroku-Client': 'cli'
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
searchConnections (appName, resourceName, addonType) {
|
|
19
|
+
const resourceNameQueryParam = resourceName ? `&resource_name=${resourceName}` : ''
|
|
20
|
+
const addonTypeQueryParam = addonType ? `&addon_type=${addonType}` : ''
|
|
21
|
+
const url = `/connections?app=${appName}${resourceNameQueryParam}${addonTypeQueryParam}`
|
|
22
|
+
return this.client(url)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
requestAppAccess (appName, addonType) {
|
|
26
|
+
const addonTypeQueryParam = addonType ? `?addon_type=${addonType}` : ''
|
|
27
|
+
const url = `/auth/${appName}${addonTypeQueryParam}`
|
|
28
|
+
return this.client({ url: url, method: 'POST' })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.DiscoveryClient = DiscoveryClient
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const cli = require('@heroku/heroku-cli-util')
|
|
3
|
+
const co = require('co')
|
|
4
|
+
const { ConnectClient } = require('../clients/connect')
|
|
5
|
+
const { DiscoveryClient } = require('../clients/discovery')
|
|
6
|
+
|
|
7
|
+
exports.ADDON_TYPE_SYNC = 1
|
|
8
|
+
exports.ADDON_TYPE_EVENTS = 2
|
|
9
|
+
|
|
10
|
+
const request = exports.request = function (context, method, url, data) {
|
|
11
|
+
if (!context.region) {
|
|
12
|
+
throw new Error('Must provide region URL')
|
|
13
|
+
}
|
|
14
|
+
const connectClient = new ConnectClient(context)
|
|
15
|
+
return connectClient.request({
|
|
16
|
+
baseURL: context.region,
|
|
17
|
+
method,
|
|
18
|
+
url,
|
|
19
|
+
data
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const withUserConnections = exports.withUserConnections = co.wrap(function * (context, appName, flags, allowNone, heroku, addonType) {
|
|
24
|
+
const connectClient = new ConnectClient(context)
|
|
25
|
+
const discoveryClient = new DiscoveryClient(context)
|
|
26
|
+
|
|
27
|
+
const searchResponse = yield discoveryClient.searchConnections(appName, context.flags.resource, addonType)
|
|
28
|
+
const connections = searchResponse.data.results
|
|
29
|
+
|
|
30
|
+
if (connections.length === 0) {
|
|
31
|
+
return yield Promise.resolve([])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fetchConnectionDetailFuncs = connections.map(function (c) {
|
|
35
|
+
return co(function * () {
|
|
36
|
+
const response = yield connectClient.getDetails(c)
|
|
37
|
+
const mergedDetails = {
|
|
38
|
+
...c,
|
|
39
|
+
...response.data
|
|
40
|
+
}
|
|
41
|
+
return yield Promise.resolve(mergedDetails)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const aggregatedConnectionsData = yield Promise.all(fetchConnectionDetailFuncs)
|
|
46
|
+
return yield Promise.resolve(aggregatedConnectionsData)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const withConnection = exports.withConnection = function (context, heroku, addonType) {
|
|
50
|
+
return co(function * () {
|
|
51
|
+
const connectClient = new ConnectClient(context)
|
|
52
|
+
const discoveryClient = new DiscoveryClient(context)
|
|
53
|
+
|
|
54
|
+
const searchResponse = yield discoveryClient.searchConnections(context.app, context.flags.resource, addonType)
|
|
55
|
+
const connections = searchResponse.data.results
|
|
56
|
+
|
|
57
|
+
if (connections.length === 0) {
|
|
58
|
+
yield Promise.reject(Error('No connection(s) found'))
|
|
59
|
+
} else if (connections.length > 1) {
|
|
60
|
+
throw new Error("Multiple connections found. Please use '--resource' to specify a single connection by resource name. Use 'connect:info' to list the resource names.")
|
|
61
|
+
} else {
|
|
62
|
+
const match = connections[0]
|
|
63
|
+
const matchDetailResponse = yield connectClient.getDetails(match)
|
|
64
|
+
const matchWithDetails = {
|
|
65
|
+
...match,
|
|
66
|
+
...matchDetailResponse.data
|
|
67
|
+
}
|
|
68
|
+
return yield Promise.resolve(matchWithDetails)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const withMapping = exports.withMapping = function (connection, objectName) {
|
|
74
|
+
return co(function * () {
|
|
75
|
+
const objectNameLower = objectName.toLowerCase()
|
|
76
|
+
let mapping
|
|
77
|
+
connection.mappings.forEach(function (m) {
|
|
78
|
+
if (m.object_name.toLowerCase() === objectNameLower) {
|
|
79
|
+
mapping = m
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
if (mapping !== undefined) {
|
|
83
|
+
return yield Promise.resolve(mapping)
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(`No mapping configured for ${objectName}`)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
exports.withStream = function (context, connection, objectName) {
|
|
91
|
+
return co(function * () {
|
|
92
|
+
if (!connection.streams) {
|
|
93
|
+
connection.streams = yield getStreams(context, connection)
|
|
94
|
+
}
|
|
95
|
+
const objectNameLower = objectName.toLowerCase()
|
|
96
|
+
let stream
|
|
97
|
+
connection.streams.forEach(function (s) {
|
|
98
|
+
if (s.object_name.toLowerCase() === objectNameLower) {
|
|
99
|
+
stream = s
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
if (stream !== undefined) {
|
|
103
|
+
return yield Promise.resolve(stream)
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(`No stream configured for ${objectName}`)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const getStreams = exports.getStreams = function (context, connection) {
|
|
111
|
+
return co(function * () {
|
|
112
|
+
const connectClient = new ConnectClient(context)
|
|
113
|
+
const response = yield connectClient.request({
|
|
114
|
+
baseURL: connection.region_url,
|
|
115
|
+
method: 'GET',
|
|
116
|
+
url: `${connection.detail_url}/streams`,
|
|
117
|
+
data: null
|
|
118
|
+
})
|
|
119
|
+
const streams = response.data.results
|
|
120
|
+
return yield Promise.resolve(streams)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
exports.withStreams = function (context, connections) {
|
|
125
|
+
return co(function * () {
|
|
126
|
+
const fetchConnectionStreams = connections.map(function (c) {
|
|
127
|
+
return co(function * () {
|
|
128
|
+
c.streams = yield getStreams(context, c)
|
|
129
|
+
return yield Promise.resolve(c)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
const aggregatedConnectionsResponse = yield Promise.all(fetchConnectionStreams)
|
|
133
|
+
return yield Promise.resolve(aggregatedConnectionsResponse)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
exports.requestAppAccess = function (context, app, flags, allowNone, heroku, addonType) {
|
|
138
|
+
return co(function * () {
|
|
139
|
+
const discoveryClient = new DiscoveryClient(context)
|
|
140
|
+
const response = yield discoveryClient.requestAppAccess(app, addonType)
|
|
141
|
+
yield Promise.resolve(response.json)
|
|
142
|
+
return yield withUserConnections(context, app, flags, allowNone, heroku, addonType)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
exports.getWriteErrors = co.wrap(function * (context, heroku) {
|
|
147
|
+
let url, action
|
|
148
|
+
const mappingName = context.args.name
|
|
149
|
+
const connection = yield withConnection(context, heroku)
|
|
150
|
+
context.region = connection.region_url
|
|
151
|
+
if (!mappingName) {
|
|
152
|
+
url = `/api/v3/connections/${connection.id}/errors`
|
|
153
|
+
action = `Retrieving write errors for ${connection.name}`
|
|
154
|
+
} else {
|
|
155
|
+
const mapping = yield withMapping(connection, mappingName)
|
|
156
|
+
url = `/api/v3/mappings/${mapping.id}/errors`
|
|
157
|
+
action = `Retrieving write errors for ${mappingName} on ${connection.name}`
|
|
158
|
+
}
|
|
159
|
+
const results = yield cli.action(action, co(function * () {
|
|
160
|
+
return yield request(context, 'GET', url)
|
|
161
|
+
}))
|
|
162
|
+
const errors = results.data
|
|
163
|
+
|
|
164
|
+
if (errors.count === 0) {
|
|
165
|
+
cli.log(cli.color.green('No write errors in the last 24 hours'))
|
|
166
|
+
} else {
|
|
167
|
+
if (context.flags.json) {
|
|
168
|
+
cli.styledJSON(errors.results)
|
|
169
|
+
} else {
|
|
170
|
+
cli.table(errors.results, {
|
|
171
|
+
columns: [
|
|
172
|
+
{ key: 'id', label: 'Trigger Log ID' },
|
|
173
|
+
{ key: 'table_name', label: 'Table Name' },
|
|
174
|
+
{ key: 'record_id', label: 'Table ID' },
|
|
175
|
+
{ key: 'message', label: 'Error Message' },
|
|
176
|
+
{ key: 'created_at', label: 'Created' }
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heroku-cli/heroku-connect-plugin",
|
|
3
|
+
"description": "Heroku Connect plugin for Heroku CLI",
|
|
4
|
+
"version": "0.11.0",
|
|
5
|
+
"author": "Heroku",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/heroku/heroku-connect-plugin.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/heroku/heroku-connect-plugin/issues"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"heroku-plugin"
|
|
15
|
+
],
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"main": "index.js",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "CONNECT_ADDON=connectqa mocha && standard",
|
|
20
|
+
"style-fix": "standard --fix"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"/index.js",
|
|
24
|
+
"/lib",
|
|
25
|
+
"/commands"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@heroku/heroku-cli-util": "^8.0.14",
|
|
29
|
+
"axios": "^0.27.2",
|
|
30
|
+
"co": "4.6.0",
|
|
31
|
+
"heroku-client": "^3.0.6",
|
|
32
|
+
"inquirer": "^5.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"mocha": "^10.7.0",
|
|
36
|
+
"mockdate": "^2.0.1",
|
|
37
|
+
"nock": "^13.5.4",
|
|
38
|
+
"sinon": "^18.0.0",
|
|
39
|
+
"standard": "^16.0.4",
|
|
40
|
+
"unexpected": "^12.0.5"
|
|
41
|
+
}
|
|
42
|
+
}
|