From d9099851da48e73622a665e8953a357de89d915b Mon Sep 17 00:00:00 2001 From: manelcecs Date: Mon, 11 Dec 2023 18:56:37 +0100 Subject: [PATCH] Temp2 --- .../flows/flow-search-service.ts | 142 +++++++++++------- src/domain-services/flows/graphql/resolver.ts | 4 +- src/domain-services/flows/graphql/types.ts | 56 +++---- src/domain-services/flows/model.ts | 8 +- ...-flow-category-conditions-strategy-impl.ts | 2 +- src/utils/graphql/pagination.ts | 2 +- 6 files changed, 127 insertions(+), 87 deletions(-) diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts index 3a410d6d..7ff4c7d8 100644 --- a/src/domain-services/flows/flow-search-service.ts +++ b/src/domain-services/flows/flow-search-service.ts @@ -1,7 +1,8 @@ import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { type Database } from '@unocha/hpc-api-core/src/db/type'; -import { Cond, Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import { Service } from 'typedi'; +import { type SortOrder } from '../../utils/graphql/pagination'; import { CategoryService } from '../categories/category-service'; import { type Category } from '../categories/graphql/types'; import { ExternalReferenceService } from '../external-reference/external-reference-service'; @@ -25,14 +26,14 @@ import { type SearchFlowsFilters, } from './graphql/args'; import { - type FlowPaged, + type Flow, type FlowParkedParentSource, type FlowSearchResult, type FlowSearchResultNonPaginated, type FlowSearchTotalAmountResult, type FlowSortField, } from './graphql/types'; -import { type FlowEntity } from './model'; +import { type FlowEntity, type FlowOrderBy } from './model'; import { type FlowSearchStrategy } from './strategy/flow-search-strategy'; import { FlowObjectFiltersStrategy } from './strategy/impl/flow-object-conditions-strategy-impl'; import { OnlyFlowFiltersStrategy } from './strategy/impl/only-flow-conditions-strategy-impl'; @@ -60,12 +61,7 @@ export class FlowSearchService { const { limit, nextPageCursor, prevPageCursor, sortField, sortOrder } = filters; - const orderBy: - | { column: FlowSortField; order: 'asc' | 'desc' } - | Array<{ column: FlowSortField; order: 'asc' | 'desc' }> = { - column: sortField ?? 'updatedAt', - order: sortOrder ?? 'desc', - }; + const orderBy: FlowOrderBy = this.buildOrderBy(sortField, sortOrder); const { flowFilters, flowObjectFilters, flowCategoryFilters } = filters; @@ -79,7 +75,8 @@ export class FlowSearchService { const { strategy, conditions } = this.determineStrategy( flowFilters, flowObjectFilters, - flowCategoryFilters + flowCategoryFilters, + orderBy ); // Fetch one more item to check for hasNextPage @@ -204,25 +201,86 @@ export class FlowSearchService { parentIDs, externalReferences, reportDetailsWithChannel, - parkedParentSource, - sortField + parkedParentSource ); }) ); + // Sort items + // FIXME: this sorts the page, not the whole result set + items.sort((a: Flow, b: Flow) => { + const nestedA = a[orderBy.entity as keyof Flow]; + const nestedB = b[orderBy.entity as keyof Flow]; + + if (nestedA && nestedB) { + const propertyA = nestedA[orderBy.column as keyof typeof nestedA]; + const propertyB = nestedB[orderBy.column as keyof typeof nestedB]; + + // Implement your custom comparison logic + // For example, compare strings or numbers + if (propertyA < propertyB) { + return orderBy.order === 'asc' ? -1 : 1; + } + if (propertyA > propertyB) { + return orderBy.order === 'asc' ? 1 : -1; + } + } + + return 0; + }); + + const isOrderByForFlows = orderBy.entity === 'flow'; + const firstItem = items[0]; + const prevPageCursorEntity = isOrderByForFlows + ? firstItem + : firstItem[orderBy.entity as keyof typeof firstItem]; + const prevPageCursorValue = prevPageCursorEntity + ? prevPageCursorEntity[ + orderBy.column as keyof typeof prevPageCursorEntity + ] ?? '' + : ''; + + const lastItem = items.at(-1); + const nextPageCursorEntity = isOrderByForFlows + ? lastItem + : lastItem![orderBy.entity as keyof typeof lastItem]; + const nextPageCursorValue = nextPageCursorEntity + ? nextPageCursorEntity[ + orderBy.column as keyof typeof nextPageCursorEntity + ]?.toString() ?? '' + : ''; + + // TODO: implement nested cursors for page return { flows: items, hasNextPage: limit <= flows.length, hasPreviousPage: nextPageCursor !== undefined, - prevPageCursor: flows.length ? items[0].cursor : '', - nextPageCursor: flows.length ? items.at(-1)?.cursor ?? '' : '', + prevPageCursor: prevPageCursorValue, + nextPageCursor: nextPageCursorValue, pageSize: flows.length, - sortField: sortField ?? 'updatedAt', + sortField: `${orderBy.entity}.${orderBy.column}` as FlowSortField, sortOrder: sortOrder ?? 'desc', total: count, }; } + buildOrderBy(sortField?: FlowSortField, sortOrder?: SortOrder) { + const orderBy: FlowOrderBy = { + column: sortField ?? 'updatedAt', + order: sortOrder ?? ('desc' as SortOrder), + entity: 'flow', + }; + + // Check if sortField is a nested property + if (orderBy.column.includes('.')) { + const [nestedEntity, propertyToSort] = orderBy.column.split('.'); + // Update orderBy object with nested information + orderBy.column = propertyToSort; + orderBy.entity = nestedEntity; + } + + return orderBy; + } prepareFlowConditions(flowFilters: SearchFlowsFilters): any { let flowConditions = {}; @@ -278,7 +336,8 @@ export class FlowSearchService { determineStrategy( flowFilters: SearchFlowsFilters, flowObjectFilters: FlowObjectFilters[], - flowCategoryFilters: FlowCategoryFilters + flowCategoryFilters: FlowCategoryFilters, + orderBy?: FlowOrderBy ): { strategy: FlowSearchStrategy; conditions: any } { const isFlowFilterDefined = flowFilters !== undefined; const isFlowObjectFilterDefined = flowObjectFilters !== undefined; @@ -287,6 +346,8 @@ export class FlowSearchService { const isFlowCategoryFilterDefined = flowCategoryFilters !== undefined; + const isOrderByForFlows = orderBy?.entity === 'flow'; + if ( (!isFlowFilterDefined && (!isFlowObjectFilterDefined || !isFlowObjectFiltersNotEmpty) && @@ -296,6 +357,15 @@ export class FlowSearchService { !isFlowCategoryFilterDefined) ) { const flowConditions = this.prepareFlowConditions(flowFilters); + if (!isOrderByForFlows) { + return { + strategy: this.flowObjectFiltersStrategy, + conditions: { + conditionsMap: this.buildConditionsMap(flowConditions, {}), + flowCategoryFilters, + }, + }; + } return { strategy: this.onlyFlowFiltersStrategy, conditions: flowConditions, @@ -412,9 +482,7 @@ export class FlowSearchService { private buildCursorCondition( beforeCursor: string, afterCursor: string, - orderBy: - | { column: FlowSortField; order: 'asc' | 'desc' } - | Array<{ column: FlowSortField; order: 'asc' | 'desc' }> + orderBy: FlowOrderBy ) { if (beforeCursor && afterCursor) { throw new Error('Cannot use before and after cursor at the same time'); @@ -424,20 +492,6 @@ export class FlowSearchService { return {}; } - if (Array.isArray(orderBy)) { - // Build iterations of cursor conditions - const cursorConditions = orderBy.map((orderBy) => { - return this.buildCursorConditionForSingleOrderBy( - beforeCursor, - afterCursor, - orderBy - ); - }); - - // Combine cursor conditions - return { [Cond.AND]: cursorConditions }; - } - return this.buildCursorConditionForSingleOrderBy( beforeCursor, afterCursor, @@ -448,7 +502,7 @@ export class FlowSearchService { private buildCursorConditionForSingleOrderBy( beforeCursor: string, afterCursor: string, - orderBy: { column: FlowSortField; order: 'asc' | 'desc' } + orderBy: FlowOrderBy ) { let cursorCondition; @@ -482,20 +536,8 @@ export class FlowSearchService { parentIDs: number[], externalReferences: any[], reportDetails: any[], - parkedParentSource: FlowParkedParentSource[], - sortColumn?: FlowSortField - ): FlowPaged { - let cursor = sortColumn ? flow[sortColumn] : flow.updatedAt; - - if (cursor instanceof Date) { - cursor = cursor.toISOString(); - } else if (typeof cursor === 'number') { - cursor = cursor.toString(); - } else if (typeof cursor === 'boolean' || cursor === null) { - // cases such as 'boolean' - cursor = flow.id.toString(); - } - + parkedParentSource: FlowParkedParentSource[] + ): Flow { return { // Mandatory fields id: flow.id.valueOf(), @@ -518,8 +560,6 @@ export class FlowSearchService { externalReferences, reportDetails, parkedParentSource, - // Paged item field - cursor, }; } @@ -558,7 +598,7 @@ export class FlowSearchService { ): Promise { const flowSearchResponse = await this.search(models, args); - const flows: FlowPaged[] = flowSearchResponse.flows; + const flows: Flow[] = flowSearchResponse.flows; let hasNextPage = flowSearchResponse.hasNextPage; diff --git a/src/domain-services/flows/graphql/resolver.ts b/src/domain-services/flows/graphql/resolver.ts index 1e149518..241bb988 100644 --- a/src/domain-services/flows/graphql/resolver.ts +++ b/src/domain-services/flows/graphql/resolver.ts @@ -4,14 +4,14 @@ import Context from '../../Context'; import { FlowSearchService } from '../flow-search-service'; import { SearchFlowsArgs, SearchFlowsArgsNonPaginated } from './args'; import { - FlowPaged, + Flow, FlowSearchResult, FlowSearchResultNonPaginated, FlowSearchTotalAmountResult, } from './types'; @Service() -@Resolver(FlowPaged) +@Resolver(Flow) export default class FlowResolver { constructor(private flowSearchService: FlowSearchService) {} diff --git a/src/domain-services/flows/graphql/types.ts b/src/domain-services/flows/graphql/types.ts index 7371ce73..30964bba 100644 --- a/src/domain-services/flows/graphql/types.ts +++ b/src/domain-services/flows/graphql/types.ts @@ -1,6 +1,6 @@ import { Field, ObjectType } from 'type-graphql'; import { BaseType } from '../../../utils/graphql/base-types'; -import { PageInfo, type IItemPaged } from '../../../utils/graphql/pagination'; +import { PageInfo } from '../../../utils/graphql/pagination'; import { Category } from '../../categories/graphql/types'; import { BaseLocation } from '../../location/graphql/types'; import { Organization } from '../../organizations/graphql/types'; @@ -98,22 +98,16 @@ export class Flow extends BaseFlow { parkedParentSource: FlowParkedParentSource[]; } -@ObjectType() -export class FlowPaged extends Flow implements IItemPaged { - @Field(() => String, { nullable: false }) - cursor: string; -} - @ObjectType() export class FlowSearchResult extends PageInfo { - @Field(() => [FlowPaged], { nullable: false }) - flows: FlowPaged[]; + @Field(() => [Flow], { nullable: false }) + flows: Flow[]; } @ObjectType() export class FlowSearchResultNonPaginated { - @Field(() => [FlowPaged], { nullable: false }) - flows: FlowPaged[]; + @Field(() => [Flow], { nullable: false }) + flows: Flow[]; @Field(() => Number, { nullable: false }) flowsCount: number; @@ -129,23 +123,23 @@ export class FlowSearchTotalAmountResult { } export type FlowSortField = - | 'id' - | 'versionID' - | 'amountUSD' - | 'updatedAt' - | 'activeStatus' - | 'restricted' - | 'newMoney' - | 'flowDate' - | 'decisionDate' - | 'firstReportedDate' - | 'budgetYear' - | 'origAmount' - | 'origCurrency' - | 'exchangeRate' - | 'description' - | 'notes' - | 'versionStartDate' - | 'versionEndDate' - | 'createdAt' - | 'deletedAt'; + | 'flow.id' + | 'flow.versionID' + | 'flow.amountUSD' + | 'flow.updatedAt' + | 'flow.activeStatus' + | 'flow.restricted' + | 'flow.newMoney' + | 'flow.flowDate' + | 'flow.decisionDate' + | 'flow.firstReportedDate' + | 'flow.budgetYear' + | 'flow.origAmount' + | 'flow.origCurrency' + | 'flow.exchangeRate' + | 'flow.description' + | 'flow.notes' + | 'flow.versionStartDate' + | 'flow.versionEndDate' + | 'flow.createdAt' + | 'flow.deletedAt'; diff --git a/src/domain-services/flows/model.ts b/src/domain-services/flows/model.ts index 3c425a7e..4a1eb492 100644 --- a/src/domain-services/flows/model.ts +++ b/src/domain-services/flows/model.ts @@ -1,4 +1,10 @@ import { type Database } from '@unocha/hpc-api-core/src/db'; import { type InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; - +import { type SortOrder } from '../../utils/graphql/pagination'; export type FlowEntity = InstanceDataOfModel; + +export type FlowOrderBy = { + column: string; + order: SortOrder; + entity: string; +}; diff --git a/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts b/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts index 1225e431..3e95bc83 100644 --- a/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts @@ -20,7 +20,7 @@ export class GetFlowIdsFromCategoryConditionsStrategyImpl async search( models: Database, - flowObjectsConditions: Map>, + _flowObjectsConditions: Map>, flowCategoryConditions: FlowCategoryFilters ): Promise { const whereClause = mapFlowCategoryConditionsToWhereClause( diff --git a/src/utils/graphql/pagination.ts b/src/utils/graphql/pagination.ts index 797d8a85..4d77af65 100644 --- a/src/utils/graphql/pagination.ts +++ b/src/utils/graphql/pagination.ts @@ -36,7 +36,7 @@ export class PageInfo { } export function prepareConditionFromCursor( - sortCondition: { column: string; order: 'asc' | 'desc' }, + sortCondition: { column: string; order: SortOrder }, afterCursor?: number, beforeCursor?: number ): any {