loopback-datasource-juggler/lib/connectors/memory.js

947 lines
26 KiB
JavaScript
Raw Normal View History

// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2016-04-01 22:25:16 +00:00
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2016-08-22 19:55:22 +00:00
'use strict';
/* global window:false */
2018-12-07 14:54:29 +00:00
const g = require('strong-globalize')();
const util = require('util');
const Connector = require('loopback-connector').Connector;
const geo = require('../geo');
const utils = require('../utils');
const fs = require('fs');
const async = require('async');
const debug = require('debug')('loopback:connector:memory');
2013-06-24 22:21:59 +00:00
/**
2014-08-30 08:53:10 +00:00
* Initialize the Memory connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @param {Function} [callback] The callback function
*/
exports.initialize = function initializeDataSource(dataSource, callback) {
dataSource.connector = new Memory(null, dataSource.settings);
// Use dataSource.connect to avoid duplicate file reads from cache
dataSource.connect(callback);
2011-10-03 13:36:43 +00:00
};
2013-06-26 03:31:00 +00:00
exports.Memory = Memory;
2014-07-27 14:30:45 +00:00
exports.applyFilter = applyFilter;
2013-06-26 03:31:00 +00:00
function Memory(m, settings) {
if (m instanceof Memory) {
2014-01-24 17:09:53 +00:00
this.isTransaction = true;
this.cache = m.cache;
this.ids = m.ids;
this.constructor.super_.call(this, 'memory', settings);
2014-01-24 17:09:53 +00:00
this._models = m._models;
} else {
this.isTransaction = false;
this.cache = {};
this.ids = {};
this.constructor.super_.call(this, 'memory', settings);
2014-01-24 17:09:53 +00:00
}
2011-10-03 13:36:43 +00:00
}
util.inherits(Memory, Connector);
Memory.prototype.getDefaultIdType = function() {
return Number;
};
Memory.prototype.getTypes = function() {
return ['db', 'nosql', 'memory'];
};
2016-04-01 11:48:17 +00:00
Memory.prototype.connect = function(callback) {
2014-01-24 17:09:53 +00:00
if (this.isTransaction) {
this.onTransactionExec = callback;
} else {
this.loadFromFile(callback);
}
};
function serialize(obj) {
2016-04-01 11:48:17 +00:00
if (obj === null || obj === undefined) {
return obj;
}
return JSON.stringify(obj);
}
function deserialize(dbObj) {
2016-04-01 11:48:17 +00:00
if (dbObj === null || dbObj === undefined) {
return dbObj;
}
2016-04-01 11:48:17 +00:00
if (typeof dbObj === 'string') {
return JSON.parse(dbObj);
} else {
return dbObj;
}
}
Memory.prototype.getCollection = function(model) {
2018-12-07 14:54:29 +00:00
const modelClass = this._models[model];
if (modelClass && modelClass.settings.memory) {
model = modelClass.settings.memory.collection || model;
}
return model;
2016-04-01 11:48:17 +00:00
};
Memory.prototype.initCollection = function(model) {
this.collection(model, {});
this.collectionSeq(model, 1);
2016-04-01 11:48:17 +00:00
};
Memory.prototype.collection = function(model, val) {
model = this.getCollection(model);
if (arguments.length > 1) this.cache[model] = val;
return this.cache[model];
};
Memory.prototype.collectionSeq = function(model, val) {
model = this.getCollection(model);
if (arguments.length > 1) this.ids[model] = val;
return this.ids[model];
};
/**
* Create a queue to serialize file read/write operations
* @returns {*} The file operation queue
*/
Memory.prototype.setupFileQueue = function() {
2018-12-07 14:54:29 +00:00
const self = this;
if (!this.fileQueue) {
// Create a queue for writes
this.fileQueue = async.queue(function(task, done) {
2018-12-07 14:54:29 +00:00
const callback = task.callback || function() {};
const file = self.settings.file;
if (task.operation === 'write') {
// Flush out the models/ids
2018-12-07 15:22:36 +00:00
const data = JSON.stringify({
ids: self.ids,
models: self.cache,
}, null, ' ');
debug('Writing cache to %s: %s', file, data);
fs.writeFile(file, data, function(err) {
debug('Cache has been written to %s', file);
done(err);
callback(err, task.data);
});
} else if (task.operation === 'read') {
2018-12-07 15:22:36 +00:00
debug('Reading cache from %s', file);
fs.readFile(file, {
encoding: 'utf8',
flag: 'r',
}, function(err, data) {
if (err && err.code !== 'ENOENT') {
done(err);
callback(err);
} else {
debug('Cache has been read from %s: %s', file, data);
self.parseAndLoad(data, function(err) {
done(err);
callback(err);
});
}
});
} else {
2018-12-07 14:54:29 +00:00
const err = new Error('Unknown type of task');
done(err);
callback(err);
}
}, 1);
}
return this.fileQueue;
};
Memory.prototype.parseAndLoad = function(data, callback) {
if (data) {
try {
data = JSON.parse(data.toString());
} catch (e) {
return callback && callback(e);
}
this.ids = data.ids || {};
this.cache = data.models || {};
} else {
if (!this.cache) {
this.ids = {};
this.cache = {};
}
}
callback && callback();
};
Memory.prototype.loadFromFile = function(callback) {
2018-12-07 14:54:29 +00:00
const hasLocalStorage = typeof window !== 'undefined' && window.localStorage;
const localStorage = hasLocalStorage && this.settings.localStorage;
2014-05-07 14:47:12 +00:00
if (this.settings.file) {
debug('Queueing read %s', this.settings.file);
this.setupFileQueue().push({
operation: 'read',
callback: callback,
});
2016-04-01 11:48:17 +00:00
} else if (localStorage) {
2018-12-07 14:54:29 +00:00
let data = window.localStorage.getItem(localStorage);
2014-05-07 14:47:12 +00:00
data = data || '{}';
this.parseAndLoad(data, callback);
2014-01-24 17:09:53 +00:00
} else {
process.nextTick(callback);
}
2013-04-01 13:49:12 +00:00
};
/*!
* Flush the cache into the json file if necessary
* @param {Function} callback
*/
2016-04-01 11:48:17 +00:00
Memory.prototype.saveToFile = function(result, callback) {
2018-12-07 14:54:29 +00:00
const file = this.settings.file;
const hasLocalStorage = typeof window !== 'undefined' && window.localStorage;
const localStorage = hasLocalStorage && this.settings.localStorage;
2014-05-07 14:47:12 +00:00
if (file) {
debug('Queueing write %s', this.settings.file);
// Enqueue the write
this.setupFileQueue().push({
operation: 'write',
data: result,
2016-04-01 11:48:17 +00:00
callback: callback,
});
2014-05-07 14:47:12 +00:00
} else if (localStorage) {
// Flush out the models/ids
2018-12-07 14:54:29 +00:00
const data = JSON.stringify({
ids: this.ids,
models: this.cache,
2014-05-07 14:47:12 +00:00
}, null, ' ');
window.localStorage.setItem(localStorage, data);
2016-04-01 11:48:17 +00:00
process.nextTick(function() {
2014-05-07 14:47:12 +00:00
callback && callback(null, result);
});
} else {
2016-04-01 11:48:17 +00:00
process.nextTick(function() {
callback && callback(null, result);
});
}
};
2013-10-03 16:14:24 +00:00
Memory.prototype.define = function defineModel(definition) {
2014-01-24 17:09:53 +00:00
this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments));
2018-12-07 14:54:29 +00:00
const m = definition.model.modelName;
2016-04-01 11:48:17 +00:00
if (!this.collection(m)) this.initCollection(m);
2011-10-03 13:36:43 +00:00
};
Memory.prototype._createSync = function(model, data, fn) {
2014-01-24 17:09:53 +00:00
// FIXME: [rfeng] We need to generate unique ids based on the id type
// FIXME: [rfeng] We don't support composite ids yet
2018-12-07 14:54:29 +00:00
let currentId = this.collectionSeq(model);
if (currentId === undefined) { // First time
currentId = this.collectionSeq(model, 1);
2014-01-24 17:09:53 +00:00
}
2018-12-07 14:54:29 +00:00
let id = this.getIdValue(model, data) || currentId;
2014-01-24 17:09:53 +00:00
if (id > currentId) {
// If the id is passed in and the value is greater than the current id
currentId = id;
}
this.collectionSeq(model, Number(currentId) + 1);
2014-01-24 17:09:53 +00:00
2018-12-07 14:54:29 +00:00
const props = this._models[model].properties;
const idName = this.idName(model);
2014-01-24 17:09:53 +00:00
id = (props[idName] && props[idName].type && props[idName].type(id)) || id;
this.setIdValue(model, data, id);
if (!this.collection(model)) {
this.collection(model, {});
}
2017-02-03 19:40:08 +00:00
if (this.collection(model)[id]) {
2018-12-07 14:54:29 +00:00
const error = new Error(g.f('Duplicate entry for %s.%s', model, idName));
2017-02-03 19:40:08 +00:00
error.statusCode = error.status = 409;
return fn(error);
}
this.collection(model)[id] = serialize(data);
fn(null, id);
};
Memory.prototype.create = function create(model, data, options, callback) {
2018-12-07 14:54:29 +00:00
const self = this;
this._createSync(model, data, function(err, id) {
if (err) {
return process.nextTick(function() {
callback(err);
});
}
self.saveToFile(id, callback);
});
2011-10-03 13:36:43 +00:00
};
2016-04-01 11:48:17 +00:00
Memory.prototype.updateOrCreate = function(model, data, options, callback) {
2018-12-07 14:54:29 +00:00
const self = this;
2016-04-01 11:48:17 +00:00
this.exists(model, self.getIdValue(model, data), options, function(err, exists) {
2014-01-24 17:09:53 +00:00
if (exists) {
self.save(model, data, options, function(err, data) {
2016-08-19 17:46:59 +00:00
callback(err, data, {isNewInstance: false});
});
2014-01-24 17:09:53 +00:00
} else {
2016-04-01 11:48:17 +00:00
self.create(model, data, options, function(err, id) {
2014-01-24 17:09:53 +00:00
self.setIdValue(model, data, id);
2016-08-19 17:46:59 +00:00
callback(err, data, {isNewInstance: true});
2014-01-24 17:09:53 +00:00
});
}
});
2012-03-22 19:46:16 +00:00
};
Memory.prototype.patchOrCreateWithWhere =
Memory.prototype.upsertWithWhere = function(model, where, data, options, callback) {
2018-12-07 14:54:29 +00:00
const self = this;
const primaryKey = this.idName(model);
const filter = {where: where};
const nodes = self._findAllSkippingIncludes(model, filter);
if (nodes.length === 0) {
return self._createSync(model, data, function(err, id) {
if (err) return process.nextTick(function() { callback(err); });
self.saveToFile(id, function(err, id) {
self.setIdValue(model, data, id);
2016-08-19 17:46:59 +00:00
callback(err, self.fromDb(model, data), {isNewInstance: true});
});
});
}
if (nodes.length === 1) {
2018-12-07 14:54:29 +00:00
const primaryKeyValue = nodes[0][primaryKey];
self.updateAttributes(model, primaryKeyValue, data, options, function(err, data) {
2016-08-19 17:46:59 +00:00
callback(err, data, {isNewInstance: false});
});
} else {
process.nextTick(function() {
2018-12-07 14:54:29 +00:00
const error = new Error('There are multiple instances found.' +
'Upsert Operation will not be performed!');
error.statusCode = 400;
callback(error);
});
}
};
2018-11-12 21:54:22 +00:00
Memory.prototype.findOrCreate = function(model, filter, data, options, callback) {
2018-12-07 14:54:29 +00:00
const self = this;
const nodes = self._findAllSkippingIncludes(model, filter);
const found = nodes[0];
2016-04-01 11:48:17 +00:00
if (!found) {
// Calling _createSync to update the collection in a sync way and to guarantee to create it in the same turn of even loop
return self._createSync(model, data, function(err, id) {
if (err) return callback(err);
self.saveToFile(id, function(err, id) {
self.setIdValue(model, data, id);
callback(err, data, true);
});
});
}
if (!filter || !filter.include) {
return process.nextTick(function() {
callback(null, found, false);
});
}
2016-04-01 11:48:17 +00:00
2018-11-12 21:54:22 +00:00
self._models[model].model.include(nodes[0], filter.include, options, function(err, nodes) {
process.nextTick(function() {
if (err) return callback(err);
callback(null, nodes[0], false);
});
});
};
Memory.prototype.save = function save(model, data, options, callback) {
2018-12-07 14:54:29 +00:00
const self = this;
const id = this.getIdValue(model, data);
const cachedModels = this.collection(model);
let modelData = cachedModels && this.collection(model)[id];
modelData = modelData && deserialize(modelData);
if (modelData) {
data = merge(modelData, data);
}
this.collection(model)[id] = serialize(data);
this.saveToFile(data, function(err) {
2016-08-19 17:46:59 +00:00
callback(err, self.fromDb(model, data), {isNewInstance: !modelData});
});
2011-10-03 13:36:43 +00:00
};
Memory.prototype.exists = function exists(model, id, options, callback) {
2016-04-01 11:48:17 +00:00
process.nextTick(function() {
callback(null, this.collection(model) && this.collection(model).hasOwnProperty(id));
2014-01-24 17:09:53 +00:00
}.bind(this));
2011-10-03 13:36:43 +00:00
};
Memory.prototype.find = function find(model, id, options, callback) {
2016-04-01 11:48:17 +00:00
process.nextTick(function() {
callback(null, id in this.collection(model) && this.fromDb(model, this.collection(model)[id]));
2014-01-24 17:09:53 +00:00
}.bind(this));
2011-10-03 13:36:43 +00:00
};
Memory.prototype.destroy = function destroy(model, id, options, callback) {
2018-12-07 14:54:29 +00:00
const exists = this.collection(model)[id];
delete this.collection(model)[id];
2016-08-19 17:46:59 +00:00
this.saveToFile({count: exists ? 1 : 0}, callback);
2011-10-03 13:36:43 +00:00
};
2016-04-01 11:48:17 +00:00
Memory.prototype.fromDb = function(model, data) {
2014-01-24 17:09:53 +00:00
if (!data) return null;
data = deserialize(data);
2018-12-07 14:54:29 +00:00
const props = this._models[model].properties;
for (const key in data) {
let val = data[key];
2014-01-24 17:09:53 +00:00
if (val === undefined || val === null) {
continue;
}
if (props[key]) {
switch (props[key].type.name) {
case 'Date':
val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
break;
case 'Boolean':
val = Boolean(val);
break;
case 'Number':
val = Number(val);
break;
}
}
2014-01-24 17:09:53 +00:00
data[key] = val;
}
return data;
2013-04-06 10:50:23 +00:00
};
function getValue(obj, path) {
if (obj == null) {
return undefined;
}
2018-12-07 14:54:29 +00:00
const keys = path.split('.');
let val = obj;
for (let i = 0, n = keys.length; i < n; i++) {
val = val[keys[i]];
if (val == null) {
return val;
}
}
return val;
}
Memory.prototype._findAllSkippingIncludes = function(model, filter) {
2018-12-07 14:54:29 +00:00
let nodes = Object.keys(this.collection(model)).map(function(key) {
return this.fromDb(model, this.collection(model)[key]);
2014-01-24 17:09:53 +00:00
}.bind(this));
2014-01-24 17:09:53 +00:00
if (filter) {
if (!filter.order) {
2018-12-07 14:54:29 +00:00
const idNames = this.idNames(model);
if (idNames && idNames.length) {
filter.order = idNames;
}
}
2014-01-24 17:09:53 +00:00
// do we need some sorting?
if (filter.order) {
2018-12-07 14:54:29 +00:00
let orders = filter.order;
2016-04-01 11:48:17 +00:00
if (typeof filter.order === 'string') {
2014-01-24 17:09:53 +00:00
orders = [filter.order];
}
2016-04-01 11:48:17 +00:00
orders.forEach(function(key, i) {
2018-12-07 14:54:29 +00:00
let reverse = 1;
const m = key.match(/\s+(A|DE)SC$/i);
2014-01-24 17:09:53 +00:00
if (m) {
key = key.replace(/\s+(A|DE)SC/i, '');
if (m[1].toLowerCase() === 'de') reverse = -1;
}
2016-08-19 17:46:59 +00:00
orders[i] = {'key': key, 'reverse': reverse};
2014-01-24 17:09:53 +00:00
});
nodes = nodes.sort(sorting.bind(orders));
}
2018-12-07 14:54:29 +00:00
const nearFilter = geo.nearFilter(filter.where);
2013-03-26 20:50:13 +00:00
2014-01-24 17:09:53 +00:00
// geo sorting
if (nearFilter) {
nodes = geo.filter(nodes, nearFilter);
}
2014-01-24 17:09:53 +00:00
// do we need some filtration?
2015-07-24 19:56:31 +00:00
if (filter.where && nodes)
nodes = nodes.filter(applyFilter(filter));
2014-01-24 17:09:53 +00:00
// field selection
if (filter.fields) {
nodes = nodes.map(utils.selectFields(filter.fields));
}
// limit/skip
2018-12-07 14:54:29 +00:00
const skip = filter.skip || filter.offset || 0;
const limit = filter.limit || nodes.length;
nodes = nodes.slice(skip, skip + limit);
2014-01-24 17:09:53 +00:00
}
return nodes;
2016-04-01 11:48:17 +00:00
2014-01-24 17:09:53 +00:00
function sorting(a, b) {
2018-12-07 14:54:29 +00:00
let undefinedA, undefinedB;
2018-12-07 14:54:29 +00:00
for (let i = 0, l = this.length; i < l; i++) {
const aVal = getValue(a, this[i].key);
const bVal = getValue(b, this[i].key);
undefinedB = bVal === undefined && aVal !== undefined;
undefinedA = aVal === undefined && bVal !== undefined;
if (undefinedB || aVal > bVal) {
2014-01-24 17:09:53 +00:00
return 1 * this[i].reverse;
} else if (undefinedA || aVal < bVal) {
2014-01-24 17:09:53 +00:00
return -1 * this[i].reverse;
}
2013-03-26 20:50:13 +00:00
}
2014-01-24 17:09:53 +00:00
return 0;
}
2011-10-03 13:36:43 +00:00
};
Memory.prototype.all = function all(model, filter, options, callback) {
2018-12-07 14:54:29 +00:00
const self = this;
const nodes = self._findAllSkippingIncludes(model, filter);
process.nextTick(function() {
if (filter && filter.include) {
self._models[model].model.include(nodes, filter.include, options, callback);
} else {
callback(null, nodes);
}
});
};
2011-10-03 13:36:43 +00:00
function applyFilter(filter) {
2018-12-07 14:54:29 +00:00
const where = filter.where;
if (typeof where === 'function') {
return where;
2014-01-24 17:09:53 +00:00
}
2018-12-07 14:54:29 +00:00
const keys = Object.keys(where);
2016-04-01 11:48:17 +00:00
return function(obj) {
return keys.every(function(key) {
2016-04-01 11:48:17 +00:00
if (key === 'and' || key === 'or') {
if (Array.isArray(where[key])) {
if (key === 'and') {
return where[key].every(function(cond) {
2016-08-19 17:46:59 +00:00
return applyFilter({where: cond})(obj);
});
}
2016-04-01 11:48:17 +00:00
if (key === 'or') {
return where[key].some(function(cond) {
2016-08-19 17:46:59 +00:00
return applyFilter({where: cond})(obj);
});
}
}
}
2018-12-07 14:54:29 +00:00
const value = getValue(obj, key);
// Support referencesMany and other embedded relations
// Also support array types. Mongo, possibly PostgreSQL
if (Array.isArray(value)) {
2018-12-07 14:54:29 +00:00
const matcher = where[key];
// The following condition is for the case where we are querying with
// a neq filter, and when the value is an empty array ([]).
if (matcher.neq !== undefined && value.length <= 0) {
return true;
}
2016-04-01 11:48:17 +00:00
return value.some(function(v, i) {
2018-12-07 14:54:29 +00:00
const filter = {where: {}};
filter.where[i] = matcher;
return applyFilter(filter)(value);
});
}
if (test(where[key], value)) {
return true;
}
// If we have a composed key a.b and b would resolve to a property of an object inside an array
// then, we attempt to emulate mongo db matching. Helps for embedded relations
2018-12-07 14:54:29 +00:00
const dotIndex = key.indexOf('.');
const subValue = obj[key.substring(0, dotIndex)];
if (dotIndex !== -1) {
2018-12-07 14:54:29 +00:00
const subFilter = {where: {}};
const subKey = key.substring(dotIndex + 1);
subFilter.where[subKey] = where[key];
if (Array.isArray(subValue)) {
return subValue.some(applyFilter(subFilter));
} else if (typeof subValue === 'object' && subValue !== null) {
return applyFilter(subFilter)(subValue);
}
2014-01-24 17:09:53 +00:00
}
return false;
2014-01-24 17:09:53 +00:00
});
2016-04-01 11:48:17 +00:00
};
2014-01-24 17:09:53 +00:00
function toRegExp(pattern) {
if (pattern instanceof RegExp) {
return pattern;
}
2018-12-07 14:54:29 +00:00
let regex = '';
2014-06-20 19:05:32 +00:00
// Escaping user input to be treated as a literal string within a regular expression
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Writing_a_Regular_Expression_Pattern
2016-04-01 11:48:17 +00:00
pattern = pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
2018-12-07 14:54:29 +00:00
for (let i = 0, n = pattern.length; i < n; i++) {
const char = pattern.charAt(i);
if (char === '\\') {
i++; // Skip to next char
if (i < n) {
regex += pattern.charAt(i);
}
continue;
} else if (char === '%') {
regex += '.*';
} else if (char === '_') {
regex += '.';
} else if (char === '.') {
regex += '\\.';
} else if (char === '*') {
regex += '\\*';
2016-04-01 13:23:42 +00:00
} else {
regex += char;
}
}
return regex;
}
2014-01-24 17:09:53 +00:00
function test(example, value) {
if (typeof value === 'string' && (example instanceof RegExp)) {
2014-01-24 17:09:53 +00:00
return value.match(example);
2011-10-03 13:36:43 +00:00
}
2015-07-24 19:56:31 +00:00
if (example === undefined) {
return undefined;
}
2014-09-02 15:36:37 +00:00
if (typeof example === 'object' && example !== null) {
if (example.regexp) {
return value ? value.match(example.regexp) : false;
}
2014-01-24 17:09:53 +00:00
// ignore geo near filter
if (example.near) {
return true;
}
2011-10-03 13:36:43 +00:00
2018-12-07 14:54:29 +00:00
let i;
2014-01-24 17:09:53 +00:00
if (example.inq) {
// if (!value) return false;
for (i = 0; i < example.inq.length; i++) {
if (example.inq[i] == value) {
return true;
}
2011-10-03 13:36:43 +00:00
}
2014-01-24 17:09:53 +00:00
return false;
}
2016-04-05 23:11:25 +00:00
if (example.nin) {
for (i = 0; i < example.nin.length; i++) {
2016-04-05 23:11:25 +00:00
if (example.nin[i] == value) {
return false;
}
}
return true;
2016-04-05 23:11:25 +00:00
}
2014-09-02 15:36:37 +00:00
if ('neq' in example) {
return compare(example.neq, value) !== 0;
}
2016-04-01 11:48:17 +00:00
if ('between' in example) {
2016-08-19 17:46:59 +00:00
return (testInEquality({gte: example.between[0]}, value) &&
testInEquality({lte: example.between[1]}, value));
}
if (example.like || example.nlike || example.ilike || example.nilike) {
2018-12-07 14:54:29 +00:00
let like = example.like || example.nlike || example.ilike || example.nilike;
if (typeof like === 'string') {
like = toRegExp(like);
}
if (example.like) {
return !!new RegExp(like).test(value);
}
if (example.nlike) {
return !new RegExp(like).test(value);
}
if (example.ilike) {
return !!new RegExp(like, 'i').test(value);
}
if (example.nilike) {
return !new RegExp(like, 'i').test(value);
}
}
if (testInEquality(example, value)) {
return true;
}
}
2014-01-24 17:09:53 +00:00
// not strict equality
2016-04-01 13:23:42 +00:00
return (example !== null ? example.toString() : example) ==
(value != null ? value.toString() : value);
2014-01-24 17:09:53 +00:00
}
/**
* Compare two values
* @param {*} val1 The 1st value
* @param {*} val2 The 2nd value
2014-06-06 16:10:47 +00:00
* @returns {number} 0: =, positive: >, negative <
* @private
*/
function compare(val1, val2) {
2016-04-01 11:48:17 +00:00
if (val1 == null || val2 == null) {
// Either val1 or val2 is null or undefined
return val1 == val2 ? 0 : NaN;
}
if (typeof val1 === 'number') {
return val1 - val2;
}
if (typeof val1 === 'string') {
return (val1 > val2) ? 1 : ((val1 < val2) ? -1 : (val1 == val2) ? 0 : NaN);
}
if (typeof val1 === 'boolean') {
return val1 - val2;
}
if (val1 instanceof Date) {
2018-12-07 14:54:29 +00:00
const result = val1 - val2;
2014-06-06 15:48:05 +00:00
return result;
}
// Return NaN if we don't know how to compare
return (val1 == val2) ? 0 : NaN;
}
function testInEquality(example, val) {
if ('gt' in example) {
2014-06-06 15:48:05 +00:00
return compare(val, example.gt) > 0;
}
if ('gte' in example) {
2014-06-06 15:48:05 +00:00
return compare(val, example.gte) >= 0;
}
if ('lt' in example) {
2014-06-06 15:48:05 +00:00
return compare(val, example.lt) < 0;
}
if ('lte' in example) {
2014-06-06 15:48:05 +00:00
return compare(val, example.lte) <= 0;
}
return false;
2014-01-24 17:09:53 +00:00
}
2011-10-03 13:36:43 +00:00
}
Memory.prototype.destroyAll = function destroyAll(model, where, options, callback) {
2018-12-07 14:54:29 +00:00
const cache = this.collection(model);
let filter = null;
let count = 0;
2014-01-24 17:09:53 +00:00
if (where) {
2016-08-19 17:46:59 +00:00
filter = applyFilter({where: where});
2016-04-01 11:48:17 +00:00
Object.keys(cache).forEach(function(id) {
2014-09-06 17:24:30 +00:00
if (!filter || filter(this.fromDb(model, cache[id]))) {
count++;
2014-09-06 17:24:30 +00:00
delete cache[id];
}
}.bind(this));
} else {
count = Object.keys(cache).length;
this.collection(model, {});
2014-01-24 17:09:53 +00:00
}
2016-08-19 17:46:59 +00:00
this.saveToFile({count: count}, callback);
2011-10-03 13:36:43 +00:00
};
Memory.prototype.count = function count(model, where, options, callback) {
2018-12-07 14:54:29 +00:00
const cache = this.collection(model);
let data = Object.keys(cache);
2014-01-24 17:09:53 +00:00
if (where) {
2018-12-07 14:54:29 +00:00
const filter = {where: where};
2016-04-01 11:48:17 +00:00
data = data.map(function(id) {
2014-01-24 17:09:53 +00:00
return this.fromDb(model, cache[id]);
}.bind(this));
data = data.filter(applyFilter(filter));
}
2016-04-01 11:48:17 +00:00
process.nextTick(function() {
2014-01-24 17:09:53 +00:00
callback(null, data.length);
});
2011-10-03 13:36:43 +00:00
};
Memory.prototype.update =
Memory.prototype.updateAll = function updateAll(model, where, data, options, cb) {
2018-12-07 14:54:29 +00:00
const self = this;
const cache = this.collection(model);
let filter = null;
where = where || {};
2016-08-19 17:46:59 +00:00
filter = applyFilter({where: where});
2018-12-07 14:54:29 +00:00
const ids = Object.keys(cache);
let count = 0;
2016-04-01 11:48:17 +00:00
async.each(ids, function(id, done) {
2018-12-07 14:54:29 +00:00
const inst = self.fromDb(model, cache[id]);
if (!filter || filter(inst)) {
count++;
// The id value from the cache is string
// Get the real id from the inst
id = self.getIdValue(model, inst);
self.updateAttributes(model, id, data, options, done);
} else {
process.nextTick(done);
}
2016-04-01 11:48:17 +00:00
}, function(err) {
if (err) return cb(err);
2016-08-19 17:46:59 +00:00
self.saveToFile({count: count}, cb);
});
};
Memory.prototype.updateAttributes = function updateAttributes(model, id, data, options, cb) {
2014-01-24 17:09:53 +00:00
if (!id) {
2018-12-07 14:54:29 +00:00
const err = new Error(g.f('You must provide an {{id}} when updating attributes!'));
2014-01-24 17:09:53 +00:00
if (cb) {
return cb(err);
} else {
2014-01-24 17:09:53 +00:00
throw err;
}
2014-01-24 17:09:53 +00:00
}
// Do not modify the data object passed in arguments
data = Object.create(data);
2014-01-24 17:09:53 +00:00
this.setIdValue(model, data, id);
2018-12-07 14:54:29 +00:00
const cachedModels = this.collection(model);
const modelData = cachedModels && this.collection(model)[id];
2014-01-24 17:09:53 +00:00
if (modelData) {
this.save(model, data, options, cb);
2014-01-24 17:09:53 +00:00
} else {
2018-12-07 14:54:29 +00:00
const msg = g.f('Could not update attributes. {{Object}} with {{id}} %s does not exist!', id);
const error = new Error(msg);
2017-04-03 20:00:31 +00:00
error.statusCode = error.status = 404;
cb(error);
2014-01-24 17:09:53 +00:00
}
2011-10-03 13:36:43 +00:00
};
Memory.prototype.replaceById = function(model, id, data, options, cb) {
2018-12-07 14:54:29 +00:00
const self = this;
if (!id) {
2018-12-07 14:54:29 +00:00
const err = new Error(g.f('You must provide an {{id}} when replacing!'));
return process.nextTick(function() { cb(err); });
}
// Do not modify the data object passed in arguments
data = Object.create(data);
this.setIdValue(model, data, id);
2018-12-07 14:54:29 +00:00
const cachedModels = this.collection(model);
const modelData = cachedModels && this.collection(model)[id];
if (!modelData) {
2018-12-07 14:54:29 +00:00
const msg = 'Could not replace. Object with id ' + id + ' does not exist!';
const error = new Error(msg);
2017-01-04 16:33:49 +00:00
error.statusCode = error.status = 404;
return process.nextTick(function() { cb(error); });
}
2018-12-07 14:54:29 +00:00
const newModelData = {};
for (const key in data) {
const val = data[key];
2016-04-01 11:48:17 +00:00
if (typeof val === 'function') {
continue; // Skip methods
}
newModelData[key] = val;
}
this.collection(model)[id] = serialize(newModelData);
2016-04-01 11:48:17 +00:00
this.saveToFile(newModelData, function(err) {
cb(err, self.fromDb(model, newModelData));
});
};
Memory.prototype.replaceOrCreate = function(model, data, options, callback) {
2018-12-07 14:54:29 +00:00
const self = this;
const idName = self.idNames(model)[0];
const idValue = self.getIdValue(model, data);
const filter = {where: {}};
filter.where[idName] = idValue;
2018-12-07 14:54:29 +00:00
const nodes = self._findAllSkippingIncludes(model, filter);
const found = nodes[0];
if (!found) {
2016-04-01 11:48:17 +00:00
// Calling _createSync to update the collection in a sync way and
// to guarantee to create it in the same turn of even loop
return self._createSync(model, data, function(err, id) {
if (err) return process.nextTick(function() { callback(err); });
self.saveToFile(id, function(err, id) {
self.setIdValue(model, data, id);
2016-08-19 17:46:59 +00:00
callback(err, self.fromDb(model, data), {isNewInstance: true});
});
});
}
2018-12-07 14:54:29 +00:00
const id = self.getIdValue(model, data);
self.collection(model)[id] = serialize(data);
self.saveToFile(data, function(err) {
2016-08-19 17:46:59 +00:00
callback(err, self.fromDb(model, data), {isNewInstance: false});
});
};
2016-04-01 11:48:17 +00:00
Memory.prototype.transaction = function() {
2014-01-24 17:09:53 +00:00
return new Memory(this);
2013-04-01 13:49:12 +00:00
};
2016-04-01 11:48:17 +00:00
Memory.prototype.exec = function(callback) {
2014-01-24 17:09:53 +00:00
this.onTransactionExec();
setTimeout(callback, 50);
2013-04-01 13:49:12 +00:00
};
2016-04-01 11:48:17 +00:00
Memory.prototype.buildNearFilter = function(filter) {
2013-06-26 03:31:00 +00:00
// noop
2016-04-01 11:48:17 +00:00
};
2013-06-26 03:31:00 +00:00
2016-04-01 11:48:17 +00:00
Memory.prototype.automigrate = function(models, cb) {
2018-12-07 14:54:29 +00:00
const self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(self._models);
if (models.length === 0) {
return process.nextTick(cb);
}
2018-12-07 14:54:29 +00:00
const invalidModels = models.filter(function(m) {
return !(m in self._models);
});
if (invalidModels.length) {
return process.nextTick(function() {
2016-07-22 19:26:07 +00:00
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
invalidModels.join(' '))));
});
}
models.forEach(function(m) {
self.initCollection(m);
});
if (cb) process.nextTick(cb);
2016-04-01 11:48:17 +00:00
};
2011-10-03 13:36:43 +00:00
function merge(base, update) {
if (!base) {
return update;
}
// We cannot use Object.keys(update) if the update is an instance of the model
// class as the properties are defined at the ModelClass.prototype level
2018-12-07 14:54:29 +00:00
for (const key in update) {
const val = update[key];
2016-04-01 11:48:17 +00:00
if (typeof val === 'function') {
continue; // Skip methods
}
base[key] = val;
}
2014-01-24 17:09:53 +00:00
return base;
2014-05-07 14:47:12 +00:00
}