├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── dist ├── index.d.ts ├── index.js ├── util.d.ts └── util.js ├── package.json ├── src ├── index.ts └── util.ts ├── test ├── test_collection.js ├── test_db.js └── test_reduxdb.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.swp 3 | node_modules 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | js/ 2 | test/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | script: 5 | - npm install typescript mocha 6 | - make test 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 wizawu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build watch test 2 | 3 | build: 4 | tsc -d -p . 5 | 6 | watch: 7 | tsc -d -p . -w 8 | 9 | test: build 10 | mocha test 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reduxdb [![](https://badge.fury.io/js/reduxdb.svg)](https://www.npmjs.com/package/reduxdb) [![](https://travis-ci.org/wizawu/reduxdb.svg)](https://travis-ci.org/wizawu/reduxdb) 2 | 3 | Redux with MongoDB-like API. 4 | 5 | Notice that NOT all the features of MongoDB are implemented here, such as multi-index and query operators. 6 | 7 | The purpose of _reduxdb_ is to avoid writing almost the same actions and stores again and again in different projects. 8 | 9 | ### Installation 10 | 11 | ```shell 12 | npm install --save reduxdb 13 | ``` 14 | 15 | ### Usage 16 | 17 | ```javascript 18 | var reduxdb = require("reduxdb") 19 | 20 | var db = reduxdb.use("test") 21 | db.createCollection("users", {index: "id"}) 22 | db.createCollection("teams", {index: "id"}) 23 | db.createCollection("books") // using default index `_id` 24 | 25 | db.subscribe(function() { 26 | console.log(db.stats()) 27 | console.log(db.users.stats()) 28 | console.log(db.teams.stats()) 29 | console.log(db.books.stats()) 30 | }) 31 | 32 | db.users.insert({id: "1234", name: "wizawu"}) 33 | db.users.findOne({name: "wizawu"}).id // 1234 34 | ``` 35 | 36 | ### API 37 | 38 | #### reduxdb 39 | 40 | + use(name) 41 | 42 | #### reduxdb.DB 43 | 44 | + [createCollection(name, options)](https://docs.mongodb.org/manual/reference/method/db.createCollection/) 45 | 46 | `options` can be only used to define index, for example 47 | 48 | ``` 49 | db.createCollection("user", {index: "uid"}) 50 | ``` 51 | 52 | + [getCollection(name)](https://docs.mongodb.org/manual/reference/method/db.getCollection/) 53 | + [getCollectionNames()](https://docs.mongodb.org/manual/reference/method/db.getCollectionNames/) 54 | + [getName()](https://docs.mongodb.org/manual/reference/method/db.getName/) 55 | + [stats()](https://docs.mongodb.org/manual/reference/method/db.stats/) 56 | + [subscribe(listener)](http://redux.js.org/docs/api/Store.html#subscribe) 57 | 58 | #### reduxdb.Collection 59 | 60 | + [copyTo(newCollection)](https://docs.mongodb.org/manual/reference/method/db.collection.copyTo/) 61 | + [count()](https://docs.mongodb.org/manual/reference/method/db.collection.count/) 62 | + [drop()](https://docs.mongodb.org/manual/reference/method/db.collection.drop/) 63 | + [find(query)](https://docs.mongodb.org/manual/reference/method/db.collection.find/) 64 | 65 | Only support [Query for Equality](https://docs.mongodb.org/manual/reference/method/db.collection.find/#query-for-equality), for example 66 | 67 | ``` 68 | // Return all users matching {"age": 18, "name": {"first": "Andrew"}} 69 | db.user.find({"age": 18, "name.first": "Andrew"}) 70 | ``` 71 | 72 | + [findOne(query)](https://docs.mongodb.org/manual/reference/method/db.collection.findOne/) 73 | 74 | Same `query` type as `find()`. 75 | 76 | + getDB() 77 | + getFullName() 78 | + getIndexKeys() 79 | + getName() 80 | + [insert(documents)](https://docs.mongodb.org/manual/reference/method/db.collection.insert/) 81 | 82 | No `options` supported here. 83 | 84 | + [remove(query)](https://docs.mongodb.org/manual/reference/method/db.collection.remove/) 85 | 86 | Same `query` type as `find()`. 87 | 88 | + [renameCollection(newName)](https://docs.mongodb.org/manual/reference/method/db.collection.renameCollection/) 89 | + [save(document)](https://docs.mongodb.org/manual/reference/method/db.collection.save/) 90 | + [stats()](https://docs.mongodb.org/manual/reference/method/db.collection.stats/) 91 | + [update(query, update, options)](https://docs.mongodb.org/manual/reference/method/db.collection.update/) 92 | 93 | Same `query` type as `find()`. `upsert` and `multi` are supported in `options`. 94 | 95 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as redux from "redux"; 2 | import { Map } from "./util"; 3 | export declare function newObjectId(): string; 4 | export declare namespace Collection { 5 | interface Options { 6 | index?: string; 7 | } 8 | interface UpdateOptions { 9 | upsert?: boolean; 10 | multi?: boolean; 11 | } 12 | } 13 | export declare class Collection { 14 | private __db__; 15 | private __name__; 16 | private __index__; 17 | private __data__; 18 | constructor(db: DB, name: string, options?: Collection.Options); 19 | copyTo(newCollection: string): number; 20 | count(): number; 21 | drop(): boolean; 22 | find(query?: any): any[]; 23 | findOne(query?: any): any; 24 | getDB(): DB; 25 | getFullName(): string; 26 | getIndexKeys(): any[]; 27 | getName(): string; 28 | insert(doc: any): void; 29 | remove(query?: any): void; 30 | renameCollection(newName: string): any; 31 | save(doc: any): void; 32 | stats(): any; 33 | update(query: any, doc: any, options?: Collection.UpdateOptions): void; 34 | __insert__(doc_: any): any; 35 | __remove__(query: any): any; 36 | __save__(doc: any): any; 37 | __update__(query: any, doc: any, options?: Collection.UpdateOptions): any; 38 | } 39 | export interface DB$ { 40 | [name: string]: Collection; 41 | } 42 | export declare class DB { 43 | private __name__; 44 | __collections__: Map; 45 | __store__: redux.Store; 46 | constructor(name: string); 47 | createCollection(name: string, options?: Collection.Options): any; 48 | getCollection(name: string): Collection; 49 | getCollectionNames(): string[]; 50 | getName(): string; 51 | stats(): any; 52 | subscribe(func: any, that?: any): any; 53 | } 54 | export declare function use(name: string): DB; 55 | export declare function drop(name: string): void; 56 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | var redux = require("redux"); 12 | var util_1 = require("./util"); 13 | function newObjectId() { 14 | var prefix = (global.location ? global.location.href.length : global.process.pid).toString(16); 15 | prefix = prefix.length < 2 ? ("0" + prefix) : prefix.substr(-2); 16 | var tsPart = Date.now().toString(16); 17 | tsPart = tsPart.length < 12 ? ("0" + tsPart) : tsPart.substr(-12); 18 | var suffix = Math.floor(Math.random() * Math.pow(16, 10)).toString(16); 19 | while (suffix.length < 10) 20 | suffix = "0" + suffix; 21 | return prefix + tsPart + suffix; 22 | } 23 | exports.newObjectId = newObjectId; 24 | var Collection = (function () { 25 | function Collection(db, name, options) { 26 | this.__index__ = "_id"; 27 | this.__data__ = {}; 28 | this.__db__ = db; 29 | this.__name__ = name; 30 | if (options && options.index) 31 | this.__index__ = options.index; 32 | } 33 | Collection.prototype.copyTo = function (newCollection) { 34 | this.__db__.createCollection(newCollection, { index: this.__index__ }); 35 | var collection = this.__db__.getCollection(newCollection); 36 | collection.__data__ = __assign({}, collection.__data__, this.__data__); 37 | return this.count(); 38 | }; 39 | Collection.prototype.count = function () { 40 | return Object.keys(this.__data__).length; 41 | }; 42 | Collection.prototype.drop = function () { 43 | var db = this.__db__; 44 | var name = this.__name__; 45 | if (db.__collections__.get(name)) { 46 | db.__collections__.delete(name); 47 | delete db[name]; 48 | return true; 49 | } 50 | else { 51 | return false; 52 | } 53 | }; 54 | Collection.prototype.find = function (query) { 55 | var data = this.__data__; 56 | if (query === undefined) { 57 | return util_1.values(data); 58 | } 59 | if (typeof query !== "object") { 60 | return []; 61 | } 62 | else { 63 | var result_1 = []; 64 | util_1.values(data).forEach(function (v) { 65 | var ok = true; 66 | Object.keys(query).forEach(function (k) { 67 | if (util_1.deepGet(v, k) !== query[k]) 68 | ok = false; 69 | }); 70 | if (ok) 71 | result_1.push(v); 72 | }); 73 | return result_1; 74 | } 75 | }; 76 | Collection.prototype.findOne = function (query) { 77 | var result = this.find(query); 78 | return result.length === 0 ? null : result[0]; 79 | }; 80 | Collection.prototype.getDB = function () { 81 | return this.__db__; 82 | }; 83 | Collection.prototype.getFullName = function () { 84 | return this.__db__.getName() + "." + this.__name__; 85 | }; 86 | Collection.prototype.getIndexKeys = function () { 87 | var result = [{}]; 88 | result[0][this.__index__] = 1; 89 | return result; 90 | }; 91 | Collection.prototype.getName = function () { 92 | return this.__name__; 93 | }; 94 | Collection.prototype.insert = function (doc) { 95 | if (!doc) 96 | throw "no object passed to insert"; 97 | this.__db__.__store__.dispatch({ 98 | ns: this.getFullName(), 99 | type: "insert", 100 | doc: doc 101 | }); 102 | }; 103 | Collection.prototype.remove = function (query) { 104 | this.__db__.__store__.dispatch({ 105 | ns: this.getFullName(), 106 | type: "remove", 107 | query: query 108 | }); 109 | }; 110 | Collection.prototype.renameCollection = function (newName) { 111 | var db = this.__db__; 112 | if (db[newName]) { 113 | return { "ok": 0, "errmsg": "target namespace exists" }; 114 | } 115 | else { 116 | db[newName] = this; 117 | db.__collections__.set(newName, this); 118 | delete db[this.__name__]; 119 | db.__collections__.delete(this.__name__); 120 | this.__name__ = newName; 121 | return { "ok": 1 }; 122 | } 123 | }; 124 | Collection.prototype.save = function (doc) { 125 | if (!doc) 126 | throw "can't save a null"; 127 | this.__db__.__store__.dispatch({ 128 | ns: this.getFullName(), 129 | type: "save", 130 | doc: doc 131 | }); 132 | }; 133 | Collection.prototype.stats = function () { 134 | return { 135 | "ns": this.getFullName(), 136 | "count": this.count(), 137 | "ok": 1 138 | }; 139 | }; 140 | Collection.prototype.update = function (query, doc, options) { 141 | if (!query) 142 | throw "need a query"; 143 | if (!doc) 144 | throw "need an object"; 145 | this.__db__.__store__.dispatch({ 146 | ns: this.getFullName(), 147 | type: "update", 148 | query: query, 149 | doc: doc, 150 | options: options 151 | }); 152 | }; 153 | Collection.prototype.__insert__ = function (doc_) { 154 | var _this = this; 155 | var index = this.__index__; 156 | var docs = []; 157 | if (typeof doc_.length === "number") { 158 | docs = doc_; 159 | } 160 | else { 161 | docs = [doc_]; 162 | } 163 | var keySet = {}; 164 | var result = null; 165 | docs.forEach(function (doc) { 166 | var key = doc[index] || newObjectId(); 167 | if (_this.__data__[key] || keySet[key]) { 168 | result = { "nInserted": 0, "errmsg": "duplicate key" }; 169 | } 170 | keySet[key] = true; 171 | }); 172 | if (result) 173 | return result; 174 | var count = 0; 175 | docs.forEach(function (doc) { 176 | var key = doc[index] || newObjectId(); 177 | var newDoc = __assign({}, doc); 178 | newDoc[index] = key; 179 | _this.__data__[key] = newDoc; 180 | count += 1; 181 | }); 182 | return { "nInserted": count }; 183 | }; 184 | Collection.prototype.__remove__ = function (query) { 185 | var _this = this; 186 | var data = this.__data__; 187 | if (query === undefined) { 188 | var result = { "nRemoved": this.count() }; 189 | this.__data__ = {}; 190 | return result; 191 | } 192 | if (typeof query !== "object") { 193 | return { "nRemoved": 0 }; 194 | } 195 | else { 196 | var count_1 = 0; 197 | util_1.values(data).forEach(function (v) { 198 | var ok = true; 199 | Object.keys(query).forEach(function (k) { 200 | if (util_1.deepGet(v, k, undefined) !== query[k]) 201 | ok = false; 202 | }); 203 | if (ok) { 204 | count_1 += 1; 205 | delete data[v[_this.__index__]]; 206 | } 207 | }); 208 | return { "nRemoved": count_1 }; 209 | } 210 | }; 211 | Collection.prototype.__save__ = function (doc) { 212 | var index = this.__index__; 213 | var result = __assign({}, doc); 214 | if (!result[index]) 215 | result[index] = newObjectId(); 216 | var key = result[index]; 217 | this.__data__[key] = result; 218 | return result; 219 | }; 220 | Collection.prototype.__update__ = function (query, doc, options) { 221 | var _this = this; 222 | var upsert = false; 223 | var multi = false; 224 | if (options) { 225 | upsert = options.upsert || false; 226 | multi = options.multi || false; 227 | } 228 | var nMatched = 0; 229 | var nUpserted = 0; 230 | var nModified = 0; 231 | var index = this.__index__; 232 | util_1.values(this.__data__).forEach(function (v) { 233 | var ok = true; 234 | Object.keys(query).forEach(function (k) { 235 | if (util_1.deepGet(v, k, undefined) !== query[k]) 236 | ok = false; 237 | }); 238 | if (ok) { 239 | if (multi || nModified < 1) { 240 | var newDoc = __assign({}, doc); 241 | newDoc[index] = v[index]; 242 | _this.__data__[v[index]] = newDoc; 243 | nMatched += 1; 244 | nModified += 1; 245 | } 246 | } 247 | }); 248 | if (nModified === 0 && upsert) { 249 | var newDoc = __assign({}, doc); 250 | var key = doc[index] || newObjectId(); 251 | newDoc[index] = key; 252 | this.__data__[key] = newDoc; 253 | nUpserted = 1; 254 | } 255 | return { 256 | "nMatched": nMatched, 257 | "nUpserted": nUpserted, 258 | "nModified": nModified 259 | }; 260 | }; 261 | return Collection; 262 | }()); 263 | exports.Collection = Collection; 264 | var DB = (function () { 265 | function DB(name) { 266 | var _this = this; 267 | this.__collections__ = new util_1.Map(); 268 | this.__name__ = name; 269 | var reducer = redux.combineReducers({ 270 | all: function (_, args) { 271 | var ns = args.ns, type = args.type, query = args.query, doc = args.doc, options = args.options; 272 | _this.__collections__.forEach(function (collection) { 273 | if (collection.getFullName() === ns) { 274 | switch (type) { 275 | case "insert": 276 | collection.__insert__(doc); 277 | break; 278 | case "remove": 279 | collection.__remove__(query); 280 | break; 281 | case "save": 282 | collection.__save__(doc); 283 | break; 284 | case "update": 285 | collection.__update__(query, doc, options); 286 | break; 287 | default: 288 | break; 289 | } 290 | } 291 | }); 292 | return _this.__collections__; 293 | } 294 | }); 295 | this.__store__ = redux.createStore(reducer, {}); 296 | } 297 | DB.prototype.createCollection = function (name, options) { 298 | if (this.hasOwnProperty(name)) { 299 | return { "ok": 0, "errmsg": "collection already exists" }; 300 | } 301 | else { 302 | this[name] = new Collection(this, name, options); 303 | this.__collections__.set(name, this[name]); 304 | return { "ok": 1 }; 305 | } 306 | }; 307 | DB.prototype.getCollection = function (name) { 308 | if (!name) 309 | throw "Collection constructor called with undefined argument"; 310 | this.createCollection(name); 311 | return this.__collections__.get(name); 312 | }; 313 | DB.prototype.getCollectionNames = function () { 314 | var result = []; 315 | this.__collections__.forEach(function (_, k) { return result.push(k); }); 316 | return result; 317 | }; 318 | DB.prototype.getName = function () { 319 | return this.__name__; 320 | }; 321 | DB.prototype.stats = function () { 322 | var objects = 0; 323 | this.__collections__.forEach(function (c) { return objects += c.count(); }); 324 | return { 325 | "db": this.__name__, 326 | "collections": this.__collections__.size, 327 | "objects": objects, 328 | "ok": 1 329 | }; 330 | }; 331 | DB.prototype.subscribe = function (func, that) { 332 | var unsubscribe = this.__store__.subscribe(func); 333 | if (that) { 334 | that.__componentWillUnmount__ = that.componentWillUnmount || (function (_) { return _; }); 335 | that.componentWillUnmount = function () { 336 | that.__componentWillUnmount__(); 337 | unsubscribe(); 338 | }; 339 | } 340 | return unsubscribe; 341 | }; 342 | return DB; 343 | }()); 344 | exports.DB = DB; 345 | var dbs = new util_1.Map(); 346 | function use(name) { 347 | if (!dbs.has(name)) 348 | dbs.set(name, new DB(name)); 349 | return dbs.get(name); 350 | } 351 | exports.use = use; 352 | function drop(name) { 353 | return dbs.delete(name); 354 | } 355 | exports.drop = drop; 356 | -------------------------------------------------------------------------------- /dist/util.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare const deepGet: { 3 | (object: T, path: ObjectPathGlobal.IPath, defaultValue?: TResult | undefined): TResult; 4 | (object: T): T; 5 | (): void; 6 | }; 7 | export declare class Map { 8 | private __map__; 9 | size: number; 10 | forEach(callback: (value: T, key: string) => void): void; 11 | has(key: string): boolean; 12 | get(key: string): T; 13 | set(key: string, value: T): void; 14 | delete(key: string): void; 15 | } 16 | export declare function values(obj: any): any[]; 17 | -------------------------------------------------------------------------------- /dist/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var object_path_1 = require("object-path"); 4 | exports.deepGet = object_path_1.get; 5 | var Map = (function () { 6 | function Map() { 7 | this.__map__ = {}; 8 | this.size = 0; 9 | } 10 | Map.prototype.forEach = function (callback) { 11 | var _this = this; 12 | Object.keys(this.__map__).forEach(function (key) { 13 | var value = _this.__map__[key]; 14 | callback(value, key); 15 | }); 16 | }; 17 | Map.prototype.has = function (key) { 18 | return this.__map__[key] !== undefined; 19 | }; 20 | Map.prototype.get = function (key) { 21 | return this.__map__[key]; 22 | }; 23 | Map.prototype.set = function (key, value) { 24 | this.__map__[key] = value; 25 | this.size = Object.keys(this.__map__).length; 26 | }; 27 | Map.prototype.delete = function (key) { 28 | delete this.__map__[key]; 29 | this.size = Object.keys(this.__map__).length; 30 | }; 31 | return Map; 32 | }()); 33 | exports.Map = Map; 34 | function values(obj) { 35 | return Object.keys(obj).map(function (k) { return obj[k]; }); 36 | } 37 | exports.values = values; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reduxdb", 3 | "version": "3.1.0", 4 | "repository": "github:wizawu/reduxdb", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "dependencies": { 8 | "object-path": "^0.11.4", 9 | "redux": "^3.6.0" 10 | }, 11 | "devDependencies": { 12 | "@types/object-path": "^0.9.28", 13 | "@types/redux": "^3.6.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as redux from "redux" 2 | import { Map, values, deepGet } from "./util" 3 | 4 | declare const global: any 5 | 6 | export function newObjectId(): string { 7 | let prefix: string = ( 8 | global.location ? global.location.href.length : global.process.pid 9 | ).toString(16) 10 | prefix = prefix.length < 2 ? ("0" + prefix) : prefix.substr(-2) 11 | 12 | let tsPart: string = Date.now().toString(16) 13 | tsPart = tsPart.length < 12 ? ("0" + tsPart) : tsPart.substr(-12) 14 | 15 | let suffix = Math.floor(Math.random() * Math.pow(16, 10)).toString(16) 16 | while (suffix.length < 10) suffix = "0" + suffix 17 | 18 | return prefix + tsPart + suffix 19 | } 20 | 21 | export declare namespace Collection { 22 | interface Options { 23 | index?: string 24 | } 25 | 26 | interface UpdateOptions { 27 | upsert?: boolean 28 | multi?: boolean 29 | } 30 | } 31 | 32 | export class Collection { 33 | private __db__: DB 34 | private __name__: string 35 | private __index__: string = "_id" 36 | private __data__: any = {} 37 | 38 | constructor(db: DB, name: string, options?: Collection.Options) { 39 | this.__db__ = db 40 | this.__name__ = name 41 | if (options && options.index) this.__index__ = options.index 42 | } 43 | 44 | copyTo(newCollection: string): number { 45 | this.__db__.createCollection(newCollection, { index: this.__index__ }) 46 | let collection: any = this.__db__.getCollection(newCollection) 47 | collection.__data__ = { ...collection.__data__, ...this.__data__ } 48 | return this.count() 49 | } 50 | 51 | count(): number { 52 | return Object.keys(this.__data__).length 53 | } 54 | 55 | drop(): boolean { 56 | let db = this.__db__ 57 | let name = this.__name__ 58 | if (db.__collections__.get(name)) { 59 | db.__collections__.delete(name) 60 | delete db[name] 61 | return true 62 | } else { 63 | return false 64 | } 65 | } 66 | 67 | find(query?: any): any[] { 68 | let data = this.__data__ 69 | if (query === undefined) { 70 | return values(data) 71 | } if (typeof query !== "object") { 72 | return [] 73 | } else { 74 | let result: any[] = [] 75 | values(data).forEach(v => { 76 | let ok = true 77 | Object.keys(query).forEach(k => { 78 | if (deepGet(v, k) !== query[k]) ok = false 79 | }) 80 | if (ok) result.push(v) 81 | }) 82 | return result 83 | } 84 | } 85 | 86 | findOne(query?: any): any { 87 | let result = this.find(query) 88 | return result.length === 0 ? null : result[0] 89 | } 90 | 91 | getDB(): DB { 92 | return this.__db__ 93 | } 94 | 95 | getFullName(): string { 96 | return this.__db__.getName() + "." + this.__name__ 97 | } 98 | 99 | getIndexKeys(): any[] { 100 | let result = [{}] 101 | result[0][this.__index__] = 1 102 | return result 103 | } 104 | 105 | getName(): string { 106 | return this.__name__ 107 | } 108 | 109 | insert(doc: any): void { 110 | if (!doc) throw "no object passed to insert" 111 | this.__db__.__store__.dispatch({ 112 | ns: this.getFullName(), 113 | type: "insert", 114 | doc: doc 115 | }) 116 | } 117 | 118 | remove(query?: any): void { 119 | this.__db__.__store__.dispatch({ 120 | ns: this.getFullName(), 121 | type: "remove", 122 | query: query 123 | }) 124 | } 125 | 126 | renameCollection(newName: string): any { 127 | let db = this.__db__ 128 | if (db[newName]) { 129 | return { "ok": 0, "errmsg": "target namespace exists" } 130 | } else { 131 | db[newName] = this 132 | db.__collections__.set(newName, this) 133 | delete db[this.__name__] 134 | db.__collections__.delete(this.__name__) 135 | this.__name__ = newName 136 | return { "ok": 1 } 137 | } 138 | } 139 | 140 | save(doc: any): void { 141 | if (!doc) throw "can't save a null" 142 | this.__db__.__store__.dispatch({ 143 | ns: this.getFullName(), 144 | type: "save", 145 | doc: doc 146 | }) 147 | } 148 | 149 | stats(): any { 150 | return { 151 | "ns": this.getFullName(), 152 | "count": this.count(), 153 | "ok": 1 154 | } 155 | } 156 | 157 | update(query: any, doc: any, options?: Collection.UpdateOptions): void { 158 | if (!query) throw "need a query" 159 | if (!doc) throw "need an object" 160 | this.__db__.__store__.dispatch({ 161 | ns: this.getFullName(), 162 | type: "update", 163 | query: query, 164 | doc: doc, 165 | options: options 166 | }) 167 | } 168 | 169 | __insert__(doc_: any): any { 170 | let index = this.__index__ 171 | let docs: any[] = [] 172 | if (typeof doc_.length === "number") { 173 | docs = doc_ 174 | } else { 175 | docs = [doc_] 176 | } 177 | 178 | let keySet = {} 179 | let result: any = null 180 | docs.forEach(doc => { 181 | let key = doc[index] || newObjectId() 182 | if (this.__data__[key] || keySet[key]) { 183 | result = { "nInserted": 0, "errmsg": "duplicate key" } 184 | } 185 | keySet[key] = true 186 | }) 187 | if (result) return result 188 | 189 | let count = 0 190 | docs.forEach(doc => { 191 | let key = doc[index] || newObjectId() 192 | let newDoc = { ...doc } 193 | newDoc[index] = key 194 | this.__data__[key] = newDoc 195 | count += 1 196 | }) 197 | return { "nInserted": count } 198 | } 199 | 200 | __remove__(query: any): any { 201 | let data = this.__data__ 202 | if (query === undefined) { 203 | let result = { "nRemoved": this.count() } 204 | this.__data__ = {} 205 | return result 206 | } if (typeof query !== "object") { 207 | return { "nRemoved": 0 } 208 | } else { 209 | let count = 0 210 | values(data).forEach(v => { 211 | let ok = true 212 | Object.keys(query).forEach(k => { 213 | if (deepGet(v, k, undefined) !== query[k]) ok = false 214 | }) 215 | if (ok) { 216 | count += 1 217 | delete data[v[this.__index__]] 218 | } 219 | }) 220 | return { "nRemoved": count } 221 | } 222 | } 223 | 224 | __save__(doc: any): any { 225 | let index = this.__index__ 226 | let result = { ...doc } 227 | if (!result[index]) result[index] = newObjectId() 228 | let key = result[index] 229 | this.__data__[key] = result 230 | return result 231 | } 232 | 233 | __update__(query: any, doc: any, options?: Collection.UpdateOptions): any { 234 | let upsert = false 235 | let multi = false 236 | if (options) { 237 | upsert = options.upsert || false 238 | multi = options.multi || false 239 | } 240 | let nMatched = 0 241 | let nUpserted = 0 242 | let nModified = 0 243 | let index = this.__index__ 244 | values(this.__data__).forEach(v => { 245 | let ok = true 246 | Object.keys(query).forEach(k => { 247 | if (deepGet(v, k, undefined) !== query[k]) ok = false 248 | }) 249 | if (ok) { 250 | if (multi || nModified < 1) { 251 | let newDoc = { ...doc } 252 | newDoc[index] = v[index] 253 | this.__data__[v[index]] = newDoc 254 | nMatched += 1 255 | nModified += 1 256 | } 257 | } 258 | }) 259 | if (nModified === 0 && upsert) { 260 | let newDoc = { ...doc } 261 | let key = doc[index] || newObjectId() 262 | newDoc[index] = key 263 | this.__data__[key] = newDoc 264 | nUpserted = 1 265 | } 266 | return { 267 | "nMatched": nMatched, 268 | "nUpserted": nUpserted, 269 | "nModified": nModified 270 | } 271 | } 272 | } 273 | 274 | export interface DB$ { 275 | [name: string]: Collection 276 | } 277 | 278 | export class DB { 279 | private __name__: string 280 | __collections__: Map = new Map() 281 | __store__: redux.Store 282 | 283 | constructor(name: string) { 284 | this.__name__ = name 285 | let reducer = redux.combineReducers({ 286 | all: (_, args: any) => { 287 | let { ns, type, query, doc, options } = args 288 | this.__collections__.forEach(collection => { 289 | if (collection.getFullName() === ns) { 290 | switch (type) { 291 | case "insert": 292 | collection.__insert__(doc) 293 | break 294 | case "remove": 295 | collection.__remove__(query) 296 | break 297 | case "save": 298 | collection.__save__(doc) 299 | break 300 | case "update": 301 | collection.__update__(query, doc, options) 302 | break 303 | default: 304 | break 305 | } 306 | } 307 | }) 308 | return this.__collections__ 309 | } 310 | }) 311 | this.__store__ = redux.createStore(reducer, {}) 312 | } 313 | 314 | createCollection(name: string, options?: Collection.Options): any { 315 | if (this.hasOwnProperty(name)) { 316 | return { "ok": 0, "errmsg": "collection already exists" } 317 | } else { 318 | this[name] = new Collection(this, name, options) 319 | this.__collections__.set(name, this[name]) 320 | return { "ok": 1 } 321 | } 322 | } 323 | 324 | getCollection(name: string): Collection { 325 | if (!name) throw "Collection constructor called with undefined argument" 326 | this.createCollection(name) 327 | return this.__collections__.get(name) as Collection 328 | } 329 | 330 | getCollectionNames(): string[] { 331 | let result: string[] = [] 332 | this.__collections__.forEach((_, k) => result.push(k)) 333 | return result 334 | } 335 | 336 | getName(): string { 337 | return this.__name__ 338 | } 339 | 340 | stats(): any { 341 | let objects = 0 342 | this.__collections__.forEach(c => objects += c.count()) 343 | return { 344 | "db": this.__name__, 345 | "collections": this.__collections__.size, 346 | "objects": objects, 347 | "ok": 1 348 | } 349 | } 350 | 351 | subscribe(func: any, that?: any): any { 352 | let unsubscribe = this.__store__.subscribe(func) 353 | if (that) { 354 | that.__componentWillUnmount__ = that.componentWillUnmount || (_ => _) 355 | that.componentWillUnmount = () => { 356 | that.__componentWillUnmount__() 357 | unsubscribe() 358 | } 359 | } 360 | return unsubscribe 361 | } 362 | } 363 | 364 | const dbs: Map = new Map() 365 | 366 | export function use(name: string): DB { 367 | if (!dbs.has(name)) dbs.set(name, new DB(name)) 368 | return dbs.get(name) as DB 369 | } 370 | 371 | export function drop(name: string) { 372 | return dbs.delete(name) 373 | } 374 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { get } from "object-path" 2 | 3 | export const deepGet = get 4 | 5 | export class Map { 6 | private __map__: any = {} 7 | public size: number = 0 8 | 9 | forEach(callback: (value: T, key: string) => void) { 10 | Object.keys(this.__map__).forEach((key: string) => { 11 | let value: T = this.__map__[key] 12 | callback(value, key) 13 | }) 14 | } 15 | 16 | has(key: string): boolean { 17 | return this.__map__[key] !== undefined 18 | } 19 | 20 | get(key: string): T { 21 | return this.__map__[key] 22 | } 23 | 24 | set(key: string, value: T) { 25 | this.__map__[key] = value 26 | this.size = Object.keys(this.__map__).length 27 | } 28 | 29 | delete(key: string) { 30 | delete this.__map__[key] 31 | this.size = Object.keys(this.__map__).length 32 | } 33 | } 34 | 35 | export function values(obj: any): any[] { 36 | return Object.keys(obj).map(k => obj[k]) 37 | } -------------------------------------------------------------------------------- /test/test_collection.js: -------------------------------------------------------------------------------- 1 | const { deepEqual, notEqual } = require("assert") 2 | const reduxdb = require("../dist/index") 3 | 4 | describe("collection", () => { 5 | let db 6 | 7 | beforeEach(() => { 8 | reduxdb.drop("test") 9 | db = reduxdb.use("test") 10 | db.createCollection("user", { index: "id" }) 11 | db.user.insert({ id: 1, name: "Buffon" }) 12 | db.user.insert({ id: 2, name: "Pirlo" }) 13 | db.user.insert({ id: 3, name: "Pogba" }) 14 | }) 15 | 16 | it("copyTo", () => { 17 | deepEqual(db.user.copyTo("visitor"), 3) 18 | deepEqual(db.visitor.count(), 3) 19 | deepEqual(db.visitor.findOne({ id: 1 }).name, "Buffon") 20 | deepEqual(db.visitor.findOne({ id: 2 }).name, "Pirlo") 21 | deepEqual(db.visitor.findOne({ id: 3 }).name, "Pogba") 22 | deepEqual(db.getCollection("empty").copyTo("user"), 0) 23 | }) 24 | 25 | it("count", () => { 26 | deepEqual(db.user.count(), 3) 27 | }) 28 | 29 | it("drop", () => { 30 | deepEqual(db.user.drop(), true) 31 | deepEqual(db.user, undefined) 32 | deepEqual(db.getCollection("user").count(), 0) 33 | }) 34 | 35 | it("find", () => { 36 | deepEqual(db.user.find().map(u => u.id).sort(), [1, 2, 3]) 37 | deepEqual(db.user.find({ id: 0 }), []) 38 | deepEqual(db.user.find({ id: 2 }), [{ id: 2, name: "Pirlo" }]) 39 | deepEqual(db.user.find({ id: 2, name: "Pirlo" }), [{ id: 2, name: "Pirlo" }]) 40 | deepEqual(db.user.find({ id: 3, name: "Pirlo" }), []) 41 | }) 42 | 43 | it("insert", () => { 44 | db.user.insert({ id: 4, name: "Pogba" }) 45 | deepEqual(db.user.find({ name: "Pogba" }).map(u => u.id).sort(), [3, 4]) 46 | 47 | db.user.insert({ id: 3, name: "Dybala" }) 48 | deepEqual(db.user.count(), 4) 49 | 50 | db.user.insert([]) 51 | deepEqual(db.user.count(), 4) 52 | 53 | db.user.insert([{ id: 5, name: "Bonucci" }, { id: 6, name: "Barzagli" }]) 54 | deepEqual(db.user.count(), 6) 55 | 56 | db.user.insert([{ id: 7, name: "PlayerA" }, { id: 7, name: "PlayerB" }]) 57 | deepEqual(db.user.count(), 6) 58 | 59 | db.user.insert([{ id: 3, name: "PlayerA" }, { id: 7, name: "PlayerB" }]) 60 | deepEqual(db.user.count(), 6) 61 | }) 62 | 63 | it("findOne", () => { 64 | deepEqual(db.user.findOne({ id: 21, name: "Pirlo" }), null) 65 | deepEqual(db.user.findOne({ id: 2 }), { id: 2, name: "Pirlo" }) 66 | }) 67 | 68 | it("getDB", () => { 69 | deepEqual(db.user.getDB(), db) 70 | }) 71 | 72 | it("getFullName", () => { 73 | deepEqual(db.user.getFullName(), "test.user") 74 | }) 75 | 76 | it("getIndexKeys", () => { 77 | deepEqual(db.user.getIndexKeys(), [{ "id": 1 }]) 78 | db.createCollection("visitor") 79 | deepEqual(db.visitor.getIndexKeys(), [{ "_id": 1 }]) 80 | }) 81 | 82 | it("getName", () => { 83 | deepEqual(db.user.getName(), "user") 84 | }) 85 | 86 | it("remove", () => { 87 | db.user.remove({ name: "Pirlo" }) 88 | deepEqual(db.user.count(), 2) 89 | db.user.remove() 90 | deepEqual(db.user.count(), 0) 91 | }) 92 | 93 | it("renameCollection", () => { 94 | db.createCollection("visitor") 95 | deepEqual(db.user.renameCollection("visitor")["ok"], 0) 96 | deepEqual(db.user.renameCollection("account")["ok"], 1) 97 | deepEqual(db.account.count(), 3) 98 | deepEqual(db.account.getName(), "account") 99 | deepEqual(db.user, undefined) 100 | deepEqual(db.getCollection("user").count(), 0) 101 | }) 102 | 103 | it("save", () => { 104 | db.user.save({ id: 4, name: "Dybala" }) 105 | deepEqual(db.user.count(), 4) 106 | db.user.save({ id: 4, name: "Bonucci" }) 107 | deepEqual(db.user.count(), 4) 108 | deepEqual(db.user.findOne({ id: 4 }).name, "Bonucci") 109 | db.user.save({ name: "Barzagli" }) 110 | deepEqual(db.user.count(), 5) 111 | deepEqual(db.user.findOne({ name: "Barzagli" }).id.length, 24) 112 | }) 113 | 114 | it("stats", () => { 115 | deepEqual(db.user.stats(), { ns: "test.user", count: 3, ok: 1 }) 116 | }) 117 | 118 | it("update", () => { 119 | db.user.update({ id: 4 }, { name: "Dybala" }) 120 | deepEqual(db.user.find({}).map(u => u.id).sort(), [1, 2, 3]) 121 | deepEqual(db.user.find({}).map(u => u.name).sort(), ["Buffon", "Pirlo", "Pogba"]) 122 | 123 | db.user.update({ id: 2, name: "Pirlo" }, { name: "Dybala", age: 21 }) 124 | deepEqual(db.user.findOne({ id: 2 }), { id: 2, name: "Dybala", age: 21 }) 125 | deepEqual(db.user.find({}).map(u => u.id).sort(), [1, 2, 3]) 126 | deepEqual(db.user.find({}).map(u => u.name).sort(), ["Buffon", "Dybala", "Pogba"]) 127 | 128 | db.user.update({ id: 5 }, { name: "Bonucci" }, { upsert: true }) 129 | deepEqual(db.user.findOne({ name: "Bonucci" }).id.length, 24) 130 | deepEqual(db.user.find({}).map(u => u.name).sort(), ["Bonucci", "Buffon", "Dybala", "Pogba"]) 131 | 132 | let id = db.user.findOne({ name: "Bonucci" }).id 133 | db.user.update({ name: "Bonucci" }, { id: 5, name: "Barzagli" }, { upsert: true }) 134 | deepEqual(db.user.find({}).map(u => u.id).sort(), [1, 2, 3, id].sort()) 135 | deepEqual(db.user.find({}).map(u => u.name).sort(), ["Barzagli", "Buffon", "Dybala", "Pogba"]) 136 | 137 | db.user.update({ id: null }, { id: 6, name: "Dybala" }, { upsert: true }) 138 | deepEqual(db.user.findOne({ id: 6 }), { id: 6, name: "Dybala" }) 139 | deepEqual(db.user.count(), 5) 140 | deepEqual(db.user.find({}).map(u => u.id).sort(), [1, 2, 3, id, 6].sort()) 141 | deepEqual(db.user.find({}).map(u => u.name).sort(), ["Barzagli", "Buffon", "Dybala", "Dybala", "Pogba"]) 142 | 143 | db.user.update({ name: "Dybala" }, { name: "Pirlo" }) 144 | deepEqual(db.user.find({}).map(u => u.name).sort(), ["Barzagli", "Buffon", "Dybala", "Pirlo", "Pogba"]) 145 | 146 | db.user.update({}, { name: "Pirlo" }, { multi: true }) 147 | deepEqual(db.user.find({}).map(u => u.name).sort(), ["Pirlo", "Pirlo", "Pirlo", "Pirlo", "Pirlo"]) 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /test/test_db.js: -------------------------------------------------------------------------------- 1 | const { deepEqual, notEqual } = require("assert") 2 | const reduxdb = require("../dist/index") 3 | 4 | describe("db", () => { 5 | let db 6 | 7 | beforeEach(() => { 8 | reduxdb.drop("test") 9 | db = reduxdb.use("test") 10 | }) 11 | 12 | it("createCollection", () => { 13 | deepEqual(db.createCollection("user"), { ok: 1 }) 14 | deepEqual(db.createCollection("user")["ok"], 0) 15 | }) 16 | 17 | it("getCollection", () => { 18 | db.createCollection("user") 19 | deepEqual(db.getCollection("user"), db.user) 20 | }) 21 | 22 | it("getCollectionNames", () => { 23 | db.createCollection("user") 24 | db.createCollection("team") 25 | db.createCollection("issue") 26 | deepEqual(db.getCollectionNames().sort(), ["issue", "team", "user"]) 27 | }) 28 | 29 | it("getName", () => { 30 | deepEqual(db.getName(), "test") 31 | }) 32 | 33 | it("stats", () => { 34 | db.createCollection("user") 35 | db.createCollection("team") 36 | db.user.insert({}) 37 | db.user.insert({}) 38 | db.user.insert({}) 39 | db.team.insert({}) 40 | db.team.insert({}) 41 | deepEqual(db.stats(), { 42 | db: "test", 43 | collections: 2, 44 | objects: 5, 45 | ok: 1 46 | }) 47 | }) 48 | 49 | it("subscribe", () => { 50 | let flag = false 51 | db.createCollection("issue") 52 | db.subscribe(() => flag = true) 53 | deepEqual(flag, false) 54 | db.issue.remove({}) 55 | deepEqual(flag, true) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/test_reduxdb.js: -------------------------------------------------------------------------------- 1 | const { deepEqual, notEqual } = require("assert") 2 | const reduxdb = require("../dist/index") 3 | 4 | describe("reduxdb", () => { 5 | beforeEach(() => { 6 | reduxdb.drop("test") 7 | }) 8 | 9 | it("use", () => { 10 | let db = reduxdb.use("test") 11 | deepEqual(reduxdb.use("test"), db) 12 | notEqual(reduxdb.use("test"), undefined) 13 | notEqual(reduxdb.use("test"), null) 14 | notEqual(reduxdb.use("test"), reduxdb.use("other")) 15 | }) 16 | 17 | it("drop", () => { 18 | let db = reduxdb.use("test") 19 | reduxdb.drop("test") 20 | notEqual(reduxdb.use("test"), db) 21 | }) 22 | 23 | it("newObjectId", () => { 24 | deepEqual(reduxdb.newObjectId().length, 24) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "lib": [ 5 | "dom", 6 | "es2017" 7 | ], 8 | "noUnusedLocals": true, 9 | "outDir": "dist", 10 | "removeComments": true, 11 | "skipLibCheck": true, 12 | "strictNullChecks": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types", 16 | "src/js/@types" 17 | ] 18 | }, 19 | "include": [ 20 | "src/**/*.ts", 21 | "src/**/*.tsx" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------