├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── docs-deploy.yml │ ├── eslint.yml │ ├── npm-publish.yml │ └── stale.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ └── db.spec.ts ├── babel.config.js ├── docs └── config.yml ├── examples ├── quickdb-driver.js └── quickmongo.js ├── jest.config.ts ├── jsdoc.json ├── package.json ├── scripts └── docgen.js ├── src ├── Database.ts ├── MongoDriver.ts ├── Util.ts ├── collection.ts └── index.ts ├── tsconfig.eslint.json ├── tsconfig.json └── tsup.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .github/ 4 | docs/ 5 | test/ 6 | coverage/ 7 | 8 | *.d.ts -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint" 9 | ], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended" 13 | ], 14 | "rules": { 15 | "@typescript-eslint/explicit-module-boundary-types": "off", 16 | "@typescript-eslint/no-unused-vars": "error", 17 | "@typescript-eslint/no-explicit-any": "warn", 18 | "@typescript-eslint/ban-ts-comment": "error", 19 | "semi": "error", 20 | "no-console": "error", 21 | "eqeqeq": "error", 22 | "no-bitwise": "error", 23 | "no-eq-null": "error", 24 | "no-empty": "error" 25 | } 26 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: "27 22 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: ["javascript"] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v1 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v1 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v1 -------------------------------------------------------------------------------- /.github/workflows/docs-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | - '!docs' 7 | tags: 8 | - '*' 9 | jobs: 10 | docs: 11 | name: Documentation 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@master 16 | 17 | - name: Install Node v17 18 | uses: actions/setup-node@master 19 | with: 20 | node-version: 17 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Build and deploy documentation 26 | uses: discordjs/action-docs@v1 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: 8 | - '*' 9 | jobs: 10 | test: 11 | name: ESLint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Install Node v17 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 17 21 | 22 | - name: Install dependencies 23 | run: yarn install 24 | 25 | - name: Run ESLint 26 | run: yarn lint -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPMJS 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | 9 | publish-npm: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 17 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm install 18 | - run: npm run build 19 | - run: npm publish --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 22 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v4 11 | with: 12 | stale-issue-message: "This issue is stale because it has been open 20 days with no activity. Remove stale label or comment or this will be closed in 3 days" 13 | days-before-stale: 20 14 | days-before-close: 3 15 | exempt-issue-labels: "bug,enhancement" 16 | exempt-issue-assignees: true 17 | exempt-pr-assignees: true 18 | stale-issue-label: "stale" 19 | stale-pr-label: "stale" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | test/ 5 | .vscode/ 6 | yarn.lock 7 | package-lock.json 8 | globalConfig.json 9 | docs/typedoc.json 10 | docs/docs.json -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run format && npm run lint:fix -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | test/ 3 | docs/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "trailingComma": "none", 5 | "printWidth": 280 6 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | contact@snowflakedev.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Hello! 2 | This document is for people who want to contribute to this project! 3 | 4 | # Code Style 5 | ## Linters 6 | We are using ESLint to lint the code. You should run `npm run lint` and fix the errors (if any) before pushing. 7 | 8 | ## Formatting 9 | We are using **[Prettier](https://prettier.io)** to format the code. You can run `npm run format` to do so. 10 | 11 | ## File names 12 | - Always use `PascalCase` for the files containing classes. 13 | - Always use `lowercase` name for other files. 14 | 15 | ## Some Rules 16 | - Use `camelCase` for `Function names`, `Variables`, etc. and `PascalCase` for `Class name` 17 | - Do not make unused variables/imports 18 | - Don't forget to write `JSDOC` 19 | - Always write function return type: 20 | ```ts 21 | const sum = (): number => 2 + 2; 22 | ``` 23 | 24 | - Use English language 25 | 26 | # Pull Requests 27 | - Do not create unnecessary pull requests 28 | - Use English language 29 | - Properly specify your changes. Example: 30 | 31 | ``` 32 | ⚡ | Update some method 33 | 34 | Now something runs smoothly and provides best performance 35 | ``` 36 | 37 | - Run tests, formatting, etc. before making Pull Requests 38 | - Always use **[Conventional Commit Messages](https://ccm.snowflakedev.org)** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Plexi Development 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 | ## ![QuickMongo Logo](https://www.plexidev.org/quickmongo.png) 2 | 3 | QuickMongo is a beginner-friendly and feature-rich wrapper for MongoDB that allows you to use Quick.db's Key-Value based syntax. 4 | 5 | # Installing 6 | 7 | ```bash 8 | $ npm install --save quickmongo 9 | 10 | OR 11 | $ yarn add quickmongo 12 | ``` 13 | 14 | # Documentation 15 | **[https://quickmongo.js.org](https://quickmongo.js.org)** 16 | 17 | # Features 18 | - Beginner friendly 19 | - Asynchronous 20 | - Dot notation support 21 | - Key-Value like interface 22 | - Easy to use 23 | - TTL (temporary storage) supported 24 | - Provides quick.db compatible API 25 | - MongoDriver for quick.db 26 | 27 | # Example 28 | 29 | ## QuickMongo 30 | 31 | ```js 32 | import { Database } from "quickmongo"; 33 | 34 | const db = new Database("mongodb://localhost:27017/quickmongo"); 35 | 36 | db.on("ready", () => { 37 | console.log("Connected to the database"); 38 | doStuff(); 39 | }); 40 | 41 | // top-level awaits 42 | await db.connect(); 43 | 44 | async function doStuff() { 45 | // Setting an object in the database: 46 | await db.set("userInfo", { difficulty: "Easy" }); 47 | // -> { difficulty: 'Easy' } 48 | 49 | // Pushing an element to an array (that doesn't exist yet) in an object: 50 | await db.push("userInfo.items", "Sword"); 51 | // -> { difficulty: 'Easy', items: ['Sword'] } 52 | 53 | // Adding to a number (that doesn't exist yet) in an object: 54 | await db.add("userInfo.balance", 500); 55 | // -> { difficulty: 'Easy', items: ['Sword'], balance: 500 } 56 | 57 | // Repeating previous examples: 58 | await db.push("userInfo.items", "Watch"); 59 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 500 } 60 | await db.add("userInfo.balance", 500); 61 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 1000 } 62 | 63 | // Fetching individual properties 64 | await db.get("userInfo.balance"); // -> 1000 65 | await db.get("userInfo.items"); // -> ['Sword', 'Watch'] 66 | 67 | // remove item 68 | await db.pull("userInfo.items", "Sword"); 69 | // -> { difficulty: 'Easy', items: ['Watch'], balance: 1000 } 70 | 71 | // set the data and automatically delete it after 1 minute 72 | await db.set("foo", "bar", 60); // 60 seconds = 1 minute 73 | 74 | // fetch the temporary data after a minute 75 | setTimeout(async () => { 76 | await db.get("foo"); // null 77 | }, 60_000); 78 | } 79 | ``` 80 | 81 | ## Usage with quick.db 82 | 83 | ```js 84 | const { QuickDB } = require("quick.db"); 85 | // get mongo driver 86 | const { MongoDriver } = require("quickmongo"); 87 | const driver = new MongoDriver("mongodb://localhost/quickdb"); 88 | 89 | driver.connect().then(() => { 90 | console.log(`Connected to the database!`); 91 | init(); 92 | }); 93 | 94 | async function init() { 95 | // use quickdb with mongo driver 96 | // make sure this part runs after connecting to mongodb 97 | const db = new QuickDB({ driver }); 98 | 99 | // self calling async function just to get async 100 | // Setting an object in the database: 101 | console.log(await db.set("userInfo", { difficulty: "Easy" })); 102 | // -> { difficulty: 'Easy' } 103 | 104 | // Getting an object from the database: 105 | console.log(await db.get("userInfo")); 106 | // -> { difficulty: 'Easy' } 107 | 108 | // Getting an object property from the database: 109 | console.log(await db.get("userInfo.difficulty")); 110 | // -> 'Easy' 111 | 112 | // Pushing an element to an array (that doesn't exist yet) in an object: 113 | console.log(await db.push("userInfo.items", "Sword")); 114 | // -> { difficulty: 'Easy', items: ['Sword'] } 115 | 116 | // Adding to a number (that doesn't exist yet) in an object: 117 | console.log(await db.add("userInfo.balance", 500)); 118 | // -> { difficulty: 'Easy', items: ['Sword'], balance: 500 } 119 | 120 | // Repeating previous examples: 121 | console.log(await db.push("userInfo.items", "Watch")); 122 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 500 } 123 | console.log(await db.add("userInfo.balance", 500)); 124 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 1000 } 125 | 126 | // Fetching individual properties 127 | console.log(await db.get("userInfo.balance")); // -> 1000 128 | console.log(await db.get("userInfo.items")); // ['Sword', 'Watch'] 129 | 130 | // disconnect from the database 131 | await driver.close(); 132 | } 133 | ``` 134 | 135 | **Maintained by Plexi Development** 136 | 137 | # Discord Support 138 | **[Plexi Development](https://discord.gg/plexidev)** 139 | 140 | **Acquired from Archaeopteryx on 10/02/2022** 141 | -------------------------------------------------------------------------------- /__tests__/db.spec.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../src"; 2 | 3 | describe("test database", () => { 4 | const db = new Database<{ 5 | difficulty: string; 6 | items: string[]; 7 | balance: number; 8 | }>("mongodb://localhost/quickmongo"); 9 | 10 | beforeAll(async () => { 11 | await db.connect(); 12 | }, 10000); 13 | 14 | afterAll(async () => { 15 | await db.deleteAll(); 16 | await db.close(); 17 | }, 10000); 18 | 19 | test("it should pass quick.db test", async () => { 20 | // Setting an object in the database: 21 | await db.set("userInfo", { difficulty: "Easy" }); 22 | // -> { difficulty: 'Easy' } 23 | 24 | // Pushing an element to an array (that doesn't exist yet) in an object: 25 | await db.push("userInfo.items", "Sword"); 26 | // -> { difficulty: 'Easy', items: ['Sword'] } 27 | 28 | // Adding to a number (that doesn't exist yet) in an object: 29 | await db.add("userInfo.balance", 500); 30 | // -> { difficulty: 'Easy', items: ['Sword'], balance: 500 } 31 | 32 | // Repeating previous examples: 33 | await db.push("userInfo.items", "Watch"); 34 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 500 } 35 | await db.add("userInfo.balance", 500); 36 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 1000 } 37 | 38 | // Fetching individual properties 39 | await db.get("userInfo.balance"); // -> 1000 40 | await db.get("userInfo.items"); // -> ['Sword', 'Watch'] 41 | 42 | const res = await db.get("userInfo"); 43 | expect(res).toStrictEqual({ difficulty: "Easy", items: ["Sword", "Watch"], balance: 1000 }); 44 | }); 45 | 46 | it("should pull item", async () => { 47 | const res = (await db.pull("userInfo.items", "Watch")) as { 48 | difficulty: string; 49 | items: string[]; 50 | balance: number; 51 | }; 52 | 53 | expect(res.items).toStrictEqual(["Sword"]); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"] 3 | }; -------------------------------------------------------------------------------- /docs/config.yml: -------------------------------------------------------------------------------- 1 | - name: General 2 | files: 3 | - name: Welcome 4 | id: welcome 5 | path: ../../README.md 6 | -------------------------------------------------------------------------------- /examples/quickdb-driver.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const { QuickDB } = require("quick.db"); 4 | const { MongoDriver } = require("quickmongo"); 5 | const driver = new MongoDriver("mongodb://localhost/quickdb"); 6 | 7 | driver.connect().then(() => { 8 | console.log(`Connected to the database!`); 9 | init(); 10 | }); 11 | 12 | async function init() { 13 | const db = new QuickDB({ driver }); 14 | 15 | await db.deleteAll(); 16 | 17 | // self calling async function just to get async 18 | // Setting an object in the database: 19 | console.log(await db.set("userInfo", { difficulty: "Easy" })); 20 | // -> { difficulty: 'Easy' } 21 | 22 | // Getting an object from the database: 23 | console.log(await db.get("userInfo")); 24 | // -> { difficulty: 'Easy' } 25 | 26 | // Getting an object property from the database: 27 | console.log(await db.get("userInfo.difficulty")); 28 | // -> 'Easy' 29 | 30 | // Pushing an element to an array (that doesn't exist yet) in an object: 31 | console.log(await db.push("userInfo.items", "Sword")); 32 | // -> { difficulty: 'Easy', items: ['Sword'] } 33 | 34 | // Adding to a number (that doesn't exist yet) in an object: 35 | console.log(await db.add("userInfo.balance", 500)); 36 | // -> { difficulty: 'Easy', items: ['Sword'], balance: 500 } 37 | 38 | // Repeating previous examples: 39 | console.log(await db.push("userInfo.items", "Watch")); 40 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 500 } 41 | console.log(await db.add("userInfo.balance", 500)); 42 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 1000 } 43 | 44 | // Fetching individual properties 45 | console.log(await db.get("userInfo.balance")); // -> 1000 46 | console.log(await db.get("userInfo.items")); // ['Sword', 'Watch'] 47 | 48 | await driver.close(); 49 | } -------------------------------------------------------------------------------- /examples/quickmongo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { Database } from "quickmongo"; 4 | 5 | const db = new Database("mongodb://localhost/quickmongo"); 6 | 7 | db.on("ready", () => { 8 | console.log("Connected to the database"); 9 | doStuff(); 10 | }); 11 | 12 | // top-level awaits 13 | await db.connect(); 14 | 15 | async function doStuff() { 16 | // Setting an object in the database: 17 | await db.set("userInfo", { difficulty: "Easy" }); 18 | // -> { difficulty: 'Easy' } 19 | 20 | // Pushing an element to an array (that doesn't exist yet) in an object: 21 | await db.push("userInfo.items", "Sword"); 22 | // -> { difficulty: 'Easy', items: ['Sword'] } 23 | 24 | // Adding to a number (that doesn't exist yet) in an object: 25 | await db.add("userInfo.balance", 500); 26 | // -> { difficulty: 'Easy', items: ['Sword'], balance: 500 } 27 | 28 | // Repeating previous examples: 29 | await db.push("userInfo.items", "Watch"); 30 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 500 } 31 | await db.add("userInfo.balance", 500); 32 | // -> { difficulty: 'Easy', items: ['Sword', 'Watch'], balance: 1000 } 33 | 34 | // Fetching individual properties 35 | await db.get("userInfo.balance"); // -> 1000 36 | await db.get("userInfo.items"); // -> ['Sword', 'Watch'] 37 | 38 | // remove item 39 | await db.pull("userInfo.items", "Sword"); 40 | // -> { difficulty: 'Easy', items: ['Watch'], balance: 1000 } 41 | 42 | // set the data and automatically delete it after 1 minute 43 | await db.set("foo", "bar", 60); // 60 seconds = 1 minute 44 | 45 | // fetch the temporary data after a minute 46 | setTimeout(async () => { 47 | await db.get("foo"); // null 48 | }, 60_000); 49 | } -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "C:\\Users\\User\\AppData\\Local\\Temp\\jest", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "\\\\node_modules\\\\" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: "babel", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | preset: "@shelf/jest-mongodb", 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "\\\\node_modules\\\\" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jest-circus/runner", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "\\\\node_modules\\\\", 180 | // "\\.pnp\\.[^\\\\]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | verbose: true, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "includePattern": ".+\\.ts(doc|x)?$" 4 | }, 5 | "plugins": ["plugins/markdown", "node_modules/jsdoc-babel"], 6 | "babel": { 7 | "extensions": ["ts"], 8 | "babelrc": false, 9 | "presets": [["@babel/preset-env", { "targets": { "node": true } }], "@babel/preset-typescript"] 10 | } 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickmongo", 3 | "version": "5.2.0", 4 | "description": "Quick Mongodb wrapper for beginners that provides key-value based interface.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | "require": "./dist/index.js", 10 | "import": "./dist/index.mjs" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "docs": "typedoc --json docs/typedoc.json src/index.ts && node scripts/docgen.js", 17 | "dev": "cd test && ts-node demo.ts", 18 | "build": "tsup", 19 | "build:check": "tsc --noEmit --incremental false", 20 | "lint": "eslint src --ext .ts", 21 | "lint:fix": "eslint src --ext .ts --fix", 22 | "format": "prettier --write src/**/*.{ts,js,json,yaml,yml} __tests__/**/*.{ts,js,json,yaml,yml}", 23 | "prepare": "husky install", 24 | "test": "jest" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/plexidev/quickmongo.git" 29 | }, 30 | "keywords": [ 31 | "quickmongo", 32 | "mongodb", 33 | "mongoose", 34 | "schema", 35 | "api", 36 | "database", 37 | "quick.db", 38 | "enmap", 39 | "endb" 40 | ], 41 | "author": "Plexi Development ", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/plexidev/quickmongo/issues" 45 | }, 46 | "homepage": "https://github.com/plexidev/quickmongo#readme", 47 | "contributors": [ 48 | "Archaeopteryx1 ", 49 | "Zyrouge ", 50 | "DevSynth ", 51 | "Zorotic " 52 | ], 53 | "devDependencies": { 54 | "@babel/cli": "^7.14.8", 55 | "@babel/core": "^7.15.0", 56 | "@babel/preset-env": "^7.15.0", 57 | "@babel/preset-typescript": "^7.15.0", 58 | "@discordjs/ts-docgen": "^0.4.1", 59 | "@shelf/jest-mongodb": "^2.0.3", 60 | "@types/jest": "^27.0.1", 61 | "@types/lodash": "^4.14.178", 62 | "@types/node": "^16.7.10", 63 | "@typescript-eslint/eslint-plugin": "^4.30.0", 64 | "@typescript-eslint/parser": "^4.30.0", 65 | "eslint": "^7.32.0", 66 | "husky": "^7.0.2", 67 | "jest": "^27.1.0", 68 | "jsdoc-babel": "^0.5.0", 69 | "prettier": "^2.3.2", 70 | "quick.db": "^9.0.6", 71 | "rimraf": "^3.0.2", 72 | "ts-node": "^10.2.1", 73 | "tsup": "^5.11.11", 74 | "typedoc": "^0.23.14", 75 | "typescript": "^4.4.2" 76 | }, 77 | "dependencies": { 78 | "lodash": "^4.17.21", 79 | "mongoose": "^6.6.1", 80 | "tiny-typed-emitter": "^2.1.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /scripts/docgen.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { runGenerator } = require("@discordjs/ts-docgen"); 3 | 4 | runGenerator({ 5 | existingOutput: "docs/typedoc.json", 6 | custom: "docs/config.yml", 7 | output: "docs/docs.json" 8 | }); -------------------------------------------------------------------------------- /src/Database.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import modelSchema, { CollectionInterface } from "./collection"; 3 | import { TypedEmitter } from "tiny-typed-emitter"; 4 | import { Util } from "./Util"; 5 | import _ from "lodash"; 6 | 7 | export interface QuickMongoOptions extends mongoose.ConnectOptions { 8 | /** 9 | * Collection name to use 10 | */ 11 | collectionName?: string; 12 | /** 13 | * If it should be created as a child db 14 | */ 15 | child?: boolean; 16 | /** 17 | * Parent db 18 | */ 19 | parent?: Database; 20 | /** 21 | * If it should share connection from parent db 22 | */ 23 | shareConnectionFromParent?: boolean; 24 | /** 25 | * If it should connect automatically 26 | */ 27 | autoConnect?: boolean; 28 | } 29 | 30 | export interface AllQueryOptions { 31 | /** 32 | * The query limit 33 | */ 34 | limit?: number; 35 | /** 36 | * Sort by 37 | */ 38 | sort?: string; 39 | /** 40 | * Query filter 41 | */ 42 | filter?: (data: AllData) => boolean; 43 | } 44 | 45 | export interface AllData { 46 | /** 47 | * The id 48 | */ 49 | ID: string; 50 | /** 51 | * The data associated with a particular ID 52 | */ 53 | data: T; 54 | } 55 | 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 57 | export type DocType = mongoose.Document> & 58 | CollectionInterface & { 59 | _id: mongoose.Types.ObjectId; 60 | }; 61 | 62 | export interface QmEvents { 63 | /** 64 | * The `ready` event 65 | */ 66 | ready: (db: Database) => unknown; 67 | /** 68 | * The `connecting` event 69 | */ 70 | connecting: () => unknown; 71 | /** 72 | * The `connected` event 73 | */ 74 | connected: () => unknown; 75 | /** 76 | * The `open` event 77 | */ 78 | open: () => unknown; 79 | /** 80 | * The `disconnecting` event 81 | */ 82 | disconnecting: () => unknown; 83 | /** 84 | * The `disconnected` event 85 | */ 86 | disconnected: () => unknown; 87 | /** 88 | * The `close` event 89 | */ 90 | close: () => unknown; 91 | /** 92 | * The `reconnected` event 93 | */ 94 | reconnected: () => unknown; 95 | /** 96 | * The `error` event 97 | */ 98 | error: (error: Error) => unknown; 99 | /** 100 | * The `fullsetup` event 101 | */ 102 | fullsetup: () => unknown; 103 | /** 104 | * The `all` event 105 | */ 106 | all: () => unknown; 107 | /** 108 | * The `reconnectFailed` event 109 | */ 110 | reconnectFailed: () => unknown; 111 | } 112 | 113 | /** 114 | * The Database constructor 115 | * @extends {EventEmitter} 116 | */ 117 | export class Database extends TypedEmitter> { 118 | public connection: mongoose.Connection; 119 | public parent: Database = null; 120 | private __child__ = false; 121 | // eslint-disable-next-line @typescript-eslint/ban-types 122 | public model: mongoose.Model, {}, {}, {}> = null; 123 | 124 | /** 125 | * Creates new quickmongo instance 126 | * @param url The database url 127 | * @param options The database options 128 | */ 129 | public constructor(public url: string, public options: QuickMongoOptions = {}) { 130 | super(); 131 | 132 | Object.defineProperty(this, "__child__", { 133 | writable: true, 134 | enumerable: false, 135 | configurable: true 136 | }); 137 | 138 | if (this.options.autoConnect) this.connect(); 139 | } 140 | 141 | /** 142 | * If this is a child database 143 | */ 144 | public isChild() { 145 | return !this.isParent(); 146 | } 147 | 148 | /** 149 | * If this is a parent database 150 | */ 151 | public isParent() { 152 | return !this.__child__; 153 | } 154 | 155 | /** 156 | * If the database is ready 157 | */ 158 | public get ready() { 159 | return this.model && this.connection ? true : false; 160 | } 161 | 162 | /** 163 | * Database ready state 164 | */ 165 | public get readyState() { 166 | return this.connection?.readyState ?? 0; 167 | } 168 | 169 | /** 170 | * Get raw document 171 | * @param key The key 172 | */ 173 | public async getRaw(key: string): Promise> { 174 | this.__readyCheck(); 175 | const doc = await this.model.findOne({ 176 | ID: Util.getKey(key) 177 | }); 178 | 179 | // return null if the doc has expired 180 | // mongodb task runs every 60 seconds therefore expired docs may exist during that timeout 181 | // this check fixes that issue and returns null if the doc has expired 182 | // letting mongodb take care of data deletion in the background 183 | if (!doc || (doc.expireAt && doc.expireAt.getTime() - Date.now() <= 0)) { 184 | return null; 185 | } 186 | 187 | return doc; 188 | } 189 | 190 | /** 191 | * Get item from the database 192 | * @param key The key 193 | */ 194 | public async get(key: string): Promise { 195 | const res = await this.getRaw(key); 196 | const formatted = this.__formatData(res); 197 | return Util.pick(formatted, key) as unknown as V; 198 | } 199 | 200 | /** 201 | * Get item from the database 202 | * @param key The key 203 | */ 204 | public async fetch(key: string): Promise { 205 | return await this.get(key); 206 | } 207 | 208 | /** 209 | * Set item in the database 210 | * @param key The key 211 | * @param value The value 212 | * @param [expireAfterSeconds=-1] if specified, quickmongo deletes this data after specified seconds. 213 | * Leave it blank or set it to `-1` to make it permanent. 214 | * Data may still persist for a minute even after the data is supposed to be expired! 215 | * Data may persist for a minute even after expiration due to the nature of mongodb. QuickMongo makes sure to never return expired 216 | * documents even if it's not deleted. 217 | * @example // permanent 218 | * await db.set("foo", "bar"); 219 | * 220 | * // delete the record after 1 minute 221 | * await db.set("foo", "bar", 60); // time in seconds (60 seconds = 1 minute) 222 | */ 223 | public async set(key: string, value: T | unknown, expireAfterSeconds = -1): Promise { 224 | this.__readyCheck(); 225 | if (!key.includes(".")) { 226 | await this.model.findOneAndUpdate( 227 | { 228 | ID: key 229 | }, 230 | { 231 | $set: Util.shouldExpire(expireAfterSeconds) 232 | ? { 233 | data: value, 234 | expireAt: Util.createDuration(expireAfterSeconds * 1000) 235 | } 236 | : { data: value } 237 | }, 238 | { upsert: true } 239 | ); 240 | 241 | return await this.get(key); 242 | } else { 243 | const keyMetadata = Util.getKeyMetadata(key); 244 | const existing = await this.model.findOne({ ID: keyMetadata.master }); 245 | if (!existing) { 246 | await this.model.create( 247 | Util.shouldExpire(expireAfterSeconds) 248 | ? { 249 | ID: keyMetadata.master, 250 | data: _.set({}, keyMetadata.target, value), 251 | expireAt: Util.createDuration(expireAfterSeconds * 1000) 252 | } 253 | : { 254 | ID: keyMetadata.master, 255 | data: _.set({}, keyMetadata.target, value) 256 | } 257 | ); 258 | 259 | return await this.get(key); 260 | } 261 | 262 | if (existing.data !== null && typeof existing.data !== "object") throw new Error("CANNOT_TARGET_NON_OBJECT"); 263 | 264 | const prev = Object.assign({}, existing.data); 265 | const newData = _.set(prev, keyMetadata.target, value); 266 | 267 | await existing.updateOne({ 268 | $set: Util.shouldExpire(expireAfterSeconds) 269 | ? { 270 | data: newData, 271 | expireAt: Util.createDuration(expireAfterSeconds * 1000) 272 | } 273 | : { 274 | data: newData 275 | } 276 | }); 277 | 278 | return await this.get(keyMetadata.master); 279 | } 280 | } 281 | 282 | /** 283 | * Returns false if the value is nullish, else true 284 | * @param key The key 285 | */ 286 | public async has(key: string) { 287 | const data = await this.get(key); 288 | // eslint-disable-next-line eqeqeq, no-eq-null 289 | return data != null; 290 | } 291 | 292 | /** 293 | * Deletes item from the database 294 | * @param key The key 295 | */ 296 | public async delete(key: string) { 297 | this.__readyCheck(); 298 | const keyMetadata = Util.getKeyMetadata(key); 299 | if (!keyMetadata.target) { 300 | const removed = await this.model.deleteOne({ 301 | ID: keyMetadata.master 302 | }); 303 | 304 | return removed.deletedCount > 0; 305 | } 306 | 307 | const existing = await this.model.findOne({ ID: keyMetadata.master }); 308 | if (!existing) return false; 309 | if (existing.data !== null && typeof existing.data !== "object") throw new Error("CANNOT_TARGET_NON_OBJECT"); 310 | const prev = Object.assign({}, existing.data); 311 | _.unset(prev, keyMetadata.target); 312 | await existing.updateOne({ 313 | $set: { 314 | data: prev 315 | } 316 | }); 317 | return true; 318 | } 319 | 320 | /** 321 | * Delete all data from this database 322 | */ 323 | public async deleteAll() { 324 | const res = await this.model.deleteMany(); 325 | return res.deletedCount > 0; 326 | } 327 | 328 | /** 329 | * Get the document count in this database 330 | */ 331 | public async count() { 332 | return await this.model.estimatedDocumentCount(); 333 | } 334 | 335 | /** 336 | * The database latency in ms 337 | */ 338 | public async ping() { 339 | if (!this.model) return NaN; 340 | if (typeof performance !== "undefined") { 341 | const initial = performance.now(); 342 | await this.get("SOME_RANDOM_KEY"); 343 | return performance.now() - initial; 344 | } else { 345 | const initial = Date.now(); 346 | await this.get("SOME_RANDOM_KEY"); 347 | return Date.now() - initial; 348 | } 349 | } 350 | 351 | /** 352 | * Create a child database, either from new connection or current connection (similar to quick.db table) 353 | * @param collection The collection name (defaults to `JSON`) 354 | * @param url The database url (not needed if the child needs to share connection from parent) 355 | * @example const child = await db.instantiateChild("NewCollection"); 356 | * console.log(child.all()); 357 | */ 358 | public async instantiateChild(collection?: string, url?: string): Promise> { 359 | const childDb = new Database(url || this.url, { 360 | ...this.options, 361 | child: true, 362 | // @ts-expect-error assign parent 363 | parent: this, 364 | collectionName: collection, 365 | shareConnectionFromParent: !!url || true 366 | }); 367 | 368 | const ndb = await childDb.connect(); 369 | return ndb; 370 | } 371 | 372 | /** 373 | * Identical to quick.db table constructor 374 | * @example const table = new db.table("table"); 375 | * table.set("foo", "Bar"); 376 | */ 377 | public get table() { 378 | return new Proxy( 379 | function () { 380 | /* noop */ 381 | } as unknown as TableConstructor, 382 | { 383 | construct: (_, args) => { 384 | return this.useCollection(args[0]); 385 | }, 386 | apply: (_, _thisArg, args) => { 387 | return this.useCollection(args[0]); 388 | } 389 | } 390 | ); 391 | } 392 | 393 | /** 394 | * Use specified collection. Alias of `db.table` 395 | * @param name The collection name 396 | */ 397 | public useCollection(name: string) { 398 | if (!name || typeof name !== "string") throw new TypeError("Invalid collection name"); 399 | const db = new Database(this.url, this.options); 400 | 401 | db.connection = this.connection; 402 | // @ts-expect-error assign collection 403 | db.model = modelSchema(this.connection, name); 404 | db.connect = () => Promise.resolve(db); 405 | 406 | return db; 407 | } 408 | 409 | /** 410 | * Returns everything from the database 411 | * @param options The request options 412 | */ 413 | public async all(options?: AllQueryOptions) { 414 | this.__readyCheck(); 415 | const everything = await this.model.find({}); 416 | let arb = everything 417 | .filter((v) => !(v.expireAt && v.expireAt.getTime() - Date.now() <= 0)) 418 | .filter((v) => options?.filter?.({ ID: v.ID, data: v.data }) ?? true) 419 | .map((m) => ({ 420 | ID: m.ID, 421 | data: this.__formatData(m) 422 | })) as AllData[]; 423 | 424 | if (typeof options?.sort === "string") { 425 | if (options.sort.startsWith(".")) options.sort = options.sort.slice(1); 426 | const pref = options.sort.split("."); 427 | arb = _.sortBy(arb, pref).reverse(); 428 | } 429 | 430 | return typeof options?.limit === "number" && options.limit > 0 ? arb.slice(0, options.limit) : arb; 431 | } 432 | 433 | /** 434 | * Drops this database 435 | */ 436 | public async drop() { 437 | this.__readyCheck(); 438 | return await this.model.collection.drop(); 439 | } 440 | 441 | /** 442 | * Identical to quick.db push 443 | * @param key The key 444 | * @param value The value or array of values 445 | */ 446 | public async push(key: string, value: unknown | unknown[]) { 447 | const data = await this.get(key); 448 | // eslint-disable-next-line eqeqeq, no-eq-null 449 | if (data == null) { 450 | if (!Array.isArray(value)) return await this.set(key, [value]); 451 | return await this.set(key, value); 452 | } 453 | if (!Array.isArray(data)) throw new Error("TARGET_EXPECTED_ARRAY"); 454 | if (Array.isArray(value)) return await this.set(key, data.concat(value)); 455 | data.push(value); 456 | return await this.set(key, data); 457 | } 458 | 459 | /** 460 | * Opposite of push, used to remove item 461 | * @param key The key 462 | * @param value The value or array of values 463 | */ 464 | public async pull(key: string, value: unknown | unknown[] | ((data: unknown) => boolean), multiple = true): Promise { 465 | let data = (await this.get(key)) as T[]; 466 | // eslint-disable-next-line eqeqeq, no-eq-null 467 | if (data == null) return false; 468 | if (!Array.isArray(data)) throw new Error("TARGET_EXPECTED_ARRAY"); 469 | if (typeof value === "function") { 470 | // @ts-expect-error apply fn 471 | data = data.filter(value); 472 | return await this.set(key, data); 473 | } else if (Array.isArray(value)) { 474 | data = data.filter((i) => !value.includes(i)); 475 | return await this.set(key, data); 476 | } else { 477 | if (multiple) { 478 | data = data.filter((i) => i !== value); 479 | return await this.set(key, data); 480 | } else { 481 | const hasItem = data.some((x) => x === value); 482 | if (!hasItem) return false; 483 | const index = data.findIndex((x) => x === value); 484 | data = data.splice(index, 1); 485 | return await this.set(key, data); 486 | } 487 | } 488 | } 489 | 490 | /** 491 | * Identical to quick.db unshift 492 | * @param key The key 493 | * @param value The value 494 | */ 495 | public async unshift(key: string, value: unknown | unknown[]) { 496 | let arr = await this.getArray(key); 497 | Array.isArray(value) ? (arr = value.concat(arr)) : arr.unshift(value as T); 498 | return await this.set(key, arr); 499 | } 500 | 501 | /** 502 | * Identical to quick.db shift 503 | * @param key The key 504 | */ 505 | public async shift(key: string) { 506 | const arr = await this.getArray(key); 507 | const removed = arr.shift(); 508 | await this.set(key, arr); 509 | return removed; 510 | } 511 | 512 | /** 513 | * Identical to quick.db pop 514 | * @param key The key 515 | */ 516 | public async pop(key: string) { 517 | const arr = await this.getArray(key); 518 | const removed = arr.pop(); 519 | await this.set(key, arr); 520 | return removed; 521 | } 522 | 523 | /** 524 | * Identical to quick.db startsWith 525 | * @param query The query 526 | */ 527 | public async startsWith(query: string) { 528 | return this.all({ 529 | filter(data) { 530 | return data.ID.startsWith(query); 531 | } 532 | }); 533 | } 534 | 535 | /** 536 | * Identical to startsWith but checks the ending 537 | * @param query The query 538 | */ 539 | public async endsWith(query: string) { 540 | return this.all({ 541 | filter(data) { 542 | return data.ID.endsWith(query); 543 | } 544 | }); 545 | } 546 | 547 | /** 548 | * Identical to quick.db add 549 | * @param key The key 550 | * @param value The value 551 | */ 552 | public async add(key: string, value: number) { 553 | if (typeof value !== "number") throw new TypeError("VALUE_MUST_BE_NUMBER"); 554 | const val = await this.get(key); 555 | return await this.set(key, (typeof val === "number" ? val : 0) + value); 556 | } 557 | 558 | /** 559 | * Identical to quick.db subtract 560 | * @param key The key 561 | * @param value The value 562 | */ 563 | public async subtract(key: string, value: number) { 564 | if (typeof value !== "number") throw new TypeError("VALUE_MUST_BE_NUMBER"); 565 | const val = await this.get(key); 566 | return await this.set(key, (typeof val === "number" ? val : 0) - value); 567 | } 568 | 569 | /** 570 | * Identical to quick.db sub 571 | * @param key The key 572 | * @param value The value 573 | */ 574 | public async sub(key: string, value: number) { 575 | return this.subtract(key, value); 576 | } 577 | 578 | /** 579 | * Identical to quick.db addSubtract 580 | * @param key The key 581 | * @param value The value 582 | * @param [sub=false] If the operation should be subtraction 583 | */ 584 | public async addSubtract(key: string, value: number, sub = false) { 585 | if (sub) return this.subtract(key, value); 586 | return this.add(key, value); 587 | } 588 | 589 | /** 590 | * Identical to quick.db getArray 591 | * @param key The key 592 | */ 593 | public async getArray(key: string): Promise { 594 | const data = await this.get(key); 595 | if (!Array.isArray(data)) { 596 | throw new TypeError(`Data type of key "${key}" is not array`); 597 | } 598 | 599 | return data; 600 | } 601 | 602 | /** 603 | * Connects to the database. 604 | */ 605 | public connect(): Promise> { 606 | return new Promise>((resolve, reject) => { 607 | if (typeof this.url !== "string" || !this.url) return reject(new Error("MISSING_MONGODB_URL")); 608 | 609 | this.__child__ = Boolean(this.options.child); 610 | this.parent = (this.options.parent as Database) || null; 611 | const collectionName = this.options.collectionName; 612 | const shareConnectionFromParent = !!this.options.shareConnectionFromParent; 613 | 614 | delete this.options["collectionName"]; 615 | delete this.options["child"]; 616 | delete this.options["parent"]; 617 | delete this.options["shareConnectionFromParent"]; 618 | delete this.options["autoConnect"]; 619 | 620 | if (shareConnectionFromParent && this.__child__ && this.parent) { 621 | if (!this.parent.connection) return reject(new Error("PARENT_HAS_NO_CONNECTION")); 622 | this.connection = this.parent.connection; 623 | // @ts-expect-error assign model 624 | this.model = modelSchema(this.connection, Util.v(collectionName, "string", "JSON")); 625 | return resolve(this); 626 | } 627 | 628 | mongoose.createConnection(this.url, this.options, (err, connection) => { 629 | if (err) return reject(err); 630 | this.connection = connection; 631 | // @ts-expect-error assign model 632 | this.model = modelSchema(this.connection, Util.v(collectionName, "string", "JSON")); 633 | this.emit("ready", this); 634 | this.__applyEventsBinding(); 635 | resolve(this); 636 | }); 637 | }); 638 | } 639 | 640 | /** 641 | * Watch collection changes 642 | */ 643 | public watch() { 644 | this.__readyCheck(); 645 | const stream = this.model.watch(); 646 | return stream; 647 | } 648 | 649 | /** 650 | * The db metadata 651 | */ 652 | public get metadata() { 653 | if (!this.model) return null; 654 | return { 655 | name: this.model.collection.name, 656 | db: this.model.collection.dbName, 657 | namespace: this.model.collection.namespace 658 | }; 659 | } 660 | 661 | /** 662 | * Returns database statistics 663 | */ 664 | public async stats() { 665 | this.__readyCheck(); 666 | const stats = await this.model.collection.stats(); 667 | return stats; 668 | } 669 | 670 | /** 671 | * Close the database connection 672 | * @param force Close forcefully 673 | */ 674 | public async close(force = false) { 675 | return await this.connection.close(force); 676 | } 677 | 678 | private __applyEventsBinding() { 679 | this.__readyCheck(); 680 | const events = ["connecting", "connected", "open", "disconnecting", "disconnected", "close", "reconnected", "error", "fullsetup", "all", "reconnectFailed", "reconnectTries"]; 681 | 682 | for (const event of events) { 683 | this.connection.on(event, (...args) => { 684 | // @ts-expect-error event forwarder 685 | this.emit(event, ...args); 686 | }); 687 | } 688 | } 689 | 690 | /** 691 | * Formats document data 692 | * @param doc The document 693 | */ 694 | private __formatData(doc: DocType) { 695 | return doc?.data ?? null; 696 | } 697 | 698 | /** 699 | * Checks if the database is ready 700 | */ 701 | private __readyCheck() { 702 | if (!this.model) throw new Error("[DATABASE_NOT_READY] Use db.connect()"); 703 | } 704 | } 705 | 706 | export interface TableConstructor { 707 | new (name: string): Database; 708 | } 709 | -------------------------------------------------------------------------------- /src/MongoDriver.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import modelSchema from "./collection"; 3 | 4 | export interface IDriver { 5 | prepare(table: string): Promise; 6 | getAllRows(table: string): Promise<{ id: string; value: any }[]>; 7 | getRowByKey(table: string, key: string): Promise<[T | null, boolean]>; 8 | setRowByKey(table: string, key: string, value: any, update: boolean): Promise; 9 | deleteAllRows(table: string): Promise; 10 | deleteRowByKey(table: string, key: string): Promise; 11 | } 12 | 13 | /** 14 | * Quick.db compatible mongo driver 15 | * @example // require quickdb 16 | * const { QuickDB } = require("quick.db"); 17 | * // require mongo driver from quickmongo 18 | * const { MongoDriver } = require("quickmongo"); 19 | * // create mongo driver 20 | * const driver = new MongoDriver("mongodb://localhost/quickdb"); 21 | * 22 | * // connect to mongodb 23 | * await driver.connect(); 24 | * 25 | * // create quickdb instance with mongo driver 26 | * const db = new QuickDB({ driver }); 27 | * 28 | * // set something 29 | * await db.set("foo", "bar"); 30 | * 31 | * // get something 32 | * console.log(await db.get("foo")); // -> foo 33 | */ 34 | export class MongoDriver implements IDriver { 35 | public connection: mongoose.Connection; 36 | private models = new Map>(); 37 | 38 | public constructor(public url: string, public options: mongoose.ConnectOptions = {}) {} 39 | 40 | public connect(): Promise { 41 | // eslint-disable-next-line 42 | return new Promise(async (resolve, reject) => { 43 | mongoose.createConnection(this.url, this.options, (err, connection) => { 44 | if (err) return reject(err); 45 | this.connection = connection; 46 | resolve(this); 47 | }); 48 | }); 49 | } 50 | 51 | public close(force?: boolean) { 52 | return this.connection?.close(force); 53 | } 54 | 55 | private checkConnection() { 56 | // eslint-disable-next-line 57 | if (this.connection == null) throw new Error(`MongoDriver is not connected to the database`); 58 | } 59 | 60 | public async prepare(table: string) { 61 | this.checkConnection(); 62 | if (!this.models.has(table)) this.models.set(table, modelSchema(this.connection, table)); 63 | } 64 | 65 | private async getModel(name: string) { 66 | await this.prepare(name); 67 | return this.models.get(name); 68 | } 69 | 70 | async getAllRows(table: string): Promise<{ id: string; value: any }[]> { 71 | this.checkConnection(); 72 | const model = await this.getModel(table); 73 | return (await model.find()).map((row: any) => ({ 74 | id: row.ID, 75 | value: row.data 76 | })); 77 | } 78 | 79 | async getRowByKey(table: string, key: string): Promise<[T | null, boolean]> { 80 | this.checkConnection(); 81 | const model: any = await this.getModel(table); 82 | // wtf quickdb? 83 | const res = await model.find({ ID: key }); 84 | return res.map((m: any) => m.data); 85 | } 86 | 87 | async setRowByKey(table: string, key: string, value: any, update: boolean): Promise { 88 | this.checkConnection(); 89 | const model = await this.getModel(table); 90 | void update; 91 | await model.findOneAndUpdate( 92 | { 93 | ID: key 94 | }, 95 | { 96 | $set: { data: value } 97 | }, 98 | { upsert: true } 99 | ); 100 | 101 | return value; 102 | } 103 | 104 | async deleteAllRows(table: string): Promise { 105 | this.checkConnection(); 106 | const model = await this.getModel(table); 107 | const res = await model.deleteMany(); 108 | return res.deletedCount; 109 | } 110 | 111 | async deleteRowByKey(table: string, key: string): Promise { 112 | this.checkConnection(); 113 | const model = await this.getModel(table); 114 | 115 | const res = await model.deleteMany({ 116 | ID: key 117 | }); 118 | return res.deletedCount; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Util.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | /** 4 | * The util class 5 | * @extends {null} 6 | */ 7 | export class Util extends null { 8 | /** 9 | * This is a static class, do not instantiate 10 | */ 11 | private constructor() { 12 | /* noop */ 13 | } 14 | 15 | /* eslint-disable @typescript-eslint/no-explicit-any */ 16 | 17 | /** 18 | * Validate 19 | * @param {any} k The source 20 | * @param {string} type The type 21 | * @param {?any} fallback The fallback value 22 | * @returns {any} 23 | */ 24 | public static v(k: any, type: string, fallback?: any) { 25 | return typeof k === type && !!k ? k : fallback; 26 | } 27 | 28 | /** 29 | * Picks from nested object by dot notation 30 | * @param {any} holder The source 31 | * @param {?string} id The prop to get 32 | * @returns {any} 33 | */ 34 | public static pick(holder: any, id?: string) { 35 | if (!holder || typeof holder !== "object") return holder; 36 | if (!id || typeof id !== "string" || !id.includes(".")) return holder; 37 | const keysMeta = Util.getKeyMetadata(id); 38 | return _.get(Object.assign({}, holder), keysMeta.target); 39 | } 40 | /* eslint-enable @typescript-eslint/no-explicit-any */ 41 | 42 | /** 43 | * Returns master key 44 | * @param {string} key The key that may have dot notation 45 | * @returns {string} 46 | */ 47 | public static getKey(key: string) { 48 | return key.split(".").shift(); 49 | } 50 | 51 | /** 52 | * Returns key metadata 53 | * @param {string} key The key 54 | * @returns {KeyMetadata} 55 | */ 56 | public static getKeyMetadata(key: string) { 57 | const [master, ...child] = key.split("."); 58 | return { 59 | master, 60 | child, 61 | target: child.join(".") 62 | }; 63 | } 64 | 65 | /** 66 | * Utility to validate duration 67 | * @param {number} dur The duration 68 | * @returns {boolean} 69 | */ 70 | public static shouldExpire(dur: number) { 71 | if (typeof dur !== "number") return false; 72 | if (dur > Infinity || dur <= 0 || Number.isNaN(dur)) return false; 73 | return true; 74 | } 75 | 76 | public static createDuration(dur: number) { 77 | if (!Util.shouldExpire(dur)) return null; 78 | const duration = new Date(Date.now() + dur); 79 | return duration; 80 | } 81 | } 82 | 83 | /** 84 | * @typedef {Object} KeyMetadata 85 | * @property {string} master The master key 86 | * @property {string[]} child The child keys 87 | * @property {string} target The child as target key 88 | */ 89 | -------------------------------------------------------------------------------- /src/collection.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | export interface CollectionInterface { 4 | ID: string; 5 | data: T; 6 | createdAt: Date; 7 | updatedAt: Date; 8 | expireAt?: Date; 9 | } 10 | 11 | export const docSchema = new mongoose.Schema( 12 | { 13 | ID: { 14 | type: mongoose.SchemaTypes.String, 15 | required: true, 16 | unique: true 17 | }, 18 | data: { 19 | type: mongoose.SchemaTypes.Mixed, 20 | required: false 21 | }, 22 | expireAt: { 23 | type: mongoose.SchemaTypes.Date, 24 | required: false, 25 | default: null 26 | } 27 | }, 28 | { 29 | timestamps: true 30 | } 31 | ); 32 | 33 | export default function modelSchema(connection: mongoose.Connection, modelName = "JSON") { 34 | // @ts-expect-error docSchema 35 | const model = connection.model>(modelName, docSchema); 36 | model.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 }).catch(() => { 37 | /* void */ 38 | }); 39 | return model; 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./collection"; 2 | export * from "./Database"; 3 | export * from "./Util"; 4 | export * from "./MongoDriver"; 5 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "**/*.ts", 5 | "**/*.js" 6 | ], 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "strictNullChecks": false, 10 | "resolveJsonModule": false, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "skipDefaultLibCheck": true 14 | }, 15 | "include": ["./src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | dts: true, 6 | entryPoints: ["src/index.ts"], 7 | outDir: "dist", 8 | format: ["esm", "cjs"], 9 | minify: false, 10 | skipNodeModulesBundle: true, 11 | sourcemap: false, 12 | target: "ES2015" 13 | }); --------------------------------------------------------------------------------