From 90e06ec556e1f1023f566d44f935b86140e22bca Mon Sep 17 00:00:00 2001 From: Mark Maher <100785809+Marco5dev@users.noreply.github.com> Date: Wed, 5 Jun 2024 01:09:19 +0300 Subject: [PATCH] v2.1 Fixed Bugs and more Enhances (#26) * v2.1 Fixed Bugs and more Enhances * Update yaml.adapter.ts * fixed the error --------- Co-authored-by: ANAS799 <156859738+ANAS799@users.noreply.github.com> --- .github/workflows/changlog.yaml | 35 + CHANGELOG.md | 38 +- README.md | 2 + SECURITY.md | 4 +- bun.lockb | Bin 0 -> 165046 bytes jest.config.js | 1 + package-lock.json | 103 +- package.json | 22 +- src/adapters/json.adapter.ts | 1495 ++++++++++------------- src/adapters/sql.adapter.ts | 8 +- src/adapters/yaml.adapter.ts | 1505 +++++++++++------------- src/core/connect.ts | 270 +++-- src/core/{ => functions}/backup.ts | 0 src/core/{ => functions}/logger.ts | 46 +- src/core/functions/operations.ts | 788 +++++++++++++ src/core/functions/schema.ts | 273 +++++ src/core/{ => functions}/secureData.ts | 127 +- src/core/schema.ts | 180 --- src/index.ts | 41 +- src/lib/date.ts | 24 +- src/types/adapter.ts | 56 +- src/types/connect.ts | 69 +- src/types/versedb.types.ts | 29 +- tests/json.test.ts | 249 ++++ tests/versedb.test.ts | 511 -------- tests/yaml.test.ts | 249 ++++ yarn.lock | 75 +- 27 files changed, 3419 insertions(+), 2781 deletions(-) create mode 100644 .github/workflows/changlog.yaml create mode 100644 bun.lockb rename src/core/{ => functions}/backup.ts (100%) rename src/core/{ => functions}/logger.ts (63%) create mode 100644 src/core/functions/operations.ts create mode 100644 src/core/functions/schema.ts rename src/core/{ => functions}/secureData.ts (77%) delete mode 100644 src/core/schema.ts create mode 100644 tests/json.test.ts delete mode 100644 tests/versedb.test.ts create mode 100644 tests/yaml.test.ts diff --git a/.github/workflows/changlog.yaml b/.github/workflows/changlog.yaml new file mode 100644 index 0000000..9852dfa --- /dev/null +++ b/.github/workflows/changlog.yaml @@ -0,0 +1,35 @@ +name: Update Changelog + +on: + push: + tags: [ v*.* ] # Triggers on pushing tags starting with "v" + +jobs: + update-changelog: + runs-on: ubuntu-latest + environment: production + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Get previous tag + id: previousTag + run: | + name=$(git --no-pager tag --sort=creatordate --merged ${{ github.ref_name }} | tail -2 | head -1) + echo "previousTag: $name" + echo "previousTag=$name" >> $GITHUB_ENV + + - name: Update CHANGELOG + id: changelog + uses: requarks/changelog-action@v1 + with: + token: ${{ github.token }} + fromTag: ${{ github.ref_name }} + toTag: ${{ env.previousTag }} + writeToFile: true + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 # Commit the updated changelog + with: + file_pattern: CHANGELOG.md + commit_message: 'Update CHANGELOG.md for ${{ github.ref_name }} [skip ci]' diff --git a/CHANGELOG.md b/CHANGELOG.md index ee33150..7757963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,39 @@ -# Verse.db +# Verse.db [Beta] ### Change log: +## Version v2.1 + +- Enhanced JSON and YAML adapter supporting complex queries and more helper operations. +- Enhanced update & updateMany & loadAll & find & remove & search & moveData. +- Remodelled Schema to support nested data. +- Recoded batchTasks. +- Added Aggregate method in JSON and YAML and adapter. +- Fixed Types. +- Fixed dropData. +- Fixed search. + ## Version v2.0 -- Added real-time data store, which's uses db.watch('dataname') +- Added real-time data store, whcihs uses db.watch('dataname') - Added more operations for each adapter, such as: [batchTasks, dataSize, docCount, search, join]. - Fixed Minor bugs in Connection and types. -- Added back the uniqueKeys for schemeless data. -- Remodeled the Schema for JSON and YAML: use SchemaTypes.String or "String". +- Added back the uniqueKeys for schemaless data. +- Remodelled the Schema for Json and Yaml: use SchemaTypes.String or "String". - Changed Security into optional setting and non required. - Added secrets.env to store your keys safely and not to be lost. -- Made `npm create verse.db@latest` for easier setup and configuration for your data connection. -- Added .config folder in dataPath to save your secrets keys for secure. +- Made `npm create verse.db@latest` for easier setup and configuration for your data connection, - Added More options, and filters for find and load all data. -- Added Move Data for JSON and YAML. now you can move specific query or full data from place to another. -- Added Functionality to remove secure from specific files and store them into their original files. -- File extensions became viewable and can be `json`, `yaml`, and `sql`. +- Added Move Data for json and yaml. now you can move specific query or full data from place to another. +- Added Functionality to remove secure from specifc files and store them into their original files. +- Fixed Bugs in update and updateMany functionality for JSON and YAML adapter. +- Fixed logger became optional. +- Updated SecureData functionality for SQL. +- Improved Schema to have ability to make trees schema. +- Added .config folder in dataPath to save your secrets keys for secure. +- Added more methods for all adapters. +- Enhanced older methods. +- Encryption changed to secure and became optional. ## Version 1.1 @@ -37,6 +54,7 @@ ### Change log: - Converting the database from `JavaScript` to `TypeScript` +- - Setup `xlsx` to the database - Setup `csv` to the database - Setup `SQL` to the database @@ -46,6 +64,6 @@ ## Contributors: -- @marco5dev +- @Marco5dev - @kmoshax - @ANAS \ No newline at end of file diff --git a/README.md b/README.md index 91510ef..6334ae7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Unlock the potential of your data with Verse.db, the premier data management too - **Performance-Driven**: Experience lightning-fast performance for all your data operations. - **Real-Time Data Store**: Harness the power of real-time data storage for instantaneous updates and access to your data. Keep your applications synchronized and up-to-date with the latest information. - **Logging System for Developers**: Streamline your development process with Verse.db's built-in logging system. Gain insights into your application's behavior and track changes effectively. Debugging and troubleshooting become effortless with detailed logs at your disposal. +- **Support for Complex Queries**: Effortlessly execute complex queries with Verse.db's advanced query capabilities. Utilize powerful filtering, sorting, and aggregation functionalities to extract valuable insights from your data with ease. - **User-Friendly Interface**: Enjoy an intuitive and easy-to-use interface that simplifies data management tasks for developers of all levels. Whether you're a seasoned professional or a beginner, Verse.db ensures a smooth and seamless experience. - **Continuous Improvement**: Benefit from regular updates and enhancements to ensure Verse.db stays ahead of the curve. Our dedicated team is committed to delivering the best-in-class data management solution tailored to your needs. @@ -82,3 +83,4 @@ For detailed information on usage, operations, and methods, visit [Verse.db Docu ### Soon: SQOL - In the future updates we will introduce our new brand database SQOL: (Structured Query Object Language). Stay tuned ;). +- Check it out its structure on [Git-Hub](https://github.com/jedi-studio/). \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 0fc60e0..a40ab3a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ We are currently supporting the following versions of the `verse.db` package: -- 1.x -- 2.x (now) +- 1.0 ... 1.1 +- 1.1 (now) ## Reporting a Vulnerability diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..02dd858908879c5bc3fbe8e6a6a3fd525d09c561 GIT binary patch literal 165046 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" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2407,17 +2391,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2520,14 +2493,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2788,25 +2753,6 @@ "node": ">=8" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -2835,19 +2781,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4001,25 +3934,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4242,12 +4156,12 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.0.tgz", + "integrity": "sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -4388,11 +4302,6 @@ "node": ">= 6" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 4d6e7c3..cd72dbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "verse.db", - "version": "2.0.1", + "version": "2.1.0", "description": "verse.db isn't just a database, it's your universal data bridge. Designed for unmatched flexibility, security, and performance, verse.db empowers you to manage your data with ease.", "license": "MIT", "author": "marco5dev (Mark Maher)", @@ -27,7 +27,7 @@ "SECURITY.md" ], "scripts": { - "build": "tsc", + "build": "tsc --build --force", "test": "jest --forceExit" }, "funding": "https://github.com/sponsors/jedi-studio", @@ -41,7 +41,6 @@ "typescript": "^5.4.2" }, "dependencies": { - "axios": "^1.6.8", "yaml": "^2.4.1" }, "keywords": [ @@ -50,8 +49,21 @@ "relational database", "non-relational database", "sql", - "SQON", - "sqon", + "verse", + "verse.db", + "versedb", + "verse.data", + "verse data", + "verseDatabase", + "verse database", + "verse manager", + "data", + "data management", + "database manager", + "data manager", + "database management", + "SQOL", + "sqol", "nosql", "data schema", "data model", diff --git a/src/adapters/json.adapter.ts b/src/adapters/json.adapter.ts index 71f5f9e..eb003b9 100644 --- a/src/adapters/json.adapter.ts +++ b/src/adapters/json.adapter.ts @@ -1,24 +1,32 @@ import fs from "fs"; import path from "path"; import { EventEmitter } from "events"; -import { logError, logInfo, logSuccess } from "../core/logger"; +import { logError, logInfo, logSuccess } from "../core/functions/logger"; import { randomUUID } from "../lib/id"; import { AdapterResults, AdapterUniqueKey, - versedbAdapter, + JsonYamlAdapter, CollectionFilter, SearchResult, queryOptions, + operationKeys } from "../types/adapter"; import { DevLogsOptions, AdapterSetting } from "../types/adapter"; -import { decodeJSON, encodeJSON } from "../core/secureData"; +import { decodeJSON, encodeJSON } from "../core/functions/secureData"; import { nearbyOptions, SecureSystem } from "../types/connect"; - -export class jsonAdapter extends EventEmitter implements versedbAdapter { +import { opSet, opInc, opPush, opUnset, opPull, opRename, opAddToSet, opMin, opMax, opMul, opBit, opCurrentDate, opPop, opSlice, opSort } from "../core/functions/operations"; + +type AggregationExpression = { + $sum?: string; + $avg?: string; + // Add other aggregation operators as needed +}; +export class jsonAdapter extends EventEmitter implements JsonYamlAdapter { public devLogs: DevLogsOptions = { enable: false, path: "" }; public secure: SecureSystem = { enable: false, secret: "" }; public dataPath: string | undefined; + private indexes: Map> = new Map(); constructor(options: AdapterSetting, key: SecureSystem) { super(); @@ -125,9 +133,8 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { options: AdapterUniqueKey = {} ): Promise { try { - const loaded: any = (await this.load(dataname)) || []; + const loaded: any = (await this.load(dataname)); let currentData: any = loaded.results; - if (typeof currentData === "undefined") { logError({ content: `Error loading data.`, @@ -170,16 +177,14 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { insertedIds.push(insertedId); }); - let data; - if (this.secure.enable) { - data = await encodeJSON(currentData, this.secure.secret); + const encodedData = await encodeJSON(flattenedNewData, this.secure.secret); + fs.appendFileSync(dataname, encodedData); } else { - data = JSON.stringify(currentData); + const data = JSON.stringify(currentData, null, 2); + fs.writeFileSync(dataname, data); } - fs.writeFileSync(dataname, data); - logSuccess({ content: "Data has been added", devLogs: this.devLogs, @@ -206,107 +211,242 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { }; } } - - private indexes: Map> = new Map(); - + private async index(dataname: string): Promise { if (!this.indexes.has(dataname)) { - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - const indexMap = new Map(); - currentData.forEach((item: any, index: any) => { - Object.keys(item).forEach((key) => { - const value = item[key]; - if (!indexMap.has(key)) { - indexMap.set(key, []); - } - indexMap.get(key)?.push(index); + const loaded: any = (await this.load(dataname)); + let currentData: any = loaded.results; + const indexMap = new Map(); + currentData.forEach((item: any, index: any) => { + Object.keys(item).forEach((key) => { + const value = item[key]; + if (!indexMap.has(key)) { + indexMap.set(key, []); + } + indexMap.get(key)?.push(index); + }); }); - }); - this.indexes.set(dataname, indexMap); + this.indexes.set(dataname, indexMap); } } - async find(dataname: string, query: any): Promise { - try { - if (!query) { - logError({ - content: "Query isn't provided.", - devLogs: this.devLogs, - }); - return { - acknowledged: false, - results: null, - errorMessage: "Query isn't provided.", - }; - } + private getValueByPath(obj: any, path: string): any { + return path.split('.').reduce((acc, part) => { + const match = part.match(/(\w+)\[(\d+)\]/); + if (match) { + const [, key, index] = match; + return acc?.[key]?.[index]; + } else { + return acc?.[part]; + } + }, obj); + } - await this.index(dataname); - const indexMap = this.indexes.get(dataname); - if (!indexMap) { - return { - acknowledged: true, - results: null, - message: "No data found matching your query.", - }; - } + private matchesQuery(item: any, query: any): boolean { + for (const key of Object.keys(query)) { + const queryValue = query[key]; + let itemValue = this.getValueByPath(item, key); - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - const candidateIndexes = Object.keys(query) - .map( - (key) => - indexMap - .get(key) - ?.filter((idx) => currentData[idx][key] === query[key]) || [] - ) - .flat(); - - for (const idx of candidateIndexes) { - const item = currentData[idx]; - let match = true; - for (const key of Object.keys(query)) { - if (item[key] !== query[key]) { - match = false; - break; - } + if (typeof queryValue === 'object') { + if (queryValue.$regex && typeof itemValue === 'string') { + const regex = new RegExp(queryValue.$regex); + if (!regex.test(itemValue)) { + return false; + } + } else if (queryValue.$some) { + if (Array.isArray(itemValue)) { + if (itemValue.length === 0) { + return false; + } + } else if (typeof itemValue === 'object' && itemValue !== null) { + if (Object.keys(itemValue).length === 0) { + return false; + } + } else { + return false; + } + } else if (queryValue.$gt !== undefined && typeof itemValue === 'number') { + if (itemValue <= queryValue.$gt) { + return false; + } + } else if (queryValue.$lt !== undefined && typeof itemValue === 'number') { + if (itemValue >= queryValue.$lt) { + return false; + } + } else if (queryValue.$exists !== undefined) { + const exists = itemValue !== undefined; + if (exists !== queryValue.$exists) { + return false; + } + } else if (queryValue.$in && Array.isArray(queryValue.$in)) { + if (!queryValue.$in.includes(itemValue)) { + return false; + } + } else if (queryValue.$not && typeof queryValue.$not === 'object') { + if (this.matchesQuery(item, { [key]: queryValue.$not })) { + return false; + } + } else if (queryValue.$elemMatch && Array.isArray(itemValue)) { + if (!itemValue.some((elem: any) => this.matchesQuery(elem, queryValue.$elemMatch))) { + return false; + } + } else if (queryValue.$typeOf && typeof queryValue.$typeOf === 'string') { + const expectedType = queryValue.$typeOf.toLowerCase(); + const actualType = typeof itemValue; + switch (expectedType) { + case 'string': + case 'number': + case 'boolean': + case 'undefined': + if (expectedType !== actualType) { + return false; + } + break; + case 'array': + if (!Array.isArray(itemValue)) { + return false; + } + break; + case 'object': + if (!(itemValue !== null && typeof itemValue === 'object') && !Array.isArray(itemValue)) { + return false; + } + break; + case 'null': + if (itemValue !== null) { + return false; + } + break; + case 'any': + break; + case 'custom': + default: + return false; + } + } else if (queryValue.$and && Array.isArray(queryValue.$and)) { + if (!queryValue.$and.every((condition: any) => this.matchesQuery(item, condition))) { + return false; + } + } else if (queryValue.$validate && typeof queryValue.$validate === 'function') { + if (!queryValue.$validate(itemValue)) { + return false; + } + } else if (queryValue.$or && Array.isArray(queryValue.$or)) { + if (!queryValue.$or.some((condition: any) => this.matchesQuery(item, condition))) { + return false; + } + } else if (queryValue.$size !== undefined && Array.isArray(itemValue)) { + if (itemValue.length !== queryValue.$size) { + return false; + } + } else if (queryValue.$nin !== undefined && Array.isArray(itemValue)) { + if (queryValue.$nin.some((val: any) => itemValue.includes(val))) { + return false; + } + } else if (queryValue.$slice !== undefined && Array.isArray(itemValue)) { + const sliceValue = Array.isArray(queryValue.$slice) ? queryValue.$slice[0] : queryValue.$slice; + itemValue = itemValue.slice(sliceValue); + } else if (queryValue.$sort !== undefined && Array.isArray(itemValue)) { + const sortOrder = queryValue.$sort === 1 ? 1 : -1; + itemValue.sort((a: any, b: any) => sortOrder * (a - b)); + } else if (queryValue.$text && typeof queryValue.$text === 'string' && typeof itemValue === 'string') { + const text = queryValue.$text.toLowerCase(); + const target = itemValue.toLowerCase(); + if (!target.includes(text)) { + return false; + } + } else if (!this.matchesQuery(itemValue, queryValue)) { + return false; + } + } else { + if (itemValue !== queryValue) { + return false; + } } - if (match) { - logInfo({ - content: `Data Found: ${item}`, - devLogs: this.devLogs, - }); - return { - acknowledged: true, - results: item, - message: "Found data matching your query.", - }; + } + return true; + } + + async find(dataname: string, query: any, options: any = {}, loadedData?: any[]): Promise { + try { + if (!query) { + logError({ + content: "Query isn't provided.", + devLogs: this.devLogs, + throwErr: true, + }); + + return { + acknowledged: false, + errorMessage: "Query isn't provided.", + results: null + }; + } + + await this.index(dataname); + const indexMap = this.indexes.get(dataname); + + if (!indexMap) { + return { + acknowledged: true, + message: "No data found matching your query.", + results: null + }; + } + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + let currentData: any[] = loaded; + + const candidateIndex = currentData.findIndex((item: any) => this.matchesQuery(item, query)); + + if (candidateIndex !== -1) { + let result = currentData[candidateIndex]; + + if (options.$project) { + result = Object.keys(options.$project).reduce((projectedItem: any, field: string) => { + if (options.$project[field]) { + projectedItem[field] = this.getValueByPath(result, field); + } + return projectedItem; + }, {}); + } + + return { + acknowledged: true, + message: "Found data matching your query.", + results: result + }; + } else { + return { + acknowledged: true, + message: "No data found matching your query.", + results: null + }; } - } - - return { - acknowledged: true, - results: null, - message: "No data found matching your query.", - }; } catch (e: any) { - logError({ - content: `Error finding data from /${dataname}: ${e.message}`, - devLogs: this.devLogs, - throwErr: false, - }); - - return { - acknowledged: false, - errorMessage: `${e.message}`, - results: null, - }; + logError({ + content: e.message, + devLogs: this.devLogs, + throwErr: true, + }); + + return { + acknowledged: false, + errorMessage: `${e.message}`, + results: null, + }; } } - + async loadAll( dataname: string, - query: queryOptions + query: queryOptions, + loadedData?: any[] ): Promise { try { const validOptions = [ @@ -324,7 +464,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { "pageSize", "displayment", ]; - + const invalidOptions = Object.keys(query).filter( (key) => !validOptions.includes(key) ); @@ -335,12 +475,18 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { throwErr: true, }); } + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + let currentData: any[] = loaded; + let filteredData = [...currentData]; - + if (query.searchText) { const searchText = query.searchText.toLowerCase(); filteredData = filteredData.filter((item: any) => @@ -351,7 +497,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { ) ); } - + if (query.fields) { const selectedFields = query.fields .split(",") @@ -366,18 +512,11 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { return selectedDoc; }); } - + if (query.filter && Object.keys(query.filter).length > 0) { - filteredData = filteredData.filter((item: any) => { - for (const key in query.filter) { - if (item[key] !== query.filter[key]) { - return false; - } - } - return true; - }); + filteredData = filteredData.filter((item: any) => this.matchesQuery(item, query.filter)); } - + if (query.projection) { const projectionFields = Object.keys(query.projection); filteredData = filteredData.map((doc: any) => { @@ -392,7 +531,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { return projectedDoc; }); } - + if ( query.sortOrder && (query.sortOrder === "asc" || query.sortOrder === "desc") @@ -405,7 +544,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { } }); } - + let groupedData: any = null; if (query.groupBy) { groupedData = {}; @@ -417,7 +556,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { groupedData[key].push(item); }); } - + if (query.distinct) { const distinctField = query.distinct; const distinctValues = [ @@ -429,7 +568,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { results: distinctValues, }; } - + if (query.dateRange) { const { startDate, endDate, dateField } = query.dateRange; filteredData = filteredData.filter((doc: any) => { @@ -437,7 +576,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { return docDate >= startDate && docDate <= endDate; }); } - + if (query.limitFields) { const limit = query.limitFields; filteredData = filteredData.map((doc: any) => { @@ -450,7 +589,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { return limitedDoc; }); } - + if (query.page && query.pageSize) { const startIndex = (query.page - 1) * query.pageSize; filteredData = filteredData.slice( @@ -458,19 +597,19 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { startIndex + query.pageSize ); } - + if (query.displayment !== null && query.displayment > 0) { filteredData = filteredData.slice(0, query.displayment); } - + const results: any = { allData: filteredData }; - + if (query.groupBy) { results.groupedData = groupedData; } - + this.emit("allData", results.allData); - + return { acknowledged: true, message: "Data found with the given options.", @@ -488,11 +627,12 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { }; } } - + async remove( dataname: string, query: any, - options?: { docCount: number } + options?: { docCount: number }, + loadedData?: any[] ): Promise { try { if (!query) { @@ -506,37 +646,54 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { results: null, }; } - - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + + let currentData: any[] = loaded; + + const dataFound = await this.find(dataname, query, currentData); + const foundDocument = dataFound.results; + + if (!foundDocument) { + return { + acknowledged: true, + errorMessage: `No document found matching the query.`, + results: null, + }; + } + let removedCount = 0; let matchFound = false; - + for (let i = 0; i < currentData.length; i++) { const item = currentData[i]; let match = true; - + for (const key of Object.keys(query)) { if (item[key] !== query[key]) { match = false; break; } } - + if (match) { currentData.splice(i, 1); removedCount++; - + if (removedCount === options?.docCount) { break; } - + i--; matchFound = true; } } - + if (!matchFound) { return { acknowledged: true, @@ -544,24 +701,24 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { results: null, }; } - + let data: any; - + if (this.secure.enable) { data = await encodeJSON(currentData, this.secure.secret); } else { - data = JSON.stringify(currentData); + data = JSON.stringify(currentData, null, 2); } - + fs.writeFileSync(dataname, data); - + logSuccess({ content: "Data has been removed", devLogs: this.devLogs, }); - + this.emit("dataRemoved", query, options?.docCount); - + return { acknowledged: true, message: `${removedCount} document(s) removed successfully.`, @@ -578,330 +735,146 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { results: null, }; } - } + } async update( dataname: string, - query: any, - updateQuery: any, - upsert: boolean = false + searchQuery: any, + updateQuery: operationKeys, + upsert?: boolean, + loadedData?: any[] ): Promise { try { - if (!query) { - logError({ - content: `Search query is not provided`, - devLogs: this.devLogs, - }); + + if (!searchQuery) { return { acknowledged: false, errorMessage: `Search query is not provided`, results: null, }; } - + if (!updateQuery) { - logError({ - content: `Update query is not provided`, - devLogs: this.devLogs, - }); return { acknowledged: false, errorMessage: `Update query is not provided`, results: null, }; } - - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + + let currentData: any[] = loaded; + const dataFound = await this.find(dataname, searchQuery, currentData); + let matchingDocument = dataFound.results; + + if (!matchingDocument) { + if (upsert) { + matchingDocument = { ...searchQuery }; + currentData.push(matchingDocument); + } else { + return { + acknowledged: false, + errorMessage: `No document found matching the query`, + results: null, + }; + } + } + + let updatedDocument = { ...matchingDocument }; let updatedCount = 0; - let updatedDocument: any = null; - let matchFound = false; - - currentData.some((item: any) => { - let match = true; - - for (const key of Object.keys(query)) { - if (typeof query[key] === "object") { - const operator = Object.keys(query[key])[0]; - const value = query[key][operator]; - switch (operator) { - case "$gt": - if (!(item[key] > value)) { - match = false; - } - break; - case "$lt": - if (!(item[key] < value)) { - match = false; - } - break; - case "$or": - if ( - !query[key].some((condition: any) => item[key] === condition) - ) { - match = false; - } - break; - default: - if (item[key] !== value) { - match = false; - } - } - } else { - if (item[key] !== query[key]) { - match = false; + + for (const operation in updateQuery) { + if (updateQuery.hasOwnProperty(operation)) { + switch (operation) { + case '$set': + opSet(updatedDocument, updateQuery[operation]); break; - } - } - } - - if (match) { - for (const key of Object.keys(updateQuery)) { - if (key.startsWith("$")) { - switch (key) { - case "$set": - Object.assign(item, updateQuery.$set); - break; - case "$unset": - for (const field of Object.keys(updateQuery.$unset)) { - delete item[field]; - } - break; - case "$inc": - for (const field of Object.keys(updateQuery.$inc)) { - item[field] = (item[field] || 0) + updateQuery.$inc[field]; - } - break; - case "$currentDate": - for (const field of Object.keys(updateQuery.$currentDate)) { - item[field] = new Date(); - } - break; - case "$push": - for (const field of Object.keys(updateQuery.$push)) { - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(updateQuery.$push[field])) { - item[field].push(...updateQuery.$push[field]); - } else { - item[field].push(updateQuery.$push[field]); - } - } - break; - case "$pull": - for (const field of Object.keys(updateQuery.$pull)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => val !== updateQuery.$pull[field] - ); - } - } - break; - case "$position": - for (const field of Object.keys(updateQuery.$position)) { - const { index, element } = updateQuery.$position[field]; - if (Array.isArray(item[field])) { - item[field].splice(index, 0, element); - } - } - break; - case "$max": - for (const field of Object.keys(updateQuery.$max)) { - item[field] = Math.max( - item[field] || Number.NEGATIVE_INFINITY, - updateQuery.$max[field] - ); - } - break; - case "$min": - for (const field of Object.keys(updateQuery.$min)) { - item[field] = Math.min( - item[field] || Number.POSITIVE_INFINITY, - updateQuery.$min[field] - ); - } - break; - case "$lt": - for (const field of Object.keys(updateQuery.$lt)) { - if (item[field] < updateQuery.$lt[field]) { - item[field] = updateQuery.$lt[field]; - } - } - break; - case "$gt": - for (const field of Object.keys(updateQuery.$gt)) { - if (item[field] > updateQuery.$gt[field]) { - item[field] = updateQuery.$gt[field]; - } - } - break; - case "$or": - const orConditions = updateQuery.$or; - const orMatch = orConditions.some((condition: any) => { - for (const field of Object.keys(condition)) { - if (item[field] !== condition[field]) { - return false; - } - } - return true; - }); - if (orMatch) { - Object.assign(item, updateQuery.$set); - } - break; - case "$addToSet": - for (const field of Object.keys(updateQuery.$addToSet)) { - if (!item[field]) { - item[field] = []; - } - if (!item[field].includes(updateQuery.$addToSet[field])) { - item[field].push(updateQuery.$addToSet[field]); - } - } - break; - case "$pushAll": - for (const field of Object.keys(updateQuery.$pushAll)) { - if (!item[field]) { - item[field] = []; - } - item[field].push(...updateQuery.$pushAll[field]); - } - break; - case "$pop": - for (const field of Object.keys(updateQuery.$pop)) { - if (Array.isArray(item[field])) { - if (updateQuery.$pop[field] === -1) { - item[field].shift(); - } else if (updateQuery.$pop[field] === 1) { - item[field].pop(); - } - } - } - break; - case "$pullAll": - for (const field of Object.keys(updateQuery.$pullAll)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => !updateQuery.$pullAll[field].includes(val) - ); - } - } - break; - case "$rename": - for (const field of Object.keys(updateQuery.$rename)) { - item[updateQuery.$rename[field]] = item[field]; - delete item[field]; - } - break; - case "$bit": - for (const field of Object.keys(updateQuery.$bit)) { - if (typeof item[field] === "number") { - item[field] = item[field] & updateQuery.$bit[field]; - } - } - break; - case "$mul": - for (const field of Object.keys(updateQuery.$mul)) { - item[field] = (item[field] || 0) * updateQuery.$mul[field]; - } - break; - case "$each": - if (updateQuery.$push) { - for (const field of Object.keys(updateQuery.$push)) { - const elementsToAdd = updateQuery.$push[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - item[field].push(...elementsToAdd); - } - } - } else if (updateQuery.$addToSet) { - for (const field of Object.keys(updateQuery.$addToSet)) { - const elementsToAdd = updateQuery.$addToSet[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - elementsToAdd.forEach((element: any) => { - if (!item[field].includes(element)) { - item[field].push(element); - } - }); - } - } - } - break; - case "$slice": - for (const field of Object.keys(updateQuery.$slice)) { - if (Array.isArray(item[field])) { - item[field] = item[field].slice( - updateQuery.$slice[field] - ); - } - } - break; - case "$sort": - for (const field of Object.keys(updateQuery.$sort)) { - if (Array.isArray(item[field])) { - item[field].sort((a: any, b: any) => a - b); - } - } - break; - default: - logError({ - content: `Unsupported operator: ${key}`, - devLogs: this.devLogs, - throwErr: true, - }); - } - } else { - item[key] = updateQuery[key]; - } + case '$unset': + opUnset(updatedDocument, updateQuery[operation]); + break; + case '$push': + opPush(updatedDocument, updateQuery[operation], upsert); + break; + case '$pull': + opPull(updatedDocument, updateQuery[operation]); + break; + case '$addToSet': + opAddToSet(updatedDocument, updateQuery[operation], upsert); + break; + case '$rename': + opRename(updatedDocument, updateQuery[operation]); + break; + case '$min': + opMin(updatedDocument, updateQuery[operation], upsert); + break; + case '$max': + opMax(updatedDocument, updateQuery[operation], upsert); + break; + case '$mul': + opMul(updatedDocument, updateQuery[operation], upsert); + break; + case '$inc': + opInc(updatedDocument, updateQuery[operation], upsert); + break; + case '$bit': + opBit(updatedDocument, updateQuery[operation], upsert); + break; + case '$currentDate': + opCurrentDate(updatedDocument, updateQuery[operation], upsert); + break; + case '$pop': + opPop(updatedDocument, updateQuery[operation], upsert); + break; + case '$slice': + opSlice(updatedDocument, updateQuery[operation], upsert); + break; + case '$sort': + opSort(updatedDocument, updateQuery[operation], upsert); + break; + default: + return { + acknowledged: false, + errorMessage: `Unsupported update operation: ${operation}`, + results: null, + }; } - - updatedDocument = item; - updatedCount++; - matchFound = true; - - return true; } - }); - - if (!matchFound && upsert) { - const newData = { _id: randomUUID(), ...query, ...updateQuery.$set }; - currentData.push(newData); - updatedDocument = newData; - updatedCount++; } - - if (!matchFound && !upsert) { - return { - acknowledged: true, - errorMessage: `No document found matching the search query.`, - results: null, - }; + + const index = currentData.findIndex((doc: any) => + Object.keys(searchQuery).every(key => doc[key] === searchQuery[key]) + ); + + if (index !== -1) { + currentData[index] = updatedDocument; + updatedCount = 1; + } else if (upsert) { + currentData.push(updatedDocument); + updatedCount = 1; } - + let data: any; - if (this.secure.enable) { data = await encodeJSON(currentData, this.secure.secret); } else { - data = JSON.stringify(currentData); + data = JSON.stringify(currentData, null, 2); } - + fs.writeFileSync(dataname, data); - + logSuccess({ - content: "Data has been updated", + content: `${updatedCount} document(s) updated`, devLogs: this.devLogs, }); - + this.emit("dataUpdated", updatedDocument); - + return { acknowledged: true, message: `${updatedCount} document(s) updated successfully.`, @@ -914,305 +887,149 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { }); return { acknowledged: false, - errorMessage: `${e.message}`, + errorMessage: e.message, results: null, }; } - } - + } + async updateMany( dataname: string, query: any, - updateQuery: any + updateQuery: any, + loadedData?: any[] ): Promise { try { + if (!query) { - logError({ - content: `Search query is not provided`, - devLogs: this.devLogs, - }); return { acknowledged: false, errorMessage: `Search query is not provided`, results: null, }; } - + if (!updateQuery) { - logError({ - content: `Update query is not provided`, - devLogs: this.devLogs, - }); return { acknowledged: false, errorMessage: `Update query is not provided`, results: null, }; } - - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + + let currentData: any[] = loaded; let updatedCount = 0; - let updatedDocuments: any[] = []; - - currentData.forEach((item: any) => { - let match = true; - - for (const key of Object.keys(query)) { - if (typeof query[key] === "object") { - const operator = Object.keys(query[key])[0]; - const value = query[key][operator]; - switch (operator) { - case "$gt": - if (!(item[key] > value)) { - match = false; - } - break; - case "$lt": - if (!(item[key] < value)) { - match = false; - } - break; - case "$or": - if ( - !query[key].some((condition: any) => item[key] === condition) - ) { - match = false; - } - break; - default: - if (item[key] !== value) { - match = false; - } - } - } else { - if (item[key] !== query[key]) { - match = false; - break; - } - } - } - - if (match) { - for (const key of Object.keys(updateQuery)) { - if (key.startsWith("$")) { - switch (key) { - case "$set": - Object.assign(item, updateQuery.$set); - break; - case "$unset": - for (const field of Object.keys(updateQuery.$unset)) { - delete item[field]; - } - break; - case "$inc": - for (const field of Object.keys(updateQuery.$inc)) { - item[field] = (item[field] || 0) + updateQuery.$inc[field]; - } - break; - case "$currentDate": - for (const field of Object.keys(updateQuery.$currentDate)) { - item[field] = new Date(); - } - break; - case "$push": - for (const field of Object.keys(updateQuery.$push)) { - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(updateQuery.$push[field])) { - item[field].push(...updateQuery.$push[field]); - } else { - item[field].push(updateQuery.$push[field]); - } - } - break; - case "$pull": - for (const field of Object.keys(updateQuery.$pull)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => val !== updateQuery.$pull[field] - ); - } - } + const updatedDocuments: any[] = []; + + let foundMatch = false; + + currentData.forEach((doc: any, index: number) => { + if (this.matchesQuery(doc, query)) { + foundMatch = true; + const updatedDocument = { ...doc }; + for (const operation in updateQuery) { + if (updateQuery.hasOwnProperty(operation)) { + switch (operation) { + case '$set': + opSet(updatedDocument, updateQuery[operation]); break; - case "$position": - for (const field of Object.keys(updateQuery.$position)) { - const { index, element } = updateQuery.$position[field]; - if (Array.isArray(item[field])) { - item[field].splice(index, 0, element); - } - } + case '$unset': + opUnset(updatedDocument, updateQuery[operation]); break; - case "$max": - for (const field of Object.keys(updateQuery.$max)) { - item[field] = Math.max( - item[field] || Number.NEGATIVE_INFINITY, - updateQuery.$max[field] - ); - } + case '$push': + opPush(updatedDocument, updateQuery[operation]); break; - case "$min": - for (const field of Object.keys(updateQuery.$min)) { - item[field] = Math.min( - item[field] || Number.POSITIVE_INFINITY, - updateQuery.$min[field] - ); - } + case '$pull': + opPull(updatedDocument, updateQuery[operation]); break; - case "$or": - const orConditions = updateQuery.$or; - const orMatch = orConditions.some((condition: any) => { - for (const field of Object.keys(condition)) { - if (item[field] !== condition[field]) { - return false; - } - } - return true; - }); - if (orMatch) { - Object.assign(item, updateQuery.$set); - } + case '$addToSet': + opAddToSet(updatedDocument, updateQuery[operation]); break; - case "$addToSet": - for (const field of Object.keys(updateQuery.$addToSet)) { - if (!item[field]) { - item[field] = []; - } - if (!item[field].includes(updateQuery.$addToSet[field])) { - item[field].push(updateQuery.$addToSet[field]); - } - } + case '$rename': + opRename(updatedDocument, updateQuery[operation]); break; - case "$pushAll": - for (const field of Object.keys(updateQuery.$pushAll)) { - if (!item[field]) { - item[field] = []; - } - item[field].push(...updateQuery.$pushAll[field]); - } + case '$min': + opMin(updatedDocument, updateQuery[operation]); break; - case "$pop": - for (const field of Object.keys(updateQuery.$pop)) { - if (Array.isArray(item[field])) { - if (updateQuery.$pop[field] === -1) { - item[field].shift(); - } else if (updateQuery.$pop[field] === 1) { - item[field].pop(); - } - } - } + case '$max': + opMax(updatedDocument, updateQuery[operation]); break; - case "$pullAll": - for (const field of Object.keys(updateQuery.$pullAll)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => !updateQuery.$pullAll[field].includes(val) - ); - } - } + case '$mul': + opMul(updatedDocument, updateQuery[operation]); break; - case "$rename": - for (const field of Object.keys(updateQuery.$rename)) { - item[updateQuery.$rename[field]] = item[field]; - delete item[field]; - } + case '$inc': + opInc(updatedDocument, updateQuery[operation]); break; - case "$bit": - for (const field of Object.keys(updateQuery.$bit)) { - if (typeof item[field] === "number") { - item[field] = item[field] & updateQuery.$bit[field]; - } - } + case '$bit': + opBit(updatedDocument, updateQuery[operation]); break; - case "$mul": - for (const field of Object.keys(updateQuery.$mul)) { - item[field] = (item[field] || 0) * updateQuery.$mul[field]; - } + case '$currentDate': + opCurrentDate(updatedDocument, updateQuery[operation]); break; - case "$each": - if (updateQuery.$push) { - for (const field of Object.keys(updateQuery.$push)) { - const elementsToAdd = updateQuery.$push[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - item[field].push(...elementsToAdd); - } - } - } else if (updateQuery.$addToSet) { - for (const field of Object.keys(updateQuery.$addToSet)) { - const elementsToAdd = updateQuery.$addToSet[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - elementsToAdd.forEach((element: any) => { - if (!item[field].includes(element)) { - item[field].push(element); - } - }); - } - } - } + case '$pop': + opPop(updatedDocument, updateQuery[operation]); break; - case "$slice": - for (const field of Object.keys(updateQuery.$slice)) { - if (Array.isArray(item[field])) { - item[field] = item[field].slice( - updateQuery.$slice[field] - ); - } - } + case '$slice': + opSlice(updatedDocument, updateQuery[operation]); break; - case "$sort": - for (const field of Object.keys(updateQuery.$sort)) { - if (Array.isArray(item[field])) { - item[field].sort((a: any, b: any) => a - b); - } - } + case '$sort': + opSort(updatedDocument, updateQuery[operation]); break; default: - logError({ - content: `Unsupported Opperator: ${key}.`, - devLogs: this.devLogs, - throwErr: true, - }); - } - } else { - item[key] = updateQuery[key]; + return { + acknowledged: false, + errorMessage: `Unsupported update operation: ${operation}`, + results: null, + }; + } } } - - updatedDocuments.push(item); + currentData[index] = updatedDocument; + updatedDocuments.push(updatedDocument); updatedCount++; } }); - - let data: any; - - if (this.secure.enable) { - data = await encodeJSON(currentData, this.secure.secret); - } else { - data = JSON.stringify(currentData); + + if (!foundMatch) { + return { + acknowledged: false, + errorMessage: `No documents found matching the query.`, + results: null, + }; } + + let data: any; + if (this.secure.enable) { + data = await encodeJSON(currentData, this.secure.secret); + } else { + data = JSON.stringify(currentData, null, 2); + } + + fs.writeFileSync(dataname, data); + + logSuccess({ + content: `${updatedCount} document(s) updated`, + devLogs: this.devLogs, + }); + + updatedDocuments.forEach((doc: any) => { + this.emit("dataUpdated", doc); + }); + + return { + acknowledged: true, + message: `${updatedCount} document(s) updated successfully.`, + results: updatedDocuments, + }; + - fs.writeFileSync(dataname, data); - - logSuccess({ - content: `${updatedCount} document(s) updated`, - devLogs: this.devLogs, - }); - - this.emit("dataUpdated", updatedDocuments); - - return { - acknowledged: true, - message: `${updatedCount} document(s) updated successfully.`, - results: updatedDocuments, - }; } catch (e: any) { logError({ content: e.message, @@ -1220,7 +1037,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { }); return { acknowledged: false, - errorMessage: `${e.message}`, + errorMessage: e.message, results: null, }; } @@ -1228,39 +1045,31 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { async drop(dataname: string): Promise { try { - const currentData = this.load(dataname); - if (Array.isArray(currentData) && currentData.length === 0) { + if (!fs.existsSync(dataname)) { return { acknowledged: true, - message: `The file already contains an empty array.`, + message: `The file does not exist.`, results: null, }; } - - let data: any; - - if (this.secure.enable) { - data = ""; - } else { - data = []; - } - - fs.writeFileSync(dataname, data); - + + fs.unlinkSync(dataname); + logSuccess({ - content: "Data has been dropped", + content: "File has been dropped", devLogs: this.devLogs, }); - - this.emit("dataDropped", `Data has been removed from ${dataname}`); - + + this.emit("dataDropped", `File ${dataname} has been dropped`); + return { acknowledged: true, - message: `All data dropped successfully.`, - results: "", + message: `File dropped successfully.`, + results: [], }; } catch (e: any) { + logError({ content: e.message, devLogs: this.devLogs, @@ -1272,63 +1081,38 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { }; } } + async search(collectionFilters: CollectionFilter[]): Promise { try { const results: SearchResult = {}; for (const filter of collectionFilters) { const { dataname, displayment, filter: query } = filter; - + let filePath: string; - + if (!this.dataPath) throw new Error("Please provide a datapath "); if (this.secure.enable) { filePath = path.join(this.dataPath, `${dataname}.verse`); } else { filePath = path.join(this.dataPath, `${dataname}.json`); } - - try { - } catch (e: any) { - logError({ - content: `Error reading file ${filePath}: ${e.message}`, - devLogs: this.devLogs, - }); - continue; - } - - let jsonData: any; - - if (this.secure.enable) { - jsonData = await decodeJSON(filePath, this.secure.secret); - } else { - const data = await fs.promises.readFile(filePath, "utf-8"); - jsonData = JSON.stringify(data); - } - - let result = jsonData || []; - - if (!jsonData) { - jsonData = []; - } - + + const jsonData = (await this.load(filePath)).results; + let result = jsonData; + if (Object.keys(query).length !== 0) { result = jsonData.filter((item: any) => { - for (const key in query) { - if (item[key] !== query[key]) { - return false; - } - } - return true; + return this.matchesQuery(item, query); }); } - + if (displayment !== null) { result = result.slice(0, displayment); } - + results[dataname] = result; } - + return { acknowledged: true, message: "Successfully searched in data for the given query.", @@ -1340,14 +1124,14 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { devLogs: this.devLogs, throwErr: false, }); - + return { acknowledged: true, errorMessage: `${e.message}`, results: null, }; } - } + } public async dataSize(dataname: string): Promise { try { @@ -1380,8 +1164,8 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { public async countDoc(dataname: string): Promise { try { - const data: any = (await this.load(dataname)) || []; - const doc = data.results.length; + const data: any = (await this.load(dataname)); + const doc = data.results?.length; return { acknowledged: true, @@ -1757,109 +1541,126 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { } } - async batchTasks(operations: any[]): Promise { - try { - const results: { [key: string]: any[] } = {}; - - if (!this.dataPath) - throw new Error("You need to provide a dataPath in connect."); - - const operationHandlers: { [key: string]: Function } = { - add: async (dataname: string, operation: any) => - await this.add(dataname, operation), - update: async (dataname: string, operation: any) => - await this.update(dataname, operation.query, operation.update), - remove: async (dataname: string, operation: any) => - await this.remove(dataname, operation.query), - bufferZone: async (dataname: string, operation: any) => - await this.bufferZone(operation.geometry, operation.bufferDistance), - polygonArea: async (dataname: string, operation: any) => - await this.calculatePolygonArea(operation.polygonCoordinates), - nearBy: async (dataname: string, operation: any) => - await this.nearbyVectors(operation.data), - find: async (dataname: string, operation: any) => - await this.find(dataname, operation.query), - updateMany: async (dataname: string, operation: any) => - await this.updateMany(dataname, operation.query, operation.newData), - loadAll: async (dataname: string, operation: any) => - await this.loadAll(dataname, operation.query), - drop: async (dataname: string, operation: any) => - await this.drop(dataname), - load: async (dataname: string, operation: any) => - await this.load(dataname), - search: async (operation: any) => - await this.search(operation.collectionFilters), - dataSize: async (dataname: string, operation: any) => - await this.dataSize(dataname), - countDoc: async (dataname: string, operation: any) => - await this.countDoc(dataname), - }; + async batchTasks(tasks: Array<{ + type: string, dataname: string, newData?: any, options?: any, + loadedData?: any, query?: any, updateQuery?: any, upsert?: any, + collectionFilters?: any, from?: any, to?: any, pipline?: any + }>): Promise { + const taskResults: Array<{ type: string, results: AdapterResults }> = []; - for (const operation of operations) { - const operationType = operation.type; - const handler = operationHandlers[operationType]; + if (!this.dataPath) throw new Error('Invalid Usage. You need to provide dataPath folder in connection.') - if (handler) { - let filePath: string; - - if (this.secure.enable) { - filePath = path.join(this.dataPath, `${operation.dataname}.verse`); - } else { - filePath = path.join(this.dataPath, `${operation.dataname}.json`); - } + for (const task of tasks) { + const dataName: string = path.join(this.dataPath, `${task.dataname}.${this.secure.enable ? 'verse' : 'json'}`); + try { + let result: AdapterResults; - const operationResult = await handler(filePath, operation); - if (!results.hasOwnProperty(operationType)) { - results[operationType] = []; - } - if (operationResult.acknowledged) { - results[operationType].push(operationResult.results); - } else { - logError({ - content: `Failed to perform ${operationType} operation: ${JSON.stringify( - operation - )}`, - devLogs: this.devLogs, - }); - return { - acknowledged: false, - errorMessage: `The batch operation: ${operationType} has faild.`, - results: null, - }; - } - } else { - logError({ - content: `Unsupported operation type: ${operationType}`, - devLogs: this.devLogs, - throwErr: true, - }); + switch (task.type) { + case 'load': + result = await this.load(dataName); + break; + case 'add': + result = await this.add(dataName, task.newData, task.options); + break; + case 'find': + result = await this.find(dataName, task.query, task.options, task.loadedData); + break; + case 'remove': + result = await this.remove(dataName, task.query, task.options); + break; + case 'update': + result = await this.update(dataName, task.query, task.updateQuery, task.upsert, task.loadedData); + break; + case 'updateMany': + result = await this.updateMany(dataName, task.query, task.updateQuery); + break; + case 'loadAll': + result = await this.loadAll(dataName, task.query, task.updateQuery); + break; + case 'search': + result = await this.search(task.collectionFilters); + break; + case 'drop': + result = await this.drop(dataName); + break; + case 'dataSize': + result = await this.dataSize(dataName); + break; + case 'moveData': + result = await this.moveData(task.from, task.to, task.options); + break; + case 'countDoc': + result = await this.countDoc(dataName); + break; + case 'countDoc': + result = await this.aggregate(dataName, task.pipline); + break; + default: + throw new Error(`Unknown task type: ${task.type}`); } + + taskResults.push({ type: task.type, results: result }); + } catch (e: any) { + taskResults.push({ type: task.type, results: { acknowledged: false, errorMessage: e.message, results: null } }); } + } - logSuccess({ - content: "Batch operations completed", - devLogs: this.devLogs, - }); + const allAcknowledge = taskResults.every(({ results }) => results.acknowledged); - return { - acknowledged: true, - message: "Batch operations completed successfully.", - results: results, - }; - } catch (e: any) { - logError({ - content: e.message, - devLogs: this.devLogs, - }); - return { - acknowledged: false, - errorMessage: `${e.message}`, - results: null, - }; + return { + acknowledged: allAcknowledge, + message: allAcknowledge ? "All tasks completed successfully." : "Some tasks failed to complete.", + results: taskResults, + }; + } + + + +async aggregate(dataname: string, pipeline: any[]): Promise { + try { + const loadedData = (await this.load(dataname)).results; + await this.index(dataname); + let aggregatedData = [...loadedData]; + + for (const stage of pipeline) { + if (stage.$match) { + aggregatedData = aggregatedData.filter(item => this.matchesQuery(item, stage.$match)); + } else if (stage.$group) { + const groupId = stage.$group._id; + const groupedData: Record = {}; + + for (const item of aggregatedData) { + const key = item[groupId]; + if (!groupedData[key]) { + groupedData[key] = []; + } + groupedData[key].push(item); + } + + aggregatedData = Object.keys(groupedData).map(key => { + const groupItems = groupedData[key]; + const aggregatedItem: Record = { _id: key }; + + for (const [field, expr] of Object.entries(stage.$group)) { + if (field === "_id") continue; + const aggExpr = expr as AggregationExpression; + if (aggExpr.$sum) { + aggregatedItem[field] = groupItems.reduce((sum, item) => sum + item[aggExpr.$sum!], 0); + } + } + + return aggregatedItem; + }); + } } + + return { results: aggregatedData, acknowledged: true, message: 'This method is not complete. Please wait for next update' }; + } catch (e) { + return { results: null, acknowledged: false, errorMessage: 'This method is not complete. Please wait for next update' }; } +} - async moveData( +async moveData( from: string, to: string, options: { query?: queryOptions; dropSource?: boolean } @@ -1896,19 +1697,19 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { sourceData, this.secure.secret ); - await fs.promises.writeFile(from, sourceDataString); + fs.writeFileSync(from, sourceDataString); } else { const sourceDataString = JSON.stringify(sourceData); - await fs.promises.writeFile(from, sourceDataString); + fs.writeFileSync(from, sourceDataString); } } } else { if (this.secure.enable) { - await fs.promises.writeFile(from, ""); + fs.writeFileSync(from, ""); } else { sourceData.results = []; const sourceDataString = JSON.stringify(sourceData); - await fs.promises.writeFile(from, sourceDataString); + fs.writeFileSync(from, sourceDataString); } } } @@ -1939,7 +1740,7 @@ export class jsonAdapter extends EventEmitter implements versedbAdapter { } else { inData = JSON.stringify(data); } - await fs.promises.writeFile(to, inData); + fs.writeFileSync(to, inData); logSuccess({ content: "Moved Data Successfully.", diff --git a/src/adapters/sql.adapter.ts b/src/adapters/sql.adapter.ts index 24d3646..e466eb5 100644 --- a/src/adapters/sql.adapter.ts +++ b/src/adapters/sql.adapter.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import { EventEmitter } from "events"; -import { logError, logInfo, logSuccess, logWarning } from "../core/logger"; +import { logError, logInfo, logSuccess, logWarning } from "../core/functions/logger"; import { AdapterResults, SQLAdapter, operationKeys } from "../types/adapter"; import { randomUUID } from "../lib/id"; import { @@ -11,7 +11,7 @@ import { MigrationParams, searchFilters, } from "../types/adapter"; -import { encodeSQL, decodeSQL, encodeJSON } from "../core/secureData"; +import { encodeSQL, decodeSQL, encodeJSON } from "../core/functions/secureData"; import { SecureSystem } from "../types/connect"; export class sqlAdapter extends EventEmitter implements SQLAdapter { @@ -281,7 +281,7 @@ export class sqlAdapter extends EventEmitter implements SQLAdapter { } } - async find( + public async find( dataname: string, tableName: string, condition?: string @@ -566,7 +566,7 @@ export class sqlAdapter extends EventEmitter implements SQLAdapter { dataname: string, tableName: string, query: any, - newData: operationKeys + newData: any ): Promise { const fileContentResult = await this.load(dataname); if (!fileContentResult.acknowledged) { diff --git a/src/adapters/yaml.adapter.ts b/src/adapters/yaml.adapter.ts index a86361a..3be6bff 100644 --- a/src/adapters/yaml.adapter.ts +++ b/src/adapters/yaml.adapter.ts @@ -2,24 +2,30 @@ import fs from "fs"; import path from "path"; import yaml from "yaml"; import { EventEmitter } from "events"; -import { logError, logInfo, logSuccess } from "../core/logger"; +import { logError, logInfo, logSuccess } from "../core/functions/logger"; import { randomUUID } from "../lib/id"; import { AdapterResults, AdapterUniqueKey, - versedbAdapter, CollectionFilter, SearchResult, queryOptions, + JsonYamlAdapter, } from "../types/adapter"; import { DevLogsOptions, AdapterSetting } from "../types/adapter"; -import { decodeYAML, encodeYAML } from "../core/secureData"; +import { decodeYAML, encodeYAML } from "../core/functions/secureData"; import { nearbyOptions, SecureSystem } from "../types/connect"; - -export class yamlAdapter extends EventEmitter implements versedbAdapter { +import { opSet, opInc, opPush, opUnset, opPull, opRename, opAddToSet, opMin, opMax, opMul, opBit, opCurrentDate, opPop, opSlice, opSort } from "../core/functions/operations"; +type AggregationExpression = { + $sum?: string; + $avg?: string; + // Add other aggregation operators as needed +}; +export class yamlAdapter extends EventEmitter implements JsonYamlAdapter { public devLogs: DevLogsOptions = { enable: false, path: "" }; public secure: SecureSystem = { enable: false, secret: "" }; public dataPath: string | undefined; + private indexes: Map> = new Map(); constructor(options: AdapterSetting, key: SecureSystem) { super(); @@ -174,13 +180,13 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { let data; if (this.secure.enable) { - data = await encodeYAML(currentData, this.secure.secret); + const encodedData = await encodeYAML(flattenedNewData, this.secure.secret); + fs.appendFileSync(dataname, encodedData); } else { - data = yaml.stringify(currentData); + const data = yaml.stringify(currentData, null, 2); + fs.writeFileSync(dataname, data); } - fs.writeFileSync(dataname, data); - logSuccess({ content: "Data has been added", devLogs: this.devLogs, @@ -208,106 +214,241 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { } } - private indexes: Map> = new Map(); - private async index(dataname: string): Promise { if (!this.indexes.has(dataname)) { - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - const indexMap = new Map(); - currentData.forEach((item: any, index: any) => { - Object.keys(item).forEach((key) => { - const value = item[key]; - if (!indexMap.has(key)) { - indexMap.set(key, []); - } - indexMap.get(key)?.push(index); + const loaded: any = (await this.load(dataname)) || []; + let currentData: any = loaded.results; + const indexMap = new Map(); + currentData.forEach((item: any, index: any) => { + Object.keys(item).forEach((key) => { + const value = item[key]; + if (!indexMap.has(key)) { + indexMap.set(key, []); + } + indexMap.get(key)?.push(index); + }); }); - }); - this.indexes.set(dataname, indexMap); + this.indexes.set(dataname, indexMap); } } - async find(dataname: string, query: any): Promise { - try { + private getValueByPath(obj: any, path: string): any { + return path.split('.').reduce((acc, part) => { + const match = part.match(/(\w+)\[(\d+)\]/); + if (match) { + const [, key, index] = match; + return acc?.[key]?.[index]; + } else { + return acc?.[part]; + } + }, obj); + } + + private matchesQuery(item: any, query: any): boolean { + for (const key of Object.keys(query)) { + const queryValue = query[key]; + let itemValue = this.getValueByPath(item, key); + + if (typeof queryValue === 'object') { + if (queryValue.$regex && typeof itemValue === 'string') { + const regex = new RegExp(queryValue.$regex); + if (!regex.test(itemValue)) { + return false; + } + } else if (queryValue.$some) { + if (Array.isArray(itemValue)) { + if (itemValue.length === 0) { + return false; + } + } else if (typeof itemValue === 'object' && itemValue !== null) { + if (Object.keys(itemValue).length === 0) { + return false; + } + } else { + return false; + } + } else if (queryValue.$gt !== undefined && typeof itemValue === 'number') { + if (itemValue <= queryValue.$gt) { + return false; + } + } else if (queryValue.$lt !== undefined && typeof itemValue === 'number') { + if (itemValue >= queryValue.$lt) { + return false; + } + } else if (queryValue.$exists !== undefined) { + const exists = itemValue !== undefined; + if (exists !== queryValue.$exists) { + return false; + } + } else if (queryValue.$in && Array.isArray(queryValue.$in)) { + if (!queryValue.$in.includes(itemValue)) { + return false; + } + } else if (queryValue.$not && typeof queryValue.$not === 'object') { + if (this.matchesQuery(item, { [key]: queryValue.$not })) { + return false; + } + } else if (queryValue.$elemMatch && Array.isArray(itemValue)) { + if (!itemValue.some((elem: any) => this.matchesQuery(elem, queryValue.$elemMatch))) { + return false; + } + } else if (queryValue.$typeOf && typeof queryValue.$typeOf === 'string') { + const expectedType = queryValue.$typeOf.toLowerCase(); + const actualType = typeof itemValue; + switch (expectedType) { + case 'string': + case 'number': + case 'boolean': + case 'undefined': + if (expectedType !== actualType) { + return false; + } + break; + case 'array': + if (!Array.isArray(itemValue)) { + return false; + } + break; + case 'object': + if (!(itemValue !== null && typeof itemValue === 'object') && !Array.isArray(itemValue)) { + return false; + } + break; + case 'null': + if (itemValue !== null) { + return false; + } + break; + case 'any': + break; + case 'custom': + default: + return false; + } + } else if (queryValue.$and && Array.isArray(queryValue.$and)) { + if (!queryValue.$and.every((condition: any) => this.matchesQuery(item, condition))) { + return false; + } + } else if (queryValue.$validate && typeof queryValue.$validate === 'function') { + if (!queryValue.$validate(itemValue)) { + return false; + } + } else if (queryValue.$or && Array.isArray(queryValue.$or)) { + if (!queryValue.$or.some((condition: any) => this.matchesQuery(item, condition))) { + return false; + } + } else if (queryValue.$size !== undefined && Array.isArray(itemValue)) { + if (itemValue.length !== queryValue.$size) { + return false; + } + } else if (queryValue.$nin !== undefined && Array.isArray(itemValue)) { + if (queryValue.$nin.some((val: any) => itemValue.includes(val))) { + return false; + } + } else if (queryValue.$slice !== undefined && Array.isArray(itemValue)) { + const sliceValue = Array.isArray(queryValue.$slice) ? queryValue.$slice[0] : queryValue.$slice; + itemValue = itemValue.slice(sliceValue); + } else if (queryValue.$sort !== undefined && Array.isArray(itemValue)) { + const sortOrder = queryValue.$sort === 1 ? 1 : -1; + itemValue.sort((a: any, b: any) => sortOrder * (a - b)); + } else if (queryValue.$text && typeof queryValue.$text === 'string' && typeof itemValue === 'string') { + const text = queryValue.$text.toLowerCase(); + const target = itemValue.toLowerCase(); + if (!target.includes(text)) { + return false; + } + } else if (!this.matchesQuery(itemValue, queryValue)) { + return false; + } + } else { + if (itemValue !== queryValue) { + return false; + } + } + } + return true; +} + +async find(dataname: string, query: any, options: any = {}, loadedData?: any[]): Promise { + try { if (!query) { - logError({ - content: "Query isn't provided.", - devLogs: this.devLogs, - }); - return { - acknowledged: false, - results: null, - errorMessage: "Query isn't provided.", - }; + logError({ + content: "Query isn't provided.", + devLogs: this.devLogs, + throwErr: true, + }); + + return { + acknowledged: false, + errorMessage: "Query isn't provided.", + results: null + }; } await this.index(dataname); const indexMap = this.indexes.get(dataname); + if (!indexMap) { - return { - acknowledged: true, - results: null, - message: "No data found matching your query.", - }; + return { + acknowledged: true, + message: "No data found matching your query.", + results: null + }; } - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - const candidateIndexes = Object.keys(query) - .map( - (key) => - indexMap - .get(key) - ?.filter((idx) => currentData[idx][key] === query[key]) || [] - ) - .flat(); - - for (const idx of candidateIndexes) { - const item = currentData[idx]; - let match = true; - for (const key of Object.keys(query)) { - if (item[key] !== query[key]) { - match = false; - break; + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + let currentData: any[] = loaded; + + const candidateIndex = currentData.findIndex((item: any) => this.matchesQuery(item, query)); + + if (candidateIndex !== -1) { + let result = currentData[candidateIndex]; + + if (options.$project) { + result = Object.keys(options.$project).reduce((projectedItem: any, field: string) => { + if (options.$project[field]) { + projectedItem[field] = this.getValueByPath(result, field); + } + return projectedItem; + }, {}); } - } - if (match) { - logInfo({ - content: `Data Found: ${item}`, - devLogs: this.devLogs, - }); + return { - acknowledged: true, - results: item, - message: "Found data matching your query.", + acknowledged: true, + message: "Found data matching your query.", + results: result + }; + } else { + return { + acknowledged: true, + message: "No data found matching your query.", + results: null }; - } } - - return { - acknowledged: true, - results: null, - message: "No data found matching your query.", - }; - } catch (e: any) { + } catch (e: any) { logError({ - content: `Error finding data from /${dataname}: ${e.message}`, - devLogs: this.devLogs, - throwErr: false, + content: e.message, + devLogs: this.devLogs, + throwErr: true, }); return { - acknowledged: false, - errorMessage: `${e.message}`, - results: null, + acknowledged: false, + errorMessage: `${e.message}`, + results: null, }; - } } +} - async loadAll( +async loadAll( dataname: string, - query: queryOptions + query: queryOptions, + loadedData?: any[] ): Promise { try { const validOptions = [ @@ -325,7 +466,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { "pageSize", "displayment", ]; - + const invalidOptions = Object.keys(query).filter( (key) => !validOptions.includes(key) ); @@ -336,12 +477,18 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { throwErr: true, }); } + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + let currentData: any[] = loaded; + let filteredData = [...currentData]; - + if (query.searchText) { const searchText = query.searchText.toLowerCase(); filteredData = filteredData.filter((item: any) => @@ -352,7 +499,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { ) ); } - + if (query.fields) { const selectedFields = query.fields .split(",") @@ -367,18 +514,11 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { return selectedDoc; }); } - + if (query.filter && Object.keys(query.filter).length > 0) { - filteredData = filteredData.filter((item: any) => { - for (const key in query.filter) { - if (item[key] !== query.filter[key]) { - return false; - } - } - return true; - }); + filteredData = filteredData.filter((item: any) => this.matchesQuery(item, query.filter)); } - + if (query.projection) { const projectionFields = Object.keys(query.projection); filteredData = filteredData.map((doc: any) => { @@ -393,7 +533,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { return projectedDoc; }); } - + if ( query.sortOrder && (query.sortOrder === "asc" || query.sortOrder === "desc") @@ -406,7 +546,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { } }); } - + let groupedData: any = null; if (query.groupBy) { groupedData = {}; @@ -418,7 +558,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { groupedData[key].push(item); }); } - + if (query.distinct) { const distinctField = query.distinct; const distinctValues = [ @@ -430,7 +570,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { results: distinctValues, }; } - + if (query.dateRange) { const { startDate, endDate, dateField } = query.dateRange; filteredData = filteredData.filter((doc: any) => { @@ -438,7 +578,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { return docDate >= startDate && docDate <= endDate; }); } - + if (query.limitFields) { const limit = query.limitFields; filteredData = filteredData.map((doc: any) => { @@ -451,7 +591,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { return limitedDoc; }); } - + if (query.page && query.pageSize) { const startIndex = (query.page - 1) * query.pageSize; filteredData = filteredData.slice( @@ -459,19 +599,19 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { startIndex + query.pageSize ); } - + if (query.displayment !== null && query.displayment > 0) { filteredData = filteredData.slice(0, query.displayment); } - + const results: any = { allData: filteredData }; - + if (query.groupBy) { results.groupedData = groupedData; } - + this.emit("allData", results.allData); - + return { acknowledged: true, message: "Data found with the given options.", @@ -493,7 +633,8 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { async remove( dataname: string, query: any, - options?: { docCount: number } + options?: { docCount: number }, + loadedData?: any[] ): Promise { try { if (!query) { @@ -507,37 +648,54 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { results: null, }; } - - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + + let currentData: any[] = loaded; + + const dataFound = await this.find(dataname, query, currentData); + const foundDocument = dataFound.results; + + if (!foundDocument) { + return { + acknowledged: true, + errorMessage: `No document found matching the query.`, + results: null, + }; + } + let removedCount = 0; let matchFound = false; - + for (let i = 0; i < currentData.length; i++) { const item = currentData[i]; let match = true; - + for (const key of Object.keys(query)) { if (item[key] !== query[key]) { match = false; break; } } - + if (match) { currentData.splice(i, 1); removedCount++; - + if (removedCount === options?.docCount) { break; } - + i--; matchFound = true; } } - + if (!matchFound) { return { acknowledged: true, @@ -545,24 +703,24 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { results: null, }; } - + let data: any; - + if (this.secure.enable) { data = await encodeYAML(currentData, this.secure.secret); } else { - data = yaml.stringify(currentData); + data = yaml.stringify(currentData, null, 2); } - + fs.writeFileSync(dataname, data); - + logSuccess({ content: "Data has been removed", devLogs: this.devLogs, }); - + this.emit("dataRemoved", query, options?.docCount); - + return { acknowledged: true, message: `${removedCount} document(s) removed successfully.`, @@ -579,330 +737,146 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { results: null, }; } - } + } async update( dataname: string, - query: any, + searchQuery: any, updateQuery: any, - upsert: boolean = false + upsert?: boolean, + loadedData?: any[] ): Promise { try { - if (!query) { - logError({ - content: `Search query is not provided`, - devLogs: this.devLogs, - }); + + if (!searchQuery) { return { acknowledged: false, errorMessage: `Search query is not provided`, results: null, }; } - + if (!updateQuery) { - logError({ - content: `Update query is not provided`, - devLogs: this.devLogs, - }); return { acknowledged: false, errorMessage: `Update query is not provided`, results: null, }; } - - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + + let currentData: any[] = loaded; + const dataFound = await this.find(dataname, searchQuery, currentData); + let matchingDocument = dataFound.results; + + if (!matchingDocument) { + if (upsert) { + matchingDocument = { ...searchQuery }; + currentData.push(matchingDocument); + } else { + return { + acknowledged: false, + errorMessage: `No document found matching the query`, + results: null, + }; + } + } + + let updatedDocument = { ...matchingDocument }; let updatedCount = 0; - let updatedDocument: any = null; - let matchFound = false; - - currentData.some((item: any) => { - let match = true; - - for (const key of Object.keys(query)) { - if (typeof query[key] === "object") { - const operator = Object.keys(query[key])[0]; - const value = query[key][operator]; - switch (operator) { - case "$gt": - if (!(item[key] > value)) { - match = false; - } - break; - case "$lt": - if (!(item[key] < value)) { - match = false; - } - break; - case "$or": - if ( - !query[key].some((condition: any) => item[key] === condition) - ) { - match = false; - } - break; - default: - if (item[key] !== value) { - match = false; - } - } - } else { - if (item[key] !== query[key]) { - match = false; + + for (const operation in updateQuery) { + if (updateQuery.hasOwnProperty(operation)) { + switch (operation) { + case '$set': + opSet(updatedDocument, updateQuery[operation]); break; - } - } - } - - if (match) { - for (const key of Object.keys(updateQuery)) { - if (key.startsWith("$")) { - switch (key) { - case "$set": - Object.assign(item, updateQuery.$set); - break; - case "$unset": - for (const field of Object.keys(updateQuery.$unset)) { - delete item[field]; - } - break; - case "$inc": - for (const field of Object.keys(updateQuery.$inc)) { - item[field] = (item[field] || 0) + updateQuery.$inc[field]; - } - break; - case "$currentDate": - for (const field of Object.keys(updateQuery.$currentDate)) { - item[field] = new Date(); - } - break; - case "$push": - for (const field of Object.keys(updateQuery.$push)) { - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(updateQuery.$push[field])) { - item[field].push(...updateQuery.$push[field]); - } else { - item[field].push(updateQuery.$push[field]); - } - } - break; - case "$pull": - for (const field of Object.keys(updateQuery.$pull)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => val !== updateQuery.$pull[field] - ); - } - } - break; - case "$position": - for (const field of Object.keys(updateQuery.$position)) { - const { index, element } = updateQuery.$position[field]; - if (Array.isArray(item[field])) { - item[field].splice(index, 0, element); - } - } - break; - case "$max": - for (const field of Object.keys(updateQuery.$max)) { - item[field] = Math.max( - item[field] || Number.NEGATIVE_INFINITY, - updateQuery.$max[field] - ); - } - break; - case "$min": - for (const field of Object.keys(updateQuery.$min)) { - item[field] = Math.min( - item[field] || Number.POSITIVE_INFINITY, - updateQuery.$min[field] - ); - } - break; - case "$lt": - for (const field of Object.keys(updateQuery.$lt)) { - if (item[field] < updateQuery.$lt[field]) { - item[field] = updateQuery.$lt[field]; - } - } - break; - case "$gt": - for (const field of Object.keys(updateQuery.$gt)) { - if (item[field] > updateQuery.$gt[field]) { - item[field] = updateQuery.$gt[field]; - } - } - break; - case "$or": - const orConditions = updateQuery.$or; - const orMatch = orConditions.some((condition: any) => { - for (const field of Object.keys(condition)) { - if (item[field] !== condition[field]) { - return false; - } - } - return true; - }); - if (orMatch) { - Object.assign(item, updateQuery.$set); - } - break; - case "$addToSet": - for (const field of Object.keys(updateQuery.$addToSet)) { - if (!item[field]) { - item[field] = []; - } - if (!item[field].includes(updateQuery.$addToSet[field])) { - item[field].push(updateQuery.$addToSet[field]); - } - } - break; - case "$pushAll": - for (const field of Object.keys(updateQuery.$pushAll)) { - if (!item[field]) { - item[field] = []; - } - item[field].push(...updateQuery.$pushAll[field]); - } - break; - case "$pop": - for (const field of Object.keys(updateQuery.$pop)) { - if (Array.isArray(item[field])) { - if (updateQuery.$pop[field] === -1) { - item[field].shift(); - } else if (updateQuery.$pop[field] === 1) { - item[field].pop(); - } - } - } - break; - case "$pullAll": - for (const field of Object.keys(updateQuery.$pullAll)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => !updateQuery.$pullAll[field].includes(val) - ); - } - } - break; - case "$rename": - for (const field of Object.keys(updateQuery.$rename)) { - item[updateQuery.$rename[field]] = item[field]; - delete item[field]; - } - break; - case "$bit": - for (const field of Object.keys(updateQuery.$bit)) { - if (typeof item[field] === "number") { - item[field] = item[field] & updateQuery.$bit[field]; - } - } - break; - case "$mul": - for (const field of Object.keys(updateQuery.$mul)) { - item[field] = (item[field] || 0) * updateQuery.$mul[field]; - } - break; - case "$each": - if (updateQuery.$push) { - for (const field of Object.keys(updateQuery.$push)) { - const elementsToAdd = updateQuery.$push[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - item[field].push(...elementsToAdd); - } - } - } else if (updateQuery.$addToSet) { - for (const field of Object.keys(updateQuery.$addToSet)) { - const elementsToAdd = updateQuery.$addToSet[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - elementsToAdd.forEach((element: any) => { - if (!item[field].includes(element)) { - item[field].push(element); - } - }); - } - } - } - break; - case "$slice": - for (const field of Object.keys(updateQuery.$slice)) { - if (Array.isArray(item[field])) { - item[field] = item[field].slice( - updateQuery.$slice[field] - ); - } - } - break; - case "$sort": - for (const field of Object.keys(updateQuery.$sort)) { - if (Array.isArray(item[field])) { - item[field].sort((a: any, b: any) => a - b); - } - } - break; - default: - logError({ - content: `Unsupported operator: ${key}`, - devLogs: this.devLogs, - throwErr: true, - }); - } - } else { - item[key] = updateQuery[key]; - } + case '$unset': + opUnset(updatedDocument, updateQuery[operation]); + break; + case '$push': + opPush(updatedDocument, updateQuery[operation], upsert); + break; + case '$pull': + opPull(updatedDocument, updateQuery[operation]); + break; + case '$addToSet': + opAddToSet(updatedDocument, updateQuery[operation], upsert); + break; + case '$rename': + opRename(updatedDocument, updateQuery[operation]); + break; + case '$min': + opMin(updatedDocument, updateQuery[operation], upsert); + break; + case '$max': + opMax(updatedDocument, updateQuery[operation], upsert); + break; + case '$mul': + opMul(updatedDocument, updateQuery[operation], upsert); + break; + case '$inc': + opInc(updatedDocument, updateQuery[operation], upsert); + break; + case '$bit': + opBit(updatedDocument, updateQuery[operation], upsert); + break; + case '$currentDate': + opCurrentDate(updatedDocument, updateQuery[operation], upsert); + break; + case '$pop': + opPop(updatedDocument, updateQuery[operation], upsert); + break; + case '$slice': + opSlice(updatedDocument, updateQuery[operation], upsert); + break; + case '$sort': + opSort(updatedDocument, updateQuery[operation], upsert); + break; + default: + return { + acknowledged: false, + errorMessage: `Unsupported update operation: ${operation}`, + results: null, + }; } - - updatedDocument = item; - updatedCount++; - matchFound = true; - - return true; } - }); - - if (!matchFound && upsert) { - const newData = { _id: randomUUID(), ...query, ...updateQuery.$set }; - currentData.push(newData); - updatedDocument = newData; - updatedCount++; } - - if (!matchFound && !upsert) { - return { - acknowledged: true, - errorMessage: `No document found matching the search query.`, - results: null, - }; + + const index = currentData.findIndex((doc: any) => + Object.keys(searchQuery).every(key => doc[key] === searchQuery[key]) + ); + + if (index !== -1) { + currentData[index] = updatedDocument; + updatedCount = 1; + } else if (upsert) { + currentData.push(updatedDocument); + updatedCount = 1; } - + let data: any; - if (this.secure.enable) { data = await encodeYAML(currentData, this.secure.secret); } else { - data = yaml.stringify(currentData); + data = yaml.stringify(currentData, null, 2); } - + fs.writeFileSync(dataname, data); - + logSuccess({ - content: "Data has been updated", + content: `${updatedCount} document(s) updated`, devLogs: this.devLogs, }); - + this.emit("dataUpdated", updatedDocument); - + return { acknowledged: true, message: `${updatedCount} document(s) updated successfully.`, @@ -915,352 +889,149 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { }); return { acknowledged: false, - errorMessage: `${e.message}`, + errorMessage: e.message, results: null, }; } - } - + } + async updateMany( dataname: string, query: any, - updateQuery: any + updateQuery: any, + loadedData?: any[] ): Promise { try { + if (!query) { - logError({ - content: `Search query is not provided`, - devLogs: this.devLogs, - }); return { acknowledged: false, errorMessage: `Search query is not provided`, results: null, }; } - + if (!updateQuery) { - logError({ - content: `Update query is not provided`, - devLogs: this.devLogs, - }); return { acknowledged: false, errorMessage: `Update query is not provided`, results: null, }; } - - const loaded: any = (await this.load(dataname)) || []; - let currentData: any = loaded.results; - + + let loaded: any = {}; + if (!loadedData) { + loaded = (await this.load(dataname)).results; + } else { + loaded = loadedData; + } + + let currentData: any[] = loaded; let updatedCount = 0; - let updatedDocuments: any[] = []; - - currentData.forEach((item: any) => { - let match = true; - - for (const key of Object.keys(query)) { - if (typeof query[key] === "object") { - const operator = Object.keys(query[key])[0]; - const value = query[key][operator]; - switch (operator) { - case "$gt": - if (!(item[key] > value)) { - match = false; - } - break; - case "$lt": - if (!(item[key] < value)) { - match = false; - } - break; - case "$or": - if ( - !query[key].some((condition: any) => item[key] === condition) - ) { - match = false; - } - break; - default: - if (item[key] !== value) { - match = false; - } - } - } else { - if (item[key] !== query[key]) { - match = false; - break; - } - } - } - - if (match) { - for (const key of Object.keys(updateQuery)) { - if (key.startsWith("$")) { - switch (key) { - case "$set": - Object.assign(item, updateQuery.$set); - break; - case "$unset": - for (const field of Object.keys(updateQuery.$unset)) { - delete item[field]; - } - break; - case "$inc": - for (const field of Object.keys(updateQuery.$inc)) { - item[field] = (item[field] || 0) + updateQuery.$inc[field]; - } - break; - case "$currentDate": - for (const field of Object.keys(updateQuery.$currentDate)) { - item[field] = new Date(); - } - break; - case "$push": - for (const field of Object.keys(updateQuery.$push)) { - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(updateQuery.$push[field])) { - item[field].push(...updateQuery.$push[field]); - } else { - item[field].push(updateQuery.$push[field]); - } - } - break; - case "$pull": - for (const field of Object.keys(updateQuery.$pull)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => val !== updateQuery.$pull[field] - ); - } - } + const updatedDocuments: any[] = []; + + let foundMatch = false; + + currentData.forEach((doc: any, index: number) => { + if (this.matchesQuery(doc, query)) { + foundMatch = true; + const updatedDocument = { ...doc }; + for (const operation in updateQuery) { + if (updateQuery.hasOwnProperty(operation)) { + switch (operation) { + case '$set': + opSet(updatedDocument, updateQuery[operation]); break; - case "$position": - for (const field of Object.keys(updateQuery.$position)) { - const { index, element } = updateQuery.$position[field]; - if (Array.isArray(item[field])) { - item[field].splice(index, 0, element); - } - } + case '$unset': + opUnset(updatedDocument, updateQuery[operation]); break; - case "$max": - for (const field of Object.keys(updateQuery.$max)) { - item[field] = Math.max( - item[field] || Number.NEGATIVE_INFINITY, - updateQuery.$max[field] - ); - } + case '$push': + opPush(updatedDocument, updateQuery[operation]); break; - case "$min": - for (const field of Object.keys(updateQuery.$min)) { - item[field] = Math.min( - item[field] || Number.POSITIVE_INFINITY, - updateQuery.$min[field] - ); - } + case '$pull': + opPull(updatedDocument, updateQuery[operation]); break; - case "$or": - const orConditions = updateQuery.$or; - const orMatch = orConditions.some((condition: any) => { - for (const field of Object.keys(condition)) { - if (item[field] !== condition[field]) { - return false; - } - } - return true; - }); - if (orMatch) { - Object.assign(item, updateQuery.$set); - } + case '$addToSet': + opAddToSet(updatedDocument, updateQuery[operation]); break; - case "$addToSet": - for (const field of Object.keys(updateQuery.$addToSet)) { - if (!item[field]) { - item[field] = []; - } - if (!item[field].includes(updateQuery.$addToSet[field])) { - item[field].push(updateQuery.$addToSet[field]); - } - } + case '$rename': + opRename(updatedDocument, updateQuery[operation]); break; - case "$pushAll": - for (const field of Object.keys(updateQuery.$pushAll)) { - if (!item[field]) { - item[field] = []; - } - item[field].push(...updateQuery.$pushAll[field]); - } + case '$min': + opMin(updatedDocument, updateQuery[operation]); break; - case "$pop": - for (const field of Object.keys(updateQuery.$pop)) { - if (Array.isArray(item[field])) { - if (updateQuery.$pop[field] === -1) { - item[field].shift(); - } else if (updateQuery.$pop[field] === 1) { - item[field].pop(); - } - } - } + case '$max': + opMax(updatedDocument, updateQuery[operation]); break; - case "$pullAll": - for (const field of Object.keys(updateQuery.$pullAll)) { - if (Array.isArray(item[field])) { - item[field] = item[field].filter( - (val: any) => !updateQuery.$pullAll[field].includes(val) - ); - } - } + case '$mul': + opMul(updatedDocument, updateQuery[operation]); break; - case "$rename": - for (const field of Object.keys(updateQuery.$rename)) { - item[updateQuery.$rename[field]] = item[field]; - delete item[field]; - } + case '$inc': + opInc(updatedDocument, updateQuery[operation]); break; - case "$bit": - for (const field of Object.keys(updateQuery.$bit)) { - if (typeof item[field] === "number") { - item[field] = item[field] & updateQuery.$bit[field]; - } - } + case '$bit': + opBit(updatedDocument, updateQuery[operation]); break; - case "$mul": - for (const field of Object.keys(updateQuery.$mul)) { - item[field] = (item[field] || 0) * updateQuery.$mul[field]; - } + case '$currentDate': + opCurrentDate(updatedDocument, updateQuery[operation]); break; - case "$each": - if (updateQuery.$push) { - for (const field of Object.keys(updateQuery.$push)) { - const elementsToAdd = updateQuery.$push[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - item[field].push(...elementsToAdd); - } - } - } else if (updateQuery.$addToSet) { - for (const field of Object.keys(updateQuery.$addToSet)) { - const elementsToAdd = updateQuery.$addToSet[field].$each; - if (!item[field]) { - item[field] = []; - } - if (Array.isArray(elementsToAdd)) { - elementsToAdd.forEach((element: any) => { - if (!item[field].includes(element)) { - item[field].push(element); - } - }); - } - } - } + case '$pop': + opPop(updatedDocument, updateQuery[operation]); break; - case "$slice": - for (const field of Object.keys(updateQuery.$slice)) { - if (Array.isArray(item[field])) { - item[field] = item[field].slice( - updateQuery.$slice[field] - ); - } - } + case '$slice': + opSlice(updatedDocument, updateQuery[operation]); break; - case "$sort": - for (const field of Object.keys(updateQuery.$sort)) { - if (Array.isArray(item[field])) { - item[field].sort((a: any, b: any) => a - b); - } - } + case '$sort': + opSort(updatedDocument, updateQuery[operation]); break; default: - logError({ - content: `Unsupported Opperator: ${key}.`, - devLogs: this.devLogs, - throwErr: true, - }); - } - } else { - item[key] = updateQuery[key]; + return { + acknowledged: false, + errorMessage: `Unsupported update operation: ${operation}`, + results: null, + }; + } } } - - updatedDocuments.push(item); + currentData[index] = updatedDocument; + updatedDocuments.push(updatedDocument); updatedCount++; } }); - - let data: any; - - if (this.secure.enable) { - data = await encodeYAML(currentData, this.secure.secret); - } else { - data = yaml.stringify(currentData); - } - - fs.writeFileSync(dataname, data); - - logSuccess({ - content: `${updatedCount} document(s) updated`, - devLogs: this.devLogs, - }); - - this.emit("dataUpdated", updatedDocuments); - - return { - acknowledged: true, - message: `${updatedCount} document(s) updated successfully.`, - results: updatedDocuments, - }; - } catch (e: any) { - logError({ - content: e.message, - devLogs: this.devLogs, - }); - return { - acknowledged: false, - errorMessage: `${e.message}`, - results: null, - }; - } - } - - async drop(dataname: string): Promise { - try { - const currentData = this.load(dataname); - - if (Array.isArray(currentData) && currentData.length === 0) { + + if (!foundMatch) { return { - acknowledged: true, - message: `The file already contains an empty array.`, + acknowledged: false, + errorMessage: `No documents found matching the query.`, results: null, }; } + + let data: any; + if (this.secure.enable) { + data = await encodeYAML(currentData, this.secure.secret); + } else { + data = yaml.stringify(currentData, null, 2); + } + + fs.writeFileSync(dataname, data); + + logSuccess({ + content: `${updatedCount} document(s) updated`, + devLogs: this.devLogs, + }); + + updatedDocuments.forEach((doc: any) => { + this.emit("dataUpdated", doc); + }); + + return { + acknowledged: true, + message: `${updatedCount} document(s) updated successfully.`, + results: updatedDocuments, + }; + - let data: any; - - if (this.secure.enable) { - data = ""; - } else { - data = []; - } - - fs.writeFileSync(dataname, data); - - logSuccess({ - content: "Data has been dropped", - devLogs: this.devLogs, - }); - - this.emit("dataDropped", `Data has been removed from ${dataname}`); - - return { - acknowledged: true, - message: `All data dropped successfully.`, - results: "", - }; } catch (e: any) { logError({ content: e.message, @@ -1268,70 +1039,55 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { }); return { acknowledged: false, - errorMessage: `${e.message}`, + errorMessage: e.message, results: null, }; } } + async search(collectionFilters: CollectionFilter[]): Promise { try { const results: SearchResult = {}; for (const filter of collectionFilters) { const { dataname, displayment, filter: query } = filter; - + let filePath: string; - + if (!this.dataPath) throw new Error("Please provide a datapath "); - if (this.secure.enable) { filePath = path.join(this.dataPath, `${dataname}.verse`); } else { - filePath = path.join(this.dataPath, `${dataname}.yaml`); - } - - try { - } catch (e: any) { - logError({ - content: `Error reading file ${filePath}: ${e.message}`, - devLogs: this.devLogs, - throwErr: false, - }); - continue; + filePath = path.join(this.dataPath, `${dataname}.json`); } - - let yamlData: any; - + + let jsonData: any; + if (this.secure.enable) { - yamlData = await decodeYAML(filePath, this.secure.secret); + jsonData = await decodeYAML(filePath, this.secure.secret); } else { const data = await fs.promises.readFile(filePath, "utf-8"); - yamlData = yaml.stringify(data); + jsonData = yaml.stringify(data); } - - let result = yamlData || []; - - if (!yamlData) { - yamlData = []; + + let result = jsonData || []; + + if (!jsonData) { + jsonData = []; } - + if (Object.keys(query).length !== 0) { - result = yamlData.filter((item: any) => { - for (const key in query) { - if (item[key] !== query[key]) { - return false; - } - } - return true; + result = jsonData.filter((item: any) => { + return this.matchesQuery(item, query); }); } - + if (displayment !== null) { result = result.slice(0, displayment); } - + results[dataname] = result; } - + return { acknowledged: true, message: "Successfully searched in data for the given query.", @@ -1343,9 +1099,49 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { devLogs: this.devLogs, throwErr: false, }); + + return { + acknowledged: true, + errorMessage: `${e.message}`, + results: null, + }; + } + } + + async drop(dataname: string): Promise { + try { + if (!fs.existsSync(dataname)) { + return { + acknowledged: true, + message: `The file does not exist.`, + results: null, + }; + } + + fs.unlinkSync(dataname); + + logSuccess({ + content: "File has been dropped", + devLogs: this.devLogs, + }); + + this.emit("dataDropped", `File ${dataname} has been dropped`); + return { acknowledged: true, + message: `File dropped successfully.`, + results: [], + }; + } catch (e: any) { + console.log(e); + + logError({ + content: e.message, + devLogs: this.devLogs, + }); + return { + acknowledged: false, errorMessage: `${e.message}`, results: null, }; @@ -1759,102 +1555,123 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { }; } } - async batchTasks(operations: any[]): Promise { - try { - const results: { [key: string]: any[] } = {}; - - if (!this.dataPath) - throw new Error("You need to provide a dataPath in connect."); - - const operationHandlers: { [key: string]: Function } = { - add: async (dataname: string, operation: any) => - await this.add(dataname, operation), - update: async (dataname: string, operation: any) => - await this.update(dataname, operation.query, operation.update), - remove: async (dataname: string, operation: any) => - await this.remove(dataname, operation.query), - bufferZone: async (dataname: string, operation: any) => - await this.bufferZone(operation.geometry, operation.bufferDistance), - polygonArea: async (dataname: string, operation: any) => - await this.calculatePolygonArea(operation.polygonCoordinates), - nearBy: async (dataname: string, operation: any) => - await this.nearbyVectors(operation.data), - find: async (dataname: string, operation: any) => - await this.find(dataname, operation.query), - updateMany: async (dataname: string, operation: any) => - await this.updateMany(dataname, operation.query, operation.newData), - loadAll: async (dataname: string, operation: any) => - await this.loadAll(dataname, operation.query), - drop: async (dataname: string, operation: any) => - await this.drop(dataname), - load: async (dataname: string, operation: any) => - await this.load(dataname), - search: async (operation: any) => - await this.search(operation.collectionFilters), - dataSize: async (dataname: string, operation: any) => - await this.dataSize(dataname), - countDoc: async (dataname: string, operation: any) => - await this.countDoc(dataname), - }; - - for (const operation of operations) { - const operationType = operation.type; - const handler = operationHandlers[operationType]; + async batchTasks(tasks: Array<{ + type: string, dataname: string, newData?: any, options?: any, + loadedData?: any, query?: any, updateQuery?: any, upsert?: any, + collectionFilters?: any, from?: any, to?: any, pipline?: any + }>): Promise { + const taskResults: Array<{ type: string, results: AdapterResults }> = []; + + if (!this.dataPath) throw new Error('Invalid Usage. You need to provide dataPath folder in connection.') + + for (const task of tasks) { + const dataName: string = path.join(this.dataPath, `${task.dataname}.${this.secure.enable ? 'verse' : 'json'}`); + try { + let result: AdapterResults; + + switch (task.type) { + case 'load': + result = await this.load(dataName); + break; + case 'add': + result = await this.add(dataName, task.newData, task.options); + break; + case 'find': + result = await this.find(dataName, task.query, task.options, task.loadedData); + break; + case 'remove': + result = await this.remove(dataName, task.query, task.options); + break; + case 'update': + result = await this.update(dataName, task.query, task.updateQuery, task.upsert, task.loadedData); + break; + case 'updateMany': + result = await this.updateMany(dataName, task.query, task.updateQuery); + break; + case 'loadAll': + result = await this.loadAll(dataName, task.query, task.updateQuery); + break; + case 'search': + result = await this.search(task.collectionFilters); + break; + case 'drop': + result = await this.drop(dataName); + break; + case 'dataSize': + result = await this.dataSize(dataName); + break; + case 'moveData': + result = await this.moveData(task.from, task.to, task.options); + break; + case 'countDoc': + result = await this.countDoc(dataName); + break; + case 'countDoc': + result = await this.aggregate(dataName, task.pipline); + break; + default: + throw new Error(`Unknown task type: ${task.type}`); + } + + taskResults.push({ type: task.type, results: result }); + } catch (e: any) { + taskResults.push({ type: task.type, results: { acknowledged: false, errorMessage: e.message, results: null } }); + } + } + + const allAcknowledge = taskResults.every(({ results }) => results.acknowledged); + + return { + acknowledged: allAcknowledge, + message: allAcknowledge ? "All tasks completed successfully." : "Some tasks failed to complete.", + results: taskResults, + }; + } + + async aggregate(dataname: string, pipeline: any[]): Promise { + try { + const loadedData = (await this.load(dataname)).results; + await this.index(dataname); + let aggregatedData = [...loadedData]; + + for (const stage of pipeline) { + if (stage.$match) { + aggregatedData = aggregatedData.filter(item => this.matchesQuery(item, stage.$match)); + } else if (stage.$group) { + const groupId = stage.$group._id; + const groupedData: Record = {}; + + for (const item of aggregatedData) { + const key = item[groupId]; + if (!groupedData[key]) { + groupedData[key] = []; + } + groupedData[key].push(item); + } - if (handler) { - let filePath: string; + aggregatedData = Object.keys(groupedData).map(key => { + const groupItems = groupedData[key]; + const aggregatedItem: Record = { _id: key }; - if (this.secure.enable) { - filePath = path.join(this.dataPath, `${operation.dataname}.verse`); - } else { - filePath = path.join(this.dataPath, `${operation.dataname}.json`); - } + for (const [field, expr] of Object.entries(stage.$group)) { + if (field === "_id") continue; + const aggExpr = expr as AggregationExpression; + if (aggExpr.$sum) { + aggregatedItem[field] = groupItems.reduce((sum, item) => sum + item[aggExpr.$sum!], 0); + } + } - const operationResult = await handler(filePath, operation); - if (!results.hasOwnProperty(operationType)) { - results[operationType] = []; - } - if (operationResult.acknowledged) { - results[operationType].push(operationResult.results); - } else { - logError({ - content: `Failed to perform ${operationType} operation: ${JSON.stringify( - operation - )}`, - devLogs: this.devLogs, + return aggregatedItem; }); - } - } else { - logError({ - content: `Unsupported operation type: ${operationType}`, - devLogs: this.devLogs, - throwErr: true, - }); } - } - - logSuccess({ - content: "Batch operations completed", - devLogs: this.devLogs, - }); - - return { - acknowledged: true, - message: "Batch operations completed successfully.", - results: results, - }; - } catch (e: any) { - logError({ - content: e.message, - devLogs: this.devLogs, - }); - return { - acknowledged: false, - errorMessage: `${e.message}`, - results: null, - }; } + + return { results: aggregatedData, acknowledged: true, message: 'This method is not complete. Please wait for next update' }; + } catch (e) { + return { results: null, acknowledged: false, errorMessage: 'This method is not complete. Please wait for next update' }; } +} + async moveData( from: string, @@ -1893,19 +1710,19 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { sourceData, this.secure.secret ); - await fs.promises.writeFile(from, sourceDataString); + fs.writeFileSync(from, sourceDataString); } else { const sourceDataString = yaml.stringify(sourceData); - await fs.promises.writeFile(from, sourceDataString); + fs.writeFileSync(from, sourceDataString); } } } else { if (this.secure.enable) { - await fs.promises.writeFile(from, ""); + fs.writeFileSync(from, ""); } else { sourceData.results = []; const sourceDataString = JSON.stringify(sourceData); - await fs.promises.writeFile(from, sourceDataString); + fs.writeFileSync(from, sourceDataString); } } } @@ -1936,7 +1753,7 @@ export class yamlAdapter extends EventEmitter implements versedbAdapter { } else { inData = yaml.stringify(data); } - await fs.promises.writeFile(to, inData); + fs.writeFileSync(to, inData); logSuccess({ content: "Moved Data Successfully.", diff --git a/src/core/connect.ts b/src/core/connect.ts index 94693ba..b413f13 100644 --- a/src/core/connect.ts +++ b/src/core/connect.ts @@ -8,32 +8,34 @@ import { CollectionFilter, DisplayOptions, operationKeys, + QueryOptions, } from "../types/connect"; -import { searchFilters, nearbyOptions } from "../types/adapter"; -import Schema from "./schema"; +import { searchFilters, nearbyOptions, } from "../types/adapter"; +import Schema from "./functions/schema"; import { jsonAdapter, yamlAdapter, sqlAdapter } from "../adapters/export"; -import { logError } from "./logger"; - +import { logError } from "./functions/logger"; /** * The main connect class for interacting with the database */ export default class connect { public adapter: jsonAdapter | yamlAdapter | sqlAdapter | null = null; - public devLogs: DevLogsOptions = { enable: false, path: "" }; - public SecureSystem: SecureSystem = { enable: false, secret: "" }; - public backup: BackupOptions = { enable: false, path: "", retention: 0 }; - public dataPath: string = ""; + public devLogs: DevLogsOptions; + public SecureSystem: SecureSystem; + public backup: BackupOptions; + public dataPath: string; public fileType: string = ""; public adapterType: string = ""; public key: string; + /** * Sets up a database with one of the adapters * @param {AdapterOptions} options - Options for setting up the adapter */ + constructor(options: AdapterOptions) { this.dataPath = options.dataPath; - this.devLogs = options.devLogs; - this.SecureSystem = options.secure; + this.devLogs = options.devLogs ?? { enable: false, path: "" }; + this.SecureSystem = options.secure ?? { enable: false, secret: "" }; this.key = this.SecureSystem?.enable ? this.SecureSystem.secret || "versedb" : "versedb"; @@ -98,9 +100,7 @@ export default class connect { } else { fs.appendFileSync(secretsFilePath, secretString); } - } catch (e: any) { - console.error('Error:', e.message); - + } catch (e: any) { if (e.code === 'ENOENT' && e.path === configPath) { fs.mkdirSync(configPath, { recursive: true }); fs.writeFileSync(secretsFilePath, secretString); @@ -161,7 +161,7 @@ export default class connect { * Add data to a data file * @param {string} dataname - The name of the data file * @param {any} newData - The new data to add - * @param {object} [options] - Additional options + * @param {AdapterUniqueKey} [options] - Additional options * @returns {Promise} - A Promise that resolves with the saved data */ async add(dataname: string, newData: any, options?: any) { @@ -193,7 +193,7 @@ export default class connect { * @param query the search query * @returns the found data */ - async find(dataname: string, query: any) { + async find(dataname: string, query: any, options?: QueryOptions, loadedData?: any[]) { if (!this.adapter) { logError({ content: "Database not connected. Please call connect method first.", @@ -207,7 +207,7 @@ export default class connect { typeof this.adapter?.add === "function" ) { const filePath = path.join(this.dataPath, `${dataname}.${this.fileType}`); - return await this.adapter?.find(filePath, query); + return await this.adapter?.find(filePath, query, options, loadedData); } else { logError({ content: "Find operation is not supported by the current adapter.", @@ -223,7 +223,7 @@ export default class connect { * @param displayOptions the options of the display of the data files * @returns all the data files you selected */ - async loadAll(dataname: string, displayOptions: any) { + async loadAll(dataname: string, displayOptions: any, loadedData?: any[] ) { if (!this.adapter) { logError({ content: "Database not connected. Please call connect method first.", @@ -237,7 +237,7 @@ export default class connect { typeof this.adapter?.loadAll === "function" ) { const filePath = path.join(this.dataPath, `${dataname}.${this.fileType}`); - return await this.adapter?.loadAll(filePath, displayOptions); + return await this.adapter?.loadAll(filePath, displayOptions, loadedData); } else { logError({ content: @@ -248,6 +248,36 @@ export default class connect { } } + /** + * + * @param dataname the name of data files to get multiple files in the same time + * @param pipeline the options of the aggregation + * @returns all the results + */ + async aggregate(dataname: string, pipeline: any[]) { + if (!this.adapter) { + logError({ + content: "Database not connected. Please call connect method first.", + devLogs: this.devLogs, + throwErr: true, + }); + } + + if ( + !(this.adapter instanceof sqlAdapter) && + typeof this.adapter?.aggregate === "function" + ) { + const filePath = path.join(this.dataPath, `${dataname}.${this.fileType}`); + return await this.adapter?.aggregate(filePath, pipeline); + } else { + logError({ + content: + "Aggregate operation is not supported by the current adapter.", + devLogs: this.devLogs, + throwErr: true, + }); + } + } /** * * @param {any[]} operations - array of objects that contains the operations you want @@ -276,13 +306,14 @@ export default class connect { }); } } - /** - * @param dataname the name of the data file you want to edit an item in - * @param query the search query of the item you want to edit - * @param newData the new data that will be edited with the old one - * @param upsert an upsert option - * @returns returnts edited data - */ +/** + * Remove data from the database. + * @param {string} dataname - The name of the data to be removed. + * @param {any} query - The query to identify the data to be removed. + * @param {Object} options - Options for the remove operation. + * @param {number} options.docCount - Number of documents to remove. + * @returns {Promise} A Promise that resolves when the operation is completed. + */ async remove(dataname: string, query: any, options: { docCount: number }) { if (!this.adapter) { logError({ @@ -317,7 +348,7 @@ export default class connect { * @param upsert an upsert option * @returns returnts edited data */ - async update(dataname: string, query: any, newData: any, upsert: boolean) { + async update(dataname: string, query: any, newData: operationKeys, upsert?: boolean, loadedData?: any[]) { if (!this.adapter) { logError({ content: "Database not connected. Please call connect method first.", @@ -398,7 +429,7 @@ export default class connect { return results || null; } - return results?.results || null; + return results || null; } else { logError({ content: @@ -408,7 +439,11 @@ export default class connect { }); } } - +/** + * Get nearby vectors from the database. + * @param {nearbyOptions} data - Options for the nearby vectors search. + * @returns {Promise} A Promise that resolves with nearby vectors. + */ async nearbyVectors(data: nearbyOptions) { try { if (!this.adapter) { @@ -459,7 +494,12 @@ export default class connect { return null; } } - +/** + * Create a buffer zone in the database. + * @param {any} geometry - The geometry used for creating the buffer zone. + * @param {any} bufferDistance - The buffer distance. + * @returns {Promise} A Promise that resolves with the created buffer zone. + */ async polygonArea(polygonCoordinates: any) { try { if (!this.adapter) { @@ -512,7 +552,12 @@ export default class connect { return null; } } - +/** + * Create a buffer zone in the database. + * @param {any} geometry - The geometry used for creating the buffer zone. + * @param {any} bufferDistance - The buffer distance. + * @returns {Promise} A Promise that resolves with the created buffer zone. + */ async bufferZone(geometry: any, bufferDistance: any) { try { if (!this.adapter) { @@ -563,9 +608,14 @@ export default class connect { return null; } } - - - async updateMany(dataname: string, queries: any[], newData: operationKeys) { +/** + * Update multiple documents in the database. + * @param {string} dataname - The name of the data to be updated. + * @param {Array} queries - Array of queries to identify the data to be updated. + * @param {operationKeys} newData - The updated data. + * @returns {Promise} A Promise that resolves when the operation is completed. + */ + async updateMany(dataname: string, queries: any, newData: operationKeys) { if (!this.adapter) { logError({ content: "Database not connected. Please call connect method first.", @@ -1125,17 +1175,13 @@ export default class connect { } } - /** - * @param dataname the schema name - * @param schema the schema defination - * @returns {add} to add data to the database - * @returns {remove} to remove data to the database - * @returns {update} to update data from the database - * @returns {find} to find data in the database - * @returns {load} to load a database - * @returns {drop} to drop a database - */ - model(dataname: string, schema: Schema): any { +/** + * Define a model for interacting with the database. + * @param {string} dataname - The name of the schema. + * @param {Schema} schema - The schema definition. + * @returns {Object} An object containing database operation functions. + */ + model(dataname: string, schema: Schema): any { if (!dataname || !schema) { logError({ content: @@ -1150,23 +1196,44 @@ export default class connect { typeof this.adapter?.add === "function" ) { return { + /** + * Add data to the database. + * @param {any} newData - The data to be added. + * @param {any} [options] - Additional options for the operation. + * @returns {Promise} A Promise that resolves when the operation is completed. + */ + add: async function (this: connect, newData: any, options?: any) { - const validationErrors: any = schema.validate(newData); + const loadingData = await this.load(dataname); + const currenData = loadingData?.results; + const validationErrors: any = schema.validate(newData, currenData); if (validationErrors) { return Promise.reject(validationErrors); } - return this.add(dataname, newData, options); + return await this.add(dataname, newData, options); }.bind(this), - + /** + * Remove data from the database. + * @param {any} query - The query to identify the data to be removed. + * @param {Object} options - Options for the remove operation. + * @param {number} options.docCount - Number of documents to remove. + * @returns {Promise} A Promise that resolves when the operation is completed. + */ remove: async function ( this: connect, query: any, options: { docCount: number } ) { - return this.remove(dataname, query, options); + return await this.remove(dataname, query, options); }.bind(this), - + /** + * Update data in the database. + * @param {any} query - The query to identify the data to be updated. + * @param {any} newData - The updated data. + * @param {boolean} upsert - Whether to perform an upsert operation. + * @returns {Promise} A Promise that resolves when the operation is completed. + */ update: async function ( this: connect, query: any, @@ -1178,21 +1245,38 @@ export default class connect { return Promise.reject(validationErrors); } - return this.update(dataname, query, newData, upsert); + return await this.update(dataname, query, newData, upsert); }.bind(this), - + /** + * Find data in the database. + * @param {any} query - The query to find the data. + * @returns {Promise} A Promise that resolves with the found data. + */ find: async function (this: connect, query: any) { - return this.find(dataname, query); + const loadingData = await this.load(dataname); + const currenData = loadingData?.results; + return await this.find(dataname, query, currenData); }.bind(this), - + /** + * Load a database. + * @returns {Promise} A Promise that resolves when the database is loaded. + */ load: async function (this: connect) { - return this.load(dataname); + return await this.load(dataname); }.bind(this), - + /** + * Drop a database. + * @returns {Promise} A Promise that resolves when the database is dropped. + */ drop: async function (this: connect) { - return this.drop(dataname); + return await this.drop(dataname); }.bind(this), - + /** + * Update multiple documents in the database. + * @param {Array} queries - Array of queries to identify the data to be updated. + * @param {operationKeys} newData - The updated data. + * @returns {Promise} A Promise that resolves when the operation is completed. + */ updateMany: async function ( this: connect, queries: any[], @@ -1203,50 +1287,88 @@ export default class connect { return Promise.reject(validationErrors); } - return this.updateMany(dataname, queries, newData); + return await this.updateMany(dataname, queries, newData); }.bind(this), - + /** + * Load all data from the database. + * @param {any} displayOptions - Options for displaying the data. + * @returns {Promise} A Promise that resolves with all data from the database. + */ allData: async function (this: connect, displayOptions: any) { - return this.loadAll(dataname, displayOptions); + return await this.loadAll(dataname, displayOptions); }.bind(this), - + /** + * Search for data in the database. + * @param {Array} collectionFilters - Filters to apply to the search. + * @returns {Promise} A Promise that resolves with the search results. + */ search: async function ( this: connect, collectionFilters: CollectionFilter[] ) { - return this.search(collectionFilters); + return await this.search(collectionFilters); }.bind(this), - + /** + * Get nearby vectors in the database. + * @param {any} data - The data used for the search. + * @returns {Promise} A Promise that resolves with nearby vectors. + */ nearbyVectors: async function (this: connect, data: any) { - return this.nearbyVectors(data); + return await this.nearbyVectors(data); }.bind(this), - + /** + * Create a buffer zone in the database. + * @param {any} geometry - The geometry used for creating the buffer zone. + * @param {any} bufferDistance - The buffer distance. + * @returns {Promise} A Promise that resolves with the created buffer zone. + */ bufferZone: async function ( this: connect, geometry: any, bufferDistance: any ) { - return this.bufferZone(geometry, bufferDistance); + return await this.bufferZone(geometry, bufferDistance); }.bind(this), - + /** + * Calculate the area of a polygon in the database. + * @param {any} polygonCoordinates - The coordinates of the polygon. + * @returns {Promise} A Promise that resolves with the area of the polygon. + */ polygonArea: async function (this: connect, polygonCoordinates: any) { - return this.polygonArea(polygonCoordinates); + return await this.polygonArea(polygonCoordinates); }.bind(this), - + /** + * Count documents in the database. + * @returns {Promise} A Promise that resolves with the count of documents. + */ countDoc: async function (this: connect) { - return this.countDoc(dataname); + return await this.countDoc(dataname); }.bind(this), - + /** + * Get the size of data in the database. + * @returns {Promise} A Promise that resolves with the size of data. + */ dataSize: async function (this: connect) { - return this.dataSize(dataname); + return await this.dataSize(dataname); }.bind(this), - + /** + * Watch for changes in the database. + * @returns {Promise} A Promise that resolves with the changes in the database. + */ watch: async function (this: connect) { - return this.watch(dataname); + return await this.watch(dataname); }.bind(this), + /** + * Perform batch tasks in the database. + * @param {Array} operations - Array of operations to perform. + * @returns {Promise} A Promise that resolves when batch tasks are completed. + */ batchTasks: async function (this: connect, operations: any[]) { return this.batchTasks(operations); }.bind(this), + aggregate: async function (this: connect, pipeline: any[]) { + return await this.aggregate(dataname, pipeline); + }.bind(this), }; } else { logError({ diff --git a/src/core/backup.ts b/src/core/functions/backup.ts similarity index 100% rename from src/core/backup.ts rename to src/core/functions/backup.ts diff --git a/src/core/logger.ts b/src/core/functions/logger.ts similarity index 63% rename from src/core/logger.ts rename to src/core/functions/logger.ts index d8d966d..9e2170a 100644 --- a/src/core/logger.ts +++ b/src/core/functions/logger.ts @@ -1,8 +1,8 @@ import path from "path"; import fs from "fs/promises"; -import colors from "../lib/colors"; -import { currentDate } from "../lib/date"; -import { DevLogsOptions } from "../types/connect"; +import colors from "../../lib/colors"; +import { currentLocalDate } from "../../lib/date"; +import { DevLogsOptions } from "../../types/connect"; type logsPath = string; type LogFile = string; @@ -27,7 +27,7 @@ async function logToFile({ await fs.mkdir(logsPath, { recursive: true }); await fs.appendFile( logFilePath, - `${currentDate} ${removeAnsiEscapeCodes(content)}\n`, + `${currentLocalDate} ${removeAnsiEscapeCodes(content)}\n`, "utf8" ); } catch (error: any) { @@ -66,27 +66,23 @@ export function logError({ }): void { if (devLogs?.enable === true) { logToFile({ - content: `${colors.bright}${colors.fg.red}[Error]:${colors.reset} ${content}`, + content: `${colors.bright}${colors.fg.red}Error${colors.reset} ${content}`, logsPath: devLogs.path, logFile: "error.log", }); if (throwErr === true) { throw new Error( - `${colors.bright}${colors.fg.red}[Error]:${colors.reset} ${content}` + `${colors.fg.offWhite}${currentLocalDate}${colors.reset} ${colors.bright}${colors.fg.red}Error${colors.reset} ${content}` ); } else { console.error( - `${colors.bright}${colors.fg.red}[Error]:${colors.reset} ${content}` + `${colors.fg.offWhite}${currentLocalDate}${colors.reset} ${colors.bright}${colors.fg.red}Error${colors.reset} ${content}` ); } } else { if (throwErr === true) { throw new Error( - `${colors.bright}${colors.fg.red}[Error]:${colors.reset} ${content}` - ); - } else { - console.error( - `${colors.bright}${colors.fg.red}[Error]:${colors.reset} ${content}` + `${colors.fg.offWhite}${currentLocalDate}${colors.reset} ${colors.bright}${colors.fg.red}Error${colors.reset} ${content}` ); } } @@ -105,16 +101,12 @@ export function logSuccess({ }): void { if (devLogs?.enable === true) { logToFile({ - content: `${colors.bright}${colors.fg.green}[Successful]:${colors.reset} ${content}`, + content: `${colors.bright}${colors.fg.green}Successful${colors.reset} ${content}`, logsPath: devLogs.path, logFile: "success.log", }); console.log( - `${colors.bright}${colors.fg.green}[Successful]:${colors.reset} ${content}` - ); - } else { - console.log( - `${colors.bright}${colors.fg.green}[Successful]:${colors.reset} ${content}` + `${colors.fg.offWhite}${currentLocalDate}${colors.reset} ${colors.bright}${colors.fg.green}Successful${colors.reset} ${content}` ); } } @@ -132,22 +124,18 @@ export function logWarning({ }): void { if (devLogs?.enable === true) { logToFile({ - content: `${colors.bright}${colors.fg.yellow}[Warning]:${colors.reset} ${content}`, + content: `${colors.bright}${colors.fg.yellow}Warning${colors.reset} ${content}`, logsPath: devLogs.path, logFile: "warning.log", }); console.warn( - `${colors.bright}${colors.fg.yellow}[Warning]:${colors.reset} ${content}` - ); - } else { - console.warn( - `${colors.bright}${colors.fg.yellow}[Warning]:${colors.reset} ${content}` + `${colors.fg.offWhite}${currentLocalDate}${colors.reset} ${colors.bright}${colors.fg.yellow}Warning${colors.reset} ${content}` ); } } /** - * @param content the content to log Info it + * @param content the content to loginfoit */ export function logInfo({ content, @@ -159,16 +147,12 @@ export function logInfo({ }): void { if (devLogs?.enable === true) { logToFile({ - content: `${colors.bright}${colors.fg.blue}[Info]:${colors.reset} ${content}`, + content: `${colors.bright}${colors.fg.blue}info${colors.reset} ${content}`, logsPath: devLogs.path, logFile: "info.log", }); console.info( - `${colors.bright}${colors.fg.blue}[Info]:${colors.reset} ${content}` - ); - } else { - console.info( - `${colors.bright}${colors.fg.blue}[Info]:${colors.reset} ${content}` + `${colors.fg.offWhite}${currentLocalDate}${colors.reset} ${colors.bright}${colors.fg.blue}info${colors.reset} ${content}` ); } } diff --git a/src/core/functions/operations.ts b/src/core/functions/operations.ts new file mode 100644 index 0000000..59ea6c5 --- /dev/null +++ b/src/core/functions/operations.ts @@ -0,0 +1,788 @@ +export const opSet = (doc: any, update: any) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + if (key.includes('[') && key.includes(']')) { + const parts = key.split(/[\[\].]+/).filter(Boolean); + let target = doc; + while (parts.length > 1) { + const part = parts.shift(); + if (part !== undefined) { + if (!target[part]) { + target[part] = isNaN(Number(parts[0])) ? {} : []; + } + target = target[part]; + } + } + const lastPart = parts[0]; + if (lastPart !== undefined) { + target[lastPart] = update[key]; + } + } else { + doc[key] = update[key]; + } + } + } + }; + +export const opUnset = (doc: any, update: any) => { + const unsetValue = (target: any, key: string) => { + if (Array.isArray(target)) { + target.forEach((item: any) => { + if (item && typeof item === 'object') { + delete item[key]; + } + }); + } else if (typeof target === 'object' && target !== null) { + delete target[key]; + } + }; + + for (const key in update) { + if (update.hasOwnProperty(key)) { + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + throw new Error(`Invalid path for $unset operation: ${key}`); + } + if (!target[index]) { + target[index] = {}; + } + target = target[index]; + } else { + if (!target[part]) { + target[part] = {}; + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + unsetValue(target, lastPart); + } + } +}; + +export const opPush = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + let value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + target[part] = []; + } else { + throw new Error(`Invalid path for $push operation: ${key}`); + } + } + if (!target[index]) { + target[index] = {}; + } + target = target[index]; + } else { + if (!target[part]) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (value && typeof value === 'object' && value.$each) { + value = value.$each; + } + + if (Array.isArray(target[lastPart])) { + if (Array.isArray(value)) { + target[lastPart].push(...value); + } else { + target[lastPart].push(value); + } + } else if (upsert) { + if (Array.isArray(value)) { + target[lastPart] = target[lastPart] ? [].concat(target[lastPart], ...value) : [...value]; + } else { + target[lastPart] = target[lastPart] ? [].concat(target[lastPart], value) : [value]; + } + } else { + throw new Error(`Invalid path for $push operation: ${key}`); + } + } + } +}; + +export const opPull = (doc: any, update: any) => { + const applyPull = (target: any, value: any) => { + if (Array.isArray(value)) { + value.forEach(val => { + const index = target.indexOf(val); + if (index > -1) { + target.splice(index, 1); + } + }); + } else if (typeof value === 'object' && value !== null) { + if (value.$each && Array.isArray(value.$each)) { + value.$each.forEach((val: any) => { + const index = target.indexOf(val); + if (index > -1) { + target.splice(index, 1); + } + }); + } else if (value.$all && Array.isArray(value.$all)) { + value.$all.forEach((val: any) => { + target = target.filter((item: any) => item !== val); + }); + } else { + target = target.filter((item: any) => { + if (typeof item === 'object' && item !== null) { + return !Object.keys(value).every(k => item[k] === value[k]); + } else { + return item !== value; + } + }); + } + } else { + const index = target.indexOf(value); + if (index > -1) { + target.splice(index, 1); + } + } + return target; + }; + + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + throw new Error(`Invalid path for $pull operation: ${key}`); + } + if (!target[index]) { + target[index] = {}; + } + target = target[index]; + } else { + if (!target[part]) { + target[part] = {}; + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (Array.isArray(target[lastPart])) { + target[lastPart] = applyPull(target[lastPart], value); + } else { + throw new Error(`Invalid path for $pull operation: ${key}`); + } + } + } +}; + +export const opRename = (doc: any, update: any) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const newKey = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + throw new Error(`Invalid path for $rename operation: ${key}`); + } + if (!target[index]) { + throw new Error(`Field to rename does not exist: ${key}`); + } + target = target[index]; + } else { + if (!target[part]) { + throw new Error(`Invalid path for $rename operation: ${key}`); + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (!target.hasOwnProperty(lastPart)) { + throw new Error(`Field to rename does not exist: ${key}`); + } + target[newKey] = target[lastPart]; + delete target[lastPart]; + } + } +}; + +export const opAddToSet = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + target[part] = []; + } else { + throw new Error(`Invalid path for $addToSet operation: ${key}`); + } + } + if (!target[index]) { + target[index] = {}; + } + target = target[index]; + } else { + if (!target[part]) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (!Array.isArray(target[lastPart])) { + if (upsert) { + target[lastPart] = []; + } else { + throw new Error(`Invalid path for $addToSet operation: ${key}`); + } + } + + if (value.$each && Array.isArray(value.$each)) { + for (const item of value.$each) { + if (typeof item === 'object') { + const exists = target[lastPart].some((existingItem: any) => { + return JSON.stringify(existingItem) === JSON.stringify(item); + }); + if (!exists) { + target[lastPart].push(item); + } + } else if (!target[lastPart].includes(item)) { + target[lastPart].push(item); + } + } + } else if (value.$all && Array.isArray(value.$all)) { + for (const item of value.$all) { + if (typeof item === 'object') { + const exists = target[lastPart].some((existingItem: any) => { + return JSON.stringify(existingItem) === JSON.stringify(item); + }); + if (!exists) { + target[lastPart].push(item); + } + } else if (!target[lastPart].includes(item)) { + target[lastPart].push(item); + } + } + } else { + if (typeof value === 'object') { + const exists = target[lastPart].some((existingItem: any) => { + return JSON.stringify(existingItem) === JSON.stringify(value); + }); + if (!exists) { + target[lastPart].push(value); + } + } else if (!target[lastPart].includes(value)) { + target[lastPart].push(value); + } + } + } + } +}; + +export const opMin = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + target[part] = []; + } else { + throw new Error(`Invalid path for $min operation: ${key}`); + } + } + if (!target[index]) { + if (upsert) { + target[index] = {}; + } else { + throw new Error(`Invalid path for $min operation: ${key}`); + } + } + target = target[index]; + } else { + if (!target[part]) { + if (upsert) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } else { + throw new Error(`Invalid path for $min operation: ${key}`); + } + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (target[lastPart] === undefined || target[lastPart] > value) { + target[lastPart] = value; + } + } + } +}; + +export const opMax = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + target[part] = []; + } else { + throw new Error(`Invalid path for $max operation: ${key}`); + } + } + if (!target[index]) { + if (upsert) { + target[index] = {}; + } else { + throw new Error(`Invalid path for $max operation: ${key}`); + } + } + target = target[index]; + } else { + if (!target[part]) { + if (upsert) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } else { + throw new Error(`Invalid path for $max operation: ${key}`); + } + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (target[lastPart] === undefined || target[lastPart] < value) { + target[lastPart] = value; + } + } + } +}; + +export const opMul = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + target[part] = []; + } else { + throw new Error(`Invalid path for $mul operation: ${key}`); + } + } + if (!target[index]) { + if (upsert) { + target[index] = {}; + } else { + throw new Error(`Invalid path for $mul operation: ${key}`); + } + } + target = target[index]; + } else { + if (!target[part]) { + if (upsert) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } else { + throw new Error(`Invalid path for $mul operation: ${key}`); + } + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (typeof target[lastPart] === 'number') { + target[lastPart] *= value; + } else if (upsert) { + target[lastPart] = value; + } else { + throw new Error(`Invalid target for $mul operation: ${key}`); + } + } + } +}; + +export const opInc = (doc: any, update: any, upsert?: boolean) => { + const incrementValue = (target: any, key: string, value: any) => { + if (typeof target[key] === 'number') { + target[key] += value; + } else if (Array.isArray(target[key])) { + target[key] = target[key].map((item: any) => { + if (typeof item === 'number') { + return item + value; + } + return item; + }); + } else if (typeof target[key] === 'object' && target[key] !== null) { + opInc(target[key], { ...value }, upsert); + } else if (upsert) { + opSet(doc, update); + return; + } else { + throw new Error(`Invalid target for $inc operation: ${key}`); + } + }; + + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\].]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + opSet(doc, update); + target = target[index]; + return + } else { + throw new Error(`Invalid path for $inc operation: ${key}`); + } + } else { + if (!target[index]) { + if (upsert) { + opSet(doc, update); + target = target[index]; + return + } else { + throw new Error(`Invalid path for $inc operation: ${key}`); + } + } else { + target = target[index]; + } + } + } else { + if (!target[part]) { + if (upsert) { + opSet(doc, update); + target = target[part]; + return + } else { + throw new Error(`Invalid path for $inc operation: ${key}`); + } + } else { + target = target[part]; + } + } + } + + const lastPart = parts[parts.length - 1]; + const arrayIndex = parseInt(lastPart, 10); + if (!isNaN(arrayIndex) && Array.isArray(target)) { + if (upsert && !target[arrayIndex]) { + target[arrayIndex] = {}; + } + const nestedTarget = target[arrayIndex]; + if (nestedTarget && typeof nestedTarget === 'object') { + incrementValue(nestedTarget, '', value); + } else if (upsert) { + opSet(doc, update); + return + } else { + throw new Error(`Invalid path for $inc operation: ${key}`); + } + } else { + incrementValue(target, lastPart, value); + } + } + } +}; + +export const opBit = (doc: any, update: any, upsert = false) => { + const applyBitOperation = (currentValue: number, bitUpdate: any) => { + for (const op in bitUpdate) { + if (bitUpdate.hasOwnProperty(op)) { + const value = bitUpdate[op]; + switch (op) { + case "and": + currentValue &= value; + break; + case "or": + currentValue |= value; + break; + case "xor": + currentValue ^= value; + break; + default: + throw new Error(`Invalid bitwise operation: ${op}`); + } + } + } + return currentValue; + }; + + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + target[part] = []; + } else { + throw new Error(`Invalid path for $bit operation: ${key}`); + } + } + if (!target[index]) { + if (upsert) { + target[index] = {}; + } else { + throw new Error(`Invalid path for $bit operation: ${key}`); + } + } + target = target[index]; + } else { + if (!target[part]) { + if (upsert) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } else { + throw new Error(`Invalid path for $bit operation: ${key}`); + } + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (typeof target[lastPart] === 'number') { + target[lastPart] = applyBitOperation(target[lastPart], value); + } else if (upsert) { + target[lastPart] = applyBitOperation(0, value); + } else { + throw new Error(`Invalid target for $bit operation: ${key}`); + } + } + } +}; + +export const opPop = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\].]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const index = parseInt(part, 10); + + if (!isNaN(index)) { + if (!Array.isArray(target)) { + if (upsert) { + target[part] = []; + } else { + throw new Error(`Invalid path for $pop operation: ${key}`); + } + } + if (!target[index]) { + if (upsert) { + target[index] = {}; + } else { + throw new Error(`Invalid path for $pop operation: ${key}`); + } + } + target = target[index]; + } else { + if (!target[part]) { + if (upsert) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } else { + throw new Error(`Invalid path for $pop operation: ${key}`); + } + } + target = target[part]; + } + } + + const lastPart = parts[parts.length - 1]; + if (!Array.isArray(target[lastPart])) { + throw new Error(`Target for $pop operation is not an array: ${key}`); + } + + if (value === 1) { + target[lastPart].pop(); + } else if (value === -1) { + target[lastPart].shift(); + } else { + throw new Error(`Invalid value for $pop operation: ${value}`); + } + } + } +}; + +export const opCurrentDate = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\].]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!target[part]) { + if (upsert) { + target[part] = {}; + } else { + throw new Error(`Invalid path for $currentDate operation: ${key}`); + } + } + target = target[part]; + } + + const lastPart = parts[parts.length - 1]; + if (value === true) { + target[lastPart] = new Date().toLocaleString(); + } else if (value && value.$type === 'date') { + target[lastPart] = new Date().toLocaleString(); + } else if (value && value.$type === 'timestamp') { + target[lastPart] = Date.now(); + } else { + throw new Error(`Invalid value for $currentDate operation: ${value}`); + } + } + } +}; + +export const opSlice = (doc: any, update: any, upsert?: boolean) => { + const sliceArray = (array: any[], value: number) => { + if (!Array.isArray(array)) { + throw new Error(`$slice operation can only be applied to arrays`); + } + if (value === 0 || value < 0) { + throw new Error(`Invalid value for $slice operation: ${value}`); + } + return array.slice(0, value); + }; + + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\].]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!target[part]) { + if (upsert) { + target[part] = isNaN(Number(parts[i + 1])) ? {} : []; + } else { + throw new Error(`Invalid path for $slice operation: ${key}`); + } + } + target = target[part]; + } + + const lastPart = parts[parts.length - 1]; + if (!Array.isArray(target[lastPart])) { + throw new Error(`Invalid path for $slice operation: ${key}`); + } + + target[lastPart] = sliceArray(target[lastPart], value); + } + } +}; + +export const opSort = (doc: any, update: any, upsert?: boolean) => { + for (const key in update) { + if (update.hasOwnProperty(key)) { + const value = update[key]; + const parts = key.split(/[\[\]\.]+/).filter(Boolean); + let target = doc; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!target[part]) { + throw new Error(`Invalid path for $sort operation: ${key}`); + } + target = target[part]; + } + + const lastPart = parts[parts.length - 1]; + if (!Array.isArray(target[lastPart])) { + throw new Error(`Invalid path for $sort operation: ${key}`); + } + + if (value !== 1 && value !== -1) { + throw new Error(`Invalid sort order for $sort operation: ${value}`); + } + + target[lastPart].sort((a: any, b: any) => { + return a - b; + }); + + if (value === -1) { + target[lastPart].reverse(); + } + } + } +}; diff --git a/src/core/functions/schema.ts b/src/core/functions/schema.ts new file mode 100644 index 0000000..a47b555 --- /dev/null +++ b/src/core/functions/schema.ts @@ -0,0 +1,273 @@ +export enum SchemaTypes { + String = "String", + Number = "Number", + Boolean = "Boolean", + Array = "Array", + Object = "Object", + Null = "Null", + Enum = "Enum", + Custom = "Custom", + Mix = "Mix", + Union = "Union", + Any = "Any", +} + +export interface FieldConfig { + type: SchemaTypes | string; + required?: boolean; + minlength?: number; + maxlength?: number; + min?: number; + max?: number; + validate?: (value: any) => boolean | string | Promise; // Asynchronous validation support + unique?: boolean; + default?: any; + schema?: { [key: string]: FieldConfig }; + alias?: string; + mix?: SchemaTypes[]; +} + +export default class Schema { + readonly fields: { [key: string]: FieldConfig }; + + constructor(fields: { [key: string]: FieldConfig }) { + this.fields = {}; + + for (const fieldName in fields) { + const fieldConfig = fields[fieldName]; + this.fields[fieldName] = fieldConfig; + if (fieldConfig.alias) { + this.fields[fieldConfig.alias] = fieldConfig; + } + } + } + + async validate(data: { [key: string]: any }, existingData: { [key: string]: any }[] | null = null): Promise<{ [key: string]: string } | null> { + const errors: { [key: string]: string } = {}; + + for (const field in this.fields) { + const fieldConfig = this.fields[field]; + if (!data.hasOwnProperty(field) && fieldConfig.default !== undefined) { + data[field] = fieldConfig.default; + } + } + + for (const field in this.fields) { + const fieldConfig = this.fields[field]; + const value = data[field]; + const expectedType = fieldConfig.type; + const actualType = Array.isArray(value) ? "Array" : typeof value; + + if (fieldConfig.required && (value === undefined || value === null)) { + errors[field] = `Field '${field}' is required.`; + continue; + } + + switch (expectedType) { + case SchemaTypes.String: + case "String": + this.validateString(field, value, fieldConfig, errors); + break; + case SchemaTypes.Number: + case "Number": + this.validateNumber(field, value, fieldConfig, errors); + break; + case SchemaTypes.Boolean: + case "Boolean": + this.validateBoolean(field, value, errors); + break; + case SchemaTypes.Null: + case "Null": + this.validateNull(field, value, errors); + break; + case SchemaTypes.Object: + case "Object": + this.validateObject(field, value, fieldConfig, errors); + break; + case SchemaTypes.Array: + case "Array": + this.validateArray(field, value, fieldConfig, errors); + break; + case SchemaTypes.Custom: + case "Custom": + this.validateCustom(field, value, fieldConfig, errors); + break; + case SchemaTypes.Mix: + case "Mix": + this.validateMix(field, value, fieldConfig, errors); + break; + case SchemaTypes.Union: + case "Union": + this.validateUnion(field, value, fieldConfig, errors); + break; + case SchemaTypes.Any: + case "Any": + break; + default: + throw new Error("Invalid SchemaTypes."); + } + + } + + return Object.keys(errors).length === 0 ? null : errors; + } + + private validateMix(field: string, value: any, fieldConfig: FieldConfig, errors: { [key: string]: string }) { + const allowedTypes = fieldConfig.mix || []; + + let isValid = false; + + for (const type of allowedTypes) { + switch (type) { + case SchemaTypes.String: + case "String": + this.validateString(field, value, fieldConfig, errors); + break; + case SchemaTypes.Number: + case "Number": + this.validateNumber(field, value, fieldConfig, errors); + break; + case SchemaTypes.Boolean: + case "Boolean": + this.validateBoolean(field, value, errors); + break; + case SchemaTypes.Null: + case "Null": + this.validateNull(field, value, errors); + break; + case SchemaTypes.Object: + case "Object": + this.validateObject(field, value, fieldConfig, errors); + break; + case SchemaTypes.Array: + case "Array": + this.validateArray(field, value, fieldConfig, errors); + break; + case SchemaTypes.Custom: + case "Custom": + this.validateCustom(field, value, fieldConfig, errors); + break; + + case SchemaTypes.Union: + case "Union": + this.validateUnion(field, value, fieldConfig, errors); + break; + case SchemaTypes.Any: + case "Any": + break; + case SchemaTypes.Mix: + case "Mix": + throw new Error("Mix validation cannot be nested."); + default: + throw new Error("Invalid SchemaTypes."); + } + + if (!errors[field]) { + isValid = true; + break; + } else { + delete errors[field]; + } + } + + if (!isValid) { + errors[field] = `Field '${field}' must be one of the specified types: ${allowedTypes.join(', ')}`; + } + } + + private validateString(field: string, value: any, fieldConfig: FieldConfig, errors: { [key: string]: string }) { + if (typeof value !== 'string') { + errors[field] = `Field '${field}' must be of type 'String'.`; + return; + } + if (fieldConfig.minlength !== undefined && value.length < fieldConfig.minlength) { + errors[field] = `Field '${field}' must have at least ${fieldConfig.minlength} characters.`; + } + if (fieldConfig.maxlength !== undefined && value.length > fieldConfig.maxlength) { + errors[field] = `Field '${field}' must have at most ${fieldConfig.maxlength} characters.`; + } + } + + private validateNumber(field: string, value: any, fieldConfig: FieldConfig, errors: { [key: string]: string }) { + if (typeof value !== 'number') { + errors[field] = `Field '${field}' must be of type 'Number'.`; + return; + } + if (fieldConfig.min !== undefined && value < fieldConfig.min) { + errors[field] = `Field '${field}' must be at least ${fieldConfig.min}.`; + } + if (fieldConfig.max !== undefined && value > fieldConfig.max) { + errors[field] = `Field '${field}' must be at most ${fieldConfig.max}.`; + } + } + + private validateBoolean(field: string, value: any, errors: { [key: string]: string }) { + if (typeof value !== 'boolean') { + errors[field] = `Field '${field}' must be of type 'Boolean'.`; + } + } + + private validateNull(field: string, value: any, errors: { [key: string]: string }) { + if (value !== null) { + errors[field] = `Field '${field}' must be of type 'Null'.`; + } + } + + private validateObject(field: string, value: any, fieldConfig: FieldConfig, errors: { [key: string]: string }) { + if (typeof value !== 'object' || Array.isArray(value)) { + errors[field] = `Field '${field}' must be of type 'Object'.`; + return; + } + if (fieldConfig.schema) { + const nestedSchema = new Schema(fieldConfig.schema); + const nestedErrors = nestedSchema.validate(value); + if (nestedErrors) { + errors[field] = `Field '${field}' has invalid nested object: ${JSON.stringify(nestedErrors)}`; + } + } + } + + private validateArray(field: string, value: any, fieldConfig: FieldConfig, errors: { [key: string]: string }) { + if (!Array.isArray(value)) { + errors[field] = `Field '${field}' must be of type 'Array'.`; + return; + } + if (fieldConfig.minlength !== undefined && value.length < fieldConfig.minlength) { + errors[field] = `Field '${field}' must have at least ${fieldConfig.minlength} items.`; + } + if (fieldConfig.maxlength !== undefined && value.length > fieldConfig.maxlength) { + errors[field] = `Field '${field}' must have at most ${fieldConfig.maxlength} items.`; + } + if (fieldConfig.schema) { + const nestedSchema = new Schema(fieldConfig.schema); + for (let i = 0; i < value.length; i++) { + const nestedErrors = nestedSchema.validate(value[i]); + if (nestedErrors) { + errors[field] = `Field '${field}' has invalid nested object at index ${i}: ${JSON.stringify(nestedErrors)}`; + break; + } + } + } + } + + private validateCustom(field: string, value: any, fieldConfig: FieldConfig, errors: { [key: string]: string }) { + const customValidationResult = fieldConfig.validate ? fieldConfig.validate(value) : true; + if (customValidationResult !== true) { + errors[field] = customValidationResult as string; + } + } + + private validateUnion(field: string, value: any, fieldConfig: FieldConfig, errors: { [key: string]: string }) { + const unionTypes = Array.isArray(fieldConfig.schema) ? fieldConfig.schema : []; + let isValid = false; + for (const type of unionTypes) { + if (type === typeof value) { + isValid = true; + break; + } + } + if (!isValid) { + errors[field] = `Field '${field}' must be one of the specified types: ${unionTypes.join(', ')}`; + } + } +} diff --git a/src/core/secureData.ts b/src/core/functions/secureData.ts similarity index 77% rename from src/core/secureData.ts rename to src/core/functions/secureData.ts index 89a82fe..bd8dfb7 100644 --- a/src/core/secureData.ts +++ b/src/core/functions/secureData.ts @@ -180,88 +180,93 @@ export async function decodeJSON( } } -function encrypt(data: Buffer, key: string): Buffer { - const keyBuffer = Buffer.from(key); - for (let i = 0; i < data.length; i++) { - data[i] ^= keyBuffer[i % keyBuffer.length]; - } - return data; -} - -function decrypt(data: Buffer, key: string): Buffer { - return encrypt(data, key); -} -export async function encodeYAML(yamlData: any, key: string): Promise { - const yamlString = yaml.stringify(yamlData); - const data = yaml.parse(yamlString); - const stringFiedData = yaml.stringify(data); - const compressedData = Buffer.from(stringFiedData, "utf-8"); - return encrypt(compressedData, key); +export async function encodeYAML(data: any, key: string): Promise { + try { + const stringedData = yaml.stringify(data); + const encryptedData = yamlEncrypt(stringedData, key); + return Buffer.from(encryptedData); + } catch (error: any) { + throw new Error(`Error occurred while encoding YAML data: ${error.message}`); + } } -export async function decodeYAML(filePath: string, key: string): Promise { +export async function decodeYAML(filePath: string, key: string): Promise { try { const buffer = fs.readFileSync(filePath); - if (buffer.length === 0) { - return []; - } - const decryptedData = decrypt(buffer, key); - const yamlData = decryptedData.toString("utf-8"); - return yaml.parse(yamlData); - } catch (e: any) { + const decryptedData = yamlDecrypt(buffer.toString(), key); + const parsedData = yaml.parse(decryptedData); + return parsedData; + } catch (error: any) { return null; } } + +function yamlEncrypt(data: string, key: string): string { + let encrypted = ''; + for (let i = 0; i < data.length; i++) { + const charCode = data.charCodeAt(i) ^ key.charCodeAt(i % key.length); + encrypted += String.fromCharCode(charCode); + } + return encrypted; +} + +function yamlDecrypt(data: string, key: string): string { + return yamlEncrypt(data, key); +} + export async function encodeSQL(data: string, key: string): Promise { let compressedEncodedData = ""; let count = 1; for (let i = 0; i < data.length; i++) { - if (data[i] === data[i + 1]) { - count++; - } else { - compressedEncodedData += count + data[i]; - count = 1; - } + if (data[i] === data[i + 1]) { + count++; + } else { + if (count > 3) { + compressedEncodedData += `#${count}#${data[i]}`; + } else { + compressedEncodedData += data[i].repeat(count); + } + count = 1; + } } let encodedData = ""; for (let i = 0; i < compressedEncodedData.length; i++) { - const charCode = - compressedEncodedData.charCodeAt(i) ^ key.charCodeAt(i % key.length); - encodedData += String.fromCharCode(charCode); + const charCode = compressedEncodedData.charCodeAt(i) ^ key.charCodeAt(i % key.length); + encodedData += String.fromCharCode(charCode); } return encodedData; } -export async function decodeSQL( - encodedData: string, - key: string -): Promise { - try { - let decodedData = ""; - for (let i = 0; i < encodedData.length; i++) { - const charCode = - encodedData.charCodeAt(i) ^ key.charCodeAt(i % key.length); +export async function decodeSQL(encodedData: string, key: string): Promise { + let decodedData = ""; + for (let i = 0; i < encodedData.length; i++) { + const charCode = encodedData.charCodeAt(i) ^ key.charCodeAt(i % key.length); decodedData += String.fromCharCode(charCode); - } - - let decompressedData = ""; - let i = 0; - while (i < decodedData.length) { - const count = parseInt(decodedData[i]); - const char = decodedData[i + 1]; - decompressedData += char.repeat(count); - i += 2; - } + } - return decompressedData; - } catch (e: any) { - return null; + let decompressedData = ""; + let i = 0; + while (i < decodedData.length) { + if (decodedData[i] === '#') { + const countStartIndex = i + 1; + const countEndIndex = decodedData.indexOf('#', countStartIndex); + const count = parseInt(decodedData.substring(countStartIndex, countEndIndex)); + const char = decodedData[countEndIndex + 1]; + decompressedData += char.repeat(count); + i = countEndIndex + 2; + } else { + decompressedData += decodedData[i]; + i++; + } } + + return decompressedData; } + export async function neutralizer(folderPath: string, info: { dataType: "json" | "yaml" | "sql", secret: string }): Promise { const foundFiles: string[] = []; @@ -320,4 +325,14 @@ export async function neutralizer(folderPath: string, info: { dataType: "json" | } return foundFiles; +} + + +export function genObjectId(): string { + const timestamp = Math.floor(Date.now() / 1000).toString(16).padStart(8, '0'); + const machineId = 'abcdef'.split('').map(() => Math.floor(Math.random() * 16).toString(16)).join(''); + const processId = Math.floor(Math.random() * 65536).toString(16).padStart(4, '0'); + const counter = Math.floor(Math.random() * 16777216).toString(16).padStart(6, '0'); + + return timestamp + machineId + processId + counter; } \ No newline at end of file diff --git a/src/core/schema.ts b/src/core/schema.ts deleted file mode 100644 index c580f62..0000000 --- a/src/core/schema.ts +++ /dev/null @@ -1,180 +0,0 @@ -"use strict"; - -// Define SchemaTypes enum -export enum SchemaTypes { - String = "String", - Number = "Number", - Boolean = "Boolean", - Array = "Array", - Object = "Object", - Null = "Null", - Undefined = "Undefined", - Date = "Date", - Enum = "Enum", - Custom = "Custom", - Union = "Union", - Any = "Any", - Color = "Color", - URL = "Url", -} - -/** - * Represents the configuration for a field in a data schema. - */ -export interface FieldConfig { - type: SchemaTypes; - required?: boolean; - minlength?: number; - maxlength?: number; - min?: number; - max?: number; - validate?: (value: any) => boolean; - unique?: boolean; -} - -/** - * Represents a data schema. - */ -export default class Schema { - readonly fields: { [key: string]: FieldConfig }; - - constructor(fields: { [key: string]: FieldConfig }) { - this.fields = fields; - } - - validate( - data: { [key: string]: any }, - existingData: any[] | null = null - ): { [key: string]: string } | null { - const errors: { [key: string]: string } = {}; - - for (const field in this.fields) { - const fieldConfig = this.fields[field]; - const schemaType = fieldConfig.type; - const value = data[field]; - - if (fieldConfig.required && (value === undefined || value === null)) { - errors[field] = "This field is required."; - } else if ( - ["String", "Number", "Boolean"].includes(schemaType) && - typeof value !== schemaType.toLowerCase() - ) { - errors[ - field - ] = `Invalid type. Expected ${schemaType}, got ${typeof value}.`; - } else if (schemaType === "Array" && !Array.isArray(value)) { - errors[field] = `Invalid type. Expected Array, got ${typeof value}.`; - } else if (schemaType === "Object" && typeof value !== "object") { - errors[field] = `Invalid type. Expected Object, got ${typeof value}.`; - } else if (schemaType === "Null" && value !== null) { - errors[field] = `Invalid type. Expected Null, got ${typeof value}.`; - } else if (schemaType === "Undefined" && value !== undefined) { - errors[ - field - ] = `Invalid type. Expected Undefined, got ${typeof value}.`; - } else if ( - schemaType === "String" && - fieldConfig.minlength && - typeof value === "string" && - value.length < fieldConfig.minlength - ) { - errors[ - field - ] = `Must be at least ${fieldConfig.minlength} characters long.`; - } else if ( - schemaType === "String" && - fieldConfig.maxlength && - typeof value === "string" && - value.length > fieldConfig.maxlength - ) { - errors[ - field - ] = `Must be at most ${fieldConfig.maxlength} characters long.`; - } else if ( - schemaType === "Number" && - fieldConfig.min !== undefined && - typeof value === "number" && - value < fieldConfig.min - ) { - errors[field] = `Must be greater than or equal to ${fieldConfig.min}.`; - } else if ( - schemaType === "Number" && - fieldConfig.max !== undefined && - typeof value === "number" && - value > fieldConfig.max - ) { - errors[field] = `Must be less than or equal to ${fieldConfig.max}.`; - } else if (schemaType === "Date" && !(value instanceof Date)) { - errors[field] = `Invalid type. Expected Date, got ${typeof value}.`; - } else if (schemaType === "Color" && !isValidColor(value)) { - errors[field] = `Invalid color value for ${field}.`; - } else if (schemaType === "Url" && !isValidURL(value)) { - errors[field] = `Invalid URL format for ${field}.`; - } else if ( - schemaType === "Enum" && - fieldConfig.validate && - !fieldConfig.validate(value) - ) { - errors[ - field - ] = `Value ${value} is not a valid enum value for ${field}.`; - } else if ( - schemaType === "Custom" && - fieldConfig.validate && - !fieldConfig.validate(value) - ) { - errors[field] = `Validation failed for ${field}.`; - } else if ( - schemaType === "Union" && - fieldConfig.validate && - !fieldConfig.validate(value) - ) { - errors[ - field - ] = `Value ${value} does not match any of the types in the union for ${field}.`; - } else if (schemaType === "Any") { - } else if (fieldConfig.unique && existingData) { - const hasDuplicate = existingData.some( - (item: any) => item[field] === value - ); - if (hasDuplicate) { - errors[field] = "This value must be unique."; - } - } - } - - return Object.keys(errors).length === 0 ? null : errors; - } -} - -function isValidColor(value: any): boolean { - if (typeof value !== "string") { - return false; - } - - const hexPattern = /^#(?:[0-9a-fA-F]{3}){1,2}$/; - const rgbPattern = - /^rgba?\(\d{1,3},\s*\d{1,3},\s*\d{1,3}(,\s*\d+(\.\d+)?)?\)$/; - const hslPattern = - /^hsla?\(\s*\d+(\.\d+)?\s*,\s*\d+(\.\d+)?%\s*,\s*\d+(\.\d+)?%\s*(,\s*\d+(\.\d+)?)?\)$/; - const namedColorPattern = /^(?:[a-z]+)$/i; - - return ( - hexPattern.test(value) || - rgbPattern.test(value) || - hslPattern.test(value) || - namedColorPattern.test(value) - ); -} - -function isValidURL(value: any): boolean { - if (typeof value !== "string") { - return false; - } - - const urlPattern = - /^(?:(?:https?|ftp):\/\/)?(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:\/[^\s]*)?(?:\.(?:jpg|jpeg|png|gif|bmp|tiff|svg|webp|ico|mp4|mov|avi|mkv|wmv|flv|webm|mp3|wav|ogg|m4a|pdf|doc|docx|ppt|pptx|xls|xlsx|txt|rtf|csv|zip|rar|tar|7z))$/i; - const emailPattern = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/; - - return urlPattern.test(value) || emailPattern.test(value); -} diff --git a/src/index.ts b/src/index.ts index 7514041..d4dcf7f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,6 @@ * @params Copyright(c) 2023 marco5dev & elias79 & kmoshax * MIT Licensed */ - -import axios from "axios"; import * as path from "path"; import * as fs from "fs"; import { @@ -14,12 +12,19 @@ import { encodeSQL, decodeSQL, neutralizer, -} from "./core/secureData"; + genObjectId, +} from "./core/functions/secureData"; +import { verseManagers, Connect } from "./types/versedb.types"; import connect from "./core/connect"; import { randomID, randomUUID } from "./lib/id"; -import { logError, logInfo, logSuccess, logWarning } from "./core/logger"; -import Schema from "./core/schema"; -import { SchemaTypes } from "./core/schema"; +import { + logError, + logInfo, + logSuccess, + logWarning, +} from "./core/functions/logger"; +import Schema from "./core/functions/schema"; +import { SchemaTypes } from "./core/functions/schema"; import colors from "./lib/colors"; const packageJsonPath: string = path.resolve(process.cwd(), "package.json"); @@ -36,20 +41,25 @@ const getLibraryVersion = function (library: string): string { return version; }; -axios - .get("https://registry.npmjs.com/-/v1/search?text=verse.db") - .then(function (response: any) { - const version: string = response.data.objects[0]?.package?.version; +fetch("https://registry.npmjs.com/-/v1/search?text=verse.db") + .then(function (response) { + if (!response.ok) { + throw new Error("Failed to fetch"); + } + return response.json(); + }) + .then(function (data) { + const version = data.objects[0]?.package?.version; if (version && getLibraryVersion("verse.db") !== version) { logWarning({ content: `Please Update verse.db to the latest verseion ` + version + - ` using ${colors.fg.green}npm install verse.db@latest${colors.reset}`, + `\nusing ${colors.fg.green}npm install verse.db@latest${colors.reset}`, }); } }) - .catch(function (error: any) { + .catch(function (error) { logError({ content: error, }); @@ -70,6 +80,7 @@ const verseParser = { encodeSQL, decodeSQL, neutralizer, + genObjectId, }; const versedb = { @@ -81,6 +92,8 @@ const versedb = { SchemaTypes, verseParser, colors, + neutralizer, + genObjectId, }; export { connect, @@ -92,5 +105,9 @@ export { SchemaTypes, colors, neutralizer, + genObjectId, + Connect, + verseManagers, }; + export default versedb; diff --git a/src/lib/date.ts b/src/lib/date.ts index 321bdfe..a9401f7 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -19,4 +19,26 @@ export function formatDateTime(date: Date) { * @param currentDataString get the current data and remove the / from the format */ export const currentDate: string = formatDateTime(new Date()); -export const currentDateString: string = currentDate.replace(/\D/g, ""); \ No newline at end of file +export const currentDateString: string = currentDate.replace(/\D/g, ""); + +/** + * @returns Local Data Formate "2024/5/31, 1:44:7 AM" + */ +function getLocalFormattedDate() { + const now = new Date(); + const options: any = { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: true + }; + return now.toLocaleString('en-US', options); +} + +/** + * @returns Local Data Formate "2024/5/31, 1:44:7 AM" + */ +export const currentLocalDate = getLocalFormattedDate(); \ No newline at end of file diff --git a/src/types/adapter.ts b/src/types/adapter.ts index bb03503..f53cb2c 100644 --- a/src/types/adapter.ts +++ b/src/types/adapter.ts @@ -46,14 +46,14 @@ export interface MigrationParams { table: string; } -export interface versedbAdapter { +export interface JsonYamlAdapter { load(dataname: string): Promise; add(dataname: string, newData: any, options?: AdapterUniqueKey): Promise; - find(dataname: string, query: any): Promise; - loadAll(dataname: string, displayOptions: queryOptions): Promise; - remove(dataname: string, query: any, options?: any): Promise; - update(dataname: string, queries: any, newData: any, upsert: boolean): Promise; - updateMany(dataname: any, queries: any[any], newData: operationKeys,): Promise; + find(dataname: string, query: any, options?: any, loadedData?: any[]): Promise; + loadAll(dataname: string, displayOptions: queryOptions, loadedData?: any[]): Promise; + remove(dataname: string, query: any, options?: any, loadedData?: any[]): Promise; + update(dataname: string, queries: any, newData: any, upsert: boolean, loadedData?: any[]): Promise; + updateMany(dataname: any, queries: any[any], newData: operationKeys, loadedData?: any[]): Promise; drop(dataname: string): Promise; search(collectionFilters: CollectionFilter[]): Promise; dataSize(dataname: string): Promise; @@ -62,6 +62,7 @@ export interface versedbAdapter { calculatePolygonArea(polygonCoordinates: any): Promise; bufferZone(geometry: any, bufferDistance: any): Promise; batchTasks(operations: any[]): Promise; + aggregate(dataname: string, pipeline: any[]): Promise; moveData(from: string, to: string, options: { query?: queryOptions, dropSource?: boolean }): Promise; } @@ -105,15 +106,24 @@ export interface SearchResult { } export interface operationKeys { - $inc?: { [key: string]: number }; $set?: { [key: string]: any }; + $unset?: { [key: string]: any }; $push?: { [key: string]: any }; + $pull?: { [key: string]: any }; + $addToSet?: { [key: string]: any }; + $rename?: { [key: string]: string }; $min?: { [key: string]: any }; $max?: { [key: string]: any }; + $mul?: { [key: string]: number }; + $inc?: { [key: string]: number }; + $bit?: { [key: string]: any }; $currentDate?: { [key: string]: boolean | { $type: 'date' | 'timestamp' }}; - upsert?: boolean; + $pop?: { [key: string]: number }; + $slice?: { [key: string]: [number, number] | number }; + $sort?: { [key: string]: 1 | -1 }; } + export interface nearbyOptions { dataName: string; point: { @@ -130,4 +140,34 @@ export interface searchFilters { pageSize?: number; sortOrder?: 'asc' | 'desc'; displayment?: number | null; +} + +export interface queries { + $and?: queries[]; + $or?: queries[]; + $validate?: (value: T) => boolean; + $text?: string; + $sort?: 1 | -1; + $slice?: number | [number, number]; + $some?: boolean; + $gt?: number; + $lt?: number; + $nin?: T[]; + $exists?: boolean; + $not?: queries; + $in?: T[]; + $elemMatch?: queries; + $typeOf?: string | 'string' | 'number' | 'boolean' | 'undefined' | 'array' | 'object' | 'null' | 'any'; + $regex?: string; + $size?: number; +} + +export type Query = { + [P in keyof T]?: T[P] | queries; +}; + +export interface QueryOptions { + $skip?: number; + $limit?: number; + $project?: { [key: string]: boolean }; } \ No newline at end of file diff --git a/src/types/connect.ts b/src/types/connect.ts index 0894f2c..c166ad5 100644 --- a/src/types/connect.ts +++ b/src/types/connect.ts @@ -1,11 +1,11 @@ export interface JSONAdapter { load(dataname: string): Promise; add(dataname: string, newData: any, options?: any): Promise; - find(dataname: string, query: any, options?: any): Promise; - loadAll(dataname: string, displayOptions?: any): Promise; - remove(dataname: string, query: any, options?: any): Promise; - update(dataname: string, query: any, newData: any): Promise; - updateMany(dataname: any, queries: any[any], newData: operationKeys,): Promise; + find(dataname: string, query: any, options?: any, loadedData?: any[]): Promise; + loadAll(dataname: string, displayOptions?: any, loadedData?: any[]): Promise; + remove(dataname: string, query: any, options?: any, loadedData?: any[]): Promise; + update(dataname: string, query: any, newData: any, loadedData?: any[]): Promise; + updateMany(dataname: any, queries: any[any], newData: operationKeys, loadedData?: any[]): Promise; drop(dataname: string): Promise; nearbyVectors(data: nearbyOptions): Promise polygonArea(polygonCoordinates: any): Promise; @@ -14,17 +14,18 @@ export interface JSONAdapter { countDoc(dataname: string): Promise; dataSize(dataname: string): Promise; batchTasks(operation: any[]): Promise; + aggregate(dataname: string, pipeline: any[]): Promise; moveData(from: string, to: string, options: { query?: any, dropSource?: boolean }): Promise; model(dataname: string, schema: any): any; } export interface YAMLAdapter { load(dataname: string): Promise; add(dataname: string, newData: any, options?: any): Promise; - find(dataname: string, query: any, options?: any): Promise; - loadAll(dataname: string, displayOptions?: any): Promise; - remove(dataname: string, query: any, options?: any): Promise; - update(dataname: string, query: any, newData: any): Promise; - updateMany(dataname: any, queries: any[any], newData: operationKeys,): Promise; + find(dataname: string, query: any, options?: any, loadedData?: any[]): Promise; + loadAll(dataname: string, displayOptions?: any, loadedData?: any[]): Promise; + remove(dataname: string, query: any, options?: any, loadedData?: any[]): Promise; + update(dataname: string, query: any, newData: any, loadedData?: any[]): Promise; + updateMany(dataname: any, queries: any[any], newData: operationKeys, loadedData?: any[]): Promise; drop(dataname: string): Promise; nearbyVectors(data: nearbyOptions): Promise polygonArea(polygonCoordinates: any): Promise; @@ -106,8 +107,8 @@ export interface AdapterOptions { adapter: string; adapterType?: string | null; dataPath: string; - devLogs: DevLogsOptions; - secure: SecureSystem; + devLogs?: DevLogsOptions; + secure?: SecureSystem; backup?: BackupOptions; } @@ -136,13 +137,21 @@ export interface MigrationParams { } export interface operationKeys { - $inc?: { [key: string]: number }; $set?: { [key: string]: any }; + $unset?: { [key: string]: any }; $push?: { [key: string]: any }; + $pull?: { [key: string]: any }; + $addToSet?: { [key: string]: any }; + $rename?: { [key: string]: string }; $min?: { [key: string]: any }; $max?: { [key: string]: any }; - $currentDate?: { [key: string]: boolean | { $type: "date" | "timestamp" } }; - upsert?: boolean; + $mul?: { [key: string]: number }; + $inc?: { [key: string]: number }; + $bit?: { [key: string]: any }; + $currentDate?: { [key: string]: boolean | { $type: 'date' | 'timestamp' }}; + $pop?: { [key: string]: number }; + $slice?: { [key: string]: [number, number] | number }; + $sort?: { [key: string]: 1 | -1 }; } export interface nearbyOptions { @@ -161,3 +170,33 @@ export interface searchFilters { sortOrder?: 'asc' | 'desc'; displayment?: number | null; } + +export interface queries { + $and?: queries[]; + $or?: queries[]; + $validate?: (value: T) => boolean; + $text?: string; + $sort?: 1 | -1; + $slice?: number | [number, number]; + $some?: boolean; + $gt?: number; + $lt?: number; + $nin?: T[]; + $exists?: boolean; + $not?: queries; + $in?: T[]; + $elemMatch?: queries; + $typeOf?: string | 'string' | 'number' | 'boolean' | 'undefined' | 'array' | 'object' | 'null' | 'any'; + $regex?: string; + $size?: number; +} + +export type Query = { + [P in keyof T]?: T[P] | queries; +}; + +export interface QueryOptions { + $skip?: number; + $limit?: number; + $project?: { [key: string]: boolean }; +} \ No newline at end of file diff --git a/src/types/versedb.types.ts b/src/types/versedb.types.ts index 8d3468e..9b42867 100644 --- a/src/types/versedb.types.ts +++ b/src/types/versedb.types.ts @@ -1,22 +1,15 @@ -import { PathLike } from "fs"; +import { JSONAdapter, SQLAdapter, YAMLAdapter } from "./connect"; export interface Connect { - options: versedbOptions; - backaupFolder?: PathLike | undefined; + adapter: 'json' | 'yaml' | 'sql' | string; + dataPath: string; + devLogs?: { enable: boolean, path: string }; + secure?: { enable: boolean, secret?: string }; + backup?: any; } -export interface versedbOptions { - adapter: Adapter; - backaupFolder?: PathLike; -} - -export interface Adapter { - filePath: string; - devLogs: boolean; - logsPath?: string; -} - -export interface versedbFindOptions { - first: boolean; - limit: number; -} \ No newline at end of file +export interface verseManagers { + JsonManager?: JSONAdapter; + YamlManager?: YAMLAdapter; + SqlManager?: SQLAdapter; + } diff --git a/tests/json.test.ts b/tests/json.test.ts new file mode 100644 index 0000000..c2d3054 --- /dev/null +++ b/tests/json.test.ts @@ -0,0 +1,249 @@ +import versedb from "../src/index"; +import fs from "fs"; + +async function Setup(adapter: string): Promise { + const adapterOptions = { + adapter: `${adapter}`, + dataPath: `./tests/${adapter}/data`, + devLogs: { enable: true, path: `./tests/${adapter}/logs` }, + secure: { + enable: true, + secret: "versedb", + }, + }; + + const db = new versedb.connect(adapterOptions); + return db; +} + +async function Teardown(db: string) { + await fs.promises.rm(`./tests/${db}`, { recursive: true, force: true }); +} + +describe("JSON", () => { + let db: any; + console.log = function () {}; + console.info = function () {}; + + beforeEach(async () => { + await Setup("json"); + db = await Setup("json"); + }); + + afterEach(async () => { + await Teardown("json"); + }); + + test("add method should add new data to the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const newData = [{ name: "Mike" }]; + const dataname = "add"; + + // Act + const result = await db.add(dataname, newData); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Data added successfully.", + results: expect.anything(), + }); + }); + + test("load method should return the data from the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const dataname = "load"; + + // Act + await db.add(dataname, data); + const result = await db.load(dataname); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Data loaded successfully.", + results: [ + { _id: expect.anything(), name: "John" }, + { _id: expect.anything(), name: "Jane" }, + ], + }); + }); + + test("remove method should remove data from the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const query = { name: "John" }; + const dataname = "remove"; + + // Act + await db.add(dataname, data); + const result = await db.remove(dataname, query); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "1 document(s) removed successfully.", + results: null, + }); + }); + + test("update method should update data in the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const updateQuery = { $set: { name: "Mike" } }; + const dataname = "update"; + + // Act + await db.add(dataname, data); + const result = await db.update(dataname, { name: "John" }, updateQuery); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "1 document(s) updated successfully.", + results: { + _id: expect.anything(), + name: "Mike", + }, + }); + }); + + test("updateMany method should update data in the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const query = { name: "John" } ; + const newData = { $set: { name: "Mike" } }; + const dataname = "updateMany"; + + // Act + await db.add(dataname, data); + const result = await db.updateMany(dataname, query, newData); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "1 document(s) updated successfully.", + results: [ + { + _id: expect.anything(), + name: "Mike", + }, + ], + }); + }); + + test("find method should return the data that matches the specified query", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const query = { name: "John" }; + const dataname = "find"; + + // Act + await db.add(dataname, data); + const result = await db.find(dataname, query); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Found data matching your query.", + results: { _id: expect.anything(), name: "John" }, + }); + }); + + test("loadAll method should return all the data in the specified file", async () => { + // Arrange + const data = [ + { name: "Mark" }, + { name: "Anas" }, + { name: "Anas" }, + { name: "Mark" }, + ]; + const dataname = "loadAll"; + const displayOptions = { + filter: { + name: "Mark", + }, + sortField: "name", + sortOrder: "asc", + page: 1, + pageSize: 10, + displayment: 10, + }; + + // Act + await db.add(dataname, data); + const result = await db.loadAll(dataname, displayOptions); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Data found with the given options.", + results: { + allData: [ + { _id: expect.anything(), name: "Mark" }, + { _id: expect.anything(), name: "Mark" }, + ], + }, + }); + }); + + test("drop method should delete all the data in the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const dataname = "drop"; + + // Act + await db.add(dataname, data); + const result = await db.drop(dataname); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "File dropped successfully.", + results: [], + }); + }); + + test("search method should return the data that matches the specified query", async () => { + // Arrange + const data = [ + { name: "mark", author: "maher" }, + { name: "anas", author: "kmosha" }, + ]; + const data2 = [ + { name: "anas", author: "kmosha" }, + { name: "mark", author: "maher" }, + ]; + const collectionFilters = [ + { + dataname: "users", + displayment: 10, + filter: { name: "mark" }, + }, + { + dataname: "posts", + displayment: 5, + filter: { author: "maher" }, + }, + ]; + const dataname = "users"; + const dataname2 = "posts"; + + // Act + await db.add(dataname, data); + await db.add(dataname2, data2); + const result = await db.search(collectionFilters); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Successfully searched in data for the given query.", + results: { + posts: [{ _id: expect.anything(), author: "maher", name: "mark" }], + users: [{ _id: expect.anything(), author: "maher", name: "mark" }], + }, + }); + }); +}); \ No newline at end of file diff --git a/tests/versedb.test.ts b/tests/versedb.test.ts deleted file mode 100644 index 496451d..0000000 --- a/tests/versedb.test.ts +++ /dev/null @@ -1,511 +0,0 @@ -import versedb from "../src/index"; -import fs from "fs"; - -async function Setup(adapter: string): Promise { - const adapterOptions = { - adapter: `${adapter}`, - dataPath: `./tests/${adapter}/data`, - devLogs: { enable: true, path: `./tests/${adapter}/logs` }, - secure: { - enable: true, - secret: "versedb", - }, - }; - - const db = new versedb.connect(adapterOptions); - return db; -} - -async function Teardown(db: string) { - await fs.promises.rm(`./tests/${db}`, { recursive: true, force: true }); -} - -describe("JSON", () => { - let db: any; - console.log = function () {}; - console.info = function () {}; - - beforeEach(async () => { - await Setup("json"); - db = await Setup("json"); - }); - - afterEach(async () => { - await Teardown("json"); - }); - - test("add method should add new data to the specified file", async () => { - // Arrange - const data = [ - { name: "John" }, - { name: "Jane" }, - ]; - const newData = [{ name: "Mike" }]; - const dataname = "add"; - - // Act - const result = await db.add(dataname, newData); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Data added successfully.", - results: expect.anything(), - }); - }); - - test("load method should return the data from the specified file", async () => { - // Arrange - const data = [ - { name: "John" }, - { name: "Jane" }, - ]; - const dataname = "load"; - - // Act - await db.add(dataname, data); - const result = await db.load(dataname); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Data loaded successfully.", - results: [ - { _id: expect.anything(), name: "John" }, - { _id: expect.anything(), name: "Jane" }, - ], - }); - }); - - test("remove method should remove data from the specified file", async () => { - // Arrange - const data = [ - { name: "John" }, - { name: "Jane" }, - ]; - const query = { name: "John" }; - const dataname = "remove"; - - // Act - await db.add(dataname, data); - const result = await db.remove(dataname, query); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "1 document(s) removed successfully.", - results: null, - }); - }); - - test("update method should update data in the specified file", async () => { - // Arrange - const data = [ - { name: "John" }, - { name: "Jane" }, - ]; - const updateQuery = { $set: { name: "Mike" } }; - const dataname = "update"; - - // Act - await db.add(dataname, data); - const result = await db.update(dataname, { name: "John" }, updateQuery); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "1 document(s) updated successfully.", - results: { - _id: expect.anything(), - name: "Mike", - }, - }); - }); - - test("updateMany method should update data in the specified file", async () => { - // Arrange - const data = [ - { name: "John" }, - { name: "Jane" }, - ]; - const filter = { name: ["John", "Jane"] }; - const updateQuery = { name: "Mike" }; - const dataname = "updateMany"; - - // Act - await db.add(dataname, data); - const result = await db.updateMany(dataname, filter, updateQuery); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "1 document(s) updated successfully.", - results: [ - { - _id: expect.anything(), - name: "Mike", - }, - ], - }); - }); - - test("find method should return the data that matches the specified query", async () => { - // Arrange - const data = [ - { name: "John" }, - { name:"Jane" }, - ]; - const query = { name: "John" }; - const dataname = "find"; - - // Act - await db.add(dataname, data); - const result = await db.find(dataname, query); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Found data matching your query.", - results: { _id: expect.anything(), name: "John" }, - }); - }); - - test("loadAll method should return all the data in the specified file", async () => { - // Arrange - const data = [ - { name:"Mark" }, - { name:"Anas" }, - { name:"Anas" }, - { name:"Mark" }, - ]; - const dataname = "loadAll"; - const displayOptions = { - filter: { - name: "Mark", - }, - sortField: "name", - sortOrder: "asc", - page: 1, - pageSize: 10, - displayment: 10, - }; - - // Act - await db.add(dataname, data); - const result = await db.loadAll(dataname, displayOptions); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Data found with the given options.", - results: { - allData: [ - { _id: expect.anything(), name: "Mark" }, - { _id: expect.anything(), name: "Mark" }, - ], - }, - }); - }); - - test("drop method should delete all the data in the specified file", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const dataname = "drop"; - - // Act - await db.add(dataname, data); - const result = await db.drop(dataname); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "All data dropped successfully.", - results: "", - }); - }); - - test("search method should return the data that matches the specified query", async () => { - // Arrange - const data = [ - { name:"mark", author: "maher" }, - { name:"anas", author: "kmosha" }, - ]; - const data2 = [ - { name:"anas", author: "kmosha" }, - { name:"mark", author: "maher" }, - ]; - const collectionFilters = [ - { - dataname: "users", - displayment: 10, - filter: { name: "mark" }, - }, - { - dataname: "posts", - displayment: 5, - filter: { author: "maher" }, - }, - ]; - const dataname = "users"; - const dataname2 = "posts"; - - // Act - await db.add(dataname, data); - await db.add(dataname2, data2); - const result = await db.search(collectionFilters); - - // Assert - expect(result).toEqual({ - posts: [{ _id: expect.anything(), author: "maher", name: "mark" }], - users: [{ _id: expect.anything(), author: "maher", name: "mark" }], - }); - }); -}); - -describe("YAML", () => { - let db: any; - console.log = function () {}; - console.info = function () {}; - - beforeEach(async () => { - await Setup("yaml"); - db = await Setup("yaml"); - }); - - afterEach(async () => { - await Teardown("yaml"); - }); - - test("add method should add new data to the specified file", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const newData = [{ name: "Mike" }]; - const dataname = "add"; - - // Act - const result = await db.add(dataname, newData); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Data added successfully.", - results: expect.anything(), - }); - }); - - test("load method should return the data from the specified file", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const dataname = "load"; - - // Act - await db.add(dataname, data); - const result = await db.load(dataname); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Data loaded successfully.", - results: [ - { _id: expect.anything(), name: "John" }, - { _id: expect.anything(), name: "Jane" }, - ], - }); - }); - - test("remove method should remove data from the specified file", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const query = { name: "John" }; - const dataname = "remove"; - - // Act - await db.add(dataname, data); - const result = await db.remove(dataname, query); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "1 document(s) removed successfully.", - results: null, - }); - }); - - test("update method should update data in the specified file", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const updateQuery = { $set: { name: "Mike" } }; - const dataname = "update"; - - // Act - await db.add(dataname, data); - const result = await db.update(dataname, { name: "John" }, updateQuery); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "1 document(s) updated successfully.", - results: { - _id: expect.anything(), - name: "Mike", - }, - }); - }); - - test("updateMany method should update data in the specified file", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const filter = { name: ["John", "Jane"] }; - const updateQuery = { name: "Mike" }; - const dataname = "updateMany"; - - // Act - await db.add(dataname, data); - const result = await db.updateMany(dataname, filter, updateQuery); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "1 document(s) updated successfully.", - results: [ - { - _id: expect.anything(), - name: "Mike", - }, - ], - }); - }); - - test("find method should return the data that matches the specified query", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const query = { name: "John" }; - const dataname = "find"; - - // Act - await db.add(dataname, data); - const result = await db.find(dataname, query); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Found data matching your query.", - results: { _id: expect.anything(), name: "John" }, - }); - }); - - test("loadAll method should return all the data in the specified file", async () => { - // Arrange - const data = [ - { name:"Mark" }, - { name:"Anas" }, - { name:"Anas" }, - { name:"Mark" }, - ]; - const dataname = "loadAll"; - const displayOptions = { - filter: { - name: "Mark", - }, - sortField: "name", - sortOrder: "asc", - page: 1, - pageSize: 10, - displayment: 10, - }; - - // Act - await db.add(dataname, data); - const result = await db.loadAll(dataname, displayOptions); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "Data found with the given options.", - results: { - allData: [ - { _id: expect.anything(), name: "Mark" }, - { _id: expect.anything(), name: "Mark" }, - ], - }, - }); - }); - - test("drop method should delete all the data in the specified file", async () => { - // Arrange - const data = [ - { name:"John" }, - { name:"Jane" }, - ]; - const dataname = "drop"; - - // Act - await db.add(dataname, data); - const result = await db.drop(dataname); - - // Assert - expect(result).toEqual({ - acknowledged: true, - message: "All data dropped successfully.", - results: "", - }); - }); - - test("search method should return the data that matches the specified query", async () => { - // Arrange - const data = [ - { name:"mark", author: "maher" }, - { name:"anas", author: "kmosha" }, - ]; - const data2 = [ - { name:"anas", author: "kmosha" }, - { name:"mark", author: "maher" }, - ]; - const collectionFilters = [ - { - dataname: "users", - displayment: 10, - filter: { name: "mark" }, - }, - { - dataname: "posts", - displayment: 5, - filter: { author: "maher" }, - }, - ]; - const dataname = "users"; - const dataname2 = "posts"; - - // Act - await db.add(dataname, data); - await db.add(dataname2, data2); - const result = await db.search(collectionFilters); - - // Assert - expect(result).toEqual({ - posts: [{ _id: expect.anything(), author: "maher", name: "mark" }], - users: [{ _id: expect.anything(), author: "maher", name: "mark" }], - }); - }); -}); diff --git a/tests/yaml.test.ts b/tests/yaml.test.ts new file mode 100644 index 0000000..8c2acc4 --- /dev/null +++ b/tests/yaml.test.ts @@ -0,0 +1,249 @@ +import versedb from "../src/index"; +import fs from "fs"; + +async function Setup(adapter: string): Promise { + const adapterOptions = { + adapter: `${adapter}`, + dataPath: `./tests/${adapter}/data`, + devLogs: { enable: true, path: `./tests/${adapter}/logs` }, + secure: { + enable: true, + secret: "versedb", + }, + }; + + const db = new versedb.connect(adapterOptions); + return db; +} + +async function Teardown(db: string) { + await fs.promises.rm(`./tests/${db}`, { recursive: true, force: true }); +} + +describe("YAML", () => { + let db: any; + console.log = function () {}; + console.info = function () {}; + + beforeEach(async () => { + await Setup("yaml"); + db = await Setup("yaml"); + }); + + afterEach(async () => { + await Teardown("yaml"); + }); + + test("add method should add new data to the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const newData = [{ name: "Mike" }]; + const dataname = "add"; + + // Act + const result = await db.add(dataname, newData); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Data added successfully.", + results: expect.anything(), + }); + }); + + test("load method should return the data from the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const dataname = "load"; + + // Act + await db.add(dataname, data); + const result = await db.load(dataname); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Data loaded successfully.", + results: [ + { _id: expect.anything(), name: "John" }, + { _id: expect.anything(), name: "Jane" }, + ], + }); + }); + + test("remove method should remove data from the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const query = { name: "John" }; + const dataname = "remove"; + + // Act + await db.add(dataname, data); + const result = await db.remove(dataname, query); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "1 document(s) removed successfully.", + results: null, + }); + }); + + test("update method should update data in the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const updateQuery = { $set: { name: "Mike" } }; + const dataname = "update"; + + // Act + await db.add(dataname, data); + const result = await db.update(dataname, { name: "John" }, updateQuery); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "1 document(s) updated successfully.", + results: { + _id: expect.anything(), + name: "Mike", + }, + }); + }); + + test("updateMany method should update data in the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const query = { name: "John" } ; + const newData = { $set: { name: "Mike" } }; + const dataname = "updateMany"; + + // Act + await db.add(dataname, data); + const result = await db.updateMany(dataname, query, newData); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "1 document(s) updated successfully.", + results: [ + { + _id: expect.anything(), + name: "Mike", + }, + ], + }); + }); + + test("find method should return the data that matches the specified query", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const query = { name: "John" }; + const dataname = "find"; + + // Act + await db.add(dataname, data); + const result = await db.find(dataname, query); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Found data matching your query.", + results: { _id: expect.anything(), name: "John" }, + }); + }); + + test("loadAll method should return all the data in the specified file", async () => { + // Arrange + const data = [ + { name: "Mark" }, + { name: "Anas" }, + { name: "Anas" }, + { name: "Mark" }, + ]; + const dataname = "loadAll"; + const displayOptions = { + filter: { + name: "Mark", + }, + sortField: "name", + sortOrder: "asc", + page: 1, + pageSize: 10, + displayment: 10, + }; + + // Act + await db.add(dataname, data); + const result = await db.loadAll(dataname, displayOptions); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Data found with the given options.", + results: { + allData: [ + { _id: expect.anything(), name: "Mark" }, + { _id: expect.anything(), name: "Mark" }, + ], + }, + }); + }); + + test("drop method should delete all the data in the specified file", async () => { + // Arrange + const data = [{ name: "John" }, { name: "Jane" }]; + const dataname = "drop"; + + // Act + await db.add(dataname, data); + const result = await db.drop(dataname); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "File dropped successfully.", + results: [], + }); + }); + + test("search method should return the data that matches the specified query", async () => { + // Arrange + const data = [ + { name: "mark", author: "maher" }, + { name: "anas", author: "kmosha" }, + ]; + const data2 = [ + { name: "anas", author: "kmosha" }, + { name: "mark", author: "maher" }, + ]; + const collectionFilters = [ + { + dataname: "users", + displayment: 10, + filter: { name: "mark" }, + }, + { + dataname: "posts", + displayment: 5, + filter: { author: "maher" }, + }, + ]; + const dataname = "users"; + const dataname2 = "posts"; + + // Act + await db.add(dataname, data); + await db.add(dataname2, data2); + const result = await db.search(collectionFilters); + + // Assert + expect(result).toEqual({ + acknowledged: true, + message: "Successfully searched in data for the given query.", + results: { + posts: [{ _id: expect.anything(), author: "maher", name: "mark" }], + users: [{ _id: expect.anything(), author: "maher", name: "mark" }], + }, + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 469b310..d915015 100644 --- a/yarn.lock +++ b/yarn.lock @@ -816,20 +816,6 @@ array-union@^2.1.0: resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -axios@^1.6.8: - version "1.6.8" - resolved "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz" - integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - babel-jest@^29.0.0, babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" @@ -1073,13 +1059,6 @@ color-name@1.1.3: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - commander@^4.0.0: version "4.1.1" resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" @@ -1139,11 +1118,6 @@ deepmerge@^4.2.2: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -1323,11 +1297,6 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -follow-redirects@^1.15.6: - version "1.15.6" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - foreground-child@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" @@ -1336,15 +1305,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -2036,6 +1996,11 @@ lodash.sortby@^4.7.0: resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== +lru-cache@^10.2.0: + version "10.2.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" @@ -2050,11 +2015,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== - make-dir@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" @@ -2092,18 +2052,6 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" @@ -2245,11 +2193,11 @@ path-parse@^1.0.7: integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + version "1.11.0" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.0.tgz" + integrity sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-type@^4.0.0: @@ -2304,11 +2252,6 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - punycode@^2.1.0: version "2.3.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"