Source: stack.js

"use strict";
/*!
 * Contentstack DataSync Filesystem SDK.
 * Enables querying on contents saved via @contentstack/datasync-content-store-filesystem
 * Copyright (c) Contentstack LLC
 * MIT Licensed
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const json_mask_1 = __importDefault(require("json-mask"));
const lodash_1 = require("lodash");
const sift_1 = __importDefault(require("sift"));
const fs_1 = require("./fs");
const utils_1 = require("./utils");
const extend = {
    compare(type) {
        return function (key, value) {
            if (typeof key === 'string' && typeof value !== 'undefined') {
                this.q.query = this.q.query || {};
                this.q.query[key] = this.q.query[key] || {};
                this.q.query[key][type] = value;
                return this;
            }
            throw new Error(`Kindly provide valid parameters for ${type}`);
        };
    },
    contained(bool) {
        const type = (bool) ? '$in' : '$nin';
        return function (key, value) {
            if (typeof key === 'string' && typeof value === 'object' && Array.isArray(value)) {
                this.q.query = this.q.query || {};
                this.q.query[key] = this.q.query[key] || {};
                this.q.query[key][type] = this.q.query[key][type] || [];
                this.q.query[key][type] = this.q.query[key][type].concat(value);
                return this;
            }
            throw new Error(`Kindly provide valid parameters for ${bool}`);
        };
    },
    exists(bool) {
        return function (key) {
            if (key && typeof key === 'string') {
                this.q.query = this.q.query || {};
                this.q.query[key] = this.q.query[key] || {};
                this.q.query[key].$exists = bool;
                return this;
            }
            throw new Error(`Kindly provide valid parameters for ${bool}`);
        };
    },
    // TODO
    logical(type) {
        return function (query) {
            this.q.query = this.q.query || {};
            this.q.query[type] = query;
            return this;
        };
    },
    sort(type) {
        return function (key) {
            if (key && typeof key === 'string') {
                this.q[type] = key;
                return this;
            }
            throw new Error(`Kindly provide valid parameters for sort-${type}`);
        };
    },
    pagination(type) {
        return function (value) {
            if (typeof value === 'number') {
                this.q[type] = value;
                return this;
            }
            throw new Error('Argument should be a number.');
        };
    },
};
/**
 * @summary
 *  Expose SDK query methods on Stack
 * @returns {this} - Returns `stack's` instance
 */
class Stack {
    constructor(config) {
        // app config
        this.config = config;
        this.contentStore = config.contentStore;
        this.projections = Object.keys(this.contentStore.projections);
        this.types = config.contentStore.internal.types;
        this.q = this.q || {};
        this.q.query = this.q.query || {};
        /**
         * @public
         * @method lessThan
         * @description Retrieves entries in which the value of a field is lesser than the provided value
         * @param {String} key - uid of the field
         * @param {*} value - Value used to match or compare
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.lessThan('created_at','2015-06-22').find()
         * data.then((result) => {
         *   // result content the data who's 'created_at date'
         *   // is less than '2015-06-22'
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.lessThan = extend.compare('$lt');
        /**
         * @public
         * @method lessThanOrEqualTo
         * @description Retrieves entries in which the value of a field is lesser than or equal to the provided value.
         * @param {String} key - uid of the field
         * @param {*} value - Value used to match or compare
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.lessThanOrEqualTo('created_at','2015-06-22').find()
         * data.then((result) => {
         *   // result contain the data of entries where the
         *   //'created_at' date will be less than or equalto '2015-06-22'.
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.lessThanOrEqualTo = extend.compare('$lte');
        /**
         * @public
         * @method greaterThan
         * @description Retrieves entries in which the value for a field is greater than the provided value.
         * @param {String} key - uid of the field
         * @param {*} value -  value used to match or compare
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.greaterThan('created_at','2015-03-12').find()
         * data.then((result) => {
         *   // result contains the data of entries where the
         *   //'created_at' date will be greaterthan '2015-06-22'
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.greaterThan = extend.compare('$gt');
        /**
         * @public
         * @method greaterThanOrEqualTo
         * @description Retrieves entries in which the value for a field is greater than or equal to the provided value.
         * @param {String} key - uid of the field
         * @param {*} value - Value used to match or compare
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.greaterThanOrEqualTo('created_at','2015-03-12').find()
         * data.then((result) => {
         *   // result contains the data of entries where the
         *   //'created_at' date will be  greaterThan or equalto '2015-06-22'
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.greaterThanOrEqualTo = extend.compare('$gte');
        /**
         * @public
         * @method notEqualTo
         * @description Retrieves entries in which the value for a field does not match the provided value.
         * @param {String} key - uid of the field
         * @param {*} value - Value used to match or compare
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.notEqualTo('title','Demo').find()
         * data.then((result) => {
         *   // ‘result’ contains the list of entries where value
         *   // of the ‘title’ field will not be 'Demo'.
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.notEqualTo = extend.compare('$ne');
        /**
         * @public
         * @method containedIn
         * @description Retrieve entries in which the value of a field matches with any of the provided array of values
         * @param {String} key - uid of the field
         * @param {*} value - Array of values that are to be used to match or compare
         * @example
         * let blogQuery = Stack.contentType('example').entries().query();
         * let data = blogQuery.containedIn('title', ['Demo', 'Welcome']).find()
         * data.then((result) => {
         *   // ‘result’ contains the list of entries where value of the
         *   // ‘title’ field will contain either 'Demo' or ‘Welcome’.
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.containedIn = extend.contained(true);
        /**
         * @public
         * @method notContainedIn
         * @description Retrieve entries in which the value of a field does not match
         *              with any of the provided array of values.
         * @param {String} key - uid of the field
         * @param {Array} value - Array of values that are to be used to match or compare
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.notContainedIn('title', ['Demo', 'Welcome']).find()
         * data.then((result) => {
         *   // 'result' contains the list of entries where value of the
         *   //title field should not be either "Demo" or ‘Welcome’
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.notContainedIn = extend.contained(false);
        /**
         * @public
         * @method exists
         * @description Retrieve entries if value of the field, mentioned in the condition, exists.
         * @param {String} key - uid of the field
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.exists('featured').find()
         * data.then((result) => {
         *   // ‘result’ contains the list of entries in which "featured" exists.
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.exists = extend.exists(true);
        /**
         * @public
         * @method notExists
         * @description Retrieve entries if value of the field, mentioned in the condition, does not exists.
         * @param {String} key - uid of the field
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.notExists('featured').find()
         * data.then((result) => {
         *   // result is the list of non-existing’featured’" data.
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.notExists = extend.exists(false);
        /**
         * @public
         * @method ascending
         * @description Sort fetched entries in the ascending order with respect to a specific field.
         * @param {String} key - field uid based on which the ordering will be done
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.ascending('created_at').find()
         * data.then((result) => {
         *   // ‘result’ contains the list of entries which is sorted in
         *   //ascending order on the basis of ‘created_at’.
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.ascending = extend.sort('asc');
        /**
         * @public
         * @method descending
         * @description Sort fetched entries in the descending order with respect to a specific field
         * @param {String} key - field uid based on which the ordering will be done.
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.descending('created_at').find()
         * data.then((result) => {
         *   // ‘result’ contains the list of entries which is sorted in
         *   //descending order on the basis of ‘created_at’.
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.descending = extend.sort('desc');
        /**
         * @public
         * @method skip
         * @description Skips at specific number of entries.
         * @param {Number} skip - number of entries to be skipped
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.skip(5).find()
         * data.then((result) => {
         *   //result
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.skip = extend.pagination('skip');
        /**
         * @public
         * @method limit
         * @description Returns a specific number of entries based on the set limit
         * @param {Number} limit - maximum number of entries to be returned
         * @example
         * let blogQuery = Stack.contentType('example').entries()
         * let data = blogQuery.limit(10).find()
         * data.then((result) => {
         *   // result contains the limited number of entries
         * }).catch((error) => {
         *   // error trace
         * })
         * @returns {this} - Returns `stack's` instance
         */
        this.limit = extend.pagination('limit');
        /**
         * @public
         * @method or
         * @description Retrieves entries that satisfy at least one of the given conditions
         * @param {object} queries - array of Query objects or raw queries
         * @example
         * let Query1 = Stack.contentType('example').entries().equalTo('title', 'Demo').find()
         * let Query2 = Stack.contentType('example').entries().lessThan('comments', 10).find()
         * blogQuery.or(Query1, Query2).find()
         * @returns {this} - Returns `stack's` instance
         */
        this.or = extend.logical('$or');
        this.nor = extend.logical('$nor');
        this.not = extend.logical('$not');
        /**
         * @public
         * @method and
         * @description Retrieve entries that satisfy all the provided conditions.
         * @param {object} queries - array of query objects or raw queries.
         * @example
         * let Query1 = Stack.contentType('example').entries().equalTo('title', 'Demo')
         * let Query2 = Stack.contentType('example').entries().lessThan('comments', 10)
         * blogQuery.and(Query1, Query2).find()
         * @returns {this} - Returns `stack's` instance
         */
        this.and = extend.logical('$and');
    }
    /**
     * TODO
     * @public
     * @method connect
     * @summary
     *  Establish connection to filesytem
     * @param {Object} overrides - Config overrides/flesystem specific config
     * @example
     * Stack.connect({overrides})
     *  .then((result) => {
     *    // db instance
     *  })
     *  .catch((error) => {
     *    // handle query errors
     *  })
     *
     * @returns {string} baseDir
     */
    connect(overrides = {}) {
        this.config = lodash_1.merge(this.config, overrides);
        return Promise.resolve(this.config);
    }
    /**
     * @public
     * @method contentType
     * @summary
     *  Content type to query on
     * @param {String} uid - Content type uid
     * @returns {this} - Returns `stack's` instance
     * @example
     * Stack.contentType('example').entries().find()
     *  .then((result) => {
     *    // returns entries filtered based on 'example' content type
     *  })
     *  .catch((error) => {
     *    // handle query errors
     *  })
     *
     * @returns {Stack} instance
     */
    contentType(uid) {
        if (typeof uid !== 'string' || uid.length === 0) {
            throw new Error('Kindly provide a uid for .contentType()');
        }
        const stack = new Stack(this.config);
        stack.q.content_type_uid = uid;
        return stack;
    }
    /**
     * @public
     * @method entries
     * @summary
     * To get entries from contentType
     * @example
     * Stack.contentType('example')
     *  .entries()
     *  .find()
     * @returns {this} - Returns `stack's` instance
     */
    entries() {
        if (typeof this.q.content_type_uid === 'undefined') {
            throw new Error('Please call .contentType() before calling .entries()!');
        }
        return this;
    }
    /**
     * @public
     * @method entry
     * @summary
     * To get entry from contentType
     * @example
     * Stack.contentType('example').entry('bltabcd12345').find()
     * //or
     * Stack.contentType('example').entry().find()
     * @param {string} uid- Optional. uid of entry
     * @returns {this} - Returns `stack's` instance
     */
    entry(uid) {
        this.q.isSingle = true;
        if (typeof this.q.content_type_uid === 'undefined') {
            throw new Error('Please call .contentType() before calling .entries()!');
        }
        if (uid && typeof uid === 'string') {
            this.q.query[uid] = uid;
        }
        return this;
    }
    /**
     * @public
     * @method asset
     * @summary
     * To get single asset
     * @example
     * Stack.asset('bltabced12435').find()
     * //or
     * Stack.asset().find()
     * @param {string} uid- Optional. uid of asset
     * @returns {this} - Returns `stack's` instance
     */
    asset(uid) {
        const stack = new Stack(this.config);
        stack.q.isSingle = true;
        stack.q.content_type_uid = stack.types.assets;
        if (uid && typeof uid === 'string') {
            stack.q.query[uid] = uid;
        }
        return stack;
    }
    /**
     * @public
     * @method assets
     * @summary Get assets details
     * @example
     * Stack.assets().find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    assets() {
        const stack = new Stack(this.config);
        stack.q.content_type_uid = stack.types.assets;
        return stack;
    }
    /**
     * @public
     * @method schemas
     * @summary Get content type schemas
     * @example
     * Stack.schemas().find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    schemas() {
        const stack = new Stack(this.config);
        stack.q.content_type_uid = stack.types.content_types;
        return stack;
    }
    /**
     * @public
     * @method contentTypes
     * @summary Get content type schemas
     * @example
     * Stack.contentTypes().find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    contentTypes() {
        const stack = new Stack(this.config);
        stack.q.content_type_uid = stack.types.content_types;
        return stack;
    }
    /**
     * @public
     * @method schema
     * @summary Get a single content type's schema
     * @param {String} uid - Optional 'uid' of the content type, who's schema is to be fetched
     * @example
     * Stack.schema(uid?: string).find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    schema(uid) {
        const stack = new Stack(this.config);
        stack.q.isSingle = true;
        stack.q.content_type_uid = stack.types.content_types;
        if (uid && typeof uid === 'string') {
            stack.q.query[uid] = uid;
        }
        return stack;
    }
    /**
     * @public
     * @method equalTo
     * @description Retrieve entries in which a specific field satisfies the value provided
     * @param {String} key - uid of the field
     * @param {Any} value - value used to match or compare
     * @example
     * let blogQuery = Stack.contentType('example').entries()
     * let data = blogQuery.equalTo('title','Demo').find()
     * data.then((result) => {
     *   // ‘result’ contains the list of entries where value of
     *   //‘title’ is equal to ‘Demo’.
     * }).catch((error) => {
     *   // error trace
     * })
     *
     * @returns {this} - Returns `stack's` instance
     */
    equalTo(key, value) {
        if (!key || typeof key !== 'string' && typeof value === 'undefined') {
            throw new Error('Kindly provide valid parameters for .equalTo()!');
        }
        this.q.query[key] = value;
        return this;
    }
    /**
     * @public
     * @method where
     * @summary Pass JS expression or a full function to the query system
     * @description Evaluate js expressions
     * @param field
     * @param value
     *
     * @example
     * const query = Stack.contentType('example').entries().where("this.title === 'Amazon_Echo_Black'").find()
     * query.then((result) => {
     *   // ‘result’ contains the list of entries where value of
     *   //‘title’ is equal to ‘Demo’.
     * }).catch(error) => {
     *   // error trace
     * })
     *
     * @returns {this} - Returns `stack's` instance
     */
    where(expr) {
        if (!expr) {
            throw new Error('Kindly provide a valid field and expr/fn value for \'.where()\'');
        }
        this.q.query.$where = expr;
        return this;
    }
    /**
     * @public
     * @method count
     * @description Returns the total number of entries
     * @example
     * const query = Stack.contentType('example').entries().count().find()
     * query.then((result) => {
     *   // returns 'example' content type's entries
     * }).catch(error) => {
     *   // error trace
     * })
     * @returns {this} - Returns `stack's` instance
     */
    count() {
        this.q.countOnly = 'count';
        return this.find();
    }
    /**
     * @public
     * @method query
     * @description Retrieve entries based on raw queries
     * @param {object} userQuery - RAW (JSON) queries
     * @returns {this} - Returns `stack's` instance
     * @example
     * Stack.contentType('example').entries().query({"authors.name": "John Doe"}).find()
     *  .then((result) => {
     *    // returns entries, who's reference author's name equals "John Doe"
     *  })
     *  .catch((error) => {
     *    // handle query errors
     *  })
     */
    query(userQuery) {
        if (!userQuery || typeof userQuery !== 'object') {
            throw new Error('Kindly provide valid parameters for \'.query()\'');
        }
        this.q.query = lodash_1.merge(this.q.query, userQuery);
        return this;
    }
    /**
     * @public
     * @method tags
     * @description Retrieves entries based on the provided tags
     * @param {Array} values - Entries/Assets that have the specified tags
     * @example
     * const query = Stack.contentType('example').entries().tags(['technology', 'business']).find()
     * query.then((result) => {
     *   // ‘result’ contains list of entries which have tags "’technology’" and ‘"business’".
     * }).catch((error) => {
     *   // error trace
     * })
     * @returns {this} - Returns `stack's` instance
     */
    tags(values) {
        if (values && typeof values === 'object' && values instanceof Array) {
            this.q.query.tags = {
                $in: values,
            };
            return this;
        }
        throw new Error('Kindly provide valid parameters for \'.tags()\'');
    }
    /**
     * @public
     * @method includeCount
     * @description Includes the total number of entries returned in the response.
     * @example
     * Stack.contentType('example')
     *  .entries()
     *  .includeCount()
     *  .find()
     * @returns {this} - Returns `stack's` instance
     */
    includeCount() {
        this.q.includeCount = true;
        return this;
    }
    /**
     * @public
     * @method language
     * @description to retrive the result bsed on the specific locale.
     * @param {String} languageCode - Language to query on
     * @example
     * Stack.contentType('example')
     *  .entries()
     *  .language('fr-fr')
     *  .find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    language(languageCode) {
        if (!languageCode || typeof languageCode !== 'string') {
            throw new Error(`${languageCode} should be of type string and non-empty!`);
        }
        this.q.locale = languageCode;
        return this;
    }
    /**
     * @public
     * @method include
     * @summary
     * Includes references of provided fields of the entries being scanned
     * @param {*} key - uid/uid's of the field
     *
     * @example
     * Stack.contentType('example')
     *  .entries()
     *  .include(['authors','categories'])
     *  .find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    include(fields) {
        if (fields && typeof fields === 'object' && fields instanceof Array && fields.length) {
            this.q.includeSpecificReferences = fields;
        }
        else if (fields && typeof fields === 'string') {
            this.q.includeSpecificReferences = [fields];
        }
        else {
            throw new Error('Kindly pass \'string\' OR \'array\' fields for .include()!');
        }
        return this;
    }
    /**
     * @public
     * @method includeReferences
     * @summary
     *  Includes all references of the entries being scanned
     * @param {number} depth - Optional parameter. Use this to override the default reference depth/level i.e. 4
     *
     * @example
     * Stack.contentType('example')
     *  .entries()
     *  .includeReferences()
     *  .find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    includeReferences(depth) {
        console.warn('.includeReferences() is a relatively slow query..!');
        this.q.includeAllReferences = true;
        if (typeof depth === 'number') {
            this.q.referenceDepth = depth;
        }
        return this;
    }
    /**
     * @public
     * @method excludeReferences
     * @summary
     *  Excludes all references of the entries being scanned
     *
     * @example
     * Stack.contentType('example')
     *  .entries()
     *  .excludeReferences()
     *  .find()
     *  .then((result) => {
     *    // ‘result’ entries without references
     *  }).catch((error) => {
     *    // error trace
     *  })
     *
     * @returns {this} - Returns `stack's` instance
     */
    excludeReferences() {
        this.q.excludeAllReferences = true;
        return this;
    }
    /**
     * @public
     * @method includeContentType
     * @description Includes the total number of entries returned in the response.
     * @example
     * Stack.contentType('example')
     *  .entries()
     *  .includeContentType()
     *  .find()
     *  .then((result) => {
     *    // Expected result
     *    {
     *      entries: [
     *        {
     *          ...,
     *        },
     *      ],
     *      content_type_uid: 'example',
     *      locale: 'en-us',
     *      content_type: {
     *        ..., // Content type example's schema
     *      }
     *    }
     *  }).catch((error) => {
     *    // error trace
     *  })
     *
     * @returns {this} - Returns `stack's` instance
     */
    includeContentType() {
        this.q.include_content_type = true;
        return this;
    }
    /**
     * @public
     * @method getQuery
     * @description Returns the raw (JSON) query based on the filters applied on Query object.
     * @example
     * Stack.contentType('example')
     *  .eqaulTo('title','Demo')
     *  .getQuery()
     *  .find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    getQuery() {
        return this.q.query;
    }
    /**
     * @public
     * @method regex
     * @description Retrieve entries that match the provided regular expressions
     * @param {String} key - uid of the field
     * @param {*} value - value used to match or compare
     * @param {String} [options] - match or compare value in entry
     * @example
     * let blogQuery = Stack.contentType('example').entries()
     * blogQuery.regex('title','^Demo').find() //regex without options
     * //or
     * blogQuery.regex('title','^Demo', 'i').find() //regex without options
     * @returns {this} - Returns `stack's` instance
     */
    regex(key, value, options = 'g') {
        if (key && value && typeof key === 'string' && typeof value === 'string') {
            this.q.query[key] = {
                $options: options,
                $regex: value,
            };
            return this;
        }
        throw new Error('Kindly provide valid parameters for .regex()!');
    }
    /**
     * @public
     * @method only
     * @description
     *  Similar to MongoDB projections. Accepts an array.
     *  Only fields mentioned in the array would be returned in the result.
     * @param {Array} result - Array of field properties
     * @example
     * const query = Stack.contentType('example').entries().only(['title','uid']).find()
     * query.then((result) => {
     *   // ‘result’ contains a list of entries with field title and uid only
     * }).catch((error) => {
     *   // error trace
     * })
     *
     * @returns {this} - Returns `stack's` instance
     */
    only(fields) {
        if (fields && typeof fields === 'object' && fields instanceof Array && fields.length) {
            this.q.only = fields;
            return this;
        }
        throw new Error('Kindly provide valid parameters for .only()!');
    }
    /**
     * @public
     * @method except
     * @description
     *  Similar to MongoDB projections. Accepts an array.
     *  Only fields mentioned in the array would be removed from the result.
     * @param {Array} result - Array of field properties
     * @example
     * const query = Stack.contentType('example').entries().except(['title','uid']).find()
     * query.then((result) => {
     *   // ‘result’ contains a list of entries with field title and uid only
     * }).catch((error) => {
     *   // error trace
     * })
     *
     * @returns {this} - Returns `stack's` instance
     */
    except(fields) {
        if (fields && typeof fields === 'object' && fields instanceof Array && fields.length) {
            this.q.except = [];
            const keys = Object.keys(this.contentStore.projections);
            this.q.except = keys.concat(fields);
            return this;
        }
        throw new Error('Kindly provide valid parameters for .except()!');
    }
    /**
     * @public
     * @method queryReferences
     * @summary
     *  Wrapper, that allows querying on the entry's references.
     * @note
     *  This is a slow method, since it scans all documents and fires the `reference` query on them
     *  Use `.query()` filters to reduce the total no of documents being scanned
     * @param {Any} query - Query filter, to be applied on referenced result
     * @example
     * Stack.contentType('blog')
     *  .entries()
     *  .includeRferences() // This would include all references of the content type
     *  .queryReferences({"authors.name": "John Doe"})
     *  .find()
     *
     * @returns {this} - Returns `stack's` instance
     */
    queryReferences(query) {
        if (!query || typeof query !== 'object') {
            throw new Error('Kindly valid parameters for \'.queryReferences()\'!');
        }
        this.q.queryOnReferences = query;
        return this;
    }
    /**
     * @public
     * @method referenceDepth
     * @deprecated
     * @summary
     * Use it along with .includeReferences()
     * Overrides the default reference depths defined for references - 2
     * i.e. If A -> B -> C -> D, so calling .includeReferences() on content type A,
     * would result in all references being resolved until its nested child reference D
     * @param {number} depth - Level of nested references to be fetched
     * @example
     * Stack.contentType('blog')
     *  .entries()
     *  .includeReferences()
     *  .referenceDepth(4)
     *  .find()
     *
     * @returns {this} - Returns the `stack's` instance
     */
    referenceDepth(depth) {
        if (typeof depth !== 'number') {
            throw new Error('Kindly valid parameters for \'.referenceDepth()\'!');
        }
        this.q.referenceDepth = depth;
        if (depth > this.contentStore.referenceDepth) {
            console.warn(`Increasing reference depth above ${this.contentStore.referenceDepth} may degrade performance!`);
        }
        return this;
    }
    /**
     * @public
     * @method find
     * @description
     * Queries the db using the query built/passed
     * Does all the processing, filtering, referencing after querying the DB
     * @param {object} query Optional query object, that overrides all the
     * previously build queries
     * @public
     * @example
     * Stack.contentType('blog').entries().find()
     * .then((result) => {
     *    // returns blog content type's entries
     * })
     * .catch((error) => {
     *    // handle query errors
     * })
     * @returns {object} - Returns a objects, that have been processed, filtered and referenced
     */
    find() {
        return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
            try {
                const { filePath, key, locale, } = this.preProcess();
                let data = yield fs_1.readFile(filePath);
                const count = data.length;
                data = data.filter(sift_1.default(this.q.query));
                if (data.length === 0 || this.q.content_type_uid === this.types.content_types || this.q.content_type_uid ===
                    this.types.assets || this.q.countOnly || this.q.excludeAllReferences) {
                    // do nothing
                }
                else if (this.q.includeSpecificReferences) {
                    yield this
                        .includeSpecificReferences(data, this.q.content_type_uid, locale, this.q
                        .includeSpecificReferences);
                }
                else if (this.q.includeAllReferences) {
                    // need re-writes
                    yield this.bindReferences(data, this.q.content_type_uid, locale);
                }
                else {
                    yield this.includeAssetsOnly(data, locale, this.q.content_type_uid);
                }
                if (this.q.queryOnReferences) {
                    data = data.filter(sift_1.default(this.q.queryOnReferences));
                }
                const { output } = yield this.postProcess(data, key, locale, count);
                return resolve(output);
            }
            catch (error) {
                return reject(error);
            }
        }));
    }
    /**
     * @public
     * @method findOne
     * @description
     * Queries the db using the query built/passed. Returns a single entry/asset/content type object
     * Does all the processing, filtering, referencing after querying the DB
     * @param {object} query Optional query object, that overrides all the previously build queries
     *
     * @example
     * Stack.contentType('blog')
     *  .entries()
     *  .findOne()
     *
     * @returns {object} - Returns an object, that has been processed, filtered and referenced
     */
    findOne() {
        this.q.isSingle = true;
        return this.find();
    }
    /**
     * @private
     * @method preProcess
     * @description
     * Runs before .find()
     * Formats the queries/sets defaults and returns the locale, key & filepath of the data
     * @returns {object} - Returns the query's key, locale & filepath of the data
     */
    preProcess() {
        const locale = (typeof this.q.locale === 'string') ? this.q.locale : this.contentStore.locale;
        let key;
        let filePath;
        switch (this.q.content_type_uid) {
            case this.types.assets:
                filePath = utils_1.getAssetsPath(locale) + '.json';
                key = (this.q.isSingle) ? 'asset' : 'assets';
                break;
            case this.types.content_types:
                filePath = utils_1.getContentTypesPath(locale) + '.json';
                key = (this.q.isSingle) ? 'content_type' : 'content_types';
                break;
            default:
                filePath = utils_1.getEntriesPath(locale, this.q.content_type_uid) + '.json';
                key = (this.q.isSingle) ? 'entry' : 'entries';
                break;
        }
        if (!fs_1.existsSync(filePath)) {
            throw new Error(`Queried content type ${this.q.content_type_uid} was not found at ${filePath}!`);
        }
        if (!this.q.hasOwnProperty('asc') && !this.q.hasOwnProperty('desc')) {
            this.q.desc = this.contentStore.defaultSortingField;
        }
        if (!this.q.hasOwnProperty('except') && !this.q.hasOwnProperty('only')) {
            const keys = Object.keys(this.contentStore.projections);
            this.q.except = keys;
        }
        this.q.referenceDepth = (typeof this.q.referenceDepth === 'number') ? this.q.referenceDepth : this.contentStore
            .referenceDepth;
        return {
            filePath,
            key,
            locale,
        };
    }
    /**
     * @private
     * @method postProcess
     * @description
     * Runs after .find()
     * Formats the data as per query
     * @param {object} data - The result data
     * @param {string} key - The key to whom the data is to be assigned
     * @param {string} locale - The query's locale
     * @returns {object} - Returns the formatted input
     */
    postProcess(data, key, locale, count) {
        return __awaiter(this, void 0, void 0, function* () {
            // tslint:disable-next-line: variable-name
            const content_type_uid = (this.q.content_type_uid === this.types.assets) ? 'assets' : (this.q.content_type_uid ===
                this.types.content_types ? 'content_types' : this.q.content_type_uid);
            const output = {
                content_type_uid,
                locale,
            };
            if (this.q.countOnly) {
                output.count = data.length;
                return { output };
            }
            if (this.q.include_content_type) {
                // ideally, if the content type doesn't exist, an error will be thrown before it reaches this line
                const contentTypes = yield fs_1.readFile(utils_1.getContentTypesPath(locale) + '.json');
                for (let i = 0, j = contentTypes.length; i < j; i++) {
                    if (contentTypes[i].uid === this.q.content_type_uid) {
                        output.content_type = contentTypes[i];
                        break;
                    }
                }
            }
            if (this.q.includeCount) {
                output.count = count;
            }
            if (this.q.isSingle) {
                data = (data.length) ? data[0] : null;
                if (this.q.only) {
                    const only = this.q.only.toString().replace(/\./g, '/');
                    data = json_mask_1.default(data, only);
                }
                else if (this.q.except) {
                    const bukcet = this.q.except.toString().replace(/\./g, '/');
                    const except = json_mask_1.default(data, bukcet);
                    data = utils_1.difference(data, except);
                }
                output[key] = /* (data.length) ? data[0] : null */ data;
                return { output };
            }
            // TODO: sorting logic
            // Experimental!
            if (this.q.hasOwnProperty('asc')) {
                data = lodash_1.sortBy(data, this.q.asc);
            }
            else if (this.q.hasOwnProperty('desc')) {
                const temp = lodash_1.sortBy(data, this.q.desc);
                data = lodash_1.reverse(temp);
            }
            if (this.q.skip) {
                data.splice(0, this.q.skip);
            }
            if (this.q.limit) {
                data = data.splice(0, this.q.limit);
            }
            if (this.q.only) {
                const only = this.q.only.toString().replace(/\./g, '/');
                data = json_mask_1.default(data, only);
            }
            else if (this.q.except) {
                const bukcet = this.q.except.toString().replace(/\./g, '/');
                const except = json_mask_1.default(data, bukcet);
                data = utils_1.difference(data, except);
            }
            output[key] = data;
            return { output };
        });
    }
    includeSpecificReferences(entries, contentTypeUid, locale, include) {
        return __awaiter(this, void 0, void 0, function* () {
            const ctQuery = {
                _content_type_uid: this.types.content_types,
                uid: contentTypeUid,
            };
            const { paths, // ref. fields in the current content types
            pendingPath, // left over of *paths*
            schemaList, } = yield this.getReferencePath(ctQuery, locale, include);
            const queries = {
                $or: [],
            }; // reference field paths
            const shelf = []; // a mapper object, that holds pointer to the original element
            // iterate over each path in the entries and fetch the references
            // while fetching, keep track of their location
            for (let i = 0, j = paths.length; i < j; i++) {
                // populates shelf and queries
                this.fetchPathDetails(entries, locale, paths[i].split('.'), queries, shelf, true, entries, 0);
            }
            // even after traversing, if no references were found, simply return the entries found thusfar
            if (shelf.length === 0) {
                return;
            }
            // else, self-recursively iterate and fetch references
            // Note: Shelf is the one holding `pointers` to the actual entry
            // Once the pointer has been used, for GC, point the object to null
            yield this.includeReferenceIteration(queries, schemaList, locale, pendingPath, shelf);
            return;
        });
    }
    includeReferenceIteration(eQuery, ctQuery, locale, include, oldShelf) {
        return __awaiter(this, void 0, void 0, function* () {
            if (oldShelf.length === 0 || ctQuery.$or.length === 0) {
                return;
            }
            const { paths, pendingPath, schemaList, } = yield this.getReferencePath(ctQuery, locale, include);
            const queries = {
                $or: [],
            };
            let result = {
                docs: [],
            };
            const shelf = [];
            yield this.subIncludeReferenceIteration(eQuery, locale, paths, include, queries, result, shelf);
            // GC to avoid mem leaks!
            eQuery = null;
            for (let i = 0, j = oldShelf.length; i < j; i++) {
                const element = oldShelf[i];
                let flag = true;
                for (let k = 0, l = result.docs.length; k < l; k++) {
                    if (result.docs[k].uid === element.uid) {
                        element.path[element.position] = result.docs[k];
                        flag = false;
                        break;
                    }
                }
                if (flag) {
                    for (let e = 0, f = oldShelf[i].path.length; e < f; e++) {
                        // tslint:disable-next-line: max-line-length
                        if (oldShelf[i].path[e].hasOwnProperty('_content_type_uid') && Object.keys(oldShelf[i].path[e]).length === 2) {
                            oldShelf[i].path.splice(e, 1);
                            break;
                        }
                    }
                }
            }
            // GC to avoid mem leaks!
            oldShelf = null;
            result = null;
            // Iterative loops, that traverses paths and binds them onto entries
            yield this.includeReferenceIteration(queries, schemaList, locale, pendingPath, shelf);
            return;
        });
    }
    subIncludeReferenceIteration(eQuieries, locale, paths, include, queries, result, shelf) {
        return __awaiter(this, void 0, void 0, function* () {
            const { contentTypes, aggQueries, } = utils_1.segregateQueries(eQuieries.$or);
            const promises = [];
            contentTypes.forEach((contentType) => {
                promises.push(this.fetchDocuments(aggQueries[contentType], locale, contentType, paths, include, queries, result, shelf));
            });
            // wait for all promises to be resolved
            yield Promise.all(promises);
            return {
                queries,
                result,
                shelf,
            };
        });
    }
    getReferencePath(query, locale, currentInclude) {
        return __awaiter(this, void 0, void 0, function* () {
            const data = yield fs_1.readFile(utils_1.getContentTypesPath(locale) + '.json');
            const schemas = data.filter(sift_1.default(query));
            const pendingPath = [];
            const schemasReferred = [];
            const paths = [];
            const schemaList = {
                $or: [],
            };
            if (schemas.length === 0) {
                return {
                    paths,
                    pendingPath,
                    schemaList,
                };
            }
            let entryReferences = {};
            schemas.forEach((schema) => {
                // Entry references
                entryReferences = lodash_1.merge(entryReferences, schema[this.types.references]);
                // tslint:disable-next-line: forin
                for (const path in schema[this.types.assets]) {
                    paths.push(path);
                }
            });
            for (let i = 0, j = currentInclude.length; i < j; i++) {
                const includePath = currentInclude[i];
                // tslint:disable-next-line: forin
                for (const path in entryReferences) {
                    const subStr = includePath.slice(0, path.length);
                    if (subStr === path) {
                        let subPath;
                        // Its the complete path!! Hurrah!
                        if (path.length !== includePath.length) {
                            subPath = subStr;
                            pendingPath.push(includePath.slice(path.length + 1));
                        }
                        else {
                            subPath = includePath;
                        }
                        if (typeof entryReferences[path] === 'string') {
                            schemasReferred.push({
                                _content_type_uid: this.types.content_types,
                                uid: entryReferences[path],
                            });
                        }
                        else if (entryReferences[path].length) {
                            entryReferences[path].forEach((contentTypeUid) => {
                                schemasReferred.push({
                                    _content_type_uid: this.types.content_types,
                                    uid: contentTypeUid,
                                });
                            });
                        }
                        paths.push(subPath);
                        break;
                    }
                }
            }
            schemaList.$or = schemasReferred;
            return {
                // path, that's possible in the current schema
                paths,
                // paths, that's yet to be traversed
                pendingPath,
                // schemas, to be loaded!
                schemaList,
            };
        });
    }
    // tslint:disable-next-line: max-line-length
    fetchPathDetails(data, locale, pathArr, queryBucket, shelf, assetsOnly = false, parent, pos, counter = 0) {
        if (counter === (pathArr.length)) {
            if (data && typeof data === 'object') {
                if (data instanceof Array && data.length) {
                    data.forEach((elem, idx) => {
                        if (typeof elem === 'string') {
                            queryBucket.$or.push({
                                _content_type_uid: this.types.assets,
                                _version: { $exists: true },
                                locale,
                                uid: elem,
                            });
                            shelf.push({
                                path: data,
                                position: idx,
                                uid: elem,
                            });
                        }
                        else if (elem && typeof elem === 'object' && elem.hasOwnProperty('_content_type_uid')) {
                            queryBucket.$or.push({
                                _content_type_uid: elem._content_type_uid,
                                locale,
                                uid: elem.uid,
                            });
                            shelf.push({
                                path: data,
                                position: idx,
                                uid: elem.uid,
                            });
                        }
                    });
                }
                else if (typeof data === 'object') {
                    if (data.hasOwnProperty('_content_type_uid')) {
                        queryBucket.$or.push({
                            _content_type_uid: data._content_type_uid,
                            locale,
                            uid: data.uid,
                        });
                        shelf.push({
                            path: parent,
                            position: pos,
                            uid: data.uid,
                        });
                    }
                }
            }
            else if (typeof data === 'string') {
                queryBucket.$or.push({
                    _content_type_uid: this.types.assets,
                    _version: { $exists: true },
                    locale,
                    uid: data,
                });
                shelf.push({
                    path: parent,
                    position: pos,
                    uid: data,
                });
            }
        }
        else {
            const currentField = pathArr[counter];
            counter++;
            if (data instanceof Array) {
                // tslint:disable-next-line: prefer-for-of
                for (let i = 0; i < data.length; i++) {
                    if (data[i][currentField]) {
                        this.fetchPathDetails(data[i][currentField], locale, pathArr, queryBucket, shelf, assetsOnly, data[i], currentField, counter);
                    }
                }
            }
            else {
                if (data[currentField]) {
                    this.fetchPathDetails(data[currentField], locale, pathArr, queryBucket, shelf, assetsOnly, data, currentField, counter);
                }
            }
        }
        // since we've reached last of the paths, return!
        return;
    }
    // tslint:disable-next-line: max-line-length
    fetchDocuments(query, locale, contentTypeUid, paths, include, queries, result, bookRack, includeAll = false) {
        return __awaiter(this, void 0, void 0, function* () {
            let contents;
            if (contentTypeUid === this.types.assets) {
                contents = yield fs_1.readFile(utils_1.getAssetsPath(locale) + '.json');
            }
            else {
                contents = yield fs_1.readFile(utils_1.getEntriesPath(locale, contentTypeUid) + '.json');
            }
            result.docs = result.docs.concat(contents.filter(sift_1.default(query)));
            result.docs.forEach((doc) => {
                this.projections.forEach((key) => {
                    if (doc.hasOwnProperty(key)) {
                        delete doc[key];
                    }
                });
            });
            if (result.length === 0) {
                return;
            }
            if (include.length || includeAll) {
                paths.forEach((path) => {
                    this.fetchPathDetails(result.docs, locale, path.split('.'), queries, bookRack, false, result, 0);
                });
            }
            else {
                // if there are no includes, only fetch assets
                paths.forEach((path) => {
                    this.fetchPathDetails(result.docs, locale, path.split('.'), queries, bookRack, true, result, 0);
                });
            }
            return;
        });
    }
    includeAssetsOnly(entries, locale, contentTypeUid) {
        return __awaiter(this, void 0, void 0, function* () {
            const schemas = yield fs_1.readFile(utils_1.getContentTypesPath(locale) + '.json');
            let schema;
            for (let i = 0, j = schemas.length; i < j; i++) {
                if (schemas[i].uid === contentTypeUid) {
                    schema = schemas[i];
                    break;
                }
            }
            // should not enter this section
            // if the schema doesn't exist, error should have occurred before
            if (typeof schema === 'undefined' || typeof schema[this.types.assets] !== 'object') {
                return;
            }
            const paths = Object.keys(schema[this.types.assets]);
            const shelf = [];
            const queryBucket = {
                $or: [],
            };
            for (let i = 0, j = paths.length; i < j; i++) {
                this.fetchPathDetails(entries, locale, paths[i].split('.'), queryBucket, shelf, true, entries, 0);
            }
            if (shelf.length === 0) {
                return;
            }
            const assets = yield fs_1.readFile(utils_1.getAssetsPath(locale) + '.json');
            // might not be required
            const filteredAssets = assets.filter(sift_1.default(queryBucket));
            for (let l = 0, m = shelf.length; l < m; l++) {
                for (let n = 0, o = filteredAssets.length; n < o; n++) {
                    if (shelf[l].uid === filteredAssets[n].uid) {
                        shelf[l].path[shelf[l].position] = filteredAssets[n];
                        break;
                    }
                }
            }
            return;
        });
    }
    bindReferences(entries, contentTypeUid, locale) {
        return __awaiter(this, void 0, void 0, function* () {
            const ctQuery = {
                $or: [{
                        _content_type_uid: this.types.content_types,
                        uid: contentTypeUid,
                    }],
            };
            const { paths, // ref. fields in the current content types
            ctQueries, } = yield this.getAllReferencePaths(ctQuery, locale);
            const queries = {
                $or: [],
            }; // reference field paths
            const objectPointerList = []; // a mapper object, that holds pointer to the original element
            // iterate over each path in the entries and fetch the references
            // while fetching, keep track of their location
            for (let i = 0, j = paths.length; i < j; i++) {
                this.fetchPathDetails(entries, locale, paths[i].split('.'), queries, objectPointerList, true, entries, 0);
            }
            // even after traversing, if no references were found, simply return the entries found thusfar
            if (objectPointerList.length === 0) {
                return entries;
            }
            // else, self-recursively iterate and fetch references
            // Note: Shelf is the one holding `pointers` to the actual entry
            // Once the pointer has been used, for GC, point the object to null
            return this.includeAllReferencesIteration(queries, ctQueries, locale, objectPointerList);
        });
    }
    // tslint:disable-next-line: max-line-length
    includeAllReferencesIteration(oldEntryQueries, oldCtQueries, locale, oldObjectPointerList, depth = 0) {
        return __awaiter(this, void 0, void 0, function* () {
            if (depth > this.q.referenceDepth || oldObjectPointerList.length === 0 || oldCtQueries.$or.length === 0) {
                return;
            }
            const { ctQueries, paths, } = yield this.getAllReferencePaths(oldCtQueries, locale);
            // GC to aviod mem leaks
            oldCtQueries = null;
            const queries = {
                $or: [],
            };
            let result = {
                docs: [],
            };
            const shelf = [];
            yield this.subIncludeAllReferencesIteration(oldEntryQueries, locale, paths, queries, result, shelf);
            // GC to avoid mem leaks!
            oldEntryQueries = null;
            for (let i = 0, j = oldObjectPointerList.length; i < j; i++) {
                const element = oldObjectPointerList[i];
                let flag = true;
                for (let k = 0, l = result.docs.length; k < l; k++) {
                    if (result.docs[k].uid === element.uid) {
                        element.path[element.position] = result.docs[k];
                        flag = false;
                        break;
                    }
                }
                if (flag) {
                    for (let e = 0, f = oldObjectPointerList[i].path.length; e < f; e++) {
                        // tslint:disable-next-line: max-line-length
                        if (oldObjectPointerList[i].path[e].hasOwnProperty('_content_type_uid') && Object.keys(oldObjectPointerList[i].path[e]).length === 2) {
                            oldObjectPointerList[i].path.splice(e, 1);
                            break;
                        }
                    }
                }
            }
            // GC to avoid mem leaks!
            oldObjectPointerList = null;
            result = null;
            ++depth;
            // Iterative loops, that traverses paths and binds them onto entries
            yield this.includeAllReferencesIteration(queries, ctQueries, locale, shelf, depth);
            return;
        });
    }
    subIncludeAllReferencesIteration(eQuieries, locale, paths, queries, result, shelf) {
        return __awaiter(this, void 0, void 0, function* () {
            const { contentTypes, aggQueries, } = utils_1.segregateQueries(eQuieries.$or);
            const promises = [];
            contentTypes.forEach((contentType) => {
                promises.push(this.fetchDocuments(aggQueries[contentType], locale, contentType, paths, [], queries, result, shelf, true));
            });
            // wait for all promises to be resolved
            yield Promise.all(promises);
            return {
                queries,
                result,
                shelf,
            };
        });
    }
    getAllReferencePaths(contentTypeQueries, locale) {
        return __awaiter(this, void 0, void 0, function* () {
            const contents = yield fs_1.readFile(utils_1.getContentTypesPath(locale) + '.json');
            const filteredContents = contents.filter(sift_1.default(contentTypeQueries));
            const ctQueries = {
                $or: [],
            };
            let paths = [];
            for (let i = 0, j = filteredContents.length; i < j; i++) {
                let assetFieldPaths;
                let entryReferencePaths;
                if (filteredContents[i].hasOwnProperty(this.types.assets)) {
                    assetFieldPaths = Object.keys(filteredContents[i][this.types.assets]);
                    paths = paths.concat(assetFieldPaths);
                }
                if (filteredContents[i].hasOwnProperty('_references')) {
                    entryReferencePaths = Object.keys(filteredContents[i][this.types.references]);
                    paths = paths.concat(entryReferencePaths);
                    for (let k = 0, l = entryReferencePaths.length; k < l; k++) {
                        if (typeof filteredContents[i][this.types.references][entryReferencePaths[k]] === 'string') {
                            ctQueries.$or.push({
                                _content_type_uid: this.types.content_types,
                                // this would probably make it slow in FS, avoid this?
                                // locale,
                                uid: filteredContents[i][this.types.references][entryReferencePaths[k]],
                            });
                        }
                        else if (filteredContents[i][this.types.references][entryReferencePaths[k]].length) {
                            filteredContents[i][this.types.references][entryReferencePaths[k]].forEach((uid) => {
                                ctQueries.$or.push({
                                    _content_type_uid: this.types.content_types,
                                    // Question: Adding extra key in query, slows querying down? Probably yes.
                                    // locale,
                                    uid,
                                });
                            });
                        }
                    }
                }
            }
            return {
                ctQueries,
                paths,
            };
        });
    }
}
exports.Stack = Stack;