From 4a8677c434b7c43591c4637fc59dd414e56a4231 Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Thu, 27 Dec 2018 20:39:34 +0330 Subject: [PATCH] Multiple bug fixes --- .travis.yml | 1 + CHANGELOG.md | 8 ++ package-lock.json | 80 ++++++------ package.json | 14 ++- src/Base.ts | 22 ++-- src/DB/Join.ts | 4 +- src/GraphQL/Model.ts | 32 ++--- src/GraphQL/Type.ts | 62 ---------- src/Relation/Base.ts | 10 +- src/Relation/EmbedMany.ts | 53 +++++--- src/Relation/HasMany.ts | 31 +++-- src/Relation/HasOne.ts | 55 +++++---- src/Relation/MorphMany.ts | 55 +++++---- src/Relation/MorphOne.ts | 57 +++++---- src/base/Query.ts | 4 +- src/index.ts | 49 +------- src/types/Any.ts | 106 ---------------- src/types/Array.ts | 62 ---------- src/types/Boolean.ts | 14 --- src/types/Date.ts | 32 ----- src/types/Id.ts | 14 +++ src/types/Number.ts | 53 -------- src/types/Object.ts | 16 --- src/types/ObjectId.ts | 14 --- src/types/String.ts | 112 ----------------- src/types/index.ts | 155 ++++++++++++++++++----- test/relations/EmbedMany.ts | 240 ++++++++++++++++++++++++++++++++++++ test/relations/HasMany.ts | 76 ++++++------ 28 files changed, 678 insertions(+), 753 deletions(-) delete mode 100644 src/GraphQL/Type.ts delete mode 100644 src/types/Any.ts delete mode 100644 src/types/Array.ts delete mode 100644 src/types/Boolean.ts delete mode 100644 src/types/Date.ts create mode 100644 src/types/Id.ts delete mode 100644 src/types/Number.ts delete mode 100644 src/types/Object.ts delete mode 100644 src/types/ObjectId.ts delete mode 100644 src/types/String.ts create mode 100644 test/relations/EmbedMany.ts diff --git a/.travis.yml b/.travis.yml index a3f6d04..d2ef997 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ os: - osx node_js: + - "11" - "10" - "9" - "8" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7605107..ee60ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ --- +## [v0.8.0](https://github.com/foxifyjs/odin/releases/tag/v0.8.0) - *(2018-12-27)* + +- :beetle: Fixed `embedMany` relation bug +- :beetle: Fixed not applying `withTrashed` to relations +- :boom: `Types` is now a peerDependency ([`@foxify/schema`](https://github.com/foxifyjs/schema)) which needs to be installed! +- :eyeglasses: Added `embedMany` tests +- :eyeglasses: Added `Node.js` version `11` to tests + ## [v0.7.0](https://github.com/foxifyjs/odin/releases/tag/v0.7.0) - *(2018-12-14)* - :zap: Added `whereHas` method to models diff --git a/package-lock.json b/package-lock.json index 822d923..f863acd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foxify/odin", - "version": "0.7.5", + "version": "0.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -89,6 +89,16 @@ "tslib": "^1.8.1" } }, + "@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" + } + }, "@types/async": { "version": "2.0.50", "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.50.tgz", @@ -129,18 +139,18 @@ "dev": true }, "@types/mongodb": { - "version": "3.1.17", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.17.tgz", - "integrity": "sha512-u6tSIpfdsgK74aE0TuyqZYhHscw+gHs6dQNSsFUTFXubhhxCqovmV3nJRS0YKSw0sfqbzUgGzbG5+yorUPRnFg==", + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.18.tgz", + "integrity": "sha512-8m8yvrDagesNNJdOIMk+g0b5z/sW48FwNp4GS1tRcHMKfQ/o40WsFXOXSfYY7y4xkutPFGQKGb4/GpzGFhpnqw==", "requires": { "@types/bson": "*", "@types/node": "*" } }, "@types/node": { - "version": "10.12.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz", - "integrity": "sha512-9kROxduaN98QghwwHmxXO2Xz3MaWf+I1sLVAA6KJDF5xix+IyXVhds0MAfdNwtcpSrzhaTsNB0/jnL86fgUhqA==" + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" }, "abab": { "version": "2.0.0", @@ -1277,8 +1287,7 @@ "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true, - "optional": true + "dev": true }, "commondir": { "version": "1.0.1", @@ -1503,6 +1512,12 @@ } } }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1965,9 +1980,9 @@ } }, "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -2620,9 +2635,9 @@ "dev": true }, "get-port": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.0.0.tgz", - "integrity": "sha512-Yy3yNI2oShgbaWg4cmPhWjkZfktEvpKI09aDX4PZzNtlU9obuYrX7x2mumQsrNxlF+Ls7OtMQW/u+X4s896bOQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.1.0.tgz", + "integrity": "sha512-4/fqAYrzrzOiqDrdeZRKXGdTGgbkfTEumGlNQPeP6Jy8w0PzN9mzeNQ3XgHaTNie8pQ3hOUkrwlZt2Fzk5H9mA==", "dev": true }, "get-stream": { @@ -4167,14 +4182,15 @@ } }, "mongodb-memory-server": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-2.8.0.tgz", - "integrity": "sha512-PKa56QdqyyO2srfba7hzUSmSJMrARXnBAbTWtJFkg8McPA07ufKtqb+iW2DOuOJYKo0k55ni7V+eCdN/sUu7+Q==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-2.9.1.tgz", + "integrity": "sha512-SmAJMTiD3X4sY6neu+UsBAmbiyK3IikvZhf9unF0VdC3YE+vMkTcClbx1DuGVWQmimGPSt9d0odFLUmgKS3I/A==", "dev": true, "requires": { - "@babel/runtime": "^7.1.2", + "@babel/runtime": "^7.2.0", "debug": "^4.1.0", "decompress": "^4.2.0", + "dedent": "^0.7.0", "find-cache-dir": "^2.0.0", "get-port": "^4.0.0", "getos": "^3.1.1", @@ -4187,9 +4203,9 @@ }, "dependencies": { "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" @@ -5961,9 +5977,9 @@ "dev": true }, "tslint": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", - "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.0.tgz", + "integrity": "sha512-CKEcH1MHUBhoV43SA/Jmy1l24HJJgI0eyLbBNSRyFlsQvb9v6Zdq+Nz2vEOH00nC5SUx4SneJ59PZUS/ARcokQ==", "dev": true, "requires": { "babel-code-frame": "^6.22.0", @@ -5980,19 +5996,13 @@ "tsutils": "^2.27.2" }, "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true - }, "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", "dev": true, "requires": { - "path-parse": "^1.0.5" + "path-parse": "^1.0.6" } } } diff --git a/package.json b/package.json index 4bf3955..8a3dc04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@foxify/odin", - "version": "0.7.5", + "version": "0.8.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.17", - "@types/node": "^10.12.15", + "@types/mongodb": "^3.1.18", + "@types/node": "^10.12.18", "async": "^2.6.1", "caller-id": "^0.1.0", "deasync": "^0.1.14", @@ -55,7 +55,11 @@ "prototyped.js": "^0.21.0", "verifications": "^0.3.0" }, + "peerDependencies": { + "@foxify/schema": "^1.0.1" + }, "devDependencies": { + "@foxify/schema": "^1.0.1", "@types/async": "^2.0.50", "@types/deasync": "^0.1.0", "@types/jest": "^23.3.10", @@ -63,10 +67,10 @@ "dotenv": "^6.2.0", "fs-readdir-recursive": "^1.1.0", "jest": "^23.6.0", - "mongodb-memory-server": "^2.8.0", + "mongodb-memory-server": "^2.9.1", "rimraf": "^2.6.2", "ts-jest": "^23.10.5", - "tslint": "^5.11.0", + "tslint": "^5.12.0", "tslint-config-airbnb": "^5.11.1", "typescript": "^3.2.2" }, diff --git a/src/Base.ts b/src/Base.ts index 57a77bd..d1a0111 100644 --- a/src/Base.ts +++ b/src/Base.ts @@ -2,11 +2,7 @@ import * as Odin from "."; import * as DB from "./DB"; import HasOne from "./Relation/HasOne"; import MorphOne from "./Relation/MorphOne"; -import * as Types from "./types"; -import TypeAny from "./types/Any"; -import TypeArray from "./types/Array"; -import TypeDate from "./types/Date"; -import TypeObjectId from "./types/ObjectId"; +import Types from "./types"; import { array, makeCollectionName, object } from "./utils"; const MODELS: { [name: string]: typeof Odin | undefined } = {}; @@ -115,26 +111,26 @@ class Base { const type = schema[key]; - if (type instanceof TypeAny) { + if (Types.isType(type)) { // Type - let schemaType: string = (type as any)._type.toLowerCase(); + let schemaType: string = (type as any).constructor.type.toLowerCase(); if ( - type instanceof TypeObjectId - || type instanceof TypeDate + (type.constructor as any).type === "ObjectId" + || (type.constructor as any).type === "Date" ) schemaType = "string"; properties[key] = { type: schemaType, }; - if (type instanceof TypeArray) { - let ofSchemaType: string = (type.ofType as any)._type.toLowerCase(); + if ((type.constructor as any).type === "Array") { + let ofSchemaType: string = (type as any)._of.constructor.type.toLowerCase(); if ( - type.ofType instanceof TypeObjectId - || type.ofType instanceof TypeDate + ofSchemaType === "objectid" + || ofSchemaType === "date" ) ofSchemaType = "string"; properties[key].items = { diff --git a/src/DB/Join.ts b/src/DB/Join.ts index fb84407..5e1216b 100644 --- a/src/DB/Join.ts +++ b/src/DB/Join.ts @@ -46,12 +46,12 @@ class Join extends Filter { protected _shouldPushExpr(value: any) { if (!string.isString(value)) return false; - return new RegExp(`^${this._ancestor}\..+`).test(value); + return new RegExp(`^\\$?${this._ancestor}\..+`).test(value); } protected _where(field: string, operator: string, value: any) { if (this._shouldPushExpr(value)) { - const keys = array.tail(value.split(".")); + const keys = array.tail(value.replace(/^\$/, "").split(".")); keys.push(prepareKey(keys.pop() as string)); diff --git a/src/GraphQL/Model.ts b/src/GraphQL/Model.ts index ef3b3be..fd67c36 100644 --- a/src/GraphQL/Model.ts +++ b/src/GraphQL/Model.ts @@ -3,7 +3,7 @@ import * as Odin from ".."; import Query from "../base/Query"; import QueryBuilder from "../base/QueryBuilder"; import * as DB from "../DB"; -import TypeAny from "../types/Any"; +import Types from "../types"; import * as utils from "../utils"; const _schema = (model: string, schema: Odin.Schema) => { @@ -13,10 +13,10 @@ const _schema = (model: string, schema: Odin.Schema) => { for (const key in schema) { const type = schema[key]; - if (type instanceof TypeAny) { + if (Types.isType(type)) { // Type - const gql = type.toGraphQL(model, key); + const gql = (type as any).toGraphQL(model, key); fields[key] = { type: gql.field as any }; args[key] = { type: gql.arg }; @@ -67,7 +67,7 @@ const _orderBy = (model: string, schema: Odin.Schema) => { const type = schema[key]; const _key = utils.array.compact([keyPrefix, key]).join("."); - if (type instanceof TypeAny) { + if (Types.isType(type)) { const ASC = orderByASC(_key); values[ASC] = { value: ASC }; @@ -126,10 +126,10 @@ class GraphQL extends QueryBuilder { public static toGraphQL(): any { const name = this.name; - const multiple = (this as any)._collection; + const multiple = this._collection; const single = utils.string.pluralize(multiple, 1); - const schema = _schema(name, (this as any)._schema); + const schema = _schema(name, this._schema); const args = schema.args; const type = new GraphQLBase.GraphQLObjectType({ name, @@ -142,9 +142,9 @@ class GraphQL extends QueryBuilder { }); const getDB = () => { - const db = (this as any).DB.connection((this as any).connection).collection((this as any)._collection); + const db = (this as any).DB.connection(this.connection).collection(this._collection); - if ((this as any).softDelete) return db.whereNull((this as any).DELETED_AT); + if (this.softDelete) return db.whereNull(this.DELETED_AT); return db; }; @@ -183,7 +183,7 @@ class GraphQL extends QueryBuilder { let db = getDB(); - if ((this as any).timestamps) db = db.orderBy("created_at", "desc"); + if (this.timestamps) db = db.orderBy("created_at", "desc"); const query: DB = utils.object.reduce( params, @@ -217,7 +217,7 @@ class GraphQL extends QueryBuilder { name: `${name}Input`, fields: utils.object.omit( args, - ["id", (this as any).CREATED_AT, (this as any).UPDATED_AT, (this as any).DELETED_AT] + ["id", this.CREATED_AT, this.UPDATED_AT, this.DELETED_AT] ) as any, }); @@ -231,7 +231,7 @@ class GraphQL extends QueryBuilder { }, resolve: async (root: any, params: any, options: any, fieldASTs: any) => { const result = await _encapsulate( - async () => await ((this as any) as typeof Odin).create(params.data) + async () => await this.create(params.data) ); return _prepare(result); @@ -245,7 +245,7 @@ class GraphQL extends QueryBuilder { }, }, resolve: async (root: any, params: any, options: any, fieldASTs: any) => await _encapsulate( - async () => await ((this as any) as typeof Odin).insert(params.data) + async () => await this.insert(params.data) ), }, [`update_${multiple}`]: { @@ -262,7 +262,7 @@ class GraphQL extends QueryBuilder { const query: Query = utils.object.reduce( params.query || {}, (query, value, key) => query.where(key, value), - (this as any) as Odin | Query + this ); return await _encapsulate(async () => await query.update(params.data)); @@ -279,7 +279,7 @@ class GraphQL extends QueryBuilder { const query: Query = utils.object.reduce( params.query || {}, (query, value, key) => query.where(key, value), - (this as any) as Odin | Query + this ); return await query.delete(); @@ -287,7 +287,7 @@ class GraphQL extends QueryBuilder { }, }; - if ((this as any).softDelete) + if (this.softDelete) mutations[`restore_${multiple}`] = { type: GraphQLBase.GraphQLInt, args: { @@ -299,7 +299,7 @@ class GraphQL extends QueryBuilder { const query: Query = utils.object.reduce( params.query, (query, value, key) => query.where(key, value), - ((this as any) as typeof Odin).withTrashed() + this.withTrashed() ); return await query.restore(); diff --git a/src/GraphQL/Type.ts b/src/GraphQL/Type.ts deleted file mode 100644 index 7acfc44..0000000 --- a/src/GraphQL/Type.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as Base from "graphql"; -import { GraphQLDateTime } from "graphql-iso-date"; - -class Type { - public toGraphQL(model: string, key: string) { - let field: Base.GraphQLType | undefined; - let arg: Base.GraphQLInputType | undefined; - - switch ((this as any)._type) { - case "Array": - const gql = (this as any).ofType.toGraphQL(model, key); - - field = new Base.GraphQLList(gql.field); - arg = new Base.GraphQLList(gql.arg); - - break; - case "Boolean": - field = Base.GraphQLBoolean; - arg = Base.GraphQLBoolean; - - break; - case "Number": - field = Base.GraphQLInt; - arg = Base.GraphQLInt; - - break; - case "Object": - field = new Base.GraphQLObjectType({ - name: `${model}_${key}`, - fields: {}, - }); - arg = new Base.GraphQLInputObjectType({ - name: `${model}_${key}_input`, - fields: {}, - }); - - break; - case "ObjectId": - field = Base.GraphQLID; - arg = Base.GraphQLID; - - break; - case "String": - field = Base.GraphQLString; - arg = Base.GraphQLString; - - break; - case "Date": - field = GraphQLDateTime; - arg = GraphQLDateTime; - - break; - } - - return { - field, - arg, - }; - } -} - -export default Type; diff --git a/src/Relation/Base.ts b/src/Relation/Base.ts index e6ff90d..e88d1bc 100644 --- a/src/Relation/Base.ts +++ b/src/Relation/Base.ts @@ -44,11 +44,17 @@ abstract class Relation { } public abstract load( - query: DB | Join, relations: Relation.Relation[], filter?: (q: Filter) => Filter + query: DB | Join, + relations: Relation.Relation[], + withTrashed?: boolean, + filter?: (q: Filter) => Filter ): DB | Join; public abstract loadCount( - query: DB | Join, relations: string[], filter?: (q: Filter) => Filter + query: DB | Join, + relations: string[], + withTrashed?: boolean, + filter?: (q: Filter) => Filter ): DB | Join; /****************************** With Relations ******************************/ diff --git a/src/Relation/EmbedMany.ts b/src/Relation/EmbedMany.ts index 4ec29ae..d23d148 100644 --- a/src/Relation/EmbedMany.ts +++ b/src/Relation/EmbedMany.ts @@ -18,9 +18,15 @@ class EmbedMany extends HasMany { super(model, relation, localKey, foreignKey, filter, caller); } - public load(query: DB | Join, relations: Relation.Relation[], filter?: (q: Filter) => Filter) { + public load( + query: DB | Join, relations: Relation.Relation[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const relation = this.relation; - const filters = [this.filter]; + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (filter) filters.push(filter); @@ -33,9 +39,9 @@ class EmbedMany extends HasMany { if (!(relation as any)._relations.includes(subRelation)) throw new Error(`Relation '${subRelation}' does not exist on '${relation.name}' Model`); - const loader = relation.prototype[subRelation](); + const loader: Relation = relation.prototype[subRelation](); - return loader.load(prev, cur.relations); + return loader.load(prev, cur.relations, withTrashed) as any; }, filters.reduce( (prev, filter) => filter(prev) as any, @@ -46,9 +52,16 @@ class EmbedMany extends HasMany { ); } - public loadCount(query: DB | Join, relations: string[], filter?: (q: Filter) => Filter) { + public loadCount( + query: DB | Join, relations: string[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const relation = this.relation; const subRelation = relations.shift(); + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (subRelation) { if (!(relation as any)._relations.includes(subRelation)) @@ -57,25 +70,25 @@ class EmbedMany extends HasMany { return query .join( relation.toString(), - q => relation.prototype[subRelation]().loadCount( - this.filter( - q - .whereIn(this.foreignKey, `${this.model.constructor.toString()}.relation.${this.localKey}`) - .aggregate({ - $project: { - relation: "$$ROOT", - }, - }) - ) as any, - relations, - filter - ), + q => relation.prototype[subRelation]() + .loadCount( + filters.reduce( + (prev, filter) => filter(prev) as any, + q + .whereIn(this.foreignKey, `${this.model.constructor.toString()}.relation.${this.localKey}`) + .aggregate({ + $project: { + relation: "$$ROOT", + }, + }) + ), + relations, + filter + ), "relation" ); } - const filters = [this.filter]; - if (filter) filters.push(filter); return query diff --git a/src/Relation/HasMany.ts b/src/Relation/HasMany.ts index dce2223..326d3ac 100644 --- a/src/Relation/HasMany.ts +++ b/src/Relation/HasMany.ts @@ -17,9 +17,15 @@ class HasMany extends Relation { super(model, relation, localKey, foreignKey, filter, caller); } - public load(query: DB | Join, relations: Relation.Relation[], filter?: (q: Filter) => Filter) { + public load( + query: DB | Join, relations: Relation.Relation[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const relation = this.relation; - const filters = [this.filter]; + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (filter) filters.push(filter); @@ -32,9 +38,9 @@ class HasMany extends Relation { if (!(relation as any)._relations.includes(subRelation)) throw new Error(`Relation '${subRelation}' does not exist on '${relation.name}' Model`); - const loader = relation.prototype[subRelation](); + const loader: Relation = relation.prototype[subRelation](); - return loader.load(prev, cur.relations); + return loader.load(prev, cur.relations, withTrashed) as any; }, filters.reduce( (prev, filter) => filter(prev) as any, @@ -45,9 +51,16 @@ class HasMany extends Relation { ); } - public loadCount(query: DB | Join, relations: string[], filter?: (q: Filter) => Filter) { + public loadCount( + query: DB | Join, relations: string[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const relation = this.relation; const subRelation = relations.shift(); + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (subRelation) { if (!(relation as any)._relations.includes(subRelation)) @@ -58,7 +71,8 @@ class HasMany extends Relation { relation.toString(), q => relation.prototype[subRelation]() .loadCount( - this.filter( + filters.reduce( + (prev, filter) => filter(prev) as any, q .where(this.foreignKey, `${this.model.constructor.toString()}.relation.${this.localKey}`) .aggregate({ @@ -66,16 +80,15 @@ class HasMany extends Relation { relation: "$$ROOT", }, }) - ) as any, + ), relations, + withTrashed, filter ), "relation" ); } - const filters = [this.filter]; - if (filter) filters.push(filter); return query diff --git a/src/Relation/HasOne.ts b/src/Relation/HasOne.ts index f59a78c..a57f8e6 100644 --- a/src/Relation/HasOne.ts +++ b/src/Relation/HasOne.ts @@ -17,10 +17,16 @@ class HasOne extends Relation { super(model, relation, localKey, foreignKey, filter, caller); } - public load(query: DB | Join, relations: Relation.Relation[], filter?: (q: Filter) => Filter) { + public load( + query: DB | Join, relations: Relation.Relation[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const relation = this.relation; const name = this.as; - const filters = [this.filter]; + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (filter) filters.push(filter); @@ -33,9 +39,9 @@ class HasOne extends Relation { if (!(relation as any)._relations.includes(subRelation)) throw new Error(`Relation '${subRelation}' does not exist on '${relation.name}' Model`); - const loader = relation.prototype[subRelation](); + const loader: Relation = relation.prototype[subRelation](); - return loader.load(prev, cur.relations); + return loader.load(prev, cur.relations, withTrashed) as any; }, filters.reduce( (prev, filter) => filter(prev) as any, @@ -49,9 +55,16 @@ class HasOne extends Relation { }); } - public loadCount(query: DB | Join, relations: string[], filter?: (q: Filter) => Filter) { + public loadCount( + query: DB | Join, relations: string[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const relation = this.relation; const subRelation = relations.shift(); + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (subRelation) { if (!(relation as any)._relations.includes(subRelation)) @@ -60,26 +73,26 @@ class HasOne extends Relation { return query .join( relation.toString(), - q => relation.prototype[subRelation]().loadCount( - this.filter( - q - .where(this.foreignKey, `${this.model.constructor.toString()}.relation.${this.localKey}`) - .limit(1) - .aggregate({ - $project: { - relation: "$$ROOT", - }, - }) - ) as any, - relations, - filter - ), + q => relation.prototype[subRelation]() + .loadCount( + filters.reduce( + (prev, filter) => filter(prev) as any, + q + .where(this.foreignKey, `${this.model.constructor.toString()}.relation.${this.localKey}`) + .limit(1) + .aggregate({ + $project: { + relation: "$$ROOT", + }, + }) + ), + relations, + filter + ), "relation" ); } - const filters = [this.filter]; - if (filter) filters.push(filter); return query diff --git a/src/Relation/MorphMany.ts b/src/Relation/MorphMany.ts index ef7034c..4cd0ed8 100644 --- a/src/Relation/MorphMany.ts +++ b/src/Relation/MorphMany.ts @@ -6,11 +6,17 @@ import Relation from "./Base"; import MorphBase from "./MorphBase"; class MorphMany extends MorphBase { - public load(query: DB | Join, relations: Relation.Relation[], filter?: (q: Filter) => Filter) { + public load( + query: DB | Join, relations: Relation.Relation[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const constructor = this.model.constructor; const relation = this.relation; const name = this.as; - const filters = [this.filter]; + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (filter) filters.push(filter); @@ -23,9 +29,9 @@ class MorphMany extends MorphBase { if (!(relation as any)._relations.includes(subRelation)) throw new Error(`Relation '${subRelation}' does not exist on '${relation.name}' Model`); - const loader = relation.prototype[subRelation](); + const loader: Relation = relation.prototype[subRelation](); - return loader.load(prev, cur.relations); + return loader.load(prev, cur.relations, withTrashed) as any; }, filters.reduce( (prev, filter) => filter(prev) as any, @@ -37,10 +43,17 @@ class MorphMany extends MorphBase { ); } - public loadCount(query: DB | Join, relations: string[], filter?: (q: Filter) => Filter) { + public loadCount( + query: DB | Join, relations: string[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const constructor = this.model.constructor; const relation = this.relation; const subRelation = relations.shift(); + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (subRelation) { if (!(relation as any)._relations.includes(subRelation)) @@ -49,26 +62,26 @@ class MorphMany extends MorphBase { return query .join( relation.toString(), - q => relation.prototype[subRelation]().loadCount( - this.filter( - q - .where(this.foreignKey, `${constructor.toString()}.relation.${this.localKey}`) - .where(`${this.type}_type`, constructor.name) - .aggregate({ - $project: { - relation: "$$ROOT", - }, - }) - ) as any, - relations, - filter - ), + q => relation.prototype[subRelation]() + .loadCount( + filters.reduce( + (prev, filter) => filter(prev) as any, + q + .where(this.foreignKey, `${constructor.toString()}.relation.${this.localKey}`) + .where(`${this.type}_type`, constructor.name) + .aggregate({ + $project: { + relation: "$$ROOT", + }, + }) + ), + relations, + filter + ), "relation" ); } - const filters = [this.filter]; - if (filter) filters.push(filter); return query diff --git a/src/Relation/MorphOne.ts b/src/Relation/MorphOne.ts index 4f17b2e..8f8825a 100644 --- a/src/Relation/MorphOne.ts +++ b/src/Relation/MorphOne.ts @@ -6,11 +6,17 @@ import Relation from "./Base"; import MorphBase from "./MorphBase"; class MorphOne extends MorphBase { - public load(query: DB | Join, relations: Relation.Relation[], filter?: (q: Filter) => Filter) { + public load( + query: DB | Join, relations: Relation.Relation[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const constructor = this.model.constructor; const relation = this.relation; const name = this.as; - const filters = [this.filter]; + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (filter) filters.push(filter); @@ -23,9 +29,9 @@ class MorphOne extends MorphBase { if (!(relation as any)._relations.includes(subRelation)) throw new Error(`Relation '${subRelation}' does not exist on '${relation.name}' Model`); - const loader = relation.prototype[subRelation](); + const loader: Relation = relation.prototype[subRelation](); - return loader.load(prev, cur.relations); + return loader.load(prev, cur.relations, withTrashed) as any; }, filters.reduce( (prev, filter) => filter(prev) as any, @@ -40,10 +46,17 @@ class MorphOne extends MorphBase { }); } - public loadCount(query: DB | Join, relations: string[], filter?: (q: Filter) => Filter) { + public loadCount( + query: DB | Join, relations: string[], withTrashed?: boolean, filter?: (q: Filter) => Filter + ) { const constructor = this.model.constructor; const relation = this.relation; const subRelation = relations.shift(); + const filters: Array<(q: Filter) => Filter> = []; + + if (relation.softDelete && !withTrashed) filters.push(q => q.whereNull(relation.DELETED_AT)); + + filters.push(this.filter); if (subRelation) { if (!(relation as any)._relations.includes(subRelation)) @@ -52,27 +65,27 @@ class MorphOne extends MorphBase { return query .join( relation.toString(), - q => relation.prototype[subRelation]().loadCount( - this.filter( - q - .where(this.foreignKey, `${constructor.toString()}.relation.${this.localKey}`) - .where(`${this.type}_type`, constructor.name) - .limit(1) - .aggregate({ - $project: { - relation: "$$ROOT", - }, - }) - ) as any, - relations, - filter - ), + q => relation.prototype[subRelation]() + .loadCount( + filters.reduce( + (prev, filter) => filter(prev) as any, + q + .where(this.foreignKey, `${constructor.toString()}.relation.${this.localKey}`) + .where(`${this.type}_type`, constructor.name) + .limit(1) + .aggregate({ + $project: { + relation: "$$ROOT", + }, + }) + ) as any, + relations, + filter + ), "relation" ); } - const filters = [this.filter]; - if (filter) filters.push(filter); return query diff --git a/src/base/Query.ts b/src/base/Query.ts index d2fc9b0..567b500 100644 --- a/src/base/Query.ts +++ b/src/base/Query.ts @@ -42,7 +42,7 @@ class Query extends DB { this.whereNull(this._model.DELETED_AT); if (withRelations) this._relations - .forEach(({ relation, relations }) => relation.load(this as any, relations) as any); + .forEach(({ relation, relations }) => relation.load(this as any, relations, this._withTrashed)); return this; } @@ -99,7 +99,7 @@ class Query extends DB { }); // join relation - this._model.prototype[currentRelation]().loadCount(this, relations, filter); + this._model.prototype[currentRelation]().loadCount(this, relations, this._withTrashed, filter); if (relationsCount > 1) { const projector = (length: number): any => { diff --git a/src/index.ts b/src/index.ts index 226dbb4..826d2cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ +import * as Schema from "@foxify/schema"; import Relational from "./base/Relational"; import Collection from "./Collection"; import Connect from "./Connect"; import * as DB from "./DB"; import events from "./events"; import GraphQL from "./GraphQL"; -import * as Types from "./types"; -import TypeAny from "./types/Any"; +import Types from "./types"; import { define, getGetterName, getSetterName, object, string } from "./utils"; const EVENTS: Odin.Event[] = ["create"]; @@ -52,45 +52,8 @@ class Odin extends Relational { return this._collection; } - public static validate(document: T, updating: boolean = false) { - const validator = (schema: Odin.Schema, doc: T) => { - const value: { [key: string]: any } = {}; - let errors: { [key: string]: any } | null = {}; - - for (const key in schema) { - const type = schema[key]; - let item = (doc as { [key: string]: any })[key]; - - if (type instanceof TypeAny) { - // Type - const validation = type.validate(item, updating); - - if (validation.value) value[key] = validation.value; - - if (validation.errors) errors[key] = validation.errors; - } else { - // Object - if (!item) item = {}; - - const validation = validator(type, item); - - if (validation.errors) - for (const errorKey in validation.errors) - errors[`${key}.${errorKey}`] = validation.errors[errorKey]; - - if (object.size(validation.value) > 0) value[key] = validation.value; - } - } - - if (object.size(errors) === 0) errors = null; - - return { - errors, - value, - }; - }; - - const validation = validator(this._schema, document); + public static validate(document: T, updating: boolean = false) { + const validation = Schema.validate(this._schema, document); if (validation.errors && updating) { object.forEach(validation.errors, (errors, key) => { @@ -110,9 +73,9 @@ class Odin extends Relational { throw error; } - const value = validation.value; + const value: any = validation.value; - if (updating && this.timestamps) value[this.UPDATED_AT] = new Date(); + if (updating && this.timestamps) (value)[this.UPDATED_AT] = new Date(); return value; } diff --git a/src/types/Any.ts b/src/types/Any.ts deleted file mode 100644 index 8759fbd..0000000 --- a/src/types/Any.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as async from "async"; -import GraphQL from "../GraphQL/Type"; -import * as utils from "../utils"; - -interface TypeAny extends GraphQL { } - -class TypeAny extends GraphQL { - protected _type = "Any"; - - protected _casts: Array<(v: any) => any> = []; - protected _tests: Array<(v: any) => string | null> = []; - - protected _required: boolean = false; - - protected _default: () => any = () => undefined; - - protected _base(v: any): string | null { - if (!utils.function.isFunction(v)) return null; - - return "Invalid type"; - } - - protected _cast(cast: (v: any) => any) { - this._casts.push(cast); - - return this; - } - - protected _test(test: (v: any) => string | null) { - this._tests.push(test); - - return this; - } - - public get required() { - this._required = true; - - return this; - } - - public default(v: any) { - if (utils.function.isFunction(v)) { - this._default = v; - - return this; - } - - if (this._base(v)) - throw new TypeError(`The given value must be of "${this.constructor.name}" type`); - - this._default = () => v; - - return this; - } - - // TODO whitelist - // allow(...vs: any[]) { - // } - - public validate(value?: any, updating: boolean = false): { value: any, errors: string[] | null } { - if (value === undefined || value === null) { - if (!updating) value = this._default(); - - if (value === undefined || value === null) { - if (this._required) return { value, errors: ["Must be provided"] }; - - return { value, errors: null }; - } - } - - const baseError = this._base(value); - if (baseError) return { value, errors: [baseError] }; - - this._casts.forEach(_cast => value = _cast(value)); - - let errors: string[] = []; - - async.map( - this._tests, - (test, cb1: (...args: any[]) => any) => - cb1(undefined, (cb2: (...args: any[]) => any) => cb2(undefined, test(value))), - (err, tests) => { - if (err) throw err; - - if (tests) - async.parallel( - tests, - (err, result) => { - if (err) throw err; - - if (result) errors = utils.array.compact(result as string[]); - } - ); - } - ); - - if (errors.length === 0) return { value, errors: null }; - - return { - errors, - value, - }; - } -} - -export default TypeAny; diff --git a/src/types/Array.ts b/src/types/Array.ts deleted file mode 100644 index 6a8d46c..0000000 --- a/src/types/Array.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { array, number } from "../utils"; -import TypeAny from "./Any"; - -interface TypeArray { - ofType: TypeAny; -} - -class TypeArray extends TypeAny { - protected _type = "Array"; - - protected _base(v: any): string | null { - if (Array.isArray(v)) return null; - - return "Must be an array"; - } - - public of(type: TypeAny) { - if (!(type instanceof TypeAny)) - throw new TypeError(`Expected 'type' to be a 'TypeAny' instance, got '${ - typeof type - }' insted`); - - this.ofType = type; - - return this._test( - (v: any[]) => array.first( - array.deepFlatten( - array.compact( - v.map(item => type.validate(item).errors) - ) - ) - ) - ) - ._cast((v: any[]) => v.map(item => type.validate(item).value)); - } - - public min(n: number) { - if (!number.isNumber(n)) throw new TypeError("'n' must be a number"); - - if (n < 0) throw new TypeError("'n' must be a positive number"); - - return this._test((v: any[]) => v.length < n ? `Must be at least ${n} items` : null); - } - - public max(n: number) { - if (!number.isNumber(n)) throw new TypeError("'n' must be a number"); - - if (n < 0) throw new TypeError("'n' must be a positive number"); - - return this._test((v: any[]) => v.length > n ? `Must be at most ${n} items` : null); - } - - public length(n: number) { - if (!number.isNumber(n)) throw new TypeError("'n' must be a number"); - - if (n < 0) throw new TypeError("'n' must be a positive number"); - - return this._test((v: any[]) => v.length !== n ? `Must be exactly ${n} items` : null); - } -} - -export default TypeArray; diff --git a/src/types/Boolean.ts b/src/types/Boolean.ts deleted file mode 100644 index d145266..0000000 --- a/src/types/Boolean.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as utils from "../utils"; -import TypeAny from "./Any"; - -class TypeBoolean extends TypeAny { - protected _type = "Boolean"; - - protected _base(v: any) { - if (utils.boolean.isBoolean(v)) return null; - - return "Must be a boolean"; - } -} - -export default TypeBoolean; diff --git a/src/types/Date.ts b/src/types/Date.ts deleted file mode 100644 index 9edd127..0000000 --- a/src/types/Date.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as utils from "../utils"; -import TypeAny from "./Any"; - -class TypeDate extends TypeAny { - protected _type = "Date"; - - protected _base(v: any) { - if (utils.string.isString(v) || utils.number.isNumber(v)) v = new Date(v); - - if (utils.date.isDate(v) && v.toString() !== "Invalid Date") return null; - - return "Must be a valid date"; - } - - public min(date: Date | number | string | (() => (Date | number | string))) { - if (utils.date.isDate(date)) date = () => date as Date; - - return this._test((v: Date) => v < (date as (() => Date))() - ? `Must be at least ${date}` - : null); - } - - public max(date: Date | number | string | (() => (Date | number | string))) { - if (utils.date.isDate(date)) date = () => date as Date; - - return this._test((v: Date) => v > (date as (() => Date))() - ? `Must be at most ${date}` - : null); - } -} - -export default TypeDate; diff --git a/src/types/Id.ts b/src/types/Id.ts new file mode 100644 index 0000000..c0abe0e --- /dev/null +++ b/src/types/Id.ts @@ -0,0 +1,14 @@ +import AnyType from "@foxify/schema/dist/Any"; +import { ObjectId } from "mongodb"; + +class IdType extends AnyType { + protected static type = "ObjectId"; + + protected _base(v: any) { + if (ObjectId.isValid(v)) return null; + + return "Must be a valid object id"; + } +} + +export default IdType; diff --git a/src/types/Number.ts b/src/types/Number.ts deleted file mode 100644 index 554ef30..0000000 --- a/src/types/Number.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { number } from "../utils"; -import TypeAny from "./Any"; - -class TypeNumber extends TypeAny { - protected _type = "Number"; - - protected _base(v: any) { - if (number.isNumber(v)) return null; - - return "Must be a number"; - } - - get integer() { - return this._test((v: number) => !Number.isInteger(v) ? `Must be an integer` : null); - } - - get positive() { - return this._test((v: number) => v < 0 ? `Must be a positive number` : null); - } - - get negative() { - return this._test((v: number) => v > 0 ? `Must be a negative number` : null); - } - - public min(n: number) { - if (!number.isNumber(n)) throw new TypeError("'n' must be a number"); - - return this._test((v: number) => v < n ? `Must be at least ${n}` : null); - } - - public max(n: number) { - if (!number.isNumber(n)) throw new TypeError("'n' must be a number"); - - return this._test((v: number) => v > n ? `Must be at most ${n}` : null); - } - - public precision(n: number) { - if (!number.isNumber(n)) throw new TypeError("'n' must be a number"); - - return this._test((v: number) => `${v}`.split(".")[1].length < n ? - `Must be have at most ${n} decimal places` : null); - } - - public multiple(n: number) { - if (!number.isNumber(n)) throw new TypeError("'n' must be a number"); - - if (n < 0) throw new TypeError("'n' must be a positive number"); - - return this._test((v: number) => v % n !== 0 ? `Must be a multiple of ${n}` : null); - } -} - -export default TypeNumber; diff --git a/src/types/Object.ts b/src/types/Object.ts deleted file mode 100644 index 3e14def..0000000 --- a/src/types/Object.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as utils from "../utils"; -import TypeAny from "./Any"; - -class TypeObject extends TypeAny { - protected _type = "Object"; - - protected _base(v: any) { - if (utils.object.isObject(v)) return null; - - return "Must be a object"; - } - - /********** TESTS **********/ -} - -export default TypeObject; diff --git a/src/types/ObjectId.ts b/src/types/ObjectId.ts deleted file mode 100644 index 78eef51..0000000 --- a/src/types/ObjectId.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ObjectId } from "mongodb"; -import TypeAny from "./Any"; - -class TypeObjectId extends TypeAny { - protected _type = "ObjectId"; - - protected _base(v: any) { - if (ObjectId.isValid(v)) return null; - - return "Must be an object id"; - } -} - -export default TypeObjectId; diff --git a/src/types/String.ts b/src/types/String.ts deleted file mode 100644 index 8c9398a..0000000 --- a/src/types/String.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as Verifications from "verifications"; -import * as utils from "../utils"; -import TypeAny from "./Any"; - -// tslint:disable-next-line:max-line-length -const ipv4Regex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; -// tslint:disable-next-line:max-line-length -const ipv6Regex = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/; - -class TypeString extends TypeAny { - protected _type = "String"; - - protected _base(v: any) { - if (utils.string.isString(v)) return null; - - return "Must be a string"; - } - - /********** TESTS **********/ - - get token() { - return this._test((v: string) => !/^[a-zA-Z0-9_]*$/.test(v) ? - `Must only contain a-z, A-Z, 0-9, and underscore _` : null); - } - - get alphanum() { - return this._test((v: string) => !/^[a-zA-Z0-9]*$/.test(v) - ? `Must only contain a-z, A-Z, 0-9` : null); - } - - get numeral() { - return this._test((v: string) => !/^[0-9]*$/.test(v) - ? `Must only contain numbers` : null); - } - - get ip() { - return this._test((v: string) => !(ipv4Regex.test(v) || ipv6Regex.test(v)) - ? `Must be an ipv4 or ipv6` : null); - } - - get ipv4() { - return this._test((v: string) => !ipv4Regex.test(v) ? `Must be an ipv4` : null); - } - - get ipv6() { - return this._test((v: string) => !ipv6Regex.test(v) ? `Must be an ipv6` : null); - } - - get email() { - return this._test((v: string) => !/^\w[\w\.]+@\w+?\.[a-zA-Z]{2,3}$/.test(v) - ? "Must be an email address" : null); - } - - get creditCard() { - return this._test((v: string) => !Verifications.CreditCard.verify(v) - ? "Must be a credit-card" : null); - } - - public min(n: number) { - if (!utils.number.isNumber(n)) throw new TypeError("'n' must be a number"); - - if (n < 0) throw new TypeError("'n' must be a positive number"); - - return this._test((v: string) => v.length < n ? `Must be at least ${n} characters` : null); - } - - public max(n: number) { - if (!utils.number.isNumber(n)) throw new TypeError("'n' must be a number"); - - if (n < 0) throw new TypeError("'n' must be a positive number"); - - return this._test((v: string) => v.length > n ? `Must be at most ${n} characters` : null); - } - - public length(n: number) { - if (!utils.number.isNumber(n)) throw new TypeError("'n' must be a number"); - - if (n < 0) throw new TypeError("'n' must be a positive number"); - - return this._test((v: string) => v.length !== n ? `Must be exactly ${n} characters` : null); - } - - public regex(r: RegExp) { - if (!(r instanceof RegExp)) throw new TypeError("'r' must be a regex"); - - return this._test((v: string) => !r.test(v) ? `Must match ${r}` : null); - } - - public enum(enums: string[]) { - enums.forEach((str) => { - if (!utils.string.isString(str)) throw new TypeError("'enums' must be an string array"); - }); - - const TYPE = JSON.stringify(enums); - - return this._test( - (v: string) => !utils.array.contains(enums, v) ? `Must be one of ${TYPE}` : null - ); - } - - /********** CASTS **********/ - - public truncate(length: number) { - return this._cast((v: string) => utils.string.truncate(v, length)); - } - - public replace(pattern: string | RegExp, replacement: string) { - return this._cast((v: string) => v.replace(pattern, replacement)); - } -} - -export default TypeString; diff --git a/src/types/index.ts b/src/types/index.ts index 39e244e..583f056 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,41 +1,130 @@ -import TypeArray from "./Array"; -import TypeBoolean from "./Boolean"; -import TypeDate from "./Date"; -import TypeNumber from "./Number"; -import TypeObject from "./Object"; -import TypeObjectId from "./ObjectId"; -import TypeString from "./String"; - -declare module Type { } - -class Type { - public static get array() { - return new TypeArray(); - } +import * as Schema from "@foxify/schema"; +import AnyType from "@foxify/schema/dist/Any"; +import ArrayType from "@foxify/schema/dist/Array"; +import ObjectType from "@foxify/schema/dist/Object"; +import * as Base from "graphql"; +import { GraphQLDateTime } from "graphql-iso-date"; +import { object, string } from "../utils"; +import IdType from "./Id"; - public static get boolean() { - return new TypeBoolean(); - } +const { forEach } = object; +const { isEmpty } = string; - public static get date() { - return new TypeDate(); - } +/******************** Array ********************/ - public static get number() { - return new TypeNumber(); - } +const ArrayTypeOf = ArrayType.prototype.of; +ArrayType.prototype.of = function of(type) { + (this as any)._of = type; - public static get object() { - return new TypeObject(); - } + return ArrayTypeOf.call(this, type); +}; - public static get id() { - return new TypeObjectId(); - } +/******************** Object ********************/ + +const ObjectTypeKeys = ObjectType.prototype.keys; +ObjectType.prototype.keys = function keys(obj) { + (this as any)._keys = obj; + + return ObjectTypeKeys.call(this, obj); +}; + +/******************** GraphQL ********************/ + +(AnyType.prototype as any).toGraphQL = function toGraphQL(model: string, key: string = "") { + let field: Base.GraphQLType | undefined; + let arg: Base.GraphQLInputType | undefined; + + switch (this.constructor.type) { + case "Array": + const gql = this._of.toGraphQL(model, key); + + field = new Base.GraphQLList(gql.field); + arg = new Base.GraphQLList(gql.arg); + + break; + case "Boolean": + field = Base.GraphQLBoolean; + arg = Base.GraphQLBoolean; + + break; + case "Number": + field = Base.GraphQLInt; + arg = Base.GraphQLInt; + + break; + case "Object": + const fields: any = {}; + const args: any = {}; - public static get string() { - return new TypeString(); + forEach(this._keys, (value, subKey) => { + const gql = value.toGraphQL(model, `${key}_${subKey}`); + + fields[subKey] = gql.field; + args[subKey] = gql.arg; + }); + + field = new Base.GraphQLObjectType({ + fields, + name: isEmpty(key) ? model : `${model}_${key}`, + }); + arg = new Base.GraphQLInputObjectType({ + name: isEmpty(key) ? `${model}_input` : `${model}_${key}_input`, + fields: args, + }); + + break; + case "ObjectId": + field = Base.GraphQLID; + arg = Base.GraphQLID; + + break; + case "String": + field = Base.GraphQLString; + arg = Base.GraphQLString; + + break; + case "Date": + field = GraphQLDateTime; + arg = GraphQLDateTime; + + break; } -} -export = Type; + return { + field, + arg, + }; +}; + +export default { + get array() { + return Schema.array; + }, + + get boolean() { + return Schema.boolean; + }, + + get date() { + return Schema.date; + }, + + get id() { + return new IdType(); + }, + + get number() { + return Schema.number; + }, + + get object() { + return Schema.object; + }, + + get string() { + return Schema.string; + }, + + // Helpers + isType: (arg: any): arg is AnyType => arg instanceof AnyType, +}; diff --git a/test/relations/EmbedMany.ts b/test/relations/EmbedMany.ts new file mode 100644 index 0000000..02bc7bd --- /dev/null +++ b/test/relations/EmbedMany.ts @@ -0,0 +1,240 @@ +import * as Odin from "../../src"; +import { array, object } from "../../src/utils"; + +declare global { + namespace NodeJS { + interface Global { + __MONGO_DB_NAME__: string; + __MONGO_CONNECTION__: any; + } + } +} + +const Types = Odin.Types; + +const USERS = [ + { + username: "ardalanamini", + email: "ardalanamini22@gmail.com", + name: { + first: "Ardalan", + last: "Amini", + }, + chat_names: ["chat 1", "chat 2"], + }, + { + username: "john", + email: "johndue@example.com", + name: { + first: "John", + last: "Due", + }, + chat_names: ["chat 3"], + }, +]; + +const CHATS = [ + { + name: "chat 1", + message_chats: ["1"], + }, + { + name: "chat 2", + message_chats: ["2"], + }, + { + name: "chat 3", + message_chats: [], + }, +]; + +const MESSAGES = [ + { + chat_name: "1", + message: "1: Hello World", + }, + { + chat_name: "1", + message: "2: Hello World", + }, + { + chat_name: "2", + message: "3: Hello World", + }, + { + chat_name: "2", + message: "4: Hello World", + }, + { + chat_name: "2", + message: "5: Hello World", + }, +]; + +Odin.Connect({ + default: { + database: global.__MONGO_DB_NAME__, + connection: global.__MONGO_CONNECTION__, + }, +}); + +@Odin.register +class User extends Odin { + public static schema = { + username: Types.string.alphanum.min(3).required, + email: Types.string.email.required, + name: { + first: Types.string.min(3).required, + last: Types.string.min(3), + }, + chat_names: Types.array.of(Types.string).default([]), + }; + + @Odin.relation + public chats() { + return this.embedMany("Chat", "chat_names", "name"); + } +} + +// tslint:disable-next-line:max-classes-per-file +@Odin.register +class Chat extends Odin { + public static schema = { + name: Types.string.required, + message_chats: Types.array.of(Types.string).default([]), + }; + + @Odin.relation + public user() { + return this.hasOne("User", "name", "chat_names"); + } + + @Odin.relation + public messages() { + return this.embedMany("Message", "message_chats", "chat_name"); + } +} + +// tslint:disable-next-line:max-classes-per-file +@Odin.register +class Message extends Odin { + public static schema = { + chat_name: Types.string.numeral.required, + message: Types.string.required, + }; + + @Odin.relation + public chat() { + return this.hasOne("Chat", "chat_name", "message_chats"); + } +} + +const refresh = async (done: jest.DoneCallback) => { + await User.delete(); + await User.insert(USERS); + const users: any[] = await User.lean().get(); + USERS.length = 0; + USERS.push(...users); + + await Chat.delete(); + await Chat.insert(CHATS); + const chats: any[] = await Chat.lean().get(); + CHATS.length = 0; + CHATS.push(...chats); + + await Message.delete(); + await Message.insert(MESSAGES); + const messages: any[] = await Message.lean().get(); + MESSAGES.length = 0; + MESSAGES.push(...messages); + + done(); +}; + +beforeAll(refresh); + +afterEach(refresh); + +afterAll(async (done) => { + await User.delete(); + await Chat.delete(); + await Message.delete(); + + done(); +}); + +test("Model.with", async () => { + expect.assertions(2); + + const items = USERS.map(user => ({ + ...user, + chats: CHATS.filter(chat => user.chat_names.includes(chat.name)), + })); + + const results = await User.with("chats").lean().get(); + + expect(results).toEqual(items); + + const results2 = await User.with("chats").get(); + + expect(results2.map((item: any) => item.toJSON())).toEqual(items); +}); + +test("Model.with (deep)", async () => { + expect.assertions(4); + + const items = USERS.map((user) => { + const chats = CHATS.filter(chat => user.chat_names.includes(chat.name)).map(chat => ({ + ...chat, + messages: MESSAGES.filter(message => chat.message_chats.includes(message.chat_name)), + })); + + return { + ...user, + chats, + }; + }); + + const results = await User.with("chats", "chats.messages").lean().get(); + + expect(results).toEqual(items); + + const results2 = await User.with("chats.messages").lean().get(); + + expect(results2).toEqual(items); + + const results3 = await User.with("chats", "chats.messages").get(); + + expect(results3.map((item: any) => item.toJSON())).toEqual(items); + + const results4 = await User.with("chats.messages").get(); + + 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 => chat.message_chats.includes(message.chat_name))); + + const results = await Chat.has("messages").lean().get(); + + expect(results).toEqual(items); +}); + +test("Model.has [deep]", async () => { + expect.assertions(1); + + const items = USERS + .filter(user => + array.any(MESSAGES, message => + CHATS + .filter(chat => user.chat_names.includes(chat.name)) + .findIndex(chat => chat.message_chats.includes(message.chat_name)) !== -1 + ) + ); + + const results = await User.has("chats.messages").lean().get(); + + expect(results).toEqual(items); +}); diff --git a/test/relations/HasMany.ts b/test/relations/HasMany.ts index 74829c1..9114cb2 100644 --- a/test/relations/HasMany.ts +++ b/test/relations/HasMany.ts @@ -160,64 +160,64 @@ afterAll(async (done) => { done(); }); -// test("Model.with", async () => { -// expect.assertions(2); +test("Model.with", async () => { + expect.assertions(2); -// const items = USERS.map(user => ({ -// ...user, -// chats: CHATS.filter(chat => chat.username === user.username), -// })); + const items = USERS.map(user => ({ + ...user, + chats: CHATS.filter(chat => chat.username === user.username), + })); -// const results = await User.with("chats").lean().get(); + const results = await User.with("chats").lean().get(); -// expect(results).toEqual(items); + expect(results).toEqual(items); -// const results2 = await User.with("chats").get(); + const results2 = await User.with("chats").get(); -// expect(results2.map((item: any) => item.toJSON())).toEqual(items); -// }); + expect(results2.map((item: any) => item.toJSON())).toEqual(items); +}); -// test("Model.with (deep)", async () => { -// expect.assertions(4); +test("Model.with (deep)", async () => { + expect.assertions(4); -// const items = USERS.map((user) => { -// const chats = CHATS.filter(chat => chat.username === user.username).map(chat => ({ -// ...chat, -// messages: MESSAGES.filter(message => message.chatname === chat.name), -// })); + const items = USERS.map((user) => { + const chats = CHATS.filter(chat => chat.username === user.username).map(chat => ({ + ...chat, + messages: MESSAGES.filter(message => message.chatname === chat.name), + })); -// return { -// ...user, -// chats, -// }; -// }); + return { + ...user, + chats, + }; + }); -// const results = await User.with("chats", "chats.messages").lean().get(); + const results = await User.with("chats", "chats.messages").lean().get(); -// expect(results).toEqual(items); + expect(results).toEqual(items); -// const results2 = await User.with("chats.messages").lean().get(); + const results2 = await User.with("chats.messages").lean().get(); -// expect(results2).toEqual(items); + expect(results2).toEqual(items); -// const results3 = await User.with("chats", "chats.messages").get(); + const results3 = await User.with("chats", "chats.messages").get(); -// expect(results3.map((item: any) => item.toJSON())).toEqual(items); + expect(results3.map((item: any) => item.toJSON())).toEqual(items); -// const results4 = await User.with("chats.messages").get(); + const results4 = await User.with("chats.messages").get(); -// expect(results4.map((item: any) => item.toJSON())).toEqual(items); -// }); + expect(results4.map((item: any) => item.toJSON())).toEqual(items); +}); -// test("Model.has", async () => { -// expect.assertions(1); +test("Model.has", async () => { + expect.assertions(1); -// const items = CHATS.filter(chat => array.any(MESSAGES, message => message.chatname === chat.name)); + const items = CHATS.filter(chat => array.any(MESSAGES, message => message.chatname === chat.name)); -// const results = await Chat.has("messages").lean().get(); + const results = await Chat.has("messages").lean().get(); -// expect(results).toEqual(items); -// }); + expect(results).toEqual(items); +}); test("Model.has [deep]", async () => { expect.assertions(1);