From 9cfe6a0552a47b0c6abdcd8119708a0b8545d258 Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Sun, 13 Sep 2020 15:41:19 +0430 Subject: [PATCH] refactor(release): Improve DB functionality [breaking] --- .eslintrc | 4 +- .gitignore | 2 + __tests__/Collection.ts | 16 +- __tests__/DB.ts | 6 +- __tests__/Model.ts | 6 +- __tests__/__internals__/setup.js | 12 +- __tests__/relations/EmbedMany.ts | 6 +- __tests__/relations/HasMany.ts | 6 +- __tests__/relations/HasOne.ts | 6 +- __tests__/relations/MorphMany.ts | 6 +- __tests__/relations/MorphOne.ts | 6 +- package-lock.json | 86 +++-- package.json | 16 +- src/Connect.ts | 65 ++-- src/DB/DB.ts | 280 +++++++++++++++ src/DB/EventEmitter.ts | 100 ------ src/DB/Filter.ts | 292 ++++++++-------- src/DB/Join.ts | 233 +++++-------- src/DB/index.ts | 535 +---------------------------- src/constants/general/DB.ts | 82 +++++ src/constants/general/Filter.ts | 71 ++++ src/constants/general/Join.ts | 34 ++ src/constants/general/general.ts | 23 ++ src/constants/general/index.ts | 4 + src/constants/index.ts | 2 + src/constants/mongo/DB.ts | 33 ++ src/constants/mongo/Filter.ts | 5 + src/constants/mongo/Join.ts | 8 + src/constants/mongo/aggregation.ts | 69 ++++ src/constants/mongo/general.ts | 22 ++ src/constants/mongo/index.ts | 5 + src/utils.ts | 2 +- tsconfig.json | 7 +- 33 files changed, 990 insertions(+), 1060 deletions(-) create mode 100644 src/DB/DB.ts delete mode 100644 src/DB/EventEmitter.ts create mode 100644 src/constants/general/DB.ts create mode 100644 src/constants/general/Filter.ts create mode 100644 src/constants/general/Join.ts create mode 100644 src/constants/general/general.ts create mode 100644 src/constants/general/index.ts create mode 100644 src/constants/index.ts create mode 100644 src/constants/mongo/DB.ts create mode 100644 src/constants/mongo/Filter.ts create mode 100644 src/constants/mongo/Join.ts create mode 100644 src/constants/mongo/aggregation.ts create mode 100644 src/constants/mongo/general.ts create mode 100644 src/constants/mongo/index.ts diff --git a/.eslintrc b/.eslintrc index ad487a8..f34f673 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,7 +19,9 @@ "indent": [ "error", 2, - { "SwitchCase": 1 } + { + "SwitchCase": 1 + } ], "quotes": [ "error", diff --git a/.gitignore b/.gitignore index 698ef82..e4ecee3 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,8 @@ typings/ # dotenv environment variables file .env +*.tsbuildinfo + .idea .vscode diff --git a/__tests__/Collection.ts b/__tests__/Collection.ts index 32060ee..262a11f 100644 --- a/__tests__/Collection.ts +++ b/__tests__/Collection.ts @@ -10,10 +10,8 @@ declare global { } Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); it("Should create collection with the given indexes (async/await)", async () => { @@ -22,7 +20,10 @@ it("Should create collection with the given indexes (async/await)", async () => const collection = new Collection("tests"); collection - .index({ field_1: 1, field_2: -1 }, { name: "field_1_field_2", background: true, unique: true }) + .index( + { field_1: 1, field_2: -1 }, + { name: "field_1_field_2", background: true, unique: true }, + ) .index({ field_3: 1 }, { name: "field_3", background: true }) .timestamps() .softDelete(); @@ -36,7 +37,10 @@ it("Should create collection with the given indexes (callback)", (done) => { const collection = new Collection("tests2"); collection - .index({ field_1: 1, field_2: -1 }, { name: "field_1_field_2", background: true, unique: true }) + .index( + { field_1: 1, field_2: -1 }, + { name: "field_1_field_2", background: true, unique: true }, + ) .index({ field_3: 1 }, { name: "field_3", background: true }) .timestamps() .softDelete(); diff --git a/__tests__/DB.ts b/__tests__/DB.ts index 70f090c..bdc6c02 100644 --- a/__tests__/DB.ts +++ b/__tests__/DB.ts @@ -68,10 +68,8 @@ const JOIN_ITEMS = [ beforeAll(async () => { Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); await DB.collection(COLLECTION).insert(ITEMS); diff --git a/__tests__/Model.ts b/__tests__/Model.ts index 15d9dcc..7740332 100644 --- a/__tests__/Model.ts +++ b/__tests__/Model.ts @@ -31,10 +31,8 @@ const ITEMS = [ ]; Odin.Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); beforeAll(async () => { diff --git a/__tests__/__internals__/setup.js b/__tests__/__internals__/setup.js index 20b395a..d805847 100644 --- a/__tests__/__internals__/setup.js +++ b/__tests__/__internals__/setup.js @@ -39,9 +39,11 @@ module.exports = async () => { await MONGOD.start(); global.__CONNECTION__ = await mongodb.connect( - await MONGOD.getConnectionString(), { - useNewUrlParser: true - } + await MONGOD.getConnectionString(), + { + useNewUrlParser: true, + useUnifiedTopology: true, + }, ); global.__MONGOD__ = MONGOD; @@ -52,7 +54,7 @@ module.exports = async () => { // await sleep(2000); - // global.__REPLICA_CONN__ = await mongodb.connect( + // global.__REPLICA_CONN__ = await mongo.connect( // await REPLICA.getConnectionString(), { // useNewUrlParser: true, // readConcern: { @@ -64,4 +66,4 @@ module.exports = async () => { // ); // global.__REPLICA__ = REPLICA; -} \ No newline at end of file +}; diff --git a/__tests__/relations/EmbedMany.ts b/__tests__/relations/EmbedMany.ts index 6895a3b..a7a2282 100644 --- a/__tests__/relations/EmbedMany.ts +++ b/__tests__/relations/EmbedMany.ts @@ -72,10 +72,8 @@ const MESSAGES = [ ]; Odin.Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); @Odin.register diff --git a/__tests__/relations/HasMany.ts b/__tests__/relations/HasMany.ts index 677bb6d..f296b5a 100644 --- a/__tests__/relations/HasMany.ts +++ b/__tests__/relations/HasMany.ts @@ -70,10 +70,8 @@ const MESSAGES = [ ]; Odin.Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); @Odin.register diff --git a/__tests__/relations/HasOne.ts b/__tests__/relations/HasOne.ts index 1fd7370..0b59972 100644 --- a/__tests__/relations/HasOne.ts +++ b/__tests__/relations/HasOne.ts @@ -46,10 +46,8 @@ const MESSAGES = [ ]; Odin.Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); @Odin.register diff --git a/__tests__/relations/MorphMany.ts b/__tests__/relations/MorphMany.ts index f6f8128..75a1291 100644 --- a/__tests__/relations/MorphMany.ts +++ b/__tests__/relations/MorphMany.ts @@ -83,10 +83,8 @@ const MESSAGES = [ ]; Odin.Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); @Odin.register diff --git a/__tests__/relations/MorphOne.ts b/__tests__/relations/MorphOne.ts index 62dd507..e3413c0 100644 --- a/__tests__/relations/MorphOne.ts +++ b/__tests__/relations/MorphOne.ts @@ -53,10 +53,8 @@ const MESSAGES = [ ]; Odin.Connect({ - default: { - database: global.__MONGO_DB_NAME__, - connection: global.__MONGO_CONNECTION__, - }, + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, }); @Odin.register diff --git a/package-lock.json b/package-lock.json index 53cea73..0ab1af3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -389,22 +389,10 @@ } }, "@foxify/schema": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@foxify/schema/-/schema-1.0.1.tgz", - "integrity": "sha512-4skI8btazW5uV9AamsvmOe/6v0BCkppxlfgb1LWt2fKTr5muZCsiD/17FufCbvQs1f7U3xrffufvBuaZ8lwwTw==", - "dev": true, - "requires": { - "prototyped.js": "^0.21.0", - "verifications": "^0.3.0" - }, - "dependencies": { - "prototyped.js": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/prototyped.js/-/prototyped.js-0.21.0.tgz", - "integrity": "sha512-7nvgfXL9SgWMJvkdlNLd2Pj5nKh0tpoSBValHjSk2jCnNBhsrth62UmPzKvBgiUA6NaGqh4L1h52eHcgSCQbRQ==", - "dev": true - } - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@foxify/schema/-/schema-2.0.0.tgz", + "integrity": "sha512-L9mYhHiF1YPBSs0kcTc9OwMNWJztXC5h07AFbcKtzuMfSMM6KCR5H5OU5GpU7jhxh22agtee4rVDqlEWweLqOg==", + "dev": true }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -982,9 +970,9 @@ } }, "@types/async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/async/-/async-2.4.1.tgz", - "integrity": "sha512-C09BK/wXzbW+/JK9zckhe+FeSbg7NmvVjUWwApnw7ksRpUq3ecGLiq2Aw1LlY4Z/VmtdhSaIs7jO5/MWRYMcOA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/async/-/async-2.4.2.tgz", + "integrity": "sha512-bWBbC7VG2jdjbgZMX0qpds8U/3h3anfIqE81L8jmVrgFZw/urEDnBA78ymGGKTTK6ciBXmmJ/xlok+Re41S8ww==", "dev": true }, "@types/babel__core": { @@ -1029,9 +1017,9 @@ } }, "@types/bson": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.11.tgz", - "integrity": "sha512-j+UcCWI+FsbI5/FQP/Kj2CXyplWAz39ktHFkXk84h7dNblKRSoNJs95PZFRd96NQGqsPEPgeclqnznWZr14ZDA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.2.tgz", + "integrity": "sha512-+uWmsejEHfmSjyyM/LkrP0orfE2m5Mx9Xel4tXNeqi1ldK5XMQcDsFkBmLDtuyKUbxj2jGDo0H240fbCRJZo7Q==", "requires": { "@types/node": "*" } @@ -1052,9 +1040,9 @@ } }, "@types/deasync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@types/deasync/-/deasync-0.1.0.tgz", - "integrity": "sha512-jxUH53LtGvbIL3TX2hD/XQuAgYJeATtx9kDXq5XtCZrWQABsiCQPjWi/KQXECUF+p9FuR6/tawnEDjXlEr4rFA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@types/deasync/-/deasync-0.1.1.tgz", + "integrity": "sha512-/AsDEUsHjyzMX0UjPgysggxFO8r7//c4aS9aeQwHzgs5POBsqaBFWW9+KYFGUyx/VYT4HrT/+JzAGTEEL2d4OQ==", "dev": true }, "@types/debug": { @@ -1284,18 +1272,18 @@ } }, "@types/mongodb": { - "version": "3.1.19", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.19.tgz", - "integrity": "sha512-H54hQEovAhyLrIZOhPNfGyCCDoTqKsjb8GQBy8nptJqfxrYCp5WVcPJf9v0kfTPR72xOhaz9+WcYxOXWwEg1Vg==", + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.27.tgz", + "integrity": "sha512-1jxKDgdfJEOO9zp+lv43p8jOqRs02xPrdUTzAZIVK9tVEySfCEmktL2jEu9A3wOBEOs18yKzpVIKUh8b8ALk3w==", "requires": { "@types/bson": "*", "@types/node": "*" } }, "@types/node": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.3.tgz", - "integrity": "sha512-DMiqG51GwES/c4ScBY0u5bDlH44+oY8AeYHjY1SGCWidD7h08o1dfHue/TGK7REmif2KiJzaUskO+Q0eaeZ2fQ==" + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -1874,9 +1862,12 @@ } }, "bindings": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", - "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } }, "bl": { "version": "4.0.3", @@ -2228,12 +2219,12 @@ } }, "deasync": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.14.tgz", - "integrity": "sha512-wN8sIuEqIwyQh72AG7oY6YQODCxIp1eXzEZlZznBuwDF8Q03Tdy9QNp1BNZXeadXoklNrw+Ip1fch+KXo/+ASw==", + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.20.tgz", + "integrity": "sha512-E1GI7jMI57hL30OX6Ht/hfQU8DO4AuB9m72WFm4c38GNbUD4Q03//XZaOIHZiY+H1xUaomcot5yk2q/qIZQkGQ==", "requires": { - "bindings": "~1.2.1", - "node-addon-api": "^1.6.0" + "bindings": "^1.5.0", + "node-addon-api": "^1.7.1" } }, "debug": { @@ -3021,6 +3012,11 @@ "flat-cache": "^2.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -5588,9 +5584,9 @@ "dev": true }, "node-addon-api": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.6.0.tgz", - "integrity": "sha512-HEUPBHfdH4CLR1Qq4/Ek8GT/qFSvpApjJQmcYdLCL51ADU/Y11kMuFAdIevhNrPh3ylqVGA8k6vI/oi4YUAHbA==" + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==" }, "node-int64": { "version": "0.4.0", @@ -5987,9 +5983,9 @@ } }, "prototyped.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prototyped.js/-/prototyped.js-1.0.0.tgz", - "integrity": "sha512-9oj0ZmakS5FNLM/EJVP/ghf1I9OLHNdJGPGS6/EzmuBEW5eKB4IGvlYZ4YvIyeUrpuXZmHFRPdd6pGhHriXSqg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prototyped.js/-/prototyped.js-1.0.1.tgz", + "integrity": "sha512-fX2XJW6QEiihrXXdOJatSvkMoZ2nKO6Spfh+5NoXOhnX2GdJlgZAWPnHik0MsWw4dGSVr2ytVJ5fZxptSXyH5g==" }, "psl": { "version": "1.1.31", diff --git a/package.json b/package.json index 1fc0f9c..91bd66e 100644 --- a/package.json +++ b/package.json @@ -48,24 +48,24 @@ "dependencies": { "@types/graphql": "^0.13.4", "@types/graphql-iso-date": "^3.3.1", - "@types/mongodb": "^3.1.19", - "@types/node": "^11.9.3", + "@types/mongodb": "^3.5.27", + "@types/node": "^14.6.4", "async": "^2.6.2", "caller-id": "^0.1.0", - "deasync": "^0.1.14", + "deasync": "^0.1.20", "graphql": "^0.13.2", "graphql-iso-date": "^3.6.1", "mongodb": "^3.6.1", - "prototyped.js": "^1.0.0", + "prototyped.js": "^1.0.1", "verifications": "^0.3.0" }, "peerDependencies": { - "@foxify/schema": "^1.0.1" + "@foxify/schema": "^2.0.0" }, "devDependencies": { - "@foxify/schema": "^1.0.1", - "@types/async": "^2.4.1", - "@types/deasync": "^0.1.0", + "@foxify/schema": "^2.0.0", + "@types/async": "^2.4.2", + "@types/deasync": "^0.1.1", "@types/jest": "^26.0.13", "@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/parser": "^4.0.1", diff --git a/src/Connect.ts b/src/Connect.ts index 87960d8..261f076 100644 --- a/src/Connect.ts +++ b/src/Connect.ts @@ -1,8 +1,7 @@ -import * as deasync from "deasync"; +import deasync from "deasync"; import * as mongodb from "mongodb"; -import { object } from "./utils"; -const CONNECTIONS: { [name: string]: () => mongodb.Db } = {}; +let CONNECTION: () => mongodb.Db; export interface Connection { database: string; @@ -16,48 +15,42 @@ export interface Connection { port?: string; } -export interface Connections { - [name: string]: Connection; -} - -export const connection = (name: string) => CONNECTIONS[name](); +export const connection = (): mongodb.Db => CONNECTION(); -export default function connect(connections: Connections) { - object.forEach(connections, (connection: Connection, name) => { - if (CONNECTIONS[name]) return; +export default function connect(connection: Connection): void { + if (CONNECTION != null) return; - if (connection.connection) { - CONNECTIONS[name] = () => - (connection.connection as mongodb.MongoClient).db(connection.database); + const client = connection.connection; - return; - } + if (client != null) { + CONNECTION = () => client.db(connection.database); - const OPTIONS: mongodb.MongoClientOptions = { - useNewUrlParser: true, - }; + return; + } - let database = connection.database; + const OPTIONS: mongodb.MongoClientOptions = { + useNewUrlParser: true, + useUnifiedTopology: true, + }; - if (connection.auth && connection.auth.user && connection.auth.password) { - OPTIONS.auth = { - user: connection.auth.user, - password: connection.auth.password, - }; + let database = connection.database; - if (connection.auth.database) database = connection.auth.database; - } + if (connection.auth && connection.auth.user && connection.auth.password) { + OPTIONS.auth = { + user: connection.auth.user, + password: connection.auth.password, + }; - const uri = `mongodb://${connection.host || "127.0.0.1"}:${ - connection.port || "27017" - }/${database}`; + if (connection.auth.database) database = connection.auth.database; + } - const server = ( - deasync(mongodb.MongoClient.connect)(uri, OPTIONS) - ); + const uri = `mongodb://${connection.host || "127.0.0.1"}:${ + connection.port || "27017" + }/${database}`; - const db = server.db(connection.database); + const server = ( + deasync(mongodb.MongoClient.connect)(uri, OPTIONS) + ); - CONNECTIONS[name] = () => db; - }); + CONNECTION = () => server.db(connection.database); } diff --git a/src/DB/DB.ts b/src/DB/DB.ts new file mode 100644 index 0000000..1f8010d --- /dev/null +++ b/src/DB/DB.ts @@ -0,0 +1,280 @@ +import * as mongo from "mongodb"; +import Join from "./Join"; +import { + MONGO_NULL, + Obj, + MongoDB, + MongoID, + MongoPipelineStageMatch, + MongoPipelineStageProject, +} from "../constants"; +import { connection as getConnection } from "../Connect"; + +export default class DB extends Join implements MongoDB { + public static table(table: string): DB { + return new this(table); + } + + protected _mongo: mongo.Collection; + + public constructor(table: string) { + super(table); + + this._mongo = getConnection().collection(table); + } + + /* ------------------------- INSERT ------------------------- */ + + public insert(item: T): Promise; + public insert(items: T[]): Promise; + public async insert(items: T | T[]): Promise { + if (Array.isArray(items)) { + if (items.length === 0) return 0; + else if (items.length > 1) { + const inserted = await this._mongo.insertMany(items); + + return inserted.insertedCount; + } else items = items[0]; + } + + const inserted = await this._mongo.insertOne(items); + + return inserted.insertedCount; + } + + /* ------------------------- INSERT GET ID ------------------------- */ + + public async insertGetId(item: T): Promise { + const inserted = await this._mongo.insertOne(item); + + return inserted.insertedId; + } + + /* ------------------------- GET ------------------------- */ + + public get(): Promise; + public get(...select: Array): Promise[]>; + public async get(...select: string[]): Promise[]> { + if (select.length > 0) { + this.pipe({ + $project: select.reduce( + (project, field) => { + project[field] = true; + + return project; + }, + { _id: false } as MongoPipelineStageProject["$project"], + ), + }); + } + + return await this._mongo.aggregate(this.pipeline).toArray(); + } + + /* ------------------------- COUNT ------------------------- */ + + public async count(): Promise { + const result = await this.pipe({ $count: "count" }).first(); + + return (result?.count as number) ?? 0; + } + + /* ------------------------- EXISTS ------------------------- */ + + public async exists(): Promise { + const count = await this.count(); + + return count > 0; + } + + /* ------------------------- PLUCK ------------------------- */ + + public pluck(field: K): Promise; + public pluck(field: string): Promise; + public async pluck(field: string): Promise { + const keys = field.split("."); + + const result = await this.pipe({ + $project: { + _id: false, + [keys[0]]: { $ifNull: [`$${keys[0]}`, MONGO_NULL] }, + }, + }).get(); + + return result.map((item) => + keys.reduce( + (prev: Obj, key) => (prev == null ? prev : (prev[key] as Obj) ?? null), + item, + ), + ); + } + + public value(field: K): Promise; + public value(field: string): Promise; + public value(field: string): Promise { + return this.pluck(field); + } + + /* ------------------------- FIRST ------------------------- */ + + public first(): Promise; + public first(...select: Array): Promise>; + public async first(...select: string[]): Promise> { + const result = await this.limit(1).get(...select); + + return result[0] ?? null; + } + + /* ------------------------- MAXIMUM ------------------------- */ + + public max(field: K): Promise; + public max(field: string): Promise; + public async max(field: string): Promise { + const result = await this.pipe({ + $group: { + _id: null, + max: { $max: `$${field}` }, + }, + }).first(); + + return result?.max ?? null; + } + + /* ------------------------- MINIMUM ------------------------- */ + + public min(field: K): Promise; + public min(field: string): Promise; + public async min(field: string): Promise { + const result = await this.pipe({ + $group: { + _id: null, + min: { $min: `$${field}` }, + }, + }).first(); + + return result?.min ?? null; + } + + /* ------------------------- AVERAGE ------------------------- */ + + public avg(field: K): Promise; + public avg(field: string): Promise; + public async avg(field: string): Promise { + const result = await this.pipe({ + $group: { + _id: null, + avg: { $avg: `$${field}` }, + }, + }).first(); + + return result?.avg ?? null; + } + + /* ------------------------- ASYNC ITERATOR ------------------------- */ + + public async *[Symbol.asyncIterator](): AsyncIterator { + const cursor = await this._mongo.aggregate(this.pipeline); + + let next: T; + + do { + next = await cursor.next(); + + yield next; + } while (next != null); + } + + /* ------------------------- UPDATE ------------------------- */ + + public async update(update: Partial): Promise { + const updated = await this._mongo.updateMany(this._pipelineFilters, { + $set: update, + }); + + return updated.modifiedCount; + } + + /* ------------------------- INCREMENT ------------------------- */ + + public async increment(field: keyof T | string, count = 1): Promise { + const updated = await this._mongo.updateMany(this._pipelineFilters, { + $inc: { + [field]: count, + }, + }); + + return updated.modifiedCount; + } + + /* ------------------------- DECREMENT ------------------------- */ + + public decrement(field: keyof T | string, count = 1): Promise { + return this.increment(field, -count); + } + + /* ------------------------- UNSET ------------------------- */ + + public unset(fields: string[]): Promise; + public unset(fields: K[]): Promise; + public async unset($unset: string[]): Promise { + const updated = await this._mongo.updateMany(this._pipelineFilters, { + $unset, + }); + + return updated.modifiedCount; + } + + /* ------------------------- DELETE ------------------------- */ + + public async delete(): Promise { + const deleted = await this._mongo.deleteMany(this._pipelineFilters); + + return deleted.deletedCount ?? 0; + } + + /* ------------------------- INDEXES ------------------------- */ + + public indexes(): Promise { + return this._mongo.indexes(); + } + + /* ------------------------- INDEX ------------------------- */ + + public index(field: string, options?: mongo.IndexOptions): Promise; + public index(spec: Obj, options?: mongo.IndexOptions): Promise; + public index( + spec: string | Obj, + options?: mongo.IndexOptions, + ): Promise { + return this._mongo.createIndex(spec, options); + } + + /* ------------------------- REINDEX ------------------------- */ + + public reIndex(): Promise { + return this._mongo.reIndex(); + } + + /* ------------------------- DROP INDEX ------------------------- */ + + public dropIndex( + index: string, + options?: mongo.CommonOptions & { maxTimeMS?: number }, + ): Promise { + return this._mongo.dropIndex(index, options); + } + + /* ------------------------- HELPERS ------------------------- */ + + protected get _pipelineFilters(): MongoPipelineStageMatch["$match"] { + const filters = this.pipeline.filter( + (pipe) => (pipe as MongoPipelineStageMatch).$match != null, + ); + + if (filters.length === 0) return {}; + + return { + $and: filters.map((pipe) => (pipe as MongoPipelineStageMatch).$match), + }; + } +} diff --git a/src/DB/EventEmitter.ts b/src/DB/EventEmitter.ts deleted file mode 100644 index cb2d24e..0000000 --- a/src/DB/EventEmitter.ts +++ /dev/null @@ -1,100 +0,0 @@ -import * as assert from "assert"; -import * as Base from "events"; - -const EMITTER = new Base(); - -const EVENTS: Event[] = ["create", "update", "delete", "restore"]; - -const ERROR_GENERATOR = (event: string) => `Unexpected event "${event}"`; - -export type Event = "create" | "update" | "delete" | "restore"; - -class EventEmitter = any> extends Base { - protected _prefix: string; - - constructor(connection: string, collection: string) { - super(); - - this._prefix = `${connection}.${collection}`; - } - - public emit(event: Event, data: T) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - return EMITTER.emit(`${this._prefix}:${event}`, data); - } - - public eventNames() { - return EMITTER.eventNames().map((event: any) => - event.replace(`${this._prefix}:`, ""), - ); - } - - public listenerCount(event: Event) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - return EMITTER.listenerCount(`${this._prefix}:${event}`); - } - - public listeners(event: Event) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - return EMITTER.listeners(`${this._prefix}:${event}`); - } - - public on(event: Event, listener: (data: T) => void) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - EMITTER.on(`${this._prefix}:${event}`, listener); - - return this; - } - - public once(event: Event, listener: (data: T) => void) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - EMITTER.once(`${this._prefix}:${event}`, listener); - - return this; - } - - public prependListener(event: Event, listener: (data: T) => void) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - EMITTER.prependListener(`${this._prefix}:${event}`, listener); - - return this; - } - - public prependOnceListener(event: Event, listener: (data: T) => void) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - EMITTER.prependOnceListener(`${this._prefix}:${event}`, listener); - - return this; - } - - public removeAllListeners(event?: Event) { - assert(event && EVENTS.includes(event), ERROR_GENERATOR(event as string)); - - EMITTER.removeAllListeners(event && `${this._prefix}:${event}`); - - return this; - } - - public removeListener(event: Event, listener: (data: T) => void) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - EMITTER.removeListener(`${this._prefix}:${event}`, listener); - - return this; - } - - public rawListeners(event: Event) { - assert(EVENTS.includes(event), ERROR_GENERATOR(event)); - - return EMITTER.rawListeners(`${this._prefix}:${event}`); - } -} - -export default EventEmitter; diff --git a/src/DB/Filter.ts b/src/DB/Filter.ts index 45fbb72..e310163 100644 --- a/src/DB/Filter.ts +++ b/src/DB/Filter.ts @@ -1,196 +1,202 @@ -import * as mongodb from "mongodb"; -import { FilterQuery, Operator } from "."; -import { function as func, OPERATORS, prepareKey, string } from "../utils"; - -export interface Filters { - $and?: Array>; - $or?: Array>; - - [operator: string]: any; +import * as mongo from "mongodb"; +import { + Obj, + MongoFilter, + FilterQuery, + Operator, + OPERATOR, + MONGO_OPERATOR_MAP, +} from "../constants"; + +export const enum FILTER_OPERATOR { + AND = "$and", + OR = "$or", } -export default class Filter = any> { - protected _filter: Filters = { - $and: [], - }; - - protected get _filters() { - const FILTERS = { - ...this._filter, - }; +export type Filters = { + [operator in FILTER_OPERATOR]?: Array | Filters>; +}; - if (FILTERS.$and && FILTERS.$and.length === 0) delete FILTERS.$and; - else if (FILTERS.$or && FILTERS.$or.length === 0) delete FILTERS.$or; - - return FILTERS; - } - - /********************************** Helpers *********************************/ - - protected _push_filter(operator: "and" | "or", value: any) { - const _filters = { ...this._filter }; - - if (operator === "and" && _filters.$or) { - _filters.$and = [this._filter]; - delete _filters.$or; - } else if (operator === "or" && _filters.$and) { - _filters.$or = [this._filter]; - delete _filters.$and; - } +export default class Filter implements MongoFilter { + protected _filters: Filters = {}; - _filters[`$${operator}`].push(value); + /* ------------------------- WHERE ------------------------- */ - this._filter = _filters; - - return this; - } - - protected _where(field: string, operator: string, value: any) { - field = prepareKey(field); - - return this._push_filter("and", { - [field]: { - [`$${operator}`]: value, - }, - }); - } - - protected _or_where(field: string, operator: string, value: any) { - field = prepareKey(field); - - return this._push_filter("or", { - [field]: { - [`$${operator}`]: value, - }, - }); - } - - /******************************* Where Clauses ******************************/ - - public where(query: FilterQuery): this; + public where(query: FilterQuery>): this; public where(field: K, value: T[K]): this; - public where(field: string, value: any): this; + public where(field: string, value: unknown): this; public where( field: K, operator: Operator, value: T[K], ): this; - public where(field: string, operator: Operator, value: any): this; + public where(field: string, operator: Operator, value: unknown): this; public where( - field: string | FilterQuery, - operator?: Operator | any, - value?: any, - ) { - if (func.isFunction(field)) { - const filter: Filter = field(new Filter()) as any; - - return this._push_filter("and", filter._filters); + field: string | FilterQuery>, + operator?: unknown, + value?: unknown, + ): this { + if (typeof field === "function") { + const filter = new Filter(); + + field(filter); + + return this._filter(FILTER_OPERATOR.AND, filter._filters); } - if (value === undefined) { + if (arguments.length === 2) { value = operator; - operator = "="; + operator = OPERATOR.EQ; } - return this._where(field, OPERATORS[operator], value); + return this._where(field, MONGO_OPERATOR_MAP[operator as Operator], value); } - public orWhere(query: FilterQuery): this; + /* ------------------------- OR WHERE ------------------------- */ + + public orWhere(query: FilterQuery>): this; public orWhere(field: K, value: T[K]): this; - public orWhere(field: string, value: any): this; + public orWhere(field: string, value: unknown): this; public orWhere( field: K, operator: Operator, value: T[K], ): this; - public orWhere(field: string, operator: Operator, value: any): this; + public orWhere(field: string, operator: Operator, value: unknown): this; public orWhere( - field: string | FilterQuery, - operator?: Operator | any, - value?: any, - ) { - if (func.isFunction(field)) { - const filter: Filter = field(new Filter()) as any; - - return this._push_filter("or", filter._filters); + field: string | FilterQuery>, + operator?: unknown, + value?: unknown, + ): this { + if (typeof field === "function") { + const filter = new Filter(); + + field(filter); + + return this._filter(FILTER_OPERATOR.OR, filter._filters); } - if (value === undefined) { + if (arguments.length === 2) { value = operator; - operator = "="; + operator = OPERATOR.EQ; } - return this._or_where(field, OPERATORS[operator], value); + return this._orWhere( + field, + MONGO_OPERATOR_MAP[operator as Operator], + value, + ); } - public whereLike(field: K, value: string | RegExp): this; - public whereLike(field: string, value: string | RegExp): this; - public whereLike(field: string, value: string | RegExp) { - if (!(value instanceof RegExp)) value = new RegExp(value, "i"); + /* ------------------------- WHERE NULL ------------------------- */ - return this._where(field, "regex", value); + whereNull(field: keyof T | string): this; + whereNull(field: string): this { + return this._where(field, "$eq", null); } - public whereNotLike( - field: K, - value: string | RegExp, - ): this; - public whereNotLike(field: string, value: string | RegExp): this; - public whereNotLike(field: string, value: string | RegExp) { - if (!(value instanceof RegExp)) value = new RegExp(value, "i"); + /* ------------------------- WHERE NOT NULL ------------------------- */ - return this._where(field, "not", value); + whereNotNull(field: keyof T | string): this; + whereNotNull(field: string): this { + return this._where(field, "$ne", null); } - public whereIn(field: string, embeddedField: string): this; - public whereIn(field: K, embeddedField: string): this; - public whereIn(field: string, values: any[]): this; - public whereIn(field: K, values: Array): this; - public whereIn(field: string, values: any) { - if (string.isString(values)) values = `$${values}`; + /* ------------------------- WHERE LIKE ------------------------- */ - return this._where(field, "in", values); + whereLike(field: keyof T | string, value: string | RegExp): this; + whereLike(field: string, value: string | RegExp): this { + if (!(value instanceof RegExp)) value = new RegExp(value); + + return this._where(field, "$regex", value); } - public whereNotIn(field: string, embeddedField: string): this; - public whereNotIn(field: K, embeddedField: string): this; - public whereNotIn(field: string, values: any[]): this; - public whereNotIn(field: K, values: Array): this; - public whereNotIn(field: string, values: any) { - if (string.isString(values)) values = `$${values}`; + /* ------------------------- WHERE NOT LIKE ------------------------- */ + + whereNotLike(field: keyof T | string, value: string | RegExp): this; + whereNotLike(field: string, value: string | RegExp): this { + if (!(value instanceof RegExp)) value = new RegExp(value); - return this._where(field, "nin", values); + return this._where(field, "$not", value); } - public whereBetween( - field: K, - start: T[K], - end: T[K], - ): this; - public whereBetween(field: string, start: any, end: any): this; - public whereBetween(field: string, start: any, end: any) { - return this._where(field, "gte", start)._where(field, "lte", end); + /* ------------------------- WHERE IN ------------------------- */ + + whereIn(field: K, values: T[K][]): this; + whereIn(field: string, values: unknown[]): this; + whereIn(field: string, values: unknown[]): this { + return this._where(field, "$in", values); } - public whereNotBetween( - field: K, - start: T[K], - end: T[K], - ): this; - public whereNotBetween(field: string, start: any, end: any): this; - public whereNotBetween(field: string, start: any, end: any) { - return this._where(field, "lt", start)._or_where(field, "gt", end); + /* ------------------------- WHERE NOT IN ------------------------- */ + + whereNotIn(field: K, values: T[K][]): this; + whereNotIn(field: string, values: unknown[]): this; + whereNotIn(field: string, values: unknown[]): this { + return this._where(field, "$nin", values); + } + + /* ------------------------- WHERE BETWEEN ------------------------- */ + + whereBetween(field: K, start: T[K], end: T[K]): this; + whereBetween(field: string, start: unknown, end: unknown): this; + whereBetween(field: string, start: unknown, end: unknown): this { + return this.where(field, OPERATOR.GTE, start).where( + field, + OPERATOR.LTE, + end, + ); + } + + /* ------------------------- WHERE NOT BETWEEN ------------------------- */ + + whereNotBetween(field: K, start: T[K], end: T[K]): this; + whereNotBetween(field: string, start: unknown, end: unknown): this; + whereNotBetween(field: string, start: unknown, end: unknown): this { + return this.where((query) => + query.where(field, OPERATOR.LT, start).orWhere(field, OPERATOR.GT, end), + ); + } + + /* ------------------------- HELPERS ------------------------- */ + + protected _filter(operator: FILTER_OPERATOR, filter: unknown): this { + const filters = { ...this._filters }; + + if (operator === FILTER_OPERATOR.AND && filters.$or) { + filters.$and = [this._filters]; + delete filters.$or; + } else if (operator === FILTER_OPERATOR.OR && filters.$and) { + filters.$or = [this._filters]; + delete filters.$and; + } + + let filtersArray = filters[operator]; + + if (filtersArray == null) filtersArray = []; + + filtersArray.push(filter as mongo.FilterQuery); + + filters[operator] = filtersArray; + + this._filters = filters; + + return this; } - public whereNull(field: K): this; - public whereNull(field: string): this; - public whereNull(field: string) { - return this._where(field, "eq", null); + protected _where(field: string, operator: string, value: unknown): this { + return this._filter(FILTER_OPERATOR.AND, { + [field]: { + [operator]: value, + }, + }); } - public whereNotNull(field: K): this; - public whereNotNull(field: string): this; - public whereNotNull(field: string) { - return this._where(field, "ne", null); + protected _orWhere(field: string, operator: string, value: unknown): this { + return this._filter(FILTER_OPERATOR.OR, { + [field]: { + [operator]: value, + }, + }); } } diff --git a/src/DB/Join.ts b/src/DB/Join.ts index a7d71e4..fb3ec6e 100644 --- a/src/DB/Join.ts +++ b/src/DB/Join.ts @@ -1,186 +1,115 @@ -import { JoinQuery, Order } from "."; -import { - array, - makeCollectionId, - object, - OPERATORS, - prepareKey, - string, -} from "../utils"; import Filter from "./Filter"; +import { + ORDER, + MONGO_ROOT, + Obj, + JoinQuery, + MongoJoin, + MongoPipeline, + MongoPipelineStageSort, + Order, +} from "../constants"; + +export default class Join + extends Filter + implements MongoJoin { + protected _pipeline: MongoPipeline = []; + + public get pipeline(): MongoPipeline { + if (Object.keys(this._filters).length > 0) { + this._pipeline = this._pipeline.concat({ $match: this._filters }); + + this._filters = {}; + } -class Join = any> extends Filter { - protected _pipeline: Record[] = []; - - protected _let: { [key: string]: any } = {}; - - public get pipeline() { - this._resetFilters(); - - return { - $lookup: { - let: this._let, - from: this._collection, - pipeline: this._pipeline, - as: this._as, - }, - }; + return this._pipeline; } - constructor( - protected _ancestor: string, - protected _collection: string, - protected _as: string = _collection, - ) { + public constructor(public table: string) { super(); } - /********************************** Helpers *********************************/ - - protected _resetFilters() { - const FILTER = this._filters; - - if (object.size(FILTER) > 0) this._pipeline.push({ $match: FILTER }); - // if (object.size(FILTER) > 0) this.aggregate({ $match: FILTER }); - - this._filter = { - $and: [], - }; - - return this; - } - - protected _shouldPushExpr(value: any) { - if (!string.isString(value)) return false; - - return new RegExp(`^\\$?${this._ancestor}\\..+`).test(value); - } - - protected _where(field: string, operator: string, value: any) { - if (this._shouldPushExpr(value)) { - const keys = array.tail(value.replace(/^\$/, "").split(".")); + /* ------------------------- JOIN ------------------------- */ - keys.push(prepareKey(keys.pop() as string)); - - const pivotKey = `pivot_${keys.join("_ODIN_")}`; - - this._let[pivotKey] = `$${keys.join(".")}`; + // TODO: right filters + public join( + table: string, + query: JoinQuery> = (q) => + q.where(`${table}_id`, `$${table}._id`), + as = table, + ): this { + const join = new Join(table); - return this._push_filter("and", { - $expr: { - [`$${operator}`]: [`$${prepareKey(field)}`, `$$${pivotKey}`], - }, - }); - } + query(join); - return super._where(field, operator, value); + return this.pipe({ + $lookup: { + let: { [this.table]: MONGO_ROOT }, + from: table, + pipeline: join.pipeline, + as, + }, + }); } - protected _or_where(field: string, operator: string, value: any) { - if (this._shouldPushExpr(value)) { - const keys = array.tail(value.split(".")); + /* ------------------------- ORDER ------------------------- */ - keys.push(prepareKey(keys.pop() as string)); - - const pivotKey = `pivot_${keys.join("_ODIN_")}`; - - this._let[pivotKey] = `$${keys.join(".")}`; - - return this._push_filter("or", { - $expr: { - [`$${operator}`]: [`$${prepareKey(field)}`, `$$${pivotKey}`], - }, - }); + public orderBy(field: K | string, order?: Order): this; + public orderBy( + fields: { [key in K | string]: Order }, + ): this; + public orderBy( + fields: string | { [key: string]: Order }, + order: Order = ORDER.ASC, + ): this { + const $sort: MongoPipelineStageSort["$sort"] = {}; + + if (typeof fields === "string") + $sort[fields] = order === ORDER.ASC ? 1 : -1; + else { + for (const field in fields as { [key in keyof T | string]?: Order }) { + if (!Object.prototype.hasOwnProperty.call(fields, field)) continue; + + $sort[field] = fields[field] === ORDER.ASC ? 1 : -1; + } } - return super._or_where(field, operator, value); + return this.pipe({ $sort }); } - /********************************** Extra **********************************/ - - public aggregate( - ...objects: Record[] | Record[][] - ) { - this._resetFilters(); - - this._pipeline.push(...array.deepFlatten(objects)); + /* ------------------------- OFFSET ------------------------- */ - return this; + public offset(offset: number): this; + public offset($skip: number): this { + return this.pipe({ $skip }); } - /*********************************** Joins **********************************/ - - public join( - collection: string, - query: JoinQuery = (q) => - q.where(makeCollectionId(collection), `${collection}.id`), - as: string = collection, - ) { - const join: Join = query(new Join(this._collection, collection, as)) as any; - - this.aggregate(join.pipeline); - - return this; + public skip(offset: number): this { + return this.offset(offset); } - /********* Mapping, Ordering, Grouping, Limit, Offset & Pagination *********/ + /* ------------------------- LIMIT ------------------------- */ - public orderBy(field: K, order?: Order): this; - public orderBy(field: string, order?: Order): this; - public orderBy(fields: { [field: string]: "asc" | "desc" }): this; - public orderBy( - fields: string | { [field: string]: "asc" | "desc" }, - order?: Order, - ) { - const $sort: { [field: string]: 1 | -1 } = {}; - - if (string.isString(fields)) $sort[fields] = order === "desc" ? -1 : 1; - else - object.forEach( - fields, - (value, field) => ($sort[field] = value === "desc" ? -1 : 1), - ); - - return this.aggregate({ $sort }); - } - - public skip(offset: number) { - return this.aggregate({ $skip: offset }); + public limit(limit: number): this; + public limit($limit: number): this { + return this.pipe({ $limit }); } - public offset(offset: number) { - return this.skip(offset); - } - - public limit(limit: number) { - return this.aggregate({ $limit: limit }); - } - - public take(limit: number) { + public take(limit: number): this { return this.limit(limit); } - public paginate(page = 0, limit = 10) { - return this.skip(page * limit).limit(limit); + /* ------------------------- PAGINATION ------------------------- */ + + public paginate(page: number, limit = 10): this { + return this.offset(page * limit).limit(limit); } - /******************************* Where Clauses ******************************/ + /* ------------------------- PIPELINE ------------------------- */ - public whereIn(field: string, embeddedField: string): this; - public whereIn(field: K, embeddedField: string): this; - public whereIn(field: string, values: any[]): this; - public whereIn(field: K, values: Array): this; - public whereIn(field: string, values: any) { - return super.whereIn(field, values); - } + public pipe(...pipeline: MongoPipeline): this { + if (pipeline.length > 0) this._pipeline = this.pipeline.concat(pipeline); - public whereNotIn(field: string, embeddedField: string): this; - public whereNotIn(field: K, embeddedField: string): this; - public whereNotIn(field: string, values: any[]): this; - public whereNotIn(field: K, values: Array): this; - public whereNotIn(field: string, values: any) { - return super.whereNotIn(field, values); + return this; } } - -export default Join; diff --git a/src/DB/index.ts b/src/DB/index.ts index 0a1ae81..94b7945 100644 --- a/src/DB/index.ts +++ b/src/DB/index.ts @@ -1,532 +1,3 @@ -import * as assert from "assert"; -import * as mongodb from "mongodb"; -import { connection as getConnection } from "../Connect"; -import { safeExec } from "../Error"; -import { - array, - function as func, - makeCollectionId, - object, - prepareKey, - prepareToRead, - prepareToStore, - string, -} from "../utils"; -import EventEmitter, { Event } from "./EventEmitter"; -import Filter from "./Filter"; -import Join from "./Join"; - -export type Operator = "<" | "<=" | "=" | "<>" | ">=" | ">"; - -export type Order = "asc" | "desc"; - -export type Id = mongodb.ObjectId; - -export type FilterQuery = any> = ( - query: Filter, -) => Filter; - -export type JoinQuery = any> = ( - query: Join, -) => Join; - -export interface GroupQueryObject = any> { - having: ( - field: string, - operator: Operator | any, - value?: any, - ) => GroupQueryObject; -} - -export type GroupQuery = any> = ( - query: GroupQueryObject, -) => void; - -export type Mapper = any> = ( - item: T, - index: number, - items: T[], -) => any; - -export interface Iterator = any> { - hasNext: () => Promise; - next: () => Promise; - [Symbol.asyncIterator]: () => AsyncIterator; -} - -export default class DB = any> extends Filter< - T -> { - protected _query: mongodb.Collection; - - protected _collection!: string; - - protected _pipeline: Array<{ [key: string]: any }> = []; - - protected _mappers: Array> = []; - - public get pipeline() { - return this._resetFilters()._pipeline; - } - - constructor(protected _connection: string) { - super(); - - assert( - string.isString(_connection), - `Expected "connection" to be string, got ${typeof _connection}`, - ); - - this._query = getConnection(_connection) as any; - - this.map(prepareToRead); - } - - protected _resetFilters() { - const FILTER = this._filters; - - if (object.size(FILTER) > 0) this._pipeline.push({ $match: FILTER }); - - this._filter = { - $and: [], - }; - - return this; - } - - /********************************** Event **********************************/ - - protected _emit(event: Event, data: T) { - return new EventEmitter(this._connection, this._collection).emit( - event, - data, - ); - } - - /******************************** Collection *******************************/ - - public static connection = any>( - connection: string, - ): DB { - return new this(connection); - } - - public static collection = any>( - collection: string, - ): DB { - let connection = "default"; - - const keys = collection.split("."); - - if (keys.length === 2) { - connection = keys[0]; - collection = keys[1]; - } - - return this.connection(connection).collection(collection); - } - - public collection(collection: string) { - assert( - (this._query as any).collection, - "Can't change collection name in the middle of query", - ); - assert( - string.isString(collection), - `Expected "collection" to be string, got ${typeof collection}`, - ); - - this._query = ((this._query as any) as mongodb.Db).collection(collection); - - this._collection = collection; - - // this._query.watch( - // [{ $match: { operationType: "update" } }], - // { fullDocument: "updateLookup" } - // ).on("change", console.log); - - return this; - } - - /********************************** Extra **********************************/ - - public aggregate( - ...objects: Record[] | Record[][] - ) { - this._resetFilters(); - - this._pipeline.push(...array.deepFlatten(objects)); - - return this; - } - - /*********************************** Joins **********************************/ - - public join( - collection: string, - query: JoinQuery = (q) => - q.where(makeCollectionId(collection), `${collection}.id`), - as: string = collection, - ) { - assert( - string.isString(collection), - `Expected "collection" to be string, got ${typeof collection}`, - ); - - const join: Join = query(new Join(this._collection, collection, as)) as any; - - this.aggregate(join.pipeline); - - return this; - } - - /********* Mapping, Ordering, Grouping, Limit, Offset & Pagination *********/ - - public map(mapper: Mapper) { - assert( - func.isFunction(mapper), - `Expected "mapper" to be a function, got ${typeof mapper}`, - ); - - this._mappers.push(mapper); - - return this; - } - - // groupBy(field: string, query?: Base.GroupQuery) { - // const MATCH: { [key: string]: any } = {}; - - // this.pipeline({ $group: { _id: field } }, { $project: { [field]: "$_id" } }); - - // if (!query) return this; - - // const QUERY = { - // having: (field: any, operator: any, value?: any) => { - // field = prepareKey(field); - - // if (value === undefined) { - // value = operator; - // operator = "="; - // } - - // MATCH[field] = { - // [`$${OPERATORS[operator]}`]: value, - // }; - - // return QUERY; - // }, - // }; - - // query(QUERY); - - // if (utils.object.isEmpty(MATCH)) return this; - - // return this.pipeline({ $match: MATCH }); - // } - - public orderBy(field: K, order?: Order): this; - public orderBy(field: string, order?: Order): this; - public orderBy(fields: { [field: string]: "asc" | "desc" }): this; - public orderBy( - fields: string | { [field: string]: "asc" | "desc" }, - order?: Order, - ) { - const $sort: { [field: string]: 1 | -1 } = {}; - - if (string.isString(fields)) $sort[fields] = order === "desc" ? -1 : 1; - else - object.forEach( - fields, - (value, field) => ($sort[field] = value === "desc" ? -1 : 1), - ); - - return this.aggregate({ $sort }); - } - - public skip(offset: number) { - return this.aggregate({ $skip: offset }); - } - - public offset(offset: number) { - return this.skip(offset); - } - - public limit(limit: number) { - return this.aggregate({ $limit: limit }); - } - - public take(limit: number) { - return this.limit(limit); - } - - public paginate(page = 0, limit = 10) { - return this.skip(page * limit).limit(limit); - } - - /********************************* Indexes *********************************/ - - public indexes(): Promise { - return safeExec(this._query, "indexes", []); - } - - public index( - fieldOrSpec: string | Record, - options?: mongodb.IndexOptions, - ) { - return safeExec(this._query, "createIndex", [fieldOrSpec, options]); - } - - public reIndex(): Promise { - return safeExec(this._query, "reIndex", []); - } - - public dropIndex( - indexName: string, - options?: mongodb.CommonOptions & { maxTimeMS?: number }, - ): Promise { - return safeExec(this._query, "dropIndex", [indexName, options]); - } - - /*********************************** Read **********************************/ - - private _aggregate(options?: mongodb.CollectionAggregationOptions) { - this._resetFilters(); - - return this._mappers.reduce( - (query: any, mapper) => query.map(mapper), - this._query.aggregate(this._pipeline, options), - ) as mongodb.AggregationCursor; - } - - private _filtersOnly() { - this._resetFilters(); - - let filters: { [key: string]: any } = { - $and: this._pipeline - .filter((pipe) => pipe.$match) - .map((pipe) => pipe.$match), - }; - - if (filters.$and.length === 0) filters = {}; - - return filters; - } - - public async count(): Promise { - this.aggregate({ $count: "count" }); - - const result = (await this.first()) as any; - - return result ? result.count : 0; - } - - public async exists(): Promise { - return (await this.count()) !== 0; - } - - public iterate(fields?: string[]): Iterator { - if (fields) - this.aggregate({ - $project: fields.reduce( - (prev, cur) => ((prev[prepareKey(cur)] = 1), prev), - { _id: 0 } as { [key: string]: any }, - ), - }); - - const cursor = this._aggregate(); - - const iterator = { - hasNext: () => cursor.hasNext(), - next: () => cursor.next(), - [Symbol.asyncIterator]: () => ({ - next: async (): Promise<{ value: T; done: boolean }> => { - const value = await iterator.next(); - - if (value) - return { - value, - done: false, - }; - - return { done: true } as any; - }, - }), - }; - - return iterator; - } - - public get(fields?: Array): Promise { - if (fields) - this.aggregate({ - $project: fields.reduce( - (prev, cur) => ((prev[prepareKey(cur as string)] = 1), prev), - { _id: 0 } as { [key: string]: any }, - ), - }); - - return safeExec(this._aggregate(), "toArray", []); - } - - public async first( - fields?: Array, - ): Promise { - this._resetFilters().limit(1); - - return (await this.get(fields))[0]; - } - - public value(field: K): Promise; - public value(field: string): Promise; - public value(field: string) { - field = prepareKey(field); - - const keys = field.split("."); - - return safeExec( - this.map((item: any) => keys.reduce((prev, key) => prev[key], item)) - .aggregate({ - $project: { - _id: 0, - [field]: { $ifNull: [`$${field}`, "$__NULL__"] }, - }, - }) - ._aggregate(), - "toArray", - [], - ); - } - - public pluck(field: K): Promise; - public pluck(field: string): Promise; - public pluck(field: string) { - return this.value(field) as any; - } - - public max(field: K): Promise; - public max(field: string): Promise; - public async max(field: string) { - this.aggregate({ $group: { _id: null, max: { $max: `$${field}` } } }); - - const result = (await this.first()) as any; - - return result && result.max; - } - - public min(field: K): Promise; - public min(field: string): Promise; - public async min(field: string) { - this.aggregate({ $group: { _id: null, min: { $min: `$${field}` } } }); - - const result = (await this.first()) as any; - - return result && result.min; - } - - public avg(field: K): Promise; - public avg(field: string): Promise; - public async avg(field: string) { - this.aggregate({ $group: { _id: null, avg: { $avg: `$${field}` } } }); - - const result = (await this.first()) as any; - - return result && result.avg; - } - - /********************************** Inserts *********************************/ - - protected _insertMany(items: T[]): Promise { - items = prepareToStore(items); - - return safeExec(this._query, "insertMany", [items], (saved) => { - if (!saved) return; - - saved.ops.forEach((op: any) => this._emit("create", op)); - }); - } - - protected _insertOne(item: T): Promise { - item = prepareToStore(item); - - return safeExec(this._query, "insertOne", [item], (saved) => { - if (!saved) return; - - saved.ops.forEach((op: any) => this._emit("create", op)); - }); - } - - public async insert(item: T | T[]): Promise { - if (Array.isArray(item)) { - return (await this._insertMany(item)).insertedCount; - } - - return (await this._insertOne(item)).insertedCount; - } - - public async insertGetId(item: T): Promise { - return (await this._insertOne(item)).insertedId; - } - - /********************************** Updates *********************************/ - - protected async _update( - update: Record, - soft?: { - type: "delete" | "restore"; - field: string; - value: Date; - }, - ): Promise { - const filters = this._filtersOnly(); - - return safeExec(this._query, "updateMany", [filters, update]); - } - - public async update(update: Partial): Promise { - const _update = { - $set: prepareToStore(update), - }; - - return (await this._update(_update)).modifiedCount; - } - - public increment(field: string, count?: number): Promise; - public increment( - field: K, - count?: number, - ): Promise; - public async increment(field: string, count = 1) { - const update = { - $inc: { - [field]: count, - }, - }; - - return (await this._update(update)).modifiedCount; - } - - public decrement(field: string, count?: number): Promise; - public decrement( - field: K, - count?: number, - ): Promise; - public decrement(field: string, count = 1) { - return this.increment(field, -count) as any; - } - - public unset(fields: string[]): Promise; - public unset(fields: K[]): Promise; - public unset(fields: string[]) { - return this._update({ - $unset: fields.reduce((prev, cur) => ({ ...prev, [cur]: 1 }), {}), - }) as any; - } - - /********************************** Deletes *********************************/ - - public async delete(): Promise { - const filters = this._filtersOnly(); - - return (await safeExec(this._query, "deleteMany", [filters])).deletedCount; - } -} +export { default as DB } from "./DB"; +export { default as Filter } from "./Filter"; +export { default as Join } from "./Join"; diff --git a/src/constants/general/DB.ts b/src/constants/general/DB.ts new file mode 100644 index 0000000..7d9a43f --- /dev/null +++ b/src/constants/general/DB.ts @@ -0,0 +1,82 @@ +import { Join } from "./Join"; +import { Obj } from "./general"; + +export interface DB extends Join { + /* ------------------------- INSERT ------------------------- */ + + insert(item: T): Promise; + + insert(items: T[]): Promise; + + /* ------------------------- INSERT GET ID ------------------------- */ + + insertGetId(item: T): Promise; + + /* ------------------------- GET ------------------------- */ + + get(): Promise; + + get(...select: Array): Promise[]>; + + /* ------------------------- COUNT ------------------------- */ + + count(): Promise; + + /* ------------------------- EXISTS ------------------------- */ + + exists(): Promise; + + /* ------------------------- PLUCK ------------------------- */ + + pluck(field: K): Promise; + + pluck(field: string): Promise; + + value(field: K): Promise; + + value(field: string): Promise; + + /* ------------------------- FIRST ------------------------- */ + + first(): Promise; + + first(...select: Array): Promise>; + + /* ------------------------- MAXIMUM ------------------------- */ + + max(field: K): Promise; + + max(field: string): Promise; + + /* ------------------------- MINIMUM ------------------------- */ + + min(field: K): Promise; + + min(field: string): Promise; + + /* ------------------------- AVERAGE ------------------------- */ + + avg(field: K): Promise; + + avg(field: string): Promise; + + /* ------------------------- ASYNC ITERATOR ------------------------- */ + + [Symbol.asyncIterator](): AsyncIterator; + + /* ------------------------- UPDATE ------------------------- */ + + update(update: Partial): Promise; + + /* ------------------------- INCREMENT ------------------------- */ + + increment(field: keyof T | string, count?: number): Promise; + + /* ------------------------- DECREMENT ------------------------- */ + + decrement(field: keyof T | string, count?: number): Promise; + + /* ------------------------- DELETE ------------------------- */ + + delete(): Promise; +} diff --git a/src/constants/general/Filter.ts b/src/constants/general/Filter.ts new file mode 100644 index 0000000..2b32758 --- /dev/null +++ b/src/constants/general/Filter.ts @@ -0,0 +1,71 @@ +import { Obj, Operator } from "./general"; + +export interface Filter { + /* ------------------------- WHERE ------------------------- */ + + where(query: FilterQuery): this; + + where(field: K, value: T[K]): this; + + where(field: string, value: unknown): this; + + where(field: K, operator: Operator, value: T[K]): this; + + where(field: string, operator: Operator, value: unknown): this; + + /* ------------------------- OR WHERE ------------------------- */ + + orWhere(query: FilterQuery): this; + + orWhere(field: K, value: T[K]): this; + + orWhere(field: string, value: unknown): this; + + orWhere(field: K, operator: Operator, value: T[K]): this; + + orWhere(field: string, operator: Operator, value: unknown): this; + + /* ------------------------- WHERE NULL ------------------------- */ + + whereNull(field: keyof T | string): this; + + /* ------------------------- WHERE NOT NULL ------------------------- */ + + whereNotNull(field: keyof T | string): this; + + /* ------------------------- WHERE LIKE ------------------------- */ + + whereLike(field: keyof T | string, value: string | RegExp): this; + + /* ------------------------- WHERE NOT LIKE ------------------------- */ + + whereNotLike(field: keyof T | string, value: string | RegExp): this; + + /* ------------------------- WHERE IN ------------------------- */ + + whereIn(field: K, values: T[K][]): this; + + whereIn(field: string, values: unknown[]): this; + + /* ------------------------- WHERE NOT IN ------------------------- */ + + whereNotIn(field: K, values: T[K][]): this; + + whereNotIn(field: string, values: unknown[]): this; + + /* ------------------------- WHERE BETWEEN ------------------------- */ + + whereBetween(field: K, start: T[K], end: T[K]): this; + + whereBetween(field: string, start: unknown, end: unknown): this; + + /* ------------------------- WHERE NOT BETWEEN ------------------------- */ + + whereNotBetween(field: K, start: T[K], end: T[K]): this; + + whereNotBetween(field: string, start: unknown, end: unknown): this; +} + +export type FilterQuery = Filter> = ( + query: F, +) => unknown; diff --git a/src/constants/general/Join.ts b/src/constants/general/Join.ts new file mode 100644 index 0000000..33a0c21 --- /dev/null +++ b/src/constants/general/Join.ts @@ -0,0 +1,34 @@ +import { Filter } from "./Filter"; +import { Obj, Order } from "./general"; + +export interface Join extends Filter { + /* ------------------------- JOIN ------------------------- */ + + join(table: string, query?: JoinQuery, as?: string): this; + + /* ------------------------- ORDER ------------------------- */ + + orderBy(field: keyof T | string, order?: Order): this; + + orderBy(field: { [key in keyof T | string]?: Order }): this; + + /* ------------------------- OFFSET ------------------------- */ + + offset(offset: number): this; + + skip(offset: number): this; + + /* ------------------------- LIMIT ------------------------- */ + + limit(limit: number): this; + + take(limit: number): this; + + /* ------------------------- PAGINATION ------------------------- */ + + paginate(page: number, limit?: number): this; +} + +export type JoinQuery = Join> = ( + query: J, +) => unknown; diff --git a/src/constants/general/general.ts b/src/constants/general/general.ts new file mode 100644 index 0000000..19dc6d7 --- /dev/null +++ b/src/constants/general/general.ts @@ -0,0 +1,23 @@ +export type Obj = { [key: string]: unknown }; + +export type Item = T; + +export type Constructor = { new (...args: unknown[]): T }; + +export const enum OPERATOR { + LT = "<", + LTE = "<=", + EQ = "=", + NEQ = "!=", + GTE = ">=", + GT = ">", +} + +export type Operator = OPERATOR | "<" | "<=" | "=" | "!=" | ">=" | ">"; + +export const enum ORDER { + ASC = "asc", + DESC = "desc", +} + +export type Order = ORDER | "asc" | "desc"; diff --git a/src/constants/general/index.ts b/src/constants/general/index.ts new file mode 100644 index 0000000..205d666 --- /dev/null +++ b/src/constants/general/index.ts @@ -0,0 +1,4 @@ +export * from "./DB"; +export * from "./Filter"; +export * from "./general"; +export * from "./Join"; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..31aa6ff --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,2 @@ +export * from "./general"; +export * from "./mongo"; diff --git a/src/constants/mongo/DB.ts b/src/constants/mongo/DB.ts new file mode 100644 index 0000000..fcaaaaf --- /dev/null +++ b/src/constants/mongo/DB.ts @@ -0,0 +1,33 @@ +import * as mongo from "mongodb"; +import { MongoID } from "./general"; +import { MongoJoin } from "./Join"; +import { Obj, DB } from "../general"; + +export interface MongoDB extends DB, MongoJoin { + /* ------------------------- UNSET ------------------------- */ + + unset(fields: string[]): Promise; + + unset(fields: K[]): Promise; + + /* ------------------------- INDEXES ------------------------- */ + + indexes(): Promise; + + /* ------------------------- INDEX ------------------------- */ + + index(field: string, options?: mongo.IndexOptions): Promise; + + index(spec: Obj, options?: mongo.IndexOptions): Promise; + + /* ------------------------- REINDEX ------------------------- */ + + reIndex(): Promise; + + /* ------------------------- DROP INDEX ------------------------- */ + + dropIndex( + index: string, + options?: mongo.CommonOptions & { maxTimeMS?: number }, + ): Promise; +} diff --git a/src/constants/mongo/Filter.ts b/src/constants/mongo/Filter.ts new file mode 100644 index 0000000..f51758b --- /dev/null +++ b/src/constants/mongo/Filter.ts @@ -0,0 +1,5 @@ +import { MongoPipeline } from "./aggregation"; +import { Obj, Filter } from "../general"; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MongoFilter extends Filter {} diff --git a/src/constants/mongo/Join.ts b/src/constants/mongo/Join.ts new file mode 100644 index 0000000..dc40339 --- /dev/null +++ b/src/constants/mongo/Join.ts @@ -0,0 +1,8 @@ +import { MongoPipeline } from "./aggregation"; +import { Obj, Join } from "../general"; + +export interface MongoJoin extends Join { + /* ------------------------- PIPELINE ------------------------- */ + + pipe(...pipeline: MongoPipeline): this; +} diff --git a/src/constants/mongo/aggregation.ts b/src/constants/mongo/aggregation.ts new file mode 100644 index 0000000..67e8099 --- /dev/null +++ b/src/constants/mongo/aggregation.ts @@ -0,0 +1,69 @@ +import { Obj } from "../general"; + +export type MongoPipeline = MongoPipelineStage[]; + +export type MongoPipelineStage = + | MongoPipelineStageCount + | MongoPipelineStageGroup + | MongoPipelineStageLimit + | MongoPipelineStageLookup + | MongoPipelineStageMatch + | MongoPipelineStageProject + | MongoPipelineStageSkip + | MongoPipelineStageSort; + +export interface MongoPipelineStageCount { + $count: string; +} + +export interface MongoPipelineStageGroup { + $group: { + // TODO: + _id: unknown; + [field: string]: unknown; + }; +} + +export interface MongoPipelineStageLimit { + $limit: number; +} + +export interface MongoPipelineStageLookup { + $lookup: + | { + from: string; + localField: keyof T; + foreignField: string; + as: string; + } + | { + from: string; + let: { [field: string]: string }; + pipeline: MongoPipeline; + as: string; + }; +} + +export interface MongoPipelineStageMatch { + $match: Partial | Obj; +} + +export interface MongoPipelineStageProject { + $project: { [field: string]: 1 | 0 | boolean | MongoPipelineExpression }; +} + +export interface MongoPipelineStageSkip { + $skip: number; +} + +export interface MongoPipelineStageSort { + $sort: { [field: string]: 1 | -1 | { $meta: string } }; +} + +export interface MongoPipelineStageUnset { + $unset: string | string[]; +} + +export interface MongoPipelineExpression { + [expression: string]: unknown; +} diff --git a/src/constants/mongo/general.ts b/src/constants/mongo/general.ts new file mode 100644 index 0000000..7ce096a --- /dev/null +++ b/src/constants/mongo/general.ts @@ -0,0 +1,22 @@ +import { ObjectId } from "mongodb"; +import { Operator, Order } from "../general"; + +export type MongoID = ObjectId; + +export const MONGO_ROOT = "$$ROOT"; + +export const MONGO_NULL = "$__NULL__"; + +export const MONGO_OPERATOR_MAP: { [key in Operator]: string } = { + "<": "$lt", + "<=": "$lte", + "=": "$eq", + "!=": "$ne", + ">=": "$gte", + ">": "$gt", +}; + +export const MONGO_ORDER_MAP: { [key in Order]: number } = { + asc: 1, + desc: -1, +}; diff --git a/src/constants/mongo/index.ts b/src/constants/mongo/index.ts new file mode 100644 index 0000000..81144f7 --- /dev/null +++ b/src/constants/mongo/index.ts @@ -0,0 +1,5 @@ +export * from "./aggregation"; +export * from "./DB"; +export * from "./Filter"; +export * from "./general"; +export * from "./Join"; diff --git a/src/utils.ts b/src/utils.ts index 2fcbb31..e140796 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -115,7 +115,7 @@ export const OPERATORS: { [operator: string]: string } = { "<": "lt", "<=": "lte", "=": "eq", - "<>": "ne", + "!=": "ne", ">=": "gte", ">": "gt", }; diff --git a/tsconfig.json b/tsconfig.json index 0912982..9178f73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ + "incremental": true, /* Enable incremental compilation */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ "lib": ["esnext"], /* Specify library files to be included in the compilation. */ @@ -48,7 +48,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -64,7 +64,8 @@ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "preserveConstEnums": true }, "include": [ "src/**/*"