warehouse
Version:
Simple JSON-based database
812 lines • 24.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const rfdc_1 = __importDefault(require("rfdc"));
const cloneDeep = (0, rfdc_1.default)();
const bluebird_1 = __importDefault(require("bluebird"));
const util_1 = require("./util");
const document_1 = __importDefault(require("./document"));
const query_1 = __importDefault(require("./query"));
const schema_1 = __importDefault(require("./schema"));
const Types = __importStar(require("./types/index"));
const error_1 = __importDefault(require("./error"));
const population_1 = __importDefault(require("./error/population"));
const mutex_1 = __importDefault(require("./mutex"));
class Model extends events_1.EventEmitter {
/**
* Model constructor.
*
* @param {string} name Model name
* @param {Schema|object} [schema_] Schema
*/
constructor(name, schema_) {
super();
this.name = name;
this._mutex = new mutex_1.default();
this.data = {};
this.length = 0;
this._dataKeys = [];
this.dirty = false;
let schema;
// Define schema
if (schema_ instanceof schema_1.default) {
schema = schema_;
}
else if (typeof schema_ === 'object') {
schema = new schema_1.default(schema_);
}
else {
schema = new schema_1.default();
}
// Set `_id` path for schema
if (!schema.path('_id')) {
schema.path('_id', { type: Types.CUID, required: true });
}
this.schema = schema;
class _Document extends document_1.default {
constructor(data) {
super(data);
// Apply getters
schema._applyGetters(this);
}
}
this.Document = _Document;
_Document.prototype._model = this;
_Document.prototype._schema = schema;
class _Query extends query_1.default {
}
this.Query = _Query;
_Query.prototype._model = this;
_Query.prototype._schema = schema;
// Apply static methods
Object.assign(this, schema.statics);
// Apply instance methods
Object.assign(_Document.prototype, schema.methods);
}
/**
* Returns the cached data keys.
*/
get dataKeys() {
if (this.dirty) {
this._dataKeys = Object.keys(this.data);
this.dirty = false;
}
return this._dataKeys;
}
/**
* Creates a new document.
*
* @param {object} data
* @return {Document}
*/
new(data) {
return new this.Document(data);
}
findById(id, options_) {
const raw = this.data[id];
if (!raw)
return;
const options = Object.assign({
lean: false
}, options_);
const data = cloneDeep(raw);
return options.lean ? data : this.new(data);
}
/**
* Checks if the model contains a document with the specified id.
*
* @param {*} id
* @return {boolean}
*/
has(id) {
return Boolean(this.data[id]);
}
/**
* Acquires write lock.
*
* @return {BluebirdPromise}
* @private
*/
_acquireWriteLock() {
const mutex = this._mutex;
return new bluebird_1.default((resolve, reject) => {
mutex.lock(resolve);
}).disposer(() => {
mutex.unlock();
});
}
/**
* Inserts a document.
*
* @param {Document|object} data
* @return {BluebirdPromise}
* @private
*/
_insertOne(data_) {
const schema = this.schema;
// Apply getters
const data = (data_ instanceof this.Document ? data_ : this.new(data_));
const id = data._id;
// Check ID
if (!id) {
return bluebird_1.default.reject(new error_1.default('ID is not defined', error_1.default.ID_UNDEFINED));
}
if (this.has(id)) {
return bluebird_1.default.reject(new error_1.default('ID `' + id + '` has been used', error_1.default.ID_EXIST));
}
// Apply setters
const result = data.toObject();
schema._applySetters(result);
// Pre-hooks
return execHooks(schema, 'pre', 'save', data).then(data => {
// Insert data
this.dirty = true;
this.data[id] = result;
this.length++;
this.emit('insert', data);
return execHooks(schema, 'post', 'save', data);
});
}
/**
* Inserts a document.
*
* @param {object} data
* @param {function} [callback]
* @return {BluebirdPromise}
*/
insertOne(data, callback) {
return bluebird_1.default.using(this._acquireWriteLock(), () => this._insertOne(data)).asCallback(callback);
}
/**
* Inserts documents.
*
* @param {object|array} data
* @param {function} [callback]
* @return {BluebirdPromise}
*/
insert(data, callback) {
if (Array.isArray(data)) {
return bluebird_1.default.mapSeries(data, item => this.insertOne(item)).asCallback(callback);
}
return this.insertOne(data, callback);
}
/**
* Inserts the document if it does not exist; otherwise updates it.
*
* @param {object} data
* @param {function} [callback]
* @return {BluebirdPromise}
*/
save(data, callback) {
const id = data._id;
if (!id)
return this.insertOne(data, callback);
return bluebird_1.default.using(this._acquireWriteLock(), () => {
if (this.has(id)) {
return this._replaceById(id, data);
}
return this._insertOne(data);
}).asCallback(callback);
}
/**
* Updates a document with a compiled stack.
*
* @param {*} id
* @param {array} stack
* @return {BluebirdPromise}
* @private
*/
_updateWithStack(id, stack) {
const schema = this.schema;
const data = this.data[id];
if (!data) {
return bluebird_1.default.reject(new error_1.default('ID `' + id + '` does not exist', error_1.default.ID_NOT_EXIST));
}
// Clone data
let result = cloneDeep(data);
// Update
for (let i = 0, len = stack.length; i < len; i++) {
stack[i](result);
}
// Apply getters
const doc = this.new(result);
// Apply setters
result = doc.toObject();
schema._applySetters(result);
// Pre-hooks
return execHooks(schema, 'pre', 'save', doc).then(data => {
// Update data
this.dirty = true;
this.data[id] = result;
this.emit('update', data);
return execHooks(schema, 'post', 'save', data);
});
}
/**
* Finds a document by its identifier and update it.
*
* @param {*} id
* @param {object} update
* @param {function} [callback]
* @return {BluebirdPromise}
*/
updateById(id, update, callback) {
return bluebird_1.default.using(this._acquireWriteLock(), () => {
const stack = this.schema._parseUpdate(update);
return this._updateWithStack(id, stack);
}).asCallback(callback);
}
/**
* Updates matching documents.
*
* @param {object} query
* @param {object} data
* @param {function} [callback]
* @return {BluebirdPromise}
*/
update(query, data, callback) {
return this.find(query).update(data, callback);
}
/**
* Finds a document by its identifier and replace it.
*
* @param {*} id
* @param {object} data
* @return {BluebirdPromise}
* @private
*/
_replaceById(id, data_) {
const schema = this.schema;
if (!this.has(id)) {
return bluebird_1.default.reject(new error_1.default('ID `' + id + '` does not exist', error_1.default.ID_NOT_EXIST));
}
data_._id = id;
// Apply getters
const data = (data_ instanceof this.Document ? data_ : this.new(data_));
// Apply setters
const result = data.toObject();
schema._applySetters(result);
// Pre-hooks
return execHooks(schema, 'pre', 'save', data).then(data => {
// Replace data
this.dirty = true;
this.data[id] = result;
this.emit('update', data);
return execHooks(schema, 'post', 'save', data);
});
}
/**
* Finds a document by its identifier and replace it.
*
* @param {*} id
* @param {object} data
* @param {function} [callback]
* @return {BluebirdPromise}
*/
replaceById(id, data, callback) {
return bluebird_1.default.using(this._acquireWriteLock(), () => this._replaceById(id, data)).asCallback(callback);
}
/**
* Replaces matching documents.
*
* @param {object} query
* @param {object} data
* @param {function} [callback]
* @return {BluebirdPromise}
*/
replace(query, data, callback) {
return this.find(query).replace(data, callback);
}
/**
* Finds a document by its identifier and remove it.
*
* @param {*} id
* @return {BluebirdPromise}
* @private
*/
_removeById(id) {
const schema = this.schema;
const data = this.data[id];
if (!data) {
return bluebird_1.default.reject(new error_1.default('ID `' + id + '` does not exist', error_1.default.ID_NOT_EXIST));
}
// Pre-hooks
return execHooks(schema, 'pre', 'remove', data).then(data => {
// Remove data
this.dirty = true;
this.data[id] = null;
this.length--;
this.emit('remove', data);
return execHooks(schema, 'post', 'remove', data);
});
}
/**
* Finds a document by its identifier and remove it.
*
* @param {*} id
* @param {function} [callback]
* @return {BluebirdPromise}
*/
removeById(id, callback) {
return bluebird_1.default.using(this._acquireWriteLock(), () => this._removeById(id)).asCallback(callback);
}
/**
* Removes matching documents.
*
* @param {object} query
* @param {function} [callback]
* @return {BluebirdPromise}
*/
remove(query, callback) {
return this.find(query).remove(callback);
}
/**
* Deletes a model.
*/
destroy() {
this._database._models[this.name] = null;
}
/**
* Returns the number of elements.
*
* @return {number}
*/
count() {
return this.length;
}
forEach(iterator, options) {
const keys = this.dataKeys;
let num = 0;
for (let i = 0, len = keys.length; i < len; i++) {
const data = this.findById(keys[i], options);
if (data)
iterator(data, num++);
}
}
toArray(options) {
const result = new Array(this.length);
this.forEach((item, i) => {
result[i] = item;
}, options);
return result;
}
find(query, options = {}) {
const filter = this.schema._execQuery(query);
const keys = this.dataKeys;
const len = keys.length;
let limit = options.limit || this.length;
let skip = options.skip;
const data = this.data;
const arr = [];
for (let i = 0; limit && i < len; i++) {
const key = keys[i];
const item = data[key];
if (item && filter(item)) {
if (skip) {
skip--;
}
else {
arr.push(this.findById(key, options));
limit--;
}
}
}
return options.lean ? arr : new this.Query(arr);
}
findOne(query, options_ = {}) {
const options = Object.assign(options_, { limit: 1 });
const result = this.find(query, options);
return options.lean ? result[0] : result.toArray()[0];
}
sort(orderby, order) {
const sort = (0, util_1.parseArgs)(orderby, order);
const fn = this.schema._execSort(sort);
return new this.Query(this.toArray().sort(fn));
}
eq(i_, options) {
let index = i_ < 0 ? this.length + i_ : i_;
const data = this.data;
const keys = this.dataKeys;
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i];
const item = data[key];
if (!item)
continue;
if (index) {
index--;
}
else {
return this.findById(key, options);
}
}
}
first(options) {
return this.eq(0, options);
}
last(options) {
return this.eq(-1, options);
}
/**
* Returns the specified range of documents.
*
* @param {Number} start
* @param {Number} [end]
* @return {Query}
*/
slice(start_, end_) {
const total = this.length;
let start = start_ | 0;
if (start < 0)
start += total;
if (start > total - 1)
return new this.Query([]);
let end = end_ | 0 || total;
if (end < 0)
end += total;
let len = start > end ? 0 : end - start;
if (len > total)
len = total - start;
if (!len)
return new this.Query([]);
const arr = new Array(len);
const keys = this.dataKeys;
const keysLen = keys.length;
let num = 0;
for (let i = 0; num < len && i < keysLen; i++) {
const data = this.findById(keys[i]);
if (!data)
continue;
if (start) {
start--;
}
else {
arr[num++] = data;
}
}
return new this.Query(arr);
}
/**
* Limits the number of documents returned.
*
* @param {Number} i
* @return {Query}
*/
limit(i) {
return this.slice(0, i);
}
/**
* Specifies the number of items to skip.
*
* @param {Number} i
* @return {Query}
*/
skip(i) {
return this.slice(i);
}
/**
* Returns documents in a reversed order.
*
* @return {Query}
*/
reverse() {
return new this.Query(this.toArray().reverse());
}
/**
* Returns documents in random order.
*
* @return {Query}
*/
shuffle() {
return new this.Query((0, util_1.shuffle)(this.toArray()));
}
map(iterator, options) {
const result = new Array(this.length);
const keys = this.dataKeys;
const len = keys.length;
for (let i = 0, num = 0; i < len; i++) {
const data = this.findById(keys[i], options);
if (data) {
result[num] = iterator(data, num);
num++;
}
}
return result;
}
/**
* Reduces a collection to a value which is the accumulated result of iterating
* each element in the collection.
*
* @param {Function} iterator
* @param {*} [initial] By default, the initial value is the first document.
* @return {*}
*/
reduce(iterator, initial) {
const arr = this.toArray();
const len = this.length;
let i, result;
if (initial === undefined) {
i = 1;
result = arr[0];
}
else {
i = 0;
result = initial;
}
for (; i < len; i++) {
result = iterator(result, arr[i], i);
}
return result;
}
/**
* Reduces a collection to a value which is the accumulated result of iterating
* each element in the collection from right to left.
*
* @param {Function} iterator
* @param {*} [initial] By default, the initial value is the last document.
* @return {*}
*/
reduceRight(iterator, initial) {
const arr = this.toArray();
const len = this.length;
let i, result;
if (initial === undefined) {
i = len - 2;
result = arr[len - 1];
}
else {
i = len - 1;
result = initial;
}
for (; i >= 0; i--) {
result = iterator(result, arr[i], i);
}
return result;
}
filter(iterator, options) {
const arr = [];
this.forEach((item, i) => {
if (iterator(item, i))
arr.push(item);
}, options);
return new this.Query(arr);
}
/**
* Tests whether all documents pass the test implemented by the provided
* function.
*
* @param {Function} iterator
* @return {Boolean}
*/
every(iterator) {
const keys = this.dataKeys;
const len = keys.length;
let num = 0;
if (!len)
return true;
for (let i = 0; i < len; i++) {
const data = this.findById(keys[i]);
if (data) {
if (!iterator(data, num++))
return false;
}
}
return true;
}
/**
* Tests whether some documents pass the test implemented by the provided
* function.
*
* @param {Function} iterator
* @return {Boolean}
*/
some(iterator) {
const keys = this.dataKeys;
const len = keys.length;
let num = 0;
if (!len)
return false;
for (let i = 0; i < len; i++) {
const data = this.findById(keys[i]);
if (data) {
if (iterator(data, num++))
return true;
}
}
return false;
}
/**
* Returns a getter function for normal population.
*
* @param {Object} data
* @param {Model} model
* @param {Object} options
* @return {Function}
* @private
*/
_populateGetter(data, model, options) {
let hasCache = false;
let cache;
return () => {
if (!hasCache) {
cache = model.findById(data);
hasCache = true;
}
return cache;
};
}
/**
* Returns a getter function for array population.
*
* @param {Object} data
* @param {Model} model
* @param {Object} options
* @return {Function}
* @private
*/
_populateGetterArray(data, model, options) {
const Query = model.Query;
let hasCache = false;
let cache;
return () => {
if (!hasCache) {
let arr = [];
for (let i = 0, len = data.length; i < len; i++) {
arr.push(model.findById(data[i]));
}
if (options.match) {
cache = new Query(arr).find(options.match, options);
}
else if (options.skip) {
if (options.limit) {
arr = arr.slice(options.skip, options.skip + options.limit);
}
else {
arr = arr.slice(options.skip);
}
cache = new Query(arr);
}
else if (options.limit) {
cache = new Query(arr.slice(0, options.limit));
}
else {
cache = new Query(arr);
}
if (options.sort) {
cache = cache.sort(options.sort);
}
hasCache = true;
}
return cache;
};
}
/**
* Populates document references with a compiled stack.
*
* @param {Object} data
* @param {Array} stack
* @return {Object}
* @private
*/
_populate(data, stack) {
const models = this._database._models;
for (let i = 0, len = stack.length; i < len; i++) {
const item = stack[i];
const model = models[item.model];
if (!model) {
throw new population_1.default('Model `' + item.model + '` does not exist');
}
const path = item.path;
const prop = (0, util_1.getProp)(data, path);
if (Array.isArray(prop)) {
(0, util_1.setGetter)(data, path, this._populateGetterArray(prop, model, item));
}
else {
(0, util_1.setGetter)(data, path, this._populateGetter(prop, model, item));
}
}
return data;
}
/**
* Populates document references.
*
* @param {String|Object} path
* @return {Query}
*/
populate(path) {
if (!path)
throw new TypeError('path is required');
const stack = this.schema._parsePopulate(path);
const arr = new Array(this.length);
this.forEach((item, i) => {
arr[i] = this._populate(item, stack);
});
return new this.Query(arr);
}
/**
* Imports data.
*
* @param {Array} arr
* @private
*/
_import(arr) {
const len = arr.length;
const data = this.data;
const schema = this.schema;
for (let i = 0; i < len; i++) {
const item = arr[i];
this.dirty = true;
data[item._id] = schema._parseDatabase(item);
}
this.length = len;
}
/**
* Exports data.
*
* @return {String}
* @private
*/
_export() {
return JSON.stringify(this.toJSON());
}
toJSON() {
const result = new Array(this.length);
const { data, schema } = this;
const keys = this.dataKeys;
const { length } = keys;
for (let i = 0, num = 0; i < length; i++) {
const raw = data[keys[i]];
if (raw) {
result[num++] = schema._exportDatabase(cloneDeep(raw));
}
}
return result;
}
}
Model.prototype.get = Model.prototype.findById;
function execHooks(schema, type, event, data) {
const hooks = schema.hooks[type][event];
if (!hooks.length)
return bluebird_1.default.resolve(data);
return bluebird_1.default.each(hooks, hook => hook(data)).thenReturn(data);
}
Model.prototype.size = Model.prototype.count;
Model.prototype.each = Model.prototype.forEach;
Model.prototype.random = Model.prototype.shuffle;
exports.default = Model;
//# sourceMappingURL=model.js.map