@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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle every persistence-related task
|
|
3
|
+
* The interface Datastore expects to be implemented is
|
|
4
|
+
* * Persistence.loadDatabase(callback) and callback has signature err
|
|
5
|
+
* * Persistence.persistNewState(newDocs, callback) where newDocs is an array of documents and callback has signature err
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var storage = require('./storage')
|
|
9
|
+
, path = require('path')
|
|
10
|
+
, model = require('./model')
|
|
11
|
+
, async = require('./async')
|
|
12
|
+
, customUtils = require('./customUtils')
|
|
13
|
+
, Index = require('./indexes')
|
|
14
|
+
;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a new Persistence object for database options.db
|
|
19
|
+
* @param {Datastore} options.db
|
|
20
|
+
* @param {Boolean} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where
|
|
21
|
+
* Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion)
|
|
22
|
+
*/
|
|
23
|
+
function Persistence (options) {
|
|
24
|
+
var i, j, randomString;
|
|
25
|
+
|
|
26
|
+
this.db = options.db;
|
|
27
|
+
this.inMemoryOnly = this.db.inMemoryOnly;
|
|
28
|
+
this.filename = this.db.filename;
|
|
29
|
+
this.corruptAlertThreshold = options.corruptAlertThreshold !== undefined ? options.corruptAlertThreshold : 0.1;
|
|
30
|
+
|
|
31
|
+
if (!this.inMemoryOnly && this.filename && this.filename.charAt(this.filename.length - 1) === '~') {
|
|
32
|
+
throw new Error("The datafile name can't end with a ~, which is reserved for crash safe backup files");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// After serialization and before deserialization hooks with some basic sanity checks
|
|
36
|
+
if (options.afterSerialization && !options.beforeDeserialization) {
|
|
37
|
+
throw new Error("Serialization hook defined but deserialization hook undefined, cautiously refusing to start NeDB to prevent dataloss");
|
|
38
|
+
}
|
|
39
|
+
if (!options.afterSerialization && options.beforeDeserialization) {
|
|
40
|
+
throw new Error("Serialization hook undefined but deserialization hook defined, cautiously refusing to start NeDB to prevent dataloss");
|
|
41
|
+
}
|
|
42
|
+
this.afterSerialization = options.afterSerialization || function (s) { return s; };
|
|
43
|
+
this.beforeDeserialization = options.beforeDeserialization || function (s) { return s; };
|
|
44
|
+
for (i = 1; i < 30; i += 1) {
|
|
45
|
+
for (j = 0; j < 10; j += 1) {
|
|
46
|
+
randomString = customUtils.uid(i);
|
|
47
|
+
if (this.beforeDeserialization(this.afterSerialization(randomString)) !== randomString) {
|
|
48
|
+
throw new Error("beforeDeserialization is not the reverse of afterSerialization, cautiously refusing to start NeDB to prevent dataloss");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// For NW apps, store data in the same directory where NW stores application data
|
|
54
|
+
if (this.filename && options.nodeWebkitAppName) {
|
|
55
|
+
console.log("==================================================================");
|
|
56
|
+
console.log("WARNING: The nodeWebkitAppName option is deprecated");
|
|
57
|
+
console.log("To get the path to the directory where Node Webkit stores the data");
|
|
58
|
+
console.log("for your app, use the internal nw.gui module like this");
|
|
59
|
+
console.log("require('nw.gui').App.dataPath");
|
|
60
|
+
console.log("See https://github.com/rogerwang/node-webkit/issues/500");
|
|
61
|
+
console.log("==================================================================");
|
|
62
|
+
this.filename = Persistence.getNWAppFilename(options.nodeWebkitAppName, this.filename);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a directory exists and create it on the fly if it is not the case
|
|
69
|
+
* cb is optional, signature: err
|
|
70
|
+
*/
|
|
71
|
+
Persistence.ensureDirectoryExists = function (dir, cb) {
|
|
72
|
+
var callback = cb || function () {}
|
|
73
|
+
;
|
|
74
|
+
|
|
75
|
+
storage.mkdirp(dir).then(function() { return callback(); }).catch(callback);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Return the path the datafile if the given filename is relative to the directory where Node Webkit stores
|
|
83
|
+
* data for this application. Probably the best place to store data
|
|
84
|
+
*/
|
|
85
|
+
Persistence.getNWAppFilename = function (appName, relativeFilename) {
|
|
86
|
+
var home;
|
|
87
|
+
|
|
88
|
+
switch (process.platform) {
|
|
89
|
+
case 'win32':
|
|
90
|
+
case 'win64':
|
|
91
|
+
home = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
92
|
+
if (!home) { throw new Error("Couldn't find the base application data folder"); }
|
|
93
|
+
home = path.join(home, appName);
|
|
94
|
+
break;
|
|
95
|
+
case 'darwin':
|
|
96
|
+
home = process.env.HOME;
|
|
97
|
+
if (!home) { throw new Error("Couldn't find the base application data directory"); }
|
|
98
|
+
home = path.join(home, 'Library', 'Application Support', appName);
|
|
99
|
+
break;
|
|
100
|
+
case 'linux':
|
|
101
|
+
home = process.env.HOME;
|
|
102
|
+
if (!home) { throw new Error("Couldn't find the base application data directory"); }
|
|
103
|
+
home = path.join(home, '.config', appName);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
throw new Error("Can't use the Node Webkit relative path for platform " + process.platform);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return path.join(home, 'nedb-data', relativeFilename);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Persist cached database
|
|
116
|
+
* This serves as a compaction function since the cache always contains only the number of documents in the collection
|
|
117
|
+
* while the data file is append-only so it may grow larger
|
|
118
|
+
* @param {Function} cb Optional callback, signature: err
|
|
119
|
+
*/
|
|
120
|
+
Persistence.prototype.persistCachedDatabase = function (cb) {
|
|
121
|
+
var callback = cb || function () {}
|
|
122
|
+
, toPersist = ''
|
|
123
|
+
, self = this
|
|
124
|
+
;
|
|
125
|
+
|
|
126
|
+
if (this.inMemoryOnly) { return callback(null); }
|
|
127
|
+
|
|
128
|
+
this.db.getAllData().forEach(function (doc) {
|
|
129
|
+
toPersist += self.afterSerialization(model.serialize(doc)) + '\n';
|
|
130
|
+
});
|
|
131
|
+
Object.keys(this.db.indexes).forEach(function (fieldName) {
|
|
132
|
+
if (fieldName != "_id") { // The special _id index is managed by datastore.js, the others need to be persisted
|
|
133
|
+
toPersist += self.afterSerialization(model.serialize({ $$indexCreated: { fieldName: fieldName, unique: self.db.indexes[fieldName].unique, sparse: self.db.indexes[fieldName].sparse }})) + '\n';
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
storage.crashSafeWriteFile(this.filename, toPersist, function (err) {
|
|
138
|
+
if (err) { return callback(err); }
|
|
139
|
+
self.db.emit('compaction.done');
|
|
140
|
+
return callback(null);
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Queue a rewrite of the datafile
|
|
147
|
+
*/
|
|
148
|
+
Persistence.prototype.compactDatafile = function () {
|
|
149
|
+
this.db.executor.push({ this: this, fn: this.persistCachedDatabase, arguments: [] });
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Set automatic compaction every interval ms
|
|
155
|
+
* @param {Number} interval in milliseconds, with an enforced minimum of 5 seconds
|
|
156
|
+
*/
|
|
157
|
+
Persistence.prototype.setAutocompactionInterval = function (interval) {
|
|
158
|
+
var self = this
|
|
159
|
+
, minInterval = 5000
|
|
160
|
+
, realInterval = Math.max(interval || 0, minInterval)
|
|
161
|
+
;
|
|
162
|
+
|
|
163
|
+
this.stopAutocompaction();
|
|
164
|
+
|
|
165
|
+
this.autocompactionIntervalId = setInterval(function () {
|
|
166
|
+
self.compactDatafile();
|
|
167
|
+
}, realInterval);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Stop autocompaction (do nothing if autocompaction was not running)
|
|
173
|
+
*/
|
|
174
|
+
Persistence.prototype.stopAutocompaction = function () {
|
|
175
|
+
if (this.autocompactionIntervalId) { clearInterval(this.autocompactionIntervalId); }
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Persist new state for the given newDocs (can be insertion, update or removal)
|
|
181
|
+
* Use an append-only format
|
|
182
|
+
* @param {Array} newDocs Can be empty if no doc was updated/removed
|
|
183
|
+
* @param {Function} cb Optional, signature: err
|
|
184
|
+
*/
|
|
185
|
+
Persistence.prototype.persistNewState = function (newDocs, cb) {
|
|
186
|
+
var self = this
|
|
187
|
+
, toPersist = ''
|
|
188
|
+
, callback = cb || function () {}
|
|
189
|
+
;
|
|
190
|
+
|
|
191
|
+
// In-memory only datastore
|
|
192
|
+
if (self.inMemoryOnly) { return callback(null); }
|
|
193
|
+
|
|
194
|
+
newDocs.forEach(function (doc) {
|
|
195
|
+
toPersist += self.afterSerialization(model.serialize(doc)) + '\n';
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (toPersist.length === 0) { return callback(null); }
|
|
199
|
+
|
|
200
|
+
storage.appendFile(self.filename, toPersist, 'utf8', function (err) {
|
|
201
|
+
return callback(err);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* From a database's raw data, return the corresponding
|
|
208
|
+
* machine understandable collection
|
|
209
|
+
*/
|
|
210
|
+
Persistence.prototype.treatRawData = function (rawData) {
|
|
211
|
+
var data = rawData.split('\n')
|
|
212
|
+
, dataById = {}
|
|
213
|
+
, tdata = []
|
|
214
|
+
, i
|
|
215
|
+
, indexes = {}
|
|
216
|
+
, corruptItems = -1 // Last line of every data file is usually blank so not really corrupt
|
|
217
|
+
;
|
|
218
|
+
|
|
219
|
+
for (i = 0; i < data.length; i += 1) {
|
|
220
|
+
var doc;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
doc = model.deserialize(this.beforeDeserialization(data[i]));
|
|
224
|
+
if (doc._id) {
|
|
225
|
+
if (doc.$$deleted === true) {
|
|
226
|
+
delete dataById[doc._id];
|
|
227
|
+
} else {
|
|
228
|
+
dataById[doc._id] = doc;
|
|
229
|
+
}
|
|
230
|
+
} else if (doc.$$indexCreated && doc.$$indexCreated.fieldName != undefined) {
|
|
231
|
+
indexes[doc.$$indexCreated.fieldName] = doc.$$indexCreated;
|
|
232
|
+
} else if (typeof doc.$$indexRemoved === "string") {
|
|
233
|
+
delete indexes[doc.$$indexRemoved];
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
corruptItems += 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// A bit lenient on corruption
|
|
241
|
+
if (data.length > 0 && corruptItems / data.length > this.corruptAlertThreshold) {
|
|
242
|
+
throw new Error("More than " + Math.floor(100 * this.corruptAlertThreshold) + "% of the data file is corrupt, the wrong beforeDeserialization hook may be used. Cautiously refusing to start NeDB to prevent dataloss");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
Object.keys(dataById).forEach(function (k) {
|
|
246
|
+
tdata.push(dataById[k]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return { data: tdata, indexes: indexes };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Load the database
|
|
255
|
+
* 1) Create all indexes
|
|
256
|
+
* 2) Insert all data
|
|
257
|
+
* 3) Compact the database
|
|
258
|
+
* This means pulling data out of the data file or creating it if it doesn't exist
|
|
259
|
+
* Also, all data is persisted right away, which has the effect of compacting the database file
|
|
260
|
+
* This operation is very quick at startup for a big collection (60ms for ~10k docs)
|
|
261
|
+
* @param {Function} cb Optional callback, signature: err
|
|
262
|
+
*/
|
|
263
|
+
Persistence.prototype.loadDatabase = function (cb) {
|
|
264
|
+
var callback = cb || function () {}
|
|
265
|
+
, self = this
|
|
266
|
+
;
|
|
267
|
+
|
|
268
|
+
self.db.resetIndexes();
|
|
269
|
+
|
|
270
|
+
// In-memory only datastore
|
|
271
|
+
if (self.inMemoryOnly) { return callback(null); }
|
|
272
|
+
|
|
273
|
+
async.waterfall([
|
|
274
|
+
function (cb) {
|
|
275
|
+
Persistence.ensureDirectoryExists(path.dirname(self.filename), function (err) {
|
|
276
|
+
storage.ensureDatafileIntegrity(self.filename, function (err) {
|
|
277
|
+
storage.readFile(self.filename, 'utf8', function (err, rawData) {
|
|
278
|
+
if (err) { return cb(err); }
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
var treatedData = self.treatRawData(rawData);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return cb(e);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Recreate all indexes in the datafile
|
|
287
|
+
Object.keys(treatedData.indexes).forEach(function (key) {
|
|
288
|
+
self.db.indexes[key] = new Index(treatedData.indexes[key]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Fill cached database (i.e. all indexes) with data
|
|
292
|
+
try {
|
|
293
|
+
self.db.resetIndexes(treatedData.data);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
self.db.resetIndexes(); // Rollback any index which didn't fail
|
|
296
|
+
return cb(e);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
self.db.persistence.persistCachedDatabase(cb);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
], function (err) {
|
|
305
|
+
if (err) { return callback(err); }
|
|
306
|
+
|
|
307
|
+
self.db.executor.processBuffer();
|
|
308
|
+
return callback(null);
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
// Interface
|
|
314
|
+
module.exports = Persistence;
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Way data is stored for this database
|
|
3
|
+
* For a Node.js/Node Webkit database it's the file system
|
|
4
|
+
* For a browser-side database it's localforage which chooses the best option depending on user browser (IndexedDB then WebSQL then localStorage)
|
|
5
|
+
*
|
|
6
|
+
* This version is the Node.js/Node Webkit version
|
|
7
|
+
* It's essentially fs, mkdirp and crash safe write and read functions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var fs = require('fs')
|
|
11
|
+
, mkdirp = require('mkdirp')
|
|
12
|
+
, async = require('./async')
|
|
13
|
+
, path = require('path')
|
|
14
|
+
, storage = {}
|
|
15
|
+
;
|
|
16
|
+
|
|
17
|
+
storage.exists = fs.exists;
|
|
18
|
+
storage.rename = fs.rename;
|
|
19
|
+
storage.writeFile = fs.writeFile;
|
|
20
|
+
storage.unlink = fs.unlink;
|
|
21
|
+
storage.appendFile = fs.appendFile;
|
|
22
|
+
storage.readFile = fs.readFile;
|
|
23
|
+
storage.mkdirp = mkdirp;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Explicit name ...
|
|
28
|
+
*/
|
|
29
|
+
storage.ensureFileDoesntExist = function (file, callback) {
|
|
30
|
+
storage.exists(file, function (exists) {
|
|
31
|
+
if (!exists) { return callback(null); }
|
|
32
|
+
|
|
33
|
+
storage.unlink(file, function (err) { return callback(err); });
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Flush data in OS buffer to storage if corresponding option is set
|
|
40
|
+
* @param {String} options.filename
|
|
41
|
+
* @param {Boolean} options.isDir Optional, defaults to false
|
|
42
|
+
* If options is a string, it is assumed that the flush of the file (not dir) called options was requested
|
|
43
|
+
*/
|
|
44
|
+
storage.flushToStorage = function (options, callback) {
|
|
45
|
+
var filename, flags;
|
|
46
|
+
if (typeof options === 'string') {
|
|
47
|
+
filename = options;
|
|
48
|
+
flags = 'r+';
|
|
49
|
+
} else {
|
|
50
|
+
filename = options.filename;
|
|
51
|
+
flags = options.isDir ? 'r' : 'r+';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Windows can't fsync (FlushFileBuffers) directories. We can live with this as it cannot cause 100% dataloss
|
|
55
|
+
// except in the very rare event of the first time database is loaded and a crash happens
|
|
56
|
+
if (flags === 'r' && (process.platform === 'win32' || process.platform === 'win64')) { return callback(null); }
|
|
57
|
+
|
|
58
|
+
fs.open(filename, flags, function (err, fd) {
|
|
59
|
+
if (err) { return callback(err); }
|
|
60
|
+
fs.fsync(fd, function (errFS) {
|
|
61
|
+
fs.close(fd, function (errC) {
|
|
62
|
+
if (errFS || errC) {
|
|
63
|
+
var e = new Error('Failed to flush to storage');
|
|
64
|
+
e.errorOnFsync = errFS;
|
|
65
|
+
e.errorOnClose = errC;
|
|
66
|
+
return callback(e);
|
|
67
|
+
} else {
|
|
68
|
+
return callback(null);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost)
|
|
78
|
+
* @param {String} filename
|
|
79
|
+
* @param {String} data
|
|
80
|
+
* @param {Function} cb Optional callback, signature: err
|
|
81
|
+
*/
|
|
82
|
+
storage.crashSafeWriteFile = function (filename, data, cb) {
|
|
83
|
+
var callback = cb || function () {}
|
|
84
|
+
, tempFilename = filename + '~';
|
|
85
|
+
|
|
86
|
+
async.waterfall([
|
|
87
|
+
async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true })
|
|
88
|
+
, function (cb) {
|
|
89
|
+
storage.exists(filename, function (exists) {
|
|
90
|
+
if (exists) {
|
|
91
|
+
storage.flushToStorage(filename, function (err) { return cb(err); });
|
|
92
|
+
} else {
|
|
93
|
+
return cb();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
, function (cb) {
|
|
98
|
+
storage.writeFile(tempFilename, data, function (err) { return cb(err); });
|
|
99
|
+
}
|
|
100
|
+
, async.apply(storage.flushToStorage, tempFilename)
|
|
101
|
+
, function (cb) {
|
|
102
|
+
storage.rename(tempFilename, filename, function (err) { return cb(err); });
|
|
103
|
+
}
|
|
104
|
+
, async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true })
|
|
105
|
+
], function (err) { return callback(err); })
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Ensure the datafile contains all the data, even if there was a crash during a full file write
|
|
111
|
+
* @param {String} filename
|
|
112
|
+
* @param {Function} callback signature: err
|
|
113
|
+
*/
|
|
114
|
+
storage.ensureDatafileIntegrity = function (filename, callback) {
|
|
115
|
+
var tempFilename = filename + '~';
|
|
116
|
+
|
|
117
|
+
storage.exists(filename, function (filenameExists) {
|
|
118
|
+
// Write was successful
|
|
119
|
+
if (filenameExists) { return callback(null); }
|
|
120
|
+
|
|
121
|
+
storage.exists(tempFilename, function (oldFilenameExists) {
|
|
122
|
+
// New database
|
|
123
|
+
if (!oldFilenameExists) {
|
|
124
|
+
return storage.writeFile(filename, '', 'utf8', function (err) { callback(err); });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Write failed, use old version
|
|
128
|
+
storage.rename(tempFilename, filename, function (err) { return callback(err); });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// Interface
|
|
136
|
+
module.exports = storage;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal underscore.js replacement with only the functions needed by nedb
|
|
3
|
+
* This replaces the full underscore library to reduce dependencies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the intersection of arrays - elements that appear in all arrays
|
|
8
|
+
* @param {Array} array - First array
|
|
9
|
+
* @param {...Array} arrays - Additional arrays to intersect with
|
|
10
|
+
* @returns {Array} - Array of elements that appear in all input arrays
|
|
11
|
+
*/
|
|
12
|
+
function intersection(array) {
|
|
13
|
+
if (!Array.isArray(array)) return [];
|
|
14
|
+
|
|
15
|
+
var result = [];
|
|
16
|
+
var argsLength = arguments.length;
|
|
17
|
+
|
|
18
|
+
for (var i = 0; i < array.length; i++) {
|
|
19
|
+
var item = array[i];
|
|
20
|
+
var included = true;
|
|
21
|
+
|
|
22
|
+
// Check if item exists in all other arrays
|
|
23
|
+
for (var j = 1; j < argsLength; j++) {
|
|
24
|
+
var otherArray = arguments[j];
|
|
25
|
+
if (!Array.isArray(otherArray) || otherArray.indexOf(item) === -1) {
|
|
26
|
+
included = false;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add to result if not already there and exists in all arrays
|
|
32
|
+
if (included && result.indexOf(item) === -1) {
|
|
33
|
+
result.push(item);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extracts property values from an array of objects
|
|
42
|
+
* @param {Array} array - Array of objects
|
|
43
|
+
* @param {String} property - Property name to extract
|
|
44
|
+
* @returns {Array} - Array of extracted property values
|
|
45
|
+
*/
|
|
46
|
+
function pluck(array, property) {
|
|
47
|
+
if (!Array.isArray(array)) return [];
|
|
48
|
+
|
|
49
|
+
var result = [];
|
|
50
|
+
for (var i = 0; i < array.length; i++) {
|
|
51
|
+
if (array[i] && array[i].hasOwnProperty(property)) {
|
|
52
|
+
result.push(array[i][property]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns a copy of object with specified keys omitted
|
|
60
|
+
* @param {Object} obj - Source object
|
|
61
|
+
* @param {...String} keys - Keys to omit
|
|
62
|
+
* @returns {Object} - New object without the omitted keys
|
|
63
|
+
*/
|
|
64
|
+
function omit(obj) {
|
|
65
|
+
if (typeof obj !== 'object' || obj === null) return {};
|
|
66
|
+
|
|
67
|
+
var result = {};
|
|
68
|
+
var keysToOmit = Array.prototype.slice.call(arguments, 1);
|
|
69
|
+
|
|
70
|
+
for (var key in obj) {
|
|
71
|
+
if (obj.hasOwnProperty(key) && keysToOmit.indexOf(key) === -1) {
|
|
72
|
+
result[key] = obj[key];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Checks if object has the given key as a direct property
|
|
81
|
+
* @param {Object} obj - Object to check
|
|
82
|
+
* @param {String} key - Key to check for
|
|
83
|
+
* @returns {Boolean} - True if object has the key
|
|
84
|
+
*/
|
|
85
|
+
function has(obj, key) {
|
|
86
|
+
return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates a new array with results of calling provided function on every element
|
|
91
|
+
* @param {Array} array - Array to map over
|
|
92
|
+
* @param {Function} iteratee - Function to call for each element
|
|
93
|
+
* @returns {Array} - New array with mapped values
|
|
94
|
+
*/
|
|
95
|
+
function map(array, iteratee) {
|
|
96
|
+
if (!Array.isArray(array) || typeof iteratee !== 'function') return [];
|
|
97
|
+
|
|
98
|
+
var result = [];
|
|
99
|
+
for (var i = 0; i < array.length; i++) {
|
|
100
|
+
result.push(iteratee(array[i], i, array));
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a new array with all elements that pass the test implemented by provided function
|
|
107
|
+
* @param {Array} array - Array to filter
|
|
108
|
+
* @param {Function} predicate - Function to test each element
|
|
109
|
+
* @returns {Array} - New array with filtered elements
|
|
110
|
+
*/
|
|
111
|
+
function filter(array, predicate) {
|
|
112
|
+
if (!Array.isArray(array) || typeof predicate !== 'function') return [];
|
|
113
|
+
|
|
114
|
+
var result = [];
|
|
115
|
+
for (var i = 0; i < array.length; i++) {
|
|
116
|
+
if (predicate(array[i], i, array)) {
|
|
117
|
+
result.push(array[i]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates a duplicate-free version of an array
|
|
125
|
+
* @param {Array} array - Array to make unique
|
|
126
|
+
* @param {Function} iteratee - Optional function to compute uniqueness criterion
|
|
127
|
+
* @returns {Array} - New array with unique values
|
|
128
|
+
*/
|
|
129
|
+
function uniq(array, iteratee) {
|
|
130
|
+
if (!Array.isArray(array)) return [];
|
|
131
|
+
|
|
132
|
+
var result = [];
|
|
133
|
+
var seen = [];
|
|
134
|
+
|
|
135
|
+
for (var i = 0; i < array.length; i++) {
|
|
136
|
+
var value = array[i];
|
|
137
|
+
var computed = iteratee ? iteratee(value) : value;
|
|
138
|
+
|
|
139
|
+
if (seen.indexOf(computed) === -1) {
|
|
140
|
+
seen.push(computed);
|
|
141
|
+
result.push(value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Checks if value is a function
|
|
150
|
+
* @param {*} value - Value to check
|
|
151
|
+
* @returns {Boolean} - True if value is a function
|
|
152
|
+
*/
|
|
153
|
+
function isFunction(value) {
|
|
154
|
+
return typeof value === 'function';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Checks if value is a boolean
|
|
159
|
+
* @param {*} value - Value to check
|
|
160
|
+
* @returns {Boolean} - True if value is a boolean
|
|
161
|
+
*/
|
|
162
|
+
function isBoolean(value) {
|
|
163
|
+
return typeof value === 'boolean';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Export the functions
|
|
167
|
+
module.exports = {
|
|
168
|
+
intersection: intersection,
|
|
169
|
+
pluck: pluck,
|
|
170
|
+
omit: omit,
|
|
171
|
+
has: has,
|
|
172
|
+
map: map,
|
|
173
|
+
filter: filter,
|
|
174
|
+
uniq: uniq,
|
|
175
|
+
isFunction: isFunction,
|
|
176
|
+
isBoolean: isBoolean
|
|
177
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@electerm/nedb",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"author": "Louis Chatriot <louis.chatriot@gmail.com>",
|
|
5
|
+
"contributors": [
|
|
6
|
+
"Louis Chatriot"
|
|
7
|
+
],
|
|
8
|
+
"description": "File-based embedded data store for node.js",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"database",
|
|
11
|
+
"datastore",
|
|
12
|
+
"embedded"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/electerm/nedb",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+ssh://git@github.com/electerm/nedb.git"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@yetzt/binary-search-tree": "^0.2.6",
|
|
21
|
+
"mkdirp": "^1.0.4"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"async": "^3.2.0",
|
|
25
|
+
"chai": "^4.3.4",
|
|
26
|
+
"commander": "^7.2.0",
|
|
27
|
+
"exec-time": "^0.0.4",
|
|
28
|
+
"mocha": "^8.4.0",
|
|
29
|
+
"request": "^2.88.2",
|
|
30
|
+
"sinon": "^10.0.0",
|
|
31
|
+
"underscore": "^1.13.7"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "mocha --reporter spec --timeout 1000",
|
|
35
|
+
"pub": "npm publish --access public"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"lib/",
|
|
39
|
+
"index.js",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"main": "index",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/electerm/nedb/issues"
|
|
47
|
+
},
|
|
48
|
+
"directories": {
|
|
49
|
+
"lib": "lib",
|
|
50
|
+
"test": "test"
|
|
51
|
+
}
|
|
52
|
+
}
|