From 06bb5d00da27f73a7ad09f6c55abeb783bc9d9c2 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Thu, 1 Sep 2022 15:11:28 +0900 Subject: [PATCH 1/3] Close #80, `JoinQueryBuilder` supports SQL queries --- package.json | 2 +- src/Model.ts | 16 +- src/builders/JoinQueryBuilder.ts | 364 +++++++++++++----- .../internal/app_join_has_many_to_many.ts | 2 +- src/functional/createJoinQueryBuilder.ts | 51 +-- ...test_join_query_builder_duplicated_join.ts | 9 +- .../features/test_join_query_builder_where.ts | 26 ++ src/test/features/test_json_select_builder.ts | 3 +- src/test/features/test_safe_query_builder.ts | 1 + 9 files changed, 348 insertions(+), 126 deletions(-) create mode 100644 src/test/features/test_join_query_builder_where.ts diff --git a/package.json b/package.json index ef6c06a..e329233 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-typeorm", - "version": "1.0.17", + "version": "2.0.0-dev.20220901", "description": "Make TypeORM much safer", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/Model.ts b/src/Model.ts index 201db6b..6ece616 100644 --- a/src/Model.ts +++ b/src/Model.ts @@ -108,12 +108,12 @@ export abstract class Model extends orm.BaseEntity { * * @template T Type of a model class that is derived from the `Model` * @param closure A callback function who can join related tables very easily and safely - * @return The newly created `TyperORM.SelectQueryBuilder` instance + * @return The newly created `JoinQueryBuilder` instance */ public static createJoinQueryBuilder( this: Model.Creator, - closure: (builder: JoinQueryBuilder) => void, - ): orm.SelectQueryBuilder; + closure?: (builder: JoinQueryBuilder) => void, + ): JoinQueryBuilder; /** * Create join query builder with alias. @@ -133,21 +133,21 @@ export abstract class Model extends orm.BaseEntity { * @template T Type of a model class that is derived from the `Model` * @param alias Alias for the table *T* * @param closure A callback function who can join related tables very easily and safely - * @return The newly created `TyperORM.SelectQueryBuilder` instance + * @return The newly created `JoinQueryBuilder` instance */ public static createJoinQueryBuilder( this: Model.Creator, alias: string, - closure: (builder: JoinQueryBuilder) => void, - ): orm.SelectQueryBuilder; + closure?: (builder: JoinQueryBuilder) => void, + ): JoinQueryBuilder; public static createJoinQueryBuilder( this: Model.Creator, ...args: any[] - ): orm.SelectQueryBuilder { + ): JoinQueryBuilder { return createJoinQueryBuilder( this, - ...(args as [string, (builder: JoinQueryBuilder) => void]), + ...(args as [string, (builder: JoinQueryBuilder) => void]), ); } diff --git a/src/builders/JoinQueryBuilder.ts b/src/builders/JoinQueryBuilder.ts index 29b86e0..33af605 100644 --- a/src/builders/JoinQueryBuilder.ts +++ b/src/builders/JoinQueryBuilder.ts @@ -10,9 +10,13 @@ import { RelationshipVariable } from "../decorators/base/RelationshipVariable"; import { ITableInfo } from "../functional/internal/ITableInfo"; import { Creator } from "../typings/Creator"; +import { Field } from "../typings/Field"; import { Relationship } from "../typings/Relationship"; import { SpecialFields } from "../typings/SpecialFields"; +import { getWhereArguments } from "../functional"; +import { Operator } from "../typings"; + /** * DB level join query builder. * @@ -44,11 +48,15 @@ import { SpecialFields } from "../typings/SpecialFields"; * Elapsed Time | 8.07508 | 0.00262 * * @template Mine Target entity to perform the DB join + * @template Query Target entity of `TypeORM.SelectQueryBuilder` * @reference [stackoverflow/join-queries-vs-multiple-queries](https://stackoverflow.com/questions/1067016/join-queries-vs-multiple-queries) * @author Jeongho Nam - https://github.com/samchon */ -export class JoinQueryBuilder { - private readonly stmt_: orm.SelectQueryBuilder; +export class JoinQueryBuilder< + Mine extends object, + Query extends object = Mine, +> { + private readonly stmt_: orm.SelectQueryBuilder; private readonly mine_: Creator; private readonly alias_: string; @@ -61,14 +69,21 @@ export class JoinQueryBuilder { CONNSTRUCTOR ----------------------------------------------------------- */ /** - * Default Constructor. - * - * @param stmt A {@link SelectQueryBuilder} instance for the *Mine* entity - * @param mine Target ORM class to perform the DB join - * @param alias Alias name specification, for the *Mine* entity, if required + * @internal */ - public constructor( - stmt: orm.SelectQueryBuilder, + public static create( + stmt: orm.SelectQueryBuilder, + mine: Creator, + alias?: string, + ): JoinQueryBuilder { + return new JoinQueryBuilder(stmt, mine, alias); + } + + /** + * @hidden + */ + private constructor( + stmt: orm.SelectQueryBuilder, mine: Creator, alias?: string, ) { @@ -87,12 +102,13 @@ export class JoinQueryBuilder { closure: | (( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void) | undefined, joiner: (asset: IAsset) => void, - ): JoinQueryBuilder> { + ): JoinQueryBuilder, Query> { // PREPARE ASSET const asset: IAsset = prepare_asset( this.mine_, @@ -140,7 +156,7 @@ export class JoinQueryBuilder { public get>>( field: SpecialFields>, - ): JoinQueryBuilder> { + ): JoinQueryBuilder, Query> { const found: IJoined | undefined = this.joined_.get(field); if (found === undefined) throw new OutOfRange( @@ -157,8 +173,167 @@ export class JoinQueryBuilder { return this.size() === 0; } + public statement(): orm.SelectQueryBuilder { + return this.stmt_; + } + + /* ----------------------------------------------------------- + STATEMENTS + ----------------------------------------------------------- */ + public addSelect( + field: SpecialFields>, + alias?: string, + ): this { + this.stmt_.addSelect(`${this.alias_}.${field}`, alias || field); + return this; + } + + public addOrderBy( + field: SpecialFields, + order?: "ASC" | "DESC" | undefined, + nulls?: "NULLS FIRST" | "NULLS LAST", + ): JoinQueryBuilder { + this.stmt_.addOrderBy(`${this.alias_}.${field}`, order, nulls); + return this; + } + + public addGroupBy( + field: SpecialFields, + ): JoinQueryBuilder { + this.stmt_.addGroupBy(`${this.alias_}.${field}`); + return this; + } + + public andWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + >( + this: JoinQueryBuilder, + field: Literal, + param: Field.MemberType | null | (() => string), + ): JoinQueryBuilder; + + public andWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + OperatorType extends Operator, + >( + this: JoinQueryBuilder, + field: Literal, + operator: OperatorType, + param: + | (OperatorType extends "=" | "!=" | "<>" + ? Field.MemberType | null + : Field.MemberType) + | (() => string), + ): JoinQueryBuilder; + + public andWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + OperatorType extends Operator, + >( + this: JoinQueryBuilder, + field: Literal, + operator: "IN" | "NOT IN", + param: Array> | (() => string), + ): JoinQueryBuilder; + + public andWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + OperatorType extends Operator, + >( + this: JoinQueryBuilder, + field: Literal, + operator: "BETWEEN", + minimum: Field.MemberType | (() => string), + maximum: Field.MemberType | (() => string), + ): JoinQueryBuilder; + + public andWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + >( + this: JoinQueryBuilder, + field: Literal, + ...rest: any[] + ): JoinQueryBuilder { + const args = getWhereArguments( + this.mine_, + `${this.alias_}.${field}`, + ...(rest as [Operator, Field.MemberType]), + ); + this.stmt_.andWhere(...args); + return (this) as JoinQueryBuilder; + } + + public orWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + >( + this: JoinQueryBuilder, + field: Literal, + param: Field.MemberType | null | (() => string), + ): JoinQueryBuilder; + + public orWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + OperatorType extends Operator, + >( + this: JoinQueryBuilder, + field: Literal, + operator: OperatorType, + param: + | (OperatorType extends "=" | "!=" | "<>" + ? Field.MemberType | null + : Field.MemberType) + | (() => string), + ): JoinQueryBuilder; + + public orWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + OperatorType extends Operator, + >( + this: JoinQueryBuilder, + field: Literal, + operator: "IN" | "NOT IN", + param: Array> | (() => string), + ): JoinQueryBuilder; + + public orWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + OperatorType extends Operator, + >( + this: JoinQueryBuilder, + field: Literal, + operator: "BETWEEN", + minimum: Field.MemberType | (() => string), + maximum: Field.MemberType | (() => string), + ): JoinQueryBuilder; + + public orWhere< + T extends Mine & { [P in Literal]: Field }, + Literal extends SpecialFields, + >( + this: JoinQueryBuilder, + field: Literal, + ...rest: any[] + ): JoinQueryBuilder { + const args = getWhereArguments( + this.mine_, + `${this.alias_}.${field}`, + ...(rest as [Operator, Field.MemberType]), + ); + this.stmt_.orWhere(...args); + return (this) as JoinQueryBuilder; + } + /* ----------------------------------------------------------- - RAW JOIN + JOINERS ----------------------------------------------------------- */ /** * Configure an inner join. @@ -183,10 +358,11 @@ export class JoinQueryBuilder { field: Field, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; /** * Configure an inner join with alias specification. @@ -213,10 +389,11 @@ export class JoinQueryBuilder { alias: string, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; public innerJoin< Field extends SpecialFields>, @@ -226,15 +403,17 @@ export class JoinQueryBuilder { | string | (( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void), closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder> { + ): JoinQueryBuilder, Query> { return this._Join_atomic( "innerJoin", field, @@ -265,10 +444,11 @@ export class JoinQueryBuilder { field: Field, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; /** * Configure left join with alias specification. @@ -295,10 +475,11 @@ export class JoinQueryBuilder { alias: string, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; public leftJoin< Field extends SpecialFields>, @@ -308,15 +489,17 @@ export class JoinQueryBuilder { | string | (( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void), closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder> { + ): JoinQueryBuilder, Query> { return this._Join_atomic( "leftJoin", field, @@ -324,50 +507,6 @@ export class JoinQueryBuilder { ); } - private _Join_atomic< - Field extends SpecialFields>, - >( - method: "innerJoin" | "leftJoin", - field: Field, - alias: string | undefined, - closure: - | (( - builder: JoinQueryBuilder< - Relationship.Joinable.TargetType - >, - ) => void) - | undefined, - ): JoinQueryBuilder> { - return this._Take_join(method, field, alias, closure, (asset) => { - // LIST UP EACH FIELDS - const [myField, targetField] = (() => { - if (asset.belongs === true) - return [ - asset.metadata.foreign_key_field, - get_primary_column(asset.metadata.target()), - ]; - - const inverseMetadata: Belongs.ManyToOne.IMetadata = - ReflectAdaptor.get( - asset.metadata.target().prototype, - asset.metadata.inverse, - ) as Belongs.ManyToOne.IMetadata; - - return [ - get_primary_column(this.mine_), - inverseMetadata.foreign_key_field, - ]; - })(); - - // DO JOIN - const condition: string = `${this.alias_}.${myField} = ${asset.alias}.${targetField}`; - this.stmt_[method](asset.metadata.target(), asset.alias, condition); - }); - } - - /* ----------------------------------------------------------- - ORM JOIN - ----------------------------------------------------------- */ /** * Configure inner join with mapping. * @@ -396,10 +535,11 @@ export class JoinQueryBuilder { field: Field, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; /** * Configure inner join with mapping and alias specification. @@ -431,10 +571,11 @@ export class JoinQueryBuilder { alias: string, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; public innerJoinAndSelect< Field extends SpecialFields>, @@ -444,15 +585,17 @@ export class JoinQueryBuilder { | string | (( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void), closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder> { + ): JoinQueryBuilder, Query> { return this._Join_and_select( "innerJoinAndSelect", field, @@ -488,10 +631,11 @@ export class JoinQueryBuilder { field: Field, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; /** * Configure left join with mapping and alias specification. @@ -523,10 +667,11 @@ export class JoinQueryBuilder { alias: string, closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder>; + ): JoinQueryBuilder, Query>; public leftJoinAndSelect< Field extends SpecialFields>, @@ -536,15 +681,17 @@ export class JoinQueryBuilder { | string | (( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void), closure?: ( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void, - ): JoinQueryBuilder> { + ): JoinQueryBuilder, Query> { return this._Join_and_select( "leftJoinAndSelect", field, @@ -552,6 +699,48 @@ export class JoinQueryBuilder { ); } + private _Join_atomic< + Field extends SpecialFields>, + >( + method: "innerJoin" | "leftJoin", + field: Field, + alias: string | undefined, + closure: + | (( + builder: JoinQueryBuilder< + Relationship.Joinable.TargetType, + Query + >, + ) => void) + | undefined, + ): JoinQueryBuilder, Query> { + return this._Take_join(method, field, alias, closure, (asset) => { + // LIST UP EACH FIELDS + const [myField, targetField] = (() => { + if (asset.belongs === true) + return [ + asset.metadata.foreign_key_field, + get_primary_column(asset.metadata.target()), + ]; + + const inverseMetadata: Belongs.ManyToOne.IMetadata = + ReflectAdaptor.get( + asset.metadata.target().prototype, + asset.metadata.inverse, + ) as Belongs.ManyToOne.IMetadata; + + return [ + get_primary_column(this.mine_), + inverseMetadata.foreign_key_field, + ]; + })(); + + // DO JOIN + const condition: string = `${this.alias_}.${myField} = ${asset.alias}.${targetField}`; + this.stmt_[method](asset.metadata.target(), asset.alias, condition); + }); + } + private _Join_and_select< Field extends SpecialFields>, >( @@ -561,7 +750,8 @@ export class JoinQueryBuilder { closure: | (( builder: JoinQueryBuilder< - Relationship.Joinable.TargetType + Relationship.Joinable.TargetType, + Query >, ) => void) | undefined, @@ -635,7 +825,7 @@ function get_primary_column(creator: Creator): string { interface IJoined { method: IJoined.Method; alias: string; - builder: JoinQueryBuilder; + builder: JoinQueryBuilder; } namespace IJoined { export type Method = diff --git a/src/builders/internal/app_join_has_many_to_many.ts b/src/builders/internal/app_join_has_many_to_many.ts index 9351bdc..2aeb378 100644 --- a/src/builders/internal/app_join_has_many_to_many.ts +++ b/src/builders/internal/app_join_has_many_to_many.ts @@ -61,7 +61,7 @@ export async function app_join_has_many_to_many< const router: Creator = metadata.router(); const stmt: orm.SelectQueryBuilder = findRepository(router).createQueryBuilder(); - new JoinQueryBuilder(stmt, router).innerJoinAndSelect( + JoinQueryBuilder.create(stmt, router).innerJoinAndSelect( metadata.target_inverse, ); diff --git a/src/functional/createJoinQueryBuilder.ts b/src/functional/createJoinQueryBuilder.ts index 13196d6..2f2bdd7 100644 --- a/src/functional/createJoinQueryBuilder.ts +++ b/src/functional/createJoinQueryBuilder.ts @@ -23,12 +23,12 @@ import { findRepository } from "./findRepository"; * * @template T Type of a model class * @param closure A callback function who can join related tables very easily and safely - * @return The newly created `TyperORM.SelectQueryBuilder` instance + * @return The newly created `JoinQueryBuilder` instance */ export function createJoinQueryBuilder( creator: Creator, - closure: (builder: JoinQueryBuilder) => void, -): orm.SelectQueryBuilder; + closure: (builder: JoinQueryBuilder) => void, +): JoinQueryBuilder; /** * Create join query builder from manager. @@ -48,13 +48,13 @@ export function createJoinQueryBuilder( * @template T Type of a model class * @param manager Entity manager of TypeORM, maybe used for the transaction scope * @param closure A callback function who can join related tables very easily and safely - * @return The newly created `TyperORM.SelectQueryBuilder` instance + * @return The newly created `JoinQueryBuilder` instance */ export function createJoinQueryBuilder( manager: orm.EntityManager, creator: Creator, - closure: (builder: JoinQueryBuilder) => void, -): orm.SelectQueryBuilder; + closure: (builder: JoinQueryBuilder) => void, +): JoinQueryBuilder; /** * Create join query builder with alias. @@ -74,13 +74,13 @@ export function createJoinQueryBuilder( * @template T Type of a model class * @param alias Alias for the table *T* * @param closure A callback function who can join related tables very easily and safely - * @return The newly created `TyperORM.SelectQueryBuilder` instance + * @return The newly created `JoinQueryBuilder` instance */ export function createJoinQueryBuilder( creator: Creator, alias: string, - closure: (builder: JoinQueryBuilder) => void, -): orm.SelectQueryBuilder; + closure?: (builder: JoinQueryBuilder) => void, +): JoinQueryBuilder; /** * Create join query builder from manager with alias. @@ -101,35 +101,40 @@ export function createJoinQueryBuilder( * @param manager Entity manager of TypeORM, maybe used for the transaction scope * @param alias Alias for the table *T* * @param closure A callback function who can join related tables very easily and safely - * @return The newly created `TyperORM.SelectQueryBuilder` instance + * @return The newly created `JoinQueryBuilder` instance */ export function createJoinQueryBuilder( manager: orm.EntityManager, creator: Creator, alias: string, - closure: (builder: JoinQueryBuilder) => void, -): orm.SelectQueryBuilder; + closure?: (builder: JoinQueryBuilder) => void, +): JoinQueryBuilder; export function createJoinQueryBuilder( ...args: any[] -): orm.SelectQueryBuilder { +): JoinQueryBuilder { // LIST UP PARAMETERS - const manager: orm.EntityManager | null = - args[0] instanceof orm.EntityManager ? args[0] : null; - const creator: Creator = manager !== null ? args[1] : args[0]; - args.splice(0, manager !== null ? 2 : 1); + const index: number = args[0] instanceof orm.EntityManager ? 1 : 0; - // LIST UP ALIAS AND CLOSURE - const [alias, closure]: [string, (builder: JoinQueryBuilder) => void] = - args.length === 1 ? [creator.name, args[0]] : [args[0], args[1]]; + const manager: orm.EntityManager | null = index === 1 ? args[0] : null; + const creator: Creator = args[index]; + const alias: string | undefined = + typeof args[index + 1] === "string" ? args[index + 1] : undefined; + const closure = alias !== undefined ? args[index + 2] : args[index + 1]; + // STATEMENT const stmt: orm.SelectQueryBuilder = ( manager !== null ? manager.getRepository(creator) : findRepository(creator) ).createQueryBuilder(alias); - const builder: JoinQueryBuilder = new JoinQueryBuilder(stmt, creator); - closure(builder); - return stmt; + // JOINER + const builder: JoinQueryBuilder = JoinQueryBuilder.create( + stmt, + creator, + alias, + ); + if (closure) closure(builder); + return builder; } diff --git a/src/test/features/test_join_query_builder_duplicated_join.ts b/src/test/features/test_join_query_builder_duplicated_join.ts index d851036..5367086 100644 --- a/src/test/features/test_join_query_builder_duplicated_join.ts +++ b/src/test/features/test_join_query_builder_duplicated_join.ts @@ -4,8 +4,8 @@ import { BbsGroup } from "../models/bbs/BbsGroup"; export async function test_join_query_builder_duplicated_join(): Promise { const group: BbsGroup = await generate_random_normal_bbs_group(); - const stmt = BbsGroup.createQueryBuilder(); - const builder = new safe.JoinQueryBuilder(stmt, BbsGroup); + const builder: safe.JoinQueryBuilder = + BbsGroup.createJoinQueryBuilder(); builder.innerJoin("articles", (article) => { article.innerJoin("__mv_last").innerJoin("content"); @@ -21,7 +21,6 @@ export async function test_join_query_builder_duplicated_join(): Promise { article.innerJoin("tags"); }); - stmt.andWhere(...BbsGroup.getWhereArguments("code", group.code)); - console.log(stmt.getQuery()); - await stmt.getMany(); + builder.andWhere("code", group.code); + await builder.statement().getMany(); } diff --git a/src/test/features/test_join_query_builder_where.ts b/src/test/features/test_join_query_builder_where.ts new file mode 100644 index 0000000..994c7f1 --- /dev/null +++ b/src/test/features/test_join_query_builder_where.ts @@ -0,0 +1,26 @@ +import safe from "../.."; +import { BbsGroup } from "../models/bbs/BbsGroup"; +import { IBbsGroup } from "../structures/IBbsGroup"; +import { test_json_select_builder } from "./test_json_select_builder"; + +export async function test_join_query_builder_where() { + const json: IBbsGroup[] = await test_json_select_builder(); + + const builder: safe.JoinQueryBuilder = + BbsGroup.createJoinQueryBuilder(); + builder.innerJoin("articles", (article) => { + article.innerJoin("contents", "BC", (content) => { + content.orWhere("title", json[1].articles[0].contents[0].title); + }); + article.orWhere("id", json[2].articles[0].id); + }); + builder.orWhere("code", json[0].code); + + const groups: BbsGroup[] = await builder.statement().distinct().getMany(); + if (groups.length !== 3) { + console.log(groups.length); + throw new Error( + "Bug on JoinQueryBuilder.where(): failed to understand where query.", + ); + } +} diff --git a/src/test/features/test_json_select_builder.ts b/src/test/features/test_json_select_builder.ts index 0e2bed4..d70ab6b 100644 --- a/src/test/features/test_json_select_builder.ts +++ b/src/test/features/test_json_select_builder.ts @@ -12,7 +12,7 @@ import { BbsGroup } from "../models/bbs/BbsGroup"; import { IBbsArticle } from "../structures/IBbsArticle"; import { IBbsGroup } from "../structures/IBbsGroup"; -export async function test_json_select_builder(): Promise { +export async function test_json_select_builder(): Promise { const builder = new safe.JsonSelectBuilder(BbsGroup, { articles: new safe.JsonSelectBuilder(BbsArticle, { group: safe.DEFAULT, @@ -89,4 +89,5 @@ export async function test_json_select_builder(): Promise { }); }); }); + return regular; } diff --git a/src/test/features/test_safe_query_builder.ts b/src/test/features/test_safe_query_builder.ts index c4e8c77..b770578 100644 --- a/src/test/features/test_safe_query_builder.ts +++ b/src/test/features/test_safe_query_builder.ts @@ -24,6 +24,7 @@ export async function test_safe_query_builder(): Promise { .leftJoin("__mv_last", "AL") .leftJoin("content", "AC"); }) + .statement() .andWhere(...BbsArticle.getWhereArguments("group", group)) .andWhere(...BbsCategory.getWhereArguments("code", "!=", category.code)) .select([ From d612f0a50fc199558ed36959ff960d5a500843d1 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Tue, 13 Sep 2022 19:07:08 +0900 Subject: [PATCH 2/3] Complemet #80, `JoinQueryBuilder` supports SQL queres --- package.json | 2 +- src/Model.ts | 12 +- src/builders/JoinQueryBuilder.ts | 162 +++++++++++++----- src/functional/createJoinQueryBuilder.ts | 4 +- src/functional/getWhereArguments.ts | 29 ++-- .../features/test_join_query_builder_where.ts | 2 +- .../{WhereColumnType.ts => FieldLike.ts} | 2 +- src/typings/index.ts | 2 +- 8 files changed, 151 insertions(+), 64 deletions(-) rename src/typings/{WhereColumnType.ts => FieldLike.ts} (50%) diff --git a/package.json b/package.json index e329233..7f41a24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-typeorm", - "version": "2.0.0-dev.20220901", + "version": "2.0.0-dev.20220913-5", "description": "Make TypeORM much safer", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/Model.ts b/src/Model.ts index 6ece616..8639714 100644 --- a/src/Model.ts +++ b/src/Model.ts @@ -15,6 +15,7 @@ import { insert } from "./functional/insert"; import { toPrimitive } from "./functional/toPrimitive"; import { update } from "./functional/update"; +import { FieldLike } from "./typings/FieldLike"; import { Creator as _Creator } from "./typings/Creator"; import { Field } from "./typings/Field"; import { Initialized } from "./typings/Initialized"; @@ -22,7 +23,6 @@ import { OmitNever } from "./typings/OmitNever"; import { Operator } from "./typings/Operator"; import { Primitive } from "./typings/Primitive"; import { SpecialFields } from "./typings/SpecialFields"; -import { WhereColumnType } from "./typings/WhereColumnType"; /** * The basic model class. @@ -233,7 +233,7 @@ export abstract class Model extends orm.BaseEntity { Literal extends SpecialFields, >( this: Model.Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, param: Field.MemberType | null | (() => string), ): [string, Record>]; @@ -267,7 +267,7 @@ export abstract class Model extends orm.BaseEntity { OperatorType extends Operator, >( this: Model.Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, operator: OperatorType, param: | (OperatorType extends "=" | "!=" | "<>" @@ -309,7 +309,7 @@ export abstract class Model extends orm.BaseEntity { Literal extends SpecialFields, >( this: Model.Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, operator: "IN" | "NOT IN", parameters: Array> | (() => string), ): [string, Record>>]; @@ -344,7 +344,7 @@ export abstract class Model extends orm.BaseEntity { Literal extends SpecialFields, >( this: Model.Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, operator: "BETWEEN", minimum: Field.MemberType | (() => string), maximum: Field.MemberType | (() => string), @@ -355,7 +355,7 @@ export abstract class Model extends orm.BaseEntity { Literal extends SpecialFields, >( this: Model.Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, ...rest: any[] ): [string, any] { return getWhereArguments( diff --git a/src/builders/JoinQueryBuilder.ts b/src/builders/JoinQueryBuilder.ts index 33af605..544db81 100644 --- a/src/builders/JoinQueryBuilder.ts +++ b/src/builders/JoinQueryBuilder.ts @@ -15,7 +15,7 @@ import { Relationship } from "../typings/Relationship"; import { SpecialFields } from "../typings/SpecialFields"; import { getWhereArguments } from "../functional"; -import { Operator } from "../typings"; +import { FieldLike, Operator } from "../typings"; /** * DB level join query builder. @@ -52,11 +52,8 @@ import { Operator } from "../typings"; * @reference [stackoverflow/join-queries-vs-multiple-queries](https://stackoverflow.com/questions/1067016/join-queries-vs-multiple-queries) * @author Jeongho Nam - https://github.com/samchon */ -export class JoinQueryBuilder< - Mine extends object, - Query extends object = Mine, -> { - private readonly stmt_: orm.SelectQueryBuilder; +export class JoinQueryBuilder { + private readonly stmt_: IStatement; private readonly mine_: Creator; private readonly alias_: string; @@ -76,14 +73,23 @@ export class JoinQueryBuilder< mine: Creator, alias?: string, ): JoinQueryBuilder { - return new JoinQueryBuilder(stmt, mine, alias); + return new JoinQueryBuilder( + { + query: stmt, + groupped: false, + ordered: false, + selected: false, + }, + mine, + alias, + ); } /** * @hidden */ private constructor( - stmt: orm.SelectQueryBuilder, + stmt: IStatement, mine: Creator, alias?: string, ) { @@ -155,7 +161,7 @@ export class JoinQueryBuilder< } public get>>( - field: SpecialFields>, + field: Field, ): JoinQueryBuilder, Query> { const found: IJoined | undefined = this.joined_.get(field); if (found === undefined) @@ -174,33 +180,79 @@ export class JoinQueryBuilder< } public statement(): orm.SelectQueryBuilder { - return this.stmt_; + return this.stmt_.query; } /* ----------------------------------------------------------- STATEMENTS ----------------------------------------------------------- */ - public addSelect( - field: SpecialFields>, + private _Decompose_field_like>( + fieldLike: FieldLike, + ): { column: string; symbol: string } { + const escape = (field: Literal) => + ( + ReflectAdaptor.get( + this.mine_.prototype, + field, + ) as Belongs.ManyToOne.IMetadata + )?.foreign_key_field || field; + const symbol = (column: string) => `${this.alias_}.${column}`; + + if (typeof fieldLike === "string") { + const column: string = escape(fieldLike); + return { column, symbol: symbol(column) }; + } else { + const column: string = escape(fieldLike[0]); + const closure: (str: string) => string = fieldLike[1]; + + return { + column, + symbol: closure(symbol(column)), + }; + } + } + + public addSelect>( + fieldLike: FieldLike, alias?: string, ): this { - this.stmt_.addSelect(`${this.alias_}.${field}`, alias || field); + const { column, symbol } = this._Decompose_field_like(fieldLike); + alias ||= column; + + if (this.stmt_.selected === false) { + this.stmt_.query.select(symbol, alias); + this.stmt_.selected = true; + } else { + this.stmt_.query.addSelect(symbol, alias || column); + } return this; } - public addOrderBy( - field: SpecialFields, + public addOrderBy>( + fieldLike: FieldLike, order?: "ASC" | "DESC" | undefined, nulls?: "NULLS FIRST" | "NULLS LAST", ): JoinQueryBuilder { - this.stmt_.addOrderBy(`${this.alias_}.${field}`, order, nulls); + const { symbol } = this._Decompose_field_like(fieldLike); + if (this.stmt_.ordered === false) { + this.stmt_.query.orderBy(symbol, order, nulls); + this.stmt_.ordered = true; + } else { + this.stmt_.query.addOrderBy(symbol, order, nulls); + } return this; } - public addGroupBy( - field: SpecialFields, + public addGroupBy>( + fieldLike: FieldLike, ): JoinQueryBuilder { - this.stmt_.addGroupBy(`${this.alias_}.${field}`); + const { symbol } = this._Decompose_field_like(fieldLike); + if (this.stmt_.groupped === false) { + this.stmt_.query.groupBy(symbol); + this.stmt_.groupped = true; + } else { + this.stmt_.query.addGroupBy(symbol); + } return this; } @@ -209,7 +261,7 @@ export class JoinQueryBuilder< Literal extends SpecialFields, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, param: Field.MemberType | null | (() => string), ): JoinQueryBuilder; @@ -219,7 +271,7 @@ export class JoinQueryBuilder< OperatorType extends Operator, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, operator: OperatorType, param: | (OperatorType extends "=" | "!=" | "<>" @@ -234,7 +286,7 @@ export class JoinQueryBuilder< OperatorType extends Operator, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, operator: "IN" | "NOT IN", param: Array> | (() => string), ): JoinQueryBuilder; @@ -245,7 +297,7 @@ export class JoinQueryBuilder< OperatorType extends Operator, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, operator: "BETWEEN", minimum: Field.MemberType | (() => string), maximum: Field.MemberType | (() => string), @@ -256,15 +308,14 @@ export class JoinQueryBuilder< Literal extends SpecialFields, >( this: JoinQueryBuilder, - field: Literal, + fieldLike: FieldLike, ...rest: any[] ): JoinQueryBuilder { - const args = getWhereArguments( - this.mine_, - `${this.alias_}.${field}`, - ...(rest as [Operator, Field.MemberType]), + this._Where( + (stmt, ...args) => stmt.andWhere(...args), + fieldLike, + ...rest, ); - this.stmt_.andWhere(...args); return (this) as JoinQueryBuilder; } @@ -273,7 +324,7 @@ export class JoinQueryBuilder< Literal extends SpecialFields, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, param: Field.MemberType | null | (() => string), ): JoinQueryBuilder; @@ -283,7 +334,7 @@ export class JoinQueryBuilder< OperatorType extends Operator, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, operator: OperatorType, param: | (OperatorType extends "=" | "!=" | "<>" @@ -298,7 +349,7 @@ export class JoinQueryBuilder< OperatorType extends Operator, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, operator: "IN" | "NOT IN", param: Array> | (() => string), ): JoinQueryBuilder; @@ -309,7 +360,7 @@ export class JoinQueryBuilder< OperatorType extends Operator, >( this: JoinQueryBuilder, - field: Literal, + field: FieldLike, operator: "BETWEEN", minimum: Field.MemberType | (() => string), maximum: Field.MemberType | (() => string), @@ -320,18 +371,37 @@ export class JoinQueryBuilder< Literal extends SpecialFields, >( this: JoinQueryBuilder, - field: Literal, + fieldLike: FieldLike, ...rest: any[] ): JoinQueryBuilder { - const args = getWhereArguments( - this.mine_, - `${this.alias_}.${field}`, - ...(rest as [Operator, Field.MemberType]), + this._Where( + (stmt, ...args) => stmt.orWhere(...args), + fieldLike, + ...rest, ); - this.stmt_.orWhere(...args); return (this) as JoinQueryBuilder; } + private _Where( + condition: ( + stmt: orm.SelectQueryBuilder, + ...args: [string, any] + ) => any, + fieldLike: string | [string, (str: string) => string], + ...rest: any[] + ) { + const parameters: any[] = [ + this.mine_, + typeof fieldLike === "string" + ? `${this.alias_}.${fieldLike}` + : [`${this.alias_}.${fieldLike[0]}`, fieldLike[1]], + ]; + parameters.push(...rest); + + const args: [string, any] = (getWhereArguments as any)(...parameters); + condition(this.stmt_.query, ...args); + } + /* ----------------------------------------------------------- JOINERS ----------------------------------------------------------- */ @@ -737,7 +807,11 @@ export class JoinQueryBuilder< // DO JOIN const condition: string = `${this.alias_}.${myField} = ${asset.alias}.${targetField}`; - this.stmt_[method](asset.metadata.target(), asset.alias, condition); + this.stmt_.query[method]( + asset.metadata.target(), + asset.alias, + condition, + ); }); } @@ -761,7 +835,7 @@ export class JoinQueryBuilder< asset.belongs ? "belongs" : "has", field, ); - this.stmt_[method](`${this.alias_}.${index}`, asset.alias); + this.stmt_.query[method](`${this.alias_}.${index}`, asset.alias); }); } } @@ -822,6 +896,12 @@ function get_primary_column(creator: Creator): string { return ITableInfo.get(creator).primaryColumn; } +interface IStatement { + query: orm.SelectQueryBuilder; + selected: boolean; + groupped: boolean; + ordered: boolean; +} interface IJoined { method: IJoined.Method; alias: string; diff --git a/src/functional/createJoinQueryBuilder.ts b/src/functional/createJoinQueryBuilder.ts index 2f2bdd7..17925fa 100644 --- a/src/functional/createJoinQueryBuilder.ts +++ b/src/functional/createJoinQueryBuilder.ts @@ -27,7 +27,7 @@ import { findRepository } from "./findRepository"; */ export function createJoinQueryBuilder( creator: Creator, - closure: (builder: JoinQueryBuilder) => void, + closure?: (builder: JoinQueryBuilder) => void, ): JoinQueryBuilder; /** @@ -53,7 +53,7 @@ export function createJoinQueryBuilder( export function createJoinQueryBuilder( manager: orm.EntityManager, creator: Creator, - closure: (builder: JoinQueryBuilder) => void, + closure?: (builder: JoinQueryBuilder) => void, ): JoinQueryBuilder; /** diff --git a/src/functional/getWhereArguments.ts b/src/functional/getWhereArguments.ts index 6175ed2..5dbfa33 100644 --- a/src/functional/getWhereArguments.ts +++ b/src/functional/getWhereArguments.ts @@ -5,9 +5,9 @@ import { BelongsAccessorBase } from "../decorators/base/BelongsAccessorBase"; import { Creator } from "../typings/Creator"; import { Field } from "../typings/Field"; +import { FieldLike } from "../typings/FieldLike"; import { Operator } from "../typings/Operator"; import { SpecialFields } from "../typings/SpecialFields"; -import { WhereColumnType } from "../typings/WhereColumnType"; import { findRepository } from "./findRepository"; import { get_column_name_tuple } from "./internal/get_column_name_tuple"; @@ -41,7 +41,7 @@ export function getWhereArguments< Literal extends SpecialFields, >( creator: Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, param: Field.MemberType | null | (() => string), ): [string, Record>]; @@ -76,7 +76,7 @@ export function getWhereArguments< OperatorType extends Operator, >( creator: Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, operator: OperatorType, param: | (OperatorType extends "=" | "!=" | "<>" @@ -115,7 +115,7 @@ export function getWhereArguments< Literal extends SpecialFields, >( creator: Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, operator: "IN" | "NOT IN", parameters: Array> | (() => string), ): [ @@ -154,7 +154,7 @@ export function getWhereArguments< Literal extends SpecialFields, >( creator: Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, operator: "BETWEEN", minimum: Field.MemberType | (() => string), maximum: Field.MemberType | (() => string), @@ -165,16 +165,18 @@ export function getWhereArguments< Literal extends SpecialFields, >( creator: Creator, - fieldLike: WhereColumnType<`${Literal}` | `${string}.${Literal}`>, + fieldLike: FieldLike<`${Literal}` | `${string}.${Literal}`>, ...rest: any[] ): [string, any] { - const tuple: [string, string] = get_column_name_tuple( + const escaper = + typeof fieldLike === "string" + ? (column: string) => column + : (column: string) => fieldLike[1](column); + const [alias, column] = get_column_name_tuple( creator, typeof fieldLike === "string" ? fieldLike : fieldLike[0], ); - const column: string = tuple[0] ? `${tuple[0]}.${tuple[1]}` : tuple[1]; - const left: string = - typeof fieldLike === "string" ? column : fieldLike[1](column); + const left: string = escaper(alias ? `${alias}.${column}` : column); // MOST OPERATORS if (rest.length <= 2) { @@ -185,7 +187,12 @@ export function getWhereArguments< ] = (() => { const [operator, param] = rest.length === 1 ? ["=", rest[0]] : [rest[0], rest[1]]; - return [operator, _Decompose_parameter(param)]; + return [ + operator, + typeof param === "function" + ? param + : _Decompose_parameter(param), + ]; })(); // IS NULL || IS-NOT-NULL diff --git a/src/test/features/test_join_query_builder_where.ts b/src/test/features/test_join_query_builder_where.ts index 994c7f1..9520722 100644 --- a/src/test/features/test_join_query_builder_where.ts +++ b/src/test/features/test_join_query_builder_where.ts @@ -12,7 +12,7 @@ export async function test_join_query_builder_where() { article.innerJoin("contents", "BC", (content) => { content.orWhere("title", json[1].articles[0].contents[0].title); }); - article.orWhere("id", json[2].articles[0].id); + article.orWhere("id", () => `'${json[2].articles[0].id}'`); }); builder.orWhere("code", json[0].code); diff --git a/src/typings/WhereColumnType.ts b/src/typings/FieldLike.ts similarity index 50% rename from src/typings/WhereColumnType.ts rename to src/typings/FieldLike.ts index f5979fb..81d51a2 100644 --- a/src/typings/WhereColumnType.ts +++ b/src/typings/FieldLike.ts @@ -1,3 +1,3 @@ -export type WhereColumnType = +export type FieldLike = | Literal | [Literal, (str: string) => string]; diff --git a/src/typings/index.ts b/src/typings/index.ts index c5ecdb1..9da4b34 100644 --- a/src/typings/index.ts +++ b/src/typings/index.ts @@ -11,4 +11,4 @@ export * from "./Relationship"; export * from "./Same"; export * from "./SpecialFields"; export * from "./StringColumnType"; -export * from "./WhereColumnType"; +export * from "./FieldLike"; From 2cc75874b7888e7f296b0be39028ebca27d51c4c Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Wed, 14 Sep 2022 18:37:28 +0900 Subject: [PATCH 3/3] Complement #80, `JoinQjueryBuilder.getColumn()` --- package.json | 2 +- src/builders/JoinQueryBuilder.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f41a24..164c5a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-typeorm", - "version": "2.0.0-dev.20220913-5", + "version": "2.0.0", "description": "Make TypeORM much safer", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/builders/JoinQueryBuilder.ts b/src/builders/JoinQueryBuilder.ts index 544db81..2fa8a81 100644 --- a/src/builders/JoinQueryBuilder.ts +++ b/src/builders/JoinQueryBuilder.ts @@ -402,6 +402,19 @@ export class JoinQueryBuilder { condition(this.stmt_.query, ...args); } + public getColumn>( + field: Literal, + ): string { + const column: string = + ( + ReflectAdaptor.get( + this.mine_.prototype, + field, + ) as Belongs.ManyToOne.IMetadata + )?.foreign_key_field || field; + return `${this.alias_}.${column}`; + } + /* ----------------------------------------------------------- JOINERS ----------------------------------------------------------- */