@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
354 lines • 12.7 kB
JavaScript
"use strict";
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseConfigStore = void 0;
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const ts_types_2 = require("@salesforce/ts-types");
const crypto_1 = require("../crypto/crypto");
const sfError_1 = require("../sfError");
const time_1 = require("../util/time");
const lwwMap_1 = require("./lwwMap");
/**
* An abstract class that implements all the config management functions but
* none of the storage functions.
*
* **Note:** To see the interface, look in typescripts autocomplete help or the npm package's ConfigStore.d.ts file.
*/
class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
// If encryptedKeys is an array of RegExps, they should not contain the /g (global) or /y (sticky) flags to avoid stateful issues.
static encryptedKeys = [];
options;
crypto;
// Initialized in setContents
contents = new lwwMap_1.LWWMap();
statics = this.constructor;
/**
* Constructor.
*
* @param options The options for the class instance.
* @ignore
*/
constructor(options) {
super(options);
this.options = options ?? {};
this.setContents(this.initialContents());
}
/**
* Returns an array of {@link ConfigEntry} for each element in the config.
*/
entries() {
return (0, ts_types_2.definiteEntriesOf)(this.contents.value ?? {});
}
// NEXT_RELEASE: update types to specify return can be | undefined
get(key, decrypt = false) {
const rawValue = this.contents.get(key);
if (this.hasEncryption() && decrypt) {
if ((0, ts_types_2.isJsonMap)(rawValue)) {
return this.recursiveDecrypt(structuredClone(rawValue), key);
}
else if (this.isCryptoKey(key)) {
return this.decrypt(rawValue);
}
}
// NEXT_RELEASE: update types to specify return can be | undefined
return rawValue;
}
/**
* Returns the list of keys that contain a value.
*
* @param value The value to filter keys on.
*/
getKeysByValue(value) {
const matchedEntries = this.entries().filter((entry) => entry[1] === value);
// Only return the keys
return matchedEntries.map((entry) => entry[0]);
}
/**
* Returns a boolean asserting whether a value has been associated to the key in the config object or not.
*
*/
has(key) {
return this.contents.has(key) ?? false;
}
/**
* Returns an array that contains the keys for each element in the config object.
*/
keys() {
return Object.keys(this.contents.value ?? {});
}
/**
* Sets the value for the key in the config object. This will override the existing value.
* To do a partial update, use {@link BaseConfigStore.update}.
*
* @param key The key.
* @param value The value.
*/
set(key, value) {
let resolvedValue = value;
if (this.hasEncryption()) {
if ((0, ts_types_2.isJsonMap)(resolvedValue)) {
resolvedValue = this.recursiveEncrypt(resolvedValue, key);
}
else if (this.isCryptoKey(key)) {
resolvedValue = this.encrypt(resolvedValue);
}
}
// set(key, undefined) means unset
if (resolvedValue === undefined) {
this.unset(key);
}
else {
this.contents.set(key, resolvedValue);
}
}
/**
* Updates the value for the key in the config object. If the value is an object, it
* will be merged with the existing object.
*
* @param key The key.
* @param value The value.
*/
update(key, value) {
const existingValue = this.get(key, true);
if ((0, ts_types_1.isPlainObject)(existingValue) && (0, ts_types_1.isPlainObject)(value)) {
const mergedValue = Object.assign({}, existingValue, value);
this.set(key, mergedValue);
}
else {
this.set(key, value);
}
}
/**
* Returns `true` if an element in the config object existed and has been removed, or `false` if the element does not
* exist. {@link BaseConfigStore.has} will return false afterwards.
*
* @param key The key
*/
unset(key) {
if (this.has(key)) {
this.contents.delete(key);
return true;
}
return false;
}
/**
* Returns `true` if all elements in the config object existed and have been removed, or `false` if all the elements
* do not exist (some may have been removed). {@link BaseConfigStore.has(key)} will return false afterwards.
*
* @param keys The keys
*/
unsetAll(keys) {
return keys.map((key) => this.unset(key)).every(Boolean);
}
/**
* Removes all key/value pairs from the config object.
*/
clear() {
this.keys().map((key) => this.unset(key));
}
/**
* Returns an array that contains the values for each element in the config object.
*/
values() {
return (0, ts_types_2.definiteValuesOf)(this.contents.value ?? {});
}
/**
* Returns the entire config contents.
*
* *NOTE:* Data will still be encrypted unless decrypt is passed in. A clone of
* the data will be returned to prevent storing un-encrypted data in memory and
* potentially saving to the file system.
*
* @param decrypt: decrypt all data in the config. A clone of the data will be returned.
*
*/
getContents(decrypt = false) {
if (this.hasEncryption() && decrypt) {
return this.recursiveDecrypt(structuredClone(this.contents?.value ?? {}));
}
return this.contents?.value ?? {};
}
/**
* Invokes `actionFn` once for each key-value pair present in the config object.
*
* @param {function} actionFn The function `(key: string, value: ConfigValue) => void` to be called for each element.
*/
forEach(actionFn) {
this.entries().map((entry) => actionFn(entry[0], entry[1]));
}
/**
* Convert the config object to a JSON object. Returns the config contents.
* Same as calling {@link ConfigStore.getContents}
*/
toObject() {
return this.contents.value ?? {};
}
/**
* Convert an object to a {@link ConfigContents} and set it as the config contents.
*
* @param obj The object.
*/
setContentsFromObject(obj) {
const objForWrite = this.hasEncryption() ? this.recursiveEncrypt(obj) : obj;
(0, ts_types_1.entriesOf)(objForWrite).map(([key, value]) => {
this.set(key, value);
});
}
/**
* Keep ConfigFile concurrency-friendly.
* Avoid using this unless you're reading the file for the first time
* and guaranteed to no be cross-saving existing contents
* */
setContentsFromFileContents(contents, timestamp) {
const state = (0, lwwMap_1.stateFromContents)(contents, timestamp ?? (0, time_1.nowBigInt)());
this.contents = new lwwMap_1.LWWMap(state);
}
/**
* Sets the entire config contents.
*
* @param contents The contents.
*/
setContents(contents = {}) {
const maybeEncryptedContents = this.hasEncryption() ? this.recursiveEncrypt(contents) : contents;
(0, ts_types_1.entriesOf)(maybeEncryptedContents).map(([key, value]) => {
this.contents.set(key, value);
});
}
getEncryptedKeys() {
return [...(this.options?.encryptedKeys ?? []), ...(this.statics?.encryptedKeys ?? [])];
}
/**
* This config file has encrypted keys and it should attempt to encrypt them.
*
* @returns Has encrypted keys
*/
hasEncryption() {
return this.getEncryptedKeys().length > 0;
}
// eslint-disable-next-line class-methods-use-this
initialContents() {
return {};
}
/**
* Used to initialize asynchronous components.
*/
async init() {
if (this.hasEncryption()) {
await this.initCrypto();
}
}
/**
* Initialize the crypto dependency.
*/
async initCrypto() {
if (!this.crypto) {
this.crypto = await crypto_1.Crypto.create();
}
}
/**
* Closes the crypto dependency. Crypto should be close after it's used and no longer needed.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async clearCrypto() {
if (this.crypto) {
this.crypto.close();
delete this.crypto;
}
}
/**
* Should the given key be encrypted on set methods and decrypted on get methods.
*
* @param key The key. Supports query key like `a.b[0]`.
* @returns Should encrypt/decrypt
*/
isCryptoKey(key) {
function resolveProperty() {
// Handle query keys
const dotAccessor = /\.([a-zA-Z0-9@._-]+)$/;
const singleQuoteAccessor = /\['([a-zA-Z0-9@._-]+)'\]$/;
const doubleQuoteAccessor = /\["([a-zA-Z0-9@._-]+)"\]$/;
const matcher = dotAccessor.exec(key) ?? singleQuoteAccessor.exec(key) ?? doubleQuoteAccessor.exec(key);
return matcher ? matcher[1] : key;
}
// Any keys named the following should be encrypted/decrypted
return (this.statics.encryptedKeys || []).find((keyOrExp) => {
const property = resolveProperty();
if (keyOrExp instanceof RegExp) {
return keyOrExp.test(property);
}
else {
return keyOrExp === property;
}
});
}
encrypt(value) {
if (!value)
return;
if (!this.crypto)
throw new sfError_1.SfError('crypto is not initialized', 'CryptoNotInitializedError');
if (!(0, ts_types_2.isString)(value))
throw new sfError_1.SfError(`can only encrypt strings but found: ${typeof value} : ${JSON.stringify(value)}`, 'InvalidCryptoValueError');
return this.crypto.isEncrypted(value) ? value : this.crypto.encrypt(value);
}
decrypt(value) {
if (!value)
return;
if (!this.crypto)
throw new sfError_1.SfError('crypto is not initialized', 'CryptoNotInitializedError');
if (!(0, ts_types_2.isString)(value))
throw new sfError_1.SfError(`can only encrypt strings but found: ${typeof value} : ${JSON.stringify(value)}`, 'InvalidCryptoValueError');
return this.crypto.isEncrypted(value) ? this.crypto.decrypt(value) : value;
}
/**
* Encrypt all values in a nested JsonMap.
*
* @param keyPaths: The complete path of the (nested) data
* @param data: The current (nested) data being worked on.
*/
recursiveEncrypt(data, parentKey) {
for (const key of Object.keys(data)) {
this.recursiveCrypto(this.encrypt.bind(this), [...(parentKey ? [parentKey] : []), key], data);
}
return data;
}
/**
* Decrypt all values in a nested JsonMap.
*
* @param keyPaths: The complete path of the (nested) data
* @param data: The current (nested) data being worked on.
*/
recursiveDecrypt(data, parentKey) {
for (const key of Object.keys(data)) {
this.recursiveCrypto(this.decrypt.bind(this), [...(parentKey ? [parentKey] : []), key], data);
}
return data;
}
/**
* Encrypt/Decrypt all values in a nested JsonMap.
*
* @param keyPaths: The complete path of the (nested) data
* @param data: The current (nested) data being worked on.
*/
recursiveCrypto(method, keyPaths, data) {
const key = keyPaths.pop();
const value = data[key];
if ((0, ts_types_2.isJsonMap)(value)) {
for (const newKey of Object.keys(value)) {
this.recursiveCrypto(method, [...keyPaths, key, newKey], value);
}
}
else if (this.isCryptoKey(key)) {
// I think this side effect is intentional
// eslint-disable-next-line no-param-reassign
data[key] = method(value);
}
}
}
exports.BaseConfigStore = BaseConfigStore;
//# sourceMappingURL=configStore.js.map