Source

services/StorageManager.ts

/**
 * The `StorageManager` constructor. Initialises the storage namespace (key prefix) to relevant `env` parameters.
 *
 * Checks for (in order):
 * - `env.REACT_APP_BASE_URL`
 * - `env.REACT_APP_NAME`
 * - `window.location.pathname`
 *
 * The storage manager builds a singleton instance, which is shared across the application. It can be imported and used
 * as per the below example:
 *
 * ```js
 * import { storage } from '@jcu/spark'
 *
 * ...
 *
 * storage.saveData('important_info', 'This is an important message', {persist: true})
 * ```
 *
 * @category Services
 */
export class StorageManager {

    localStorage: Storage = window.localStorage
    sessionStorage: Storage = window.sessionStorage

    _namespace: string

    /**
     * Save data to local or session storage depending on options provided. Contains a `JSON.stringify` call to allow the
     * storage of more complex object data. If `JSON.parse(JSON.stringify(<your object>))` works then it can be saved.
     * The `StorageManager` internally handles adding an application specific prefix to the storage keys to prevent the
     * possible overlap of data spaces for co-hosted applications.
     *
     * The default behaviour of the save function is not persistent (session storage) and doesn't overwrite existing keys.
     *
     * @param {string} key The key you want to save the data under
     * @param {*} value The data you want to save
     * @param {Object} options Additional save options
     * @param {boolean} options.update Used to overwrite data if the provided key already exists
     * @param {boolean} options.persist Used to determine if session or local storage is used. True is local (persistent)
     * @returns {Object} An Object containing data defining the outcome of the save request
     */
    saveData(key: string, value: any, options: any = {}) {
        // Results object
        let results: any = {
            success: false,
            key: key,
            value: value,
            errors: []
        }

        if (key) {
            // Check if the key already exists in storage
            if (this.loadData(key) && !options.update) {
                console.log("The key provided already exists in storage. If you'd like to overwrite the data, use 'updateData()'")
                results.success = false
                results.errors.push(`Key ${key} already exists in storage. To update, use updateData()`)
            } else {
                // Convert value into JSON string to preserve type and structure when written to storage
                let jsonValue: string = JSON.stringify(value)
                // Create app specific key for data
                let scopedKey: string = `${this.namespace}-${key}`.toLowerCase()

                
                if (options.persist) {
                    this.localStorage.setItem(scopedKey, jsonValue)
                    results.success = true
                } else {
                    this.sessionStorage.setItem(scopedKey, jsonValue)
                    results.success = true
                }
            }
        } else {
            results.errors.push("Key not provided")
        }

        return results
    }

    /**
     * A wrapper function for saveData where the `update` option is forced to true.
     * Will overwrite existing keys or save the new key.
     *
     * @param {string} key The key you want update or save new data too
     * @param {*} value The data you want to save
     * @param {Object} options Additional update options
     * @param {boolean} options.persist Used to determine if session or local storage is used. True is local (persistent)
     */
    updateData(key: string, value: any, options: any = {}) {
        return this.saveData(key, value, {...options, update: true})
    }

    /**
     * Delete data from storage for a given key.
     *
     * Checks session then local storage for a key, deletes the first one it finds.
     *
     * @param {string} key The key you want to delete from storage
     */
    deleteData(key: string) {
        if (key) {
            let scopedKey = `${this.namespace}-${key}`.toLowerCase()
            if (this.sessionStorage.getItem(scopedKey)) {
                this.sessionStorage.removeItem(scopedKey)
            } else {
                this.localStorage.removeItem(scopedKey)
            }
        }
    }

    /**
     * Load data from storage for a given key. The `StorageManager` internally handles adding an application specific
     * prefix to the key provided to prevent possible overlap of data spaces for co-hosted applications.
     *
     * There are currently no supported `options` for the `loadData` function.
     *
     * @param {string} key The key you want to load from storage
     * @param {Object} options Additional load options
     */
    loadData(key: string, options: any = {}) {
        // Options is cast to any because otherwise it complains if you dont have every possible object key type defined
        // TODO: Fix the ts-ignore blocks.

        if (key) {
            let scopedKey = `${this.namespace}-${key}`.toLowerCase()
            if (this.sessionStorage.getItem(scopedKey)) {
                // Session storage is first precedence (due to being cleared each time)
                //@ts-ignore
                return JSON.parse(this.sessionStorage.getItem(scopedKey))
            } else if (this.localStorage.getItem(scopedKey)) {
                // Local storage is checked second as the data persists across browser sessions
                //@ts-ignore
                return JSON.parse(this.localStorage.getItem(scopedKey))
            }
        }
        return null
    }

    get namespace() {
        return this._namespace
    }

    constructor() {
        // Set up the namespace for app specific browser storage
        if (process.env.REACT_APP_BASE_URL) {
            // Replace spaces and underscores with hyphens
            this._namespace = `jcu--${process.env.REACT_APP_BASE_URL.replace('/', '')}`
        } else if (process.env.REACT_APP_NAME) {
            // Replace spaces and underscores with hyphens
            this._namespace = `jcu--${process.env.REACT_APP_NAME.replace('_', '-').replace(' ', '-')}`
        } else {
            this._namespace = `jcu-${window.location.pathname.replace('/', '-')}`
        }
    }

}

//TODO: Better name for export
export let storage = new StorageManager()