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 0000000..02dd858 Binary files /dev/null and b/bun.lockb differ diff --git a/jest.config.js b/jest.config.js index a93551f..34a5a11 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,5 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", + verbose: true, }; diff --git a/package-lock.json b/package-lock.json index dbe57fb..7e5e43c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "verse.db", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "verse.db", - "version": "2.0.1", + "version": "2.1.0", "license": "MIT", "dependencies": { - "axios": "^1.6.8", "yaml": "^2.4.1" }, "devDependencies": { @@ -2000,21 +1999,6 @@ "node": ">=8" } }, - "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"