├── .eggignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── deno.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── deps.ts ├── egg.yml ├── example ├── hello_world.ts ├── sort_and_select.ts └── with_oak.ts ├── mod.ts └── src ├── collection.ts ├── collection_test.ts ├── dataset.ts ├── dataset_test.ts ├── db.ts ├── db_test.ts ├── fsmanager.ts ├── fsmanager_test.ts └── types.ts /.eggignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .github/ 3 | .vscode/ 4 | .eggignore 5 | .gitignore 6 | CHANGELOG.md 7 | CODE_OF_CONDUCT.md 8 | CONTRIBUTING.md 9 | egg.yml 10 | LICENSE 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will install Deno and run tests across stable and nightly builds on Windows, Ubuntu and macOS. 7 | # For more information see: https://github.com/denolib/setup-deno 8 | 9 | name: Deno 10 | 11 | on: 12 | push: 13 | branches: ['*'] 14 | pull_request: 15 | branches: [main] 16 | 17 | jobs: 18 | test: 19 | runs-on: ${{ matrix.os }} # runs a test on Ubuntu, Windows and macOS 20 | 21 | strategy: 22 | matrix: 23 | deno: ["v1.4.6"] 24 | os: [ubuntu-latest] 25 | 26 | steps: 27 | - name: ACTIONS_ALLOW_UNSECURE_COMMANDS 28 | id: ACTIONS_ALLOW_UNSECURE_COMMANDS 29 | run: echo 'ACTIONS_ALLOW_UNSECURE_COMMANDS=true' >> $GITHUB_ENV 30 | 31 | - name: Setup repo 32 | uses: actions/checkout@v2 33 | 34 | - name: Setup Deno 35 | uses: denolib/setup-deno@c7d7968ad4a59c159a777f79adddad6872ee8d96 36 | with: 37 | deno-version: ${{ matrix.deno }} # tests across multiple Deno versions 38 | 39 | - name: Cache Dependencies 40 | run: deno cache deps.ts --unstable 41 | 42 | - name: Run Tests 43 | run: deno test -A --unstable 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db 2 | data 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.import_intellisense_origins": { 4 | "https://deno.land": true 5 | }, 6 | "editor.detectIndentation": true, 7 | "editor.tabSize": 2 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - data can now be updated using a updater function. 10 | - README added `Permission Required` section to clearly state which flags are needed to be applied. 11 | ### Changed 12 | - hello_world example to use the new functions. 13 | - README example to use the new functions. 14 | - README `Usage` section changed to `Getting Started` section, to illustrate the whole idea step by step. 15 | - collection function "find" refactor to "findMany". 16 | 17 | ## [0.0.5] - 2020-10-26 18 | ### Added 19 | - CHANGELOG to log the changes between versions like other standardied open source projects. 20 | - README add badges. 21 | - fsmanager to manage database file system. 22 | - dataset to manage the operations on retrieved data from database. 23 | - sort_and_select example to demonstrate dataset operation. 24 | ### Changed 25 | - test cases 26 | - documentations 27 | - now we can use filter functions to select data from database 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at js.wildcards@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | I am glad to see you here, because this project needs you to become more complete. Thanks for your enthusiastic! :heart: 4 | 5 | Please: 6 | - fork a copy of this project to your account 7 | - open a new branch for your changes to the code base 8 | - format and test the code 9 | - pass the GitHub Action before creating a pull request to the main branch 10 | 11 | If you have any questions, please open an issue. I will review it as soon as possible. 12 | 13 | ## Formatting and Testing 14 | 15 | Thanks to the Deno ecosystem, it is very simple for doing formatting and testing in Deno. So, please do the following after changing the code base. 16 | 17 | ```bash 18 | $ deno fmt 19 | $ deno test --allow-read --allow-write 20 | ``` 21 | 22 | Remember, you must pass all test cases before opening a pull request. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-now jswildcards 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 | # FileDB 2 | 3 | ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/jswildcards/filedb/Deno/develop) 4 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/jswildcards/filedb) 5 | ![GitHub Release Date](https://img.shields.io/github/release-date/jswildcards/filedb) 6 | ![LGTM Grade](https://img.shields.io/lgtm/grade/javascript/github/jswildcards/filedb) 7 | ![deno-code-coverage](https://img.shields.io/badge/code%20coverage-93.97%25-brightgreen.svg) 8 | ![GitHub Repo stars](https://img.shields.io/github/stars/jswildcards/filedb?style=social) 9 | ![GitHub](https://img.shields.io/github/license/jswildcards/filedb) 10 | [![nest badge](https://nest.land/badge.svg)](https://nest.land/package/filedb) 11 | 12 | :zap: A lightweight local JSON database for Deno. 13 | 14 | ## Why Use FileDB? 15 | 16 | - Simplicity: the module is semantic and easy to use. 17 | - Flexibility: the module have multiple implementations for different situations. 18 | - Suit with RESTful API: the module API is suited with RESTful API, you may refer to [this](https://github.com/jswildcards/filedb/blob/main/example/with_oak.ts) example. 19 | 20 | ## Quick Start 21 | 22 | ```bash 23 | $ git clone https://github.com/jswildcards/filedb.git 24 | $ cd ./filedb/example 25 | $ deno run --allow-read --allow-write hello_world.ts 26 | ``` 27 | 28 | ## Getting Started 29 | 30 | ### Setup 31 | 32 | Let's start with importing the FileDB module and creating a database. 33 | 34 | ```ts 35 | // main.ts 36 | import { FileDB, Document } from "https://deno.land/x/filedb/mod.ts"; 37 | const db = new FileDB({ rootDir: "./data", isAutosave: true }); // create database with autosave 38 | ``` 39 | 40 | Then, create a `User` collection. The `User` collection has three attributes: `firstName`, `lastName` and `favourites` - a list of fruits the user loves! 41 | 42 | To achieve this step, we need to define a `User` interface with attributes, and get (and implicitly create!) the `User` collection from the database. 43 | 44 | ```ts 45 | // main.ts 46 | interface User extends Document { 47 | firstName?: string; 48 | lastName?: string; 49 | favourites?: string[]; 50 | } 51 | const users = await db.getCollection("users"); // implicitly create and get User collection 52 | ``` 53 | 54 | ### Insert Records 55 | 56 | We now have a `User` collection which can be inserted some records. Let's add one `User` first who is `fancy foo` and loves `🍎 Apple` and `🍐 Pear`. 57 | 58 | ```ts 59 | // main.ts 60 | await users.insertOne({ 61 | firstName: "fancy", 62 | lastName: "foo", 63 | favourites: ["🍎 Apple", "🍐 Pear"], 64 | }); 65 | ``` 66 | 67 | Great! We have our first records inserted into the collection. Now let's try inserting more `User` by using `insertMany` method. 68 | 69 | ```ts 70 | // main.ts 71 | await users.insertMany([ 72 | { 73 | firstName: "betty", 74 | lastName: "bar", 75 | favourites: ["🍌 Banana"], 76 | }, 77 | { 78 | firstName: "benson", 79 | lastName: "baz", 80 | favourites: ["🍌 Banana"], 81 | }, 82 | ]); 83 | ``` 84 | 85 | ### Retrieve Records 86 | 87 | Now we have totally 3 `User` in our collection. We now want to know the information about `fancy foo`. We can use `findOne` method and pass a *filtered object* to do that. 88 | 89 | ```ts 90 | // main.ts 91 | console.log(users.findOne({ firstName: "fancy", lastName: "foo" })); 92 | ``` 93 | 94 | Great! But how about we now want to get *all* `User` who loves `🍌 Banana`? We can use `findMany` method and pass a `filter method` to do that. Remember to call `.value()` after calling the `findMany` method. 95 | 96 | ```ts 97 | // main.ts 98 | console.log(users.findMany((el) => el.favourites?.includes("🍌 Banana")).value()); 99 | ``` 100 | 101 | ### Update Records 102 | 103 | As time goes by, some `User` may change their favourites. We now want to update **only the first** `User` who only loves `🍌 Banana` before, loves `🍎 Apple` and `🍐 Pear` only in this moment. 104 | 105 | In this case, the database will update the `User betty bar` as obviously she was inserted into the database earlier than `User benson baz`. 106 | 107 | ```ts 108 | // main.ts 109 | await users.updateOne( 110 | (el) => el.favourites?.[0] === "🍌 Banana", 111 | { favourites: ["🍎 Apple", "🍐 Pear"] }, 112 | ); 113 | ``` 114 | 115 | Now we want to update **all** `User` whose `lastName` contains "ba". As besides love whatever they loved before, they all love "🍉 Watermelon" now. 116 | 117 | ```ts 118 | // main.ts 119 | await users.updateMany( 120 | (el) => el.lastName?.includes("ba"), 121 | (el) => { 122 | el.favourites = ["🍉 Watermelon", ...(el.favourites || [])]; 123 | return el; 124 | }, 125 | ); 126 | ``` 127 | 128 | ### Delete Records 129 | 130 | Now we want to delete some records in our database. First we delete **only one** `User` whose `firstName` is `fancy`. 131 | 132 | ```ts 133 | // main.ts 134 | await users.deleteOne({ firstName: "fancy" }); 135 | ``` 136 | 137 | Now we want to delete **all** `User` whose has at least one favourites. 138 | 139 | ```ts 140 | // main.ts 141 | await users.deleteMany((el) => (el.favourites?.length ?? []) >= 1); 142 | ``` 143 | 144 | ### Drop Database 145 | 146 | ```ts 147 | // main.ts 148 | await db.drop(); 149 | ``` 150 | 151 | The whole example can be found [here](https://github.com/jswildcards/filedb/tree/main/example/hello_world.ts). 152 | 153 | This module can do stuffs more than that! More examples can be found [here](https://github.com/jswildcards/filedb/tree/main/example). 154 | 155 | ## Permission Required 156 | 157 | This module requires `--allow-read` and `--allow-write` flags. 158 | 159 | ## API 160 | 161 | Please see the [documentation](https://doc.deno.land/https/deno.land/x/filedb/mod.ts). 162 | 163 | ## Contribution 164 | 165 | Welcome to Contribute to this module. Read this [guideline](https://github.com/jswildcards/filedb/blob/main/CONTRIBUTING.md) to get started. 166 | 167 | ## Disclaimer 168 | 169 | This module is still unstable. So the module API may have breaking changes. 170 | 171 | This module is only suitable for small-scaled projects. As when the database is large enough, it will be slow down with this file-based database structure and unoptimised searching algorithms. 172 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { assertEquals } from "https://deno.land/std@0.74.0/testing/asserts.ts"; 2 | export { v4 as uuid } from "https://deno.land/std@0.74.0/uuid/mod.ts"; 3 | export { ensureDir } from "https://deno.land/std@0.74.0/fs/ensure_dir.ts"; 4 | export { ensureFile } from "https://deno.land/std@0.74.0/fs/ensure_file.ts"; 5 | export { exists } from "https://deno.land/std@0.74.0/fs/exists.ts"; 6 | -------------------------------------------------------------------------------- /egg.yml: -------------------------------------------------------------------------------- 1 | name: filedb 2 | description: ⚡ A lightweight local JSON database for Deno. 3 | repository: 'https://github.com/jswildcards/filedb' 4 | unstable: true 5 | entry: ./mod.ts 6 | files: 7 | - ./example/**/* 8 | - ./README.md 9 | - ./mod.ts 10 | - ./deps.ts 11 | - ./src/**/* 12 | - ./mod.ts 13 | version: 0.0.6 14 | checkAll: true 15 | unlisted: false 16 | -------------------------------------------------------------------------------- /example/hello_world.ts: -------------------------------------------------------------------------------- 1 | import { Document, FileDB } from "../mod.ts"; 2 | 3 | interface User extends Document { 4 | firstName?: string; 5 | lastName?: string; 6 | favourites?: string[]; 7 | } 8 | 9 | const db = new FileDB({ rootDir: "./data", isAutosave: true }); 10 | // Notice the error message in console 11 | await db.drop(); 12 | const users = await db.getCollection("users"); 13 | 14 | await users.insertOne({ 15 | firstName: "fancy", 16 | lastName: "foo", 17 | favourites: ["🍎 Apple", "🍐 Pear"], 18 | }); 19 | 20 | // if autosave option is unset or set to false, you need the code below to save data 21 | // db.save(); 22 | 23 | await users.insertMany([ 24 | { 25 | firstName: "betty", 26 | lastName: "bar", 27 | favourites: ["🍌 Banana"], 28 | }, 29 | { 30 | firstName: "benson", 31 | lastName: "baz", 32 | favourites: ["🍌 Banana"], 33 | }, 34 | ]); 35 | 36 | console.log(users.findMany((el) => el.lastName?.includes("ba")).value()); 37 | console.log(users.findOne({ firstName: "fancy" })); 38 | 39 | await users.updateOne( 40 | (el) => el.favourites?.[0] === "🍌 Banana", 41 | { favourites: ["🍎 Apple", "🍐 Pear"] }, 42 | ); 43 | (await users.updateMany( 44 | (el) => el.lastName?.includes("ba"), 45 | (el) => { 46 | el.favourites = ["🍉 Watermelon", ...(el.favourites || [])]; 47 | return el; 48 | }, 49 | )).value(); 50 | 51 | await users.deleteOne({ firstName: "fancy" }); 52 | await users.deleteMany((el) => (el.favourites?.length ?? []) >= 1); 53 | await db.drop(); 54 | -------------------------------------------------------------------------------- /example/sort_and_select.ts: -------------------------------------------------------------------------------- 1 | import { Document, FileDB } from "../mod.ts"; 2 | 3 | interface User extends Document { 4 | username?: string; 5 | favourites?: string[]; 6 | } 7 | 8 | const db = new FileDB(); 9 | const users = await db.getCollection("users"); 10 | 11 | const compareFavourites = (a: User, b: User) => { 12 | const numberMoreThan = (a.favourites?.length ?? 0) - 13 | (b.favourites?.length ?? 0); 14 | return numberMoreThan / Math.abs(numberMoreThan || 1); 15 | }; 16 | 17 | const compareUsername = (a: User, b: User) => { 18 | const aName = a.username ?? ""; 19 | const bName = b.username ?? ""; 20 | return aName > bName ? 1 : (aName < bName ? -1 : 0); 21 | }; 22 | 23 | users.insertMany([ 24 | { 25 | username: "foo", 26 | favourites: ["🍎 Apple", "🍐 Pear"], 27 | }, 28 | { 29 | username: "baz", 30 | favourites: ["🍌 Banana"], 31 | }, 32 | { 33 | username: "bar", 34 | favourites: ["🍌 Banana"], 35 | }, 36 | ]); 37 | 38 | // Test 1: Sort by number of favourites 39 | const value1 = users.findMany({}).sort(compareFavourites).select(["username"]) 40 | .value(); 41 | 42 | console.log(value1); // "baz", "bar", "foo" 43 | 44 | // Test 2: First sort by number of favourites, then sort by username 45 | const value2 = users.findMany({}).sort((a, b) => 46 | compareFavourites(a, b) || compareUsername(a, b) 47 | ).select(["username"]).value(); 48 | 49 | console.log(value2); // "bar", "baz", "foo" 50 | 51 | // Test 3: First sort by number of favourites in DECENDING order, then sort by username 52 | const value3 = users.findMany({}).sort((a, b) => 53 | -compareFavourites(a, b) || compareUsername(a, b) 54 | ).select(["username"]).value(); 55 | 56 | console.log(value3); // "foo", "bar", "baz" 57 | 58 | db.drop(); 59 | -------------------------------------------------------------------------------- /example/with_oak.ts: -------------------------------------------------------------------------------- 1 | import { Application, Router } from "https://deno.land/x/oak@v6.3.1/mod.ts"; 2 | import { Document, FileDB } from "../mod.ts"; 3 | 4 | interface User extends Document { 5 | username?: string; 6 | } 7 | 8 | const db = new FileDB({ rootDir: "./data", isAutosave: true }); 9 | const users = await db.getCollection("users"); 10 | 11 | const router = new Router(); 12 | 13 | // GET /api/users: Get all users 14 | router.get("/api/users", (context) => { 15 | context.response.body = users.findMany({}); 16 | }); 17 | 18 | // GET /api/users/:id: Get one user by user ID 19 | router.get("/api/users/:id", (context) => { 20 | if (context?.params?.id) { 21 | context.response.body = users.findOne({ id: context.params.id }); 22 | return; 23 | } 24 | 25 | context.response.status = 400; 26 | context.response.body = { error: "id not provided" }; 27 | }); 28 | 29 | // POST /api/users: Create a user 30 | router.post("/api/users", async (context) => { 31 | const userInsert = context.request.body({ type: "json" }); 32 | const user = await users.insertOne(await userInsert.value); 33 | context.response.body = user; 34 | }); 35 | 36 | // PUT /api/users/:id: Update a user 37 | router.put("/api/users/:id", async (context) => { 38 | const userUpdate = context.request.body({ type: "json" }); 39 | if (context?.params?.id) { 40 | const user = await users.updateOne( 41 | { id: context.params.id }, 42 | await userUpdate.value, 43 | ); 44 | context.response.body = user; 45 | return; 46 | } 47 | 48 | context.response.status = 400; 49 | context.response.body = { error: "id not provided" }; 50 | }); 51 | 52 | // DELETE /api/users/:id: Delete a user 53 | router.delete("/api/users/:id", async (context) => { 54 | if (context?.params?.id) { 55 | const id = await users.deleteOne({ id: context.params.id }); 56 | context.response.body = { id }; 57 | return; 58 | } 59 | 60 | context.response.status = 400; 61 | context.response.body = { error: "id not provided" }; 62 | }); 63 | 64 | const app = new Application(); 65 | app.use(router.routes()); 66 | app.use(router.allowedMethods()); 67 | 68 | await app.listen({ port: 8000 }); 69 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/db.ts"; 2 | export * from "./src/collection.ts"; 3 | export * from "./src/types.ts"; 4 | export * from "./src/fsmanager.ts"; 5 | export * from "./src/dataset.ts"; 6 | -------------------------------------------------------------------------------- /src/collection.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from "../deps.ts"; 2 | import { Document, Selector, Updater } from "./types.ts"; 3 | import { FileSystemManager } from "./fsmanager.ts"; 4 | import { Dataset } from "./dataset.ts"; 5 | 6 | /** 7 | * A collection to store data 8 | * @property {string} name - collection name 9 | * @property {T[]} data - the collection data stored 10 | * @property {FileSystemManager} fsManager - the file system manager of database 11 | */ 12 | export class Collection { 13 | private name = ""; 14 | private data: T[] = []; 15 | private fsManager: FileSystemManager; 16 | 17 | /** 18 | * Ensure the collection file is existed. Read the initial 19 | * data from the file if the file existed. 20 | * 21 | * @constructor 22 | * @param {string} name - the Collection name 23 | */ 24 | constructor(name: string, fsManager: FileSystemManager) { 25 | this.name = name; 26 | this.fsManager = fsManager; 27 | } 28 | 29 | async init() { 30 | await this.fsManager.register(this.name); 31 | this.data = JSON.parse(await this.fsManager.read(this.name) || "[]"); 32 | return this.fsManager.write(this.name, this.data); 33 | } 34 | 35 | /** 36 | * Find some documents by using filter conditions 37 | * 38 | * @param selector filter conditions 39 | * @return the filtered documents 40 | */ 41 | findMany(selector: Selector) { 42 | if (selector instanceof Function) { 43 | return new Dataset(this.data.filter(selector)); 44 | } 45 | 46 | return new Dataset(this.data.filter((el) => { 47 | return Object.keys(selector).every((key) => { 48 | return selector[key as keyof T] === el[key as keyof T]; 49 | }); 50 | })); 51 | } 52 | /** 53 | * Find one document by using filter conditions 54 | * @param selector filter conditions 55 | * @return the filtered document 56 | */ 57 | findOne(selector: Selector) { 58 | return this.findMany(selector)?.value()?.[0]; 59 | } 60 | 61 | private insert(document: T) { 62 | document.createdAt = document.updatedAt = new Date(); 63 | document.id = uuid.generate(); 64 | 65 | while (this.data.find((t) => t.id === document.id)) { 66 | document.id = uuid.generate(); 67 | } 68 | 69 | this.data = [...this.data, document]; 70 | return this.findOne({ id: document.id } as T); 71 | } 72 | 73 | /** 74 | * Insert a document 75 | * 76 | * @param document the document to be inserted 77 | * @return the inserted document 78 | */ 79 | async insertOne(document: T) { 80 | const inserted = this.insert(document); 81 | await this.autosave(); 82 | return inserted; 83 | } 84 | 85 | /** 86 | * Bulk Insert 87 | * 88 | * @param documents an array of documents to be inserted 89 | * @return the inserted documents 90 | */ 91 | async insertMany(documents: T[]) { 92 | const inserted = documents.map((el) => this.insert(el)); 93 | await this.autosave(); 94 | return new Dataset(inserted); 95 | } 96 | 97 | private update(oldDocument: T, updater: Updater) { 98 | let updated: T; 99 | 100 | if (updater instanceof Function) { 101 | updated = updater(oldDocument); 102 | } else { 103 | updated = { 104 | ...oldDocument, 105 | ...updater, 106 | updatedAt: new Date(), 107 | }; 108 | } 109 | const index = this.data.findIndex(({ id }) => id === updated.id); 110 | this.data[index] = updated; 111 | return updated.id; 112 | } 113 | 114 | /** 115 | * Update a document 116 | * 117 | * @param selector filter condition of documents 118 | * @param updater the updated document attributes 119 | * @return the updated document 120 | */ 121 | async updateOne(selector: Selector, updater: Updater) { 122 | const selected = this.findOne(selector); 123 | const updatedId = this.update(selected, updater); 124 | await this.autosave(); 125 | 126 | return this.findOne({ id: updatedId } as T); 127 | } 128 | 129 | /** 130 | * Bulk Update 131 | * 132 | * @param {Selector} selector - filter condition of documents 133 | * @param {T} updater - the updated document attributes 134 | * @return the updated documents 135 | */ 136 | async updateMany(selector: Selector, updater: Updater) { 137 | const selected = this.findMany(selector); 138 | const updatedIds = selected.value().map((oldDocument: T) => 139 | this.update(oldDocument, updater) 140 | ); 141 | await this.autosave(); 142 | 143 | return new Dataset(updatedIds.map((id) => this.findOne({ id } as T))); 144 | } 145 | 146 | /** 147 | * Delete a document 148 | * 149 | * @param {Selector} selector Filter conditions of documents 150 | * @return {string} the deleted document ID 151 | */ 152 | async deleteOne(selector: Selector) { 153 | const document = this.findOne(selector); 154 | this.data = this.data.filter(({ id }) => id !== document.id); 155 | await this.autosave(); 156 | 157 | return document.id; 158 | } 159 | 160 | /** 161 | * Bulk delete 162 | * 163 | * @param {Selector} selector - Filter conditions of documents 164 | * @return {string[]} the deleted document IDs 165 | */ 166 | async deleteMany(selector: Selector) { 167 | const documents = this.findMany(selector).value(); 168 | this.data = this.data.filter((el) => !documents.includes(el)); 169 | await this.autosave(); 170 | 171 | return documents.map((t: T) => t.id); 172 | } 173 | 174 | /** 175 | * Save a copy of the data snapshot at this point to the file. 176 | */ 177 | async save() { 178 | return this.fsManager.write(this.name, this.data); 179 | } 180 | 181 | /** 182 | * Autosave data when inserting, updating and deleting data 183 | */ 184 | async autosave() { 185 | return this.fsManager.autowrite(this.name, this.data).catch((err) => 186 | Promise.resolve(err) 187 | ); 188 | } 189 | } 190 | 191 | export default Collection; 192 | -------------------------------------------------------------------------------- /src/collection_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, exists } from "../deps.ts"; 2 | import { Collection } from "./collection.ts"; 3 | import { FileSystemManager } from "./fsmanager.ts"; 4 | import { Document } from "./types.ts"; 5 | 6 | interface User extends Document { 7 | username?: string; 8 | favourites?: string[]; 9 | } 10 | 11 | const collection = new Collection("users", new FileSystemManager()); 12 | const path = "./db/users.json"; 13 | 14 | Deno.test("collection: init", async function () { 15 | await collection.init(); 16 | assertEquals(await exists(path), true); 17 | assertEquals(await Deno.readTextFile(path), "[]"); 18 | }); 19 | 20 | Deno.test("collection: findMany(empty)", function () { 21 | assertEquals(collection.findMany({}).value().length, 0); 22 | }); 23 | 24 | Deno.test("collection: findOne(empty)", function () { 25 | assertEquals(collection.findOne({}), undefined); 26 | }); 27 | 28 | Deno.test("collection: insertOne", async function () { 29 | await collection.insertOne( 30 | { username: "foo", favourites: ["🍎 Apple", "🍐 Pear"] }, 31 | ); 32 | assertEquals(collection.findMany({}).value().length, 1); 33 | assertEquals(collection.findMany({}).value()?.[0]?.username, "foo"); 34 | }); 35 | 36 | Deno.test("collection: findMany(not empty (function))", async function () { 37 | assertEquals( 38 | collection.findMany((el) => el.username === "foo").value().length, 39 | 1, 40 | ); 41 | }); 42 | 43 | Deno.test("collection: findOne(not empty (function))", async function () { 44 | assertEquals( 45 | collection.findOne((el) => el.username === "foo")?.username, 46 | "foo", 47 | ); 48 | }); 49 | 50 | Deno.test("collection: insertMany", async function () { 51 | await collection.insertMany( 52 | [ 53 | { username: "bar", favourites: ["🍌 Banana"] }, 54 | { username: "baz", favourites: ["🍌 Banana"] }, 55 | ], 56 | ); 57 | assertEquals(collection.findMany({}).value().length, 3); 58 | assertEquals( 59 | collection.findMany((el) => el.username?.includes("ba")).value().length, 60 | 2, 61 | ); 62 | }); 63 | 64 | Deno.test("collection: updateOne", async function () { 65 | await collection.updateOne( 66 | (el) => el.favourites?.[0] === "🍌 Banana", 67 | { favourites: ["🍎 Apple", "🍐 Pear"] }, 68 | ); 69 | assertEquals( 70 | collection.findMany((el) => el.favourites?.includes("🍎 Apple")).value().length, 71 | 2, 72 | ); 73 | }); 74 | 75 | Deno.test("collection: updateMany", async function () { 76 | await collection.updateMany( 77 | (el) => el.username?.includes("ba"), 78 | (el) => { 79 | el.favourites = ["🍉 Watermelon", ...(el.favourites || [])]; 80 | return el; 81 | }, 82 | ); 83 | console.log( 84 | collection.findMany((el) => el.favourites?.[0] === "🍉 Watermelon").value(), 85 | ); 86 | assertEquals( 87 | collection.findMany((el) => el.favourites?.[0] === "🍉 Watermelon").value() 88 | .length, 89 | 2, 90 | ); 91 | }); 92 | 93 | Deno.test("collection: deleteOne", async function () { 94 | await collection.deleteOne((el) => el.username?.includes("ba")); 95 | assertEquals(collection.findMany({}).value().length, 2); 96 | }); 97 | 98 | Deno.test("collection: deleteMany", async function () { 99 | await collection.deleteMany((el) => (el.favourites?.length ?? []) >= 1); 100 | assertEquals(collection.findMany({}).value().length, 0); 101 | }); 102 | -------------------------------------------------------------------------------- /src/dataset.ts: -------------------------------------------------------------------------------- 1 | import { CompareFn } from "./types.ts"; 2 | 3 | export class Dataset { 4 | private originalData: T[]; 5 | private newData: T[]; 6 | private isChainFinished = false; 7 | 8 | constructor(data: T[]) { 9 | this.originalData = [...data]; 10 | this.newData = [...data]; 11 | } 12 | 13 | retrieveData() { 14 | let data = [...this.newData]; 15 | 16 | if (this.isChainFinished) { 17 | this.isChainFinished = false; 18 | data = [...this.originalData]; 19 | } 20 | 21 | return data; 22 | } 23 | 24 | /** 25 | * Select which fields to retrieves 26 | * @param {(keyof T)[]} fields - field keys to retrieves 27 | */ 28 | select(fields: (keyof T)[]) { 29 | const data = this.retrieveData(); 30 | this.newData = data.map((el) => 31 | fields.reduce((obj, key) => ({ ...obj, [key]: el[key] }), {}) 32 | ) as T[]; 33 | return this; 34 | } 35 | 36 | /** 37 | * Sort the dataset 38 | * @param {CompareFn} compareFn - compare function tell how to sort 39 | */ 40 | sort(compareFn: CompareFn) { 41 | const data = this.retrieveData(); 42 | this.newData = data.sort(compareFn); 43 | return this; 44 | } 45 | 46 | /** 47 | * get the sorted and selected fields dataset 48 | * @return {T[]} required dataset 49 | */ 50 | value() { 51 | this.isChainFinished = true; 52 | return this.newData; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/dataset_test.ts: -------------------------------------------------------------------------------- 1 | import { Dataset } from "./dataset.ts"; 2 | import { assertEquals } from "../deps.ts"; 3 | 4 | const data = [ 5 | { 6 | username: "foo", 7 | favourites: ["🍎 Apple", "🍐 Pear"], 8 | }, 9 | { 10 | username: "baz", 11 | favourites: ["🍌 Banana"], 12 | }, 13 | { 14 | username: "bar", 15 | favourites: ["🍌 Banana"], 16 | }, 17 | ]; 18 | 19 | const dataset = new Dataset(data); 20 | 21 | Deno.test("dataset: 1", function () { 22 | const value = dataset.sort((a, b) => 23 | a.favourites.length - b.favourites.length 24 | ).select(["username"]).value(); 25 | assertEquals( 26 | value, 27 | [{ username: "baz" }, { username: "bar" }, { username: "foo" }], 28 | ); 29 | }); 30 | 31 | Deno.test("dataset: 2", function () { 32 | const value = dataset.sort((a, b) => 33 | a.favourites.length > b.favourites.length 34 | ? 1 35 | : (a.username > b.username ? 1 : -1) 36 | ).select(["username"]).value(); 37 | assertEquals( 38 | value, 39 | [{ username: "bar" }, { username: "baz" }, { username: "foo" }], 40 | ); 41 | }); 42 | 43 | Deno.test("dataset: 3", function () { 44 | const value = dataset.sort((a, b) => 45 | a.favourites.length < b.favourites.length 46 | ? 1 47 | : (a.username > b.username ? 1 : -1) 48 | ).select(["username"]).value(); 49 | assertEquals( 50 | value, 51 | [{ username: "foo" }, { username: "bar" }, { username: "baz" }], 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "./collection.ts"; 2 | import { Document, FileDBOptions } from "./types.ts"; 3 | import { FileSystemManager } from "./fsmanager.ts"; 4 | 5 | /** 6 | * The Database 7 | * @class 8 | * @property {Record>} collections - collections of database 9 | * @property {FileSystemManager} fsManager - the file system manager of database 10 | */ 11 | export class FileDB { 12 | private collections: Record> = {}; 13 | private fsManager: FileSystemManager; 14 | 15 | /** 16 | * @constructor 17 | * @param {FileDBOptions} dbOptions database options 18 | */ 19 | constructor(dbOptions?: FileDBOptions) { 20 | this.fsManager = new FileSystemManager(dbOptions); 21 | } 22 | 23 | /** 24 | * Get a collection 25 | * 26 | * @param {string} collectionName - Collection Name 27 | * @template T - a type extending Collection Model 28 | * @return the specified collection 29 | */ 30 | async getCollection(collectionName: string) { 31 | const isCollectionExist = this.collections?.[collectionName] ?? null; 32 | 33 | if (!isCollectionExist) { 34 | this.collections[collectionName] = new Collection( 35 | collectionName, 36 | this.fsManager, 37 | ); 38 | await this.collections[collectionName].init(); 39 | } 40 | 41 | return this.collections[collectionName] as Collection; 42 | } 43 | 44 | /** 45 | * Get all collection names 46 | * 47 | * @return an array containing collection names 48 | */ 49 | getCollectionNames() { 50 | return Object.keys(this.collections); 51 | } 52 | 53 | /** 54 | * Save all data of all collections 55 | */ 56 | async save() { 57 | return Promise.all( 58 | Object.values(this.collections).map((collection) => collection.save()), 59 | ); 60 | } 61 | 62 | /** 63 | * Drop the database 64 | * @param {boolean} silence - silently drop without console.log even cannot find the database 65 | */ 66 | async drop(silence = false) { 67 | return this.fsManager.deregister().catch((err) => { 68 | if (!silence) { 69 | console.error(err); 70 | } 71 | return Promise.resolve(err); 72 | }); 73 | } 74 | } 75 | 76 | export default FileDB; 77 | -------------------------------------------------------------------------------- /src/db_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, exists } from "../deps.ts"; 2 | import { FileDB } from "./db.ts"; 3 | import { Document } from "./types.ts"; 4 | 5 | interface User extends Document { 6 | username?: string; 7 | } 8 | 9 | const collectionName = "users"; 10 | const rootDirs = ["./db", "./data"]; 11 | const ext = ".json"; 12 | 13 | Deno.test("db: getCollection", async function () { 14 | const db = new FileDB(); 15 | const collection = await db.getCollection(collectionName); 16 | assertEquals(collection.findMany({}).value(), []); 17 | assertEquals(await exists(`${rootDirs[0]}/${collectionName + ext}`), true); 18 | }); 19 | 20 | Deno.test("db: getCollectionNames(empty)", async function () { 21 | const db = new FileDB(); 22 | const names = db.getCollectionNames(); 23 | assertEquals(names.length, 0); 24 | }); 25 | 26 | Deno.test("db: getCollectionNames(not empty)", async function () { 27 | const db = new FileDB(); 28 | await db.getCollection(collectionName); 29 | const names = db.getCollectionNames(); 30 | assertEquals(names.length, 1); 31 | assertEquals(names?.[0], collectionName); 32 | }); 33 | 34 | Deno.test("db: save", async function () { 35 | const db = new FileDB(); 36 | const users = await db.getCollection(collectionName); 37 | users.insertOne({ username: "foo" }); 38 | assertEquals( 39 | await Deno.readTextFile(`${rootDirs[0]}/${collectionName + ext}`), 40 | "[]", 41 | ); 42 | await db.save(); 43 | const rawData = await Deno.readTextFile( 44 | `${rootDirs[0]}/${collectionName + ext}`, 45 | ); 46 | const json = JSON.parse(rawData); 47 | assertEquals(json[0].username, "foo"); 48 | }); 49 | 50 | Deno.test("db: constructor(with rootDir)", async function () { 51 | const db = new FileDB({ rootDir: rootDirs[1] }); 52 | await db.getCollection(collectionName); 53 | assertEquals(await exists(`${rootDirs[1]}/${collectionName + ext}`), true); 54 | }); 55 | 56 | Deno.test("db: drop", async function () { 57 | const db1 = new FileDB({ rootDir: rootDirs[1] }); 58 | await db1.drop(); 59 | assertEquals(await exists(rootDirs[1]), false); 60 | const db2 = new FileDB(); 61 | await db2.drop(); 62 | assertEquals(await exists(rootDirs[0]), false); 63 | }); 64 | -------------------------------------------------------------------------------- /src/fsmanager.ts: -------------------------------------------------------------------------------- 1 | import { ensureFile, exists } from "../deps.ts"; 2 | import { FileDBOptions } from "./types.ts"; 3 | 4 | /** 5 | * The File System used to store the data. 6 | * @class 7 | * @property {string} rootDir - root directory of storing data 8 | * @property {boolean} isAutowrite - is autowrite when inserting, updating and deleting data 9 | * @property {string} collectionExt - the file extension of storing data 10 | */ 11 | export class FileSystemManager { 12 | private rootDir = "./db"; 13 | private isAutowrite = false; 14 | private collectionExt = ".json"; 15 | 16 | /** 17 | * @constructor 18 | * @param {FileDBOptions} dbOptions - Configuration when creating a database 19 | */ 20 | constructor(dbOptions?: FileDBOptions) { 21 | const { rootDir, isAutosave } = dbOptions ?? {}; 22 | this.rootDir = rootDir || this.rootDir; 23 | this.isAutowrite = isAutosave ?? this.isAutowrite; 24 | } 25 | 26 | /** 27 | * Get the collection file path by the provided collection name 28 | * @param {string} collectionName - the collection name 29 | */ 30 | getCollectionFilePath(collectionName: string) { 31 | return `${this.rootDir}/${collectionName + this.collectionExt}`; 32 | } 33 | 34 | /** 35 | * read text from the provided collection name 36 | * @param {string} collectionName - the collection name 37 | */ 38 | read(collectionName: string) { 39 | return Deno.readTextFile(this.getCollectionFilePath(collectionName)); 40 | } 41 | 42 | /** 43 | * Write data to file 44 | * @param {string} collectionName - the collection name 45 | * @param {T[]} data - the data to be written 46 | */ 47 | write(collectionName: string, data: T[]) { 48 | return Deno.writeTextFile( 49 | this.getCollectionFilePath(collectionName), 50 | JSON.stringify(data), 51 | ); 52 | } 53 | 54 | /** 55 | * Autowrite data to file 56 | * @param {string} collectionName - the collection name 57 | * @param {T[]} data - the data to be written 58 | */ 59 | autowrite(collectionName: string, data: T[]) { 60 | if (this.isAutowrite) { 61 | return this.write(collectionName, data); 62 | } 63 | 64 | return Promise.reject( 65 | "Data has not been written as autosave is not enabled", 66 | ); 67 | } 68 | 69 | /** 70 | * register a collection 71 | * @param {string} collectionName - the collection name 72 | */ 73 | register(collectionName: string) { 74 | return ensureFile(this.getCollectionFilePath(collectionName)); 75 | } 76 | 77 | /** 78 | * deregister a collection 79 | * @param {string} collectionName - the collection name 80 | */ 81 | async deregister(collectionName?: string) { 82 | if (collectionName) { 83 | const collectionFile = this.getCollectionFilePath(collectionName); 84 | if (await exists(collectionFile)) { 85 | return Deno.remove(collectionFile); 86 | } 87 | 88 | return Promise.reject( 89 | `File "${this.getCollectionFilePath(collectionName)}" does not exists`, 90 | ); 91 | } 92 | 93 | if (await exists(this.rootDir)) { 94 | return Deno.remove(this.rootDir, { recursive: true }); 95 | } 96 | 97 | return Promise.reject(`Directory "${this.rootDir}" does not exists`); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/fsmanager_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, exists } from "../deps.ts"; 2 | import { FileSystemManager } from "./fsmanager.ts"; 3 | 4 | const fs = new FileSystemManager(); 5 | const collectionName = "users"; 6 | const dir = "./db"; 7 | 8 | Deno.test("fs: getInstance", async function () { 9 | assertEquals(fs instanceof FileSystemManager, true); 10 | }); 11 | 12 | Deno.test("fs: deregister(dir (not exists))", async function () { 13 | await fs.deregister().catch((err) => 14 | assertEquals(err, `Directory "${dir}" does not exists`) 15 | ); 16 | assertEquals(await exists("${dir}"), false); 17 | }); 18 | 19 | Deno.test("fs: getCollectionFile", function () { 20 | assertEquals( 21 | fs.getCollectionFilePath(collectionName), 22 | `${dir}/${collectionName}.json`, 23 | ); 24 | }); 25 | 26 | Deno.test("fs: register", async function () { 27 | await fs.register(collectionName); 28 | assertEquals(await exists(fs.getCollectionFilePath(collectionName)), true); 29 | }); 30 | 31 | Deno.test("fs: read", async function () { 32 | const data = await fs.read(collectionName); 33 | assertEquals(data, ""); 34 | }); 35 | 36 | Deno.test("fs: write", async function () { 37 | await fs.write(collectionName, [{ username: "foo" }]); 38 | const data = await fs.read(collectionName); 39 | assertEquals(data, '[{"username":"foo"}]'); 40 | }); 41 | 42 | Deno.test("fs: autowrite(false)", async function () { 43 | await fs.write(collectionName, []); 44 | await fs.autowrite(collectionName, [{ username: "foo" }]).catch(( 45 | err, 46 | ) => 47 | assertEquals(err, "Data has not been written as autosave is not enabled") 48 | ); 49 | const data = await fs.read(collectionName); 50 | assertEquals(data, "[]"); 51 | }); 52 | 53 | Deno.test("fs: autowrite(true)", async function () { 54 | const fs2 = new FileSystemManager({ isAutosave: true }); 55 | await fs2.write(collectionName, []); 56 | await fs2.autowrite(collectionName, [{ username: "foo" }]).catch(( 57 | err, 58 | ) => 59 | assertEquals(err, "Data has not been written as autosave is not enabled") 60 | ); 61 | const data = await fs.read(collectionName); 62 | assertEquals(data, '[{"username":"foo"}]'); 63 | }); 64 | 65 | Deno.test("fs: deregister(file (not exists))", async function () { 66 | const notExists = "not-exists"; 67 | await fs.deregister(notExists).catch((err) => 68 | assertEquals(err, `File "${dir}/${notExists}.json" does not exists`) 69 | ); 70 | assertEquals(await exists(fs.getCollectionFilePath(notExists)), false); 71 | }); 72 | 73 | Deno.test("fs: deregister(file (exists))", async function () { 74 | await fs.deregister(collectionName); 75 | assertEquals(await exists(fs.getCollectionFilePath(collectionName)), false); 76 | assertEquals(await exists(dir), true); 77 | }); 78 | 79 | Deno.test("fs: deregister(dir (exists))", async function () { 80 | await fs.deregister(); 81 | assertEquals(await exists(dir), false); 82 | }); 83 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * the database model skeleton 3 | * @property {string} id - the document ID 4 | * @property {Date} createdAt - the document created time 5 | * @property {Date} updatedAt - the document updated time 6 | */ 7 | export interface Document { 8 | id?: string; 9 | createdAt?: Date; 10 | updatedAt?: Date; 11 | } 12 | 13 | /** 14 | * Configuration when creating a database 15 | * @property {string} rootDir - root directory for saving data 16 | * @property {boolean} isAutosave - is autosave when inserting, updating and deleting data 17 | */ 18 | export interface FileDBOptions { 19 | rootDir?: string; 20 | isAutosave?: boolean; 21 | } 22 | 23 | /** 24 | * Selector finding a document 25 | * @typedef {T | ((document: T) => boolean | undefined)} Selector 26 | */ 27 | export type Selector = T | ((document: T) => boolean | undefined); 28 | 29 | /** 30 | * Updater updating a document 31 | * @typedef {T | ((document: T) => T)} Updater 32 | */ 33 | export type Updater = T | ((document: T) => T); 34 | 35 | /** 36 | * A compare function used to sort found documents 37 | * @typedef {(a: T, b: T) => number} CompareFn 38 | */ 39 | export type CompareFn = (a: T, b: T) => number; 40 | --------------------------------------------------------------------------------