@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.
@@ -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
+ }