From 6113bf5b1aadc4ba2ff63a6147ea10704f8f642f Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Tue, 4 Dec 2018 22:52:02 +0330 Subject: [PATCH] WIP --- CHANGELOG.md | 4 ++++ package-lock.json | 26 +++++++++++++------------- package.json | 6 +++--- src/DB/Join.ts | 14 ++++++-------- src/Relation/Base.ts | 7 +++++++ src/Relation/EmbedMany.ts | 19 +++++++++++++++++-- src/Relation/HasMany.ts | 17 ++++++++++++++++- src/Relation/HasOne.ts | 20 +++++++++++++++++--- src/Relation/MorphMany.ts | 19 +++++++++++++++++++ src/Relation/MorphOne.ts | 20 ++++++++++++++++++++ src/base/Query.ts | 12 ++++++++---- src/base/QueryBuilder.ts | 6 ++++++ test/relations/HasMany.ts | 11 +++++++++++ test/relations/HasOne.ts | 11 +++++++++++ test/relations/MorphMany.ts | 11 +++++++++++ test/relations/MorphOne.ts | 13 +++++++++++++ 16 files changed, 182 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9837a1f..dcea102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ --- +## [v0.6.0](https://github.com/foxifyjs/odin/releases/tag/v0.6.0) - *(2018-12-04)* + +- :zap: Added `has` to model queries (use with caution since it may have a negative impact on your performance) + ## [v0.5.4](https://github.com/foxifyjs/odin/releases/tag/v0.5.4) - *(2018-12-03)* - :zap: Added `Collection` to create collection and indexes diff --git a/package-lock.json b/package-lock.json index d26baf7..d8035fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foxify/odin", - "version": "0.5.4", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -129,18 +129,18 @@ "dev": true }, "@types/mongodb": { - "version": "3.1.15", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.15.tgz", - "integrity": "sha512-JSvtmrdrh88WH0Lo8Hq7sB1FkEChkrt6+fAZdFhEsRXcUetnrdU7wd2yar40tPg5wfRI2t31yduQgPiMUvgEEA==", + "version": "3.1.17", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.17.tgz", + "integrity": "sha512-u6tSIpfdsgK74aE0TuyqZYhHscw+gHs6dQNSsFUTFXubhhxCqovmV3nJRS0YKSw0sfqbzUgGzbG5+yorUPRnFg==", "requires": { "@types/bson": "*", "@types/node": "*" } }, "@types/node": { - "version": "10.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.11.tgz", - "integrity": "sha512-3iIOhNiPGTdcUNVCv9e5G7GotfvJJe2pc9w2UgDXlUwnxSZ3RgcUocIU+xYm+rTU54jIKih998QE4dMOyMN1NQ==" + "version": "10.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.12.tgz", + "integrity": "sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A==" }, "abab": { "version": "2.0.0", @@ -979,7 +979,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, "requires": { @@ -1048,7 +1048,7 @@ }, "buffer": { "version": "3.6.0", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", "dev": true, "requires": { @@ -1487,13 +1487,13 @@ "dependencies": { "file-type": { "version": "3.9.0", - "resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", "dev": true }, "get-stream": { "version": "2.3.1", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, "requires": { @@ -1707,7 +1707,7 @@ }, "es6-promisify": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "dev": true, "requires": { @@ -5805,7 +5805,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, diff --git a/package.json b/package.json index 7ee0a1e..a474721 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@foxify/odin", - "version": "0.5.4", + "version": "0.6.0", "description": "Active Record Model", "author": "Ardalan Amini [https://github.com/ardalanamini]", "contributors": [ @@ -44,8 +44,8 @@ "dependencies": { "@types/graphql": "^0.13.4", "@types/graphql-iso-date": "^3.3.1", - "@types/mongodb": "^3.1.15", - "@types/node": "^10.12.11", + "@types/mongodb": "^3.1.17", + "@types/node": "^10.12.12", "async": "^2.6.1", "caller-id": "^0.1.0", "deasync": "^0.1.14", diff --git a/src/DB/Join.ts b/src/DB/Join.ts index 544e68d..3bbbbc9 100644 --- a/src/DB/Join.ts +++ b/src/DB/Join.ts @@ -12,10 +12,10 @@ class Join extends Filter { return { $lookup: { - as: this._as, - from: this._collection, let: this._let, + from: this._collection, pipeline: this._pipeline, + as: this._as, }, }; } @@ -54,10 +54,9 @@ class Join extends Filter { keys.push(prepareKey(keys.pop() as string)); - const key = keys.join("."); - const pivotKey = `pivot_${key}`; + const pivotKey = `pivot_${keys.join("_ODIN_")}`; - this._let[pivotKey] = `$${key}`; + this._let[pivotKey] = `$${keys.join(".")}`; return this._push_filter("and", { $expr: { @@ -75,10 +74,9 @@ class Join extends Filter { keys.push(prepareKey(keys.pop() as string)); - const key = keys.join("."); - const pivotKey = `pivot_${key}`; + const pivotKey = `pivot_${keys.join("_ODIN_")}`; - this._let[pivotKey] = `$${key}`; + this._let[pivotKey] = `$${keys.join(".")}`; return this._push_filter("or", { $expr: { diff --git a/src/Relation/Base.ts b/src/Relation/Base.ts index e5b9695..43d040b 100644 --- a/src/Relation/Base.ts +++ b/src/Relation/Base.ts @@ -10,6 +10,11 @@ namespace Relation { name: string; relations: Relation[]; } + + export interface RelationCount { + name: string; + relation?: RelationCount; + } } abstract class Relation { @@ -38,6 +43,8 @@ abstract class Relation { public abstract load(query: DB | Join, relations: Relation.Relation[]): DB | Join; + public abstract loadCount(query: DB | Join): DB | Join; + /****************************** With Relations ******************************/ public with(...relations: string[]): Query; diff --git a/src/Relation/EmbedMany.ts b/src/Relation/EmbedMany.ts index dd37d9e..09fb3cd 100644 --- a/src/Relation/EmbedMany.ts +++ b/src/Relation/EmbedMany.ts @@ -1,7 +1,7 @@ import * as Odin from ".."; import * as DB from "../DB"; import Join from "../DB/Join"; -import { makeCollectionId } from "../utils"; +import { array, makeCollectionId } from "../utils"; import Relation from "./Base"; import HasMany from "./HasMany"; @@ -34,10 +34,25 @@ class EmbedMany extends HasMany { }, q.whereIn(this.foreignKey, `${this.model.constructor.toString()}.${this.localKey}`) ), - // q => q.whereIn(this.foreignKey, `${this.model.constructor.toString()}.${this.localKey}`), this.as ); } + + public loadCount(query: DB | Join) { + return query + .join( + this.relation.toString(), + q => q + .whereIn(this.foreignKey, `${this.model.constructor.toString()}.data.${this.localKey}`), + "relation" + ) + .pipeline({ + $project: { + data: 1, + count: { $size: "$relation" }, + }, + }); + } } export default EmbedMany; diff --git a/src/Relation/HasMany.ts b/src/Relation/HasMany.ts index 2894a35..7e562b8 100644 --- a/src/Relation/HasMany.ts +++ b/src/Relation/HasMany.ts @@ -33,10 +33,25 @@ class HasMany extends Relation { }, q.where(this.foreignKey, `${this.model.constructor.toString()}.${this.localKey}`) ), - // q => q.where(this.foreignKey, `${this.model.constructor.toString()}.${this.localKey}`), this.as ); } + + public loadCount(query: DB | Join) { + return query + .join( + this.relation.toString(), + q => q + .where(this.foreignKey, `${this.model.constructor.toString()}.data.${this.localKey}`), + "relation" + ) + .pipeline({ + $project: { + data: 1, + count: { $size: "$relation" }, + }, + }); + } } export default HasMany; diff --git a/src/Relation/HasOne.ts b/src/Relation/HasOne.ts index aee8705..66eddbd 100644 --- a/src/Relation/HasOne.ts +++ b/src/Relation/HasOne.ts @@ -35,15 +35,29 @@ class HasOne extends Relation { q.where(this.foreignKey, `${this.model.constructor.toString()}.${this.localKey}`) .limit(1) ), - // q => q - // .where(this.foreignKey, `${this.model.constructor.toString()}.${this.localKey}`) - // .limit(1), name ).pipeline({ $unwind: { path: `$${name}`, preserveNullAndEmptyArrays: true }, }); } + public loadCount(query: DB | Join) { + return query + .join( + this.relation.toString(), + q => q + .where(this.foreignKey, `${this.model.constructor.toString()}.data.${this.localKey}`) + .limit(1), + "relation" + ) + .pipeline({ + $project: { + data: 1, + count: { $size: "$relation" }, + }, + }); + } + public insert(items: T[]): Promise; public insert(items: T[], callback: DB.Callback): void; public async insert(items: T[], callback?: DB.Callback) { diff --git a/src/Relation/MorphMany.ts b/src/Relation/MorphMany.ts index ddc1aed..164eb6d 100644 --- a/src/Relation/MorphMany.ts +++ b/src/Relation/MorphMany.ts @@ -31,6 +31,25 @@ class MorphMany extends MorphBase { name ); } + + public loadCount(query: DB | Join) { + const constructor = this.model.constructor; + + return query + .join( + this.relation.toString(), + q => q + .where(this.foreignKey, `${constructor.toString()}.data.${this.localKey}`) + .where(`${this.type}_type`, constructor.name), + "relation" + ) + .pipeline({ + $project: { + data: 1, + count: { $size: "$relation" }, + }, + }); + } } export default MorphMany; diff --git a/src/Relation/MorphOne.ts b/src/Relation/MorphOne.ts index eea80aa..c591958 100644 --- a/src/Relation/MorphOne.ts +++ b/src/Relation/MorphOne.ts @@ -36,6 +36,26 @@ class MorphOne extends MorphBase { }); } + public loadCount(query: DB | Join) { + const constructor = this.model.constructor; + + return query + .join( + this.relation.toString(), + q => q + .where(this.foreignKey, `${constructor.toString()}.data.${this.localKey}`) + .where(`${this.type}_type`, constructor.name) + .limit(1), + "relation" + ) + .pipeline({ + $project: { + data: 1, + count: { $size: "$relation" }, + }, + }); + } + public insert(items: T[]): Promise; public insert(items: T[], callback: DB.Callback): void; public async insert(items: T[], callback?: DB.Callback) { diff --git a/src/base/Query.ts b/src/base/Query.ts index e3ff85e..460621a 100644 --- a/src/base/Query.ts +++ b/src/base/Query.ts @@ -63,21 +63,25 @@ class Query extends DB { /****************************** Has & WhereHas ******************************/ - // TODO: join relation's count public has(relation: string, operator: DB.Operator = ">", count: number = 0) { + if (!(this._model as any)._relations.includes(relation)) + throw new Error(`Relation '${relation}' does not exist on '${this._model.name}' Model`); + this.pipeline({ $project: { - _id: 0, data: "$$ROOT", }, }); // join relation + (this._model.prototype as any)[relation]().loadCount(this); return this.pipeline( { - relation: { - [OPERATORS[operator]]: count, // filter data according to count and operator + $match: { + count: { + [`$${OPERATORS[operator]}`]: count, // filter data according to count and operator + }, }, }, { diff --git a/src/base/QueryBuilder.ts b/src/base/QueryBuilder.ts index 47fec87..6e8101c 100644 --- a/src/base/QueryBuilder.ts +++ b/src/base/QueryBuilder.ts @@ -47,6 +47,12 @@ class QueryBuilder extends Base { return this.query().lean(); } + /****************************** Has & WhereHas ******************************/ + + public static has(relation: string, operator?: DB.Operator, count?: number) { + return this.query().has(relation, operator, count); + } + /****************************** With Relations ******************************/ public static with(...relations: string[]): Query; diff --git a/test/relations/HasMany.ts b/test/relations/HasMany.ts index bc9641d..805ebc2 100644 --- a/test/relations/HasMany.ts +++ b/test/relations/HasMany.ts @@ -1,4 +1,5 @@ import * as Odin from "../../src"; +import { array } from "../../src/utils"; declare global { namespace NodeJS { @@ -207,3 +208,13 @@ test("Model.with deep", async () => { expect(results4.map((item: any) => item.toJSON())).toEqual(items); }); + +test("Model.has", async () => { + expect.assertions(1); + + const items = CHATS.filter(chat => array.any(MESSAGES, message => message.chatname === chat.name)); + + const results = await Chat.has("messages").lean().get(); + + expect(results).toEqual(items); +}); diff --git a/test/relations/HasOne.ts b/test/relations/HasOne.ts index 90616ec..440a072 100644 --- a/test/relations/HasOne.ts +++ b/test/relations/HasOne.ts @@ -1,4 +1,5 @@ import * as Odin from "../../src"; +import { array } from "../../src/utils"; declare global { namespace NodeJS { @@ -182,3 +183,13 @@ test("Model.with deep", async () => { expect(results4.map((item: any) => item.toJSON())).toEqual(items); }); + +test("Model.has", async () => { + expect.assertions(1); + + const items = USERS.filter(user => array.any(CHATS, chat => chat.username === user.username)); + + const results = await User.has("chat").lean().get(); + + expect(results).toEqual(items); +}); diff --git a/test/relations/MorphMany.ts b/test/relations/MorphMany.ts index c763604..6eb44e1 100644 --- a/test/relations/MorphMany.ts +++ b/test/relations/MorphMany.ts @@ -1,4 +1,5 @@ import * as Odin from "../../src"; +import { array } from "../../src/utils"; declare global { namespace NodeJS { @@ -221,3 +222,13 @@ test("Model.with deep", async () => { expect(results4.map((item: any) => item.toJSON())).toEqual(items); }); + +test("Model.has", async () => { + expect.assertions(1); + + const items = CHATS.filter(chat => array.any(MESSAGES, message => message.chat === chat.name)); + + const results = await Chat.has("messages").lean().get(); + + expect(results).toEqual(items); +}); diff --git a/test/relations/MorphOne.ts b/test/relations/MorphOne.ts index 7f39ed6..7b9a029 100644 --- a/test/relations/MorphOne.ts +++ b/test/relations/MorphOne.ts @@ -1,4 +1,5 @@ import * as Odin from "../../src"; +import { array } from "../../src/utils"; declare global { namespace NodeJS { @@ -189,3 +190,15 @@ test("Model.with deep", async () => { expect(results4.map((item: any) => item.toJSON())).toEqual(items); }); + +test("Model.has", async () => { + expect.assertions(1); + + const items = USERS.filter( + user => array.any(CHATS, chat => chat.chatable_id === user.username && chat.chatable_type === "User") + ); + + const results = await User.has("chat").lean().get(); + + expect(results).toEqual(items); +});