├── .gitignore ├── .travis.yml ├── LICENSE ├── gulpfile.js ├── package.json ├── readme.md ├── src ├── csv-editor │ ├── index.ts │ └── types.ts ├── database │ ├── crud │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── read.ts │ │ └── update.ts │ ├── index.ts │ └── init.ts ├── index.ts └── utils.ts ├── test └── index.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | /node_modules 3 | bin -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "7" 5 | - "8" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ysnglt 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 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const ts = require("gulp-typescript"); 3 | const jeditor = require("gulp-json-editor"); 4 | const del = require("del"); 5 | 6 | const project = ts.createProject("tsconfig.json"); 7 | const OUT_DIR = project.options.outDir; 8 | 9 | // clean build dir 10 | gulp.task("clean", () => del(["bin"])); 11 | 12 | // transpile project to JS using tsconfig.json 13 | gulp.task("compile", ["clean"], () => { 14 | project 15 | .src() 16 | .pipe(project()) 17 | .pipe(gulp.dest(OUT_DIR)); 18 | }); 19 | 20 | // remove source utils from package and copies it to dist folder 21 | gulp.task("edit-package", ["clean"], () => { 22 | gulp 23 | .src("./package.json") 24 | .pipe( 25 | jeditor(json => { 26 | json.main = "index.js"; 27 | delete json.scripts; 28 | delete json.devDependencies; 29 | return json; 30 | }) 31 | ) 32 | .pipe(gulp.dest(OUT_DIR)); 33 | }); 34 | 35 | gulp.task("copy-readme", ["clean"], () => { 36 | gulp.src("./readme.md").pipe(gulp.dest(OUT_DIR)); 37 | }); 38 | 39 | gulp.task("default", ["clean", "compile", "edit-package", "copy-readme"]); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-database", 3 | "version": "0.9.2", 4 | "description": "lightweight CSV database", 5 | "main": "src/index.ts", 6 | "keywords": [ 7 | "csv", 8 | "database", 9 | "db", 10 | "crud", 11 | "storage", 12 | "flat file", 13 | "file" 14 | ], 15 | "repository": { 16 | "url": "https://github.com/ysnglt/node-csvdb" 17 | }, 18 | "dependencies": { 19 | "fast-csv": "^2.4.1", 20 | "proper-lockfile": "^3.0.2", 21 | "tempy": "^0.2.1" 22 | }, 23 | "devDependencies": { 24 | "@types/mocha": "^2.2.44", 25 | "@types/mock-fs": "^3.6.30", 26 | "@types/node": "^8.0.53", 27 | "@types/tempy": "^0.1.0", 28 | "del": "^3.0.0", 29 | "gulp": "^3.9.1", 30 | "gulp-json-editor": "^2.2.1", 31 | "gulp-typescript": "^3.2.3", 32 | "mocha": "^4.0.1", 33 | "mock-fs": "^4.4.2", 34 | "prettier": "^1.9.2", 35 | "ts-node": "^4.0.2", 36 | "tslint": "^5.8.0", 37 | "tslint-config-prettier": "^1.6.0", 38 | "typescript": "^2.6.2" 39 | }, 40 | "scripts": { 41 | "prepare": "gulp", 42 | "test": "mocha -r ts-node/register test/index.ts", 43 | "lint": "tslint --project .", 44 | "format": "prettier --write {src,test}/**/*.ts" 45 | }, 46 | "author": "ysnglt", 47 | "license": "MIT" 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # csv-database : node.js CSV database 2 | 3 | Lightweight CRUD database, using CSV files as a storage. 4 | 5 | Features : 6 | 7 | * complete CRUD API with model validation 8 | * native JS objects manipulation 9 | * Typescript typings 10 | * concurrency-ready 11 | 12 | ## Usage 13 | 14 | ```js 15 | > const db = await csvdb("users.csv", ["id","name","mail"]); 16 | 17 | > await db.get({mail: "johndoe@github.com"}); 18 | [ {id: 1, name: "johndoe", mail: "john@github.com"} ] 19 | 20 | > await db.add({id: 2, name: "stevejobs", mail: "jobs@github.com"}); 21 | ``` 22 | 23 | ## Installation 24 | 25 | `$ npm install csv-database` 26 | 27 | ## API Reference 28 | 29 | * [csvdb](#module_csvdb) 30 | * [.get](#module_csvdb.get) 31 | * [.edit](#module_csvdb.edit) 32 | * [.add](#module_csvdb.add) 33 | * [.delete](#module_csvdb.delete) 34 | 35 | 36 | 37 | ### csvdb(filename, model [, delimiter]) ⇒ `Promise` 38 | 39 | Returns a database, given a CSV file and its model. 40 | 41 | * `;` as default delimiter 42 | * file is created if it doesn't exist 43 | 44 | | Param | Type | Description | 45 | | -------------------- | ---------- | ---------------- | 46 | | filename | `string` | csv file | 47 | | model | `string[]` | database model | 48 | | [optional] delimiter | `string` | custom delimiter | 49 | 50 | **Example** 51 | 52 | ```js 53 | const db = await csvdb("users.csv", ["id","name","mail"], ","); 54 | ``` 55 | 56 | 57 | 58 | ### db.get([predicate]) ⇒ `Promise` 59 | 60 | Returns database content. If given a predicate, returns data that matches its key-values pairs. 61 | 62 | * empty array if nothing found 63 | * throws an error if predicate does not match csv model 64 | 65 | | Param | Type | Description | 66 | | -------------------- | ---------------- | ---------------- | 67 | | [optional] predicate | `Object.partial` | search predicate | 68 | 69 | **Example** 70 | 71 | ```js 72 | > await db.get(); 73 | [ 74 | {id: 1, name: "johndoe", mail: "john@github.com"}, 75 | {id: 2, name: "frankmass", mail: "frankmass@github.com"} 76 | ] 77 | 78 | > await db.get({name: "johndoe"}); 79 | [ {id: 1, name: "johndoe", mail: "john@github.com"} ] 80 | 81 | > await db.get({name: "stevejobs"}); 82 | [ ] 83 | ``` 84 | 85 | 86 | 87 | ### db.edit(predicate, data) ⇒ `Promise` 88 | 89 | Edits data, given a search predicate object. 90 | 91 | * returns a list of edited occurences 92 | 93 | | Param | Type | Description | 94 | | --------- | ---------------- | -------------------------------------------------- | 95 | | predicate | `Object.partial` | search predicate | 96 | | data | `Object.partial` | data that will replace found occurences key/values | 97 | 98 | **Example** 99 | 100 | ```js 101 | > await db.edit({name: "johndoe"}, {mail: "john@gitlab.com"}); 102 | [{1, "johndoe", "john@gitlab.com"}] 103 | ``` 104 | 105 | 106 | 107 | ### db.add(data) ⇒ `Promise` 108 | 109 | Adds data (single object or array) to CSV. 110 | 111 | * returns created occurences 112 | 113 | | Param | Type | Description | 114 | | ----- | ------------------ | ---------------- | 115 | | data | `Object, Object[]` | data to be added | 116 | 117 | **Example** 118 | 119 | ```js 120 | > await db.add({id: 3, name: "stevejobs", mail: "jobs@github.com"}); 121 | [{id: 3, name: "stevejobs", mail: "jobs@github.com"}] 122 | ``` 123 | 124 | 125 | 126 | ### db.delete(predicate) ⇒ `Promise` 127 | 128 | Deletes all data that matches the given predicate. 129 | 130 | * returns deleted occurences 131 | 132 | | Param | Type | Description | 133 | | --------- | ---------------- | ---------------- | 134 | | predicate | `Object.partial` | search predicare | 135 | 136 | **Example** 137 | 138 | ```js 139 | > await db.delete({id: 1}); 140 | [ {id: 1, name: "johndoe", mail: "john@github.com"} ] 141 | ``` 142 | 143 | ## Local installation 144 | 145 | Clone the project : 146 | 147 | `$ git clone https://github.com/ysnglt/node-csvdb` 148 | 149 | This project is made using Typescript. To generate the transpiled npm package, you need to run gulp : 150 | 151 | `$ gulp` 152 | 153 | You can run the full test suite with mocha : 154 | 155 | `$ npm i && npm run test` 156 | -------------------------------------------------------------------------------- /src/csv-editor/index.ts: -------------------------------------------------------------------------------- 1 | import fastcsv = require("fast-csv"); 2 | import fs = require("fs"); 3 | import lockfile = require("proper-lockfile"); 4 | import tempy = require("tempy"); 5 | 6 | import { ICSVEditor, IEditEvents, IReadEvents } from "./types"; 7 | 8 | // Map SIGINT & SIGTERM to process exit 9 | // so that lockfile removes the lockfile automatically 10 | process 11 | .once("SIGINT", () => process.exit(1)) 12 | .once("SIGTERM", () => process.exit(1)); 13 | 14 | // lock middleware to ensure async/thread safety 15 | const lock = async (file: string, next) => { 16 | const release = await lockfile.lock(file, { 17 | retries: { 18 | retries: 500, 19 | factor: 3, 20 | minTimeout: 1 * 10, 21 | maxTimeout: 60 * 1000, 22 | randomize: true 23 | } 24 | }); 25 | 26 | await next(); 27 | release(); 28 | }; 29 | 30 | /** file i/o helpers */ 31 | const getCsvStream = (file: string, delimiter: string) => { 32 | // append flag : we only work with empty files or adding data 33 | const fileStream = fs.createWriteStream(file, { flags: "a" }); 34 | const csvStream = fastcsv.createWriteStream({ 35 | headers: true, 36 | delimiter 37 | }); 38 | 39 | csvStream.pipe(fileStream); 40 | return csvStream; 41 | }; 42 | 43 | const copyCsv = (from: string, to: string) => 44 | new Promise((resolve, reject) => { 45 | const fromStream = fs.createReadStream(from); 46 | fromStream.on("error", reject); 47 | 48 | const toStream = fs.createWriteStream(to); 49 | toStream.on("error", reject).on("close", resolve); 50 | 51 | fromStream.pipe(toStream); 52 | }); 53 | /** */ 54 | 55 | const read = async (filename: string, delimiter: string, events: IReadEvents) => 56 | new Promise((resolve, reject) => { 57 | fastcsv 58 | .fromPath(filename, { delimiter, headers: true }) 59 | .on("data", events.onData) 60 | .on("error", reject) 61 | .on("end", resolve); 62 | }); 63 | 64 | const edit = (filename: string, delimiter: string, events: IEditEvents) => 65 | new Promise((resolve, reject) => { 66 | const copy = tempy.file(); 67 | const tempStream = getCsvStream(copy, delimiter); 68 | 69 | fastcsv 70 | .fromPath(filename, { delimiter, headers: true }) 71 | .on("data", data => { 72 | const newData = events.onEdit(data); 73 | // handling deletion case when editing returns nothing 74 | if (newData) { 75 | tempStream.write(newData); 76 | } 77 | }) 78 | .on("error", reject) 79 | .on("end", () => { 80 | tempStream.end(); 81 | }); 82 | 83 | tempStream.on("end", () => { 84 | // copy data from tempfile to original file 85 | copyCsv(copy, filename) 86 | .then(resolve) 87 | .catch(reject); 88 | }); 89 | }); 90 | 91 | const add = (filename: string, delimiter: string, data: Object[]) => 92 | new Promise((resolve, reject) => { 93 | const copy = tempy.file(); 94 | const tempStream = getCsvStream(copy, delimiter); 95 | 96 | fastcsv 97 | .fromPath(filename, { delimiter, headers: true }) 98 | .on("error", reject) 99 | .on("end", () => { 100 | // appending data at end of file 101 | for (const row of data) { 102 | tempStream.write(row); 103 | } 104 | tempStream.end(); 105 | }) 106 | .pipe(tempStream); 107 | 108 | tempStream.on("end", () => { 109 | // copy data from tempfile to original file 110 | copyCsv(copy, filename) 111 | .then(resolve) 112 | .catch(reject); 113 | }); 114 | }); 115 | 116 | const lockedEdit = async ( 117 | filename: string, 118 | delimiter: string, 119 | events: IEditEvents 120 | ) => { 121 | const func = async () => edit(filename, delimiter, events); 122 | 123 | return lock(filename, func); 124 | }; 125 | 126 | const lockedAdd = async ( 127 | filename: string, 128 | delimiter: string, 129 | data: Object[] 130 | ) => { 131 | const func = async () => add(filename, delimiter, data); 132 | 133 | return lock(filename, func); 134 | }; 135 | 136 | const csvEditor = (filename, delimiter): ICSVEditor => ({ 137 | read: events => read(filename, delimiter, events), 138 | add: data => lockedAdd(filename, delimiter, data), 139 | edit: events => lockedEdit(filename, delimiter, events) 140 | }); 141 | 142 | export = csvEditor; 143 | -------------------------------------------------------------------------------- /src/csv-editor/types.ts: -------------------------------------------------------------------------------- 1 | // CSV editor typings, moved in a separate file to be used throughout the project 2 | // disappears at npm package transpilation to JS 3 | 4 | export interface IReadEvents { 5 | onData: (data: Object) => Object | void; 6 | } 7 | 8 | export interface IEditEvents { 9 | onEdit: (data: Object) => Object | void; 10 | } 11 | 12 | export interface ICSVEditor { 13 | read: (e: IReadEvents) => any; 14 | add: (data) => any; 15 | edit: (e: IEditEvents) => any; 16 | } 17 | -------------------------------------------------------------------------------- /src/database/crud/create.ts: -------------------------------------------------------------------------------- 1 | import read = require("./read"); 2 | 3 | import { ICSVEditor, IReadEvents } from "../../csv-editor/types"; 4 | 5 | const create = async ( 6 | parser: ICSVEditor, 7 | data: Object[] | Object 8 | ): Promise => { 9 | const arrayData = data instanceof Array ? data : [data]; 10 | if (arrayData.length < 1) return arrayData; 11 | 12 | await parser.add(arrayData); 13 | return arrayData; 14 | }; 15 | 16 | export = create; 17 | -------------------------------------------------------------------------------- /src/database/crud/delete.ts: -------------------------------------------------------------------------------- 1 | import utils = require("../../utils"); 2 | import read = require("./read"); 3 | 4 | import { ICSVEditor, IEditEvents } from "../../csv-editor/types"; 5 | 6 | const erase = async ( 7 | parser: ICSVEditor, 8 | predicate: Object 9 | ): Promise => { 10 | const deletedData = []; 11 | 12 | const deleteData = data => { 13 | if (utils.isSubsetOf(predicate, data)) { 14 | deletedData.push(data); 15 | } else { 16 | return data; 17 | } 18 | }; 19 | 20 | const events: IEditEvents = { 21 | onEdit: deleteData 22 | }; 23 | 24 | await parser.edit(events); 25 | return deletedData; 26 | }; 27 | 28 | export = erase; 29 | -------------------------------------------------------------------------------- /src/database/crud/read.ts: -------------------------------------------------------------------------------- 1 | import fs = require("fs"); 2 | import util = require("util"); 3 | 4 | import utils = require("../../utils"); 5 | 6 | import { ICSVEditor, IReadEvents } from "../../csv-editor/types"; 7 | 8 | const addFilteredData = (array: Object[], subset: Object, data: Object) => 9 | utils.isSubsetOf(subset, data) ? array.push(data) : array; 10 | 11 | const addData = (array: Object[], data: Object) => array.push(data); 12 | 13 | const get = async ( 14 | parser: ICSVEditor, 15 | predicate?: Object 16 | ): Promise => { 17 | const foundData = []; 18 | 19 | // changes behavior if a predicate is given 20 | const filterData = predicate 21 | ? data => addFilteredData(foundData, predicate, data) 22 | : data => addData(foundData, data); 23 | 24 | const events: IReadEvents = { 25 | onData: filterData 26 | }; 27 | 28 | await parser.read(events); 29 | return foundData; 30 | }; 31 | 32 | export = get; 33 | -------------------------------------------------------------------------------- /src/database/crud/update.ts: -------------------------------------------------------------------------------- 1 | import fs = require("fs"); 2 | import util = require("util"); 3 | 4 | import utils = require("../../utils"); 5 | import create = require("./create"); 6 | 7 | import { ICSVEditor, IEditEvents, IReadEvents } from "../../csv-editor/types"; 8 | 9 | const editObject = (object, subset) => { 10 | const editedObject = { ...object }; 11 | Object.keys(subset).forEach(key => { 12 | editedObject[key] = subset[key]; 13 | }); 14 | return editedObject; 15 | }; 16 | 17 | const update = async ( 18 | parser: ICSVEditor, 19 | predicate: Object, 20 | updateValue: Object 21 | ): Promise => { 22 | const editedData = []; 23 | 24 | const editData = data => { 25 | if (utils.isSubsetOf(predicate, data)) { 26 | const updated = editObject(data, updateValue); 27 | 28 | editedData.push(updated); 29 | return updated; 30 | } 31 | return data; 32 | }; 33 | 34 | const events: IEditEvents = { 35 | onEdit: editData 36 | }; 37 | 38 | await parser.edit(events); 39 | return editedData; 40 | }; 41 | 42 | export = update; 43 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | import csv = require("../csv-editor"); 2 | import create = require("./crud/create"); 3 | import erase = require("./crud/delete"); 4 | import get = require("./crud/read"); 5 | import update = require("./crud/update"); 6 | import init = require("./init"); 7 | 8 | import { ICSVEditor } from "../csv-editor/types"; 9 | 10 | const DEFAULT_DELIM = ";"; 11 | 12 | const database = (editor: ICSVEditor, delimiter?: string) => { 13 | return { 14 | get: (filter?: Object) => get(editor, filter), 15 | add: (data: Object[] | Object) => create(editor, data), 16 | edit: (filter: Object, data: Object) => update(editor, filter, data), 17 | delete: (predicate: Object) => erase(editor, predicate) 18 | }; 19 | }; 20 | 21 | const csvdb = async (filename: string, model: string[], delim?: string) => { 22 | const delimiter = delim ? delim : DEFAULT_DELIM; 23 | const editor = csv(filename, delimiter); 24 | 25 | await init(filename, model, delimiter, editor); 26 | return database(editor, delimiter); 27 | }; 28 | 29 | export = csvdb; 30 | -------------------------------------------------------------------------------- /src/database/init.ts: -------------------------------------------------------------------------------- 1 | import fs = require("fs"); 2 | import util = require("util"); 3 | import csv = require("../csv-editor"); 4 | 5 | import { ICSVEditor, IReadEvents } from "../csv-editor/types"; 6 | 7 | const doesFileExist = (filename: string) => 8 | new Promise((resolve, reject) => { 9 | fs.exists(filename, resolve); 10 | }); 11 | 12 | const createFile = (filename: string, data: any) => 13 | new Promise((resolve, reject) => { 14 | fs.writeFile(filename, data, err => { 15 | err ? reject(err) : resolve(); 16 | }); 17 | }); 18 | 19 | const createFileIfNotExist = async ( 20 | filename: string, 21 | model: string[], 22 | delimiter: string 23 | ) => { 24 | try { 25 | if (!await doesFileExist(filename)) { 26 | const headers = model.reduce((header, cell) => header + delimiter + cell); 27 | await createFile(filename, headers); 28 | } 29 | } catch (err) { 30 | throw new Error("error creating csv file : " + err); 31 | } 32 | }; 33 | 34 | const hasSameModel = (obj: Object, model: string[]) => { 35 | const objKeys = Object.keys(obj); 36 | const objHasKey = key => objKeys.find(k => k === key); 37 | 38 | for (const key of model) { 39 | if (!objHasKey(key)) return false; 40 | } 41 | 42 | // if length is different, arrays are not equal 43 | return objKeys.length === model.length; 44 | }; 45 | 46 | const validate = async (parser: ICSVEditor, model: string[]) => { 47 | let validated = false; 48 | // stop at first line once model has been validated 49 | const checkModel = data => { 50 | if (!validated && !hasSameModel(data, model)) 51 | throw new Error("csv model doesn't correspond"); 52 | validated = true; 53 | }; 54 | 55 | const events: IReadEvents = { 56 | onData: checkModel 57 | }; 58 | 59 | return parser.read(events); 60 | }; 61 | 62 | const initDb = async ( 63 | filename: string, 64 | model: string[], 65 | delimiter: string, 66 | parser: ICSVEditor 67 | ) => { 68 | await createFileIfNotExist(filename, model, delimiter); 69 | await validate(parser, model); 70 | }; 71 | 72 | export = initDb; 73 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import csvdb = require("./database"); 2 | 3 | export = csvdb; 4 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const isSubsetOf = (subset: Object, obj: Object) => { 2 | for (const key of Object.keys(subset)) { 3 | if (subset[key] !== obj[key]) { 4 | return false; 5 | } 6 | } 7 | return true; 8 | }; 9 | 10 | export = { 11 | isSubsetOf 12 | }; 13 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import assert = require("assert"); 2 | import fs = require("fs"); 3 | import mockfs = require("mock-fs"); 4 | 5 | import csvdb = require("../src"); 6 | 7 | const EXISTINGFILECONTENT = "foo;bar\na;b\nc;d"; 8 | const CUSTOMDELIMFILECONTENT = "foo$bar\na$b\nc$d"; 9 | const INVALIDFILECONTENT = "foo;bar\n1;2;3"; 10 | 11 | mockfs({ 12 | fixtures: { 13 | "db.csv": EXISTINGFILECONTENT, 14 | "custom.csv": CUSTOMDELIMFILECONTENT, 15 | "invalid.csv": INVALIDFILECONTENT 16 | } 17 | }); 18 | 19 | const NEWFILE = "fixtures/new.csv"; 20 | const EXISTINGFILE = "fixtures/db.csv"; 21 | const CUSTOMDELIMFILE = "fixtures/custom.csv"; 22 | const INVALIDFILE = "fixtures/invalid.csv"; 23 | const MODEL = ["foo", "bar"]; 24 | 25 | after(() => mockfs.restore()); 26 | 27 | describe("database init", () => { 28 | it("should create a new csv if it doesn't exist", async () => { 29 | const db = await csvdb(NEWFILE, MODEL); 30 | const contents = fs.readFileSync(NEWFILE).toString(); 31 | assert.deepEqual(contents, "foo;bar"); 32 | }); 33 | 34 | it("should read a valid CSV without throwing error", async () => { 35 | try { 36 | await csvdb(EXISTINGFILE, MODEL); 37 | } catch (err) { 38 | throw new Error("csv is valid but not read"); 39 | } 40 | }); 41 | 42 | it("should error when reading an invalid CSV", async () => { 43 | try { 44 | await csvdb(INVALIDFILE, MODEL); 45 | } catch (err) { 46 | return; 47 | } 48 | throw new Error("file is invalid but has been validated"); 49 | }); 50 | 51 | it("should check CSV model - different keys", async () => { 52 | try { 53 | await csvdb(EXISTINGFILE, ["not", "csv", "keys"]); 54 | } catch (err) { 55 | return; 56 | } 57 | throw new Error("file has wrong model but has been validated"); 58 | }); 59 | 60 | it("should check CSV model - extra key", async () => { 61 | try { 62 | await csvdb(EXISTINGFILE, MODEL.concat("plusonekey")); 63 | } catch (err) { 64 | return; 65 | } 66 | throw new Error("file has wrong model but has been validated"); 67 | }); 68 | 69 | it("should check CSV model - model subset", async () => { 70 | try { 71 | await csvdb(EXISTINGFILE, MODEL.slice(0, 1)); 72 | } catch (err) { 73 | return; 74 | } 75 | throw new Error("file has wrong model but has been validated"); 76 | }); 77 | }); 78 | 79 | describe("database behavior - read", () => { 80 | it("should find all data", async () => { 81 | const db = await csvdb(EXISTINGFILE, MODEL); 82 | const values = await db.get(); 83 | 84 | assert.deepEqual(values, [{ foo: "a", bar: "b" }, { foo: "c", bar: "d" }]); 85 | }); 86 | 87 | it("should find all data - custom delimiter", async () => { 88 | const db = await csvdb(CUSTOMDELIMFILE, MODEL, "$"); 89 | const values = await db.get(); 90 | 91 | assert.deepEqual(values, [{ foo: "a", bar: "b" }, { foo: "c", bar: "d" }]); 92 | }); 93 | 94 | it("should find filtered data", async () => { 95 | const db = await csvdb(EXISTINGFILE, MODEL); 96 | const values = await db.get({ foo: "a" }); 97 | 98 | assert.deepEqual(values, [{ foo: "a", bar: "b" }]); 99 | }); 100 | 101 | it("should find nothing", async () => { 102 | const db = await csvdb(EXISTINGFILE, MODEL); 103 | const values = await db.get({ foox: "a" }); 104 | 105 | assert.deepEqual(values, []); 106 | }); 107 | }); 108 | 109 | describe("database behavior - create | update | delete", () => { 110 | beforeEach(() => { 111 | mockfs({ fixtures: { "db.csv": EXISTINGFILECONTENT } }); 112 | }); 113 | 114 | it("should add data", async () => { 115 | const db = await csvdb(EXISTINGFILE, MODEL); 116 | await db.add([{ foo: "x", bar: "x" }, { foo: "y", bar: "y" }]); 117 | const values = await db.get(); 118 | 119 | assert.deepEqual(values, [ 120 | { foo: "a", bar: "b" }, 121 | { foo: "c", bar: "d" }, 122 | { foo: "x", bar: "x" }, 123 | { foo: "y", bar: "y" } 124 | ]); 125 | }); 126 | 127 | it("should add a single occurence", async () => { 128 | const db = await csvdb(EXISTINGFILE, MODEL); 129 | await db.add({ foo: "x", bar: "x" }); 130 | const values = await db.get(); 131 | 132 | assert.deepEqual(values, [ 133 | { foo: "a", bar: "b" }, 134 | { foo: "c", bar: "d" }, 135 | { foo: "x", bar: "x" } 136 | ]); 137 | }); 138 | 139 | it("should edit filtered data", async () => { 140 | const db = await csvdb(EXISTINGFILE, MODEL); 141 | const editedValues = await db.edit({ foo: "a" }, { foo: "x", bar: "y" }); 142 | const newValue = await db.get({ foo: "x" }); 143 | const oldValue = await db.get({ foo: "a" }); 144 | 145 | assert.deepEqual(editedValues, [{ foo: "x", bar: "y" }]); 146 | assert.deepEqual(oldValue, []); 147 | assert.deepEqual(newValue, [{ foo: "x", bar: "y" }]); 148 | }); 149 | 150 | it("should return nothing when editing non existent data", async () => { 151 | const db = await csvdb(EXISTINGFILE, MODEL); 152 | db.add([]); 153 | const editedValue = await db.edit({ foo: "x" }, { foo: "x" }); 154 | const newValue = await db.get({ foo: "x" }); 155 | const oldValue = await db.get({ foo: "a" }); 156 | 157 | assert.deepEqual(editedValue, []); 158 | assert.deepEqual(oldValue, [{ foo: "a", bar: "b" }]); 159 | assert.deepEqual(newValue, []); 160 | }); 161 | 162 | it("should delete filtered data", async () => { 163 | const db = await csvdb(EXISTINGFILE, MODEL); 164 | const deletedValue = await db.delete({ foo: "a" }); 165 | const values = await db.get(); 166 | 167 | assert.deepEqual(values, [{ foo: "c", bar: "d" }]); 168 | assert.deepEqual(deletedValue, [{ foo: "a", bar: "b" }]); 169 | }); 170 | 171 | it("should delete nothing", async () => { 172 | const db = await csvdb(EXISTINGFILE, MODEL); 173 | const deletedValue = await db.delete({ foo: "x" }); 174 | const values = await db.get(); 175 | 176 | assert.deepEqual(values, [{ foo: "a", bar: "b" }, { foo: "c", bar: "d" }]); 177 | assert.deepEqual(deletedValue, []); 178 | }); 179 | }); 180 | 181 | describe("database behavior - concurrency", () => { 182 | beforeEach(() => { 183 | mockfs({ fixtures: { "db.csv": EXISTINGFILECONTENT } }); 184 | }); 185 | 186 | it("should edit data concurrently", async () => { 187 | const db = await csvdb(EXISTINGFILE, MODEL); 188 | const firstEdit = db.edit({ foo: "a" }, { foo: "x", bar: "y" }); 189 | const lastEdit = db.edit({ foo: "c" }, { foo: "i", bar: "j" }); 190 | await Promise.all([firstEdit, lastEdit]); 191 | 192 | const values = await db.get(); 193 | 194 | assert.deepEqual(values, [{ foo: "x", bar: "y" }, { foo: "i", bar: "j" }]); 195 | }); 196 | 197 | it("should add data concurrently", async () => { 198 | const db = await csvdb(EXISTINGFILE, MODEL); 199 | const firstEdit = db.add([{ foo: "x", bar: "x" }]); 200 | const lastEdit = db.add([{ foo: "y", bar: "y" }]); 201 | await Promise.all([firstEdit, lastEdit]); 202 | const values = await db.get(); 203 | 204 | assert.deepEqual(values, [ 205 | { foo: "a", bar: "b" }, 206 | { foo: "c", bar: "d" }, 207 | { foo: "x", bar: "x" }, 208 | { foo: "y", bar: "y" } 209 | ]); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "bin", 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "noImplicitUseStrict": true 8 | }, 9 | "exclude": ["test"] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "linterOptions": { "exclude": ["bin"] }, 4 | "rules": { 5 | "object-literal-sort-keys": false, 6 | "no-implicit-dependencies": [true, "dev"], 7 | "ban-types": false, 8 | "curly": false 9 | } 10 | } 11 | --------------------------------------------------------------------------------