├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── ci-test.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json ├── reset.js ├── src ├── collection.js ├── configs │ └── index.js ├── database.js ├── finder.js ├── main.js └── utils │ ├── fixPath.js │ ├── index.js │ ├── logger.js │ ├── normalize.js │ ├── readFile.js │ └── writeFile.js ├── test └── db │ └── movies.json └── tests ├── db └── movies.json ├── movies.json ├── specs ├── collection.js ├── finder.js ├── main.js └── utils.js └── start.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | node_modules/* 3 | dist 4 | **/vendor/*.js 5 | .nyc_output/* 6 | LICENSE 7 | package.json 8 | README.md 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-goes", 3 | "rules": { 4 | "require-jsdoc": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/ci-test.yml: -------------------------------------------------------------------------------- 1 | # GitHub actions 2 | # https://docs.github.com/en/free-pro-team@latest/actions 3 | 4 | name: ci-test 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | test: 10 | 11 | runs-on: ubuntu-20.04 12 | 13 | strategy: 14 | matrix: 15 | node_version: [10.14.2, 14.x, 15.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: setup Node.js v${{ matrix.node_version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node_version }} 24 | 25 | - name: run npm scripts 26 | run: | 27 | npm install 28 | npm run lint 29 | npm run build --if-present 30 | npm run citest 31 | 32 | - name: sync to coveralls 33 | uses: coverallsapp/github-action@v1.1.2 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: cache node modules 38 | uses: actions/cache@v2 39 | with: 40 | path: ~/.npm 41 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 42 | restore-keys: | 43 | ${{ runner.os }}-node- -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | *.debug 5 | 6 | # Runtime data 7 | *.pid 8 | *.seed 9 | 10 | node_modules 11 | coverage 12 | .nyc_output 13 | 14 | storage 15 | 16 | yarn.lock 17 | coverage.lcov 18 | package-lock.json 19 | pnpm-lock.yaml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dong Nguyen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flat-db 2 | Flat-file based data storage 3 | 4 | [![NPM](https://badge.fury.io/js/flat-db.svg)](https://badge.fury.io/js/flat-db) 5 | ![CI test](https://github.com/ndaidong/flat-db/workflows/ci-test/badge.svg) 6 | [![Coverage Status](https://coveralls.io/repos/github/ndaidong/flat-db/badge.svg)](https://coveralls.io/github/ndaidong/flat-db) 7 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ndaidong_flat-db&metric=alert_status)](https://sonarcloud.io/dashboard?id=ndaidong_flat-db) 8 | 9 | 10 | # Setup 11 | 12 | ``` 13 | npm install flat-db --save 14 | ``` 15 | 16 | # APIs 17 | 18 | - FlatDB.configure(Object options) 19 | - FlatDB.Collection(String name[, Object schema]) - Constructor - return Collection class instance 20 | - .add(Object entry | Array entries) 21 | - .get(String key) 22 | - .update(String key, Object updates) 23 | - .remove(String key) 24 | - .all() 25 | - .count() 26 | - .reset() 27 | - .find() - return Finder class instance 28 | - .equals(String property, String | Number value) 29 | - .notEqual(String property, String | Number value) 30 | - .gt(String property, Number value) 31 | - .gte(String property, Number value) 32 | - .lt(String property, Number value) 33 | - .lte(String property, Number value) 34 | - .matches(String property, RegExp value) 35 | - .skip(Number value) 36 | - .limit(Number value) 37 | - .run() 38 | 39 | 40 | Example: 41 | 42 | ```js 43 | 44 | const FlatDB = require('flat-db'); 45 | 46 | // configure path to storage dir 47 | FlatDB.configure({ 48 | dir: './storage', 49 | }); 50 | // since now, everything will be saved under ./storage 51 | 52 | // create Movie collection with schema 53 | const Movie = new FlatDB.Collection('movies', { 54 | title: '', 55 | imdb: 0, 56 | }); 57 | 58 | // The schema is optional. 59 | // But once it was defined, any new item come later 60 | // will be compared with this schema's structure and data type 61 | 62 | // insert a set of movies into collection 63 | const keys = Movie.add([ 64 | { 65 | title: 'The Godfather', 66 | imdb: 9.2, 67 | }, 68 | { 69 | title: 'Independence Day: Resurgence', 70 | imdb: 7.1, 71 | }, 72 | { 73 | title: 'Free State of Jones', 74 | imdb: 6.4, 75 | }, 76 | { 77 | title: 'Star Trek Beyond', 78 | imdb: 5.7, 79 | }, 80 | ]); 81 | console.log('\nkeys returned after adding multi items:'); 82 | console.log(keys); 83 | 84 | // add a single movie 85 | const key = Movie.add({ 86 | title: 'X-Men', 87 | imdb: 8.3, 88 | year: 2011, 89 | }); 90 | // the property "year" will be ignored 91 | // because it does not match with schema 92 | console.log('\nkey returned after adding single item:'); 93 | console.log(key); 94 | 95 | // get item with given key 96 | const movie = Movie.get(key); 97 | console.log(`\nget item by key ${key}:`); 98 | console.log(movie); 99 | 100 | // update it 101 | const updating = Movie.update(key, { 102 | title: 123456, 103 | imdb: 8.2, 104 | }); 105 | // the property "title" will be ignored 106 | // because it does not match with expected type 107 | console.log('\nupdating result:'); 108 | console.log(updating); 109 | 110 | // remove it 111 | const removing = Movie.remove(key); 112 | console.log('\nremoving result:'); 113 | console.log(removing); 114 | 115 | // count collection size 116 | const count = Movie.count(); 117 | console.log('\ncollection size:'); 118 | console.log(count); 119 | 120 | // get all item 121 | const all = Movie.all(); 122 | console.log('\nall items:'); 123 | console.log(all); 124 | 125 | // find items with imdb < 7.1 126 | const results = Movie.find().lt('imdb', 7.1).run(); 127 | console.log('\nitems with imdb < 7.1:'); 128 | console.log(results); 129 | 130 | // get 2 items since 2nd item (skip first item), which have "re" in the title 131 | const results = Movie.find().matches('title', /re/i).skip(1).limit(2).run(); 132 | console.log('\n2 items since 2nd item (skip first one), \n which have "re" in the title:'); 133 | console.log(results); 134 | 135 | 136 | // find items with imdb > 6 and title contains "God" 137 | const results = Movie 138 | .find() 139 | .gt('imdb', 6) 140 | .matches('title', /God/) 141 | .run(); 142 | console.log('\nitems with imdb > 6 and title contains "God":'); 143 | console.log(results); 144 | 145 | // remove all 146 | Movie.reset(); 147 | 148 | // count collection size after removing all 149 | const count = Movie.count(); 150 | console.log('\ncollection size after removing all:'); 151 | console.log(count); 152 | ``` 153 | 154 | # Test 155 | 156 | ``` 157 | git clone https://github.com/ndaidong/flat-db.git 158 | cd flat-db 159 | npm install 160 | npm test 161 | ``` 162 | 163 | # License 164 | 165 | The MIT License (MIT) 166 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Starting app 3 | * @ndaidong 4 | **/ 5 | 6 | const main = require('./src/main'); 7 | main.version = require('./package').version; 8 | module.exports = main; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.0.0", 3 | "name": "flat-db", 4 | "description": "Flat-file based data storage", 5 | "homepage": "https://www.npmjs.com/package/flat-db", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/ndaidong/flat-db" 9 | }, 10 | "author": "@ndaidong", 11 | "main": "./index.js", 12 | "engines": { 13 | "node": ">= 10.14.2" 14 | }, 15 | "scripts": { 16 | "lint": "eslint .", 17 | "pretest": "npm run lint", 18 | "test": "tap tests/start.js --coverage --reporter=spec --coverage-report=html --no-browser", 19 | "citest": "tap tests/start.js --coverage --reporter=spec --coverage-report=lcov --no-browser", 20 | "reset": "node reset" 21 | }, 22 | "dependencies": { 23 | "bellajs": "^9.2.2", 24 | "debug": "^4.2.0", 25 | "mkdirp": "^1.0.4" 26 | }, 27 | "devDependencies": { 28 | "eslint-config-goes": "^1.1.8", 29 | "tap": "^14.10.8" 30 | }, 31 | "keywords": [ 32 | "storage", 33 | "database", 34 | "nosql", 35 | "util" 36 | ], 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /reset.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { 4 | existsSync, 5 | unlinkSync, 6 | } = require('fs'); 7 | 8 | const {execSync} = require('child_process'); 9 | 10 | const dirs = [ 11 | 'dist', 12 | 'docs', 13 | '.nyc_output', 14 | 'coverage', 15 | 'node_modules', 16 | '.nuxt', 17 | ]; 18 | 19 | const files = [ 20 | 'yarn.lock', 21 | 'pnpm-lock.yaml', 22 | 'package-lock.json', 23 | 'coverage.lcov', 24 | ]; 25 | 26 | dirs.forEach((d) => { 27 | execSync(`rm -rf ${d}`); 28 | }); 29 | 30 | files.forEach((f) => { 31 | if (existsSync(f)) { 32 | unlinkSync(f); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/collection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB: Collection 3 | * @ndaidong 4 | **/ 5 | 6 | const { 7 | time, 8 | genid, 9 | hasProperty, 10 | isObject, 11 | isArray, 12 | isEmpty, 13 | isString, 14 | } = require('bellajs'); 15 | 16 | const { 17 | fixPath, 18 | readFile, 19 | delFile, 20 | writeFile, 21 | normalize, 22 | logger, 23 | } = require('./utils'); 24 | 25 | const {info} = logger; 26 | 27 | const config = require('./configs'); 28 | 29 | const Finder = require('./finder'); 30 | 31 | 32 | const C = new Map(); 33 | 34 | class Collection { 35 | constructor(name, schema = {}, forceReload = false) { 36 | const n = normalize(name); 37 | if (!n) { 38 | throw new Error(`Invalid collection name "${name}"`); 39 | } 40 | 41 | if (!forceReload) { 42 | const c = C.get(n); 43 | if (c) { 44 | return c; 45 | } 46 | } 47 | 48 | this.name = n; 49 | this.schema = schema; 50 | 51 | this.lastModified = time(); 52 | this.entries = []; 53 | 54 | const { 55 | dir, 56 | ext, 57 | } = config; 58 | 59 | const file = fixPath(`${dir}/${n}${ext}`); 60 | 61 | this.file = file; 62 | 63 | this.status = 0; 64 | 65 | const data = readFile(file); 66 | if (data) { 67 | const { 68 | schema: cschema, 69 | lastModified, 70 | entries, 71 | } = data; 72 | 73 | this.schema = isEmpty(schema) ? cschema : schema; 74 | this.lastModified = lastModified; 75 | this.entries = entries; 76 | } 77 | 78 | C.set(n, this); 79 | 80 | return this; 81 | } 82 | 83 | onchange() { 84 | const lastModified = time(); 85 | this.lastModified = lastModified; 86 | writeFile(this.file, { 87 | name: this.name, 88 | lastModified, 89 | schema: this.schema, 90 | entries: this.all(), 91 | }); 92 | } 93 | 94 | all() { 95 | return [...this.entries]; 96 | } 97 | 98 | count() { 99 | return this.entries.length; 100 | } 101 | 102 | add(item) { 103 | if (!isObject(item) && !isArray(item)) { 104 | throw new Error('Invalid parameter. Object required.'); 105 | } 106 | 107 | const entries = this.all(); 108 | 109 | const schema = this.schema; 110 | const noSchema = isEmpty(schema); 111 | 112 | const addOne = (entry) => { 113 | const id = genid(32); 114 | entry._id_ = id; 115 | entry._ts_ = time(); 116 | 117 | if (!noSchema) { 118 | const _item = Object.assign({ 119 | _id_: '', 120 | _ts_: 0, 121 | }, schema); 122 | for (const key in _item) { 123 | if (hasProperty(entry, key) && typeof entry[key] === typeof _item[key]) { 124 | _item[key] = entry[key]; 125 | } 126 | } 127 | entry = _item; 128 | } 129 | return entry; 130 | }; 131 | 132 | if (!isArray(item)) { 133 | const newEntry = addOne(item); 134 | this.entries.push(newEntry); 135 | this.onchange(); 136 | return newEntry._id_; 137 | } 138 | 139 | const newEntries = item.map(addOne); 140 | this.entries = entries.concat(newEntries); 141 | this.onchange(); 142 | return newEntries.map((a) => { 143 | return a._id_; 144 | }); 145 | } 146 | 147 | get(id) { 148 | if (!isString(id)) { 149 | throw new Error('Invalid parameter. String required.'); 150 | } 151 | 152 | const entries = this.all(); 153 | 154 | const candidates = entries.filter((item) => { 155 | return item._id_ === id; 156 | }); 157 | 158 | return candidates.length > 0 ? candidates[0] : null; 159 | } 160 | 161 | update(id, data) { 162 | if (!isString(id)) { 163 | throw new Error('Invalid parameter. String required.'); 164 | } 165 | 166 | const entries = this.all(); 167 | let changed = false; 168 | 169 | const k = entries.findIndex((el) => { 170 | return el._id_ === id; 171 | }); 172 | 173 | if (k >= 0) { 174 | const obj = entries[k]; 175 | 176 | for (const key in obj) { 177 | if (key !== '_id_' && 178 | hasProperty(data, key) && 179 | typeof data[key] === typeof obj[key] && 180 | data[key] !== obj[key]) { 181 | obj[key] = data[key]; 182 | changed = true; 183 | } 184 | } 185 | 186 | if (!changed) { 187 | info('Nothing to update'); 188 | return false; 189 | } 190 | 191 | obj._ts_ = time(); 192 | entries[k] = obj; 193 | this.onchange(); 194 | info(`Updated an item: ${id}`); 195 | return obj; 196 | } 197 | 198 | return false; 199 | } 200 | 201 | remove(id) { 202 | if (!isString(id)) { 203 | throw new Error('Invalid parameter. String required.'); 204 | } 205 | 206 | const entries = this.all(); 207 | const k = entries.findIndex((el) => { 208 | return el._id_ === id; 209 | }); 210 | 211 | if (k >= 0) { 212 | entries.splice(k, 1); 213 | this.entries = entries; 214 | this.onchange(); 215 | info(`Removed an item: ${id}`); 216 | return true; 217 | } 218 | 219 | return false; 220 | } 221 | 222 | find() { 223 | return new Finder(this.all()); 224 | } 225 | 226 | reset() { 227 | this.lastModified = time(); 228 | this.entries = []; 229 | delFile(this.file); 230 | } 231 | } 232 | 233 | module.exports = Collection; 234 | -------------------------------------------------------------------------------- /src/configs/index.js: -------------------------------------------------------------------------------- 1 | // config 2 | 3 | const {error} = require('../utils/logger'); 4 | 5 | const env = process.env || {}; // eslint-disable-line no-process-env 6 | 7 | [ 8 | 'NODE_ENV', 9 | 'FLATDB_DIR', 10 | ].forEach((name) => { 11 | if (!env[name]) { 12 | error(`Environment variable ${name} is missing, use default instead.`); 13 | } 14 | }); 15 | 16 | const config = { 17 | ENV: env.NODE_ENV || 'development', 18 | dir: env.FLATDB_DIR || './flatdb/', 19 | ext: '.fdb', 20 | }; 21 | module.exports = config; 22 | -------------------------------------------------------------------------------- /src/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB: Database 3 | * @ndaidong 4 | **/ 5 | 6 | const {readdirSync} = require('fs'); 7 | const {basename} = require('path'); 8 | 9 | const {info} = require('./utils/logger'); 10 | 11 | const config = require('./configs'); 12 | 13 | const Collection = require('./collection'); 14 | 15 | const loadPersistentData = () => { 16 | const { 17 | dir, 18 | ext, 19 | } = config; 20 | 21 | const dirs = readdirSync(dir, 'utf8'); 22 | if (dirs && dirs.length) { 23 | dirs.forEach((file) => { 24 | if (file.endsWith(ext)) { 25 | const fname = basename(file, ext); 26 | const c = new Collection(fname); 27 | if (c) { 28 | info(`Loaded persistent data for collection "${c.name}"`); 29 | } 30 | } 31 | }); 32 | } 33 | }; 34 | 35 | const getCollection = (n) => { 36 | return new Collection(n, false, true); 37 | }; 38 | 39 | module.exports = { 40 | loadPersistentData, 41 | Collection, 42 | getCollection, 43 | }; 44 | -------------------------------------------------------------------------------- /src/finder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB: Finder 3 | * @ndaidong 4 | **/ 5 | 6 | const { 7 | hasProperty, 8 | isString, 9 | isNumber, 10 | isInteger, 11 | } = require('bellajs'); 12 | 13 | class Finder { 14 | constructor(entries = []) { 15 | this.entries = entries; 16 | this._skip = 0; 17 | this._limit = entries.length; 18 | } 19 | 20 | equals(key, val) { 21 | const entries = this.entries; 22 | this.entries = entries.filter((item) => { 23 | if (hasProperty(item, key)) { 24 | return item[key] === val; 25 | } 26 | return false; 27 | }); 28 | return this; 29 | } 30 | 31 | notEqual(key, val) { 32 | const entries = this.entries; 33 | this.entries = entries.filter((item) => { 34 | if (hasProperty(item, key)) { 35 | return item[key] !== val; 36 | } 37 | return true; 38 | }); 39 | return this; 40 | } 41 | 42 | gt(key, val) { 43 | const entries = this.entries; 44 | this.entries = entries.filter((item) => { 45 | if (hasProperty(item, key)) { 46 | const a = item[key]; 47 | if (isNumber(a)) { 48 | return a > val; 49 | } 50 | } 51 | return false; 52 | }); 53 | return this; 54 | } 55 | 56 | gte(key, val) { 57 | const entries = this.entries; 58 | this.entries = entries.filter((item) => { 59 | if (hasProperty(item, key)) { 60 | const a = item[key]; 61 | if (isNumber(a)) { 62 | return a >= val; 63 | } 64 | } 65 | return false; 66 | }); 67 | return this; 68 | } 69 | 70 | lt(key, val) { 71 | const entries = this.entries; 72 | this.entries = entries.filter((item) => { 73 | if (hasProperty(item, key)) { 74 | const a = item[key]; 75 | if (isNumber(a)) { 76 | return a < val; 77 | } 78 | } 79 | return false; 80 | }); 81 | return this; 82 | } 83 | 84 | lte(key, val) { 85 | const entries = this.entries; 86 | this.entries = entries.filter((item) => { 87 | if (hasProperty(item, key)) { 88 | const a = item[key]; 89 | if (isNumber(a)) { 90 | return a <= val; 91 | } 92 | } 93 | return false; 94 | }); 95 | return this; 96 | } 97 | 98 | matches(key, reg) { 99 | if (isString(reg)) { 100 | reg = reg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 101 | } 102 | 103 | const entries = this.entries; 104 | this.entries = entries.filter((item) => { 105 | if (hasProperty(item, key)) { 106 | const a = item[key]; 107 | if (isString(a)) { 108 | return a.match(reg) !== null; 109 | } 110 | } 111 | return false; 112 | }); 113 | return this; 114 | } 115 | 116 | skip(k) { 117 | if (isInteger(k)) { 118 | this._skip = k; 119 | } 120 | return this; 121 | } 122 | 123 | limit(k) { 124 | if (isInteger(k)) { 125 | this._limit = k; 126 | } 127 | return this; 128 | } 129 | 130 | run() { 131 | let entries = this.entries; 132 | const skip = this._skip; 133 | const limit = this._limit; 134 | const leng = entries.length; 135 | if (skip > 0 && skip < leng) { 136 | entries = entries.slice(skip, leng); 137 | } 138 | if (limit > 0 && limit < entries.length) { 139 | entries = entries.slice(0, limit); 140 | } 141 | return entries; 142 | } 143 | } 144 | 145 | module.exports = Finder; 146 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB - Flat file based database 3 | * @ndaidong 4 | **/ 5 | 6 | const { 7 | copies, 8 | clone, 9 | } = require('bellajs'); 10 | 11 | 12 | const config = require('./configs'); 13 | 14 | const { 15 | fixPath, 16 | mkdir, 17 | } = require('./utils'); 18 | 19 | const { 20 | loadPersistentData, 21 | Collection, 22 | getCollection, 23 | } = require('./database'); 24 | 25 | const configure = (settings = {}) => { 26 | copies(settings, config, true); 27 | const { 28 | dir, 29 | } = config; 30 | 31 | const f = fixPath(dir); 32 | if (dir !== f) { 33 | config.dir = f; 34 | } 35 | 36 | mkdir(f); 37 | loadPersistentData(); 38 | 39 | return clone(config); 40 | }; 41 | 42 | module.exports = { 43 | configure, 44 | Collection, 45 | getCollection, 46 | }; 47 | -------------------------------------------------------------------------------- /src/utils/fixPath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB - utils -> fixPath 3 | * @ndaidong 4 | **/ 5 | 6 | const { 7 | normalize, 8 | } = require('path'); 9 | 10 | const fixPath = (p = '') => { 11 | return normalize(p); 12 | }; 13 | 14 | module.exports = fixPath; 15 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | // index 2 | 3 | const exec = require('child_process').execSync; 4 | const { 5 | existsSync, 6 | unlinkSync, 7 | } = require('fs'); 8 | const mkdir = require('mkdirp').sync; 9 | 10 | const rmdir = (d) => { 11 | return exec(`rm -rf ${d}`); 12 | }; 13 | 14 | const fixPath = require('./fixPath'); 15 | const readFile = require('./readFile'); 16 | const writeFile = require('./writeFile'); 17 | const delFile = (f) => unlinkSync(f); 18 | const exists = (f) => existsSync(f); 19 | 20 | const normalize = require('./normalize'); 21 | const logger = require('./logger'); 22 | 23 | module.exports = { 24 | fixPath, 25 | readFile, 26 | writeFile, 27 | delFile, 28 | exists, 29 | mkdir, 30 | rmdir, 31 | normalize, 32 | logger, 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | // utils / logger 2 | 3 | const { 4 | name, 5 | } = require('../../package.json'); 6 | 7 | const debug = require('debug'); 8 | 9 | const info = debug(`${name}:info`); 10 | const error = debug(`${name}:error`); 11 | const warning = debug(`${name}:warning`); 12 | 13 | module.exports = { 14 | info, 15 | error, 16 | warning, 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/normalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB - utils -> normalize 3 | * @ndaidong 4 | **/ 5 | 6 | const { 7 | isString, 8 | } = require('bellajs'); 9 | 10 | const isValidCol = (name = '') => { 11 | const re = /^([A-Z_])+([_A-Z0-9])+$/i; 12 | return isString(name) && re.test(name); 13 | }; 14 | 15 | const normalize = (name) => { 16 | if (isValidCol(name)) { 17 | return name.toLowerCase(); 18 | } 19 | return false; 20 | }; 21 | 22 | module.exports = normalize; 23 | -------------------------------------------------------------------------------- /src/utils/readFile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB - utils -> readFile 3 | * @ndaidong 4 | **/ 5 | 6 | const { 7 | existsSync, 8 | readFileSync, 9 | } = require('fs'); 10 | 11 | const {error} = require('./logger'); 12 | 13 | const readFile = (f) => { 14 | if (existsSync(f)) { 15 | const s = readFileSync(f, 'utf8'); 16 | try { 17 | const c = JSON.parse(s); 18 | return c; 19 | } catch (err) { 20 | error(err); 21 | } 22 | } 23 | return null; 24 | }; 25 | 26 | module.exports = readFile; 27 | -------------------------------------------------------------------------------- /src/utils/writeFile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlatDB - utils -> writeFile 3 | * @ndaidong 4 | **/ 5 | 6 | const { 7 | writeFileSync, 8 | } = require('fs'); 9 | 10 | const { 11 | isString, 12 | } = require('bellajs'); 13 | 14 | const writeFile = (f, data = '') => { 15 | const content = isString(data) ? data : JSON.stringify(data); 16 | return writeFileSync(f, content, 'utf8'); 17 | }; 18 | 19 | module.exports = writeFile; 20 | -------------------------------------------------------------------------------- /test/db/movies.json: -------------------------------------------------------------------------------- 1 | {"name":"movies","lastModified":1604851890471,"schema":{"title":"","imdb":0},"entries":[{"_id_":"WNzBCNARkBRP6RDiaZzxVLFT8aMJmpiL","_ts_":1604851740275,"title":"Independence Day: Resurgence","imdb":7.1}]} -------------------------------------------------------------------------------- /tests/db/movies.json: -------------------------------------------------------------------------------- 1 | {"name":"movies","lastModified":1531840113424,"schema":{"title":"","imdb":0},"entries":[{"_id_":"JUBxByiVNZoQWlwOkviL2bYCNAQ9o1t8","_ts_":1495990464634,"title":"Independence Day: Resurgence","imdb":7.1}]} -------------------------------------------------------------------------------- /tests/movies.json: -------------------------------------------------------------------------------- 1 | {"name":"movies","lastModified":1495988937848,"schema":{"title":"","imdb":0},"entries":[{"_id_":"7wBvqMSJ9TrgD5w8DSDvTnuVrlQTUGgA","_ts_":1495988937848,"title":"The Godfather","imdb":9.2}]} -------------------------------------------------------------------------------- /tests/specs/collection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing 3 | * @ndaidong 4 | */ 5 | 6 | const test = require('tap').test; 7 | 8 | const { 9 | isFunction, 10 | isString, 11 | } = require('bellajs'); 12 | 13 | const FlatDB = require('../../src/main'); 14 | const Finder = require('../../src/finder'); 15 | 16 | test('Test FlatDB.Collection class:', (assert) => { 17 | const userCollection = FlatDB.getCollection('users'); 18 | 19 | const methods = [ 20 | 'all', 21 | 'add', 22 | 'get', 23 | 'update', 24 | 'remove', 25 | 'find', 26 | 'reset', 27 | ]; 28 | 29 | const sampleData = [ 30 | { 31 | name: 'Alice', 32 | age: 16, 33 | }, 34 | { 35 | name: 'Bob', 36 | age: 15, 37 | }, 38 | { 39 | name: 'Kelly', 40 | age: 17, 41 | }, 42 | ]; 43 | 44 | assert.ok(userCollection instanceof FlatDB.Collection, 'userCollection must be instance of FlatDB.Collection'); 45 | 46 | methods.forEach((met) => { 47 | assert.ok(isFunction(userCollection[met]), `userCollection must have the method .${met}()`); 48 | }); 49 | 50 | assert.comment('Add sample data with Collection.add()'); 51 | const keys = userCollection.add(sampleData); 52 | assert.equals(keys.length, 3, 'userCollection.add(sampleData) must return 3 keys'); 53 | 54 | assert.comment('Get all collection entries with Collection.all()'); 55 | const users = userCollection.all(); 56 | assert.equals(users.length, 3, 'userCollection.all() must return 3 entries'); 57 | 58 | assert.comment('Get specific entry with Collection.get()'); 59 | 60 | let k = 0; 61 | users.forEach((u) => { 62 | const user = userCollection.get(u._id_); 63 | const ref = sampleData[k]; 64 | assert.equals(user.name, ref.name, `user.name must be "${ref.name}"`); 65 | assert.equals(user.age, ref.age, `user.age must be "${ref.age}"`); 66 | k++; 67 | }); 68 | 69 | assert.comment('Update specific entry with Collection.update()'); 70 | const alice = users[0]; 71 | const {_id_: id} = alice; 72 | userCollection.update(id, {name: 'Ecila'}); 73 | const ecila = userCollection.get(id); 74 | assert.equals(ecila.name, 'Ecila', `ecila.name must be "Ecila"`); 75 | assert.equals(ecila.age, alice.age, `ecila.age must be "${alice.age}"`); 76 | 77 | assert.comment('Test exceptions with bad input'); 78 | 79 | const badGet = userCollection.get('abc'); 80 | assert.equals(badGet, null, `userCollection.get('abc') must return null`); 81 | 82 | const badUpdate = userCollection.update('abc', {name: 'Tom'}); 83 | assert.equals(badUpdate, false, `userCollection.update('unexistKey') must return false`); 84 | 85 | const nochangeUpdate = userCollection.update(id, {name: 'Ecila'}); 86 | assert.equals(nochangeUpdate, false, `userCollection.update(id, {name: 'Ecila'}) must return false`); 87 | 88 | assert.throws(() => { 89 | return new FlatDB.Collection('&*^*(&^(*()'); 90 | }, true, `new FlatDB.Collection('&*^*(&^(*()') must throw error`); 91 | 92 | assert.throws(() => { 93 | return userCollection.get(123); 94 | }, true, `userCollection.get(123) must throw error`); 95 | 96 | assert.throws(() => { 97 | return userCollection.add(123); 98 | }, true, `userCollection.add(123) must throw error`); 99 | 100 | assert.throws(() => { 101 | return userCollection.update(123, {name: 'X'}); 102 | }, true, `userCollection.update(123, {name: 'X'}) must throw error`); 103 | 104 | const badRemove = userCollection.remove('abc'); 105 | assert.equals(badRemove, false, `userCollection.remove('abc') must return false`); 106 | 107 | assert.throws(() => { 108 | return userCollection.remove(); 109 | }, true, `userCollection.remove() must throw error`); 110 | 111 | 112 | assert.comment('Test if collection single item add() return a key'); 113 | const key = userCollection.add({ 114 | name: 'Zic', 115 | age: 19, 116 | }); 117 | assert.ok(isString(key), 'userCollection.add(user) must return a key string'); 118 | assert.equals(key.length, 32, 'key.length must be 32'); 119 | 120 | assert.comment('Test if collection update() return a mutual item'); 121 | const mutual = userCollection.update(key, {name: 'Lina'}); 122 | assert.equals(mutual.name, 'Lina', 'mutual.name must be "Lina"'); 123 | 124 | assert.comment('Test collection find()'); 125 | const finder = userCollection.find(); 126 | assert.ok(finder instanceof Finder, 'userCollection.find() must return an instance of Finder'); 127 | 128 | userCollection.reset(); 129 | 130 | assert.end(); 131 | }); 132 | 133 | test('Test FlatDB.Collection class with persistent data:', (assert) => { 134 | FlatDB.configure({ 135 | dir: './/test///db', 136 | }); 137 | 138 | const Movie = new FlatDB.Collection('movies', { 139 | title: '', 140 | imdb: 0, 141 | }); 142 | 143 | Movie.add({ 144 | title: 'Independence Day: Resurgence', 145 | imdb: 7.1, 146 | }); 147 | 148 | const arr = Movie.all(); 149 | assert.equals(arr.length, 2, `Movie must have 2 entries`); 150 | 151 | const {_id_} = arr[1]; 152 | Movie.remove(_id_); 153 | 154 | assert.equals(Movie.count(), 1, `Movie must have 1 entry`); 155 | 156 | assert.end(); 157 | }); 158 | -------------------------------------------------------------------------------- /tests/specs/finder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing 3 | * @ndaidong 4 | */ 5 | 6 | const path = require('path'); 7 | const test = require('tap').test; 8 | 9 | const { 10 | isFunction, 11 | } = require('bellajs'); 12 | 13 | const rootDir = '../../src/'; 14 | const FlatDB = require(path.join(rootDir, 'main')); 15 | 16 | test('Test Finder class methods:', (assert) => { 17 | FlatDB.configure({ 18 | dir: './storage/', 19 | }); 20 | 21 | const entries = [ 22 | { 23 | title: 'The Godfather', 24 | director: 'Francis Ford Coppola', 25 | writer: 'Mario Puzo', 26 | imdb: 9.2, 27 | year: 1974, 28 | }, 29 | { 30 | title: 'Independence Day: Resurgence', 31 | director: 'Roland Emmerich', 32 | writer: 'Nicolas Wright', 33 | imdb: 7.1, 34 | year: 1981, 35 | }, 36 | { 37 | title: 'Free State of Jones', 38 | director: 'Gary Ross', 39 | writer: 'Leonard Hartman', 40 | imdb: 6.4, 41 | year: 1995, 42 | }, 43 | { 44 | title: 'Star Trek Beyond', 45 | director: 'Justin Lin', 46 | writer: 'Simon Pegg', 47 | imdb: 5.7, 48 | year: 2009, 49 | }, 50 | ]; 51 | 52 | const Movie = new FlatDB.Collection('topmovies'); 53 | Movie.add(entries); 54 | 55 | const MovieFinder = Movie.find(); 56 | 57 | assert.comment('Check the public methods'); 58 | [ 59 | 'matches', 'equals', 'notEqual', 'gt', 'lt', 'skip', 'limit', 'run', 60 | ].forEach((m) => { 61 | assert.ok(isFunction(MovieFinder[m]), `MovieFinder must have method .${m}()`); 62 | }); 63 | 64 | assert.comment('Check Finder.skip()'); 65 | const skips = Movie.find().skip(2).run(); 66 | assert.equals(skips.length, 2, 'It must find out 2 items with skip(2)'); 67 | 68 | assert.comment('Check Finder.limit()'); 69 | const limits = Movie.find().limit(1).run(); 70 | assert.equals(limits.length, 1, 'It must find out 1 item with limit(1)'); 71 | 72 | assert.comment('Check Finder.matches() - without "i" flag'); 73 | 74 | let matches = Movie.find().matches('title', /re/).run(); 75 | assert.equals(matches.length, 2, 'It must find out 2 items with "re" in the title'); 76 | matches = Movie.find().matches('phone', /75687/).run(); 77 | assert.equals(matches.length, 0, 'It must find out 0 items with "75687" in the phone'); 78 | 79 | assert.comment('Check Finder.equals()'); 80 | let equals = Movie.find().equals('year', 2009).run(); 81 | assert.equals(equals.length, 1, 'It must find out 1 movie produced in 2009'); 82 | equals = Movie.find().equals('cost', 1000000).run(); 83 | assert.equals(equals.length, 0, 'It must find out no movie with cost = 1000000'); 84 | 85 | assert.comment('Check Finder.notEqual()'); 86 | let notEquals = Movie.find().notEqual('year', 2009).run(); 87 | assert.equals(notEquals.length, 3, 'It must find out 3 movies that were not produced in 2009'); 88 | notEquals = Movie.find().notEqual('cost', 1000000).run(); 89 | assert.equals(notEquals.length, 4, 'It must find out 4 movies with cost != 1000000'); 90 | 91 | 92 | assert.comment('Check Finder.gt()'); 93 | let gts = Movie.find().gt('imdb', 7.1).run(); 94 | assert.equals(gts.length, 1, 'It must find out 1 item with imdb > 7.1'); 95 | gts = Movie.find().gt('cost', 1000000).run(); 96 | assert.equals(gts.length, 0, 'It must find out no movie with non-exist property'); 97 | 98 | assert.comment('Check Finder.gte()'); 99 | let gtes = Movie.find().gte('imdb', 7.1).run(); 100 | assert.equals(gtes.length, 2, 'It must find out 2 items with imdb >= 7.1'); 101 | gtes = Movie.find().gte('cost', 1000000).run(); 102 | assert.equals(gtes.length, 0, 'It must find out no movie with non-exist property'); 103 | 104 | assert.comment('Check Finder.lt()'); 105 | let lts = Movie.find().lt('imdb', 7.1).run(); 106 | assert.equals(lts.length, 2, 'It must find out 2 items with imdb < 7.1'); 107 | lts = Movie.find().lt('cost', 1000000).run(); 108 | assert.equals(lts.length, 0, 'It must find out no movie with non-exist property'); 109 | 110 | assert.comment('Check Finder.lte()'); 111 | let ltes = Movie.find().lte('imdb', 7.1).run(); 112 | assert.equals(ltes.length, 3, 'It must find out 3 items with imdb <= 7.1'); 113 | ltes = Movie.find().lte('cost', 1000000).run(); 114 | assert.equals(ltes.length, 0, 'It must find out no movie with non-exist property'); 115 | 116 | Movie.reset(); 117 | assert.end(); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/specs/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing 3 | * @ndaidong 4 | */ 5 | 6 | const test = require('tap').test; 7 | 8 | const { 9 | hasProperty, 10 | isObject, 11 | } = require('bellajs'); 12 | 13 | const FlatDB = require('../../src/main'); 14 | 15 | test('Test FlatDB.configure() method:', (assert) => { 16 | const structure = [ 17 | 'ENV', 18 | 'dir', 19 | 'ext', 20 | ]; 21 | 22 | const sampleConf = { 23 | dir: 'storage', 24 | ext: '.json', 25 | }; 26 | 27 | const config = FlatDB.configure(sampleConf); 28 | 29 | assert.ok(isObject(config), 'config must be an object'); 30 | 31 | structure.forEach((key) => { 32 | assert.ok(hasProperty(config, key), `config must have the property "${key}"`); 33 | }); 34 | 35 | assert.equals(config.dir, sampleConf.dir, `config.dir must be "${sampleConf.dir}"`); 36 | assert.equals(config.ext, sampleConf.ext, `config.ext must be "${sampleConf.ext}"`); 37 | 38 | assert.end(); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/specs/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing 3 | * @ndaidong 4 | */ 5 | 6 | const test = require('tap').test; 7 | 8 | const { 9 | isFunction, 10 | genid, 11 | } = require('bellajs'); 12 | 13 | const utils = require('../../src/utils'); 14 | 15 | const { 16 | fixPath, 17 | readFile, 18 | writeFile, 19 | delFile, 20 | exists, 21 | mkdir, 22 | rmdir, 23 | normalize, 24 | } = utils; 25 | 26 | test('Test utils:', (assert) => { 27 | const methods = [ 28 | 'fixPath', 29 | 'readFile', 30 | 'writeFile', 31 | 'delFile', 32 | 'exists', 33 | 'mkdir', 34 | 'rmdir', 35 | 'normalize', 36 | ]; 37 | 38 | methods.forEach((met) => { 39 | assert.ok(isFunction(utils[met]), `${met} must be function`); 40 | }); 41 | 42 | const sampleDir = `./sampleDir_${genid()}`; 43 | 44 | mkdir(sampleDir); 45 | assert.ok(exists(sampleDir), `${sampleDir} must be created`); 46 | 47 | const sampleTextFile = fixPath(`${sampleDir}/tmp.txt`); 48 | const sampleFileContent = 'Hello world'; 49 | writeFile(sampleTextFile, sampleFileContent); 50 | assert.ok(exists(sampleTextFile), `${sampleTextFile} must be created`); 51 | 52 | const nonJSON = readFile(sampleTextFile); 53 | assert.equals(nonJSON, null, `Read data from ${sampleTextFile} must return null`); 54 | 55 | delFile(sampleTextFile); 56 | assert.equals(exists(sampleTextFile), false, `${sampleTextFile} must be removed`); 57 | 58 | const sampleJSONFile = fixPath(`${sampleDir}/tmp.json`); 59 | const sampleFileData = {message: 'Hello world'}; 60 | writeFile(sampleJSONFile, sampleFileData); 61 | assert.ok(exists(sampleJSONFile), `${sampleJSONFile} must be created`); 62 | 63 | const json = readFile(sampleJSONFile); 64 | const sampleJSON = JSON.stringify(sampleFileData); 65 | assert.equals(JSON.stringify(json), sampleJSON, `json must be ${sampleJSON}`); 66 | 67 | rmdir(sampleDir); 68 | assert.ok(!exists(sampleDir), `${sampleDir} must be removed`); 69 | 70 | assert.equals(normalize('abc'), 'abc', `normalize('abc') must be "abc"`); 71 | assert.equals(normalize('abc&^%'), false, `normalize('abc&^%') must be false`); 72 | 73 | assert.end(); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/start.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /** 4 | * Import specs 5 | */ 6 | 7 | const dir = '../tests/specs/'; 8 | [ 9 | 'utils', 10 | 'main', 11 | 'collection', 12 | 'finder', 13 | ].forEach((script) => { 14 | require(path.join(dir, script)); 15 | }); 16 | --------------------------------------------------------------------------------