diff --git a/packages/polaris-common/package.json b/packages/polaris-common/package.json index 6bf07e79..cf003de4 100644 --- a/packages/polaris-common/package.json +++ b/packages/polaris-common/package.json @@ -44,6 +44,6 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" } } diff --git a/packages/polaris-core/package.json b/packages/polaris-core/package.json index 67755bec..63cb4735 100644 --- a/packages/polaris-core/package.json +++ b/packages/polaris-core/package.json @@ -65,7 +65,7 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2", + "typescript": "^4.2.3", "ws": "^7.3.1" } } diff --git a/packages/polaris-graphql-logger/package.json b/packages/polaris-graphql-logger/package.json index ab5b8d90..55bc53ac 100644 --- a/packages/polaris-graphql-logger/package.json +++ b/packages/polaris-graphql-logger/package.json @@ -45,6 +45,6 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" } } diff --git a/packages/polaris-middlewares/package.json b/packages/polaris-middlewares/package.json index 242be534..fe0b95c7 100644 --- a/packages/polaris-middlewares/package.json +++ b/packages/polaris-middlewares/package.json @@ -57,6 +57,6 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" } } diff --git a/packages/polaris-nest/package.json b/packages/polaris-nest/package.json index e1aaea44..4401ce5e 100644 --- a/packages/polaris-nest/package.json +++ b/packages/polaris-nest/package.json @@ -28,10 +28,10 @@ ], "dependencies": { "@enigmatis/polaris-core": "^2.0.0-beta.66", - "@nestjs/common": "^7.6.12", - "@nestjs/core": "^7.6.12", - "@nestjs/graphql": "^7.9.9", - "@nestjs/platform-express": "^7.6.12", + "@nestjs/common": "^7.6.14", + "@nestjs/core": "^7.6.14", + "@nestjs/graphql": "^7.10.2", + "@nestjs/platform-express": "^7.6.14", "@nestjs/typeorm": "^7.1.5", "apollo-server": "^2.17.0", "apollo-server-plugin-base": "0.6.10", @@ -65,7 +65,7 @@ "tslint-config-prettier": "^1.18.0", "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" }, "peerDependencies": { "@nestjs/common": "^6.7.0 || ^7.0.0", diff --git a/packages/polaris-nest/tsconfig.json b/packages/polaris-nest/tsconfig.json index feb890d0..6b97941e 100644 --- a/packages/polaris-nest/tsconfig.json +++ b/packages/polaris-nest/tsconfig.json @@ -14,6 +14,7 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, + "noImplicitAny": false, "strictPropertyInitialization": false, "lib": [ "es2020.bigint" diff --git a/packages/polaris-permissions/package.json b/packages/polaris-permissions/package.json index 5a92a0c5..d89d2690 100644 --- a/packages/polaris-permissions/package.json +++ b/packages/polaris-permissions/package.json @@ -52,6 +52,6 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" } } diff --git a/packages/polaris-schema/package.json b/packages/polaris-schema/package.json index 96f2055c..15736cd3 100644 --- a/packages/polaris-schema/package.json +++ b/packages/polaris-schema/package.json @@ -52,6 +52,6 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" } } diff --git a/packages/polaris-test/nest-server-code-first/graphql/services/author.service.ts b/packages/polaris-test/nest-server-code-first/graphql/services/author.service.ts index 2a0e633e..574f7992 100644 --- a/packages/polaris-test/nest-server-code-first/graphql/services/author.service.ts +++ b/packages/polaris-test/nest-server-code-first/graphql/services/author.service.ts @@ -22,7 +22,7 @@ export class AuthorService { } public async createManyAuthors(): Promise { - for (let i = 0; i < 15; i++) { + for (let i = 0; i < 10; i++) { const author = new Author(`Ron${i}`, 'Katz'); await this.authorRepository.save(author); } diff --git a/packages/polaris-test/nest-server-schema-first/graphql/services/author.service.ts b/packages/polaris-test/nest-server-schema-first/graphql/services/author.service.ts index 74395a9b..b9d2ed0f 100644 --- a/packages/polaris-test/nest-server-schema-first/graphql/services/author.service.ts +++ b/packages/polaris-test/nest-server-schema-first/graphql/services/author.service.ts @@ -22,7 +22,7 @@ export class AuthorService { } public async createManyAuthors(): Promise { - for (let i = 0; i < 15; i++) { + for (let i = 0; i < 10; i++) { const author = new Author(`Ron${i}`, 'Katz'); await this.authorRepository.save(author); } diff --git a/packages/polaris-test/package.json b/packages/polaris-test/package.json index 2131fb0d..9250c7b1 100644 --- a/packages/polaris-test/package.json +++ b/packages/polaris-test/package.json @@ -29,10 +29,10 @@ "dependencies": { "@enigmatis/polaris-core": "^2.0.0-beta.66", "@enigmatis/polaris-nest": "^1.9.0", - "@nestjs/common": "^7.6.12", - "@nestjs/core": "^7.6.12", - "@nestjs/graphql": "^7.9.9", - "@nestjs/platform-express": "^7.6.12", + "@nestjs/common": "^7.6.14", + "@nestjs/core": "^7.6.14", + "@nestjs/graphql": "^7.10.2", + "@nestjs/platform-express": "^7.6.14", "@nestjs/typeorm": "^7.1.5", "@types/pg": "^7.14.5", "apollo-server": "^2.17.0", @@ -78,7 +78,7 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" }, "peerDependencies": { "@nestjs/common": "^6.7.0 || ^7.0.0", diff --git a/packages/polaris-test/server/schema/resolvers.ts b/packages/polaris-test/server/schema/resolvers.ts index f970717e..dd1ea72f 100644 --- a/packages/polaris-test/server/schema/resolvers.ts +++ b/packages/polaris-test/server/schema/resolvers.ts @@ -265,7 +265,7 @@ export const resolvers = { args: any, context: PolarisGraphQLContext, ): Promise => { - for (let i = 1; i <= 15; i++) { + for (let i = 0; i < 10; i++) { const connection = getPolarisConnectionManager().get(process.env.SCHEMA_NAME); const authorRepo = connection.getRepository(Author, context); const newAuthor = new Author(`Ron${i}`, `Katz`); diff --git a/packages/polaris-test/tests/online-pagination-without-relay.test.ts b/packages/polaris-test/tests/online-pagination-without-relay.test.ts index ecfabef6..7cdb96ef 100644 --- a/packages/polaris-test/tests/online-pagination-without-relay.test.ts +++ b/packages/polaris-test/tests/online-pagination-without-relay.test.ts @@ -68,7 +68,7 @@ const extractRelevantIrrelevantEntitiesByQuery = (query: string, irrelevantEntit } }; -describe('online pagination tests - left outer join implementation', () => { +describe('online pagination tests', () => { test.each(createServersWithInnerAndLeftJoin())( 'fetch authors, page-size and data version sent, return accordingly', async (server, query) => { @@ -251,4 +251,25 @@ describe('online pagination tests - left outer join implementation', () => { }); }, ); + test.each(createServersWithInnerAndLeftJoin())( + 'fetch authors, entities with a specific data-version are not returned as irrelevant entities in a following page with the same data-version', + async (server, query) => { + await polarisTest(server, async () => { + await graphqlRawRequest(createManyAuthors.request, {}, {}); + let result = await graphqlRawRequest( + query, + { 'page-size': 5, 'data-version': 1 }, + {}, + ); + const lastIdInDv = result.extensions.lastIdInDataVersion; + const lastDv = result.extensions.lastDataVersionInPage; + result = await graphqlRawRequest( + query, + { 'page-size': 5, 'data-version': lastDv, 'last-id-in-dv': lastIdInDv }, + {}, + ); + expect(result.extensions.irrelevantEntities).toBeUndefined(); + }); + }, + ); }); diff --git a/packages/polaris-typeorm/package.json b/packages/polaris-typeorm/package.json index 06a6cfc7..6126c4fd 100644 --- a/packages/polaris-typeorm/package.json +++ b/packages/polaris-typeorm/package.json @@ -56,6 +56,6 @@ "tslint-consistent-codestyle": "^1.16.0", "tslint-eslint-rules": "^5.4.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^4.0.2" + "typescript": "^4.2.3" } } diff --git a/packages/polaris-typeorm/src/handlers/data-version-handler.ts b/packages/polaris-typeorm/src/handlers/data-version-handler.ts index 5f0fec66..a6bf87ea 100644 --- a/packages/polaris-typeorm/src/handlers/data-version-handler.ts +++ b/packages/polaris-typeorm/src/handlers/data-version-handler.ts @@ -1,9 +1,11 @@ import { PolarisExtensions, PolarisGraphQLContext } from '@enigmatis/polaris-common'; -import { EntityMetadata } from 'typeorm'; +import { Brackets, EntityMetadata, QueryRunner, EntityTarget } from 'typeorm'; import { RelationMetadata } from 'typeorm/metadata/RelationMetadata'; import { DataVersion, PolarisConnection, PolarisEntityManager, SelectQueryBuilder } from '..'; import { isDescendentOfCommonModel } from '../utils/descendent-of-common-model'; +import { setWhereCondition } from '../utils/query-builder-util'; import { cloneDeep } from 'lodash'; +import { FindHandler } from './find-handler'; export class DataVersionHandler { public async updateDataVersion( @@ -182,22 +184,33 @@ function joinDataVersionRelations( return qb; } +function getOrDataVersionCondition( + qb: SelectQueryBuilder, + dataVersion: number, + names: string[], +) { + return new Brackets((qb2) => { + qb2.where(`${qb.alias}.dataVersion > :dataVersion`, { dataVersion }); + names = names.slice(1); + for (const name of names) { + qb2 = qb2.orWhere(`${name}.dataVersion > :dataVersion`); + } + }); +} + function applyDataVersionWhereConditions( context: PolarisGraphQLContext, qb: SelectQueryBuilder, names: string[], + shouldLoadRelations: boolean, ): SelectQueryBuilder { let dataVersion = context.requestHeaders.dataVersion || 0; const lastIdInDataVersion = context.requestHeaders.lastIdInDV; - if (lastIdInDataVersion) { + if (lastIdInDataVersion && shouldLoadRelations) { dataVersion--; } if (dataVersion > 0) { - qb.where(`${qb.alias}.dataVersion > :dataVersion`, { dataVersion }); - names = names.slice(1); - for (const name of names) { - qb = qb.orWhere(`${name}.dataVersion > :dataVersion`); - } + qb = setWhereCondition(qb, getOrDataVersionCondition(qb, dataVersion, names)); } return qb; } @@ -226,7 +239,7 @@ export const leftJoinDataVersionFilter = ( entityMetadata, names, ); - return applyDataVersionWhereConditions(context, qb, names); + return applyDataVersionWhereConditions(context, qb, names, shouldLoadRelations); } return qb; }; @@ -235,11 +248,17 @@ export const InnerJoinDataVersionQuery = ( connection: PolarisConnection, context: PolarisGraphQLContext, rootEntityMetadata: EntityMetadata, -): string => { + criteria: any, + entityClass: EntityTarget, + queryRunner?: QueryRunner, +): { query: string; parameters: any } => { const selectQueriesMap: Map> = createEntitiesSelectQueries( connection, rootEntityMetadata, context, + criteria, + entityClass, + queryRunner, ); const rootEntityIdSelection = `"${rootEntityMetadata.tableName}"."id"`; @@ -249,10 +268,12 @@ export const InnerJoinDataVersionQuery = ( function buildInnerJoinQuery( selectQueries: SelectQueryBuilder[], rootEntityIdSelection: string, -): string { +): { query: string; parameters: any } { let finalQuery = 'WITH w1(id, dv) AS ('; const union = ' UNION '; + const parameters: any[] = []; selectQueries.forEach((query: SelectQueryBuilder) => { + parameters.push(...selectQueries[0].getQueryAndParameters()[1]); const splitQuery = query.getSql().split('SELECT'); finalQuery = finalQuery.concat(`SELECT ${rootEntityIdSelection},`); finalQuery = @@ -263,17 +284,27 @@ function buildInnerJoinQuery( finalQuery = finalQuery.concat( ') SELECT w1.id AS "entityId", MAX(w1.dv) AS "maxDV" FROM w1 GROUP BY w1.id ORDER BY "maxDV", "entityId"', ); - return finalQuery; + return { query: finalQuery, parameters }; } function createEntitiesSelectQueries( connection: PolarisConnection, rootEntityMetadata: EntityMetadata, context: PolarisGraphQLContext, + criteria: any, + entityClass: EntityTarget, + queryRunner?: QueryRunner, ): Map> { const selectQueries: Map> = new Map(); - const rootEntitySelectQuery = getRootEntitySelectQuery(connection, rootEntityMetadata, context); + const rootEntitySelectQuery = getRootEntitySelectQuery( + connection, + rootEntityMetadata, + context, + entityClass, + queryRunner, + criteria, + ); selectQueries.set(rootEntityMetadata.tableName, rootEntitySelectQuery); createChildEntitiesSelectQueries( @@ -282,6 +313,7 @@ function createEntitiesSelectQueries( context.dataVersionContext!.mapping!, selectQueries, rootEntityMetadata.tableName, + criteria, ); return selectQueries; @@ -291,12 +323,23 @@ function getRootEntitySelectQuery( connection: PolarisConnection, rootEntityMetadata: EntityMetadata, context: PolarisGraphQLContext, + entityClass: EntityTarget, + queryRunner?: QueryRunner, + criteria?: any, ): SelectQueryBuilder { - const rootEntityQueryBuilder = connection.createQueryBuilder(); - setWhereClauseOfQuery(rootEntityQueryBuilder, context, rootEntityMetadata); - return rootEntityQueryBuilder - .addSelect(`${rootEntityMetadata.tableName}.dataVersion`) - .addFrom(rootEntityMetadata.tableName, rootEntityMetadata.tableName); + const rootEntityQueryBuilder = connection.createQueryBuilder( + entityClass, + rootEntityMetadata.tableName, + queryRunner, + ); + setWhereClauseOfQuery( + rootEntityQueryBuilder, + context, + rootEntityMetadata, + rootEntityMetadata, + criteria, + ); + return rootEntityQueryBuilder.select(`${rootEntityMetadata.tableName}.dataVersion`); } function createChildEntitiesSelectQueries( @@ -305,6 +348,7 @@ function createChildEntitiesSelectQueries( mapping: Map, selectQueries: Map>, currentEntityPath: string, + criteria?: any, ): void { if (entityMetadata.relations && mapping.size > 0) { for (const relation of entityMetadata.relations) { @@ -315,9 +359,11 @@ function createChildEntitiesSelectQueries( const relationQueryBuilder: SelectQueryBuilder = cloneDeep(parentSelectQuery!); handleInnerJoinForRelation( relationQueryBuilder, + entityMetadata, relationEntityMetadata, relation, context, + criteria, ); const entityPath = `${currentEntityPath}.${relationEntityMetadata.tableName}`; selectQueries.set(entityPath, relationQueryBuilder); @@ -335,11 +381,13 @@ function createChildEntitiesSelectQueries( function handleInnerJoinForRelation( queryBuilder: SelectQueryBuilder, + rootEntityMetadata: EntityMetadata, entityMetadata: EntityMetadata, relation: RelationMetadata, context: PolarisGraphQLContext, + criteria?: any, ) { - setWhereClauseOfQuery(queryBuilder, context, entityMetadata); + setWhereClauseOfQuery(queryBuilder, context, rootEntityMetadata, entityMetadata, criteria); if (relation.relationType === 'one-to-many' || relation.relationType === 'many-to-one') { handleInnerJoinForOneToManyRelation(queryBuilder, entityMetadata, relation); } else if (relation.relationType === 'many-to-many') { @@ -428,7 +476,9 @@ function getInnerJoinConditionByEntityMetadata( function setWhereClauseOfQuery( queryBuilder: SelectQueryBuilder, context: PolarisGraphQLContext, + rootEntityMetadata: EntityMetadata, entityMetadata: EntityMetadata, + criteria?: any, ) { let dataVersionThreshold = context.requestHeaders.dataVersion || 0; if (context.requestHeaders.lastIdInDV) { @@ -436,8 +486,9 @@ function setWhereClauseOfQuery( } const realityIdThreshold = context.requestHeaders.realityId || 0; queryBuilder.where(`${entityMetadata.tableName}.dataVersion > ${dataVersionThreshold}`); - queryBuilder.andWhere(`${entityMetadata.tableName}.realityId = ${realityIdThreshold}`); - queryBuilder.andWhere(`${entityMetadata.tableName}.deleted = false`); + queryBuilder.andWhere(`${rootEntityMetadata.tableName}.realityId = ${realityIdThreshold}`); + queryBuilder.andWhere(`${rootEntityMetadata.tableName}.deleted = false`); + FindHandler.applyUserConditions(criteria, queryBuilder); } function getChildDVMapping( diff --git a/packages/polaris-typeorm/src/handlers/find-handler.ts b/packages/polaris-typeorm/src/handlers/find-handler.ts index aa4ec484..7ed8cdcd 100644 --- a/packages/polaris-typeorm/src/handlers/find-handler.ts +++ b/packages/polaris-typeorm/src/handlers/find-handler.ts @@ -1,50 +1,90 @@ import { PolarisGraphQLContext, PolarisRequestHeaders } from '@enigmatis/polaris-common'; -import { FindManyOptions, FindOneOptions, In } from 'typeorm'; - -const realityIdCriteria = (includeLinkedOper: boolean, headers: PolarisRequestHeaders) => - includeLinkedOper && headers.realityId !== 0 && headers.includeLinkedOper - ? In([headers.realityId, 0]) - : headers.realityId || 0; +import { Brackets, FindManyOptions, FindOneOptions, SelectQueryBuilder } from 'typeorm'; +import { setWhereCondition, setWhereInIdsCondition } from '../utils/query-builder-util'; export class FindHandler { - public findConditions( + public applyFindConditionsToQueryBuilder( includeLinkedOper: boolean, context: PolarisGraphQLContext, + qb: SelectQueryBuilder, criteria?: string | any[] | FindManyOptions | FindOneOptions, shouldIncludeDeletedEntities: boolean = false, - ) { + ): SelectQueryBuilder { const headers: PolarisRequestHeaders = context?.requestHeaders || {}; + this.applyRealityCondition(criteria, includeLinkedOper, headers, qb); + FindHandler.applyUserConditions(criteria, qb); + this.applyDeleteCondition(criteria as any, shouldIncludeDeletedEntities, qb); + return qb; + } + + private applyDeleteCondition( + criteria: any, + shouldIncludeDeletedEntities: boolean, + qb: SelectQueryBuilder, + ) { + let shouldAddDeleteCondition: boolean = true; + if (criteria?.where?.deleted !== undefined) { + this.deleteFindConditionIfRedundant(criteria.where); + shouldAddDeleteCondition = false; + } + if (shouldAddDeleteCondition && !shouldIncludeDeletedEntities) { + qb = setWhereCondition(qb, `${qb.alias}.deleted = :deleted`, { deleted: false }); + } + return qb; + } - let polarisCriteria: any; + public static applyUserConditions(criteria: any, qb: SelectQueryBuilder) { if (typeof criteria === 'string') { - polarisCriteria = { where: { id: criteria } }; + setWhereCondition(qb, `${qb.alias}.id = :id`, { id: criteria }); } else if (criteria instanceof Array) { - polarisCriteria = { where: { id: In(criteria) } }; + setWhereInIdsCondition(qb, criteria); } else { - polarisCriteria = criteria || {}; + if (criteria && criteria.where) { + let whereCondition: any; + if (criteria.where instanceof Array && criteria.where.length > 0) { + whereCondition = this.createOrWhereCondition(criteria); + } + setWhereCondition(qb, whereCondition ?? criteria.where); + } } + } - polarisCriteria.where = { ...polarisCriteria.where }; - if (polarisCriteria.where.deleted === undefined && !shouldIncludeDeletedEntities) { - polarisCriteria.where.deleted = false; - } else { - this.deleteFindConditionIfRedundant(polarisCriteria); - } - if (polarisCriteria.where.realityId === undefined) { - polarisCriteria.where.realityId = realityIdCriteria(includeLinkedOper, headers); + private static createOrWhereCondition(criteria: any) { + return new Brackets((qb2) => { + qb2.where(criteria.where[0]); + for (let i = 1; i < criteria.where.length; i++) { + qb2.orWhere(criteria.where[i]); + } + }); + } + + private applyRealityCondition( + criteria: any, + includeLinkedOper: boolean, + headers: PolarisRequestHeaders, + qb: SelectQueryBuilder, + ) { + if ((criteria as any)?.where?.realityId === undefined) { + const realityIdFromHeader = headers.realityId || 0; + qb = setWhereCondition( + qb, + includeLinkedOper && headers.realityId !== 0 && headers.includeLinkedOper + ? `${qb.alias}.realityId in (${headers.realityId},0)` + : `${qb.alias}.realityId = ${realityIdFromHeader}`, + ); } - return polarisCriteria; + return qb; } private deleteFindConditionIfRedundant(polarisCriteria: any) { if ( - polarisCriteria.where.deleted?._type === 'in' && - ((polarisCriteria.where.deleted?._value[0] === true && - polarisCriteria.where.deleted?._value[1] === false) || - (polarisCriteria.where.deleted?._value[1] === true && - polarisCriteria.where.deleted?._value[0] === false)) + polarisCriteria.deleted?._type === 'in' && + ((polarisCriteria.deleted?._value[0] === true && + polarisCriteria.deleted?._value[1] === false) || + (polarisCriteria.deleted?._value[1] === true && + polarisCriteria.deleted?._value[0] === false)) ) { - delete polarisCriteria.where.deleted; + delete polarisCriteria.deleted; } } } diff --git a/packages/polaris-typeorm/src/typeorm-bypasses/polaris-entity-manager.ts b/packages/polaris-typeorm/src/typeorm-bypasses/polaris-entity-manager.ts index 10a738d5..9f4cbdd6 100644 --- a/packages/polaris-typeorm/src/typeorm-bypasses/polaris-entity-manager.ts +++ b/packages/polaris-typeorm/src/typeorm-bypasses/polaris-entity-manager.ts @@ -32,7 +32,11 @@ import { isDescendentOfCommonModel } from '../utils/descendent-of-common-model'; import { PolarisConnection } from './polaris-connection'; import { PolarisRepository } from './polaris-repository'; import { PolarisRepositoryFactory } from './polaris-repository-factory'; -import { addDateRangeCriteria } from '../utils/query-builder-util'; +import { + addDateRangeCriteria, + setWhereCondition, + setWhereInIdsCondition, +} from '../utils/query-builder-util'; import { CommonModelSubscriber } from '../subscribers/common-model-subscriber'; export class PolarisEntityManager extends EntityManager { @@ -145,7 +149,11 @@ export class PolarisEntityManager extends EntityManager { await this.startTransaction(); const metadata = this.connection.getMetadata(entityClass); if (isDescendentOfCommonModel(metadata) && this.context) { - criteria = this.findHandler.findConditions(true, this.context, criteria); + return this.createQueryBuilderWithPolarisConditions( + entityClass, + metadata.name, + criteria, + ).getOne(); } return super.findOne(entityClass, criteria, maybeOptions); } @@ -196,15 +204,18 @@ export class PolarisEntityManager extends EntityManager { const metadata = this.connection.getMetadata(entityClass); if (isDescendentOfCommonModel(metadata) && this.context) { if (this.context.dataVersionContext?.mapping) { - const innerJoinQuery = InnerJoinDataVersionQuery( + const { query, parameters } = InnerJoinDataVersionQuery( this.connection, this.context, metadata, + criteria, + entityClass, + this.queryRunner, ); - const result = await this.connection.manager.query(innerJoinQuery!); + const result = await this.connection.manager.query(query!, parameters); const { ids, lastId } = this.getSortedIdsToReturnByPageSize(result); this.updateOnlinePaginatedContext(ids, result, lastId); - return this.findByIds(entityClass, ids, criteria); + return this.findByIdsWithoutWhereCriteria(entityClass, ids, criteria); } else { return this.getSortedByDataVersion( entityClass, @@ -239,7 +250,17 @@ export class PolarisEntityManager extends EntityManager { ); const { ids, lastId } = this.getSortedIdsToReturnByPageSize(result); this.updateOnlinePaginatedContext(ids, result, lastId); - return this.findByIds(entityClass, ids, criteria); + return this.findByIdsWithoutWhereCriteria(entityClass, ids, criteria); + } + + private findByIdsWithoutWhereCriteria( + entityClass: EntityTarget, + ids: string[], + criteria?: FindManyOptions, + ): Promise { + const criteriaToSend = this.copyCriteria(criteria); + delete criteriaToSend?.where; + return this.findByIds(entityClass, ids, criteriaToSend); } private getSortedIdsToReturnByPageSize(result: { entityId: string; maxDV: number }[]) { @@ -403,8 +424,19 @@ export class PolarisEntityManager extends EntityManager { this.queryRunner, ); const metadata = this.connection.getMetadata(entityClass as EntityTarget); - let criteriaToSend: any = { ...criteria }; + let criteriaToSend: any; if (this.context) { + if (isDescendentOfCommonModel(metadata)) { + qb = this.findHandler.applyFindConditionsToQueryBuilder( + true, + this.context, + qb, + criteria, + shouldIncludeDeletedEntities, + ); + criteriaToSend = this.copyCriteria(criteria); + delete criteriaToSend.where; + } qb = leftJoinDataVersionFilter( this.connection, qb, @@ -413,20 +445,12 @@ export class PolarisEntityManager extends EntityManager { !shouldIncludeDeletedEntities, findSorted || false, ); - if (isDescendentOfCommonModel(metadata)) { - criteriaToSend = this.findHandler.findConditions( - true, - this.context, - criteria, - shouldIncludeDeletedEntities, - ); - } } if (findSorted) { delete criteriaToSend.relations; } if (criteriaToSend?.where) { - qb = qb.andWhere(criteriaToSend.where); + qb = setWhereCondition(qb, criteriaToSend.where); delete criteriaToSend.where; } const dateRangeFilter = this.context?.entityDateRangeFilter; @@ -445,6 +469,12 @@ export class PolarisEntityManager extends EntityManager { return FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, criteriaToSend); } + private copyCriteria(criteria: any) { + return !(criteria instanceof Array) && !(typeof criteria === 'string') + ? { ...criteria } + : {}; + } + public async startTransaction() { try { if (!this.queryRunner?.isTransactionActive) { diff --git a/packages/polaris-typeorm/src/utils/query-builder-util.ts b/packages/polaris-typeorm/src/utils/query-builder-util.ts index fb2c75c2..78e2c280 100644 --- a/packages/polaris-typeorm/src/utils/query-builder-util.ts +++ b/packages/polaris-typeorm/src/utils/query-builder-util.ts @@ -49,3 +49,17 @@ export const addDateRangeCriteria = ( ); } }; + +export const setWhereCondition = ( + qb: SelectQueryBuilder, + condition: any, + parameters?: any, +) => { + return qb.expressionMap.wheres.length === 0 + ? qb.where(condition, parameters) + : qb.andWhere(condition, parameters); +}; + +export const setWhereInIdsCondition = (qb: SelectQueryBuilder, ids: any) => { + return qb.expressionMap.wheres.length === 0 ? qb.whereInIds(ids) : qb.andWhereInIds(ids); +}; diff --git a/packages/polaris-typeorm/test/dal/author.ts b/packages/polaris-typeorm/test/dal/author.ts index 6f8f1be9..812b925d 100644 --- a/packages/polaris-typeorm/test/dal/author.ts +++ b/packages/polaris-typeorm/test/dal/author.ts @@ -9,6 +9,9 @@ export class Author extends CommonModel { @Column({ nullable: true }) public name: string; + @Column({ nullable: true }) + public nickname: string; + @OneToMany(() => Book, (books) => books.author, { cascade: true }) public books: Book[]; diff --git a/packages/polaris-typeorm/test/integration-tests/postgres-tests/find-sorted-by-data-version.test.ts b/packages/polaris-typeorm/test/integration-tests/postgres-tests/find-sorted-by-data-version.test.ts index 43bdbf86..bf56afe3 100644 --- a/packages/polaris-typeorm/test/integration-tests/postgres-tests/find-sorted-by-data-version.test.ts +++ b/packages/polaris-typeorm/test/integration-tests/postgres-tests/find-sorted-by-data-version.test.ts @@ -1,4 +1,4 @@ -import { PolarisConnection } from '../../../src'; +import { In, PolarisConnection } from '../../../src'; import { Author } from '../../dal/author'; import { Book } from '../../dal/book'; import { Chapter } from '../../dal/chapter'; @@ -19,6 +19,7 @@ afterEach(async () => { const createEntities = async (iterations: number = 15) => { for (let i = 0; i < iterations; i++) { const rowlingAuthor = new Author(rowling + i); + rowlingAuthor.nickname = 'jk ' + i; const hpBook = new Book(harryPotter + i, rowlingAuthor); const chapter1 = new Chapter(1, hpBook); const pen = new Pen(color, rowlingAuthor); @@ -65,6 +66,33 @@ describe('find sorted by data version tests', () => { expect(result.length).toEqual(3); }, ); + + it.each(joinOptions)( + 'fetch authors, add where or conditions, return according to the page size & conditions', + async (join) => { + mappingBooks.set('books', undefined); + mapping.set('Author', mappingBooks); + await createEntities(7); + const repository = await connection.getRepository(Author, dvContext(1, 3)); + const whereCondition = { + where: [ + { name: In([rowling + '0', rowling + '1', rowling + '2']) }, + { nickname: In(['jk 3', 'jk 4']) }, + ], + }; + const result = + join === joinOptions[0] + ? await repository.findSortedByDataVersionUsingInnerJoin(whereCondition) + : await repository.findSortedByDataVersionUsingLeftOuterJoin(whereCondition); + const repository2 = await connection.getRepository(Author, dvContext(11, 3)); + const result2 = + join === joinOptions[0] + ? await repository2.findSortedByDataVersionUsingInnerJoin(whereCondition) + : await repository2.findSortedByDataVersionUsingLeftOuterJoin(whereCondition); + expect(result.length).toEqual(3); + expect(result2.length).toEqual(2); + }, + ); it.each(joinOptions)('fetch last page, returns correct amount', async (join) => { mappingBooks.set('books', undefined); mapping.set('Author', mappingBooks); @@ -76,7 +104,7 @@ describe('find sorted by data version tests', () => { : await repository.findSortedByDataVersionUsingLeftOuterJoin(); expect(result.length).toEqual(2); }); - it.each(joinOptions)('fetch all heroes in two pages, returns correctly', async (join) => { + it.each(joinOptions)('fetch all authors in two pages, returns correctly', async (join) => { mappingBooks.set('books', undefined); mapping.set('Author', mappingBooks); await createEntities(5); @@ -98,26 +126,29 @@ describe('find sorted by data version tests', () => { expect(allHeroes).toEqual([...firstThree, ...lastTwo]); }); it.each(joinOptions)( - 'fetch all heroes in two pages with only root entity, returns correctly', + 'fetch all authors in two pages with filter, first page is filled to its max page', async (join) => { - mapping.set('Author', undefined); - await createEntities(5); - const allHeroesRepository = connection.getRepository(Author, dvContext(1, 5)); - const allHeroes = - join === joinOptions[0] - ? await allHeroesRepository.findSortedByDataVersionUsingInnerJoin() - : await allHeroesRepository.findSortedByDataVersionUsingLeftOuterJoin(); - const firstThreeRepository = connection.getRepository(Author, dvContext(1, 3)); - const firstThree = + mappingBooks.set('books', undefined); + mapping.set('Author', mappingBooks); + await createEntities(7); + const repository = await connection.getRepository(Author, dvContext(1, 3)); + const whereCondition = { + where: [ + { name: In([rowling + '0', rowling + '1', rowling + '5']) }, + { nickname: In(['jk 3', 'jk 4']) }, + ], + }; + const result = join === joinOptions[0] - ? await firstThreeRepository.findSortedByDataVersionUsingInnerJoin() - : await firstThreeRepository.findSortedByDataVersionUsingLeftOuterJoin(); - const lastTwoRepository = connection.getRepository(Author, dvContext(13, 2)); - const lastTwo = + ? await repository.findSortedByDataVersionUsingInnerJoin(whereCondition) + : await repository.findSortedByDataVersionUsingLeftOuterJoin(whereCondition); + const repository2 = await connection.getRepository(Author, dvContext(15, 3)); + const result2 = join === joinOptions[0] - ? await lastTwoRepository.findSortedByDataVersionUsingInnerJoin() - : await lastTwoRepository.findSortedByDataVersionUsingLeftOuterJoin(); - expect(allHeroes).toEqual([...firstThree, ...lastTwo]); + ? await repository2.findSortedByDataVersionUsingInnerJoin(whereCondition) + : await repository2.findSortedByDataVersionUsingLeftOuterJoin(whereCondition); + expect(result.length).toEqual(3); + expect(result2.length).toEqual(2); }, ); }); diff --git a/packages/polaris-typeorm/test/unit-tests/handlers/find-handler.test.ts b/packages/polaris-typeorm/test/unit-tests/handlers/find-handler.test.ts index fdb77b72..740db9b6 100644 --- a/packages/polaris-typeorm/test/unit-tests/handlers/find-handler.test.ts +++ b/packages/polaris-typeorm/test/unit-tests/handlers/find-handler.test.ts @@ -1,22 +1,58 @@ import { PolarisGraphQLContext } from '@enigmatis/polaris-common'; -import { FindManyOptions, In } from 'typeorm'; import { FindHandler } from '../../../src/handlers/find-handler'; - +const createQueryBuilderMock = () => { + const qb: any = { + alias: 'author', + expressionMap: { + wheres: [], + }, + where: jest.fn((condition, parameters) => { + qb.expressionMap.wheres = [{ condition, parameters }]; + return qb; + }), + andWhere: jest.fn((condition, parameters) => { + if (qb.expressionMap.wheres.length === 0) { + throw new Error(); + } + qb.expressionMap.wheres.push({ condition, parameters }); + return qb; + }), + andWhereInIds: jest.fn(), + }; + return qb; +}; describe('find handler tests', () => { - it('dataVersion property supplied in options or conditions and not in headers, get with data version condition', async () => { + it('name property supplied in options or conditions, get with name condition', async () => { const findHandler = new FindHandler(); - const find = findHandler.findConditions(true, {} as PolarisGraphQLContext, { - where: { dataVersion: 5 }, - }); - expect(find).toEqual({ where: { deleted: false, realityId: 0, dataVersion: 5 } }); + let testQB = createQueryBuilderMock(); + testQB = findHandler.applyFindConditionsToQueryBuilder( + true, + {} as PolarisGraphQLContext, + testQB, + { + where: { name: 'chen' }, + }, + ); + expect(testQB.expressionMap.wheres.length).toBe(3); + expect(testQB.expressionMap.wheres[0].condition).toBe('author.realityId = 0'); + expect(testQB.expressionMap.wheres[1].condition).toEqual({ name: 'chen' }); + expect(testQB.expressionMap.wheres[2].condition).toBe('author.deleted = :deleted'); + expect(testQB.expressionMap.wheres[2].parameters).toEqual({ deleted: false }); }); it('realityId property supplied in options or conditions and not in the headers, get condition of given reality', async () => { const findHandler = new FindHandler(); - const find = findHandler.findConditions(true, {} as PolarisGraphQLContext, { - where: { realityId: 3 }, - }); - expect(find).toEqual({ where: { deleted: false, realityId: 3 } }); + let testQB = createQueryBuilderMock(); + testQB = findHandler.applyFindConditionsToQueryBuilder( + true, + {} as PolarisGraphQLContext, + testQB, + { where: { realityId: 3 } }, + ); + expect(testQB.expressionMap.wheres.length).toBe(2); + expect(testQB.expressionMap.wheres[0].condition).toEqual({ realityId: 3 }); + expect(testQB.expressionMap.wheres[1].condition).toBe('author.deleted = :deleted'); + expect(testQB.expressionMap.wheres[1].parameters).toEqual({ deleted: false }); }); it('include linked oper is true in headers, get realities of real and reality in headers', async () => { @@ -24,8 +60,12 @@ describe('find handler tests', () => { requestHeaders: { realityId: 1, includeLinkedOper: true }, } as PolarisGraphQLContext; const findHandler = new FindHandler(); - const find = findHandler.findConditions(true, context, {}); - expect(find).toEqual({ where: { deleted: false, realityId: In([1, 0]) } }); + let testQB = createQueryBuilderMock(); + testQB = findHandler.applyFindConditionsToQueryBuilder(true, context, testQB, {}); + expect(testQB.expressionMap.wheres.length).toBe(2); + expect(testQB.expressionMap.wheres[0].condition).toBe('author.realityId in (1,0)'); + expect(testQB.expressionMap.wheres[1].condition).toBe('author.deleted = :deleted'); + expect(testQB.expressionMap.wheres[1].parameters).toEqual({ deleted: false }); }); it('include linked oper is true in headers, get condition of default reality', async () => { @@ -33,8 +73,12 @@ describe('find handler tests', () => { requestHeaders: { realityId: 0, includeLinkedOper: true }, } as PolarisGraphQLContext; const findHandler = new FindHandler(); - const find = findHandler.findConditions(true, context, {}); - expect(find).toEqual({ where: { deleted: false, realityId: 0 } }); + let testQB = createQueryBuilderMock(); + testQB = findHandler.applyFindConditionsToQueryBuilder(true, context, testQB, {}); + expect(testQB.expressionMap.wheres.length).toBe(2); + expect(testQB.expressionMap.wheres[0].condition).toBe('author.realityId = 0'); + expect(testQB.expressionMap.wheres[1].condition).toBe('author.deleted = :deleted'); + expect(testQB.expressionMap.wheres[1].parameters).toEqual({ deleted: false }); }); it('include linked oper is true in headers but false in find setting, get condition of reality in headers', async () => { @@ -42,25 +86,44 @@ describe('find handler tests', () => { requestHeaders: { realityId: 1, includeLinkedOper: true }, } as PolarisGraphQLContext; const findHandler = new FindHandler(); - const find = findHandler.findConditions(false, context, {}); - expect(find).toEqual({ where: { deleted: false, realityId: 1 } }); + let testQB = createQueryBuilderMock(); + testQB = findHandler.applyFindConditionsToQueryBuilder(false, context, testQB, {}); + expect(testQB.expressionMap.wheres.length).toBe(2); + expect(testQB.expressionMap.wheres[0].condition).toBe('author.realityId = 1'); + expect(testQB.expressionMap.wheres[1].condition).toBe('author.deleted = :deleted'); + expect(testQB.expressionMap.wheres[1].parameters).toEqual({ deleted: false }); }); it('deleted property supplied in options or conditions, get condition of supplied setting', async () => { const findHandler = new FindHandler(); - const find = findHandler.findConditions(true, {} as PolarisGraphQLContext, { - where: { deleted: true }, - }); - expect(find).toEqual({ where: { deleted: true, realityId: 0 } }); + let testQB = createQueryBuilderMock(); + testQB = findHandler.applyFindConditionsToQueryBuilder( + true, + {} as PolarisGraphQLContext, + testQB, + { + where: { deleted: true }, + }, + ); + expect(testQB.expressionMap.wheres.length).toBe(2); + expect(testQB.expressionMap.wheres[0].condition).toBe('author.realityId = 0'); + expect(testQB.expressionMap.wheres[1].condition).toEqual({ deleted: true }); }); it('linked oper supplied in header property, supplied in options or conditions, get only from headers reality', async () => { const findHandler = new FindHandler(); - const find = findHandler.findConditions( + let testQB = createQueryBuilderMock(); + testQB = findHandler.applyFindConditionsToQueryBuilder( true, { requestHeaders: { realityId: 1 } } as PolarisGraphQLContext, + testQB, { where: { includeLinkedOper: true } }, ); - expect(find).toEqual({ where: { deleted: false, realityId: 1, includeLinkedOper: true } }); + + expect(testQB.expressionMap.wheres.length).toBe(3); + expect(testQB.expressionMap.wheres[0].condition).toBe('author.realityId = 1'); + expect(testQB.expressionMap.wheres[1].condition).toEqual({ includeLinkedOper: true }); + expect(testQB.expressionMap.wheres[2].condition).toBe('author.deleted = :deleted'); + expect(testQB.expressionMap.wheres[2].parameters).toEqual({ deleted: false }); }); });