datastore/BaseStoreNamespace.js

import Api from '../api/Api'
import { isString, isArray } from '../lib/check'

/**
 * @private
 * @description
 * Represents a namespace in the dataStore that can be used to be used to interact with
 * the remote API.
 *
 * @property {Array} keys an array of the loaded keys.
 * @property {String} namespace name of this namespace as on the server.
 *
 * @memberof module:datastore
 */
class BaseStoreNamespace {
    /**
     * @param {string} namespace - the name of the namespace this represents.
     * @param {string[]} keys - preloaded keys for this namespace.
     * @param {module:api.Api} api - the api implementation, used for testing.
     * @param {string} endPoint - the relative API-endpoint, one of ['dataStore, userDataStore'].
     */
    constructor(namespace, keys, api = Api.getApi(), endPoint) {
        if (!isString(namespace)) {
            throw new Error(
                'BaseStoreNamespace must be called with a string to identify the Namespace'
            )
        }
        if (!isString(endPoint)) {
            throw new Error(
                'BaseStoreNamespace must be called with an endPoint'
            )
        }
        if (this.constructor === BaseStoreNamespace) {
            throw new Error("Can't instantiate abstract class!")
        }

        this.api = api

        /**
         * The name of the namespace
         * @type {string}
         */
        this.namespace = namespace

        /**
         * an array of the loaded keys.
         * @type {string[]}
         */
        this.keys = keys || []
        this.endPoint = endPoint
    }

    /**
     * Get the keys for this namespace.
     *
     * @returns {Promise} - The internal list of keys for current namespace.
     */
    getKeys() {
        return this.api
            .get([this.endPoint, this.namespace].join('/'))
            .then(response => {
                if (response && isArray(response)) {
                    this.keys = response
                    return response
                }
                return Promise.reject(
                    new Error(
                        'The requested namespace has no keys or does not exist.'
                    )
                )
            })
    }

    /**
     * Retrieves the value of given key in current namespace.
     *
     * @param key - key to retrieve.
     * @returns {Promise} - The value of the given key.
     */
    get(key) {
        return this.api.get([this.endPoint, this.namespace, key].join('/'))
    }

    /**
     * Sets the value of given key to given value.
     *
     * This will also create a new namespace on the API-end if it does not exist.
     * If the key exists <a href='#update'> update</a> will be called, unless <code>overrideUpdate</code> equals
     * true.
     *
     * @param key - key in this namespace to set.
     * @param value - JSON-value to be set.
     * @param [overrideUpdate=false] - If true a post-request is sent even if key exists.
     * @param [encrypt=false] - If the value should be encrypted on the server.
     * @returns {Promise} - the response body from the {@link module:api.Api#get API}.
     */
    set(key, value, overrideUpdate = false, encrypt = false) {
        if (!overrideUpdate && this.keys.includes(key)) {
            return this.update(key, value)
        }
        const queryParams = encrypt === true ? '?encrypt=true' : ''
        return this.api
            .post(
                [this.endPoint, this.namespace, key + queryParams].join('/'),
                value
            )
            .then(resp => {
                this.keys = [...this.keys, key]
                return resp
            })
    }

    /**
     * Deletes given key from the API.
     * @param {string} key - key to delete.
     * @returns {Promise} - the response body from the {@link module:api.Api#get API}.
     */
    delete(key) {
        if (!isString(key)) {
            return Promise.reject(
                new Error(`Expected key to be string, but got ${typeof key}`)
            )
        }
        return this.api
            .delete([this.endPoint, this.namespace, key].join('/'))
            .then(resp => {
                this.keys = this.keys.filter(elem => elem !== key)
                return resp
            })
    }

    /**
     * Updates a key with given value.
     * @param key - key to update.
     * @param value - value to update to.
     * @returns {Promise} - the response body from the {@link module:api.Api#get API}.
     */
    update(key, value) {
        return this.api.update(
            [this.endPoint, this.namespace, key].join('/'),
            value
        )
    }
}

export default BaseStoreNamespace