diff --git a/.changeset/pink-tigers-drum.md b/.changeset/pink-tigers-drum.md new file mode 100644 index 0000000..af9ccca --- /dev/null +++ b/.changeset/pink-tigers-drum.md @@ -0,0 +1,5 @@ +--- +"@codelytv/criteria-to-mongo": major +--- + +add criteria to mongo converter diff --git a/README.md b/README.md index cab1894..aa5ef04 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Create a Criteria from: Convert a Criteria to: - [Elasticsearch (and esql)](./packages/criteria-to-elasticsearch) - [MySql](./packages/criteria-to-mysql) +- [MongoDB](./packages/criteria-to-mongo) You can also create your custom transformer. diff --git a/packages/criteria-to-mongo/README.md b/packages/criteria-to-mongo/README.md new file mode 100644 index 0000000..98c9cb5 --- /dev/null +++ b/packages/criteria-to-mongo/README.md @@ -0,0 +1,23 @@ +

+ + + + + Codely logo + + +

+ +

+ 🎼 Criteria to MongoDB converter +

+ +

+ codely.com +

+ +## 📥 Installation + +```sh +npm i @codelytv/criteria-to-mongo +``` diff --git a/packages/criteria-to-mongo/package.json b/packages/criteria-to-mongo/package.json new file mode 100644 index 0000000..cd2db70 --- /dev/null +++ b/packages/criteria-to-mongo/package.json @@ -0,0 +1,20 @@ +{ + "name": "@codelytv/criteria-to-mongo", + "version": "1.0.0", + "description": "", + "keywords": [], + "author": "Codely (https://codely.com)", + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "test": "node --import tsx --test test/*.test.ts", + "build": "tsc --build --verbose tsconfig.json" + }, + "dependencies": { + "@codelytv/criteria": "workspace:^" + }, + "devDependencies": { + "@codelytv/criteria-test-mother": "workspace:^" + } +} diff --git a/packages/criteria-to-mongo/src/CriteriaToMongoConverter.ts b/packages/criteria-to-mongo/src/CriteriaToMongoConverter.ts new file mode 100644 index 0000000..8384a24 --- /dev/null +++ b/packages/criteria-to-mongo/src/CriteriaToMongoConverter.ts @@ -0,0 +1,108 @@ +import { Criteria, Filter, OrderTypes } from "@codelytv/criteria"; + +type MongoFilterOperator = "$eq" | "$ne" | "$gt" | "$gte" | "$lt" | "$lte" | "$regex"; +type MongoFilterOperation = { + [operator in MongoFilterOperator]?: unknown; +}; + +type MongoFilter = + | { + [field: string]: MongoFilterOperation; + } + | { + [field: string]: { $not: MongoFilterOperation }; + }; + +type MongoSortDirection = 1 | -1; + +type MongoSort = { + [field: string]: MongoSortDirection; +}; + +export type MongoQuery = { + filter: MongoFilter; + sort?: MongoSort; + skip?: number; + limit?: number; +}; + +export class CriteriaToMongoConverter { + convert(criteria: Criteria): MongoQuery { + const query: MongoQuery = { + filter: {}, + }; + + if (criteria.hasFilters()) { + query.filter = criteria.filters.value.reduce((acc, filter) => { + return { ...acc, ...this.generateMongoFilter(filter) }; + }, {}); + } + + if (criteria.hasOrder()) { + query.sort = { + [criteria.order.orderBy.value]: criteria.order.orderType.value === OrderTypes.ASC ? 1 : -1, + }; + } + + if (criteria.pageSize !== null) { + query.limit = criteria.pageSize; + } + + if (criteria.pageSize !== null && criteria.pageNumber !== null) { + query.skip = criteria.pageSize * (criteria.pageNumber - 1); + } + + return query; + } + + private generateMongoFilter(filter: Filter): MongoFilter { + const field = filter.field.value; + const value = filter.value.value; + + if (filter.operator.isContains()) { + return { + [field]: { $regex: value }, + }; + } + + if (filter.operator.isNotContains()) { + return { + [field]: { $not: { $regex: value } }, + }; + } + + if (filter.operator.isGreaterThan()) { + return { + [field]: { $gt: value }, + }; + } + + if (filter.operator.isGreaterThanOrEqual()) { + return { + [field]: { $gte: value }, + }; + } + + if (filter.operator.isLowerThan()) { + return { + [field]: { $lt: value }, + }; + } + + if (filter.operator.isLowerThanOrEqual()) { + return { + [field]: { $lte: value }, + }; + } + + if (filter.operator.isNotEquals()) { + return { + [field]: { $ne: value }, + }; + } + + return { + [field]: { $eq: value }, + }; + } +} diff --git a/packages/criteria-to-mongo/src/index.ts b/packages/criteria-to-mongo/src/index.ts new file mode 100644 index 0000000..0fa5cef --- /dev/null +++ b/packages/criteria-to-mongo/src/index.ts @@ -0,0 +1 @@ +export * from "./CriteriaToMongoConverter"; diff --git a/packages/criteria-to-mongo/test/CriteriaToMongoConverter.test.ts b/packages/criteria-to-mongo/test/CriteriaToMongoConverter.test.ts new file mode 100644 index 0000000..95e3434 --- /dev/null +++ b/packages/criteria-to-mongo/test/CriteriaToMongoConverter.test.ts @@ -0,0 +1,183 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; + +import { CriteriaMother } from "@codelytv/criteria-test-mother"; + +import { CriteriaToMongoConverter } from "../src"; + +describe("CriteriaToMongoConverter should", () => { + const converter = new CriteriaToMongoConverter(); + + it("Generate simple select with an empty criteria", () => { + const actualQuery = converter.convert(CriteriaMother.empty()); + + assert.deepEqual(actualQuery, { filter: {} }); + }); + + it("Generate select with order", () => { + const actualQuery = converter.convert(CriteriaMother.emptySorted("_id", "DESC")); + + assert.deepEqual(actualQuery, { + filter: {}, + sort: { _id: -1 }, + }); + }); + + it("Generate select with one filter", () => { + const actualQuery = converter.convert(CriteriaMother.withOneFilter("name", "EQUAL", "Javier")); + + assert.deepEqual(actualQuery, { + filter: { name: { $eq: "Javier" } }, + }); + }); + + it("Generate select with one greater than filter", () => { + const actualQuery = converter.convert( + CriteriaMother.withOneFilter("age", "GREATER_THAN", "25"), + ); + + assert.deepEqual(actualQuery, { + filter: { age: { $gt: 25 } }, + }); + }); + + it("Generate select with one greater than or equal filter", () => { + const actualQuery = converter.convert( + CriteriaMother.withOneFilter("age", "GREATER_THAN_OR_EQUAL", "25"), + ); + + assert.deepEqual(actualQuery, { + filter: { age: { $gte: 25 } }, + }); + }); + + it("Generate select with one lower than filter", () => { + const actualQuery = converter.convert(CriteriaMother.withOneFilter("age", "LOWER_THAN", "18")); + + assert.deepEqual(actualQuery, { + filter: { age: { $lt: 18 } }, + }); + }); + + it("Generate select with one lower than or equal filter", () => { + const actualQuery = converter.convert( + CriteriaMother.withOneFilter("age", "LOWER_THAN_OR_EQUAL", "18"), + ); + + assert.deepEqual(actualQuery, { + filter: { age: { $lte: 18 } }, + }); + }); + + it("Generate select with one filter sorted", () => { + const actualQuery = converter.convert( + CriteriaMother.withOneFilterSorted("name", "EQUAL", "Javier", "_id", "DESC"), + ); + + assert.deepEqual(actualQuery, { + filter: { name: { $eq: "Javier" } }, + sort: { _id: -1 }, + }); + }); + + it("Generate select with multiples filters", () => { + const actualQuery = converter.convert( + CriteriaMother.create({ + filters: [ + { + field: "name", + operator: "EQUAL", + value: "Javier", + }, + { + field: "email", + operator: "EQUAL", + value: "javier@terra.es", + }, + ], + orderBy: null, + orderType: null, + pageSize: null, + pageNumber: null, + }), + ); + + assert.deepEqual(actualQuery, { + filter: { + name: { $eq: "Javier" }, + email: { $eq: "javier@terra.es" }, + }, + }); + }); + + it("Generate select with multiples filters and sort", () => { + const actualQuery = converter.convert( + CriteriaMother.create({ + filters: [ + { + field: "name", + operator: "EQUAL", + value: "Javier", + }, + { + field: "email", + operator: "EQUAL", + value: "javier@terra.es", + }, + ], + orderBy: "_id", + orderType: "DESC", + pageSize: null, + pageNumber: null, + }), + ); + + assert.deepEqual(actualQuery, { + filter: { + name: { $eq: "Javier" }, + email: { $eq: "javier@terra.es" }, + }, + sort: { _id: -1 }, + }); + }); + + it("Generate select with one contains filter", () => { + const actualQuery = converter.convert( + CriteriaMother.withOneFilter("name", "CONTAINS", "Javier"), + ); + + assert.deepEqual(actualQuery, { + filter: { name: { $regex: "Javier" } }, + }); + }); + + it("Generate select with one not contains filter", () => { + const actualQuery = converter.convert( + CriteriaMother.withOneFilter("name", "NOT_CONTAINS", "Javier"), + ); + + assert.deepEqual(actualQuery, { + filter: { name: { $not: { $regex: "Javier" } } }, + }); + }); + + it("Generate simple select paginated", () => { + const actualQuery = converter.convert(CriteriaMother.emptyPaginated(10, 3)); + + assert.deepEqual(actualQuery, { + filter: {}, + limit: 10, + skip: 20, + }); + }); + + it("Generate select with not equal filter", () => { + const actualQuery = converter.convert( + CriteriaMother.withOneFilter("name", "NOT_EQUAL", "Javier"), + ); + + assert.deepEqual(actualQuery, { + filter: { name: { $ne: "Javier" } }, + }); + }); +}); diff --git a/packages/criteria-to-mongo/tsconfig.json b/packages/criteria-to-mongo/tsconfig.json new file mode 100644 index 0000000..e0d6637 --- /dev/null +++ b/packages/criteria-to-mongo/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07af765..2121ea6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,16 @@ importers: specifier: workspace:^ version: link:../criteria-test-mother + packages/criteria-to-mongo: + dependencies: + '@codelytv/criteria': + specifier: workspace:^ + version: link:../criteria + devDependencies: + '@codelytv/criteria-test-mother': + specifier: workspace:^ + version: link:../criteria-test-mother + packages/criteria-to-mysql: dependencies: '@codelytv/criteria':