-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathexternalobjects.ts
190 lines (163 loc) · 7.65 KB
/
externalobjects.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { flags, SfdxCommand } from '@salesforce/command';
import * as fs from 'fs-extra';
import * as puppeteer from 'puppeteer';
import { getMatchingApp, patchApp, defaultHerokuRequest, credentialParser } from '../../../shared/herokuConnectApi';
import { checkHerokuEnvironmentVariables } from '../../../shared/herokuCheck';
import { writeJSONasXML } from '@mshanemc/plugin-helpers/dist/JSONXMLtools';
import { getExisting } from '@mshanemc/plugin-helpers/dist/getExisting';
import { herokuAppNameValidator } from '../../../shared/flagParsing';
import request = require('request-promise-native');
const metadataTypeName = 'ExternalDataSource';
export default class HerokuExternalObjects extends SfdxCommand {
public static description = 'set up heroku connect on an existing app with external objects';
public static examples = [
`sfdx shane:heroku:externalobjects -a sneaky-platypus
// enables external objects on all tables
`,
`sfdx shane:heroku:externalobjects -a sneak-platypus -t corgis -c force-app/main/default/dataSources -l theDataSource
// enables external objects on the postgres table called corgis and creates an external data source locally
`,
`sfdx shane:heroku:externalobjects -a sneak-platypus -f force-app/main/default/dataSources/existingXDS.dataSource-meta.xml
// enables external objects on all tables and modifies the local file specified
`
];
protected static supportsUsername = true;
protected static flagsConfig = {
app: flags.string({
char: 'a',
description: 'name of the heroku app',
required: true,
parse: input => herokuAppNameValidator(input)
}),
tables: flags.array({
char: 't',
description: 'comma separated list of postgres table names to share. If omitted, you want them all!'
}),
createdir: flags.directory({
char: 'c',
description: 'creates an external data source in the chosen directory',
exclusive: ['updatefile']
}),
updatefile: flags.filepath({
char: 'f',
description: 'updates an existing external data source with username/password/url',
exclusive: ['createdir']
}),
label: flags.string({
char: 'l',
description: 'label that will appear for the external data source you create',
exclusive: ['updatefile']
}),
showbrowser: flags.boolean({ char: 'b', description: 'show the browser...useful for local debugging' }),
verbose: flags.builtin()
};
public async run(): Promise<any> {
// validations
checkHerokuEnvironmentVariables();
if (this.flags.createdir) {
await fs.ensureDir(this.flags.createdir);
}
if (this.flags.updatefile && !fs.existsSync(this.flags.updatefile)) {
throw new Error(`file not found ${this.flags.updatefile}`);
}
const logVerbosely = !this.flags.json && this.flags.verbose;
// actual API work
const matchingApp = await getMatchingApp(this.flags.app, logVerbosely);
await patchApp(matchingApp, logVerbosely);
this.ux.setSpinnerStatus('logging into heroku');
const browser = await puppeteer.launch({
headless: !this.flags.showbrowser,
args: ['--no-sandbox', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process']
});
const page = await browser.newPage();
await page.goto(`https://connect.heroku.com/sync/${matchingApp.id}/external`, {
waitUntil: 'networkidle2'
});
// heroku login page pops up instead
await page.waitForSelector('input#email');
await page.type('input#email', process.env.HEROKU_USERNAME);
await page.type('input#password', process.env.HEROKU_PASSWORD);
await page.click('button');
await page.waitFor(3000);
// actual XO page
this.ux.setSpinnerStatus('creating credentials');
const createCredentialsSelector = '.btn.btn-default';
await page.waitForSelector(createCredentialsSelector);
await page.click(createCredentialsSelector);
this.ux.setSpinnerStatus('getting odata url from API');
// calculate the odata url
const matchingAppDetails = await request.get({
...defaultHerokuRequest,
uri: matchingApp.detail_url
});
this.ux.setSpinnerStatus('getting the credentials');
// show the credentials
const showCredentialsSelector = 'div.hc-external-objects > div > p:nth-child(3) > button:nth-child(1)';
await page.waitFor(showCredentialsSelector);
await page.click(showCredentialsSelector);
const odataUrl = `${matchingApp.region_url}/odata/v4/${matchingAppDetails.odata.slug}`;
this.ux.log(`odata url: ${odataUrl}`);
const credentialsSelector = 'div.hc-external-objects > div > p:nth-child(4)';
await page.waitForSelector(credentialsSelector);
const credentialsBlock = await page.$eval(credentialsSelector, el => el.textContent);
const finalOutput = {
...credentialParser(credentialsBlock),
endpoint: odataUrl
};
this.ux.setSpinnerStatus('setting the tables');
const dataSourceTableSelector = '#sync-content-inner > div > div.hc-external-objects > div > table:nth-child(10) > tbody > tr';
await page.waitForSelector(dataSourceTableSelector);
const tableBodyRows = await page.$$(dataSourceTableSelector);
// iterate each row. If the name column matches the tables flag OR there isn't a tables flag, then check the box
// eslint-disable-next-line no-restricted-syntax
for (const row of tableBodyRows) {
await page.waitFor(2000);
const databaseTableName = await row.$eval('td:nth-child(2)', el => el.textContent);
if (!this.flags.tables || this.flags.tables.includes(databaseTableName)) {
await (await row.$('td input')).click();
}
}
if (!this.flags.showbrowser) {
await page.waitFor(2000);
await browser.close();
}
this.ux.stopSpinner();
if (logVerbosely) {
this.ux.logJson(finalOutput);
}
if (this.flags.createdir) {
// create the external data source
// https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_externaldatasource.htm
const path = `${this.flags.createdir}/${this.flags.label.replace(/ /g, '')}.dataSource-meta.xml`;
await writeJSONasXML({
path,
type: metadataTypeName,
json: {
'@': {
xmlns: 'http://soap.sforce.com/2006/04/metadata'
},
isWritable: true,
principalType: 'NamedUser',
label: this.flags.label,
protocol: 'Password',
type: 'OData4',
...finalOutput
}
});
this.ux.log(`created new data source at ${path}`);
}
if (this.flags.updatefile) {
const existing = await getExisting(this.flags.updatefile, metadataTypeName);
await writeJSONasXML({
path: this.flags.updatefile,
type: metadataTypeName,
json: {
...existing,
...finalOutput
}
});
this.ux.log(`updated ${this.flags.updatefile}`);
}
return finalOutput;
}
}