"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;