Repository: IRCraziestTaxi/typeorm-linq-repository Branch: master Commit: 4ce43f2cdcc2 Files: 56 Total size: 144.3 KB Directory structure: gitextract_ckorfcxu/ ├── .gitignore ├── .typeorm/ │ ├── connection/ │ │ ├── get-migration-data-source.ts │ │ ├── get-typeorm-data-source.config.ts │ │ └── get-typeorm-data-source.function.ts │ ├── migrations/ │ │ └── 1629340508553-Initial.ts │ └── seed/ │ ├── functions/ │ │ ├── main.function.ts │ │ ├── seed-artists.function.ts │ │ ├── seed-genres.function.ts │ │ ├── seed-songs.function.ts │ │ ├── seed-user-profile-attributes.function.ts │ │ └── seed-users.function.ts │ └── index.ts ├── .vscode/ │ ├── launch.json │ └── settings.json ├── LICENSE ├── README.md ├── index.ts ├── ormconfig.example.json ├── package.json ├── src/ │ ├── constants/ │ │ └── SqlConstants.ts │ ├── enums/ │ │ ├── QueryMode.ts │ │ └── QueryWhereType.ts │ ├── query/ │ │ ├── Query.ts │ │ ├── QueryBuilderPart.ts │ │ └── interfaces/ │ │ ├── IComparableQuery.ts │ │ ├── IComparableQueryBase.ts │ │ ├── IJoinedComparableQuery.ts │ │ ├── IJoinedQuery.ts │ │ ├── IQuery.ts │ │ ├── IQueryBase.ts │ │ ├── IQueryBuilderPart.ts │ │ ├── IQueryInternal.ts │ │ ├── ISelectQuery.ts │ │ └── ISelectQueryInternal.ts │ ├── repository/ │ │ ├── LinqRepository.ts │ │ └── interfaces/ │ │ └── ILinqRepository.ts │ └── types/ │ ├── ComparableValue.ts │ ├── EntityBase.ts │ ├── EntityConstructor.ts │ ├── JoinedEntityType.ts │ ├── QueryConditionOptions.ts │ ├── QueryConditionOptionsInternal.ts │ ├── QueryOrderOptions.ts │ └── RepositoryOptions.ts ├── test/ │ ├── entities/ │ │ ├── artist.entity.ts │ │ ├── genre.entity.ts │ │ ├── song.entity.ts │ │ ├── user-profile-attribute.entity.ts │ │ └── user.entity.ts │ ├── jasmine-ts.helper.js │ ├── jasmine.json │ └── scenarios/ │ └── query/ │ └── query.spec.ts ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ **/node_modules/** **/*.log **/*.js !jasmine-ts.helper.js **/*.d.ts ormconfig.json .DS_Store ================================================ FILE: .typeorm/connection/get-migration-data-source.ts ================================================ import { DataSource } from "typeorm"; import { dataSourceOptions } from "./get-typeorm-data-source.config"; const dataSource = new DataSource(dataSourceOptions); dataSource.initialize(); export default dataSource; ================================================ FILE: .typeorm/connection/get-typeorm-data-source.config.ts ================================================ import { DataSourceOptions } from "typeorm"; import * as ormconfig from "../../ormconfig.json"; import { Artist } from "../../test/entities/artist.entity"; import { Genre } from "../../test/entities/genre.entity"; import { Song } from "../../test/entities/song.entity"; import { UserProfileAttribute } from "../../test/entities/user-profile-attribute.entity"; import { User } from "../../test/entities/user.entity"; export const dataSourceOptions: DataSourceOptions = { ...ormconfig as DataSourceOptions, entities: [ Artist, Genre, Song, UserProfileAttribute, User ] }; ================================================ FILE: .typeorm/connection/get-typeorm-data-source.function.ts ================================================ import { DataSource } from "typeorm"; import { dataSourceOptions } from "./get-typeorm-data-source.config"; export async function getTypeormDataSource(): Promise { const dataSource = new DataSource(dataSourceOptions); await dataSource.initialize(); return dataSource; } ================================================ FILE: .typeorm/migrations/1629340508553-Initial.ts ================================================ import {MigrationInterface, QueryRunner} from "typeorm"; export class Initial1629340508553 implements MigrationInterface { name = 'Initial1629340508553' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query("CREATE TABLE `genre` (`id` int NOT NULL, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB"); await queryRunner.query("CREATE TABLE `song` (`artistId` int NOT NULL, `genreId` int NOT NULL, `id` int NOT NULL, `name` varchar(100) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB"); await queryRunner.query("CREATE TABLE `artist` (`id` int NOT NULL, `name` varchar(100) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB"); await queryRunner.query("CREATE TABLE `user` (`id` int NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB"); await queryRunner.query("CREATE TABLE `user_profile_attribute` (`genreId` int NOT NULL, `id` int NOT NULL, `userId` int NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB"); await queryRunner.query("ALTER TABLE `song` ADD CONSTRAINT `FK_fe76da76684ccb3d70d0f75994e` FOREIGN KEY (`artistId`) REFERENCES `artist`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION"); await queryRunner.query("ALTER TABLE `song` ADD CONSTRAINT `FK_d9ffa20e72f9e6834680ead9fe4` FOREIGN KEY (`genreId`) REFERENCES `genre`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION"); await queryRunner.query("ALTER TABLE `user_profile_attribute` ADD CONSTRAINT `FK_2e584d105f300ed4f325ba0a43c` FOREIGN KEY (`genreId`) REFERENCES `genre`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION"); await queryRunner.query("ALTER TABLE `user_profile_attribute` ADD CONSTRAINT `FK_6689edd2d3aced2ac9510387a06` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION"); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query("ALTER TABLE `user_profile_attribute` DROP FOREIGN KEY `FK_6689edd2d3aced2ac9510387a06`"); await queryRunner.query("ALTER TABLE `user_profile_attribute` DROP FOREIGN KEY `FK_2e584d105f300ed4f325ba0a43c`"); await queryRunner.query("ALTER TABLE `song` DROP FOREIGN KEY `FK_d9ffa20e72f9e6834680ead9fe4`"); await queryRunner.query("ALTER TABLE `song` DROP FOREIGN KEY `FK_fe76da76684ccb3d70d0f75994e`"); await queryRunner.query("DROP TABLE `user_profile_attribute`"); await queryRunner.query("DROP TABLE `user`"); await queryRunner.query("DROP TABLE `artist`"); await queryRunner.query("DROP TABLE `song`"); await queryRunner.query("DROP TABLE `genre`"); } } ================================================ FILE: .typeorm/seed/functions/main.function.ts ================================================ import { getTypeormDataSource } from "../../connection/get-typeorm-data-source.function"; import { seedArtists } from "./seed-artists.function"; import { seedGenres } from "./seed-genres.function"; import { seedSongs } from "./seed-songs.function"; import { seedUserProfileAttributes } from "./seed-user-profile-attributes.function"; import { seedUsers } from "./seed-users.function"; export async function main(): Promise { console.log("Creating connection."); const dataSource = await getTypeormDataSource(); await seedGenres(); await seedArtists(); await seedSongs(); await seedUsers(); await seedUserProfileAttributes(); console.log("Closing connection."); await dataSource.destroy(); } ================================================ FILE: .typeorm/seed/functions/seed-artists.function.ts ================================================ import { LinqRepository } from "../../../src/repository/LinqRepository"; import { Artist } from "../../../test/entities/artist.entity"; export async function seedArtists(): Promise { console.log("Seeding artists."); const artistRepository = new LinqRepository(Artist); const artists: Artist[] = [ { id: 1, name: "Artist One" }, { id: 2, name: "Artist Two" }, { id: 3, name: "Artist Three" } ]; await artistRepository.typeormRepository.insert(artists); console.log("Done seeding artists."); } ================================================ FILE: .typeorm/seed/functions/seed-genres.function.ts ================================================ import { LinqRepository } from "../../../src/repository/LinqRepository"; import { Genre } from "../../../test/entities/genre.entity"; export async function seedGenres(): Promise { console.log("Seeding genres."); const genreRepository = new LinqRepository(Genre); const genres: Genre[] = [ { id: 1, name: "Rock" }, { id: 2, name: "Hip Hop" }, { id: 3, name: "Pop" } ]; await genreRepository.typeormRepository.insert(genres); console.log("Done seeding genres."); } ================================================ FILE: .typeorm/seed/functions/seed-songs.function.ts ================================================ import { LinqRepository } from "../../../src/repository/LinqRepository"; import { Song } from "../../../test/entities/song.entity"; export async function seedSongs(): Promise { console.log("Seeding songs."); const songRepository = new LinqRepository(Song); const songs: Song[] = [ { artistId: 1, genreId: 1, id: 1, name: "Rock Song" }, { artistId: 1, genreId: 2, id: 2, name: "Hip Hop Song" }, { artistId: 1, genreId: 3, id: 3, name: "Pop Song" }, { artistId: 2, genreId: 1, id: 4, name: "Rock Song" }, { artistId: 2, genreId: 2, id: 5, name: "Hip Hop Song" }, { artistId: 3, genreId: 1, id: 6, name: "Rock Song" }, { artistId: 3, genreId: 3, id: 7, name: "Pop Song" } ]; await songRepository.typeormRepository.insert(songs); console.log("Done seeding songs."); } ================================================ FILE: .typeorm/seed/functions/seed-user-profile-attributes.function.ts ================================================ import { LinqRepository } from "../../../src/repository/LinqRepository"; import { UserProfileAttribute } from "../../../test/entities/user-profile-attribute.entity"; export async function seedUserProfileAttributes(): Promise { console.log("Seeding user profile attributes."); const userProfileAttributeRepository = new LinqRepository(UserProfileAttribute); const userProfileAttributes: UserProfileAttribute[] = [ { genreId: 3, id: 1, userId: 1 } ]; await userProfileAttributeRepository.typeormRepository.insert(userProfileAttributes); console.log("Done seeding user profile attributes."); } ================================================ FILE: .typeorm/seed/functions/seed-users.function.ts ================================================ import { LinqRepository } from "../../../src/repository/LinqRepository"; import { User } from "../../../test/entities/user.entity"; export async function seedUsers(): Promise { console.log("Seeding users."); const userRepository = new LinqRepository(User); const users: User[] = [ { id: 1 } ]; await userRepository.typeormRepository.insert(users); console.log("Done seeding users."); } ================================================ FILE: .typeorm/seed/index.ts ================================================ import { main } from "./functions/main.function"; main() .then(() => { console.log("Done."); }) .catch(error => { console.error("Error seeding database:"); console.error(error); }); ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug Spec", "program": "${workspaceFolder}/node_modules/.bin/jasmine", "args": [ "--config=./test/jasmine.json" ] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "files.exclude": { "**/*.d.ts": true, "src/**/*.js": true, "index.js": true }, "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 IRCraziestTaxi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # typeorm-linq-repository Wraps TypeORM repository pattern and QueryBuilder using fluent, LINQ-style queries. ## What's New Two new libraries, [typeorm-linq-repository-testing](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing) and [typeorm-linq-repository-testing-nestjs](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing-nestjs), now make it easier to unit test `LinqRepository`. [typeorm-linq-repository-testing-nestjs](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing-nestjs) contains a more "complete" example of usage since it has more practical usage in the context of another framework, but [typeorm-linq-repository-testing](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing) provides the raw components if you need to build something similar for another framework. ### Latest Changes As of version 2.0.0: Due to [breaking changes in TypeORM](https://github.com/typeorm/typeorm/issues/7428), namely the removal of `getConnectionManager`, the constructor for `LinqRepository` now requires the TypeORM `DataSource` in addition to the entity model. Likewise, since individually managed `DataSource` objects replace the `ConnectionManager` paradigm, `RepositoryOptions` now no longer contains the `connectionName` property. ### Older Changes As of version 1.1.3: A fix was implemented so that if a `where` with mapped properties was called that was not in the following format (without parentheses): ```ts .where(e => e.relationshipOne.map(r => r.relationshipTwo)) ``` an error would occur. Now, the following is allowed (with parentheses): ```ts .where((e) => e.relationshipOne.map((r) => r.relationshipTwo)) ``` As of version 1.1.2: A fix was implemented to make `from`, `inSelected`, and `notInSelected` compatible with entities that do not contain a property named `id` that is a `number`. As of version 1.1.0: A fix was implemented in which entities not implementing a property named `id` were not compatible with `LinqRepository`. To mitigate this, `id` was removed from the base `EntityBase` type. In addition: * `RepositoryOptions` now allows you to specify the name of the entity's primary key in case it is not `id` so that `create` may still be used with the option `autoGenerateId` enabled and `getById` may be used. To do so: ```ts new LinqRepository(Entity, { primaryKey: e => e.entityId }); ``` As of version 1.0.1: A fix/improvement was implemented in which `include`d or `thenInclude`d relations may now be filtered by later using `join` or `thenJoin` along with a `where`. See the Filtering Included Relations section below. As of version 1.0.0: * The `update` method is now an alias for the new `upsert` method. This change was made to clarify that typeorm-linq-repository calls TypeORM's `save` method, which performs upserts on the provided entities. The `update` method was left in place to avoid breaking changes. * The `ts-simple-nameof` dependency was updated to strip assertion operators from parsed property names in order to allow the following: ```ts await fooRepository .getAll() .where(foo => foo.bar!.baz) .equal(value); ``` which is sometimes necessary when using strict null checks, for instance when a relationship is typed as optional/nullable. * `LinqRepository` now exposes a `typeormRepository` property, which allows you to use the underlying TypeORM Repository if you need to access methods not available via the `createQueryBuilder` method. The `createQueryBuilder` method, although now redundant, was left in place to avoid breaking changes. In version 1.0.0-alpha.23, a bug was fixed in which a call to the `where` method on a non-joined query with multiple joins in the property selector (i.e. `.where(p => p.comments.map(c => c.user.email))`) would use the wrong alias and throw an error. As of version 1.0.0-alpha.22: * Checking for existence or absence of relations in an array of relations (or existence or absense of relations that meet a certain condition) is now supported! For example: ```ts const accessiblePosts = await postRepository // Get posts where... .getAll() // Note: Must use groupBy method to check relations. .groupBy(p => p.id) // ...no tags exist... .whereNone(p => p.tags, t => t.id) // ...or the post contains the tag being searched for. .orAny(p => p.tags, t => t.id, t => t.id) .equal(tagId); ``` Notice the second argument in the `whereNone` and `orAny` methods. This argument simply serves as an arbitrary primitive property to `COUNT` relationships in the `HAVING` statement(s) resulting from those methods. This was done in order to not restrict entities to have a primary key named `id`; although that restriction would have conveniently shortened the signature of the method, not all schemas may name primary keys `id`. See the Checking Relations section below. * A bug was fixed in usage of the `where` method following the `include` or `thenInclude` methods. Previously, although the interface claimed that a `where` method following an `include` method operated on the query's base type, the query actually continued using the last included property type. ## Foreword This is a work in progress. This project has just recently come out of alpha and should be treated as such. That being said, it has received some massive updates for a wide range of query complexity and is periodically being updated for bug fixes, so I hope it will continue to see lots of use and continue to mature. `typeorm-linq-repository`'s queries handle simple includes, joins, and join conditions very well and now has the capability to take on more complex queries. The only way it will continue to mature is to have its limits tested see some issues and pull requests come in. `typeorm-linq-repository` has been tested with Postgres and MySQL, but since TypeORM manages the ubiquity of queries amongst different database engines, it should work just fine with all database engines. Please feel free to give it a try and provide as much testing as possible for it! ## Prerequisites [TypeORM](https://github.com/typeorm/typeorm "TypeORM"), a code-first relational database ORM for typescript, is the foundation of this project. If you are unfamiliar with TypeORM, I strongly suggest that you check it out. ## Installation To add `typeorm-linq-repository` and its dependencies to your project using NPM: ``` npm install --save typeorm typeorm-linq-repository ``` ## Linq Repository `LinqRepository` is the repository that is constructed to interact with the table represented by the entity used as the type argument for the repository. `LinqRepository` takes the TypeORM `DataSource` containing the connection to the entity's datbase and a class type representing a TypeORM model as its constructor argument. ```ts import { LinqRepository } from "typeorm-linq-repository"; import { dataSource } from "path-to-initialized-data-source"; import { User } from "../../entities/User"; const userRepository: LinqRepository = new LinqRepository(dataSource, User); ``` ### Repository Options To modify default behavior when setting up a repository, use `RepositoryOptions`. Repository options include: - `autoGenerateId`: A boolean value indicating whether the entity implements a primary key that is auto-generated. Default is `true`. - `primaryKey`: A lambda function providing the entity's primary key property if it is not named `id`. ```ts new LinqRepository(dataSource, Entity, { // This entity has a primary key that is not auto-generated. autoGenerateId: false, // This entity has a primary key whose name is not "id". primaryKey: e => e.entityId }); ``` Or as a repository extending `LinqRepository`: ```ts import { DataSource } from "typeorm"; import { LinqRepository } from "typeorm-linq-repository"; import { Entity } from "../entities/Entity"; export class EntityRepository extends LinqRepository { public constructor(dataSource: DataSource) { super(dataSource, Entity, { // This entity has a primary key that is not auto-generated. autoGenerateId: false, // This entity has a primary key whose name is not "id". primaryKey: e => e.entityId }); } } ``` ### Injecting LinqRepository Protip: You can easily make `LinqRepository` injectable! For example, using InversifyJS: ```ts import { decorate, injectable, unmanaged } from "inversify"; import { LinqRepository } from "typeorm-linq-repository"; decorate(injectable(), LinqRepository); decorate(unmanaged(), LinqRepository, 0); decorate(unmanaged(), LinqRepository, 1); export { LinqRepository }; ``` ### Injecting LinqRepository with NestJS When creating injectable repositories extending `LinqRepository` in NestJS, you must use `@nestjs/typeorm`'s `InjectDataSource` decorator to inject a data source into your repository's constructor. Doing so forces Nest to wait until the TypeORM connection is established before continuing to construct the repository. If you do not use `InjectDataSource`, you will encounter errors because `LinqRepository` will try to get the entity's repository from the connection before it is established. Here is an example of an injectable repository in NestJS: ```ts import { Injectable } from "@nestjs/common"; import { InjectDataSource } from "@nestjs/typeorm"; import { DataSource } from "typeorm"; import { LinqRepository } from "typeorm-linq-repository"; import { Entity } from "./entity.entity"; @Injectable() export class EntityRepository extends LinqRepository { // NOTE: @InjectDataSource is required to force Nest to wait for the TypeORM connection to be established // before typeorm-linq-repository's LinqRepository attempts to get the repository from the connection. public constructor( @InjectDataSource(/* "data-source-name" or empty for "default" */) dataSource: DataSource ) { super(dataSource, Entity); } } ``` You can see a working example of injecting repositories extending `LinqRepository` in NestJS [in this repository](https://github.com/IRCraziestTaxi/responsekit-nestjs-demo). ## Using Queries `typeorm-linq-repository` not only makes setting up repositories incredibly easy; it also gives you powerful, LINQ-style query syntax. ### Retrieving Entities You can query entities for all, many, or one result: ```ts // Gets all entities. this._userRepository.getAll(); // Gets many entities. this._userRepository .getAll() .where(u => u.admin) .isTrue(); // Gets one entity. this._userRepository .getOne() .where(u => u.email) .equal(email); // Finds one entity using its ID. this._userRepository.getById(id); ``` ### Counting Results You may call `count()` on a query to get the count of records matching the current query conditions without killing the query as awaiting or calling `.then()` on the query otherwise would; this way, you can use a query to count all records matching a set of conditions and then set paging parameters on the same query. For example: ```ts let activeUserQuery = this._userRepository .getAll() .where(u => u.active) .isTrue(); // Count all active users. const activeUserCount = await activeUserQuery.count(); // Set paging parameters on the query. activeUserQuery = activeUserQuery .skip(skip) .take(take); const pagedActiveUsers = await activeUserQuery; ``` ### Type Safe Querying This LINQ-style querying really shines by giving you type-safe includes, joins, and where statements, eliminating the need for hard-coded property names in query functions. This includes conditional statements: ```ts this._userRepository .getOne() .where(u => u.email) .equal(email); ``` As well as include statements: ```ts this._userRepository .getById(id) .include(u => u.posts); ``` If the property `posts` ever changes, you get compile-time errors, ensuring the change is not overlooked in query statements. ### Multiple Includes You can use `include()` more than once to include several properties on the query's base type: ```ts this._userRepository .getById(id) .include(u => u.posts) .include(u => u.orders); ``` ### Subsequent Includes and Current Property Type Include statements transform the "current property type" on the Query so that subsequent `thenInclude()`s can be executed while maintaining this type safety. ```ts this._userRepository .getById(id) .include(u => u.orders) .thenInclude(o => o.items); ``` ```ts this._userRepository .getById(id) .include(u => u.posts) .thenInclude(p => p.comments) .thenInclude(c => c.user); ``` You can use `include()` or `thenInclude()` on the same property more than once to subsequently include another relation without duplicating the include in the executed query. ```ts this._userRepository .getById(id) .include(u => u.posts) .thenInclude(p => p.comments) .include(u => u.posts) .thenInclude(p => p.subscribedUsers); ``` ### Filtering Results Queries can be filtered on one or more conditions using `where()`, `and()`, and `or()`. Note that, just as with TypeORM's QueryBuilder, using `where()` more than once will overwrite previous `where()`s, so use `and()` and `or()` to add more conditions. ```ts this._userRepository .getAll() .where(u => u.isActive) .isTrue() .and(u => u.lastLogin) .greaterThan(date); ``` Note also that this caveat only applies to "normal" where conditions; a where condition on a join is local to that join and does not affect any "normal" where conditions on a query. ```ts this._postRepository .getAll() .join((p: Post) => p.user) .where((u: User) => u.id) .equal(id) .where((p: Post) => p.archived) .isTrue(); ``` ### Filtering Included Relations To filter `include`d or `thenInclude`d relationships (which is not possible by using `.include(...).where(...)` since using `where` after `include` resets the query rather than performing a `where` on the `include`), use `join` or `thenJoin` after the `include` or `thenInclude`. ```ts this._postRepository .getAll() .include(p => p.comments) // We want to exclude included comments based on conditions on replies // while not filtering any posts. .thenInclude(c => c.replies) // Therefore, use a joinAlso() here to maintain a LEFT JOIN on comments .joinAlso(p => p.comments) // but use a thenJoin() here to restrict comments and replies // to an INNER JOIN based on conditions. .thenJoin(c => c.replies) .where(r => r.user.email) .equal(filterEmail); ``` ### Joined Properties in Comparisons It is possible to join relationships on the fly during a conditional clause in order to compare a relationship's value. ```ts this._postRepository .getAll() .where(p => p.date) .greaterThan(date) .and(p => p.user.id) .equal(userId); ``` In order to join through collections in this fashion, use the `Array.map()` method. ```ts this._userRepository .getAll() .where(u => u.posts.map(p => p.comments.map(c => c.flagged))) .isTrue(); ``` Note: If not already joined via one of the available `join` or `include` methods, relationships joined in this fashion will be joined as follows: * `where()` and `and()` result in an `INNER JOIN`. * `or()` results in a `LEFT JOIN`. ### Grouped (Bracketed) Conditional Clauses In order to group conditional clauses into parentheses, use `isolatedWhere()`, `isolatedAnd()`, and `isolatedOr()`. ```ts this._userRepository .getOne() .where(u => u.isAdmin) .isTrue() .isolatedOr(q => q .where(u => u.firstName) .equals("John") .and(u => u.lastName) .equals("Doe") ).isolatedOr(q => q .where(u => u.firstName) .equals("Jane") .and(u => u.lastName) .equals("Doe") ); ``` ### Comparing Basic Values The following query conditions are available for basic comparisons: `beginsWith(value: string)`: Finds results where the queried text begins with the supplied string. `contains(value: string)`: Finds results were the queried text contains the supplied string. `endsWith(value: string)`: Finds results where the queried text ends with the supplied string. `equal(value: string | number | boolean)`: Finds results where the queried value is equal to the supplied value. `greaterThan(value: number)`: Finds results where the queried value is greater than the supplied number. `greaterThanOrEqual(value: number)`: Finds results where the queried value is greater than or equal to the supplied number. `in(include: string[] | number[])`: Finds results where the queried value intersects the specified array of values to include. `isFalse()`: Finds results where the queried boolean value is false. `isNotNull()`: Finds results where the queried relation is not null. `isNull()`: Finds results where the queried relation is null. `isTrue()`: Finds results where the queried boolean value is true. `lessThan(value: number)`: Finds results where the queried value is less than the supplied number. `lessThanOrEqual(value: number)`: Finds results where the queried value is less than or equal to the supplied number. `notEqual(value: string | number | boolean)`: Finds results where the queried value is not equal to the supplied value. `notIn(exclude: string[] | number[])`: Finds results where the queried value intersects the specified array of values to exclude. `inSelected()` and `notInSelected()` are also available and are covered later in this guide. ### String Comparison When comparing strings, the default behavior is to not match case (case-insensitive comparison). If a case-sensitive comparison is desired, use the `matchCase` option when executing a comparison. ```ts // Perform a case-sensitive comparison rather than the default case-insensitive. equal(value, { matchCase: true }); ``` Note that, due to a lack of type reflection in JavaScript, the opposite is true for comparing values with joined entities. See the Comparing Values With Joined Entities section below. ### Inner Joins Filter joined relations by using `where()`, `and()`, and `or()` on inner joins using `join()` and `thenJoin()`. ```ts this._userRepository .getAll() .join(u => u.posts) .where(p => p.archived) .isTrue(); this._userRepository .getOne() .join(u => u.posts) .where(p => p.flagged) .isTrue() .and(p => p.date) .greaterThan(date); ``` Just as with `include()` and `thenInclude()`, `join()` always uses the query's base type, while `thenJoin()` continues to use the last joined entity's type. ```ts this._postRepository .getAll() .join(p => p.user) .where(u => u.id) .equal(id) .thenJoin(u => u.comments) .where(c => c.flagged) .isTrue() .join(p => p.comments) .thenJoin(c => c.user) .where(u => u.dateOfBirth) .lessThan(date); ``` ### Left Joins As the above `join()` and `thenJoin()` perform an `INNER JOIN`, desired results may be lost if you wish to not exclude previously included results if the joined relations fail the join condition. Filter joined relations while not excluding previously included results by using `joinAlso()` and `thenJoinAlso()` to perform a `LEFT JOIN` instead. ### Regarding Included Relationships Note that `.include()` and `.thenInclude()` are not intended to work the same way as `.join()`, `.joinAlso()`, `.thenJoin()`, and `.thenJoinAlso()` in conjunction with `.where()`. That is, using `.include().where()` does NOT behave the same way as `.join().where()` (interpreted in "plain English" as "join where"). `.include()` and `.thenInclude()` were meant to stand alone in their own context rather than filtering the main entity based on joined relationships. For example: ```ts this._userRepository .getMany() // I want to include posts in my results, but I am filtering included posts without filtering user results. .include(u => u.posts) // However, I am also filtering on the user itself (.where() after .include() filters on the base type). .where(u => u.active) .isTrue(); ``` On the other hand, if you do intend to include a relationship while also filtering results based on a condition on that included relationship, use `.include()` in conjunction with `.join().where()`, i.e.: ```ts this._userRepository .getMany() // Include posts in results. .include(u => u.posts) // Use .join rather than .joinAlso to actually filter user results by post criteria. .join(u => u.posts) .where(p => p.archived) .isTrue(); ``` Finally, if you intend to include a relationship while filtering those included relationships but not filtering out any entities of the base type, then use `.joinAlso().where()` in order to perform a `LEFT JOIN` as opposed to an `INNER JOIN`. ```ts this._userRepository .getMany() // Include posts in results. .include(u => u.posts) // Use .joinAlso rather than .join to perform a left join to filter posts but not filter users. .joinAlso(u => u.posts) .where(p => p.archived) .isTrue(); ``` ### Joining Foreign Entities Join from an unrelated entity using `from()`. A simple example of this is not easily provided, so see examples below for further guidance on using this method. ```ts this._songRepository .getAll() .join(s => s.artist) .where(a => a.id) .equal(artistId) .from(UserProfileAttribute) .thenJoin(p => p.genre) // ... ``` ### Comparing Values With Joined Entities Perform comparisons with values on joined entities by calling `from()`, `join()`, and `thenJoin()` after calling `where()`, `and()`, or `or()`. ```ts this._userRepository .getAll() .join(u => u.posts) .where(p => p.recordLikeCount) .thenJoin(p => p.category) .greaterThanJoined(c => c.averageLikeCount); ``` The following query conditions are available for comparisons on related entities' properties: `equalJoined(selector: (obj: P) => any)`: Determines whether the property specified in the last "where" is equal to the specified property on the last joined entity. `greaterThanJoined(selector: (obj: P) => any)`: Determines whether the property specified in the last "where" is less than the specified property on the last joined entity. `greaterThanOrEqualJoined(selector: (obj: P) => any)`: Determines whether the property specified in the last "where" is greater than or equal to the specified property on the last joined entity. `lessThanJoined(selector: (obj: P) => any)`: Determines whether the property specified in the last "where" is less than the specified property on the last joined entity. `lessThanOrEqualJoined(selector: (obj: P) => any)`: Determines whether the property specified in the last "where" is less than or equal to the specified property on the last joined entity. `notEqualJoined(selector: (obj: P) => any)`: Determines whether the property specified in the last "where" is not equal to the specified property on the last joined entity. ### String Comparison When Comparing Values With Joined Entities Note that although non-joined string comparisons defaults to case-insensitive comparison, due to a lack of type reflection in JavaScript, the opposite is true for comparing values with joined entities. Therefore, the default behavior when using the above methods is to perform a case sensitive comparison, so you must specify `matchCase: false` when using the above methods if you wish to perform a case-insensitive comparison. ```ts // Perform a case-insensitive comparison rather than the default case-sensitive when comparing joined entity's properties. equalJoined(x => x.property, { matchCase: false }); ``` ### Checking Relations It is possible to check for existence or absence of relations in an array of relations (or existence or absense of relations that meet a certain condition). For example: ```ts const accessiblePosts = await postRepository // Get posts where... .getAll() // Note: Must use groupBy method to check relations. .groupBy(p => p.id) // ...no tags exist (meaning the post is not restricted to a certain tag)... .whereNone(p => p.tags, t => t.id) // ...or the post contains the tag being searched for. .orAny(p => p.tags, t => t.id, t => t.id) .equal(tagId); ``` NOTE: As the underlying query executes methods that check relations as `HAVING COUNT(...)`, you MUST use the `groupBy` method to group results on an arbitrary primitive property of the query's base type; for instance, the primary key. The following relation checking methods are available: `whereAny`: Checks for existence of the specified relations; optionally checks for existence of relations meeting a criteria determined by the optional `conditionPropSelector` argument in conjunction with the following comparing method. `whereNone`: Checks for absence of the specified relations; optionally checks for absence of relations meeting a criteria determined by the optional `conditionPropSelector` argument in conjunction with the following comparing method. `andAny`: The same as `whereAny` but performed as `AND COUNT(...) > 0` (supplementing the initial `HAVING COUNT(...)`). `andNone`: The same as `whereNone` but performed as `AND COUNT(...) = 0` (supplementing the initial `HAVING COUNT(...)`). `orAny`: The same as `whereAny` but performed as `OR COUNT(...) > 0` (supplementing the initial `HAVING COUNT(...)`). `orNone`: The same as `whereNone` but performed as `OR COUNT(...) = 0` (supplementing the initial `HAVING COUNT(...)`). ### Including or Excluding Results Within an Inner Query To utilize an inner query, use the `inSelected()` and `notInSelected()` methods. Each takes an inner `ISelectQuery`, which is obtained by calling `select()` on the inner query after its construction and simply specifies which value to select from the inner query to project to the `IN` or `NOT IN` list. The following example is overkill since, in reality, you would simply add the condition that the post is not archived on the main query, but consider what is going on within the queries in order to visualize how inner queries in `typeorm-linq-repository` work. Consider a `PostRepository` from which we want to get all posts belonging to a certain user and only those that are not archived. The outer query in this instance gets all posts belonging to the specified user, while the inner query specified all posts that are not archived. The union of the two produces the results we want. ```ts this._postRepository .getAll() .join(p => p.user) .where(u => u.id) .equal(id) .where(p => p.id) .inSelected( this._postRepository .getAll() .where(p => p.archived) .isFalse() .select(p => p.id) ); ``` This next example is more representative of an actual situation in which an inner query is useful. Consider an application in which users set up a profile and add Profile Attributes which specify genres of songs they do NOT wish to hear; that is, the application would avoid songs with genres specified by the user's profile. Given the following models: `Artist.ts` ```ts import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Song } from "./Song"; @Entity() export class Artist { @PrimaryGeneratedColumn() public id: number; @Column({ nullable: false }) public name: string; @OneToMany(() => Song, (song: Song) => song.artist) public songs: Song[]; } ``` `Genre.ts` ```ts import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { SongGenre } from "./SongGenre"; @Entity() export class Genre { @PrimaryGeneratedColumn() public id: number; @Column({ nullable: false }) public name: string; @OneToMany(() => SongGenre, (songGenre: SongGenre) => songGenre.genre) public songs: SongGenre[]; } ``` `Song.ts` ```ts import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Artist } from "./Artist"; import { SongGenre } from "./SongGenre"; @Entity() export class Song { @ManyToOne(() => Artist, (artist: Artist) => artist.songs) public artist: Artist; @OneToMany(() => SongGenre, (songGenre: SongGenre) => songGenre.song) public genres: SongGenre[]; @PrimaryGeneratedColumn() public id: number; @Column({ nullable: false }) public name: string; } ``` `SongGenre.ts` ```ts import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import { Genre } from "./Genre"; import { Song } from "./Song"; /** * Links a song to a genre. */ @Entity() export class SongGenre { @ManyToOne(() => Genre, (genre: Genre) => genre.songs) public genre: Genre; @PrimaryGeneratedColumn() public id: number; @ManyToOne(() => Song, (song: Song) => song.genres) public song: Song; } ``` `User.ts` ```ts import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { UserProfileAttribute } from "./UserProfileAttribute"; @Entity() export class User { @Column({ nullable: false }) public email: string; @PrimaryGeneratedColumn() public id: number; @Column({ nullable: false }) public password: string; @OneToMany(() => UserProfileAttribute, (profileAttribute: UserProfileAttribute) => profileAttribute.user) public profile: UserProfileAttribute[]; } ``` `UserProfileAttribute.ts` ```ts import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import { Genre } from "./Genre"; import { User } from "./User"; /** * An attribute of a user's profile specifying a genre that user does not wish to hear. */ @Entity() export class UserProfileAttribute { @ManyToOne(() => Genre) public genre: Genre; @PrimaryGeneratedColumn() public id: number; @ManyToOne(() => User, (user: User) => user.profile) public user: User; } ``` Now, consider the following query from which we want to gather all songs by a certain artist that a certain user wants to hear; that is, songs by that artist that do not match a genre blocked by the user's profile. ```ts this._songRepository .getAll() .join(s => s.artist) .where(a => a.id) .equal(artistId) .where(s => s.id) .notInSelected( this._songRepository .getAll() .join(s => s.artist) .where(a => a.id) .equal(artistId) .from(UserProfileAttribute) .thenJoin(p => p.genre) .where(g => g.id) .join(s => s.songGenre) .thenJoin(sg => sg.genre) .equalJoined(g => g.id) .from(UserProfileAttribute) .thenJoin(p => p.user) .where(u => u.id) .equal(userId) .select(s => s.id) ); ``` ### Selection Type Calling `select()` after completing any comparison operations uses the query's base type. If you wish to select a property from a relation rather than the query's base type, you may call `select()` after one or more joins on the query. ```ts this._songRepository .getAll() .join(s => s.genres) .thenJoin(sg => sg.genre) .select(g => g.id); ``` ### Ordering Queries You can order queries in either direction and using as many subsequent order statements as needed. ```ts this._userRepository .getAll() .orderBy(u => u.lastName) .thenBy(u => u.firstName); ``` You can use include statements to change the query's property type and order on properties of that child. ```ts this._userRepository .getAll() .orderByDescending(u => u.email) .include(u => u.posts) .thenByDescending(p => p.date); ``` ### Grouping Results You can group results by one or more properties using `groupBy` and `thenGroupBy`. ```ts this._userRepository .getAll() .groupBy(u => u.lastName) .thenGroupBy(u => u.firstName); ``` ### Using Query Results Queries are transformed into promises whenever you are ready to consume the results. Queries can be returned as raw promises: ```ts this._userRepository .getById(id) .toPromise(); ``` Or invoked as a promise on the spot: ```ts this._userRepository .getById(id) .then(user => { // ... }); ``` Or, using ES6 async syntax: ```ts const user = await this._userRepository.getById(user); ``` ### Using TypeORM's Query Builder If you encounter an issue or a query which this query wrapper cannot accommodate, you can use TypeORM's native QueryBuilder. ```ts this._userRepository.createQueryBuilder("user"); ``` ## Persisting Entities The following methods persist and remove entities from the database: ```ts // Creates one or more entities. create(entities: T | T[]): Promise; // Deletes one or more entities by reference or one entity by ID. delete(entities: number | string | T | T[]): Promise; // Updates one or more entities. update(entities: T | T[]): Promise; ``` ## Transaction support This library was unfortunately developed without regard to transactions, but another library called [typeorm-transactional-cls-hooked](https://github.com/odavid/typeorm-transactional-cls-hooked) makes utilizing transations extremely easy! To use this library in conjuntion with `typeorm-linq-repository`, install `typeorm-transactional-cls-hooked` along with its dependencies: ``` npm install --save typeorm-transactional-cls-hooked cls-hooked npm install --save-dev @types/cls-hooked ``` Then, per `typeorm-transactional-cls-hooked`'s documentation, simply patch TypeORM's repository with `typeorm-transactional-cls-hooked`'s base repository when bootstrapping your app: ```ts import { initializeTransactionalContext, patchTypeORMRepositoryWithBaseRepository } from "typeorm-transactional-cls-hooked"; // Initialize cls-hooked. initializeTransactionalContext(); // Patch TypeORM's Repository with typeorm-transactional-cls-hooked's BaseRepository. patchTypeORMRepositoryWithBaseRepository(); ``` That's it! Now all you need to do is use `typeorm-transactional-cls-hooked`'s `@Transactional()` decorator on methods that persist entities to your repositories. See `typeorm-transactional-cls-hooked`'s docs for more details. ### Unit testing Two libraries, [typeorm-linq-repository-testing](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing) and [typeorm-linq-repository-testing-nestjs](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing-nestjs), now make it easier to unit test `LinqRepository`. [typeorm-linq-repository-testing-nestjs](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing-nestjs) contains a more "complete" example of usage since it has more practical usage in the context of another framework, but [typeorm-linq-repository-testing](https://github.com/IRCraziestTaxi/typeorm-linq-repository-testing) provides the raw components if you need to build something similar for another framework. ================================================ FILE: index.ts ================================================ export { IComparableQuery } from "./src/query/interfaces/IComparableQuery"; export { IJoinedComparableQuery } from "./src/query/interfaces/IJoinedComparableQuery"; export { IJoinedQuery } from "./src/query/interfaces/IJoinedQuery"; export { IQuery } from "./src/query/interfaces/IQuery"; export { ISelectQuery } from "./src/query/interfaces/ISelectQuery"; export { ILinqRepository } from "./src/repository/interfaces/ILinqRepository"; export { LinqRepository } from "./src/repository/LinqRepository"; export { RepositoryOptions } from "./src/types/RepositoryOptions"; ================================================ FILE: ormconfig.example.json ================================================ { "database": "typeorm-linq-repository-test", "host": "localhost", "migrations": [ ".typeorm/migrations/*.ts" ], "password": "root", "port": 3306, "type": "mysql", "username": "root" } ================================================ FILE: package.json ================================================ { "name": "typeorm-linq-repository", "version": "2.0.2", "description": "Wraps TypeORM repository pattern and QueryBuilder using fluent, LINQ-style queries.", "main": "index.js", "scripts": { "db:seed": "ts-node ./.typeorm/seed/index.ts", "mig:make": "npm run typeorm:registered -- migration:generate -d \"./.typeorm/connection/get-migration-data-source.ts\"", "mig:revert": "npm run typeorm:registered -- migration:revert -d \"./.typeorm/connection/get-migration-data-source.ts\"", "mig:run": "npm run typeorm:registered -- migration:run -d \"./.typeorm/connection/get-migration-data-source.ts\"", "test": "jasmine --config=\"./test/jasmine.json\"", "tsc": "node_modules/.bin/tsc -d --project \"./tsconfig.build.json\"", "tslint": "node_modules/.bin/tslint --project .", "typeorm:registered": "ts-node ./node_modules/typeorm/cli.js" }, "repository": { "type": "git", "url": "git+https://github.com/IRCraziestTaxi/typeorm-linq-repository.git" }, "keywords": [ "typeorm", "repository", "linq", "query" ], "author": "IRCraziestTaxi", "license": "MIT", "bugs": { "url": "https://github.com/IRCraziestTaxi/typeorm-linq-repository/issues" }, "homepage": "https://github.com/IRCraziestTaxi/typeorm-linq-repository#readme", "dependencies": { "ts-simple-nameof": "^1.3.1" }, "devDependencies": { "@types/jasmine": "^5.1.4", "@types/node": "^20.11.0", "jasmine": "^5.1.0", "mysql": "^2.18.1", "ts-node": "^10.9.2", "tslint": "6.1.3", "typeorm": "^0.3.19", "typescript": "^5.3.3" }, "peerDependencies": { "typeorm": "^0.3.19" }, "files": [ "package.json", "LICENSE", "index.js", "index.d.ts", "src/**/*.js", "src/**/*.d.ts" ] } ================================================ FILE: src/constants/SqlConstants.ts ================================================ export class SqlConstants { public static readonly OPERATOR_AND: "AND" = "AND"; public static readonly OPERATOR_EQUAL: "=" = "="; public static readonly OPERATOR_GREATER: ">" = ">"; public static readonly OPERATOR_GREATER_EQUAL: ">=" = ">="; public static readonly OPERATOR_IN: "IN" = "IN"; public static readonly OPERATOR_IS: "IS" = "IS"; public static readonly OPERATOR_LESS: "<" = "<"; public static readonly OPERATOR_LESS_EQUAL: "<=" = "<="; public static readonly OPERATOR_LIKE: "LIKE" = "LIKE"; public static readonly OPERATOR_NOT_EQUAL: "!=" = "!="; public static readonly OPERATOR_NOT_IN: "NOT IN" = "NOT IN"; public static readonly OPERATOR_NOT_NULL: "NOT NULL" = "NOT NULL"; public static readonly OPERATOR_NULL: "NULL" = "NULL"; public static readonly OPERATOR_OR: "OR" = "OR"; } ================================================ FILE: src/enums/QueryMode.ts ================================================ export enum QueryMode { /** * The default mode of a query in which results are returned. */ Get = 0, /** * The mode of a query in which a relation is joined or included. */ Join = 1, /** * The mode of a query in which a comparison is being made. */ Compare = 2 } ================================================ FILE: src/enums/QueryWhereType.ts ================================================ export enum QueryWhereType { /** * A normal comparison (not on a joined entity). */ Normal = 0, /** * A comparison involving a joined entity. */ Joined = 1 } ================================================ FILE: src/query/Query.ts ================================================ import { nameof } from "ts-simple-nameof"; import { Brackets, ObjectLiteral, SelectQueryBuilder, WhereExpression } from "typeorm"; import { SqlConstants } from "../constants/SqlConstants"; import { QueryMode } from "../enums/QueryMode"; import { QueryWhereType } from "../enums/QueryWhereType"; import { ComparableValue } from "../types/ComparableValue"; import { EntityBase } from "../types/EntityBase"; import { JoinedEntityType } from "../types/JoinedEntityType"; import { QueryConditionOptions } from "../types/QueryConditionOptions"; import { QueryConditionOptionsInternal } from "../types/QueryConditionOptionsInternal"; import { QueryOrderOptions } from "../types/QueryOrderOptions"; import { IComparableQuery } from "./interfaces/IComparableQuery"; import { IJoinedComparableQuery } from "./interfaces/IJoinedComparableQuery"; import { IJoinedQuery } from "./interfaces/IJoinedQuery"; import { IQuery } from "./interfaces/IQuery"; import { IQueryBuilderPart } from "./interfaces/IQueryBuilderPart"; import { IQueryInternal } from "./interfaces/IQueryInternal"; import { ISelectQuery } from "./interfaces/ISelectQuery"; import { ISelectQueryInternal } from "./interfaces/ISelectQueryInternal"; import { QueryBuilderPart } from "./QueryBuilderPart"; export class Query implements IQuery, IJoinedQuery, IComparableQuery, IJoinedComparableQuery, IQueryInternal, ISelectQueryInternal { private readonly _duplicateAliasHistory: string[]; private readonly _getAction: () => Promise; private readonly _includeAliasHistory: string[]; private readonly _initialAlias: string; private readonly _query: SelectQueryBuilder; private readonly _queryParts: IQueryBuilderPart[]; private _lastAlias: string; private _queryMode: QueryMode; private _queryWhereType: QueryWhereType; private _selectedProperty: string; /** * Constructs a Query wrapper. * @param queryBuilder The QueryBuilder to wrap. * @param getAction Either queryBuilder.getOne or queryBuilder.getMany. */ public constructor( queryBuilder: SelectQueryBuilder, getAction: () => Promise, includeAliasHistory: string[] = [] ) { this._duplicateAliasHistory = []; this._getAction = getAction; this._includeAliasHistory = includeAliasHistory; this._initialAlias = queryBuilder.alias; this._query = queryBuilder; this._queryParts = []; this._lastAlias = this._initialAlias; this._queryMode = QueryMode.Get; this._queryWhereType = QueryWhereType.Normal; this._selectedProperty = ""; } public get getAction(): () => Promise { return this._getAction; } public get query(): SelectQueryBuilder { return this._query; } public get queryParts(): IQueryBuilderPart[] { return this._queryParts; } public get selected(): string { return this._selectedProperty; } public and(propertySelector: (obj: PP) => S): IComparableQuery { return this.andOr(propertySelector, SqlConstants.OPERATOR_AND, this._query.andWhere); } // TODO: Can we use something besides the VERY unfortunate "any" here? // With return type "IQuery | IComparableQuery", // TS is complaining about IComparableQuery not having a whole bunch of methods // from IQuery (which is true). public andAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector?: (obj: S) => ComparableValue ): any { return this.relationCount( relationSelector, relationCountPropSelector, this._query.andHaving, ">", conditionPropSelector ); } // TODO: Can we use something besides the VERY unfortunate "any" here? // With return type "IQuery | IComparableQuery", // TS is complaining about IComparableQuery not having a whole bunch of methods // from IQuery (which is true). public andNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector?: (obj: S) => ComparableValue ): any { return this.relationCount( relationSelector, relationCountPropSelector, this._query.andHaving, "=", conditionPropSelector ); } public beginsWith(value: string, options?: QueryConditionOptions): IQuery { return this.completeWhere( SqlConstants.OPERATOR_LIKE, value, { beginsWith: true }, options ); } public async catch(rejected: (error: any) => void | Promise | IQuery): Promise { return this.toPromise() .catch(rejected); } public contains(value: string, options?: QueryConditionOptions): IQuery { return this.completeWhere( SqlConstants.OPERATOR_LIKE, value, { beginsWith: true, endsWith: true }, options ); } public count(): Promise { const targetQueryBuilder = this._query.clone(); this.compileQueryParts(this._queryParts, targetQueryBuilder); return targetQueryBuilder.getCount(); } public endsWith(value: string, options?: QueryConditionOptions): IQuery { return this.completeWhere( SqlConstants.OPERATOR_LIKE, value, { endsWith: true }, options ); } public equal(value: ComparableValue, options?: QueryConditionOptions): IQuery { return this.completeWhere(SqlConstants.OPERATOR_EQUAL, value, null, options); } public equalJoined(selector: (obj: P) => any, options?: QueryConditionOptions): IQuery { return this.completeJoinedWhere(SqlConstants.OPERATOR_EQUAL, selector, options); } // is necessary here because the usage of this method // depends on the interface from which it was called. public from( foreignEntity: { new(...params: any[]): F; } ): IJoinedQuery | IComparableQuery | any { return this.joinForeignEntity(foreignEntity); } public greaterThan(value: number | Date): IQuery { return this.completeWhere(SqlConstants.OPERATOR_GREATER, value); } public greaterThanJoined(selector: (obj: P) => any): IQuery { return this.completeJoinedWhere(SqlConstants.OPERATOR_GREATER, selector); } public greaterThanOrEqual(value: number | Date): IQuery { return this.completeWhere(SqlConstants.OPERATOR_GREATER_EQUAL, value); } public greaterThanOrEqualJoined(selector: (obj: P) => any): IQuery { return this.completeJoinedWhere(SqlConstants.OPERATOR_GREATER_EQUAL, selector); } public groupBy(propertySelector: (obj: P) => any): IQuery { const propertyName: string = nameof

(propertySelector); const groupProperty: string = `${this._lastAlias}.${propertyName}`; return this.completeGroupBy( this._query.groupBy, groupProperty ); } public in(include: string[] | number[], options?: QueryConditionOptions): IQuery { // If comparing strings, must escape them as strings in the query. this.escapeStringArray(include as string[]); return this.completeWhere( SqlConstants.OPERATOR_IN, `(${include.join(", ")})`, { quoteString: false }, options ); } public include(propertySelector: (obj: T) => JoinedEntityType): IQuery { return this.includePropertyUsingAlias(propertySelector, this._initialAlias); } public inSelected( innerQuery: ISelectQuery ): IQuery { return this.includeOrExcludeFromInnerQuery( >innerQuery, SqlConstants.OPERATOR_IN ); } public isFalse(): IQuery { this.equal(false); return this; } public isNotNull(): IQuery { return this.completeWhere( SqlConstants.OPERATOR_IS, SqlConstants.OPERATOR_NOT_NULL, { quoteString: false } ); } public isNull(): IQuery { return this.completeWhere( SqlConstants.OPERATOR_IS, SqlConstants.OPERATOR_NULL, { quoteString: false } ); } public isolatedAnd(and: (query: IQuery) => IQuery): IQuery { // TODO: These types are not lining up. return >this.isolatedConditions( <() => IQuery>and, this._query.andWhere ); } public isolatedOr(and: (query: IQuery) => IQuery): IQuery { // TODO: These types are not lining up. return >this.isolatedConditions( <() => IQuery>and, this._query.orWhere ); } public isolatedWhere( where: (query: IQuery) => IQuery ): IQuery { // TODO: These types are not lining up. return this.isolatedConditions(<() => IQuery>where, this._query.where); } public isTrue(): IQuery { this.equal(true); return this; } public join( propertySelector: (obj: T) => JoinedEntityType ): IQuery | IComparableQuery | any { return this.joinPropertyUsingAlias(propertySelector, this._initialAlias); } public joinAlso( propertySelector: (obj: T) => JoinedEntityType ): IQuery | IComparableQuery | any { return this.joinPropertyUsingAlias(propertySelector, this._initialAlias, this._query.leftJoin); } public lessThan(value: number | Date): IQuery { return this.completeWhere(SqlConstants.OPERATOR_LESS, value); } public lessThanJoined(selector: (obj: P) => any): IQuery { return this.completeJoinedWhere(SqlConstants.OPERATOR_LESS, selector); } public lessThanOrEqual(value: number | Date): IQuery { return this.completeWhere(SqlConstants.OPERATOR_LESS_EQUAL, value); } public lessThanOrEqualJoined(selector: (obj: P) => any): IQuery { return this.completeJoinedWhere(SqlConstants.OPERATOR_LESS_EQUAL, selector); } public notEqual( value: ComparableValue, options?: QueryConditionOptions ): IQuery { return this.completeWhere(SqlConstants.OPERATOR_NOT_EQUAL, value, null, options); } public notEqualJoined(selector: (obj: P) => any, options?: QueryConditionOptions): IQuery { return this.completeJoinedWhere(SqlConstants.OPERATOR_NOT_EQUAL, selector, options); } public notIn(exclude: string[] | number[], options?: QueryConditionOptions): IQuery { // If comparing strings, must escape them as strings in the query. this.escapeStringArray(exclude as string[]); return this.completeWhere( SqlConstants.OPERATOR_NOT_IN, `(${exclude.join(", ")})`, { quoteString: false }, options ); } public notInSelected( innerQuery: ISelectQuery ): IQuery { return this.includeOrExcludeFromInnerQuery( >innerQuery, SqlConstants.OPERATOR_NOT_IN ); } public or(propertySelector: (obj: P) => S): IComparableQuery { return this.andOr(propertySelector, SqlConstants.OPERATOR_OR, this._query.orWhere); } // TODO: Can we use something besides the VERY unfortunate "any" here? // With return type "IQuery | IComparableQuery", // TS is complaining about IComparableQuery not having a whole bunch of methods // from IQuery (which is true). public orAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector?: (obj: S) => ComparableValue ): any { return this.relationCount( relationSelector, relationCountPropSelector, this._query.orHaving, ">", conditionPropSelector ); } public orderBy(propertySelector: (obj: P) => any, options?: QueryOrderOptions): IQuery { const propertyName: string = nameof

(propertySelector); const orderProperty: string = `${this._lastAlias}.${propertyName}`; return this.completeOrderBy( this._query.orderBy, [orderProperty, "ASC"], options ); } public orderByDescending( propertySelector: (obj: P) => any, options?: QueryOrderOptions ): IQuery { const propertyName: string = nameof

(propertySelector); const orderProperty: string = `${this._lastAlias}.${propertyName}`; return this.completeOrderBy( this._query.orderBy, [orderProperty, "DESC"], options ); } // TODO: Can we use something besides the VERY unfortunate "any" here? // With return type "IQuery | IComparableQuery", // TS is complaining about IComparableQuery not having a whole bunch of methods // from IQuery (which is true). public orNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector?: (obj: S) => ComparableValue ): any { return this.relationCount( relationSelector, relationCountPropSelector, this._query.orHaving, "=", conditionPropSelector ); } public reset(): IQuery { this._lastAlias = this._initialAlias; // Exit the "join chain" so that additional comparisons may be made on the base entity. this._queryWhereType = QueryWhereType.Normal; return >this; } public select(propertySelector: (obj: any) => any): ISelectQuery | ISelectQuery { const selectedProperty: string = nameof(propertySelector); let alias: string = null; // If coming out of a comparison, query is back in "base mode" (where and select use base type). if (this._queryMode === QueryMode.Get) { alias = this._initialAlias; } // If in a join, use the last joined entity to select a property. else { alias = this._lastAlias; } this._selectedProperty = `${alias}.${selectedProperty}`; return this; } public skip(skip: number): IQuery { if (skip > 0) { this._queryParts.push(new QueryBuilderPart( this._query.skip, [skip] )); } return this; } public take(limit: number): IQuery { if (limit > 0) { this._queryParts.push(new QueryBuilderPart( this._query.take, [limit] )); } return this; } public async then(resolved: (results: R) => void | Promise): Promise { return this.toPromise() .then(resolved); } public thenBy(propertySelector: (obj: P) => any, options?: QueryOrderOptions): IQuery { const propertyName: string = nameof

(propertySelector); const orderProperty: string = `${this._lastAlias}.${propertyName}`; return this.completeOrderBy( this._query.addOrderBy, [orderProperty, "ASC"], options ); } public thenByDescending( propertySelector: (obj: P) => any, options?: QueryOrderOptions ): IQuery { const propertyName: string = nameof

(propertySelector); const orderProperty: string = `${this._lastAlias}.${propertyName}`; return this.completeOrderBy( this._query.addOrderBy, [orderProperty, "DESC"], options ); } public thenGroupBy(propertySelector: (obj: P) => any): IQuery { const propertyName: string = nameof

(propertySelector); const groupProperty: string = `${this._lastAlias}.${propertyName}`; return this.completeGroupBy( this._query.addGroupBy, groupProperty ); } public thenInclude( propertySelector: (obj: P) => JoinedEntityType ): IQuery { return this.includePropertyUsingAlias(propertySelector, this._lastAlias); } public thenJoin( propertySelector: (obj: P) => JoinedEntityType ): IQuery | IComparableQuery | any { return this.joinPropertyUsingAlias(propertySelector, this._lastAlias); } public thenJoinAlso( propertySelector: (obj: P) => JoinedEntityType ): IQuery | IComparableQuery | any { return this.joinPropertyUsingAlias(propertySelector, this._lastAlias, this._query.leftJoin); } public toPromise(): Promise { return this._getAction.call(this.buildQuery(this)); } public usingBaseType(): IQuery { this._lastAlias = this._initialAlias; return >this; } public where( propertySelector: (obj: F) => S ): IComparableQuery | IComparableQuery | any { const whereProperties: string = nameof(propertySelector); let whereProperty: string = null; // Keep up with the last alias in order to restore it after joinMultipleProperties. let lastAlias: string = this._lastAlias; // In the event of performing a normal where after a join-based where, use the initial alias. if (this._queryMode === QueryMode.Get) { this._queryWhereType = QueryWhereType.Normal; // Following a normal where, restore last alias to the initial alias. // Last alias may be changed in joinMultipleProperties, but in case it is not, // it needs to be reset to the initial alias before setting the where property. lastAlias = this._lastAlias = this._initialAlias; // If accessing multiple properties, join relationships using an INNER JOIN. whereProperty = this.joinMultipleProperties(whereProperties); const where: string = `${this._lastAlias}.${whereProperty}`; this._queryParts.push(new QueryBuilderPart( this._query.where, [where] )); } // Otherwise, this where was performed on a join operation. else { // If accessing multiple properties, join relationships using an INNER JOIN. whereProperty = this.joinMultipleProperties(whereProperties); this._queryWhereType = QueryWhereType.Joined; this.createJoinCondition(whereProperty); } // Restore the last alias after joinMultipleProperties. this._lastAlias = lastAlias; this._queryMode = QueryMode.Compare; return >this; } // TODO: Can we use something besides the VERY unfortunate "any" here? // With return type "IQuery | IComparableQuery", // TS is complaining about IComparableQuery not having a whole bunch of methods // from IQuery (which is true). public whereAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector?: (obj: S) => ComparableValue ): any { return this.relationCount( relationSelector, relationCountPropSelector, this._query.having, ">", conditionPropSelector ); } // TODO: Can we use something besides the VERY unfortunate "any" here? // With return type "IQuery | IComparableQuery", // TS is complaining about IComparableQuery not having a whole bunch of methods // from IQuery (which is true). public whereNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector?: (obj: S) => ComparableValue ): any { return this.relationCount( relationSelector, relationCountPropSelector, this._query.having, "=", conditionPropSelector ); } private addJoinCondition( whereProperty: string, condition: "AND" | "OR", targetQueryPart: IQueryBuilderPart = null ): void { // Result of calling .include(x => x.prop).where(...).(...).(...) // [ // QueryBuilder.leftJoinAndSelect, // ["alias.includedProperty", "includedProperty", "includedProperty.property = 'something'"] // ] // OR // Result of calling .join(x => x.pop).where(...).(...).(...) // [ // QueryBuilder.innerJoin, // ["alias.includedProperty", "includedProperty", "includedProperty.property = 'something'"] // ] const part: IQueryBuilderPart = targetQueryPart || this._queryParts.pop(); // "includedProperty.property = 'something'" let joinCondition: string = (<[string]>part.queryParams).pop(); joinCondition += ` ${condition} ${this._lastAlias}.${whereProperty}`; (<[string]>part.queryParams).push(joinCondition); // If we did not receive the optional taretQueryPart argument, // that means we used the last query part, which was popped from this._queryParts. if (!targetQueryPart) { this._queryParts.push(part); } } private andOr( propertySelector: (obj: PP) => S, operation: "AND" | "OR", queryAction: (where: string, parameters?: ObjectLiteral) => SelectQueryBuilder ): IComparableQuery { const whereProperties: string = nameof(propertySelector); // If accessing multiple properties during an AND, join relationships using an INNER JOIN. // If accessing multiple properties during an OR, join relationships using a LEFT JOIN. const joinAction: (...params: any[]) => SelectQueryBuilder = operation === "AND" ? this._query.innerJoin : this._query.leftJoin; // Keep up with the last alias in order to restore it after joinMultipleProperties. const lastAlias: string = this._lastAlias; // If accessing multiple properties, join relationships using an INNER JOIN. const whereProperty: string = this.joinMultipleProperties(whereProperties, joinAction); // A third parameter on the query parameters indicates additional join conditions. // Only add a join condition if performing a conditional join. if ( this._queryWhereType === QueryWhereType.Joined && this._queryParts[this._queryParts.length - 1].queryParams.length === 3 ) { this.addJoinCondition(whereProperty, operation); } else { const where: string = `${this._lastAlias}.${whereProperty}`; this._queryParts.push(new QueryBuilderPart( queryAction, [where] )); } // Restore the last alias after joinMultipleProperties. this._lastAlias = lastAlias; this._queryMode = QueryMode.Compare; return >this; } private buildQuery(query: IQueryInternal): SelectQueryBuilder { // Unpack and apply the QueryBuilder parts. this.compileQueryParts(query.queryParts, query.query); return query.query; } private compileQueryParts(queryParts: IQueryBuilderPart[], builder: WhereExpression): void { if (queryParts.length) { for (const queryPart of queryParts) { queryPart.queryAction.call(builder, ...queryPart.queryParams); } } } private completeGroupBy( groupAction: (...params: any[]) => SelectQueryBuilder, groupProperty: string ): IQuery { this._queryParts.push(new QueryBuilderPart( groupAction, [groupProperty] )); return this; } private completeOrderBy( orderAction: (...params: any[]) => SelectQueryBuilder, orderParams: [string, "ASC" | "DESC"], options?: QueryOrderOptions ): IQuery { if (options) { if (typeof (options.nullsFirst) === "boolean") { orderParams.push(options.nullsFirst ? "NULLS FIRST" : "NULLS LAST"); } } this._queryParts.push(new QueryBuilderPart( orderAction, orderParams )); return this; } private completeJoinedWhere( operator: string, selector: (obj: P) => any, options?: QueryConditionOptions ): IQuery { const selectedProperty: string = nameof

(selector); const compareValue: string = `${this._lastAlias}.${selectedProperty}`; // compareValue is a string but should be treated as a join property // (not a quoted string) in the query, so use "false" for the "quoteString" argument. // If the user specifies a matchCase option, then assume the property is, in fact, a string // and allow completeWhere to apply case insensitivity if necessary. return this.completeWhere( operator, compareValue, { joiningString: !!options && typeof (options.matchCase) === "boolean", quoteString: false }, options ); } private completeWhere( operator: string, value: ComparableValue, optionsInternal?: QueryConditionOptionsInternal, options?: QueryConditionOptions ): IQuery { let beginsWith: boolean = false; let endsWith: boolean = false; let joiningString: boolean = false; let quoteString: boolean = true; let matchCase: boolean = false; if (optionsInternal) { if (typeof (optionsInternal.beginsWith) === "boolean") { beginsWith = optionsInternal.beginsWith; } if (typeof (optionsInternal.endsWith) === "boolean") { endsWith = optionsInternal.endsWith; } if (typeof (optionsInternal.joiningString) === "boolean") { joiningString = optionsInternal.joiningString; } if (typeof (optionsInternal.quoteString) === "boolean") { quoteString = optionsInternal.quoteString; } } if (options) { if (typeof (options.matchCase) === "boolean") { matchCase = options.matchCase; } } if (beginsWith) { value += "%"; } if (endsWith) { value = `%${value}`; } if (typeof (value) === "string" && quoteString) { value = value.replace(/'/g, "''"); value = `'${value}'`; } // In case of a from or join within a "where", must find the last "where" in the query parts. const nonWheres: IQueryBuilderPart[] = []; let wherePart: IQueryBuilderPart = null; while (this._queryParts.length && !wherePart) { const part = this._queryParts.pop(); if ( ( // Could either be a normal where function: this._queryWhereType === QueryWhereType.Normal && ( // tslint:disable-next-line: triple-equals part.queryAction == this._query.where // tslint:disable-next-line: triple-equals || part.queryAction == this._query.andWhere // tslint:disable-next-line: triple-equals || part.queryAction == this._query.orWhere ) ) || ( // or a join condition: this._queryWhereType === QueryWhereType.Joined && ( part.queryAction === this._query.innerJoin || part.queryAction === this._query.leftJoin || part.queryAction === this._query.leftJoinAndSelect || part.queryAction === this._query.innerJoinAndSelect ) && part.queryParams.length === 3 ) ) { wherePart = part; } else { nonWheres.unshift(part); } } if (!wherePart) { throw new Error("typeorm-linq-repository: Invalid use of conditional method."); } this._queryParts.push(...nonWheres); // If processing a join condition. if (this._queryWhereType === QueryWhereType.Joined) { // [ // QueryBuilder.leftJoinAndSelect, // ["alias.includedProperty", "includedProperty", "includedProperty.property"] // ] const part: IQueryBuilderPart = wherePart; // "includedProperty.property" let joinCondition: string = (<[string]>part.queryParams).pop(); if (typeof (value) === "string" && (quoteString || joiningString) && !matchCase) { value = value.toLowerCase(); joinCondition = `LOWER(${joinCondition})`; } else if (value instanceof Date) { value = `'${value.toISOString()}'`; } // "includedProperty.property = 'something'" joinCondition += ` ${operator} ${value}`; (<[string]>part.queryParams).push(joinCondition); this._queryParts.push(part); } // If processing a normal comparison. else { // [QueryBuilder., ["alias.property"]] const part: IQueryBuilderPart = wherePart; // "alias.property" let where: string = (<[string]>part.queryParams).pop(); if (typeof (value) === "string" && (quoteString || joiningString) && !matchCase) { value = value.toLowerCase(); where = `LOWER(${where})`; } else if (value instanceof Date) { value = `'${value.toISOString()}'`; } where += ` ${operator} ${value}`; (<[string, ObjectLiteral]>part.queryParams).push(where); this._queryParts.push(part); } this._queryMode = QueryMode.Get; return this; } private createJoinCondition(joinConditionProperty: string): void { // Find the query part on which to add the condition. Usually will be the last, but not always. let targetQueryPart: IQueryBuilderPart = null; const otherParts: IQueryBuilderPart[] = []; while (!targetQueryPart && this._queryParts.length) { const part: IQueryBuilderPart = this._queryParts.pop(); // See if this query part is the one in which the last alias was joined. if (part.queryParams && part.queryParams.length > 1 && part.queryParams[1] === this._lastAlias) { targetQueryPart = part; } else { otherParts.unshift(part); } } if (!targetQueryPart) { throw new Error("typeorm-linq-repository: Invalid use of conditional join."); } this._queryParts.push(...otherParts); // There should not already be a join condition on this query builder part. // If there is, we want to add a join condition, not overwrite it. if (targetQueryPart.queryParams.length === 3) { this.addJoinCondition(joinConditionProperty, "AND", targetQueryPart); } else { const joinCondition: string = `${this._lastAlias}.${joinConditionProperty}`; (<[string]>targetQueryPart.queryParams).push(joinCondition); } this._queryParts.push(targetQueryPart); } private escapeStringArray(array: string[]): void { array.forEach((value, i) => { if (typeof (value) === "string") { array[i] = `'${value}'`; } }); } private includeOrExcludeFromInnerQuery( innerQuery: ISelectQueryInternal, operator: string ): IQuery { innerQuery.queryParts.unshift(new QueryBuilderPart( innerQuery.query.select, [innerQuery.selected] )); // Use since all that matters is that the base type of any query // contains a property named "id". const query: string = this.buildQuery(innerQuery) .getQuery(); this.completeWhere(operator, `(${query})`, { quoteString: false }); return this; } private includePropertyUsingAlias( propertySelector: (obj: T | P) => JoinedEntityType, queryAlias: string ): IQuery { return this.joinOrIncludePropertyUsingAlias( propertySelector, queryAlias, this._query.leftJoinAndSelect ); } private isolatedConditions( conditions: (query: IQuery) => IQuery, conditionAction: (...params: any[]) => SelectQueryBuilder ): IQuery { const query: Query = >conditions(>new Query( this._query, this._getAction, this._includeAliasHistory )); // Do not include joins in bracketed condition; perform those in the outer query. const conditionParts: IQueryBuilderPart[] = query .queryParts .filter(qp => // tslint:disable-next-line: triple-equals qp.queryAction == query.query.where // tslint:disable-next-line: triple-equals || qp.queryAction == query.query.andWhere // tslint:disable-next-line: triple-equals || qp.queryAction == query.query.orWhere ); // Perform joins in the outer query. const joinParts: IQueryBuilderPart[] = query .queryParts .filter(qp => conditionParts.indexOf(qp) < 0); this._queryParts.push(...joinParts); this._queryParts.push(new QueryBuilderPart( conditionAction, [ new Brackets(qb => { this.compileQueryParts(conditionParts, qb); }) ] )); return >this; } private joinForeignEntity( foreignEntity: { new(...params: any[]): F; } ): IQuery | IComparableQuery { const entityName: string = nameof(foreignEntity); const resultAlias: string = entityName; this._lastAlias = resultAlias; // If just passing through a chain of possibly already executed includes for semantics, // don't execute the include again. // Only execute the include if it has not been previously executed. if (!(this._includeAliasHistory.find(a => a === resultAlias))) { this._includeAliasHistory.push(resultAlias); this._queryParts.push(new QueryBuilderPart( this._query.innerJoin, [foreignEntity, resultAlias, "true"] )); } this.setJoinIfNotCompare(); return | IComparableQuery>this; } private joinMultipleProperties( whereProperties: string, joinAction: (...params: any[]) => SelectQueryBuilder = this._query.innerJoin, checkAliasHistory: boolean = true ): string { // Array.map() is used to select a property from a relationship collection. // .where(x => x.relationshipOne.map(y => y.relationshipTwo.map(z => z.relationshipThree)))... // Becomes, via ts-simple-nameof... // "relationshipOne.map(y => y.relationshipTwo.map(z => z.relationshipThree))" // Now get... // "relationshipOne.map(y=>y.relationshipTwo.map(z=>z.relationshipThree))" whereProperties = whereProperties.replace(/ /g, ""); // "relationshipOne.relationshipTwo.relationshipThree" // Regex allows: // .map(y=>y.relationshipTwo) // or: // .map((y)=>y.relationshipTwo) // or: // .map((y:Entity)=>y.relationshipTwo) whereProperties = whereProperties .replace(/\.map\((\(?[a-zA-Z0-9_:]+\)?)=>[a-zA-Z0-9]+/g, "") .replace(/\)/g, ""); const separatedProperties: string[] = whereProperties.split("."); // If not checking alias history, we are performing a , // so do not pop the last property, which is a relation rather than a primitive property. let whereProperty = ""; if (checkAliasHistory) { whereProperty = separatedProperties.pop(); } for (let property of separatedProperties) { // Array.map() is used to select a property from a relationship collection. if (property.indexOf("map(") === 0) { property = property.substring(4); } this.joinPropertyUsingAlias(property, this._lastAlias, joinAction, checkAliasHistory); } return whereProperty; } private joinOrIncludePropertyUsingAlias( propertySelector: ((obj: T | P) => JoinedEntityType) | string, queryAlias: string, queryAction: (...params: any[]) => SelectQueryBuilder, checkAliasHistory: boolean = true ): IQuery { let propertyName: string = null; if (propertySelector instanceof Function) { propertyName = nameof

(propertySelector); } else { propertyName = propertySelector; } let resultAlias: string = `${queryAlias}_${propertyName}`; // If including, do not set join mode. if (queryAction !== this._query.leftJoinAndSelect) { this.setJoinIfNotCompare(); } // If not checking alias history, we are performing a , // so make this instance of this relation's alias unique. if (!checkAliasHistory) { const existingAliasCount = this._duplicateAliasHistory .filter(a => a === resultAlias) .length; this._duplicateAliasHistory.push(resultAlias); resultAlias += existingAliasCount.toString(); } this._lastAlias = resultAlias; const aliasAlreadyIncluded = this._includeAliasHistory.some(a => a === resultAlias); // If just passing through a chain of possibly already executed includes for semantics, // don't execute the include again. // Only execute the include if it has not been previously executed OR if not checking alias history, // meaning we are performing an . if (!checkAliasHistory || !aliasAlreadyIncluded) { this._includeAliasHistory.push(resultAlias); const queryProperty: string = `${queryAlias}.${propertyName}`; this._queryParts.push(new QueryBuilderPart( queryAction, [queryProperty, resultAlias] )); } // If passing through a previous include but restricting the previously included entities // to a condition based on a deeper relationship, then restrict the previously used // leftJoinAndSelect to an innerJoinAndSelect instead. else if ( checkAliasHistory && aliasAlreadyIncluded && queryAction === this._query.innerJoin ) { const includeQueryPartIndex = this._queryParts.findIndex(qp => qp.queryAction === this._query.leftJoinAndSelect && qp.queryParams[1] === resultAlias ); if (includeQueryPartIndex >= 0) { const includeQueryPart = this._queryParts[includeQueryPartIndex]; this._queryParts[includeQueryPartIndex] = new QueryBuilderPart( this._query.innerJoinAndSelect, includeQueryPart.queryParams ); } } return >this; } private joinPropertyUsingAlias( propertySelector: ((obj: T | P) => JoinedEntityType) | string, queryAlias: string, queryAction: (...params: any[]) => SelectQueryBuilder = this._query.innerJoin, checkAliasHistory: boolean = true ): IQuery { return this.joinOrIncludePropertyUsingAlias(propertySelector, queryAlias, queryAction, checkAliasHistory); } private relationCount( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, havingQueryAction: (where: string, parameters?: ObjectLiteral) => SelectQueryBuilder, havingCountComparer: "=" | ">", conditionPropSelector?: (obj: S) => ComparableValue ): IQuery | IComparableQuery { // When using , always start at base type. this.reset(); // Note: For simplicity, always LEFT JOIN the specified relationship and perform check on that instance of LEFT JOIN. // There may be potential for optimization of this in the future, but it will be tricky. const relations: string = nameof(relationSelector); this.joinMultipleProperties( relations, this._query.leftJoin, false ); // Add arbitrary COUNT property to HAVING statement driving this check. const countProp = nameof(relationCountPropSelector); this._queryParts.push(new QueryBuilderPart( havingQueryAction, [`COUNT(${this._lastAlias}.${countProp}) ${havingCountComparer} 0`] )); // Create join condition if necessary. if (conditionPropSelector) { const conditionProp = nameof(conditionPropSelector); // Set QueryWhereType.Joined to enable valid use of conditional method. this._queryWhereType = QueryWhereType.Joined; this.createJoinCondition(conditionProp); } return | IComparableQuery>this; } private setJoinIfNotCompare(): void { // We may be joining a relation to make a comparison on that relation. // If so, leave QueryMode as Compare. // If not, set QueryMode to Join. if (this._queryMode !== QueryMode.Compare) { this._queryMode = QueryMode.Join; } } } ================================================ FILE: src/query/QueryBuilderPart.ts ================================================ import { SelectQueryBuilder } from "typeorm"; import { EntityBase } from "../types/EntityBase"; import { IQueryBuilderPart } from "./interfaces/IQueryBuilderPart"; export class QueryBuilderPart implements IQueryBuilderPart { private readonly _queryAction: (...params: any[]) => SelectQueryBuilder; private readonly _queryParams: any[]; public constructor(queryAction: (...params: any[]) => SelectQueryBuilder, queryParams: any[]) { this._queryAction = queryAction; this._queryParams = queryParams; } public get queryAction(): (...params: any[]) => SelectQueryBuilder { return this._queryAction; } public get queryParams(): any[] { return this._queryParams; } } ================================================ FILE: src/query/interfaces/IComparableQuery.ts ================================================ import { ComparableValue } from "../../types/ComparableValue"; import { EntityBase } from "../../types/EntityBase"; import { JoinedEntityType } from "../../types/JoinedEntityType"; import { QueryConditionOptions } from "../../types/QueryConditionOptions"; import { IComparableQueryBase } from "./IComparableQueryBase"; import { IJoinedComparableQuery } from "./IJoinedComparableQuery"; import { IQuery } from "./IQuery"; import { ISelectQuery } from "./ISelectQuery"; /** * Finalizes the comparison portion of a Query operation or joins a relation or foreign entity against which to compare a value. */ export interface IComparableQuery extends IComparableQueryBase { /** * Finds results where the specified property starts with the provided string (using LIKE "string%"). * @param value The value against which to compare. * @param options Options for query conditions such as string case matching. */ beginsWith(value: string, options?: QueryConditionOptions): IQuery; /** * Finds results where the specified property contains the provided string (using LIKE "%string%"). * @param value The value against which to compare. * @param options Options for query conditions such as string case matching. */ contains(value: string, options?: QueryConditionOptions): IQuery; /** * Finds results where the specified property ends with the provided string (using LIKE "%string"). * @param value The value against which to compare. * @param options Options for query conditions such as string case matching. */ endsWith(value: string, options?: QueryConditionOptions): IQuery; /** * Determines whether the previously selected property is equal to the specified value. * @param value The value against which to compare. * @param options Options for query conditions such as string case matching. */ equal(value: ComparableValue, options?: QueryConditionOptions): IQuery; /** * Joins an unrelated table using a TypeORM entity. * @type {F} The type of the foreign entity to join. * @param foreignEntity The TypeORM entity whose table to join. */ from(foreignEntity: { new(...params: any[]): F; }): IJoinedComparableQuery; /** * Determines whether the previously selected property is greater than the specified value. * @param value The value against which to compare. */ greaterThan(value: number | Date): IQuery; /** * Determines whether the previously selected property is greater than or equal to the specified value. * @param value The value against which to compare. */ greaterThanOrEqual(value: number | Date): IQuery; /** * Determines whether the previously selected value is contained in the specified array of values. * @param include The array of values to check for inclusion of the previously selected value. * @param options Options for query conditions such as string case matching. */ in(include: string[] | number[], options?: QueryConditionOptions): IQuery; /** * Determines whether the previously selected value is contained in the result of values selected from an inner query. * @type {TI} The base type of the inner Query. * @type {RI} The return type of the inner Query. * @type {PI1} The type of the last joined navigation property from the inner Query. * @param innerQuery The inner query from which to select the specified value. */ inSelected(innerQuery: ISelectQuery): IQuery; /** * Determines whether the previously selected property is false. * @param value The value to check for falsity. */ isFalse(): IQuery; /** * Finds results where the specified property is not null. */ isNotNull(): IQuery; /** * Finds results where the specified property is null. */ isNull(): IQuery; /** * Determines whether the previously selected property is true. * @param value The value to check for truth. */ isTrue(): IQuery; /** * Determines whether the previously selected property is less than the specified value. * @param value The value against which to compare. */ lessThan(value: number | Date): IQuery; /** * Determines whether the previously selected property is less than or equal to the specified value. * @param value The value against which to compare. */ lessThanOrEqual(value: number | Date): IQuery; /** * Joins the specified navigation property for where conditions on that property. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to join, ex. x => x.prop */ join(propertySelector: (obj: T) => JoinedEntityType): IJoinedComparableQuery; /** * Determines whether the previously selected property differs from the specified value. * @param value The value against which to compare. * @param options Options for query conditions such as string case matching. */ notEqual(value: string | number | boolean, options?: QueryConditionOptions): IQuery; /** * Determines whether the previously selected value is not contained in the specified array of values. * @param include The array of values to check for exclusion of the previously selected value. * @param options Options for query conditions such as string case matching. */ notIn(exclude: string[] | number[], options?: QueryConditionOptions): IQuery; /** * Determines whether the previously selected value is not contained in the result of values selected from an inner query. * @type {TI} The base type of the inner Query. * @type {RI} The return type of the inner Query. * @type {PI1} The type of the last joined navigation property from the inner Query. * @param innerQuery The inner query from which to select the specified property. */ notInSelected(innerQuery: ISelectQuery): IQuery; } ================================================ FILE: src/query/interfaces/IComparableQueryBase.ts ================================================ import { EntityBase } from "../../types/EntityBase"; import { JoinedEntityType } from "../../types/JoinedEntityType"; import { IJoinedComparableQuery } from "./IJoinedComparableQuery"; /** * Enables IComparableQuery and IJoinedComparableQuery to join a relation from the current Query type. */ export interface IComparableQueryBase { /** * Joins a subsequent navigation property on the previously joined relationship of type P for where conditions on that property. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to join, ex. x => x.prop */ thenJoin(propertySelector: (obj: P) => JoinedEntityType): IJoinedComparableQuery; } ================================================ FILE: src/query/interfaces/IJoinedComparableQuery.ts ================================================ import { EntityBase } from "../../types/EntityBase"; import { QueryConditionOptions } from "../../types/QueryConditionOptions"; import { IComparableQueryBase } from "./IComparableQueryBase"; import { IQuery } from "./IQuery"; /** * Finalizes the comparing portion of a Query operation by performing comparison with the specified joined value. */ export interface IJoinedComparableQuery extends IComparableQueryBase { /** * Determines whether the property specified in the last "where" is equal to the specified property on the last joined entity. * @param selector Property selection lambda for property to compare, ex. x => x.prop * @param options Options for query conditions such as string case matching. */ equalJoined(selector: (obj: P) => any, options?: QueryConditionOptions): IQuery; /** * Determines whether the property specified in the last "where" is greater than the specified property on the last joined entity. * @param selector Property selection lambda for property to compare, ex. x => x.prop */ greaterThanJoined(selector: (obj: P) => any): IQuery; /** * Determines whether the property specified in the last "where" is greater than or equal to the specified property on the last joined entity. * @param selector Property selection lambda for property to compare, ex. x => x.prop */ greaterThanOrEqualJoined(selector: (obj: P) => any): IQuery; /** * Determines whether the property specified in the last "where" is less than the specified property on the last joined entity. * @param selector Property selection lambda for property to compare, ex. x => x.prop */ lessThanJoined(selector: (obj: P) => any): IQuery; /** * Determines whether the property specified in the last "where" is less than or equal to the specified property on the last joined entity. * @param selector Property selection lambda for property to compare, ex. x => x.prop */ lessThanOrEqualJoined(selector: (obj: P) => any): IQuery; /** * Determines whether the property specified in the last "where" is not equal to the specified property on the last joined entity. * @param selector Property selection lambda for property to compare, ex. x => x.prop * @param options Options for query conditions such as string case matching. */ notEqualJoined(selector: (obj: P) => any, options?: QueryConditionOptions): IQuery; } ================================================ FILE: src/query/interfaces/IJoinedQuery.ts ================================================ import { EntityBase } from "../../types/EntityBase"; import { IComparableQuery } from "./IComparableQuery"; import { IQueryBase } from "./IQueryBase"; import { ISelectQuery } from "./ISelectQuery"; /** * Allows .where() to use the last joined entity's alias. */ export interface IJoinedQuery extends IQueryBase { /** * Selects a property from the last joined entity to select while performing an inner query. * @param propertySelector Property selection lambda for the property to select. */ select(propertySelector: (obj: P) => any): ISelectQuery; /** * Filters the query with a conditional statement based on the last joined entity's type. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to compare. */ where(propertySelector: (obj: P) => S): IComparableQuery; } ================================================ FILE: src/query/interfaces/IQuery.ts ================================================ import { EntityBase } from "../../types/EntityBase"; import { IComparableQuery } from "./IComparableQuery"; import { IQueryBase } from "./IQueryBase"; import { ISelectQuery } from "./ISelectQuery"; import { JoinedEntityType } from "../../types/JoinedEntityType"; import { ComparableValue } from "../../types/ComparableValue"; /** * Basic query operations for Queries that are not in Comparable mode. */ export interface IQuery extends IQueryBase { /** * Checks whether any items exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. */ andAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue ): IQuery; /** * Checks whether any items that meet certain criteria exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. * @param conditionPropSelector Property on which to compare */ andAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector: (obj: S) => ComparableValue ): IComparableQuery; /** * Checks whether no items exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. */ andNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue ): IQuery; /** * Checks whether no items that meet certain criteria exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. * @param conditionPropSelector Property on which to compare. */ andNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector: (obj: S) => ComparableValue ): IComparableQuery; /** * Isolates a group of conditions into one WHERE clause. * @param where The Query representing the WHERE conditions to group. */ isolatedWhere(where: (query: IQuery) => IQuery): IQuery; /** * Checks whether any items exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. */ orAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue ): IQuery; /** * Checks whether any items that meet certain criteria exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. * @param conditionPropSelector Property on which to compare. */ orAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector: (obj: S) => ComparableValue ): IComparableQuery; /** * Checks whether no items exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. */ orNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue ): IQuery; /** * Checks whether no items that meet certain criteria exist in the specified relationship (used after whereNone or whereAny). * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. * @param conditionPropSelector Property on which to compare */ orNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector: (obj: S) => ComparableValue ): IComparableQuery; /** * Selects a property from the last joined entity to select while performing an inner query. * @param propertySelector Property selection lambda for the property to select. */ select(propertySelector: (obj: T) => any): ISelectQuery; /** * Filters the query with a conditional statement based on the query's base type. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to compare. */ where(propertySelector: (obj: T) => S): IComparableQuery; /** * Checks whether any items exist in the specified relationship. * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. */ whereAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue ): IQuery; /** * Checks whether any items that meet certain criteria exist in the specified relationship. * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. * @param conditionPropSelector Property on which to compare. */ whereAny( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector: (obj: S) => ComparableValue ): IComparableQuery; /** * Checks whether no items exist in the specified relationship. * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. */ whereNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue ): IQuery; /** * Checks whether no items that meet certain criteria exist in the specified relationship. * NOTE: This method requires an arbitrary groupBy call (for instance, the base entity's primary key). * @param relationSelector The relationship to check for items. * @param relationCountPropSelector Arbitrary primitive property on which to count relation items. * @param conditionPropSelector Property on which to compare */ whereNone( relationSelector: (obj: T) => JoinedEntityType, relationCountPropSelector: (obj: S) => ComparableValue, conditionPropSelector: (obj: S) => ComparableValue ): IComparableQuery; } ================================================ FILE: src/query/interfaces/IQueryBase.ts ================================================ import { EntityBase } from "../../types/EntityBase"; import { JoinedEntityType } from "../../types/JoinedEntityType"; import { QueryOrderOptions } from "../../types/QueryOrderOptions"; import { IComparableQuery } from "./IComparableQuery"; import { IJoinedQuery } from "./IJoinedQuery"; import { IQuery } from "./IQuery"; /** * Base set of operations for all Queries that are not in Comparable mode. */ export interface IQueryBase { /** * Adds an additional logical AND condition for which to query results. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to compare. */ and(propertySelector: (obj: P) => S): IComparableQuery; /** * Catches an error thrown during the execution of the underlying QueryBuilder's Promise. * @param rejected The rejection callback for the error thrown on the underlying QueryBuilder's Promise. */ catch(rejected: (error: any) => void | Promise | IQuery): Promise; /** * Gets the count of results matching the current query conditions. */ count(): Promise; /** * Joins an unrelated table using a TypeORM entity. * @type {F} The type of the foreign entity to join. * @param foreignEntity The TypeORM entity whose table to join. */ from(foreignEntity: { new(...params: any[]): F; }): IJoinedQuery; /** * Groups entities on the specified property. * @param propertySelector Property selection lambda for property by which to group. */ groupBy(propertySelector: (obj: P) => any): IQuery; /** * Includes the specified navigation property in the queried results. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to include, ex. x => x.prop */ include(propertySelector: (obj: T) => JoinedEntityType): IQuery; /** * Isolates a group of conditions into one AND clause. * @param and The Query representing the AND conditions to group. */ isolatedAnd(and: (query: IQuery) => IQuery): IQuery; /** * Isolates a group of conditions into one OR clause. * @param or The Query representing the OR conditions to group. */ isolatedOr(or: (query: IQuery) => IQuery): IQuery; /** * Joins the specified navigation property using an INNER JOIN * (thus excluding results from the joining entity if its joined relationship fails the next join condition) * without including it in the results (useful for subsequent join conditions). * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to join, ex. x => x.prop */ join(propertySelector: (obj: T) => JoinedEntityType): IJoinedQuery; /** * Joins the specified navigation property using a LEFT JOIN * (thus including results from the joining entity even if its joined relationship fails the next join condition) * without including it in the results (useful for subsequent join conditions). * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to join, ex. x => x.prop */ joinAlso(propertySelector: (obj: T) => JoinedEntityType): IJoinedQuery; /** * Adds an additional logical OR condition for which to query results. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to compare. */ or(propertySelector: (obj: P) => S): IComparableQuery; /** * Orders the query on the specified property in ascending order. * @param propertySelector Property selection lambda for property on which to sort. */ orderBy(propertySelector: (obj: P) => any, options?: QueryOrderOptions): IQuery; /** * Orders the query on the specified property in descending order. * @param propertySelector Property selection lambda for property on which to sort. */ orderByDescending(propertySelector: (obj: P) => any, options?: QueryOrderOptions): IQuery; /** * Returns the query back to its base type while also exiting "join mode", * thus ending a join chain so that where conditions may be continued on the base type. */ reset(): IQuery; /** * Sets the number of results to skip before taking results from the query. * @param skip The number of results to skip. */ skip(skip: number): IQuery; /** * Limits the number of results to take from the query. * @param limit The number of results to take. */ take(limit: number): IQuery; /** * Executes the query by invoking the Promise to get the underlying QueryBuilder's results. * @param resolved The resolution callback for the underlying QueryBuilder's results Promise. */ then(resolved: (results: R) => void | Promise): Promise; /** * Adds a subsequent ordering to the query on the specified property in ascending order. * @param propertySelector Property selection lambda for property on which to sort. */ thenBy(propertySelector: (obj: P) => any, options?: QueryOrderOptions): IQuery; /** * Adds a subsequent ordering to the query on the specified property in descending order. * @param propertySelector Property selection lambda for property on which to sort. */ thenByDescending(propertySelector: (obj: P) => any, options?: QueryOrderOptions): IQuery; /** * Groups entities on a subsequent specified property. * @param propertySelector Property selection lambda for property by which to subsequently group. */ thenGroupBy(propertySelector: (obj: P) => any): IQuery; /** * Includes a subsequent navigation property in the previously included relationship of type P. * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to include, ex. x => x.prop */ thenInclude(propertySelector: (obj: P) => JoinedEntityType): IQuery; /** * Joins a subsequent navigation property on the previously joined relationship of type P * (thus excluding results from the joining entity if its joined relationship fails the next join condition) * without including it in the results (useful for subsequent join conditions). * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to join, ex. x => x.prop */ thenJoin(propertySelector: (obj: P) => JoinedEntityType): IJoinedQuery; /** * Joins a subsequent navigation property on the previously joined relationship of type P using a LEFT JOIN * (thus including results from the joining entity even if its joined relationship fails the next join condition) * without including it in the results (useful for subsequent join conditions). * @type {S} The type of the joined navigation property. * @param propertySelector Property selection lambda for property to join, ex. x => x.prop */ thenJoinAlso(propertySelector: (obj: P) => JoinedEntityType): IJoinedQuery; /** * Invokes and returns the Promise to get the underlying QueryBuilder's results. */ toPromise(): Promise; /** * Returns the "current query type" to the base type WITHOUT resetting join chains. * Therefore, does NOT allow where conditions to be continued on the base type, * but rather uses the base type in the current join chain. * @deprecated WARNING: This method was found to be faulty based on its initial intended use, * but remains nonetheless in case its use is, in fact, desired. * However, you may be looking for **reset()** instead. * * This method will remain deprecated for a while to alert users who used it based on * the initially intended use that it may result in unexpected behavior. * The deprecated status will be removed later so that users using it * based on the results it actually produces are not annoyed by it. */ usingBaseType(): IQuery; } ================================================ FILE: src/query/interfaces/IQueryBuilderPart.ts ================================================ import { SelectQueryBuilder } from "typeorm"; import { EntityBase } from "../../types/EntityBase"; /** * Represents a part of a TypeORM SelectQueryBuilder of type T. */ export interface IQueryBuilderPart { queryParams: any[]; queryAction(...params: any[]): SelectQueryBuilder; } ================================================ FILE: src/query/interfaces/IQueryInternal.ts ================================================ import { SelectQueryBuilder } from "typeorm"; import { EntityBase } from "../../types/EntityBase"; import { IQuery } from "./IQuery"; import { IQueryBuilderPart } from "./IQueryBuilderPart"; /** * Contains properties used internally by the Query class to construct TypeORM QueryBuilder queries from Queries. */ export interface IQueryInternal extends IQuery { /** * Gets the underlying SelectQueryBuilder represented by the Query. Normally only used internally by the Query class for innery Queries. */ query: SelectQueryBuilder; /** * Gets the QueryParts used by the Query for the SelectQueryBuilder. Normally only used internally by the Query class for innery Queries. */ queryParts: IQueryBuilderPart[]; /** * Gets the underlying database action used by the Query's SelectQueryBuilder. Normally only used internally by the Query class for innery Queries. */ getAction(): Promise; } ================================================ FILE: src/query/interfaces/ISelectQuery.ts ================================================ import { EntityBase } from "../../types/EntityBase"; /** * Used to select the desired propery from the desired entity when using an inner query. * No further methods may be used after selecting a property because this interface is meant to serve just that purpose. */ export interface ISelectQuery { } ================================================ FILE: src/query/interfaces/ISelectQueryInternal.ts ================================================ import { EntityBase } from "../../types/EntityBase"; import { IQueryInternal } from "./IQueryInternal"; import { ISelectQuery } from "./ISelectQuery"; export interface ISelectQueryInternal extends ISelectQuery, IQueryInternal { /** * Gets the propert that was selected from a Query to produce an ISelectQuery. Normally only used internally by the Query class for inner Queries. */ selected: string; } ================================================ FILE: src/repository/LinqRepository.ts ================================================ import { nameof } from "ts-simple-nameof"; import { DataSource, EntitySchema, Repository, SelectQueryBuilder } from "typeorm"; import { IQuery } from "../query/interfaces/IQuery"; import { Query } from "../query/Query"; import { EntityBase } from "../types/EntityBase"; import { EntityConstructor } from "../types/EntityConstructor"; import { RepositoryOptions } from "../types/RepositoryOptions"; import { ILinqRepository } from "./interfaces/ILinqRepository"; /** * Base repository operations for TypeORM entities. */ export class LinqRepository implements ILinqRepository { protected readonly _repository: Repository; private readonly _autoGenerateId: boolean; private readonly _primaryKeyName: string; /** * Constructs the repository for the specified entity with, unless otherwise specified, * a primry key named "id" that is auto-generated. * @param entityType The entity whose repository to create. * @param options Options for setting up the repository. */ public constructor( dataSource: DataSource, entityType: EntityConstructor | EntitySchema, options?: RepositoryOptions ) { let autoGenerateId: boolean = true; let primaryKeyName: string = "id"; if (options) { if (typeof (options.autoGenerateId) === "boolean") { autoGenerateId = options.autoGenerateId; } if (options.primaryKey) { primaryKeyName = nameof(options.primaryKey); } } this._repository = dataSource.getRepository(entityType); this._autoGenerateId = autoGenerateId; this._primaryKeyName = primaryKeyName; } public get typeormRepository(): Repository { return this._repository; } public async create(entities: E): Promise { if (this._autoGenerateId) { // Set "id" to undefined in order to allow auto-generation. if (entities instanceof Array) { for (const entity of (entities)) { // Not sure what is going on with this... // Even defining EntityBase as { [key: string]: any; } // or even Record results in the error // "Type 'string' cannot be used to index type T". // https://github.com/microsoft/TypeScript/issues/31661 (>entity)[this._primaryKeyName] = undefined; } } else { // Not sure what is going on with this... // Even defining EntityBase as { [key: string]: any; } // or even Record results in the error // "Type 'string' cannot be used to index type T". // https://github.com/microsoft/TypeScript/issues/31661 (>entities)[this._primaryKeyName] = undefined; } } return this.upsert(entities); } public createQueryBuilder(alias: string): SelectQueryBuilder { return this._repository.createQueryBuilder(alias); } public async delete(entities: number | string | T | T[]): Promise { if (typeof (entities) === "number" || typeof (entities) === "string") { await this._repository.delete(entities); } else { await this._repository.remove(entities); } return true; } public getAll(): IQuery { const queryBuilder: SelectQueryBuilder = this.createQueryBuilder("entity"); const query: IQuery = new Query( queryBuilder, queryBuilder.getMany ); return query; } public getById(id: number | string): IQuery { const alias: string = "entity"; let queryBuilder: SelectQueryBuilder = this.createQueryBuilder(alias); queryBuilder = queryBuilder.where(`${alias}.${this._primaryKeyName} = :id`, { id }); const query: IQuery = new Query( queryBuilder, queryBuilder.getOne ); return query; } public getOne(): IQuery { const queryBuilder: SelectQueryBuilder = this.createQueryBuilder("entity"); const query: IQuery = new Query( queryBuilder, queryBuilder.getOne ); return query; } public async update(entities: E): Promise { return this.upsert(entities); } public async upsert(entities: E): Promise { return >this._repository.save(entities); } } ================================================ FILE: src/repository/interfaces/ILinqRepository.ts ================================================ import { Repository, SelectQueryBuilder } from "typeorm"; import { IQuery } from "../../query/interfaces/IQuery"; import { EntityBase } from "../../types/EntityBase"; /** * Base repository operations for TypeORM entities. */ export interface ILinqRepository { /** * The underlying TypeORM repository. */ typeormRepository: Repository; /** * Creates one or more entities in the database. * @param entities The entity or entities to create. */ create(entities: E): Promise; /** * Gets an instance of a QueryBuilder (useful if the Query returned by this repository does not meet your needs yet). */ createQueryBuilder(alias: string): SelectQueryBuilder; /** * Deletes one or more entities from the database. * @param entities The entity or entities to delete or the ID of the entity to delete. */ delete(entities: number | string | T | T[]): Promise; /** * Returns a Query returning a set of results. */ getAll(): IQuery; /** * Finds one entity with the specified ID. * @param id The ID of the entity to find. */ getById(id: number | string): IQuery; /** * Returns a Query returning one entity. */ getOne(): IQuery; /** * Upserts one or more entities in the database. * Note: Now an alias for upsert. * @param entities The entity or entities to upsert. */ update(entities: E): Promise; /** * Upserts one or more entities in the database. * @param entities The entities or entities to upsert. */ upsert(entities: E): Promise; } ================================================ FILE: src/types/ComparableValue.ts ================================================ export type ComparableValue = string | number | boolean | Date; ================================================ FILE: src/types/EntityBase.ts ================================================ export declare type EntityBase = {}; ================================================ FILE: src/types/EntityConstructor.ts ================================================ import { EntityBase } from "./EntityBase"; export declare type EntityConstructor = { new (...params: any[]): T; }; ================================================ FILE: src/types/JoinedEntityType.ts ================================================ // Support *-to-one, one-to-many, and lazy loaded relations. export declare type JoinedEntityType = J | J[] | Promise | Promise; ================================================ FILE: src/types/QueryConditionOptions.ts ================================================ /** * Options for query conditions such as string case matching. */ export interface QueryConditionOptions { /** * Whether to enforce case sensitivity when comparing strings. */ matchCase?: boolean; } ================================================ FILE: src/types/QueryConditionOptionsInternal.ts ================================================ export interface QueryConditionOptionsInternal { beginsWith?: boolean; endsWith?: boolean; quoteString?: boolean; joiningString?: boolean; } ================================================ FILE: src/types/QueryOrderOptions.ts ================================================ /** * Options for query ordering such as where to put nulls (supported by some databases). */ export interface QueryOrderOptions { /** * Whether to put null values first when ordering query results. */ nullsFirst?: boolean; } ================================================ FILE: src/types/RepositoryOptions.ts ================================================ import { EntityBase } from "./EntityBase"; /** * Options for setting up the repository. */ export interface RepositoryOptions { /** * True if the entity contains a primary key that is auto-generated; defaults to true. * If the auto-generated primary key is NOT "id", then also set "primaryKeyName". */ autoGenerateId?: boolean; /** * The entity's primary key property in lambda form (e.g. "e => e.entityId"); * if omitted, the default primary key property name is "id". */ primaryKey?(entity: T): string | number; } ================================================ FILE: test/entities/artist.entity.ts ================================================ import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm"; import { Song } from "./song.entity"; @Entity() export class Artist { @PrimaryColumn() public id: number; @Column({ length: 100, nullable: false }) public name: string; @OneToMany(() => Song, a => a.artist) public songs?: Song[]; } ================================================ FILE: test/entities/genre.entity.ts ================================================ import { Column, Entity, PrimaryColumn } from "typeorm"; @Entity() export class Genre { @PrimaryColumn() public id: number; @Column({ length: 50, nullable: false }) public name: string; } ================================================ FILE: test/entities/song.entity.ts ================================================ import { nameof } from "ts-simple-nameof"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; import { Artist } from "./artist.entity"; import { Genre } from "./genre.entity"; @Entity() export class Song { @ManyToOne(() => Artist, a => a.songs) @JoinColumn({ name: nameof(s => s.artistId) }) public artist?: Artist; @Column({ nullable: false }) public artistId: number; @ManyToOne(() => Genre) @JoinColumn({ name: nameof(s => s.genreId) }) public genre?: Genre; @Column({ nullable: false }) public genreId: number; @PrimaryColumn() public id: number; @Column({ length: 100, nullable: false }) public name: string; } ================================================ FILE: test/entities/user-profile-attribute.entity.ts ================================================ import { nameof } from "ts-simple-nameof"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; import { Genre } from "./genre.entity"; import { User } from "./user.entity"; @Entity() export class UserProfileAttribute { @ManyToOne(() => Genre) @JoinColumn({ name: nameof(upa => upa.genreId) }) public genre?: Genre; @Column({ nullable: false }) public genreId: number; @PrimaryColumn() public id: number; @ManyToOne(() => User, u => u.profileAttributes) @JoinColumn({ name: nameof(upa => upa.userId) }) public user?: User; @Column({ nullable: false }) public userId: number; } ================================================ FILE: test/entities/user.entity.ts ================================================ import { Entity, OneToMany, PrimaryColumn } from "typeorm"; import { UserProfileAttribute } from "./user-profile-attribute.entity"; @Entity() export class User { @PrimaryColumn() public id: number; @OneToMany(() => UserProfileAttribute, upa => upa.user) public profileAttributes?: UserProfileAttribute[]; } ================================================ FILE: test/jasmine-ts.helper.js ================================================ const { register } = require("ts-node"); register({ project: "tsconfig.spec.json" }); ================================================ FILE: test/jasmine.json ================================================ { "spec_files": [ "test/scenarios/**/*.spec.ts" ], "helpers": [ "test/jasmine-ts.helper.js" ], "stopSpecOnExpectationFailure": false, "random": false } ================================================ FILE: test/scenarios/query/query.spec.ts ================================================ import { DataSource } from "typeorm"; import { getTypeormDataSource } from "../../../.typeorm/connection/get-typeorm-data-source.function"; import { LinqRepository } from "../../../src/repository/LinqRepository"; import { Artist } from "../../entities/artist.entity"; import { Song } from "../../entities/song.entity"; import { UserProfileAttribute } from "../../entities/user-profile-attribute.entity"; describe("Query", () => { let dataSource: DataSource; let artistRepository: LinqRepository; let songRepository: LinqRepository; beforeAll(async () => { dataSource = await getTypeormDataSource(); artistRepository = new LinqRepository(dataSource, Artist); songRepository = new LinqRepository(dataSource, Song); }); it("gets all entities", async () => { const artists = await artistRepository.getAll(); expect(artists.length) .toBe(3); }); it("gets many entities", async () => { const rockSongs = await songRepository .getAll() .where(s => s.genreId) .equal(1); expect(rockSongs.length) .toBe(3); }); it("gets one entity", async () => { const song = await songRepository .getOne() .where(s => s.artistId) .equal(1) .and(s => s.genreId) .equal(3); expect(song) .not .toBeUndefined(); }); it("gets entity by id", async () => { const song = await songRepository.getById(1); expect(song) .not .toBeUndefined(); }); it("counts entities", async () => { const rockSongCount = await songRepository .getAll() .where(s => s.genreId) .equal(1) .count(); expect(rockSongCount) .toBe(3); }); it("includes entities", async () => { const song = await songRepository .getById(1) .include(s => s.artist) .include(s => s.genre); expect(song.artist) .not .toBeUndefined(); expect(song.genre) .not .toBeUndefined(); }); it("isolates and conditions", async () => { const songs = await songRepository .getAll() .where(s => s.artistId) .equal(1) .isolatedAnd(q => q .where(s => s.genreId) .equal(1) .or(s => s.genreId) .equal(2) ); expect(songs.length) .toBe(2); }); it("isolates or conditions", async () => { const songs = await songRepository .getAll() .where(s => s.artistId) .equal(1) .isolatedOr(q => q .where(s => s.genreId) .notEqual(2) .and(s => s.genreId) .notEqual(3) ); expect(songs.length) .toBe(5); }); it("joins mapped relations (without parens)", async () => { const artists = await artistRepository .getAll() .where(a => a.songs.map(s => s.genreId)) .equal(1); expect(artists.length) .toBe(3); }); it("joins mapped relations (with parens)", async () => { const artists = await artistRepository .getAll() // tslint:disable-next-line: arrow-parens .where((a) => a.songs.map((s) => s.genreId)) .equal(1); expect(artists.length) .toBe(3); }); it("filters where any (without condition)", async () => { const artists = await artistRepository .getAll() .whereAny(a => a.songs, s => s.id) // Must add all columns to group by to mitigate // "incompatible with sql_mode=only_full_group_by" errors in mysql... .reset() .groupBy(a => a.id) .thenGroupBy(a => a.name); expect(artists.length) .toBe(3); }); it("filters where any (with condition)", async () => { const artists = await artistRepository .getAll() .whereAny(a => a.songs, s => s.id, s => s.genreId) .equal(2) // Must add all columns to group by to mitigate // "incompatible with sql_mode=only_full_group_by" errors in mysql... .reset() .groupBy(a => a.id) .thenGroupBy(a => a.name); expect(artists.length) .toBe(2); }); it("filters where none (without condition)", async () => { const artists = await artistRepository .getAll() .whereNone(a => a.songs, s => s.id) // Must add all columns to group by to mitigate // "incompatible with sql_mode=only_full_group_by" errors in mysql... .reset() .groupBy(a => a.id) .thenGroupBy(a => a.name); expect(artists.length) .toBe(0); }); it("filters where none (with condition)", async () => { const artists = await artistRepository .getAll() .whereNone(a => a.songs, s => s.id, s => s.genreId) .equal(2) // Must add all columns to group by to mitigate // "incompatible with sql_mode=only_full_group_by" errors in mysql... .reset() .groupBy(a => a.id) .thenGroupBy(a => a.name); expect(artists.length) .toBe(1); }); it("works with inner queries", async () => { const songs = await songRepository .getAll() .join(s => s.artist) .where(a => a.id) .equal(1) .where(s => s.id) .notInSelected( songRepository .getAll() .join(s => s.artist) .where(a => a.id) .equal(1) .from(UserProfileAttribute) .thenJoin(upa => upa.genre) .where(g => g.id) .join(s => s.genre) .equalJoined(g => g.id) .from(UserProfileAttribute) .thenJoin(upa => upa.user) .where(u => u.id) .equal(1) .select(s => s.id) ); expect(songs.length) .toBe(2); }); afterAll(async () => { await dataSource.destroy(); }); }); ================================================ FILE: tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "exclude": [ ".typeorm", "test" ] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "lib": [ "es6" ], "resolveJsonModule": true, "noImplicitAny": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "rootDir": "." } } ================================================ FILE: tsconfig.spec.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "sourceMap": true, "inlineSourceMap": true, "inlineSources": true } } ================================================ FILE: tslint.json ================================================ { "rules": { "adjacent-overload-signatures": true, "align": true, "array-type": [ true, "array" ], "arrow-parens": [ true, "ban-single-arg-parens" ], "binary-expression-operand-order": true, "class-name": true, "comment-format": [ true, "check-space" ], "curly": true, "forin": true, "import-spacing": true, "member-access": [ true, "check-constructor" ], "member-ordering": [ true, { "order": "instance-sandwich" } ], "new-parens": true, "newline-before-return": true, "newline-per-chained-call": true, "no-boolean-literal-compare": true, "no-consecutive-blank-lines": true, "no-construct": true, "no-duplicate-imports": true, "no-duplicate-super": true, "no-duplicate-switch-case": true, "no-duplicate-variable": true, "no-for-in-array": true, "no-internal-module": true, "no-invalid-template-strings": true, "no-invalid-this": true, "no-irregular-whitespace": true, "no-mergeable-namespace": true, "no-parameter-properties": true, "no-shadowed-variable": [ true, { "class": true, "enum": true, "function": true, "import": false, "interface": true, "namespace": true, "typeAlias": true, "typeParameter": true } ], "no-string-literal": true, "no-string-throw": true, "no-switch-case-fall-through": true, "no-this-assignment": true, "no-trailing-whitespace": true, "no-unnecessary-callback-wrapper": true, "no-unnecessary-initializer": true, "no-unused-expression": true, "no-var-keyword": true, "number-literal-format": true, "object-literal-key-quotes": [ true, "as-needed" ], "object-literal-shorthand": true, "object-literal-sort-keys": [ true, "ignore-case" ], "only-arrow-functions": [ true, "allow-declarations", "allow-named-functions" ], "prefer-const": true, "prefer-for-of": true, "prefer-method-signature": true, "prefer-object-spread": true, "prefer-readonly": true, "prefer-template": true, "quotemark": [ true, "double", "avoid-template" ], "restrict-plus-operands": true, "semicolon": true, "space-before-function-paren": [ true, { "anonymous": "never", "asyncArrow": "always", "constructor": "never", "method": "never", "named": "never" } ], "switch-default": true, "switch-final-break": true, "trailing-comma": [ true, { "multiline": "never", "singleline": "never" } ], "triple-equals": true, "typedef": [ true, "array-destructuring", "call-signature", "member-variable-declaration", "object-destructuring", "parameter", "property-declaration" ], "typedef-whitespace": [ true, { "call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" }, { "call-signature": "onespace", "index-signature": "onespace", "parameter": "onespace", "property-declaration": "onespace", "variable-declaration": "onespace" } ], "unified-signatures": true, "use-isnan": true, "whitespace": true } }