UNPKG

23.5 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2018, salesforce.com, inc.
4 * All rights reserved.
5 * SPDX-License-Identifier: BSD-3-Clause
6 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9const kit_1 = require("@salesforce/kit");
10const ts_types_1 = require("@salesforce/ts-types");
11const path_1 = require("path");
12const authInfo_1 = require("./authInfo");
13const aliases_1 = require("./config/aliases");
14const authInfoConfig_1 = require("./config/authInfoConfig");
15const config_1 = require("./config/config");
16const configAggregator_1 = require("./config/configAggregator");
17const orgUsersConfig_1 = require("./config/orgUsersConfig");
18const sandboxOrgConfig_1 = require("./config/sandboxOrgConfig");
19const connection_1 = require("./connection");
20const global_1 = require("./global");
21const logger_1 = require("./logger");
22const sfdxError_1 = require("./sfdxError");
23const fs_1 = require("./util/fs");
24const sfdc_1 = require("./util/sfdc");
25/**
26 * Provides a way to manage a locally authenticated Org.
27 *
28 * **See** {@link AuthInfo}
29 *
30 * **See** {@link Connection}
31 *
32 * **See** {@link Aliases}
33 *
34 * **See** {@link Config}
35 *
36 * ```
37 * // Email username
38 * const org1: Org = await Org.create({ aliasOrUsername: 'foo@example.com' });
39 * // The defaultusername config property
40 * const org2: Org = await Org.create({});
41 * // Full Connection
42 * const org3: Org = await Org.create({
43 * connection: await Connection.create({
44 * authInfo: await AuthInfo.create({ username: 'username' })
45 * })
46 * });
47 * ```
48 *
49 * **See** https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm
50 */
51class Org extends kit_1.AsyncCreatable {
52 /**
53 * @ignore
54 */
55 constructor(options) {
56 super(options);
57 // tslint:disable-next-line:no-unused-variable
58 this.status = Org.Status.UNKNOWN;
59 this.options = options;
60 }
61 /**
62 * Clean all data files in the org's data path. Usually <workspace>/.sfdx/orgs/<username>.
63 * @param orgDataPath A relative path other than "orgs/".
64 * @param throwWhenRemoveFails Should the remove org operations throw an error on failure?
65 */
66 async cleanLocalOrgData(orgDataPath, throwWhenRemoveFails = false) {
67 let dataPath;
68 try {
69 const rootFolder = await config_1.Config.resolveRootFolder(false);
70 dataPath = path_1.join(rootFolder, global_1.Global.STATE_FOLDER, orgDataPath ? orgDataPath : 'orgs');
71 this.logger.debug(`cleaning data for path: ${dataPath}`);
72 }
73 catch (err) {
74 if (err.name === 'InvalidProjectWorkspace') {
75 // If we aren't in a project dir, we can't clean up data files.
76 // If the user unlink this org outside of the workspace they used it in,
77 // data files will be left over.
78 return;
79 }
80 throw err;
81 }
82 return this.manageDelete(async () => await fs_1.fs.remove(dataPath), dataPath, throwWhenRemoveFails);
83 }
84 /**
85 * @ignore
86 */
87 async retrieveOrgUsersConfig() {
88 return await orgUsersConfig_1.OrgUsersConfig.create(orgUsersConfig_1.OrgUsersConfig.getOptions(this.getOrgId()));
89 }
90 /**
91 * Removes the scratch org config file at $HOME/.sfdx/[name].json, any project level org
92 * files, all user auth files for the org, matching default config settings, and any
93 * matching aliases.
94 * @param throwWhenRemoveFails Determines if the call should throw an error or fail silently.
95 */
96 async remove(throwWhenRemoveFails = false) {
97 // If deleting via the access token there shouldn't be any auth config files
98 // so just return;
99 if (this.getConnection().isUsingAccessToken()) {
100 return Promise.resolve();
101 }
102 await this.removeSandboxConfig(throwWhenRemoveFails);
103 await this.removeUsers(throwWhenRemoveFails);
104 await this.removeUsersConfig();
105 // An attempt to remove this org's auth file occurs in this.removeUsersConfig. That's because this org's usersname is also
106 // included in the OrgUser config file.
107 //
108 // So, just in case no users are added to this org we will try the remove again.
109 await this.removeAuth();
110 }
111 /**
112 * Check that this org is a scratch org by asking the dev hub if it knows about it.
113 *
114 * **Throws** *{@link SfdxError}{ name: 'NotADevHub' }* Not a Dev Hub.
115 *
116 * **Throws** *{@link SfdxError}{ name: 'NoResults' }* No results.
117 *
118 * @param devHubUsernameOrAlias The username or alias of the dev hub org.
119 */
120 async checkScratchOrg(devHubUsernameOrAlias) {
121 let aliasOrUsername = devHubUsernameOrAlias;
122 if (!aliasOrUsername) {
123 aliasOrUsername = ts_types_1.asString(this.configAggregator.getPropertyValue(config_1.Config.DEFAULT_DEV_HUB_USERNAME));
124 }
125 const devHubConnection = (await Org.create({ aliasOrUsername })).getConnection();
126 const thisOrgAuthConfig = this.getConnection().getAuthInfoFields();
127 const trimmedId = sfdc_1.sfdc.trimTo15(thisOrgAuthConfig.orgId);
128 const DEV_HUB_SOQL = `SELECT CreatedDate,Edition,ExpirationDate FROM ActiveScratchOrg WHERE ScratchOrg=\'${trimmedId}\'`;
129 let results;
130 try {
131 results = await devHubConnection.query(DEV_HUB_SOQL);
132 }
133 catch (err) {
134 if (err.name === 'INVALID_TYPE') {
135 throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'NotADevHub', [devHubConnection.getUsername()]);
136 }
137 throw err;
138 }
139 if (ts_types_1.getNumber(results, 'records.length') !== 1) {
140 throw new sfdxError_1.SfdxError('No results', 'NoResults');
141 }
142 return thisOrgAuthConfig;
143 }
144 /**
145 * Returns the Org object or null if this org is not affiliated with a Dev Hub (according to the local config).
146 */
147 async getDevHubOrg() {
148 if (this.isDevHubOrg()) {
149 return this;
150 }
151 else if (this.getField(Org.Fields.DEV_HUB_USERNAME)) {
152 const devHubUsername = ts_types_1.ensureString(this.getField(Org.Fields.DEV_HUB_USERNAME));
153 return Org.create({
154 connection: await connection_1.Connection.create({
155 authInfo: await authInfo_1.AuthInfo.create({ username: devHubUsername })
156 })
157 });
158 }
159 }
160 /**
161 * Returns `true` if the org is a Dev Hub.
162 *
163 * **Note** This relies on a cached value in the auth file. If that property
164 * is not cached, this method will **always return false even if the org is a
165 * dev hub**. If you need accuracy, use the {@link Org.determineIfDevHubOrg} method.
166 */
167 isDevHubOrg() {
168 const isDevHub = this.getField(Org.Fields.IS_DEV_HUB);
169 if (ts_types_1.isBoolean(isDevHub)) {
170 return isDevHub;
171 }
172 else {
173 return false;
174 }
175 }
176 /**
177 * Returns `true` if the org is a Dev Hub.
178 *
179 * Use a cached value. If the cached value is not set, then check access to the
180 * ScratchOrgInfo object to determine if the org is a dev hub.
181 *
182 * @param forceServerCheck Ignore the cached value and go straight to the server
183 * which will be required if the org flips on the dev hub after the value is already
184 * cached locally.
185 */
186 async determineIfDevHubOrg(forceServerCheck = false) {
187 const cachedIsDevHub = this.getField(Org.Fields.IS_DEV_HUB);
188 if (!forceServerCheck && ts_types_1.isBoolean(cachedIsDevHub)) {
189 return cachedIsDevHub;
190 }
191 if (this.isDevHubOrg()) {
192 return true;
193 }
194 this.logger.debug('isDevHub is not cached - querying server...');
195 const conn = this.getConnection();
196 let isDevHub = false;
197 try {
198 await conn.query('SELECT Id FROM ScratchOrgInfo');
199 isDevHub = true;
200 }
201 catch (err) {
202 /* Not a dev hub */
203 }
204 const username = ts_types_1.ensure(this.getUsername());
205 const auth = await authInfo_1.AuthInfo.create({ username });
206 await auth.save({ isDevHub });
207 authInfo_1.AuthInfo.clearCache(username);
208 // Reset the connection with the updated auth file
209 this.connection = await connection_1.Connection.create({
210 authInfo: await authInfo_1.AuthInfo.create({ username })
211 });
212 return isDevHub;
213 }
214 /**
215 * Refreshes the auth for this org's instance by calling HTTP GET on the baseUrl of the connection object.
216 */
217 async refreshAuth() {
218 this.logger.debug('Refreshing auth for org.');
219 const requestInfo = {
220 url: this.getConnection().baseUrl(),
221 method: 'GET'
222 };
223 const conn = this.getConnection();
224 await conn.request(requestInfo);
225 }
226 /**
227 * Reads and returns the content of all user auth files for this org as an array.
228 */
229 async readUserAuthFiles() {
230 const config = await this.retrieveOrgUsersConfig();
231 const contents = await config.read();
232 const thisUsername = ts_types_1.ensure(this.getUsername());
233 const usernames = ts_types_1.ensureJsonArray(contents.usernames || [thisUsername]);
234 return Promise.all(usernames.map(username => {
235 if (username === thisUsername) {
236 return authInfo_1.AuthInfo.create({
237 username: this.getConnection().getUsername()
238 });
239 }
240 else {
241 return authInfo_1.AuthInfo.create({ username: ts_types_1.ensureString(username) });
242 }
243 }));
244 }
245 /**
246 * Adds a username to the user config for this org. For convenience `this` object is returned.
247 *
248 * ```
249 * const org: Org = await Org.create({
250 * connection: await Connection.create({
251 * authInfo: await AuthInfo.create('foo@example.com')
252 * })
253 * });
254 * const userAuth: AuthInfo = await AuthInfo.create({
255 * username: 'bar@example.com'
256 * });
257 * await org.addUsername(userAuth);
258 * ```
259 *
260 * @param {AuthInfo | string} auth The AuthInfo for the username to add.
261 */
262 async addUsername(auth) {
263 if (!auth) {
264 throw new sfdxError_1.SfdxError('Missing auth info', 'MissingAuthInfo');
265 }
266 const _auth = ts_types_1.isString(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth;
267 this.logger.debug(`adding username ${_auth.getFields().username}`);
268 const orgConfig = await this.retrieveOrgUsersConfig();
269 const contents = await orgConfig.read();
270 // TODO: This is kind of screwy because contents values can be `AnyJson | object`...
271 // needs config refactoring to improve
272 const usernames = contents.usernames || [];
273 if (!ts_types_1.isArray(usernames)) {
274 throw new sfdxError_1.SfdxError('Usernames is not an array', 'UnexpectedDataFormat');
275 }
276 let shouldUpdate = false;
277 const thisUsername = ts_types_1.ensure(this.getUsername());
278 if (!usernames.includes(thisUsername)) {
279 usernames.push(thisUsername);
280 shouldUpdate = true;
281 }
282 const username = _auth.getFields().username;
283 if (username) {
284 usernames.push(username);
285 shouldUpdate = true;
286 }
287 if (shouldUpdate) {
288 orgConfig.set('usernames', usernames);
289 await orgConfig.write();
290 }
291 return this;
292 }
293 /**
294 * Removes a username from the user config for this object. For convenience `this` object is returned.
295 *
296 * **Throws** *{@link SfdxError}{ name: 'MissingAuthInfo' }* Auth info is missing.
297 *
298 * @param {AuthInfo | string} auth The AuthInfo containing the username to remove.
299 */
300 async removeUsername(auth) {
301 if (!auth) {
302 throw new sfdxError_1.SfdxError('Missing auth info', 'MissingAuthInfo');
303 }
304 const _auth = ts_types_1.isString(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth;
305 this.logger.debug(`removing username ${_auth.getFields().username}`);
306 const orgConfig = await this.retrieveOrgUsersConfig();
307 const contents = await orgConfig.read();
308 const targetUser = _auth.getFields().username;
309 const usernames = (contents.usernames || []);
310 contents.usernames = usernames.filter(username => username !== targetUser);
311 await orgConfig.write();
312 return this;
313 }
314 /**
315 * Sets the key/value pair in the sandbox config for this org. For convenience `this` object is returned.
316 *
317 *
318 * @param {key} The key for this value
319 * @param {value} The value to save
320 */
321 async setSandboxOrgConfigField(field, value) {
322 const sandboxOrgConfig = await this.retrieveSandboxOrgConfig();
323 sandboxOrgConfig.set(field, value);
324 await sandboxOrgConfig.write();
325 return this;
326 }
327 /**
328 * Returns an org config field. Returns undefined if the field is not set or invalid.
329 */
330 async getSandboxOrgConfigField(field) {
331 const sandboxOrgConfig = await this.retrieveSandboxOrgConfig();
332 return sandboxOrgConfig.get(field);
333 }
334 /**
335 * Retrieves the highest api version that is supported by the target server instance. If the apiVersion configured for
336 * Sfdx is greater than the one returned in this call an api version mismatch occurs. In the case of the CLI that
337 * results in a warning.
338 */
339 async retrieveMaxApiVersion() {
340 return await this.getConnection().retrieveMaxApiVersion();
341 }
342 /**
343 * Returns the admin username used to create the org.
344 */
345 getUsername() {
346 return this.getConnection().getUsername();
347 }
348 /**
349 * Returns the orgId for this org.
350 */
351 getOrgId() {
352 return this.getField(Org.Fields.ORG_ID);
353 }
354 /**
355 * Returns for the config aggregator.
356 */
357 getConfigAggregator() {
358 return this.configAggregator;
359 }
360 /**
361 * Returns an org field. Returns undefined if the field is not set or invalid.
362 */
363 getField(key) {
364 // @ts-ignore TODO: Need to refactor storage of these values on both Org and AuthFields
365 return this[key] || this.getConnection().getAuthInfoFields()[key];
366 }
367 /**
368 * Returns a map of requested fields.
369 */
370 getFields(keys) {
371 const json = {};
372 return keys.reduce((map, key) => {
373 map[key] = this.getField(key);
374 return map;
375 }, json);
376 }
377 /**
378 * Returns the JSForce connection for the org.
379 */
380 getConnection() {
381 return this.connection;
382 }
383 /**
384 * Initialize async components.
385 */
386 async init() {
387 this.logger = await logger_1.Logger.child('Org');
388 this.configAggregator = this.options.aggregator ? this.options.aggregator : await configAggregator_1.ConfigAggregator.create();
389 if (!this.options.connection) {
390 if (this.options.aliasOrUsername == null) {
391 this.configAggregator = this.getConfigAggregator();
392 const aliasOrUsername = this.options.isDevHub
393 ? ts_types_1.getString(this.configAggregator.getInfo(config_1.Config.DEFAULT_DEV_HUB_USERNAME), 'value')
394 : ts_types_1.getString(this.configAggregator.getInfo(config_1.Config.DEFAULT_USERNAME), 'value');
395 this.options.aliasOrUsername = aliasOrUsername || undefined;
396 }
397 const username = this.options.aliasOrUsername;
398 this.connection = await connection_1.Connection.create({
399 // If no username is provided or resolvable from an alias, AuthInfo will throw an SfdxError.
400 authInfo: await authInfo_1.AuthInfo.create({
401 username: (username != null && (await aliases_1.Aliases.fetch(username))) || username
402 })
403 });
404 }
405 else {
406 this.connection = this.options.connection;
407 }
408 }
409 /**
410 * **Throws** *{@link SfdxError} Throws and unsupported error.
411 */
412 getDefaultOptions() {
413 throw new sfdxError_1.SfdxError('Not Supported');
414 }
415 /**
416 * Returns a promise to delete an auth info file from the local file system and any related cache information for
417 * this Org.. You don't want to call this method directly. Instead consider calling Org.remove()
418 */
419 async removeAuth() {
420 const username = ts_types_1.ensure(this.getUsername());
421 this.logger.debug(`Removing auth for user: ${username}`);
422 const config = await authInfoConfig_1.AuthInfoConfig.create(Object.assign({}, authInfoConfig_1.AuthInfoConfig.getOptions(username), { throwOnNotFound: false }));
423 this.logger.debug(`Clearing auth cache for user: ${username}`);
424 authInfo_1.AuthInfo.clearCache(username);
425 if (await config.exists()) {
426 await config.unlink();
427 }
428 }
429 /**
430 * Deletes the users config file
431 */
432 async removeUsersConfig() {
433 const config = await this.retrieveOrgUsersConfig();
434 if (await config.exists()) {
435 this.logger.debug(`Removing org users config at: ${config.getPath()}`);
436 await config.unlink();
437 }
438 }
439 /**
440 * @ignore
441 */
442 async retrieveSandboxOrgConfig() {
443 return await sandboxOrgConfig_1.SandboxOrgConfig.create(sandboxOrgConfig_1.SandboxOrgConfig.getOptions(this.getOrgId()));
444 }
445 manageDelete(cb, dirPath, throwWhenRemoveFails) {
446 return cb().catch(e => {
447 if (throwWhenRemoveFails) {
448 throw e;
449 }
450 else {
451 this.logger.warn(`failed to read directory ${dirPath}`);
452 return;
453 }
454 });
455 }
456 /**
457 * Remove the org users auth file.
458 * @param throwWhenRemoveFails true if manageDelete should throw or not if the deleted fails.
459 */
460 async removeUsers(throwWhenRemoveFails) {
461 this.logger.debug(`Removing users associate with org: ${this.getOrgId()}`);
462 const config = await this.retrieveOrgUsersConfig();
463 this.logger.debug(`using path for org users: ${config.getPath()}`);
464 if (await config.exists()) {
465 const _auths = await this.readUserAuthFiles();
466 const aliases = await aliases_1.Aliases.create(aliases_1.Aliases.getDefaultOptions());
467 this.logger.info(`Cleaning up usernames in org: ${this.getOrgId()}`);
468 for (const auth of _auths) {
469 const username = auth.getFields().username;
470 const aliasKeys = (username && aliases.getKeysByValue(username)) || [];
471 aliases.unsetAll(aliasKeys);
472 let orgForUser;
473 if (username === this.getUsername()) {
474 orgForUser = this;
475 }
476 else {
477 const _info = await authInfo_1.AuthInfo.create({ username });
478 const connection = await connection_1.Connection.create({ authInfo: _info });
479 orgForUser = await Org.create({ connection });
480 }
481 const orgType = this.isDevHubOrg() ? config_1.Config.DEFAULT_DEV_HUB_USERNAME : config_1.Config.DEFAULT_USERNAME;
482 const configInfo = await orgForUser.configAggregator.getInfo(orgType);
483 if ((configInfo.value === username || aliasKeys.includes(configInfo.value)) &&
484 (configInfo.isGlobal() || configInfo.isLocal())) {
485 await config_1.Config.update(configInfo.isGlobal(), orgType, undefined);
486 }
487 await orgForUser.removeAuth();
488 }
489 await aliases.write();
490 }
491 }
492 /**
493 * Remove an associate sandbox config.
494 * @param throwWhenRemoveFails true if manageDelete should throw or not if the deleted fails.
495 */
496 async removeSandboxConfig(throwWhenRemoveFails) {
497 const sandboxOrgConfig = await this.retrieveSandboxOrgConfig();
498 if (await sandboxOrgConfig.exists()) {
499 await this.manageDelete(async () => await sandboxOrgConfig.unlink(), sandboxOrgConfig.getPath(), throwWhenRemoveFails);
500 }
501 }
502}
503exports.Org = Org;
504(function (Org) {
505 /**
506 * Scratch Org status.
507 */
508 let Status;
509 (function (Status) {
510 /**
511 * The scratch org is active.
512 */
513 Status["ACTIVE"] = "ACTIVE";
514 /**
515 * The scratch org has expired.
516 */
517 Status["EXPIRED"] = "EXPIRED";
518 /**
519 * The org is a scratch Org but no dev hub is indicated.
520 */
521 Status["UNKNOWN"] = "UNKNOWN";
522 /**
523 * The dev hub configuration is reporting an active Scratch org but the AuthInfo cannot be found.
524 */
525 Status["MISSING"] = "MISSING";
526 })(Status = Org.Status || (Org.Status = {}));
527 /**
528 * Org Fields.
529 */
530 // A subset of fields from AuthInfoFields and properties that are specific to Org,
531 // and properties that are defined on Org itself.
532 let Fields;
533 (function (Fields) {
534 /**
535 * The org alias.
536 */
537 // From AuthInfo
538 Fields["ALIAS"] = "alias";
539 Fields["CREATED"] = "created";
540 /**
541 * The Salesforce instance the org was created on. e.g. `cs42`.
542 */
543 Fields["CREATED_ORG_INSTANCE"] = "createdOrgInstance";
544 /**
545 * The username of the dev hub org that created this org. Only populated for scratch orgs.
546 */
547 Fields["DEV_HUB_USERNAME"] = "devHubUsername";
548 /**
549 * The full url of the instance the org lives on.
550 */
551 Fields["INSTANCE_URL"] = "instanceUrl";
552 /**
553 * Is the current org a dev hub org. e.g. They have access to the `ScratchOrgInfo` object.
554 */
555 Fields["IS_DEV_HUB"] = "isDevHub";
556 /**
557 * The login url of the org. e.g. `https://login.salesforce.com` or `https://test.salesforce.com`.
558 */
559 Fields["LOGIN_URL"] = "loginUrl";
560 /**
561 * The org ID.
562 */
563 Fields["ORG_ID"] = "orgId";
564 /**
565 * The `OrgStatus` of the org.
566 */
567 Fields["STATUS"] = "status";
568 /**
569 * The snapshot used to create the scratch org.
570 */
571 Fields["SNAPSHOT"] = "snapshot";
572 // Should it be on org? Leave it off for now, as it might
573 // be confusing to the consumer what this actually is.
574 // USERNAMES = 'usernames',
575 // Keep separation of concerns. I think these should be on a "user" that belongs to the org.
576 // Org can have a list of user objects that belong to it? Should connection be on user and org.getConnection()
577 // gets the orgs current user for the process? Maybe we just want to keep with the Org only model for
578 // the end of time?
579 // USER_ID = 'userId',
580 // USERNAME = 'username',
581 // PASSWORD = 'password',
582 // USER_PROFILE_NAME = 'userProfileName'
583 })(Fields = Org.Fields || (Org.Fields = {}));
584})(Org = exports.Org || (exports.Org = {}));
585//# sourceMappingURL=org.js.map
\No newline at end of file