lib/data-store/index.js
"use strict";
module.exports = DataStore;
const _ = require("lodash");
const ono = require("@jsdevtools/ono");
const util = require("../helpers/util");
const Resource = require("./resource");
require("./buffer-polyfill");
/**
* An abstract base class for data-storage of REST resources.
*
* @constructor
*/
function DataStore () {
/**
* The Express Application or Router that's used to determine case-sensitivity and/or strict matching
* of collection paths and resource names.
*
* @type {express#Router}
* @protected
*/
this.__router = {};
}
/**
* Returns the given resource.
*
* @param {Resource|string} resource - The resource (path) or Resource object to be retrieved
* @param {function} callback
* - An error-first callback. The second parameter is the {@link Resource} object,
* or undefined if no match was found.
*/
DataStore.prototype.get = function (resource, callback) {
let self = this;
if (_.isString(resource)) {
resource = new Resource(resource);
}
openCollection(self, resource, (err, collection, resources) => {
if (err) {
doCallback(callback, err);
}
else {
// Find the resource
resource = _.find(resources, resource.filter(self.__router));
doCallback(callback, null, resource);
}
});
};
// noinspection JSClosureCompilerSyntax
/**
* Saves the given resource(s) to the data store.
* If any of the resources already exist, the new data is merged with the existing data.
*
* @param {...Resource|Resource[]} resources
* - The resource(s) or array of resources to be saved
*
* @param {function} [callback]
* - An error-first callback. The second parameter is the {@link Resource} object
* or array of {@link Resource} objects that were saved.
*/
DataStore.prototype.save = function (resources, callback) {
call(this, save, arguments);
};
/**
* Removes the given resource from the data store.
*
* @param {...Resource|Resource[]} resources
* - The resource(s) or array of resources to be removed
*
* @param {function} [callback]
* - An error-first callback. The second parameter is the {@link Resource} object
* or array of {@link Resource} objects that were removed.
*/
DataStore.prototype.delete = DataStore.prototype.remove = function (resources, callback) {
call(this, remove, arguments);
};
/**
* Returns all resources in the given collection.
*
* @param {string} collection
* - The collection path (e.g. "/", "/users", "/users/jdoe/orders/")
*
* @param {function} callback
* - An error-first callback. The second parameter is the array of {@link Resource} objects in the collection.
* If there are no resources for the given collection, then the array is empty.
*/
DataStore.prototype.getCollection = function (collection, callback) {
let self = this;
openCollection(self, collection, (err, collection, resources) => {
if (err) {
doCallback(callback, err);
}
else {
// Return the resources in the collection
resources = _.filter(resources, collection.filter(self.__router, true));
doCallback(callback, null, resources);
}
});
};
/**
* Removes all resources in the given collection.
*
* @param {string} collection
* - The collection path (e.g. "/", "/users", "/users/jdoe/orders/")
*
* @param {function} callback
* - An error-first callback. The second parameter is the array of {@link Resource} objects that were deleted.
* If nothing was deleted, then the array is empty.
*/
DataStore.prototype.deleteCollection = DataStore.prototype.removeCollection = function (collection, callback) {
let self = this;
openCollection(self, collection, (err, collection, resources) => {
if (err) {
doCallback(callback, err);
}
else {
// Remove all resources in the collection
let removed = _.remove(resources, collection.filter(self.__router, true));
if (removed.length > 0) {
// Normalize the collection name
let collectionName = collection.valueOf(self.__router, true);
// Save the changes
self.__saveDataStore(collectionName, resources, (err) => {
if (err) {
doCallback(callback, err);
}
else {
doCallback(callback, null, removed);
}
});
}
else {
doCallback(callback, null, []);
}
}
});
};
/* istanbul ignore next: abstract method */
/**
* Opens the underlying data-store and returns its data.
* Depending on the implementation, this may be the contents of a flat file, a database query, etc. instead.
*
* @param {string} collection
* - The Resource collection that is being operated upon.
* Some DataStore implementations may use this to determine which data to return.
*
* @param {function} callback
* - An error-first callback. The second parameter is an array of {@link Resource} objects
* that correspond to the given `collection` and `name`.
*
* @protected
*/
DataStore.prototype.__openDataStore = function (collection, callback) {};
/* istanbul ignore next: abstract method */
/**
* Persists changes to the underlying data-store.
* Depending on the implementation, this may write to a flat file, a database, etc. instead.
*
* @param {string} collection
* - The Resource collection that is being operated upon.
* Some DataStore implementations may use this to determine which data to persist/overwrite.
*
* @param {Resource[]} resources
* - An array of {@link Resource} objects that should be persisted to the underlying data-store.
*
* @param {function} callback
* - An error-first callback. Called when the data has been persisted, or an error occurs.
*
* @protected
*/
DataStore.prototype.__saveDataStore = function (collection, resources, callback) {};
/**
* Saves the given resources to the data store.
* If any of the resources already exist, the new data is merged with the existing data.
*
* @param {DataStore} dataStore - The DataStore to operate on
* @param {string} collectionName - The collection that all the resources belong to
* @param {Resource[]} resources - The Resources to be saved
* @param {function} callback - Callback function
*/
function save (dataStore, collectionName, resources, callback) {
// Open the data store
dataStore.__openDataStore(collectionName, (err, existingResources) => {
if (err) {
return callback(err);
}
resources.forEach((resource) => {
// Set the timestamp properties
let now = Date.now();
resource.createdOn = new Date(now);
resource.modifiedOn = new Date(now);
// Does the resource already exist?
let existing = _.find(existingResources, resource.filter(dataStore.__router));
if (existing) {
// Merge the new data into the existing resource
util.debug("%s already exists. Merging new data with existing data.", resource.toString());
existing.merge(resource);
// Update the calling code's reference to the resource
_.extend(resource, existing);
}
else {
existingResources.push(resource);
}
});
// Save the changes
dataStore.__saveDataStore(collectionName, existingResources, (err) => {
callback(err, resources);
});
});
}
/**
* Removes the given resource from the data store.
*
* @param {DataStore} dataStore - The DataStore to operate on
* @param {string} collectionName - The collection that all the resources belong to
* @param {Resource[]} resources - The Resources to be removed
* @param {function} callback - Callback function
*/
function remove (dataStore, collectionName, resources, callback) {
// Open the data store
dataStore.__openDataStore(collectionName, (err, existingResources) => {
if (err) {
return callback(err);
}
// Remove the resources from the existing resources
let removedResources = [];
resources.forEach((resource) => {
let removed = _.remove(existingResources, resource.filter(dataStore.__router));
removedResources = removedResources.concat(removed);
});
if (removedResources.length > 0) {
// Save the changes
dataStore.__saveDataStore(collectionName, existingResources, (err) => {
if (err) {
callback(err);
}
else {
callback(null, removedResources);
}
});
}
else {
callback(null, []);
}
});
}
/**
* Opens the given collection.
*
* @param {DataStore} dataStore - The DataStore to operate on
* @param {string|Resource} collection - The collection path or a Resource object
* @param {function} callback - Called with Error, collection Resource, and Resource array
*/
function openCollection (dataStore, collection, callback) {
if (_.isString(collection)) {
collection = new Resource(collection, "", "");
}
else if (!(collection instanceof Resource)) {
throw ono("Expected a string or Resource object. Got a %s instead.", typeof (collection));
}
// Normalize the collection name
let collectionName = collection.valueOf(dataStore.__router, true);
// Open the data store
dataStore.__openDataStore(collectionName, (err, resources) => {
callback(err, collection, resources);
});
}
/**
* Calls the given callback with the given arguments, if the callback is defined.
*
* @param {function|*} callback
* @param {Error|null} err
* @param {*} arg
*/
function doCallback (callback, err, arg) {
if (_.isFunction(callback)) {
callback(err, arg);
}
}
/**
* Calls the given function with normalized parameters:
* the DataStore, collection name, an array of {@link Resource} objects, and a callback function.
*
* The given function might be called multiple times. Each time it is called, the array of resources
* will all belong to the same collection.
*
* @param {DataStore} dataStore - The DataStore to operate on
* @param {function} fn - The function to be called
* @param {arguments} args - The non-normalized arguments (one resource, multiple resources, a resource array, a callback)
*/
function call (dataStore, fn, args) {
let resources, callback;
// If only a single resource was passed-in, then only a single resource will be passed-back
let singleResource = _.first(args) instanceof Resource && (args.length === 0 || _.isFunction(args[1]));
// Normalize the arguments
if (_.isFunction(_.last(args))) {
resources = _.flatten(_.initial(args), true);
callback = _.last(args);
}
else {
resources = _.flatten(args, true);
callback = _.noop;
}
// Group the resources into collections
let collections = {};
for (let i = 0; i < resources.length; i++) {
let resource = resources[i];
if (!(resource instanceof Resource)) {
throw ono("Expected a Resource object, but parameter %d is a %s.", i + 1, typeof (resource));
}
let collectionName = resource.valueOf(dataStore.__router, true);
let collection = collections[collectionName] || (collections[collectionName] = []);
collection.push(resource);
}
// Call the function for each collection of resources
let collectionNames = Object.keys(collections);
let collectionIndex = 0, processedResources = [];
processNextCollection();
function processNextCollection (err, resources) {
if (err) {
// An error occurred, so abort.
finished(err);
return;
}
if (resources) {
// Add the resources to the list of processed resources
processedResources = processedResources.concat(resources);
}
if (collectionIndex >= collectionNames.length) {
// We're done processing all collections, so return the results
finished(null, processedResources);
}
else {
// Process the next collection
let collectionName = collectionNames[collectionIndex++];
fn(dataStore, collectionName, collections[collectionName], processNextCollection);
}
}
function finished (err, resources) {
if (err) {
callback(err);
}
else {
// Call the callback with a single resource or an array of resources
callback(null, singleResource ? resources[0] : resources);
}
}
}