@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.
@@ -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
+ }