├── .gitignore ├── operator ├── operators.ts ├── base.ts ├── types.ts ├── primitive.ts ├── utils.ts └── collection.ts ├── data ├── README.md ├── blog.ts └── pokemon.ts ├── .vscode └── settings.json ├── .editorconfig ├── writeWorker.ts ├── tests ├── CasualDB.test.ts ├── PrimitiveOperator.test.ts └── CollectionOperator.test.ts ├── LICENSE ├── mod.ts ├── .all-contributorsrc ├── connector.ts ├── .github └── contributing.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .casualdb 3 | -------------------------------------------------------------------------------- /operator/operators.ts: -------------------------------------------------------------------------------- 1 | export * from "./primitive.ts"; 2 | export * from "./collection.ts"; 3 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # data 2 | 3 | Holds all the test data for testing `casualdb` during development. 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": false, 3 | "deno.enable": true, 4 | "deno.lint": true, 5 | "deno.unstable": true 6 | } -------------------------------------------------------------------------------- /operator/base.ts: -------------------------------------------------------------------------------- 1 | export class BaseOperator { 2 | protected data: Op; 3 | 4 | constructor(data: Op) { 5 | this.data = data; 6 | } 7 | 8 | value() { 9 | return this.data; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Default styles 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /operator/types.ts: -------------------------------------------------------------------------------- 1 | export interface PredicateFunction { 2 | (value: T): boolean; 3 | } 4 | 5 | export type Predicate = Partial | PredicateFunction; 6 | 7 | export type ID = string | number; 8 | 9 | export type SortKeys = (keyof M)[]; 10 | 11 | export interface SortMethod { 12 | (a: M, b: M): number; 13 | } 14 | 15 | export type SortArg = SortKeys | SortMethod; 16 | -------------------------------------------------------------------------------- /writeWorker.ts: -------------------------------------------------------------------------------- 1 | import { writeJson } from "https://deno.land/std@0.68.0/fs/write_json.ts"; 2 | 3 | self.onmessage = async (e) => { 4 | const { file, data, taskId } = e.data; 5 | console.debug(`[casualdb:worker:debug] write task: ${taskId}`); 6 | 7 | try { 8 | await writeJson(file, data); 9 | self.postMessage({ taskId, error: false }); 10 | } catch (error) { 11 | self.postMessage({ taskId, error: error.valueOf() }); 12 | } finally { 13 | self.close(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /operator/primitive.ts: -------------------------------------------------------------------------------- 1 | import { BaseOperator } from "./base.ts"; 2 | import pick from "https://deno.land/x/lodash@4.17.15-es/pick.js"; 3 | 4 | export class PrimitiveOperator extends BaseOperator { 5 | constructor(data: Op) { 6 | super(data); 7 | 8 | if (Array.isArray(data)) { 9 | throw new Error( 10 | "[casualdb] PrimitiveOperator; initialized with a value that is an array.", 11 | ); 12 | } 13 | } 14 | 15 | update( 16 | updateMethod: (currentValue: Op) => T, 17 | ): PrimitiveOperator> { 18 | return new PrimitiveOperator>( 19 | updateMethod(this.data), 20 | ); 21 | } 22 | 23 | pick(paths: Array) { 24 | const picked = pick(this.data, ...paths) as { [P in U]: Op[P] }; 25 | return new PrimitiveOperator<{ [P in U]: Op[P] }>(picked); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/CasualDB.test.ts: -------------------------------------------------------------------------------- 1 | import { CasualDB } from "../mod.ts"; 2 | import { assertEquals } from "https://deno.land/std@0.84.0/testing/asserts.ts"; 3 | import { emptyDir } from 'https://deno.land/std@0.88.0/fs/mod.ts'; 4 | 5 | import { blog, Blog } from "../data/blog.ts"; 6 | 7 | const DIR_TEST_DB = './.casualdb'; 8 | 9 | Deno.test("casualdb: creates a db and can perform operations on it", async () => { 10 | const db = new CasualDB(); 11 | const timestamp = Date.now(); 12 | 13 | await emptyDir(DIR_TEST_DB); 14 | await db.connect(`${DIR_TEST_DB}/${timestamp}-casual-test.json`); 15 | await db.seed(blog); 16 | 17 | const posts = await db.get("posts"); 18 | const postTitlesByViews = posts.sort(["views"]).pick(["title"]); 19 | 20 | assertEquals( 21 | postTitlesByViews.value(), 22 | [ 23 | { title: "Color Me Addams" }, 24 | { title: "Hand Delivered" }, 25 | { title: "Little Doll Lost" }, 26 | ], 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /data/blog.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | name: string; 4 | } 5 | 6 | export interface Article { 7 | id: number; 8 | title: string; 9 | views: number; 10 | authorId: number; 11 | } 12 | 13 | export interface Blog { 14 | posts: Article[]; 15 | authors: User[]; 16 | } 17 | 18 | export const blog: Blog = { 19 | posts: [ 20 | { 21 | id: 1, 22 | title: "Color Me Addams", 23 | views: 62, 24 | authorId: 666, 25 | }, 26 | { 27 | id: 2, 28 | title: "Hand Delivered", 29 | views: 505, 30 | authorId: 5, 31 | }, 32 | { 33 | id: 3, 34 | title: "Little Doll Lost", 35 | views: 646, 36 | authorId: 3, 37 | }, 38 | ], 39 | authors: [ 40 | { 41 | id: 666, 42 | name: "Morticia Addams", 43 | }, 44 | { 45 | id: 5, 46 | name: "Thing", 47 | }, 48 | { 49 | id: 3, 50 | name: "Wednesday Addams", 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Camp Vanilla 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 | -------------------------------------------------------------------------------- /tests/PrimitiveOperator.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.84.0/testing/asserts.ts"; 2 | 3 | import { PrimitiveOperator } from "../operator/operators.ts"; 4 | import { Pokemon, pokemon } from "../data/pokemon.ts"; 5 | 6 | Deno.test("PrimitiveOperator: instantiates with value", () => { 7 | const data = { id: "1234", name: "foo" }; 8 | const op = new PrimitiveOperator(data); 9 | 10 | assertEquals(op.value(), data); 11 | }); 12 | 13 | Deno.test("PrimitiveOperator: updates a value", () => { 14 | const data = { 15 | name: "John Doe", 16 | }; 17 | 18 | const op = new PrimitiveOperator(data); 19 | 20 | assertEquals( 21 | op 22 | .update((value) => ({ 23 | name: "Jane Doe", 24 | })) 25 | .value(), 26 | { 27 | name: "Jane Doe", 28 | }, 29 | ); 30 | }); 31 | 32 | Deno.test("PrimitiveOperator: picks object keys", () => { 33 | const rawPokemon = pokemon[0]; 34 | 35 | const op = new PrimitiveOperator(rawPokemon); 36 | const charredPokemon = op.pick(["name", "weight"]); 37 | 38 | assertEquals( 39 | charredPokemon.value(), 40 | { name: rawPokemon.name, weight: rawPokemon.weight }, 41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import get from "https://deno.land/x/lodash@4.17.15-es/get.js"; 2 | import set from "https://deno.land/x/lodash@4.17.15-es/set.js"; 3 | 4 | import { Connector, ConnectOptions } from "./connector.ts"; 5 | import { createNewOperator } from "./operator/utils.ts"; 6 | 7 | class CasualDB { 8 | private _connector: Connector; 9 | 10 | constructor() { 11 | this._connector = new Connector(); 12 | } 13 | 14 | connect(fsPath: string, options?: ConnectOptions): Promise { 15 | return this._connector.connect(fsPath, options); 16 | } 17 | 18 | async get(path: string) { 19 | const data = await this._connector.read(); 20 | const value: T = get(data, path, null); 21 | 22 | return createNewOperator(value); 23 | } 24 | 25 | seed(data: Schema): Promise { 26 | return this._connector.write(data); 27 | } 28 | 29 | async write(path: string, value: T) { 30 | const data = await this._connector.read(); 31 | const updatedData = set(data, path, value); 32 | return this._connector.write(updatedData as Schema); 33 | } 34 | } 35 | 36 | export default CasualDB; 37 | export { CasualDB }; 38 | export { PrimitiveOperator, CollectionOperator } from "./operator/operators.ts"; 39 | -------------------------------------------------------------------------------- /operator/utils.ts: -------------------------------------------------------------------------------- 1 | import { SortKeys } from "./types.ts"; 2 | import { CollectionOperator } from "./collection.ts"; 3 | import { PrimitiveOperator } from "./primitive.ts"; 4 | 5 | export const compareByType = (a: any, b: any) => { 6 | if (typeof a !== typeof b) { 7 | throw new Error("[casualdb] Cannot compare dissimilar types when sorting."); 8 | } 9 | 10 | if (typeof a === "number") { 11 | return a - b; 12 | } 13 | 14 | if (typeof a === "string") { 15 | if (a === b) { 16 | return 0; 17 | } else if (a < b) { 18 | return -1; 19 | } 20 | return 1; 21 | } 22 | 23 | throw new Error(`Cannot compare types '${typeof a}' and '${typeof b}'`); 24 | }; 25 | 26 | export const compareFunction = (keys: SortKeys) => { 27 | return (a: M, b: M) => { 28 | for (let key of keys) { 29 | const compareResult = compareByType(a[key], b[key]); 30 | if (compareResult !== 0) { 31 | return compareResult; 32 | } 33 | } 34 | return 0; 35 | }; 36 | }; 37 | 38 | export const createNewOperator = ( 39 | data: T, 40 | ): T extends (infer U)[] ? CollectionOperator : PrimitiveOperator => { 41 | if (Array.isArray(data)) { 42 | return new CollectionOperator(data) as any; 43 | } 44 | return new PrimitiveOperator(data) as any; 45 | }; 46 | -------------------------------------------------------------------------------- /data/pokemon.ts: -------------------------------------------------------------------------------- 1 | export interface Pokemon { 2 | id: number; 3 | name: string; 4 | weight: number; 5 | height: number; 6 | types: string[]; 7 | } 8 | 9 | export const pokemon: Pokemon[] = [ 10 | { 11 | "name": "charizard", 12 | "id": 6, 13 | "weight": 905, 14 | "height": 17, 15 | "types": ["fire", "flying"], 16 | }, 17 | { 18 | "name": "typhlosion", 19 | "id": 157, 20 | "weight": 795, 21 | "height": 17, 22 | "types": ["fire"], 23 | }, 24 | { 25 | "name": "emboar", 26 | "id": 500, 27 | "weight": 1500, 28 | "height": 16, 29 | "types": ["fire", "fighting"], 30 | }, 31 | { 32 | "name": "garchomp", 33 | "id": 445, 34 | "weight": 950, 35 | "height": 19, 36 | "types": ["ground", "dragon"], 37 | }, 38 | { 39 | "name": "diglett", 40 | "id": 50, 41 | "weight": 8, 42 | "height": 2, 43 | "types": ["ground"], 44 | }, 45 | { 46 | "name": "jirachi", 47 | "id": 385, 48 | "weight": 11, 49 | "height": 3, 50 | "types": ["steel", "psychic"], 51 | }, 52 | { 53 | "name": "flabebe", 54 | "id": 669, 55 | "weight": 1, 56 | "height": 1, 57 | "types": ["fairy"], 58 | }, 59 | { 60 | "name": "wailord", 61 | "id": 321, 62 | "weight": 3980, 63 | "height": 145, 64 | "types": ["water"], 65 | }, 66 | ]; 67 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "abinavseelan", 10 | "name": "Abinav Seelan", 11 | "avatar_url": "https://avatars2.githubusercontent.com/u/6417910?v=4", 12 | "profile": "https://abinavseelan.com/?utm_source=github&utm_medium=documentation-allcontributors&utm_content=casualdb", 13 | "contributions": [ 14 | "code", 15 | "doc", 16 | "ideas", 17 | "test" 18 | ] 19 | }, 20 | { 21 | "login": "rheaditi", 22 | "name": "Aditi Mohanty", 23 | "avatar_url": "https://avatars3.githubusercontent.com/u/6426069?v=4", 24 | "profile": "https://aditimohanty.com/?utm_source=github&utm_medium=documentation-allcontributors&utm_content=casualdb", 25 | "contributions": [ 26 | "code", 27 | "doc", 28 | "ideas", 29 | "test" 30 | ] 31 | }, 32 | { 33 | "login": "Tezza48", 34 | "name": "William Terry", 35 | "avatar_url": "https://avatars0.githubusercontent.com/u/12277149?v=4", 36 | "profile": "http://www.willterry.me", 37 | "contributions": [ 38 | "bug" 39 | ] 40 | }, 41 | { 42 | "login": "kebot", 43 | "name": "Keith Yao", 44 | "avatar_url": "https://avatars0.githubusercontent.com/u/289392?v=4", 45 | "profile": "http://yaofur.com/", 46 | "contributions": [ 47 | "bug", 48 | "code" 49 | ] 50 | }, 51 | { 52 | "login": "jackfiszr", 53 | "name": "Jacek Fiszer", 54 | "avatar_url": "https://avatars.githubusercontent.com/u/7147395?v=4", 55 | "profile": "https://github.com/jackfiszr", 56 | "contributions": [ 57 | "code" 58 | ] 59 | } 60 | ], 61 | "contributorsPerLine": 7, 62 | "projectName": "casualdb", 63 | "projectOwner": "campvanilla", 64 | "repoType": "github", 65 | "repoHost": "https://github.com", 66 | "skipCi": true, 67 | "commitConvention": "none" 68 | } 69 | -------------------------------------------------------------------------------- /connector.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "https://deno.land/std@0.84.0/path/mod.ts" 2 | import { readJson } from "https://deno.land/std@0.68.0/fs/read_json.ts"; 3 | import { writeJson } from "https://deno.land/std@0.68.0/fs/write_json.ts"; 4 | 5 | export interface ConnectOptions { 6 | bailIfNotPresent?: boolean; 7 | } 8 | 9 | const getNow = Date.now; 10 | 11 | export class Connector { 12 | private _filePath = ""; 13 | private readonly WRITE_TIMEOUT: number = 10000; 14 | private readonly WRITE_WORKER_PATH: string = join(dirname(import.meta.url), "writeWorker.ts"); 15 | private readonly WRITE_WORKER_OPTIONS: { type: "module"; deno: boolean } = { 16 | type: "module", 17 | deno: true, 18 | }; 19 | 20 | private _checkConnection() { 21 | if (!this._filePath) { 22 | throw new Error("DB connection not set. Cannot read."); 23 | } 24 | } 25 | 26 | private _createTaskId(): string { 27 | return `casualdb:connector:${getNow()}`; 28 | } 29 | 30 | read() { 31 | this._checkConnection(); 32 | return readJson(this._filePath) as Promise; 33 | } 34 | 35 | async connect(fsPath: string, options?: ConnectOptions): Promise { 36 | try { 37 | const fileInfo = await Deno.stat(fsPath); 38 | 39 | if (!fileInfo.isFile) { 40 | throw new Error("Not a file"); 41 | } 42 | 43 | this._filePath = fsPath; 44 | this.read(); 45 | } catch (err) { 46 | if ( 47 | err instanceof Error && err.toString().startsWith("NotFound") && 48 | !options?.bailIfNotPresent 49 | ) { 50 | await writeJson(fsPath, {}); 51 | this._filePath = fsPath; 52 | return; 53 | } 54 | throw err; 55 | } 56 | } 57 | 58 | write(data: Schema) { 59 | return new Promise((resolve, reject) => { 60 | this._checkConnection(); 61 | const taskId = this._createTaskId(); 62 | let timeout: null | number = null; 63 | 64 | const worker = new Worker( 65 | this.WRITE_WORKER_PATH, 66 | { ...this.WRITE_WORKER_OPTIONS }, 67 | ); 68 | 69 | worker.onmessage = (e) => { 70 | const { taskId: returnedTaskId, error } = e.data; 71 | 72 | if ( 73 | returnedTaskId && returnedTaskId.toString() === taskId && 74 | error === false 75 | ) { 76 | resolve(undefined); 77 | } else if (error) { 78 | reject(error); 79 | } else { 80 | console.debug( 81 | "[casualdb:connector:debug]", 82 | { returnedTaskId, taskId, error }, 83 | ); 84 | reject(new Error(`[casualdb] unknown error while writing to file`)); 85 | } 86 | if (timeout) { 87 | clearTimeout(timeout); 88 | } 89 | }; 90 | 91 | timeout = setTimeout(() => { 92 | reject( 93 | new Error( 94 | `[casualdb] timed out while waiting for worker to respond while writing.`, 95 | ), 96 | ); 97 | }, this.WRITE_TIMEOUT); 98 | 99 | worker.postMessage({ 100 | file: this._filePath, 101 | taskId, 102 | data, 103 | }); 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /operator/collection.ts: -------------------------------------------------------------------------------- 1 | import matches from "https://deno.land/x/lodash@4.17.15-es/matches.js"; 2 | 3 | import { BaseOperator } from "./base.ts"; 4 | import { SortArg, Predicate } from "./types.ts"; 5 | import { compareFunction, createNewOperator } from "./utils.ts"; 6 | 7 | export class CollectionOperator extends BaseOperator { 8 | constructor(data: Op[]) { 9 | super(data); 10 | 11 | if (!Array.isArray(data)) { 12 | throw new Error( 13 | "[casualdb] CollectionOperator initialized with a value that is not an array.", 14 | ); 15 | } 16 | } 17 | 18 | /** 19 | * Get the size (length) of the collection. 20 | */ 21 | size(): number { 22 | return this.data.length; 23 | } 24 | 25 | push(data: Op) { 26 | return new CollectionOperator([...this.data, data]); 27 | } 28 | 29 | /** 30 | * Find & return one element from the collection 31 | */ 32 | findOne( 33 | /** the predicate must be either: 34 | * - a function which returns true when the desired item from the collection is "found". 35 | * - Pass an object which will be deep-compared with each item in the collection until a match is "found"; 36 | * using `_.matches` from lodash as the function. 37 | */ 38 | predicate: Predicate, 39 | ) { 40 | const predicateFunction = typeof predicate === "function" 41 | ? predicate 42 | : matches(predicate); 43 | 44 | const found = this.data.find((i) => predicateFunction(i)); 45 | return createNewOperator(found || null); 46 | } 47 | 48 | findAll(predicate: Predicate): CollectionOperator { 49 | const predicateFunction = typeof predicate === "function" 50 | ? predicate 51 | : matches(predicate); 52 | const filtered = this.data.filter((value) => predicateFunction(value)); 53 | return new CollectionOperator(filtered); 54 | } 55 | 56 | findAllAndUpdate( 57 | predicate: Predicate>, 58 | updateMethod: (value: Op) => Op, 59 | ): CollectionOperator { 60 | const predicateFunction = typeof predicate === "function" 61 | ? predicate 62 | : matches(predicate); 63 | 64 | return new CollectionOperator(this.data.map((value: Op) => { 65 | if (predicateFunction(value)) { 66 | return updateMethod(value); 67 | } 68 | 69 | return value; 70 | })); 71 | } 72 | 73 | findAllAndRemove( 74 | predicate: Predicate>, 75 | ): CollectionOperator { 76 | const predicateFunction = typeof predicate === "function" 77 | ? predicate 78 | : matches(predicate); 79 | return new CollectionOperator( 80 | this.data.filter((value) => !predicateFunction(value)), 81 | ); 82 | } 83 | 84 | findById(id: string | number) { 85 | return this.findOne({ id } as unknown as Op); 86 | } 87 | 88 | findByIdAndRemove(id: string | number) { 89 | return this.findAllAndRemove<{ id?: string | number }>({ id }); 90 | } 91 | 92 | findByIdAndUpdate(id: string | number, updateMethod: (value: Op) => Op) { 93 | return this.findAllAndUpdate<{ id?: string | number }>( 94 | { id }, 95 | updateMethod, 96 | ); 97 | } 98 | 99 | sort(compare: SortArg) { 100 | if (typeof compare === "function") { 101 | const sorted = [...this.data].sort(compare); 102 | return new CollectionOperator(sorted); 103 | } 104 | const sorted = [...this.data].sort(compareFunction(compare)); 105 | return new CollectionOperator(sorted); 106 | } 107 | 108 | page(page: number, pageSize: number): CollectionOperator { 109 | const start = (page - 1) * pageSize; 110 | const end = start + pageSize; 111 | 112 | return new CollectionOperator(this.data.slice(start, end)); 113 | } 114 | 115 | pick(keys: Array) { 116 | return new CollectionOperator(this.data.map((value) => { 117 | const obj = {} as { [P in U]: Op[P] }; 118 | 119 | keys.forEach((key) => { 120 | if (value[key]) { 121 | obj[key] = value[key]; 122 | } 123 | }); 124 | 125 | return obj; 126 | })); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via an issue, email, or any other method with the owners of this repository before making a change. 4 | 5 | Please note we have a [code of conduct](#code-of-conduct), please follow it in all your interactions with the project. 6 | 7 | ## Issue Process 8 | 9 | 1. Specify a context to the issue in the title if possible. For eg: `[Bug] This is a bug` or `[Feature Request] This is a feature` 10 | 11 | 2. Provide screenshots, logs and code-snippets if possible to better understand the problem/feature! 12 | 13 | 3. A brief about you, where you're from and the company you work. This is optional, but it helps understand where and how our product is being used. We'd love to get to know our users personally! 😄 14 | 15 | ## Pull Request Process 16 | 17 | 1. Ensure all your branch passes all linting tests and unit tests. 18 | 19 | 2. Tag the relevant issue in the Pull Request description. If an issue does not exist, please raise one. 20 | 21 | 3. Provide a high-level overview of the change, and to which files 22 | 23 | 4. Provide _before_ and _after_ screenshots if applicable. 24 | 25 | 5. The Pull Request will be merged once the PR is signed off by at least 2 maintainers. 26 | 27 | ## Code of Conduct 28 | 29 | ### Our Pledge 30 | 31 | In the interest of fostering an open and welcoming environment, we as 32 | contributors and maintainers pledge to making participation in our project and 33 | our community a harassment-free experience for everyone, regardless of age, body 34 | size, disability, ethnicity, gender identity and expression, level of experience, 35 | nationality, personal appearance, race, religion, or sexual identity and 36 | orientation. 37 | 38 | ### Our Standards 39 | 40 | Examples of behavior that contributes to creating a positive environment 41 | include: 42 | 43 | * Using welcoming and inclusive language 44 | * Being respectful of differing viewpoints and experiences 45 | * Gracefully accepting constructive criticism 46 | * Focusing on what is best for the community 47 | * Showing empathy towards other community members 48 | 49 | Examples of unacceptable behavior by participants include: 50 | 51 | * The use of sexualized language or imagery and unwelcome sexual attention or 52 | advances 53 | * Trolling, insulting/derogatory comments, and personal or political attacks 54 | * Public or private harassment 55 | * Publishing others' private information, such as a physical or electronic 56 | address, without explicit permission 57 | * Other conduct which could reasonably be considered inappropriate in a 58 | professional setting 59 | 60 | ### Our Responsibilities 61 | 62 | Project maintainers are responsible for clarifying the standards of acceptable 63 | behavior and are expected to take appropriate and fair corrective action in 64 | response to any instances of unacceptable behavior. 65 | 66 | Project maintainers have the right and responsibility to remove, edit, or 67 | reject comments, commits, code, wiki edits, issues, and other contributions 68 | that are not aligned to this Code of Conduct, or to ban temporarily or 69 | permanently any contributor for other behaviors that they deem inappropriate, 70 | threatening, offensive, or harmful. 71 | 72 | ### Scope 73 | 74 | This Code of Conduct applies both within project spaces and in public spaces 75 | when an individual is representing the project or its community. Examples of 76 | representing a project or community include using an official project e-mail 77 | address, posting via an official social media account, or acting as an appointed 78 | representative at an online or offline event. Representation of a project may be 79 | further defined and clarified by project maintainers. 80 | 81 | ### Enforcement 82 | 83 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 84 | reported by contacting the authors - [Abinav Seelan](mailto:abinav.n.seelan@gmail.com) or [Aditi Mohanty](mailto:aditi.anomita@gmail.com). All 85 | complaints will be reviewed and investigated and will result in a response that 86 | is deemed necessary and appropriate to the circumstances. The project team is 87 | obligated to maintain confidentiality with regard to the reporter of an incident. 88 | Further details of specific enforcement policies may be posted separately. 89 | 90 | Project maintainers who do not follow or enforce the Code of Conduct in good 91 | faith may face temporary or permanent repercussions as determined by other 92 | members of the project's leadership. 93 | 94 | ### Attribution 95 | 96 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 97 | available at [http://contributor-covenant.org/version/1/4][version] 98 | 99 | [homepage]: http://contributor-covenant.org 100 | [version]: http://contributor-covenant.org/version/1/4/ 101 | -------------------------------------------------------------------------------- /tests/CollectionOperator.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CollectionOperator, 3 | PrimitiveOperator, 4 | } from "../operator/operators.ts"; 5 | import { 6 | assertEquals, 7 | assertNotEquals, 8 | } from "https://deno.land/std@0.84.0/testing/asserts.ts"; 9 | 10 | import { Pokemon, pokemon } from "../data/pokemon.ts"; 11 | 12 | Deno.test("CollectionOperator: instantiates with value", () => { 13 | const data = [{ id: "1234", name: "foo" }]; 14 | const op = new CollectionOperator(data); 15 | 16 | assertEquals(op.value(), data); 17 | }); 18 | 19 | Deno.test("CollectionOperator: returns size of array", () => { 20 | const data = [ 21 | { id: "1", name: "foo" }, 22 | { id: "2", name: "bar" }, 23 | ]; 24 | const op = new CollectionOperator(data); 25 | 26 | assertEquals(op.size(), 2); 27 | }); 28 | 29 | Deno.test("CollectionOperator: findOne with simple predicate", () => { 30 | const data = [ 31 | { id: "1", name: "foo" }, 32 | { id: "2", name: "bar" }, 33 | ]; 34 | const op = new CollectionOperator(data); 35 | 36 | assertEquals(op.findOne({ name: "foo" }).value(), { id: "1", name: "foo" }); 37 | }); 38 | 39 | Deno.test("CollectionOperator: findOne with collection result", () => { 40 | const numbers = new CollectionOperator([[1, 3, 5], [2, 4, 6]]); 41 | const isEven = (item: number) => item % 2 === 0; 42 | const isFour = (item: number) => item === 4; 43 | 44 | const evenElements = numbers.findOne((element) => element.some(isEven)); 45 | 46 | assertEquals(evenElements instanceof CollectionOperator, true); 47 | assertEquals(evenElements.value(), [2, 4, 6]); 48 | 49 | if (evenElements instanceof CollectionOperator) { 50 | const four = evenElements.findOne(isFour); 51 | assertNotEquals(four.value(), null); 52 | 53 | if (four instanceof PrimitiveOperator) { 54 | assertEquals(four.value(), 4); 55 | } 56 | } 57 | }); 58 | 59 | Deno.test("CollectionOperator: findOne with cb predicate", () => { 60 | const data = [ 61 | { id: "1", name: "foo" }, 62 | { id: "2", name: "bar" }, 63 | ]; 64 | const op = new CollectionOperator(data); 65 | 66 | assertEquals( 67 | op 68 | .findOne((value) => { 69 | return value.name !== "foo"; 70 | }) 71 | .value(), 72 | { id: "2", name: "bar" }, 73 | ); 74 | }); 75 | 76 | Deno.test("CollectionOperator: pushes a value", () => { 77 | const data = [ 78 | { id: "1", name: "foo" }, 79 | { id: "2", name: "bar" }, 80 | ]; 81 | const op = new CollectionOperator(data); 82 | 83 | assertEquals(op.push({ id: "3", name: "John Doe" }).value(), [ 84 | { id: "1", name: "foo" }, 85 | { id: "2", name: "bar" }, 86 | { id: "3", name: "John Doe" }, 87 | ]); 88 | }); 89 | 90 | Deno.test("CollectionOperator: findAll with simple predicate", () => { 91 | const data = [ 92 | { id: "1", name: "John Doe" }, 93 | { id: "2", name: "Jane Doe" }, 94 | { id: "3", name: "John Doe" }, 95 | ]; 96 | const op = new CollectionOperator(data); 97 | 98 | assertEquals(op.findAll({ name: "John Doe" }).value(), [ 99 | { id: "1", name: "John Doe" }, 100 | { id: "3", name: "John Doe" }, 101 | ]); 102 | }); 103 | 104 | Deno.test("CollectionOperator: findAll with cb predicate", () => { 105 | const data = [ 106 | { id: "1", name: "John Doe" }, 107 | { id: "2", name: "Jane Doe" }, 108 | { id: "3", name: "John Doe" }, 109 | ]; 110 | const op = new CollectionOperator(data); 111 | 112 | assertEquals( 113 | op 114 | .findAll((value) => { 115 | return value.name === "John Doe"; 116 | }) 117 | .value(), 118 | [ 119 | { id: "1", name: "John Doe" }, 120 | { id: "3", name: "John Doe" }, 121 | ], 122 | ); 123 | }); 124 | 125 | Deno.test("CollectionOperator: sort by certain key(s) or compare function", () => { 126 | const allPokemon = new CollectionOperator(pokemon); 127 | const sortByHeight = (first: Pokemon, second: Pokemon) => { 128 | return first.height - second.height; 129 | }; 130 | const sortedByHeight = allPokemon.sort(sortByHeight).pick(["name", "height"]) 131 | .value(); 132 | const sortedByHeightThenWeight = allPokemon.sort(["height", "weight"]).pick( 133 | ["name", "height", "weight"], 134 | ).value(); 135 | 136 | assertEquals(sortedByHeight, [ 137 | { name: "flabebe", height: 1 }, 138 | { name: "diglett", height: 2 }, 139 | { name: "jirachi", height: 3 }, 140 | { name: "emboar", height: 16 }, 141 | { name: "charizard", height: 17 }, 142 | { name: "typhlosion", height: 17 }, 143 | { name: "garchomp", height: 19 }, 144 | { name: "wailord", height: 145 }, 145 | ]); 146 | assertEquals(sortedByHeightThenWeight, [ 147 | { name: "flabebe", height: 1, weight: 1 }, 148 | { name: "diglett", height: 2, weight: 8 }, 149 | { name: "jirachi", height: 3, weight: 11 }, 150 | { name: "emboar", height: 16, weight: 1500 }, 151 | { name: "typhlosion", height: 17, weight: 795 }, 152 | { name: "charizard", height: 17, weight: 905 }, 153 | { name: "garchomp", height: 19, weight: 950 }, 154 | { name: "wailord", height: 145, weight: 3980 }, 155 | ]); 156 | }); 157 | 158 | Deno.test("CollectionOperator: pagination", () => { 159 | const allPokemon = new CollectionOperator(pokemon); 160 | 161 | assertEquals(allPokemon.sort(["height"]).page(1, 2).value(), [ 162 | { 163 | name: "flabebe", 164 | id: 669, 165 | weight: 1, 166 | height: 1, 167 | types: ["fairy"], 168 | }, 169 | { 170 | name: "diglett", 171 | id: 50, 172 | weight: 8, 173 | height: 2, 174 | types: ["ground"], 175 | }, 176 | ]); 177 | }); 178 | 179 | Deno.test("CollectionOperator: pick", () => { 180 | const allPokemon = new CollectionOperator(pokemon); 181 | 182 | const result = allPokemon.pick(["name", "id"]).value(); 183 | 184 | assertEquals(result, [ 185 | { 186 | name: "charizard", 187 | id: 6, 188 | }, 189 | { 190 | name: "typhlosion", 191 | id: 157, 192 | }, 193 | { 194 | name: "emboar", 195 | id: 500, 196 | }, 197 | { 198 | name: "garchomp", 199 | id: 445, 200 | }, 201 | { 202 | name: "diglett", 203 | id: 50, 204 | }, 205 | { 206 | name: "jirachi", 207 | id: 385, 208 | }, 209 | { 210 | name: "flabebe", 211 | id: 669, 212 | }, 213 | { 214 | name: "wailord", 215 | id: 321, 216 | }, 217 | ]); 218 | }); 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | Simple JSON "database" for Deno with type-safety! ⚡️ 8 | 9 |

10 | 11 |

12 | WARNING: This project is still in beta phase. We are actively working on enhancing the API and ironing out kinks. If you find a bug or have a feature request, feel free to create an issue or contribute. 🙂 13 |

14 | 15 | ![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/campvanilla/casualdb?color=%232ecc71&include_prereleases&style=flat-square) 16 | [![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) 17 | 18 | 19 | ## Contents 20 | 21 | * [Quick Usage](#quick-usage) 22 | * [Installation](#installation) 23 | * [API](#api) 24 | * [Inspiration](#inspiration) 25 | * [Disclaimer](#disclaimer) ⚠️ 26 | * [Contributing](#contributing) 27 | 28 | ## Quick Usage 29 | 30 | ``` ts 31 | // create an interface to describe the structure of your JSON 32 | interface Schema { 33 | posts: Array<{ 34 | id: number; 35 | title: string; 36 | views: number; 37 | }>; 38 | user: { 39 | name: string; 40 | }; 41 | } 42 | 43 | const db = new CasualDB(); // instantiate the db, casually 🤓 44 | await db.connect("./test-db.json"); // "connect" to the db (JSON file) 45 | 46 | // (optional) seed it with data, if starting with an empty db 47 | await db.seed({ 48 | posts: [ 49 | { id: 1, title: "Post 1", views: 99 }, 50 | { id: 2, title: "Post 2", views: 30 }, 51 | ], 52 | user: { name: "Camp Vanilla" }, 53 | }); 54 | 55 | const posts = await db.get('posts'); // pass the interface key in order for type-checking to work 56 | 57 | const postTitlesByViews = ( 58 | posts 59 | .sort(['views']) // sort by views (ascending) 60 | .pick(['title']) // pick the title of every post 61 | .value() // => ['Post 2', 'Post 1'] 62 | ); 63 | ``` 64 | 65 | ## Installation 66 | 67 | ``` ts 68 | import { CasualDB } from "https://deno.land/x/casualdb@0.1.4/mod.ts"; 69 | 70 | // create an interface to describe the structure of your JSON 71 | interface Schema { 72 | posts: Array<{ 73 | id: number; 74 | title: string; 75 | views: number; 76 | }>; 77 | user: { 78 | name: string; 79 | }; 80 | } 81 | 82 | const db = new CasualDB(); 83 | ``` 84 | 85 | Note: When running via deno, this module will require you to pass the following flags (all flags are mandatory):- 86 | 87 | * `--allow-read` : in order to be able to **read** the JSON files 88 | * `--allow-write`: in order to be able to **write** to the JSON files 89 | * `--unstable` : this module uses the experimental Worker API in deno, and hence requires this flag 90 | * `--allow-net` : this is to enable to download of the Worker file. 91 | 92 | If you want to always run the latest code (from the `master` branch) of this module, install via: 93 | ```ts 94 | import { CasualDB } from "https://deno.land/x/casualdb/mod.ts"; 95 | ``` 96 | 97 | ## API 98 | 99 | ### new CasualDB() 100 | 101 | Returns an instance of the _CasualDB_. Passing in a interface describing your JSON data ensures that **type checking works correctly**. The following are the methods available on this class instance 102 | 103 | * [.connect()](#casual-db-connect) 104 | * [.get()](#casual-db-get) 105 | * [.seed()](#casual-db-seed) 106 | * [.write()](#casual-db-write) 107 | 108 |

109 | .connect(pathToJsonFile: string, options?: ConnectOptions) 110 |

111 | 112 | Creates a _connection_ to a json file passed as parameter. Returns a promise. 113 | 114 | ConnectOptions: 115 | 116 | + `bailIfNotPresent` : Controls whether you would like an error to be thrown if the file being connected to does not exist. Default = `false` . 117 | 118 | ``` ts 119 | await db.connect("./test-db.json"); 120 | 121 | // or with options 122 | 123 | await db.connect("./test-db.json", { 124 | bailIfNotPresent: true, 125 | }); 126 | ``` 127 | 128 |

129 | .get(jsonPath: string) 130 |

131 | 132 | Fetches value from connected JSON file. Takes an object _path_ as parameter. Returns a `Promise` . 133 | **Important**: For type checking to work, ensure that the Template Type is provided to `.get()` . If this is not provided, typescript cannot decide a _CollectionOperator_ or _PrimitiveOperator_ has been returned and hence you'd have to manually narrow it down for TS. 134 | 135 | ``` ts 136 | interface Schema { 137 | posts: Array<{ 138 | id: number; 139 | title: string; 140 | views: number; 141 | }>; 142 | user: { 143 | name: string; 144 | }; 145 | } 146 | 147 | await db.get('posts'); // Returns a Promise 148 | 149 | // or 150 | 151 | await db.get('posts.0.id'); // Returns a Promise 152 | ``` 153 | 154 |

155 | .seed(data: Schema) 156 |

157 | 158 | Overrides the contents of the connected JSON file. This is beneficial for when you don't already have data in the file or you want to add some defaults. Returns a promise. 159 | 160 | ``` ts 161 | interface Schema { 162 | posts: Array<{ 163 | id: number; 164 | title: string; 165 | views: number; 166 | }>; 167 | user: { 168 | name: string; 169 | }; 170 | } 171 | 172 | await db.seed({ 173 | posts: [ 174 | { id: 1, title: "Post 1", views: 99 }, 175 | { id: 2, title: "Post 2", views: 30 }, 176 | ], 177 | user: { name: "Camp Vanilla" }, 178 | }); 179 | ``` 180 | 181 |

182 | .write(jsonPath: string, data: any) 183 |

184 | 185 | Writes the provided value to the Object path provided. Returns a promise. 186 | 187 | ``` ts 188 | await db.write('posts', [ 189 | { id: 1, title: "Post 1", views: 99 }, 190 | { id: 2, title: "Post 2", views: 30 }, 191 | ]); 192 | 193 | // or 194 | 195 | await db.write('posts.0.title', 'Post 1'); 196 | ``` 197 | 198 | ### PrimitiveOperator 199 | 200 | When performing a `db.get()` on a path that returns a non-array value, the Promise resolves to an instance of `PrimitiveOperator` . The _PrimitiveOperator_ class encapsulates functions that allow you work with any non-array-like data in javascript (eg. `object` , `string` , `number` , `boolean` ). All functions that are a part of _PrimitiveOperator_ allow function chaining. 201 | 202 | ``` ts 203 | interface Schema { 204 | posts: Array<{ 205 | id: number; 206 | title: string; 207 | views: number; 208 | }>; 209 | user: { 210 | name: string; 211 | }; 212 | } 213 | 214 | const data = await db.get('posts'); // ❌ Not a PrimitiveOperator as the value is going to be an array 215 | 216 | const data = await db.get('posts.0'); // ✅ PrimitiveOperator as the value is a non-array. 217 | ``` 218 | 219 | Instances of this class have the following methods: 220 | 221 | * [.value()](#primitive-operator-value) 222 | * [.update()](#primitive-operator-update) 223 | * [.pick()](#primitive-operator-pick) 224 | 225 |

226 | .value() 227 |

228 | 229 | Returns the value of the data. 230 | 231 | ``` ts 232 | const data = await db.get('posts.0'); 233 | 234 | data.value(); // { id: 1, title: "Post 1", views: 99 } 235 | ``` 236 | 237 |

238 | .update(updateMethod: (currentValue) => T) 239 |

240 | 241 | Method to update the data. Method takes an updater-function as parameter. The updater-function will receive the value you want to update and expects a return value. The type of the updated data is inferred by the ReturnType of the updater-function. 242 | 243 | ``` ts 244 | const data = await db.get('posts.0'); 245 | 246 | data 247 | .update((value) => ({ 248 | title: "Modified Post", 249 | })) 250 | .value(); // { id: 1, title: "Modified Post" } 251 | ``` 252 | 253 |

254 | .pick(keys: string[]) 255 |

256 | 257 | Picks and returns a subset of keys from the data. Method allows only keys present on data. If the data is not an object, method returns the data as is. 258 | 259 | ``` ts 260 | const data = await db.get('posts.0'); 261 | 262 | data 263 | .pick(["id", "title"]) 264 | .value(); // { id: 1, title: "Post 1" } 265 | ``` 266 | 267 | ### CollectionOperator 268 | 269 | When performing a `db.get()` on a path that returns an array, the Promise resolves to a instance of `CollectionOperator` . The _CollectionOperator_ class encapsulates functions that allow you work with array-like data (collection of items). All functions that are a part of _CollectionOperator_ allow function chaining. 270 | 271 | ``` ts 272 | interface Schema { 273 | posts: Array<{ 274 | id: number; 275 | title: string; 276 | views: number; 277 | }>; 278 | user: { 279 | name: string; 280 | }; 281 | } 282 | 283 | const data = await db.get('posts'); // ✅ CollectionOperator as the value is an array. 284 | 285 | const data = await db.get('posts.0'); // ❌ PrimitiveOperator as the value is a non-array. 286 | ``` 287 | 288 | Instances of this class contain the following methods. All methods are chainable: 289 | 290 | * [.value()](#collection-operator-value) 291 | * [.size()](#collection-operator-size) 292 | * [.findOne()](#collection-operator-findOne) 293 | * [.findAllAndUpdate()](#collection-operator-findAllAndUpdate) 294 | * [.findAllAndRemove()](#collection-operator-findAllAndRemove) 295 | * [.findById()](#collection-operator-findById) 296 | * [.findByIdAndRemove()](#collection-operator-findByIdAndRemove) 297 | * [.findByIdAndUpdate()](#collection-operator-findByIdAndUpdate) 298 | * [.sort()](#collection-operator-sort) 299 | * [.page()](#collection-operator-page) 300 | * [.pick()](#collection-operator-pick) 301 | 302 |

303 | .value() 304 |

305 | 306 | Returns the value of the data. 307 | 308 | ``` ts 309 | const data = await db.get('posts'); 310 | 311 | console.log(data.value()); // [ { id: 1, title: "Post 1", views: 99 }, { id: 2, title: "Post 2", views: 30 }, ] 312 | ``` 313 | 314 |

315 | .size() 316 |

317 | 318 | Returns the length of the data. 319 | 320 | ``` ts 321 | const data = await db.get('posts'); 322 | 323 | console.log(data.size()); // 2 324 | ``` 325 | 326 |

327 | .findOne(predicate: Object | Function => boolean) 328 |

329 | 330 | Searches through the collection items and returns an item if found, else returns an instance of `PrimitiveOperator` . The predicate can be of two forms: 331 | 332 | 1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection. 333 | 2. A search-function where you can provide your custom logic and return `true` for the condition you are looking for. 334 | 335 | Returns a `PrimitiveOperator` or `CollectionOperator` based on type of the found element. 336 | 337 | ``` ts 338 | const data = await db.get('posts'); 339 | 340 | data 341 | .findOne({ id: 1 }) 342 | .value();// { id: 1, title: "Post 1", views: 99 } 343 | 344 | // or 345 | 346 | data 347 | .findOne((value) => { 348 | return value.id === 1 349 | }) 350 | .value(); // { id: 1, title: "Post 1", views: 99 } 351 | ``` 352 | 353 |

354 | .push(value) 355 |

356 | 357 | Push a new value into the collection. Returns a `CollectionOperator` with the updated items. 358 | 359 | ``` ts 360 | const data = await db.get('posts'); 361 | 362 | data 363 | .push({ id: 3, post: 'Post 3', views: 45 }) 364 | .value(); // [ { id: 1, title: "Post 1", views: 99 }, { id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Post 3", views: 45 } ] 365 | ``` 366 | 367 |

368 | .findAll(predicate: Object | Function => boolean) 369 |

370 | 371 | Searches through the items of the collection and returns a `CollectionOperator` of all occurrences that satisfy the predicate. The predicate can be of two forms: 372 | 373 | 1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection. 374 | 2. A search-function where you can provide your custom logic and return `true` for the condition you are looking for. 375 | 376 | Returns a `CollectionOperator` with the subset of items. 377 | 378 | ``` ts 379 | const data = await db.get('posts'); 380 | 381 | data 382 | .findAll({ title: 'Post 1' }) 383 | .value();// [{ id: 1, title: "Post 1", views: 99 }] 384 | 385 | // or 386 | 387 | data 388 | .findAll((value) => { 389 | return value.views > 40; 390 | }) 391 | .value(); // [{ id: 1, title: "Post 1", views: 99 },{ id: 3, title: "Post 3", views: 45 }]; 392 | ``` 393 | 394 |

395 | .findAllAndUpdate(predicate: Object | Function => boolean, updateMethod: (value) => T) 396 |

397 | 398 | Searches through the collection and returns a `CollectionOperator` with all occurrences that satisfy the predicate updated with the return value of the _updateMethod_. The predicate can be of two forms: 399 | 400 | 1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection. 401 | 2. A search-function where you can provide your custom logic and return `true` for the condition you are looking for. 402 | 403 | Returns a `CollectionOperator` with the updated array. 404 | 405 | ``` ts 406 | const data = await db.get('posts'); 407 | 408 | data 409 | .findAllAndUpdate({ title: 'Post 1' }, (value) => ({ ...value, title: 'Modified Post' })) 410 | .value(); // [{ id: 1, title: "Modified Post", views: 99 },{ id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Post 3", views: 45 }] 411 | 412 | // or 413 | 414 | data 415 | .findAllAndUpdate((value) => { 416 | return value.views > 40; 417 | }, (value) => ({ 418 | ...value, 419 | title: 'Trending Post' 420 | })) 421 | .value(); // [{ id: 1, title: "Trending Post", views: 99 }, { id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Trending Post", views: 45 }]; 422 | ``` 423 | 424 |

425 | .findAllAndRemove(predicate: Object | Function => boolean, updateMethod: (value) => T) 426 |

427 | 428 | Searches through the collection and returns a new `CollectionOperator` where all occurrences that satisfy the predicate are *omitted*. The predicate can be of two forms: 429 | 430 | 1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection. 431 | 2. A search-function where you can provide your custom logic and return `true` for the condition you are looking for. 432 | 433 | Returns a `CollectionOperator` with the updated array. 434 | 435 | ``` ts 436 | const data = await db.get('posts'); 437 | 438 | data 439 | .findAllAndRemove({ title: 'Post 1' }) 440 | .value(); // [{ id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Post 3", views: 45 }] 441 | 442 | // or 443 | 444 | data 445 | .findAllAndRemove((value) => value.views > 40) 446 | .value(); // [{ id: 2, title: "Post 2", views: 30 }]; 447 | ``` 448 | 449 |

450 | .findById(id: string) 451 |

452 | 453 | Syntactical sugar for `.findOne({ id })` . 454 | 455 |

456 | .findByIdAndRemove(id: string) 457 |

458 | 459 | Syntactical sugar for `.findAllAndRemove({ id })` . 460 | 461 |

462 | .findByIdAndUpdate(id: string, updateMethod: (value) => T) 463 |

464 | 465 | Syntactical sugar for `.findAllAndUpdate({ id }, updateMethod)` . 466 | 467 |

468 | .sort(predicate: string[] | Function => boolean) 469 |

470 | 471 | Sorts and returns a new sorted `CollectionOperator` instance. The comparison predicate can be one of two types: 472 | 473 | * **an array of keys** to select for sorting the items in the collection (priority is left-right).
474 | For example, when the predicate is `['views','id']` , the method will first sort *posts* in ascending order of *views* that each post has. Any posts which have the *same* number of views, will then be sorted by `id` . 475 | * a **compare function** similar to [ `Array.prototype.sort` ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters)'s `compareFunction` . 476 | 477 | ``` ts 478 | const posts = await db.get('posts'); 479 | 480 | posts 481 | .sort(['views']) 482 | .value() // [{ id: 2, title: "Post 2", views: 30 }, { id: 1, title: "Post 1", views: 99 }] 483 | 484 | // or 485 | 486 | posts 487 | .sort((a,b) => a.views - b.views) 488 | .value() // [{ id: 2, title: "Post 2", views: 30 }, { id: 1, title: "Post 1", views: 99 }] 489 | ``` 490 | 491 |

492 | .page(page: number, pageSize: number) 493 |

494 | 495 | Returns a paginated subset of the collection. 496 | 497 | ``` ts 498 | const posts = await db.get('posts'); 499 | 500 | posts 501 | .page(1, 1) 502 | .value() // [{ id: 1, title: "Post 1", views: 99 }] 503 | ``` 504 | 505 |

506 | .pick(keys: string[]) 507 |

508 | 509 | Returns a `CollectionOperator` of items with each item having only the *picked* keys. Only keys present on the type of the items in the collection are allowed. If the item is not an object, this method returns an empty object ( `{}` ) for it. 510 | 511 | ``` ts 512 | const posts = await db.get('posts'); 513 | 514 | posts 515 | .pick(['title']) 516 | .value() // [{ title: "Post 1" }, { title: "Post 2" }] 517 | ``` 518 | 519 | ## Inspiration 520 | 521 | This project has taken inspiration from [lowdb](https://github.com/typicode/lowdb) for the concept and [mongoose](https://mongoosejs.com/) for certain parts of the `CollectionOperator` API. 522 | 523 | It aims to simplify the process of setting up a full-fledged db when building prototypes or small-scale applications like CLI tools or toy apps for Deno. 524 | 525 | 526 | ### 🚧 ⚠️ Disclaimer ⚠️ 🚧 527 | 528 | 529 | 530 | **Disclaimer** : As mentioned above, this module is best used for small-scale apps and should not be used in a large production application and you may face issues like: 531 | * concurrency management (for writes) 532 | * storing and parsing large amounts of JSON data. 533 | 534 | ## Contributing 535 | 536 | Want to raise an issue or pull request? Do give our [Contribution Guidelines](./.github/CONTRIBUTING.md) page a read. 🤓 537 | 538 | ## Contributors ✨ 539 | 540 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 |

Abinav Seelan

💻 📖 🤔 ⚠️

Aditi Mohanty

💻 📖 🤔 ⚠️

William Terry

🐛

Keith Yao

🐛 💻

Jacek Fiszer

💻
554 | 555 | 556 | 557 | 558 | 559 | 560 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 561 | --------------------------------------------------------------------------------