diff --git a/src/common-libs/plan/index.ts b/src/common-libs/plan/index.ts index 71eff3c3..c3e385e8 100644 --- a/src/common-libs/plan/index.ts +++ b/src/common-libs/plan/index.ts @@ -10,7 +10,7 @@ export const getNextTag = async ( ): Promise => { const latestPlanTag = await models.planTag.find({ where: { - planId: createBrandedValue(planTag.planId), + planId: planTag.planId, }, orderBy: { column: 'createdAt', order: 'desc' }, }); @@ -38,7 +38,7 @@ export const setPlanReportingPeriod = async ( ) => { const reportingPeriods = await models.planReportingPeriod.find({ where: { - planId: createBrandedValue(planTag.planId), + planId: planTag.planId, }, orderBy: { column: 'periodNumber', order: 'asc' }, }); @@ -51,7 +51,7 @@ export const setPlanReportingPeriod = async ( const latestPlanVersion = await models.planVersion.findOne({ where: { - planId: createBrandedValue(planTag.planId), + planId: planTag.planId, latestVersion: true, }, }); diff --git a/src/common-libs/plan/versioning.ts b/src/common-libs/plan/versioning.ts index 8c3b5dc5..d2ff5ef8 100644 --- a/src/common-libs/plan/versioning.ts +++ b/src/common-libs/plan/versioning.ts @@ -151,11 +151,11 @@ const updateBaseAndVersionModelTags = async ( } idSet.add(baseRow.id); } else { - inactiveRows.push(createBrandedValue(baseRow.id)); + inactiveRows.push(baseRow.id); } } - for (const [versionTagsString, rowIds] of activeRows.entries()) { + for (const [versionTagsString, rowIds] of activeRows) { const versionTags = versionTagsString === '' ? [] : versionTagsString.split(','); @@ -231,7 +231,7 @@ const updateBaseAndVersionModelTags = async ( idSet.add(latestVersion.id); } - for (const [versionTagsString, rowIds] of versionTagsMap.entries()) { + for (const [versionTagsString, rowIds] of versionTagsMap) { const versionTags = versionTagsString === '' ? [] : versionTagsString.split(','); @@ -329,7 +329,7 @@ const updateBaseModelTags = async ( }, }); } else { - inactiveRows.push(createBrandedValue(baseRow.id)); + inactiveRows.push(baseRow.id); } } diff --git a/src/domain-services/categories/category-service.ts b/src/domain-services/categories/category-service.ts index 6c84ace3..89a7bf88 100644 --- a/src/domain-services/categories/category-service.ts +++ b/src/domain-services/categories/category-service.ts @@ -1,4 +1,5 @@ import { type Database } from '@unocha/hpc-api-core/src/db'; +import type { CategoryId } from '@unocha/hpc-api-core/src/db/models/category'; import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { Cond, @@ -7,6 +8,7 @@ import { } from '@unocha/hpc-api-core/src/db/util/conditions'; import { type InstanceOfModel } from '@unocha/hpc-api-core/src/db/util/types'; import { getOrCreate } from '@unocha/hpc-api-core/src/util'; +import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; import { Service } from 'typedi'; import { type ReportDetail } from '../report-details/graphql/types'; import { type Category } from './graphql/types'; @@ -25,29 +27,24 @@ export class CategoryService { async getCategoriesForFlows( flowWithVersion: Map, models: Database - ): Promise>> { + ): Promise>> { // Group of flowIDs and its versions // Structure: // flowID: { // versionID: [categories] // } - const flowVersionCategoryMap = new Map>(); + const flowVersionCategoryMap = new Map>(); - const flowIDs: FlowId[] = []; - for (const flowID of flowWithVersion.keys()) { - flowIDs.push(flowID); - } - - const categoriesRef: CategoryRefInstance[] = await models.categoryRef.find({ + const categoriesRef = await models.categoryRef.find({ where: { objectID: { - [Op.IN]: flowIDs, + [Op.IN]: flowWithVersion.keys(), }, objectType: 'flow', }, }); - const categories: CategoryInstance[] = await models.category.find({ + const categories = await models.category.find({ where: { id: { [Op.IN]: categoriesRef.map((catRef) => catRef.categoryID), @@ -57,7 +54,7 @@ export class CategoryService { // Populate the map with categories for each flow for (const catRef of categoriesRef) { - const flowId = catRef.objectID.valueOf(); + const flowId: FlowId = createBrandedValue(catRef.objectID); if (!flowVersionCategoryMap.has(flowId)) { flowVersionCategoryMap.set(flowId, new Map()); @@ -70,14 +67,9 @@ export class CategoryService { () => new Map() ); - const flowVersion = catRef.versionID; - if (!flowVersionMap.has(flowVersion)) { - flowVersionMap.set(flowVersion, []); - } - const categoriesPerFlowVersion = getOrCreate( flowVersionMap, - flowVersion, + catRef.versionID, () => [] ); @@ -85,9 +77,7 @@ export class CategoryService { if ( category && - !categoriesPerFlowVersion.some( - (cat) => cat.id === category.id.valueOf() - ) + !categoriesPerFlowVersion.some((cat) => cat.id === category.id) ) { const mappedCategory = this.mapCategoryToFlowCategory(category, catRef); categoriesPerFlowVersion.push(mappedCategory); @@ -108,13 +98,13 @@ export class CategoryService { createdAt: category.createdAt.toISOString(), updatedAt: category.updatedAt.toISOString(), description: category.description ?? '', - parentID: category.parentID ? category.parentID.valueOf() : null, + parentID: category.parentID, code: category.code ?? '', categoryRef: { - objectID: categoryRef.objectID.valueOf(), + objectID: categoryRef.objectID, versionID: categoryRef.versionID, objectType: categoryRef.objectType, - categoryID: category.id.valueOf(), + categoryID: category.id, createdAt: categoryRef.createdAt.toISOString(), updatedAt: categoryRef.updatedAt.toISOString(), }, @@ -137,41 +127,39 @@ export class CategoryService { listOfCategoryRefORs.push(orClause); } - const categoriesRef: CategoryRefInstance[] = await models.categoryRef.find({ + const categoriesRef = await models.categoryRef.find({ where: { [Cond.OR]: listOfCategoryRefORs, }, }); - const mapOfCategoriesAndReportDetails = new Map(); + const mapOfCategoriesAndReportDetails = new Map< + CategoryId, + ReportDetail[] + >(); for (const categoryRef of categoriesRef) { const reportDetail = reportDetails.find( - (reportDetail) => reportDetail.id === categoryRef.objectID.valueOf() + (reportDetail) => reportDetail.id === categoryRef.objectID ); if (!reportDetail) { continue; } - if ( - !mapOfCategoriesAndReportDetails.has(categoryRef.categoryID.valueOf()) - ) { - mapOfCategoriesAndReportDetails.set( - categoryRef.categoryID.valueOf(), - [] - ); + if (!mapOfCategoriesAndReportDetails.has(categoryRef.categoryID)) { + mapOfCategoriesAndReportDetails.set(categoryRef.categoryID, []); } const reportDetailsPerCategory = getOrCreate( mapOfCategoriesAndReportDetails, - categoryRef.categoryID.valueOf(), + categoryRef.categoryID, () => [] ); reportDetailsPerCategory.push(reportDetail); } - const categories: CategoryInstance[] = await models.category.find({ + const categories = await models.category.find({ where: { id: { [Op.IN]: categoriesRef.map((catRef) => catRef.categoryID), @@ -179,10 +167,7 @@ export class CategoryService { }, }); - for (const [ - category, - reportDetails, - ] of mapOfCategoriesAndReportDetails.entries()) { + for (const [category, reportDetails] of mapOfCategoriesAndReportDetails) { const categoryObj = categories.find((cat) => cat.id === category); if (!categoryObj) { @@ -249,9 +234,9 @@ export class CategoryService { const shortcutFilters: ShortcutCategoryFilter[] = usedFilters .map((filter) => { - const categoryId = categories - .find((category) => category.name.includes(filter.category)) - ?.id.valueOf(); + const categoryId = categories.find((category) => + category.name.includes(filter.category) + )?.id; return { category: filter.category, diff --git a/src/domain-services/categories/graphql/types.ts b/src/domain-services/categories/graphql/types.ts index 21b8eee5..56560895 100644 --- a/src/domain-services/categories/graphql/types.ts +++ b/src/domain-services/categories/graphql/types.ts @@ -1,4 +1,5 @@ -import { Field, Int, ObjectType } from 'type-graphql'; +import type { CategoryId } from '@unocha/hpc-api-core/src/db/models/category'; +import { Field, ID, Int, ObjectType } from 'type-graphql'; import { BaseType } from '../../base-types'; @ObjectType() @@ -18,8 +19,8 @@ export class CategoryRef extends BaseType { @ObjectType() export class Category extends BaseType { - @Field({ nullable: true }) - id: number; + @Field(() => ID, { nullable: true }) + id: CategoryId | null; @Field({ nullable: false }) name: string; diff --git a/src/domain-services/categories/model.ts b/src/domain-services/categories/model.ts index e008d0ed..79c2545f 100644 --- a/src/domain-services/categories/model.ts +++ b/src/domain-services/categories/model.ts @@ -1,7 +1,8 @@ +import type { CategoryId } from '@unocha/hpc-api-core/src/db/models/category'; import { type Op } from '@unocha/hpc-api-core/src/db/util/conditions'; export type ShortcutCategoryFilter = { category: string; operation: typeof Op.IN | typeof Op.NOT_IN; - id?: number; + id?: CategoryId; }; diff --git a/src/domain-services/external-reference/external-reference-service.ts b/src/domain-services/external-reference/external-reference-service.ts index d754d176..9b699b3f 100644 --- a/src/domain-services/external-reference/external-reference-service.ts +++ b/src/domain-services/external-reference/external-reference-service.ts @@ -3,10 +3,8 @@ import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import { type InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; import { type InstanceOfModel } from '@unocha/hpc-api-core/src/db/util/types'; -import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; import { Service } from 'typedi'; import { type FlowExternalReference } from '../flows/graphql/types'; -import { type UniqueFlowEntity } from '../flows/model'; import { type SystemID } from '../report-details/graphql/types'; @Service() @@ -21,7 +19,7 @@ export class ExternalReferenceService { skipValidation: true, }); - const externalReferencesMap = new Map(); + const externalReferencesMap = new Map(); // First we add all flowIDs to the map // Since there might be flows without external references @@ -51,7 +49,7 @@ export class ExternalReferenceService { async getUniqueFlowIDsBySystemID( models: Database, systemID: SystemID - ): Promise { + ): Promise> { const externalRefences: Array< InstanceDataOfModel > = await models.externalReference.find({ @@ -61,13 +59,11 @@ export class ExternalReferenceService { skipValidation: true, }); - const flowIDs: UniqueFlowEntity[] = []; - - for (const reference of externalRefences) { - flowIDs.push(this.mapExternalDataToUniqueFlowEntity(reference)); - } - - return flowIDs; + return new Set( + externalRefences.map((externalReference) => { + return `${externalReference.flowID}:${externalReference.versionID}`; + }) + ); } private mapExternalReferenceToExternalReferenceFlows( @@ -83,13 +79,4 @@ export class ExternalReferenceService { versionID: externalReference.versionID ?? 0, }; } - - private mapExternalDataToUniqueFlowEntity( - external: InstanceDataOfModel - ): UniqueFlowEntity { - return { - id: createBrandedValue(external.flowID), - versionID: external.versionID, - }; - } } diff --git a/src/domain-services/flow-link/flow-link-service.ts b/src/domain-services/flow-link/flow-link-service.ts index 03a0fdce..10c09810 100644 --- a/src/domain-services/flow-link/flow-link-service.ts +++ b/src/domain-services/flow-link/flow-link-service.ts @@ -10,7 +10,7 @@ export class FlowLinkService { async getFlowLinksForFlows( flowIds: FlowId[], models: Database - ): Promise>>> { + ): Promise>>> { // Fetch all flow links in one go const flowLinks = await models.flowLink.find({ where: { diff --git a/src/domain-services/flow-object/flow-object-service.ts b/src/domain-services/flow-object/flow-object-service.ts index da97b58e..50de9bff 100644 --- a/src/domain-services/flow-object/flow-object-service.ts +++ b/src/domain-services/flow-object/flow-object-service.ts @@ -9,7 +9,6 @@ import type { FieldsOfModel, InstanceOfModel, } from '@unocha/hpc-api-core/src/db/util/types'; -import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; import { Service } from 'typedi'; import { type UniqueFlowEntity } from '../flows/model'; import { buildSearchFlowsObjectConditions } from '../flows/strategy/impl/utils'; @@ -22,6 +21,7 @@ type FlowObjectInstance = InstanceOfModel; export type FlowObjectsFieldsDefinition = FieldsOfModel; export type FlowObjectOrderByCond = OrderByCond; export type FlowObjectWhere = Condition; + @Service() export class FlowObjectService { // Merge with getFlowsObjectsByFlows @@ -48,7 +48,7 @@ export class FlowObjectService { ...new Set( flowObjects.map((flowObject) => { return { - id: createBrandedValue(flowObject.flowID), + id: flowObject.flowID, versionID: flowObject.versionID, }; }) @@ -92,7 +92,7 @@ export class FlowObjectService { distinctColumns.reverse(); } - const flowsObjects: FlowObjectInstance[] = await models.flowObject.find({ + const flowsObjects = await models.flowObject.find({ orderBy, where: whereClauses, distinct: distinctColumns, diff --git a/src/domain-services/flow-object/utils.ts b/src/domain-services/flow-object/utils.ts index 420875e4..d7375396 100644 --- a/src/domain-services/flow-object/utils.ts +++ b/src/domain-services/flow-object/utils.ts @@ -5,7 +5,7 @@ import { type FlowObjectFilterGrouped } from './model'; /** * This alg iterates over the flowObjectFilters and creates a join for each flowObjectType * and refDirection allowing to filter the flowObjects by the flowObjectType and refDirection - * inclusivelly for each + * inclusively for each * @param flowObjectFiltersGrouped * @returns FlowObjectWhere */ @@ -13,8 +13,8 @@ export function buildWhereConditionsForFlowObjectFilters( flowObjectFiltersGrouped: FlowObjectFilterGrouped ): FlowObjectWhere { const ANDConditions = []; - for (const [flowObjectType, group] of flowObjectFiltersGrouped.entries()) { - for (const [direction, ids] of group.entries()) { + for (const [flowObjectType, group] of flowObjectFiltersGrouped) { + for (const [direction, ids] of group) { const condition = { [Cond.AND]: [ { diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts index 5395bce8..33bb166f 100644 --- a/src/domain-services/flows/flow-search-service.ts +++ b/src/domain-services/flows/flow-search-service.ts @@ -385,7 +385,7 @@ export class FlowSearchService { ): Flow { return { // Mandatory fields - id: flow.id.valueOf(), + id: flow.id, versionID: flow.versionID, amountUSD: flow.amountUSD.toString(), createdAt: flow.createdAt.toISOString(), @@ -494,14 +494,14 @@ export class FlowSearchService { const sourceUsageYearFilter: FlowObjectFilters = { objectType: 'usageYear', direction: 'source', - objectID: usageYear.id.valueOf(), + objectID: usageYear.id, inclusive: true, }; const destinationUsageYearFilter: FlowObjectFilters = { objectType: 'usageYear', direction: 'destination', - objectID: usageYear.id.valueOf(), + objectID: usageYear.id, inclusive: true, }; diff --git a/src/domain-services/flows/flow-service.ts b/src/domain-services/flows/flow-service.ts index 095c0268..3cdc2088 100644 --- a/src/domain-services/flows/flow-service.ts +++ b/src/domain-services/flows/flow-service.ts @@ -96,7 +96,7 @@ export class FlowService { const refDirection = orderBy.direction ?? 'source'; let entityIDsSorted: number[] = []; - + let entityCondKeyFlowObjectType: FlowObjectType; switch (entity) { case 'emergency': { columns = getTableColumns(database.emergency); @@ -118,9 +118,8 @@ export class FlowService { orderBy: orderByEmergency, }); - entityIDsSorted = emergencies.map((emergency) => - emergency.id.valueOf() - ); + entityIDsSorted = emergencies.map((emergency) => emergency.id); + entityCondKeyFlowObjectType = 'emergency' satisfies FlowObjectType; break; } case 'globalCluster': { @@ -142,9 +141,11 @@ export class FlowService { orderBy: orderByGlobalCluster, }); - entityIDsSorted = globalClusters.map((globalCluster) => - globalCluster.id.valueOf() + entityIDsSorted = globalClusters.map( + (globalCluster) => globalCluster.id ); + entityCondKeyFlowObjectType = 'globalCluster' satisfies FlowObjectType; + break; } case 'governingEntity': { @@ -166,9 +167,12 @@ export class FlowService { orderBy: orderByGoverningEntity, }); - entityIDsSorted = governingEntities.map((governingEntity) => - governingEntity.id.valueOf() + entityIDsSorted = governingEntities.map( + (governingEntity) => governingEntity.id ); + entityCondKeyFlowObjectType = + 'governingEntity' satisfies FlowObjectType; + break; } case 'location': { @@ -190,7 +194,9 @@ export class FlowService { orderBy: orderByLocation, }); - entityIDsSorted = locations.map((location) => location.id.valueOf()); + entityIDsSorted = locations.map((location) => location.id); + entityCondKeyFlowObjectType = 'location' satisfies FlowObjectType; + break; } case 'organization': { @@ -212,9 +218,9 @@ export class FlowService { orderBy: orderByOrganization, }); - entityIDsSorted = organizations.map((organization) => - organization.id.valueOf() - ); + entityIDsSorted = organizations.map((organization) => organization.id); + entityCondKeyFlowObjectType = 'organization' satisfies FlowObjectType; + break; } case 'plan': { @@ -236,7 +242,9 @@ export class FlowService { orderBy: orderByPlan, }); - entityIDsSorted = plans.map((plan) => plan.id.valueOf()); + entityIDsSorted = plans.map((plan) => plan.id); + entityCondKeyFlowObjectType = 'plan' satisfies FlowObjectType; + break; } case 'project': { @@ -258,7 +266,9 @@ export class FlowService { orderBy: orderByProject, }); - entityIDsSorted = projects.map((project) => project.id.valueOf()); + entityIDsSorted = projects.map((project) => project.id); + entityCondKeyFlowObjectType = 'project' satisfies FlowObjectType; + break; } case 'usageYear': { @@ -280,7 +290,9 @@ export class FlowService { orderBy: orderByUsageYear, }); - entityIDsSorted = usageYears.map((usageYear) => usageYear.id.valueOf()); + entityIDsSorted = usageYears.map((usageYear) => usageYear.id); + entityCondKeyFlowObjectType = 'usageYear' satisfies FlowObjectType; + break; } case 'planVersion': { @@ -292,7 +304,7 @@ export class FlowService { ); } // Get planVersion entities sorted - // Collect fisrt part of the entity key by the fisrt Case letter + // Collect first part of the entity key by the first Case letter const entityKey = `${ entity.split(/[A-Z]/)[0] }Id` as keyof InstanceOfModel; @@ -307,9 +319,9 @@ export class FlowService { orderBy: orderByPlanVersion, }); - entityIDsSorted = planVersions.map((planVersion) => - planVersion.planId.valueOf() - ); + entityIDsSorted = planVersions.map((planVersion) => planVersion.planId); + entityCondKeyFlowObjectType = 'plan' satisfies FlowObjectType; + break; } default: { @@ -319,8 +331,6 @@ export class FlowService { // After getting the sorted entityID list // we can now get the flowObjects - const entityCondKey = orderBy.entity as unknown; - const entityCondKeyFlowObjectType = entityCondKey as FlowObjectType; // Order map const orderMap = new Map(); @@ -384,7 +394,7 @@ export class FlowService { (flowLink) => flowLink.parentID !== flow.id && flowLink.childID === flow.id ) - .map((flowLink) => flowLink.parentID.valueOf()); + .map((flowLink) => flowLink.parentID); if (flowLinksParentsIDs.length === 0) { return null; @@ -396,7 +406,7 @@ export class FlowService { }, }); - const parentFlows: number[] = []; + const parentFlows: FlowId[] = []; for (const flowLinkParentID of flowLinksParentsIDs) { const parkedParentCategoryRef = await models.categoryRef.find({ @@ -419,7 +429,7 @@ export class FlowService { const parkedParentOrganizationFlowObject = await models.flowObject.findOne({ where: { - flowID: createBrandedValue(parentFlow), + flowID: parentFlow, objectType: 'organization', refDirection: 'source', versionID: flow.versionID, @@ -451,7 +461,7 @@ export class FlowService { for (const parkedParentOrganization of parkedParentOrganizations) { mappedParkedParentOrganizations.organization.push( - parkedParentOrganization.id.valueOf() + parkedParentOrganization.id ); mappedParkedParentOrganizations.orgName.push( parkedParentOrganization.name diff --git a/src/domain-services/flows/graphql/args.ts b/src/domain-services/flows/graphql/args.ts index 367e3e13..786beb55 100644 --- a/src/domain-services/flows/graphql/args.ts +++ b/src/domain-services/flows/graphql/args.ts @@ -1,4 +1,6 @@ -import { ArgsType, Field, InputType, Int } from 'type-graphql'; +import type { CategoryId } from '@unocha/hpc-api-core/src/db/models/category'; +import type { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { ArgsType, Field, ID, InputType, Int } from 'type-graphql'; import { PaginationArgs } from '../../../utils/graphql/pagination'; import { FlowObjectType } from '../../flow-object/model'; import { type SystemID } from '../../report-details/graphql/types'; @@ -6,8 +8,8 @@ import { type FlowSortField, type FlowStatusFilter } from './types'; @InputType() export class SearchFlowsFilters { - @Field(() => [Int], { nullable: true }) - id: number[] | null; + @Field(() => [ID], { nullable: true }) + id: FlowId[] | null; @Field(() => Boolean, { nullable: true }) activeStatus: boolean | null; @@ -57,8 +59,8 @@ export class FlowObjectFilters { @InputType() export class FlowCategory { - @Field(() => Number, { nullable: true }) - id: number; + @Field(() => ID, { nullable: true }) + id: CategoryId | null; @Field({ nullable: true }) group: string; diff --git a/src/domain-services/flows/graphql/types.ts b/src/domain-services/flows/graphql/types.ts index 1280c9dd..afade41e 100644 --- a/src/domain-services/flows/graphql/types.ts +++ b/src/domain-services/flows/graphql/types.ts @@ -1,4 +1,5 @@ -import { Field, ObjectType } from 'type-graphql'; +import type { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { Field, ID, ObjectType } from 'type-graphql'; import { PageInfo } from '../../../utils/graphql/pagination'; import { BaseType } from '../../base-types'; import { Category } from '../../categories/graphql/types'; @@ -7,6 +8,7 @@ import { Organization } from '../../organizations/graphql/types'; import { BasePlan } from '../../plans/graphql/types'; import { ReportDetail } from '../../report-details/graphql/types'; import { UsageYear } from '../../usage-years/graphql/types'; +import { FlowKeys } from '../model'; @ObjectType() export class FlowExternalReference { @@ -46,8 +48,8 @@ export class FlowParkedParentSource { @ObjectType() export class BaseFlow extends BaseType { - @Field(() => Number, { nullable: false }) - id: number; + @Field(() => ID, { nullable: false }) + id: FlowId; @Field(() => Number, { nullable: false }) versionID: number; @@ -175,26 +177,6 @@ export class FlowSearchResultNonPaginated { flowsCount: number; } -export type FlowSortField = - | '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'; +export type FlowSortField = `flow.${FlowKeys}` export type FlowStatusFilter = 'new' | 'updated' | undefined; diff --git a/src/domain-services/flows/model.ts b/src/domain-services/flows/model.ts index 1944ca10..8f02a25f 100644 --- a/src/domain-services/flows/model.ts +++ b/src/domain-services/flows/model.ts @@ -8,11 +8,13 @@ import type { } from '@unocha/hpc-api-core/src/db/util/types'; import { type SortOrder } from '../../utils/graphql/pagination'; import { type EntityDirection } from '../base-types'; +import { type FlowObjectType } from '../flow-object/model'; export type FlowModel = Database['flow']; export type FlowInstance = InstanceOfModel; export type FlowWhere = Condition; export type FlowFieldsDefinition = FieldsOfModel; +export type FlowKeys = Extract; export type FlowOrderByCond = OrderByCond; // Can this be simplified somehow? export type UniqueFlowEntity = { id: FlowId; @@ -20,10 +22,20 @@ export type UniqueFlowEntity = { }; export type FlowOrderByWithSubEntity = { - column: keyof FlowInstance | string; + column: + | keyof FlowInstance + | keyof InstanceOfModel + | keyof InstanceOfModel + | keyof InstanceOfModel + | keyof InstanceOfModel + | keyof InstanceOfModel + | keyof InstanceOfModel + | keyof InstanceOfModel + | keyof InstanceOfModel + | keyof InstanceOfModel; order: SortOrder; - entity: string; - subEntity?: string; + entity: 'flow' | 'externalReference'; + subEntity?: FlowObjectType | 'planVersion'; direction?: EntityDirection; }; 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 96d9d393..5c90fad3 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 @@ -50,14 +50,14 @@ export class GetFlowIdsFromCategoryConditionsStrategyImpl if (shortcutFilters) { for (const shortcut of shortcutFilters) { + if (!shortcut.id) { + continue; + } + if (shortcut.operation === Op.IN) { - categoriesIdsFromShortcutFilterIN.push( - createBrandedValue(shortcut.id) - ); + categoriesIdsFromShortcutFilterIN.push(shortcut.id); } else { - categoriesIdsFromShortcutFilterNOTIN.push( - createBrandedValue(shortcut.id) - ); + categoriesIdsFromShortcutFilterNOTIN.push(shortcut.id); } } } diff --git a/src/domain-services/flows/strategy/impl/get-flowIds-flow-from-nested-flow-filters-strategy-impl.ts b/src/domain-services/flows/strategy/impl/get-flowIds-flow-from-nested-flow-filters-strategy-impl.ts index 109348df..383dbc9e 100644 --- a/src/domain-services/flows/strategy/impl/get-flowIds-flow-from-nested-flow-filters-strategy-impl.ts +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-from-nested-flow-filters-strategy-impl.ts @@ -3,13 +3,12 @@ import { ExternalReferenceService } from '../../../external-reference/external-r import { LegacyService } from '../../../legacy/legacy-service'; import { ReportDetailService } from '../../../report-details/report-detail-service'; import { FlowService } from '../../flow-service'; -import type { UniqueFlowEntity } from '../../model'; import { type FlowIDSearchStrategy, type FlowIdSearchStrategyArgs, type FlowIdSearchStrategyResponse, } from '../flowID-search-strategy'; -import { intersectUniqueFlowEntities } from './utils'; +import { intersectSets, parseFlowIdVersionSet } from './utils'; @Service() export class GetFlowIdsFromNestedFlowFiltersStrategyImpl @@ -18,7 +17,7 @@ export class GetFlowIdsFromNestedFlowFiltersStrategyImpl constructor( private readonly reportDetailService: ReportDetailService, private readonly legacyService: LegacyService, - private readonly externalRefenceService: ExternalReferenceService, + private readonly externalReferenceService: ExternalReferenceService, private readonly flowService: FlowService ) {} @@ -27,10 +26,10 @@ export class GetFlowIdsFromNestedFlowFiltersStrategyImpl ): Promise { const { models, nestedFlowFilters } = args; - let flowsReporterReferenceCode: UniqueFlowEntity[] = []; - let flowsSourceSystemId: UniqueFlowEntity[] = []; - let flowsSystemId: UniqueFlowEntity[] = []; - const flowsLegacyId: UniqueFlowEntity[] = []; + let flowsReporterReferenceCode: Set = new Set(); + let flowsSourceSystemId: Set = new Set(); + let flowsSystemId: Set = new Set(); + const flowsLegacyId: Set = new Set(); // Get the flowIDs using 'reporterReferenceCode' if (nestedFlowFilters?.reporterRefCode) { @@ -50,10 +49,10 @@ export class GetFlowIdsFromNestedFlowFiltersStrategyImpl ); } - // Get the flowIDs using 'systemID' from 'externalRefecence' + // Get the flowIDs using 'systemID' from 'externalReference' if (nestedFlowFilters?.systemID) { flowsSystemId = - await this.externalRefenceService.getUniqueFlowIDsBySystemID( + await this.externalReferenceService.getUniqueFlowIDsBySystemID( models, nestedFlowFilters.systemID ); @@ -67,30 +66,26 @@ export class GetFlowIdsFromNestedFlowFiltersStrategyImpl ); if (flowID) { - flowsLegacyId.push({ - id: flowID, - versionID: 1, - }); + flowsLegacyId.add(`${flowID}:1`); } } // Intersect the flowIDs from the nestedFlowFilters - const flowIDsFromNestedFlowFilters: UniqueFlowEntity[] = - intersectUniqueFlowEntities( - flowsReporterReferenceCode, - flowsSourceSystemId, - flowsSystemId, - flowsLegacyId - ); + const flowIDsFromNestedFlowFilters: Set = intersectSets( + flowsReporterReferenceCode, + flowsSourceSystemId, + flowsSystemId, + flowsLegacyId + ); - if (flowIDsFromNestedFlowFilters.length === 0) { + if (flowIDsFromNestedFlowFilters.size === 0) { return { flows: [] }; } // Once gathered and disjoined the flowIDs from the nestedFlowFilters // Look after this uniqueFlows in the flow table const flows = await this.flowService.progresiveSearch( models, - flowIDsFromNestedFlowFilters, + parseFlowIdVersionSet(flowIDsFromNestedFlowFilters), 1000, 0, false, // Stop when we have the limit diff --git a/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts b/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts index 0b354fc8..9d4060c6 100644 --- a/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts @@ -1,12 +1,11 @@ import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import { Service } from 'typedi'; -import { type UniqueFlowEntity } from '../../model'; import { type FlowIDSearchStrategy, type FlowIdSearchStrategyArgs, type FlowIdSearchStrategyResponse, } from '../flowID-search-strategy'; -import { intersectUniqueFlowEntities } from './utils'; +import { intersectSets, parseFlowIdVersionSet } from './utils'; @Service() export class GetFlowIdsFromObjectConditionsStrategyImpl @@ -23,10 +22,10 @@ export class GetFlowIdsFromObjectConditionsStrategyImpl return { flows: [] }; } - let intersectedFlows: UniqueFlowEntity[] = []; + let intersectedFlows: Set = new Set(); - for (const [flowObjectType, group] of flowObjectFilterGrouped.entries()) { - for (const [direction, ids] of group.entries()) { + for (const [flowObjectType, group] of flowObjectFilterGrouped) { + for (const [direction, ids] of group) { const condition = { objectType: flowObjectType, refDirection: direction, @@ -36,22 +35,19 @@ export class GetFlowIdsFromObjectConditionsStrategyImpl where: condition, }); - const uniqueFlowObjectsEntities: UniqueFlowEntity[] = + const uniqueFlowObjectsEntities: Set = new Set( flowObjectsFound.map( - (flowObject) => - ({ - id: flowObject.flowID, - versionID: flowObject.versionID, - }) satisfies UniqueFlowEntity - ); - - intersectedFlows = intersectUniqueFlowEntities( + (flowObject) => `${flowObject.flowID}:${flowObject.versionID}` + ) + ); + + intersectedFlows = intersectSets( intersectedFlows, uniqueFlowObjectsEntities ); } } - return { flows: intersectedFlows }; + return { flows: parseFlowIdVersionSet(intersectedFlows) }; } } diff --git a/src/domain-services/flows/strategy/impl/search-flow-by-filters-strategy-impl.ts b/src/domain-services/flows/strategy/impl/search-flow-by-filters-strategy-impl.ts index 3222fc91..c027b424 100644 --- a/src/domain-services/flows/strategy/impl/search-flow-by-filters-strategy-impl.ts +++ b/src/domain-services/flows/strategy/impl/search-flow-by-filters-strategy-impl.ts @@ -1,4 +1,5 @@ import { Service } from 'typedi'; +import { FlowObjectFilterGrouped } from '../../../flow-object/model'; import { FlowService } from '../../flow-service'; import type { FlowWhere, UniqueFlowEntity } from '../../model'; import type { @@ -11,14 +12,14 @@ import { GetFlowIdsFromCategoryConditionsStrategyImpl } from './get-flowIds-flow import { GetFlowIdsFromNestedFlowFiltersStrategyImpl } from './get-flowIds-flow-from-nested-flow-filters-strategy-impl'; import { GetFlowIdsFromObjectConditionsStrategyImpl } from './get-flowIds-flow-object-conditions-strategy-impl'; import { - defaultFlowOrderBy, defaultSearchFlowFilter, - intersectUniqueFlowEntities, + intersectSets, mapFlowFiltersToFlowObjectFiltersGrouped, mapFlowOrderBy, - mergeUniqueEntities, + parseFlowIdVersionSet, prepareFlowConditions, prepareFlowStatusConditions, + stringifyFlowIdVersionArray, } from './utils'; @Service() @@ -45,68 +46,47 @@ export class SearchFlowByFiltersStrategy implements FlowSearchStrategy { shouldIncludeChildrenOfParkedFlows, } = args; - // First, we need to check if we need to sort by a certain entity + // We need to check if we need to sort by a certain entity // and if so, we need to map the orderBy to be from that entity // obtain the entities relation to the flow // to be able to sort the flows using the entity const isSortByEntity = orderBy && orderBy.entity !== 'flow'; - const sortByFlowIDs: UniqueFlowEntity[] = []; + let sortByFlowIDsSet = new Set(); + let sortByFlowIDsPromise: Promise = Promise.resolve([]); const orderByForFlow = mapFlowOrderBy(orderBy); + // Fetch sorted flow IDs only for the filtered subset instead of the whole table if (isSortByEntity) { - // Get the flowIDs using the orderBy entity - const flowIDsFromSortingEntity: UniqueFlowEntity[] = - await this.flowService.getFlowIDsFromEntity(models, orderBy); - // Since there can be many flowIDs returned - // This can cause 'Maximum call stack size exceeded' error - // When using the spread operator - a workaround is to use push fot each element - // also, we need to map the FlowEntity to UniqueFlowEntity - for (const uniqueFlow of flowIDsFromSortingEntity) { - sortByFlowIDs.push(uniqueFlow); - } + // Get entity-sorted IDs then intersect with filtered subset + sortByFlowIDsPromise = this.flowService.getFlowIDsFromEntity( + models, + orderBy + ); } else { - // In this case we fetch the list of flows from the database - // using the orderBy - const flowsToSort: UniqueFlowEntity[] = await this.flowService.getFlows({ + // Let the DB sort only the filtered IDs + sortByFlowIDsPromise = this.flowService.getFlows({ models, orderBy: orderByForFlow, }); - - // Since there can be many flowIDs returned - // This can cause 'Maximum call stack size exceeded' error - // When using the spread operator - a workaround is to use push fot each element - // also, we need to map the FlowEntity to UniqueFlowEntity - for (const flow of flowsToSort) { - sortByFlowIDs.push(flow); - } } - // We need to fetch the flowIDs by the nestedFlowFilters // if there are any const isFilterByNestedFilters = nestedFlowFilters !== undefined; - const flowIDsFromNestedFlowFilters: UniqueFlowEntity[] = []; - + let flowIDsFromNestedFlowFiltersSet = new Set(); + let flowsFromNestedFiltersPromise: Promise = + Promise.resolve({ flows: [] }); + let didFlowsFromNestedFiltersPromiseCreated = false; if (isFilterByNestedFilters) { - const { flows }: FlowIdSearchStrategyResponse = - await this.getFlowIdsFromNestedFlowFilters.search({ + flowsFromNestedFiltersPromise = + this.getFlowIdsFromNestedFlowFilters.search({ models, nestedFlowFilters, }); - - // If after this filter we have no flows, we can return an empty array - if (flows.length === 0) { - return { flows: [], count: 0 }; - } - // Since there can be many flowIDs returned - // This can cause 'Maximum call stack size exceeded' error - // When using the spread operator - a workaround is to use push fot each element - for (const flow of flows) { - flowIDsFromNestedFlowFilters.push(flow); - } + didFlowsFromNestedFiltersPromiseCreated = true; } // Now we need to check if we need to filter by category - // if it's using any of the shorcuts + // if it's using any of the shortcuts // or if there are any flowCategoryFilters const isSearchByCategoryShotcut = shortcutFilters !== null && shortcutFilters.length > 0; @@ -114,73 +94,44 @@ export class SearchFlowByFiltersStrategy implements FlowSearchStrategy { const isFilterByCategory = isSearchByCategoryShotcut || flowCategoryFilters?.length > 0; - const flowsFromCategoryFilters: UniqueFlowEntity[] = []; + let flowIDsFromCategoryFiltersSet = new Set(); + let flowsFromCategoryFiltersPromise: Promise = + Promise.resolve({ flows: [] }); + let didFlowsFromCategoryFiltersPromiseCreated = false; if (isFilterByCategory) { - const { flows }: FlowIdSearchStrategyResponse = - await this.getFlowIdsFromCategoryConditions.search({ + flowsFromCategoryFiltersPromise = + this.getFlowIdsFromCategoryConditions.search({ models, flowCategoryConditions: flowCategoryFilters ?? [], shortcutFilters, }); - - // If after this filter we have no flows, we can return an empty array - if (flows.length === 0) { - return { flows: [], count: 0 }; - } - - // Since there can be many flowIDs returned - // This can cause 'Maximum call stack size exceeded' error - // When using the spread operator - a workaround is to use push fot each element - for (const flow of flows) { - flowsFromCategoryFilters.push(flow); - } + didFlowsFromCategoryFiltersPromiseCreated = true; } // After that, if we need to filter by flowObjects // Obtain the flowIDs from the flowObjects const isFilterByFlowObjects = flowObjectFilters?.length > 0; - const flowsFromObjectFilters: UniqueFlowEntity[] = []; + let flowIDsFromObjectFiltersSet = new Set(); + let flowsFromObjectFiltersPromise: Promise = + Promise.resolve({ flows: [] }); + let didFlowsFromObjectFiltersPromiseCreated = false; + let flowObjectFiltersGrouped: FlowObjectFilterGrouped | null = null; + if (isFilterByFlowObjects) { - // Firts step is to map the filters to the FlowObjectFiltersGrouped + // First step is to map the filters to the FlowObjectFiltersGrouped // To allow doing inclusive filtering between filters of the same type+direction // But exclusive filtering between filters of different type+direction - const flowObjectFiltersGrouped = + flowObjectFiltersGrouped = mapFlowFiltersToFlowObjectFiltersGrouped(flowObjectFilters); - const { flows }: FlowIdSearchStrategyResponse = - await this.getFlowIdsFromObjectConditions.search({ + flowsFromObjectFiltersPromise = + this.getFlowIdsFromObjectConditions.search({ models, flowObjectFilterGrouped: flowObjectFiltersGrouped, }); - - // If after this filter we have no flows, we can return an empty array - if (flows.length === 0) { - return { flows: [], count: 0 }; - } - - // Since there can be many flowIDs returned - // This can cause 'Maximum call stack size exceeded' error - // When using the spread operator - a workaround is to use push fot each element - for (const flow of flows) { - flowsFromObjectFilters.push(flow); - } - - // If 'includeChildrenOfParkedFlows' is defined and true - // we need to obtain the flowIDs from the childs whose parent flows are parked - if (shouldIncludeChildrenOfParkedFlows) { - // We need to obtain the flowIDs from the childs whose parent flows are parked - const childs = - await this.flowService.getParkedParentFlowsByFlowObjectFilter( - models, - flowObjectFiltersGrouped - ); - - for (const child of childs) { - flowsFromObjectFilters.push(child); - } - } + didFlowsFromObjectFiltersPromiseCreated = true; } // Lastly, we need to check if we need to filter by flow @@ -189,7 +140,11 @@ export class SearchFlowByFiltersStrategy implements FlowSearchStrategy { const isFilterByFlow = flowFilters !== undefined; const isFilterByFlowStatus = statusFilter !== undefined; - const flowsFromFlowFilters: UniqueFlowEntity[] = []; + let flowIDsFromFlowFiltersSet = new Set(); + let flowsFromFlowFiltersPromise: Promise = + Promise.resolve([]); + let didFlowsFromFlowFiltersPromiseCreated = false; + if (isFilterByFlow || isFilterByFlowStatus) { let flowConditions: FlowWhere = prepareFlowConditions(flowFilters); // Add status filter conditions if provided @@ -198,59 +153,118 @@ export class SearchFlowByFiltersStrategy implements FlowSearchStrategy { statusFilter ); - const orderByForFlowFilter = isSortByEntity - ? defaultFlowOrderBy() - : orderByForFlow; - - const flows: UniqueFlowEntity[] = await this.flowService.getFlows({ + flowsFromFlowFiltersPromise = this.flowService.getFlows({ models, conditions: flowConditions, - orderBy: orderByForFlowFilter, }); + didFlowsFromFlowFiltersPromiseCreated = true; + } - // If after this filter we have no flows, we can return an empty array - if (flows.length === 0) { - return { flows: [], count: 0 }; - } + // Now we need to wait for all the promises to be resolved + const [ + flowsFromCategoryFilters, + flowsFromNestedFilters, + flowsFromObjectFilters, + flowsFromFlowFilters, + sortByFlowIDs, + ] = await Promise.all([ + flowsFromCategoryFiltersPromise, + flowsFromNestedFiltersPromise, + flowsFromObjectFiltersPromise, + flowsFromFlowFiltersPromise, + sortByFlowIDsPromise, + ]); + + // First check if we have created the promises + // and if so, check if the flows are empty + // If they are empty, we can return an empty array + // and a count of 0 + if ( + didFlowsFromNestedFiltersPromiseCreated && + flowsFromNestedFilters.flows.length === 0 + ) { + return { flows: [], count: 0 }; + } + + if ( + didFlowsFromCategoryFiltersPromiseCreated && + flowsFromCategoryFilters.flows.length === 0 + ) { + return { flows: [], count: 0 }; + } + if ( + didFlowsFromObjectFiltersPromiseCreated && + flowsFromObjectFilters.flows.length === 0 + ) { + return { flows: [], count: 0 }; + } + + if ( + didFlowsFromFlowFiltersPromiseCreated && + flowsFromFlowFilters.length === 0 + ) { + return { flows: [], count: 0 }; + } - // Since there can be many flowIDs returned - // This can cause 'Maximum call stack size exceeded' error - // When using the spread operator - a workaround is to use push fot each element - // also, we need to map the FlowEntity to UniqueFlowEntity - for (const flow of flows) { - flowsFromFlowFilters.push(flow); + // Now we need to obtain the flowIDs from the flows filtering promises + flowIDsFromNestedFlowFiltersSet = stringifyFlowIdVersionArray( + flowsFromNestedFilters.flows + ); + flowIDsFromCategoryFiltersSet = stringifyFlowIdVersionArray( + flowsFromCategoryFilters.flows + ); + + // If 'includeChildrenOfParkedFlows' is defined and true + // we need to obtain the flowIDs from the childs whose parent flows are parked + // if (shouldIncludeChildrenOfParkedFlows) { + // We need to obtain the flowIDs from the childs whose parent flows are parked + if (shouldIncludeChildrenOfParkedFlows && flowObjectFiltersGrouped) { + const childs = + await this.flowService.getParkedParentFlowsByFlowObjectFilter( + models, + flowObjectFiltersGrouped + ); + + for (const child of childs) { + flowsFromObjectFilters.flows.push(child); } } + flowIDsFromObjectFiltersSet = stringifyFlowIdVersionArray( + flowsFromObjectFilters.flows + ); + flowIDsFromFlowFiltersSet = + stringifyFlowIdVersionArray(flowsFromFlowFilters); + // Lastly, we need to obtain the flowIDs from the sortByFlowIDs + sortByFlowIDsSet = stringifyFlowIdVersionArray(sortByFlowIDs); // We need to intersect the flowIDs from the flowObjects, flowCategoryFilters and flowFilters // to obtain the flowIDs that match all the filters - const deduplicatedFlows: UniqueFlowEntity[] = intersectUniqueFlowEntities( - flowsFromCategoryFilters, - flowsFromObjectFilters, - flowsFromFlowFilters, - flowIDsFromNestedFlowFilters + const intersectedFlows: Set = intersectSets( + flowIDsFromCategoryFiltersSet, + flowIDsFromFlowFiltersSet, + flowIDsFromNestedFlowFiltersSet, + flowIDsFromObjectFiltersSet ); - if (deduplicatedFlows.length === 0) { + if (intersectedFlows.size === 0) { return { flows: [], count: 0 }; } - // We are going to sort the deduplicated flows - // using the sortByFlowIDs if there are any - let sortedFlows: UniqueFlowEntity[] = []; - // While sorting we have the same amount or less flows 'sorted' than deduplicatedFlows - // That means we need to keep the sortedFilters and then keep the rest of deduplicatedFlows thar are not in sortedFlows - // If we don't do this it may cause that just changing the orderBy we get different results - // Because we get rid of those flows that are not present in the sortedFlows list - sortedFlows = intersectUniqueFlowEntities(sortByFlowIDs, deduplicatedFlows); - - sortedFlows = mergeUniqueEntities(sortedFlows, deduplicatedFlows); - - const count = sortedFlows.length; + // The method Set.prototype.intersection(...) compares the bigger set with the smaller one + // and returns the smaller one, so we need to do the opposite + // More likely the `sortedFlows` will be smaller than the `intersectedFlows`, + // since `intersectedFlows` is the intersection of all the filters + // so we need to reverse the list of `sortedFlows` + const sortedFlows: Set = intersectSets( + intersectedFlows, + sortByFlowIDsSet + ); + const parsedSortedFlows = parseFlowIdVersionSet(sortedFlows).reverse(); + const count = sortedFlows.size; const flows = await this.flowService.progresiveSearch( models, - sortedFlows, + parsedSortedFlows, limit, offset ?? 0, true, // Stop when we have the limit @@ -262,8 +276,8 @@ export class SearchFlowByFiltersStrategy implements FlowSearchStrategy { if (isSortByEntity) { // Sort the flows using the sortedFlows as referenceList flows.sort((a, b) => { - const aIndex = sortedFlows.findIndex((flow) => flow.id === a.id); - const bIndex = sortedFlows.findIndex((flow) => flow.id === b.id); + const aIndex = parsedSortedFlows.findIndex((flow) => flow.id === a.id); + const bIndex = parsedSortedFlows.findIndex((flow) => flow.id === b.id); return aIndex - bIndex; }); } diff --git a/src/domain-services/flows/strategy/impl/utils.ts b/src/domain-services/flows/strategy/impl/utils.ts index 07e54539..11ea18a6 100644 --- a/src/domain-services/flows/strategy/impl/utils.ts +++ b/src/domain-services/flows/strategy/impl/utils.ts @@ -1,7 +1,9 @@ import { type Database } from '@unocha/hpc-api-core/src/db'; +import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { Cond, Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import type { InstanceDataOf } from '@unocha/hpc-api-core/src/db/util/model-definition'; import { type InstanceOfModel } from '@unocha/hpc-api-core/src/db/util/types'; +import { objectEntries } from '@unocha/hpc-api-core/src/util'; import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; import type * as t from 'io-ts'; import { type OrderBy } from '../../../../utils/database-types'; @@ -178,12 +180,17 @@ export const prepareFlowConditions = ( ): FlowWhere => { let flowConditions: FlowWhere = { ...defaultSearchFlowFilter }; + // Cannot be undefined according to type if (flowFilters) { - for (const [key, value] of Object.entries(flowFilters)) { + for (const [key, value] of objectEntries(flowFilters)) { + // Cannot be undefined according to type if (value !== undefined) { if (key === 'id') { - const brandedIDs = value.map((id: number) => createBrandedValue(id)); - flowConditions[key] = { [Op.IN]: brandedIDs }; + // If `key` is `'id'`, then we know the type of this (`FlowId[] | null`) + const flowIds = value as SearchFlowsFilters['id']; + // @ts-ignore + // Type error, can be `null` + flowConditions[key] = { [Op.IN]: flowIds }; } else { const typedKey = key as keyof FlowWhere; flowConditions = { ...flowConditions, [typedKey]: value }; @@ -192,7 +199,8 @@ export const prepareFlowConditions = ( } } - return flowConditions satisfies FlowWhere; + // It's of type `FlowWhere` already. No need for `satisfies`, return type of function does type-checking already + return flowConditions; }; export const mergeUniqueEntities = ( @@ -222,6 +230,7 @@ export const mergeUniqueEntities = ( return mapUniqueFlowEntitisSetKeyToUniqueFlowEntity(entityMapListA); }; +/** @deprecated - use _intersectSets_ instead*/ export const intersectUniqueFlowEntities = ( ...lists: UniqueFlowEntity[][] ): UniqueFlowEntity[] => { @@ -255,6 +264,23 @@ export const intersectUniqueFlowEntities = ( return mapUniqueFlowEntitisSetKeyToUniqueFlowEntity(initialSet); }; +export const intersectSets = (...sets: Array>): Set => { + // We need to iterate over the collection of sets + // and perform the intersection only for those + // sets that are not empty + let intersectedSet = new Set(); + for (const set of sets) { + if (set.size > 0) { + if (intersectedSet.size === 0) { + intersectedSet = set; + } else { + intersectedSet = intersectedSet.intersection(set); + } + } + } + return intersectedSet; +}; + export const mapUniqueFlowEntitisSetKeyToSetkey = ( entity: UniqueFlowEntity ): string => { @@ -414,3 +440,31 @@ export const buildOrderBy = ( return orderBy; }; + +/** + * Converts a Set of "id:versionID" strings into the array + * of UniqueFlowEntity objects your existing search method expects. + */ +export const parseFlowIdVersionSet = ( + idVersionSet: Set +): UniqueFlowEntity[] => { + return [...idVersionSet].map((entry) => { + const [idStr, versionStr] = entry.split(':'); + const id: FlowId = createBrandedValue(Number(idStr)); + return { + id, + versionID: versionStr !== undefined ? Number(versionStr) : 0, + } satisfies UniqueFlowEntity; + }); +}; + +/** + * Converts an array of UniqueFlowEntity objects into a Set of "id:versionID" strings. + */ +export const stringifyFlowIdVersionArray = ( + flowEntities: UniqueFlowEntity[] +): Set => { + return new Set( + flowEntities.map((entity) => `${entity.id}:${entity.versionID}`) + ); +}; diff --git a/src/domain-services/location/graphql/resolver.ts b/src/domain-services/location/graphql/resolver.ts index 7203e083..74e1a105 100644 --- a/src/domain-services/location/graphql/resolver.ts +++ b/src/domain-services/location/graphql/resolver.ts @@ -1,3 +1,4 @@ +import type { LocationId } from '@unocha/hpc-api-core/src/db/models/location'; import { type Database } from '@unocha/hpc-api-core/src/db/type'; import { type InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; import { MinLength } from 'class-validator'; @@ -21,7 +22,7 @@ export default class LocationResolver { @Query(() => Location) async location( - @Arg('id') id: number, + @Arg('id') id: LocationId, @Ctx() context: Context ): Promise> { return await this.locationService.findById(context.models, id); diff --git a/src/domain-services/location/graphql/types.ts b/src/domain-services/location/graphql/types.ts index aef28e1e..3fa8b000 100644 --- a/src/domain-services/location/graphql/types.ts +++ b/src/domain-services/location/graphql/types.ts @@ -1,4 +1,4 @@ -import { Brand } from '@unocha/hpc-api-core/src/util/types'; +import type { LocationId } from '@unocha/hpc-api-core/src/db/models/location'; import { MaxLength } from 'class-validator'; import { Field, ID, ObjectType, registerEnumType } from 'type-graphql'; import { BaseType, BaseTypeWithDirection } from '../../base-types'; @@ -15,7 +15,7 @@ registerEnumType(LocationStatus, { @ObjectType() export default class Location extends BaseType { @Field(() => ID) - id: Brand; + id: LocationId; @Field({ nullable: true }) @MaxLength(255) @@ -56,8 +56,8 @@ export default class Location extends BaseType { @ObjectType() export class BaseLocationWithDirection extends BaseTypeWithDirection { - @Field(() => Number, { nullable: true }) - id: number; + @Field(() => ID, { nullable: true }) + id: LocationId | null; @Field(() => String, { nullable: true }) name: string | null; diff --git a/src/domain-services/location/location-service.ts b/src/domain-services/location/location-service.ts index bbfdc70a..8fbc7eee 100644 --- a/src/domain-services/location/location-service.ts +++ b/src/domain-services/location/location-service.ts @@ -1,3 +1,4 @@ +import type { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { type LocationId } from '@unocha/hpc-api-core/src/db/models/location'; import { type Database } from '@unocha/hpc-api-core/src/db/type'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; @@ -11,9 +12,9 @@ import { type BaseLocationWithDirection } from './graphql/types'; export class LocationService { async findById( models: Database, - id: number + id: LocationId ): Promise> { - const location = await models.location.get(createBrandedValue(id)); + const location = await models.location.get(id); if (!location) { throw new Error(`Location with ID ${id} does not exist`); @@ -34,7 +35,7 @@ export class LocationService { async getLocationsForFlows( locationsFO: Array>, models: Database - ): Promise> { + ): Promise> { const locationObjectsIDs: LocationId[] = locationsFO.map((locFO) => createBrandedValue(locFO.objectID) ); @@ -48,7 +49,7 @@ export class LocationService { }, }); - const locationsMap = new Map(); + const locationsMap = new Map(); for (const locFO of locationsFO) { const flowId = locFO.flowID; diff --git a/src/domain-services/organizations/graphql/types.ts b/src/domain-services/organizations/graphql/types.ts index b146751b..b4456f96 100644 --- a/src/domain-services/organizations/graphql/types.ts +++ b/src/domain-services/organizations/graphql/types.ts @@ -1,10 +1,11 @@ -import { Field, ObjectType } from 'type-graphql'; +import type { OrganizationId } from '@unocha/hpc-api-core/src/db/models/organization'; +import { Field, ID, ObjectType } from 'type-graphql'; import { BaseTypeWithDirection } from '../../base-types'; @ObjectType() export class Organization extends BaseTypeWithDirection { - @Field(() => Number, { nullable: false }) - id: number; + @Field(() => ID, { nullable: false }) + id: OrganizationId; @Field({ nullable: true }) name: string; diff --git a/src/domain-services/organizations/organization-service.ts b/src/domain-services/organizations/organization-service.ts index c35bd873..4852d467 100644 --- a/src/domain-services/organizations/organization-service.ts +++ b/src/domain-services/organizations/organization-service.ts @@ -1,4 +1,5 @@ import { type Database } from '@unocha/hpc-api-core/src/db'; +import type { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import { type InstanceOfModel } from '@unocha/hpc-api-core/src/db/util/types'; import { getOrCreate } from '@unocha/hpc-api-core/src/util'; @@ -17,18 +18,17 @@ export class OrganizationService { organizationsFO: FlowObject[], models: Database ) { - const organizations: OrganizationInstance[] = - await models.organization.find({ - where: { - id: { - [Op.IN]: organizationsFO.map((orgFO) => - createBrandedValue(orgFO.objectID) - ), - }, + const organizations = await models.organization.find({ + where: { + id: { + [Op.IN]: organizationsFO.map((orgFO) => + createBrandedValue(orgFO.objectID) + ), }, - }); + }, + }); - const organizationsMap = new Map(); + const organizationsMap = new Map(); for (const orgFO of organizationsFO) { const flowId = orgFO.flowID; @@ -49,8 +49,7 @@ export class OrganizationService { if ( !organizationPerFlow.some( (org) => - org.id === organization.id.valueOf() && - org.direction === orgFO.refDirection + org.id === organization.id && org.direction === orgFO.refDirection ) ) { const organizationMapped: Organization = @@ -84,12 +83,12 @@ export class OrganizationService { updatedAt: organization.updatedAt.toISOString(), abbreviation: organization.abbreviation, url: organization.url, - parentID: organization.parentID?.valueOf() ?? null, + parentID: organization.parentID, nativeName: organization.nativeName, comments: organization.comments, collectiveInd: organization.collectiveInd, active: organization.active, - newOrganizationId: organization.newOrganizationId?.valueOf() ?? null, + newOrganizationId: organization.newOrganizationId, verified: organization.verified, notes: organization.notes, }; diff --git a/src/domain-services/plan-tag/graphql/resolver.ts b/src/domain-services/plan-tag/graphql/resolver.ts index de58fbb6..d976538d 100644 --- a/src/domain-services/plan-tag/graphql/resolver.ts +++ b/src/domain-services/plan-tag/graphql/resolver.ts @@ -1,3 +1,4 @@ +import type { PlanTagId } from '@unocha/hpc-api-core/src/db/models/planTag'; import { Arg, Ctx, Query, Resolver } from 'type-graphql'; import { Service } from 'typedi'; import Context from '../../Context'; @@ -10,7 +11,7 @@ export default class PlanTagResolver { constructor(private planTagService: PlanTagService) {} @Query(() => PlanTag) - async planTag(@Arg('id') id: number, @Ctx() context: Context) { + async planTag(@Arg('id') id: PlanTagId, @Ctx() context: Context) { return await this.planTagService.findById(context.models, id); } } diff --git a/src/domain-services/plan-tag/graphql/types.ts b/src/domain-services/plan-tag/graphql/types.ts index d7186162..3498b6b6 100644 --- a/src/domain-services/plan-tag/graphql/types.ts +++ b/src/domain-services/plan-tag/graphql/types.ts @@ -1,4 +1,6 @@ -import { Brand } from '@unocha/hpc-api-core/src/util/types'; +import type { PlanId } from '@unocha/hpc-api-core/src/db/models/plan'; +import type { PlanReportingPeriodId } from '@unocha/hpc-api-core/src/db/models/planReportingPeriod'; +import type { PlanTagId } from '@unocha/hpc-api-core/src/db/models/planTag'; import { IsEnum, MaxLength } from 'class-validator'; import { Field, @@ -30,7 +32,7 @@ registerEnumType(PlanTagType, { @ObjectType() export default class PlanTag { @Field(() => ID) - id: Brand; + id: PlanTagId; @Field() planId: number; @@ -58,7 +60,7 @@ export default class PlanTag { @InputType({ description: 'New plan tag data' }) export class AddPlanTagInput implements Partial { @Field(() => ID) - planId: Brand; + planId: PlanId; @Field() @MaxLength(50) @@ -76,9 +78,7 @@ export class AddPlanTagInput implements Partial { revisionState: RevisionState; @Field(() => [ID], { nullable: true }) - reportingPeriodIds: Array< - Brand - >; + reportingPeriodIds: PlanReportingPeriodId[]; @Field() publishMeasurements: boolean; diff --git a/src/domain-services/plan-tag/plan-tag-service.ts b/src/domain-services/plan-tag/plan-tag-service.ts index b37dae5a..55b83f3d 100644 --- a/src/domain-services/plan-tag/plan-tag-service.ts +++ b/src/domain-services/plan-tag/plan-tag-service.ts @@ -1,3 +1,4 @@ +import type { PlanId } from '@unocha/hpc-api-core/src/db/models/plan'; import { type PlanTagId } from '@unocha/hpc-api-core/src/db/models/planTag'; import { type Database } from '@unocha/hpc-api-core/src/db/type'; import { type InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; @@ -15,9 +16,9 @@ import { type AddPlanTagInput } from './graphql/types'; export class PlanTagService { async findById( models: Database, - id: number + id: PlanTagId ): Promise<{ id: PlanTagId; name?: string | null }> { - const planTag = await models.planTag.get(createBrandedValue(id)); + const planTag = await models.planTag.get(id); if (!planTag) { throw new Error(`Plan tag with ID ${id} does not exist`); @@ -28,11 +29,11 @@ export class PlanTagService { async findByPlanId( models: Database, - planId: number + planId: PlanId ): Promise>> { return await models.planTag.find({ where: { - planId: createBrandedValue(planId), + planId, }, }); } @@ -47,14 +48,14 @@ export class PlanTagService { revisionState: 'none', }, where: { - id: createBrandedValue(planTag.planId), + id: planTag.planId, }, }); const createdPlanTag = await models.planTag.create({ name: await getNextTag(models, planTag), public: true, - planId: createBrandedValue(planTag.planId), + planId: planTag.planId, revisionState: planTag.revisionState, comment: planTag.comments, type: planTag.type, diff --git a/src/domain-services/plans/graphql/resolver.ts b/src/domain-services/plans/graphql/resolver.ts index 0e18864a..fa7964d7 100644 --- a/src/domain-services/plans/graphql/resolver.ts +++ b/src/domain-services/plans/graphql/resolver.ts @@ -1,3 +1,4 @@ +import type { PlanId } from '@unocha/hpc-api-core/src/db/models/plan'; import { Arg, Ctx, @@ -23,7 +24,7 @@ export default class PlanResolver { ) {} @Query(() => Plan) - async plan(@Arg('id') id: number, @Ctx() context: Context) { + async plan(@Arg('id') id: PlanId, @Ctx() context: Context) { return await this.planService.findById(context.models, id); } diff --git a/src/domain-services/plans/graphql/types.ts b/src/domain-services/plans/graphql/types.ts index 31dd8382..8fd8933c 100644 --- a/src/domain-services/plans/graphql/types.ts +++ b/src/domain-services/plans/graphql/types.ts @@ -1,4 +1,6 @@ -import { Brand } from '@unocha/hpc-api-core/src/util/types'; +import type { AttachmentId } from '@unocha/hpc-api-core/src/db/models/attachment'; +import type { GoverningEntityId } from '@unocha/hpc-api-core/src/db/models/governingEntity'; +import type { PlanId } from '@unocha/hpc-api-core/src/db/models/plan'; import { MaxLength } from 'class-validator'; import { Field, ID, ObjectType } from 'type-graphql'; import { BaseTypeWithDirection } from '../../base-types'; @@ -7,7 +9,7 @@ import PlanTag from '../../plan-tag/graphql/types'; @ObjectType() export class PlanCaseload { @Field(() => ID) - id: number; + id: AttachmentId; @Field() name: string; @@ -37,7 +39,7 @@ export class PlanCaseload { @ObjectType() export class PlanCluster { @Field(() => ID) - id: number; + id: GoverningEntityId; @Field() name: string; @@ -72,7 +74,7 @@ export class PlanFunding { @ObjectType() export default class Plan { @Field(() => ID) - id: Brand; + id: PlanId; @Field() @MaxLength(255) @@ -99,8 +101,8 @@ export default class Plan { @ObjectType() export class BasePlan extends BaseTypeWithDirection { - @Field(() => Number, { nullable: true }) - id: number; + @Field(() => ID, { nullable: true }) + id: PlanId | null; @Field({ nullable: true }) name: string; diff --git a/src/domain-services/plans/plan-service.ts b/src/domain-services/plans/plan-service.ts index 523a95f7..28838e4a 100644 --- a/src/domain-services/plans/plan-service.ts +++ b/src/domain-services/plans/plan-service.ts @@ -1,3 +1,4 @@ +import type { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { type PlanId } from '@unocha/hpc-api-core/src/db/models/plan'; import { type Database } from '@unocha/hpc-api-core/src/db/type'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; @@ -13,9 +14,9 @@ import { type BasePlan } from './graphql/types'; export class PlanService { async findById( models: Database, - id: number + id: PlanId ): Promise<{ id: PlanId; name?: string | null }> { - const plan = await models.plan.get(createBrandedValue(id)); + const plan = await models.plan.get(id); if (!plan) { throw new Error(`Plan with ID ${id} does not exist`); @@ -34,10 +35,10 @@ export class PlanService { return { id: planId, name: currentPlanVersion.name }; } - async findPlanYears(models: Database, planId: number): Promise { + async findPlanYears(models: Database, planId: PlanId): Promise { const planYears = await models.planYear.find({ where: { - planId: createBrandedValue(planId), + planId, }, }); @@ -53,7 +54,7 @@ export class PlanService { async getPlansForFlows( plansFO: Array>, models: Database - ): Promise> { + ): Promise> { const planObjectsIDs: PlanId[] = plansFO.map((planFO) => createBrandedValue(planFO.objectID) ); @@ -66,7 +67,7 @@ export class PlanService { }, }); - const plansMap = new Map(); + const plansMap = new Map(); for (const plan of plans) { const planVersion = await models.planVersion.find({ @@ -109,7 +110,7 @@ export class PlanService { direction: EntityDirection ): BasePlan { return { - id: plan.id.valueOf(), + id: plan.id, name: planVersion.name, createdAt: plan.createdAt.toISOString(), updatedAt: plan.updatedAt.toISOString(), diff --git a/src/domain-services/report-details/graphql/types.ts b/src/domain-services/report-details/graphql/types.ts index 5a3135bb..3b7e4ce5 100644 --- a/src/domain-services/report-details/graphql/types.ts +++ b/src/domain-services/report-details/graphql/types.ts @@ -1,11 +1,13 @@ import { type EXTERNAL_DATA_SYSTEM_ID } from '@unocha/hpc-api-core/src/db/models/externalData'; +import type { ReportDetailId } from '@unocha/hpc-api-core/src/db/models/reportDetail'; import type * as t from 'io-ts'; -import { Field, ObjectType } from 'type-graphql'; +import { Field, ID, ObjectType } from 'type-graphql'; import { BaseType } from '../../base-types'; + @ObjectType() export class ReportDetail extends BaseType { - @Field(() => Number, { nullable: false }) - id: number; + @Field(() => ID, { nullable: false }) + id: ReportDetailId; @Field({ nullable: false }) flowID: number; diff --git a/src/domain-services/report-details/report-detail-service.ts b/src/domain-services/report-details/report-detail-service.ts index a0e1cae5..47a5756f 100644 --- a/src/domain-services/report-details/report-detail-service.ts +++ b/src/domain-services/report-details/report-detail-service.ts @@ -4,7 +4,6 @@ import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import { type InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; import { type InstanceOfModel } from '@unocha/hpc-api-core/src/db/util/types'; import { getOrCreate } from '@unocha/hpc-api-core/src/util'; -import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; import { Service } from 'typedi'; import { type UniqueFlowEntity } from '../flows/model'; import { type ReportDetail } from './graphql/types'; @@ -13,7 +12,7 @@ export class ReportDetailService { async getReportDetailsForFlows( flowIds: FlowId[], models: Database - ): Promise> { + ): Promise> { const reportDetails: Array> = await models.reportDetail.find({ where: { @@ -24,7 +23,7 @@ export class ReportDetailService { skipValidation: true, }); - const reportDetailsMap = new Map(); + const reportDetailsMap = new Map(); for (const flowId of flowIds) { if (!reportDetailsMap.has(flowId)) { @@ -78,28 +77,27 @@ export class ReportDetailService { async getUniqueFlowIDsFromReportDetailsByReporterReferenceCode( models: Database, reporterRefCode: string - ): Promise { + ): Promise> { const reportDetails: Array> = await models.reportDetail.find({ where: { - refCode: reporterRefCode, + refCode: { [models.Op.ILIKE]: `%${reporterRefCode}%` }, }, skipValidation: true, + distinct: ['flowID'], }); - const flowIDs: UniqueFlowEntity[] = []; - - for (const reportDetail of reportDetails) { - flowIDs.push(this.mapReportDetailToUniqueFlowEntity(reportDetail)); - } - - return flowIDs; + return new Set( + reportDetails.map((report) => { + return `${report.flowID}:${report.versionID}`; + }) + ); } async getUniqueFlowIDsFromReportDetailsBySourceSystemID( models: Database, sourceSystemID: string - ): Promise { + ): Promise> { const reportDetails: Array> = await models.reportDetail.find({ where: { @@ -108,20 +106,18 @@ export class ReportDetailService { skipValidation: true, }); - const flowIDs: UniqueFlowEntity[] = []; - - for (const report of reportDetails) { - flowIDs.push(this.mapReportDetailToUniqueFlowEntity(report)); - } - - return flowIDs; + return new Set( + reportDetails.map((report) => { + return `${report.flowID}:${report.versionID}`; + }) + ); } private mapReportDetailToUniqueFlowEntity( reportDetail: InstanceDataOfModel ): UniqueFlowEntity { return { - id: createBrandedValue(reportDetail.flowID), + id: reportDetail.flowID, versionID: reportDetail.versionID, }; } diff --git a/src/domain-services/usage-years/usage-year-service.ts b/src/domain-services/usage-years/usage-year-service.ts index 3b0eb9f9..0199b9e3 100644 --- a/src/domain-services/usage-years/usage-year-service.ts +++ b/src/domain-services/usage-years/usage-year-service.ts @@ -1,4 +1,5 @@ import { type Database } from '@unocha/hpc-api-core/src/db'; +import type { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import { type InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; import { getOrCreate } from '@unocha/hpc-api-core/src/util'; @@ -15,7 +16,7 @@ export class UsageYearService { async getUsageYearsForFlows( usageYearsFO: FlowObject[], models: Database - ): Promise> { + ): Promise> { const usageYears: UsageYearInstance[] = await models.usageYear.find({ where: { id: { @@ -26,7 +27,7 @@ export class UsageYearService { }, }); - const usageYearsMap = new Map(); + const usageYearsMap = new Map(); for (const usageYearFO of usageYearsFO) { const flowId = usageYearFO.flowID; diff --git a/tests/resolvers/flows.spec.ts b/tests/resolvers/flows.spec.ts index 30f22f8e..4f1b9bbe 100644 --- a/tests/resolvers/flows.spec.ts +++ b/tests/resolvers/flows.spec.ts @@ -239,7 +239,7 @@ describe('Query should return Flow search', () => { await models.category.createMany(categoriesProt); - // Asign categories to flows + // Assign categories to flows const activeFlowRelationCategory = activeFlows.map((flow) => { return { objectID: flow.id,