zotoio/launchdarkly-nodeutils

View on GitHub
src/LaunchDarklyUtilsFlags.js

Summary

Maintainability
A
0 mins
Test Coverage
import { default as json } from 'format-json';
import { default as jsonPatch } from 'fast-json-patch';
import { default as fs } from 'fs';
import { default as _ } from 'lodash';

// Class representing Feature flag functionality
export class LaunchDarklyUtilsFlags {
    /**
     * Feature flag specific api functions attached as 'LaunchDarklyUtils.flags'
     * @constructor LaunchDarklyUtilsFlags
     * @param { Swagger } apiClient - generated launchdarkly apiClient
     * @param { Object } log - logger implementation, or 'console'
     * @param { LaunchDarklyUtils } ldUtils - primary utils class
     * @returns { LaunchDarklyUtilsFlags } feature flag api functions
     */
    constructor(apiClient, log, ldUtils) {
        this.log = log;
        this.apiClient = apiClient;
        this.ldUtils = ldUtils;
        if (!this.ldUtils) {
            throw {
                message: 'LaunchDarklyUtilsRoles constructor requires ldUtils parameter'
            };
        }
    }

    /**
     * Api group object key in LD api
     * @returns {string}
     */
    get API_GROUP() {
        return 'Feature flags';
    }

    /**
     * Get all feature flags in project
     * @param {string} projectKey - project identifier
     * @returns {Promise}
     * @fulfil {Object} feature flag list json
     * @reject {Error} object with message
     * @example ldutils getFeatureFlags my-project
     */
    async getFeatureFlags(projectKey) {
        try {
            return this.apiClient.apis[this.API_GROUP].getFeatureFlags({ projectKey: projectKey }).then(response => {
                return response.body;
            });
        } catch (e) {
            throw {
                api: 'getFeatureFlags',
                message: e.message,
                docs: 'https://apidocs.launchdarkly.com/tag/Feature-flags#operation/getFeatureFlags'
            };
        }
    }

    /**
     * Get a single feature flag by key, and optional environment
     * @param {string} projectKey - project identifier
     * @param {string} featureFlagKey - feature flag identifier
     * @param {string} environmentKeyQuery - optional environment name
     * @returns {Promise}
     * @fulfil {Object} feature flag json
     * @reject {Error} object with message
     * @example ldutils getFeatureFlag my-project my-flag dev
     */
    async getFeatureFlag(projectKey, featureFlagKey, environmentKeyQuery) {
        try {
            return this.apiClient.apis[this.API_GROUP]
                .getFeatureFlag({
                    projectKey: projectKey,
                    featureFlagKey: featureFlagKey,
                    env: environmentKeyQuery
                })
                .then(response => {
                    return response.body;
                });
        } catch (e) {
            throw {
                api: 'getFeatureFlag',
                message: e.message,
                docs: 'https://apidocs.launchdarkly.com/tag/Feature-flags#operation/getFeatureFlag'
            };
        }
    }

    /**
     * Get the boolean state of a single feature flag by key, and optional environment
     * @param {string} projectKey - project identifier
     * @param {string} featureFlagKey - feature flag identifier
     * @param {string} environmentKeyQuery - environment name
     * @returns {Promise}
     * @fulfil {boolean} true/false
     * @reject {Error} object with message
     * @example ldutils getFeatureFlagState my-project my-flag dev
     */
    async getFeatureFlagState(projectKey, featureFlagKey, environmentKeyQuery) {
        return this.getFeatureFlag(projectKey, featureFlagKey, environmentKeyQuery).then(result => {
            return result.environments[environmentKeyQuery].on;
        });
    }

    /**
     * patch a feature flag by key
     * @param {string} projectKey - project identifier
     * @param {string} featureFlagKey - feature flag identifier
     * @param {Array<Object>} patch - array of valid json patch descriptors
     * @returns {Promise}
     * @fulfil {Object} updated feature flag json
     * @reject {Error} object with message
     * @example ldutils updateFeatureFlag my-project my-flag {jsonPatch}
     */
    async updateFeatureFlag(projectKey, featureFlagKey, patch) {
        try {
            return this.apiClient.apis[this.API_GROUP]
                .patchFeatureFlag(
                    {
                        projectKey: projectKey,
                        featureFlagKey: featureFlagKey
                    },
                    {
                        requestBody: patch
                    }
                )
                .then(response => {
                    return response.body;
                });
        } catch (e) {
            throw {
                api: 'patchFeatureFlag',
                message: e.message,
                docs: 'https://apidocs.launchdarkly.com/tag/Feature-flags#operation/patchFeatureFlag'
            };
        }
    }

    /**
     * Set the boolean state of a single feature flag by key, and environment name
     * @param {string} projectKey - project identifier
     * @param {string} featureFlagKey - feature flag identifier
     * @param {string} environmentKeyQuery - environment name
     * @param {boolean} value - true or false
     * @returns {Promise}
     * @fulfil {Object} updated feature flag json
     * @reject {Error} object with message
     * @example ldutils toggleFeatureFlag my-project my-flag dev true
     */
    async toggleFeatureFlag(projectKey, featureFlagKey, environmentKeyQuery, value) {
        return this.updateFeatureFlag(projectKey, featureFlagKey, [
            { op: 'replace', path: `/environments/${environmentKeyQuery}/on`, value: value }
        ]);
    }

    /**
     * Migrate feature flag properties between environments in a project. this includes:
     * targets, rules, fallthrough, offVariation, prerequisites and optionally the flags on/off state.
     * @param {string} projectKey - project identifier
     * @param {string} featureFlagKey - feature flag identifier
     * @param {string} fromEnv - environment to copy flag attributes from
     * @param {string} toEnv - environment to copy flag attributes to
     * @param {boolean} includeState - optionally copy boolean state true/false
     * @returns {Promise}
     * @fulfil {Object} updated feature flag json
     * @reject {Error} object with message
     * @example ldutils migrateFeatureFlag my-project my-flag dev test
     */
    async migrateFeatureFlag(projectKey, featureFlagKey, fromEnv, toEnv, includeState) {
        let that = this;

        return this.getFeatureFlag(projectKey, featureFlagKey)
            .then(flag => {
                let patchDelta = jsonPatch.compare(flag.environments[toEnv], flag.environments[fromEnv]);
                that.log.debug(`flagDelta for '${featureFlagKey}' ${json.plain(patchDelta)}`);
                return patchDelta;
            })
            .then(patchDelta => {
                let patch = this.assembleFlagPatch(patchDelta, toEnv, includeState);

                that.log.debug(`patch for '${featureFlagKey}' in ${toEnv} : ${json.plain(patch)}`);
                return this.updateFeatureFlag(projectKey, featureFlagKey, patch);
            });
    }

    assembleFlagPatch(patchDelta, targetEnv, includeState) {
        let patches = [];
        patchDelta.forEach(patch => {
            if (
                patch.path.startsWith('/targets') ||
                patch.path.startsWith('/rules') ||
                patch.path.startsWith('/fallthrough') ||
                patch.path.startsWith('/offVariation') ||
                patch.path.startsWith('/prerequisites') ||
                (includeState && patch.path.startsWith('/on'))
            ) {
                // add target env obj path and push
                patch.path = `/environments/${targetEnv}${patch.path}`;
                patches.push(patch);
            }
        });
        return patches;
    }

    /**
     * Migrate multiple feature flags properties between environments in a project. this includes:
     * targets, rules, fallthrough, offVariation, prerequisites and optionally the flags on/off state.
     * @param {string} projectKey - project identifier
     * @param {string} featureFlagKeys - comma-separated feature flag identifiers
     * @param {string} fromEnv - environment to copy flag attributes from
     * @param {string} toEnv - environment to copy flag attributes to
     * @param {boolean} includeState - optionally copy boolean state true/false
     * @returns {Promise}
     * @fulfil {Object} updated feature flag json array
     * @reject {Error} object with message
     * @example ldutils bulkMigrateFeatureFlags my-project my-flag,my-flag-two dev test
     */
    async bulkMigrateFeatureFlags(projectKey, featureFlagKeys, fromEnv, toEnv, includeState) {
        let promises = [];

        featureFlagKeys.split(',').forEach(key => {
            promises.push(this.migrateFeatureFlag(projectKey, key, fromEnv, toEnv, includeState));
        });

        return Promise.all(promises);
    }

    /**
     * Restore feature flags to state captured in a backup json file generated by getFeatureFlags(proj).
     * @param {string} projectKey - project identifier
     * @param {string} featureFlagKeys - feature flag identifiers comma separated
     * @param {string} targetEnv - environment to restore flag attributes to
     * @param {string} backupJsonFile - file to restore from from getFeatureFlags(proj)
     * @param {boolean} includeState - optionally restore boolean state true/false
     * @returns {Promise}
     * @fulfil {Object} updated feature flag json
     * @reject {Error} object with message
     * @example ldutils restoreFeatureFlags my-project my-flag,my-flag-two prod ./preReleaseBackup.json true
     */
    async restoreFeatureFlags(projectKey, featureFlagKeys, targetEnv, backupJsonFile, includeState) {
        let that = this;
        let promises = [];

        const backupJson = JSON.parse(fs.readFileSync(process.cwd() + '/' + backupJsonFile, 'utf-8'));

        //foreach flag, lookup node in env branch
        featureFlagKeys.split(',').forEach(key => {
            promises.push(
                this.getFeatureFlag(projectKey, key)
                    .then(flag => {
                        let backupFlagArr = _.filter(backupJson.items, { key: key });
                        if (backupFlagArr.length === 0) {
                            that.log.error(`flag does not exist in backup: ${key}`);
                        }
                        let backupFlag = backupFlagArr[0];
                        that.log.debug(`backupFlag: ${json.plain(backupFlag)}`);
                        let patchDelta = jsonPatch.compare(
                            flag.environments[targetEnv],
                            backupFlag.environments[targetEnv]
                        );
                        that.log.debug(`flagDelta for '${key}' ${json.plain(patchDelta)}`);
                        return patchDelta;
                    })
                    .then(patchDelta => {
                        let patch = this.assembleFlagPatch(patchDelta, targetEnv, includeState);

                        that.log.debug(`patch for '${key}' in ${targetEnv} : ${json.plain(patch)}`);
                        return this.updateFeatureFlag(projectKey, key, patch);
                    })
            );
        });

        return Promise.all(promises);
    }
}