@electerm/nedb 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +22 -0
- package/index.js +3 -0
- package/lib/async.js +237 -0
- package/lib/cursor.js +204 -0
- package/lib/customUtils.js +22 -0
- package/lib/datastore.js +705 -0
- package/lib/executor.js +78 -0
- package/lib/indexes.js +294 -0
- package/lib/model.js +835 -0
- package/lib/persistence.js +314 -0
- package/lib/storage.js +136 -0
- package/lib/underscore.js +177 -0
- package/package.json +52 -0
package/lib/executor.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsible for sequentially executing actions on the database
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
var async = require('./async')
|
|
6
|
+
;
|
|
7
|
+
|
|
8
|
+
function Executor () {
|
|
9
|
+
this.buffer = [];
|
|
10
|
+
this.ready = false;
|
|
11
|
+
|
|
12
|
+
// This queue will execute all commands, one-by-one in order
|
|
13
|
+
this.queue = async.queue(function (task, cb) {
|
|
14
|
+
var newArguments = [];
|
|
15
|
+
|
|
16
|
+
// task.arguments is an array-like object on which adding a new field doesn't work, so we transform it into a real array
|
|
17
|
+
for (var i = 0; i < task.arguments.length; i += 1) { newArguments.push(task.arguments[i]); }
|
|
18
|
+
var lastArg = task.arguments[task.arguments.length - 1];
|
|
19
|
+
|
|
20
|
+
// Always tell the queue task is complete. Execute callback if any was given.
|
|
21
|
+
if (typeof lastArg === 'function') {
|
|
22
|
+
// Callback was supplied
|
|
23
|
+
newArguments[newArguments.length - 1] = function () {
|
|
24
|
+
if (typeof setImmediate === 'function') {
|
|
25
|
+
setImmediate(cb);
|
|
26
|
+
} else {
|
|
27
|
+
process.nextTick(cb);
|
|
28
|
+
}
|
|
29
|
+
lastArg.apply(null, arguments);
|
|
30
|
+
};
|
|
31
|
+
} else if (!lastArg && task.arguments.length !== 0) {
|
|
32
|
+
// false/undefined/null supplied as callbback
|
|
33
|
+
newArguments[newArguments.length - 1] = function () { cb(); };
|
|
34
|
+
} else {
|
|
35
|
+
// Nothing supplied as callback
|
|
36
|
+
newArguments.push(function () { cb(); });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
task.fn.apply(task.this, newArguments);
|
|
41
|
+
}, 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* If executor is ready, queue task (and process it immediately if executor was idle)
|
|
47
|
+
* If not, buffer task for later processing
|
|
48
|
+
* @param {Object} task
|
|
49
|
+
* task.this - Object to use as this
|
|
50
|
+
* task.fn - Function to execute
|
|
51
|
+
* task.arguments - Array of arguments, IMPORTANT: only the last argument may be a function (the callback)
|
|
52
|
+
* and the last argument cannot be false/undefined/null
|
|
53
|
+
* @param {Boolean} forceQueuing Optional (defaults to false) force executor to queue task even if it is not ready
|
|
54
|
+
*/
|
|
55
|
+
Executor.prototype.push = function (task, forceQueuing) {
|
|
56
|
+
if (this.ready || forceQueuing) {
|
|
57
|
+
this.queue.push(task);
|
|
58
|
+
} else {
|
|
59
|
+
this.buffer.push(task);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Queue all tasks in buffer (in the same order they came in)
|
|
66
|
+
* Automatically sets executor as ready
|
|
67
|
+
*/
|
|
68
|
+
Executor.prototype.processBuffer = function () {
|
|
69
|
+
var i;
|
|
70
|
+
this.ready = true;
|
|
71
|
+
for (i = 0; i < this.buffer.length; i += 1) { this.queue.push(this.buffer[i]); }
|
|
72
|
+
this.buffer = [];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// Interface
|
|
78
|
+
module.exports = Executor;
|
package/lib/indexes.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
var BinarySearchTree = require('@yetzt/binary-search-tree').AVLTree
|
|
2
|
+
, model = require('./model')
|
|
3
|
+
, _ = require('./underscore')
|
|
4
|
+
, util = require('util')
|
|
5
|
+
;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Two indexed pointers are equal iif they point to the same place
|
|
9
|
+
*/
|
|
10
|
+
function checkValueEquality (a, b) {
|
|
11
|
+
return a === b;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type-aware projection
|
|
16
|
+
*/
|
|
17
|
+
function projectForUnique (elt) {
|
|
18
|
+
if (elt === null) { return '$null'; }
|
|
19
|
+
if (typeof elt === 'string') { return '$string' + elt; }
|
|
20
|
+
if (typeof elt === 'boolean') { return '$boolean' + elt; }
|
|
21
|
+
if (typeof elt === 'number') { return '$number' + elt; }
|
|
22
|
+
if (util.isArray(elt)) { return '$date' + elt.getTime(); }
|
|
23
|
+
|
|
24
|
+
return elt; // Arrays and objects, will check for pointer equality
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new index
|
|
30
|
+
* All methods on an index guarantee that either the whole operation was successful and the index changed
|
|
31
|
+
* or the operation was unsuccessful and an error is thrown while the index is unchanged
|
|
32
|
+
* @param {String} options.fieldName On which field should the index apply (can use dot notation to index on sub fields)
|
|
33
|
+
* @param {Boolean} options.unique Optional, enforce a unique constraint (default: false)
|
|
34
|
+
* @param {Boolean} options.sparse Optional, allow a sparse index (we can have documents for which fieldName is undefined) (default: false)
|
|
35
|
+
*/
|
|
36
|
+
function Index (options) {
|
|
37
|
+
this.fieldName = options.fieldName;
|
|
38
|
+
this.unique = options.unique || false;
|
|
39
|
+
this.sparse = options.sparse || false;
|
|
40
|
+
|
|
41
|
+
this.treeOptions = { unique: this.unique, compareKeys: model.compareThings, checkValueEquality: checkValueEquality };
|
|
42
|
+
|
|
43
|
+
this.reset(); // No data in the beginning
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reset an index
|
|
49
|
+
* @param {Document or Array of documents} newData Optional, data to initialize the index with
|
|
50
|
+
* If an error is thrown during insertion, the index is not modified
|
|
51
|
+
*/
|
|
52
|
+
Index.prototype.reset = function (newData) {
|
|
53
|
+
this.tree = new BinarySearchTree(this.treeOptions);
|
|
54
|
+
|
|
55
|
+
if (newData) { this.insert(newData); }
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Insert a new document in the index
|
|
61
|
+
* If an array is passed, we insert all its elements (if one insertion fails the index is not modified)
|
|
62
|
+
* O(log(n))
|
|
63
|
+
*/
|
|
64
|
+
Index.prototype.insert = function (doc) {
|
|
65
|
+
var key, self = this
|
|
66
|
+
, keys, i, failingI, error
|
|
67
|
+
;
|
|
68
|
+
|
|
69
|
+
if (util.isArray(doc)) { this.insertMultipleDocs(doc); return; }
|
|
70
|
+
|
|
71
|
+
key = model.getDotValue(doc, this.fieldName);
|
|
72
|
+
|
|
73
|
+
// We don't index documents that don't contain the field if the index is sparse
|
|
74
|
+
if (key === undefined && this.sparse) { return; }
|
|
75
|
+
|
|
76
|
+
if (!util.isArray(key)) {
|
|
77
|
+
this.tree.insert(key, doc);
|
|
78
|
+
} else {
|
|
79
|
+
// If an insert fails due to a unique constraint, roll back all inserts before it
|
|
80
|
+
keys = _.uniq(key, projectForUnique);
|
|
81
|
+
|
|
82
|
+
for (i = 0; i < keys.length; i += 1) {
|
|
83
|
+
try {
|
|
84
|
+
this.tree.insert(keys[i], doc);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
error = e;
|
|
87
|
+
failingI = i;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (error) {
|
|
93
|
+
for (i = 0; i < failingI; i += 1) {
|
|
94
|
+
this.tree.delete(keys[i], doc);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Insert an array of documents in the index
|
|
105
|
+
* If a constraint is violated, the changes should be rolled back and an error thrown
|
|
106
|
+
*
|
|
107
|
+
* @API private
|
|
108
|
+
*/
|
|
109
|
+
Index.prototype.insertMultipleDocs = function (docs) {
|
|
110
|
+
var i, error, failingI;
|
|
111
|
+
|
|
112
|
+
for (i = 0; i < docs.length; i += 1) {
|
|
113
|
+
try {
|
|
114
|
+
this.insert(docs[i]);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
error = e;
|
|
117
|
+
failingI = i;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (error) {
|
|
123
|
+
for (i = 0; i < failingI; i += 1) {
|
|
124
|
+
this.remove(docs[i]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove a document from the index
|
|
134
|
+
* If an array is passed, we remove all its elements
|
|
135
|
+
* The remove operation is safe with regards to the 'unique' constraint
|
|
136
|
+
* O(log(n))
|
|
137
|
+
*/
|
|
138
|
+
Index.prototype.remove = function (doc) {
|
|
139
|
+
var key, self = this;
|
|
140
|
+
|
|
141
|
+
if (util.isArray(doc)) { doc.forEach(function (d) { self.remove(d); }); return; }
|
|
142
|
+
|
|
143
|
+
key = model.getDotValue(doc, this.fieldName);
|
|
144
|
+
|
|
145
|
+
if (key === undefined && this.sparse) { return; }
|
|
146
|
+
|
|
147
|
+
if (!util.isArray(key)) {
|
|
148
|
+
this.tree.delete(key, doc);
|
|
149
|
+
} else {
|
|
150
|
+
_.uniq(key, projectForUnique).forEach(function (_key) {
|
|
151
|
+
self.tree.delete(_key, doc);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update a document in the index
|
|
159
|
+
* If a constraint is violated, changes are rolled back and an error thrown
|
|
160
|
+
* Naive implementation, still in O(log(n))
|
|
161
|
+
*/
|
|
162
|
+
Index.prototype.update = function (oldDoc, newDoc) {
|
|
163
|
+
if (util.isArray(oldDoc)) { this.updateMultipleDocs(oldDoc); return; }
|
|
164
|
+
|
|
165
|
+
this.remove(oldDoc);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
this.insert(newDoc);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
this.insert(oldDoc);
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Update multiple documents in the index
|
|
178
|
+
* If a constraint is violated, the changes need to be rolled back
|
|
179
|
+
* and an error thrown
|
|
180
|
+
* @param {Array of oldDoc, newDoc pairs} pairs
|
|
181
|
+
*
|
|
182
|
+
* @API private
|
|
183
|
+
*/
|
|
184
|
+
Index.prototype.updateMultipleDocs = function (pairs) {
|
|
185
|
+
var i, failingI, error;
|
|
186
|
+
|
|
187
|
+
for (i = 0; i < pairs.length; i += 1) {
|
|
188
|
+
this.remove(pairs[i].oldDoc);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (i = 0; i < pairs.length; i += 1) {
|
|
192
|
+
try {
|
|
193
|
+
this.insert(pairs[i].newDoc);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
error = e;
|
|
196
|
+
failingI = i;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If an error was raised, roll back changes in the inverse order
|
|
202
|
+
if (error) {
|
|
203
|
+
for (i = 0; i < failingI; i += 1) {
|
|
204
|
+
this.remove(pairs[i].newDoc);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (i = 0; i < pairs.length; i += 1) {
|
|
208
|
+
this.insert(pairs[i].oldDoc);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Revert an update
|
|
218
|
+
*/
|
|
219
|
+
Index.prototype.revertUpdate = function (oldDoc, newDoc) {
|
|
220
|
+
var revert = [];
|
|
221
|
+
|
|
222
|
+
if (!util.isArray(oldDoc)) {
|
|
223
|
+
this.update(newDoc, oldDoc);
|
|
224
|
+
} else {
|
|
225
|
+
oldDoc.forEach(function (pair) {
|
|
226
|
+
revert.push({ oldDoc: pair.newDoc, newDoc: pair.oldDoc });
|
|
227
|
+
});
|
|
228
|
+
this.update(revert);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get all documents in index whose key match value (if it is a Thing) or one of the elements of value (if it is an array of Things)
|
|
235
|
+
* @param {Thing} value Value to match the key against
|
|
236
|
+
* @return {Array of documents}
|
|
237
|
+
*/
|
|
238
|
+
Index.prototype.getMatching = function (value) {
|
|
239
|
+
var self = this;
|
|
240
|
+
|
|
241
|
+
if (!util.isArray(value)) {
|
|
242
|
+
return self.tree.search(value);
|
|
243
|
+
} else {
|
|
244
|
+
var _res = {}, res = [];
|
|
245
|
+
|
|
246
|
+
value.forEach(function (v) {
|
|
247
|
+
self.getMatching(v).forEach(function (doc) {
|
|
248
|
+
_res[doc._id] = doc;
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
Object.keys(_res).forEach(function (_id) {
|
|
253
|
+
res.push(_res[_id]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return res;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get all documents in index whose key is between bounds are they are defined by query
|
|
263
|
+
* Documents are sorted by key
|
|
264
|
+
* @param {Query} query
|
|
265
|
+
* @return {Array of documents}
|
|
266
|
+
*/
|
|
267
|
+
Index.prototype.getBetweenBounds = function (query) {
|
|
268
|
+
return this.tree.betweenBounds(query);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get all elements in the index
|
|
274
|
+
* @return {Array of documents}
|
|
275
|
+
*/
|
|
276
|
+
Index.prototype.getAll = function () {
|
|
277
|
+
var res = [];
|
|
278
|
+
|
|
279
|
+
this.tree.executeOnEveryNode(function (node) {
|
|
280
|
+
var i;
|
|
281
|
+
|
|
282
|
+
for (i = 0; i < node.data.length; i += 1) {
|
|
283
|
+
res.push(node.data[i]);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return res;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
// Interface
|
|
294
|
+
module.exports = Index;
|