Skip to content

Commit

Permalink
Merge pull request #306 from UN-OCHA/env/stage
Browse files Browse the repository at this point in the history
🚀 Release `v4.8.0` and deploy to production
  • Loading branch information
Pl217 authored Dec 19, 2024
2 parents 0855553 + fed08ea commit 3d597bc
Show file tree
Hide file tree
Showing 43 changed files with 4,764 additions and 122 deletions.
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hpc-api",
"version": "4.7.1",
"version": "4.8.0",
"description": "api for HPC applications",
"main": "src/server.ts",
"license": "MIT",
Expand All @@ -17,14 +17,14 @@
"lint": "yarn lint-prettier && yarn lint-eslint"
},
"dependencies": {
"@unocha/hpc-api-core": "^10.1.1",
"@unocha/hpc-api-core": "^10.6.0",
"apollo-server-hapi": "^3.13.0",
"bunyan": "^1.8.15",
"class-validator": "^0.14.1",
"graphql": "^15.9.0",
"knex": "3.1.0",
"pg": "^8.12.0",
"pm2": "^5.4.2",
"pg": "^8.13.1",
"pm2": "^5.4.3",
"reflect-metadata": "^0.2.2",
"ts-node": "^10.9.2",
"type-graphql": "^1.1.1",
Expand All @@ -35,15 +35,15 @@
"@hapi/hapi": "^20.3.0",
"@types/bunyan": "^1.8.11",
"@types/hapi__hapi": "^20.0.9",
"@types/jest": "^29.5.13",
"@types/node": "^22.7.5",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.1",
"@types/pg": "^8.11.10",
"@unocha/hpc-repo-tools": "^5.0.0",
"eslint": "9.9.1",
"husky": "^9.1.6",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^15.2.10",
"prettier": "3.3.3",
"prettier": "3.4.1",
"ts-jest": "^29.2.5",
"ts-node-dev": "^2.0.0"
},
Expand Down
15 changes: 11 additions & 4 deletions src/domain-services/base-types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { Field, ObjectType } from 'type-graphql';

export type EntityDirection = 'source' | 'destination';
@ObjectType()
export class BaseType {
@Field()
createdAt: Date;
createdAt: string;

@Field()
updatedAt: Date;
updatedAt: string;
}

@ObjectType()
export class BaseTypeWithDirection extends BaseType {
@Field()
direction: EntityDirection;
}

@ObjectType()
export class BaseTypeWithSoftDelete extends BaseType {
@Field({ nullable: true })
deletedAt: Date;
@Field(() => String, { nullable: true })
deletedAt: string | null;
}
267 changes: 267 additions & 0 deletions src/domain-services/categories/category-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
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,
type Condition,
} 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 { Service } from 'typedi';
import { type ReportDetail } from '../report-details/graphql/types';
import { type Category } from './graphql/types';
import { type ShortcutCategoryFilter } from './model';

// Local types definition to increase readability
type CategoryRefModel = Database['categoryRef'];
type CategoryRefInstance = InstanceOfModel<CategoryRefModel>;

type CategoryModel = Database['category'];
type CategoryInstance = InstanceOfModel<CategoryModel>;
type CategoryWhere = Condition<CategoryInstance>;

@Service()
export class CategoryService {
async getCategoriesForFlows(
flowWithVersion: Map<FlowId, number[]>,
models: Database
): Promise<Map<number, Map<number, Category[]>>> {
// Group of flowIDs and its versions
// Structure:
// flowID: {
// versionID: [categories]
// }
const flowVersionCategoryMap = new Map<number, Map<number, Category[]>>();

const flowIDs: FlowId[] = [];
for (const flowID of flowWithVersion.keys()) {
flowIDs.push(flowID);
}

const categoriesRef: CategoryRefInstance[] = await models.categoryRef.find({
where: {
objectID: {
[Op.IN]: flowIDs,
},
objectType: 'flow',
},
});

const categories: CategoryInstance[] = await models.category.find({
where: {
id: {
[Op.IN]: categoriesRef.map((catRef) => catRef.categoryID),
},
},
});

// Populate the map with categories for each flow
for (const catRef of categoriesRef) {
const flowId = catRef.objectID.valueOf();

if (!flowVersionCategoryMap.has(flowId)) {
flowVersionCategoryMap.set(flowId, new Map());
}

// Here the key is the versionID of the flow
const flowVersionMap = getOrCreate(
flowVersionCategoryMap,
flowId,
() => new Map<number, Category[]>()
);

const flowVersion = catRef.versionID;
if (!flowVersionMap.has(flowVersion)) {
flowVersionMap.set(flowVersion, []);
}

const categoriesPerFlowVersion = getOrCreate(
flowVersionMap,
flowVersion,
() => []
);

const category = categories.find((cat) => cat.id === catRef.categoryID);

if (
category &&
!categoriesPerFlowVersion.some(
(cat) => cat.id === category.id.valueOf()
)
) {
const mappedCategory = this.mapCategoryToFlowCategory(category, catRef);
categoriesPerFlowVersion.push(mappedCategory);
}
}

return flowVersionCategoryMap;
}

private mapCategoryToFlowCategory(
category: CategoryInstance,
categoryRef: CategoryRefInstance
): Category {
return {
id: category.id,
name: category.name,
group: category.group,
createdAt: category.createdAt.toISOString(),
updatedAt: category.updatedAt.toISOString(),
description: category.description ?? '',
parentID: category.parentID ? category.parentID.valueOf() : null,
code: category.code ?? '',
includeTotals: category.includeTotals ?? false,
categoryRef: {
objectID: categoryRef.objectID.valueOf(),
versionID: categoryRef.versionID,
objectType: categoryRef.objectType,
categoryID: category.id.valueOf(),
createdAt: categoryRef.createdAt.toISOString(),
updatedAt: categoryRef.updatedAt.toISOString(),
},
versionID: categoryRef.versionID,
};
}

async addChannelToReportDetails(
models: Database,
reportDetails: ReportDetail[]
): Promise<ReportDetail[]> {
const listOfCategoryRefORs: Array<Condition<CategoryRefInstance>> = [];

for (const reportDetail of reportDetails) {
const orClause = {
objectID: reportDetail.id,
objectType: 'reportDetail',
} satisfies Condition<CategoryRefInstance>;

listOfCategoryRefORs.push(orClause);
}

const categoriesRef: CategoryRefInstance[] = await models.categoryRef.find({
where: {
[Cond.OR]: listOfCategoryRefORs,
},
});

const mapOfCategoriesAndReportDetails = new Map<number, ReportDetail[]>();

for (const categoryRef of categoriesRef) {
const reportDetail = reportDetails.find(
(reportDetail) => reportDetail.id === categoryRef.objectID.valueOf()
);

if (!reportDetail) {
continue;
}

if (
!mapOfCategoriesAndReportDetails.has(categoryRef.categoryID.valueOf())
) {
mapOfCategoriesAndReportDetails.set(
categoryRef.categoryID.valueOf(),
[]
);
}

const reportDetailsPerCategory = getOrCreate(
mapOfCategoriesAndReportDetails,
categoryRef.categoryID.valueOf(),
() => []
);
reportDetailsPerCategory.push(reportDetail);
}

const categories: CategoryInstance[] = await models.category.find({
where: {
id: {
[Op.IN]: categoriesRef.map((catRef) => catRef.categoryID),
},
},
});

for (const [
category,
reportDetails,
] of mapOfCategoriesAndReportDetails.entries()) {
const categoryObj = categories.find((cat) => cat.id === category);

if (!categoryObj) {
continue;
}

for (const reportDetail of reportDetails) {
reportDetail.channel = categoryObj.name;
}
}

return reportDetails;
}

/**
* This method returns the shortcut filter defined with the operation
* IN if is true or NOT IN if is false
*
* @param isPendingFlows
* @param isCommitmentFlows
* @param isPaidFlows
* @param isPledgedFlows
* @param isCarryoverFlows
* @param isParkedFlows
* @param isPassThroughFlows
* @param isStandardFlows
* @returns [{ category: String, operation: Op.IN | Op.NOT_IN}]
*/
async mapShortcutFilters(
models: Database,
isPendingFlows: boolean,
isCommitmentFlows: boolean,
isPaidFlows: boolean,
isPledgedFlows: boolean,
isCarryoverFlows: boolean,
isParkedFlows: boolean,
isPassThroughFlows: boolean,
isStandardFlows: boolean
): Promise<ShortcutCategoryFilter[] | null> {
const filters = [
{ flag: isPendingFlows, category: 'Pending' },
{ flag: isCommitmentFlows, category: 'Commitment' },
{ flag: isPaidFlows, category: 'Paid' },
{ flag: isPledgedFlows, category: 'Pledge' },
{ flag: isCarryoverFlows, category: 'Carryover' },
{ flag: isParkedFlows, category: 'Parked' },
{ flag: isPassThroughFlows, category: 'Pass Through' },
{ flag: isStandardFlows, category: 'Standard' },
];

const usedFilters = filters.filter((filter) => filter.flag !== undefined);

const searchCategories = usedFilters.map((filter) => filter.category);

const whereClause: CategoryWhere = {
[Cond.OR]: searchCategories.map((cat) => ({
name: { [Op.ILIKE]: `%${cat}%` },
})),
};

const categories = await models.category.find({
where: whereClause,
});

const shortcutFilters: ShortcutCategoryFilter[] = usedFilters
.map((filter) => {
const categoryId = categories
.find((category) => category.name.includes(filter.category))
?.id.valueOf();

return {
category: filter.category,
operation: filter.flag ? Op.IN : Op.NOT_IN,
id: categoryId,
} satisfies ShortcutCategoryFilter;
})
.filter((filter) => filter.id !== undefined);

return shortcutFilters.length > 0 ? shortcutFilters : null;
}
}
47 changes: 47 additions & 0 deletions src/domain-services/categories/graphql/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Field, Int, ObjectType } from 'type-graphql';
import { BaseType } from '../../base-types';

@ObjectType()
export class CategoryRef extends BaseType {
@Field({ nullable: false })
objectID: number;

@Field({ nullable: false })
versionID: number;

@Field({ nullable: false })
objectType: string;

@Field({ nullable: false })
categoryID: number;
}

@ObjectType()
export class Category extends BaseType {
@Field({ nullable: true })
id: number;

@Field({ nullable: false })
name: string;

@Field({ nullable: false })
group: string;

@Field({ nullable: true })
description: string;

@Field(() => Int, { nullable: true })
parentID: number | null;

@Field({ nullable: true })
code: string;

@Field({ nullable: true })
includeTotals: boolean;

@Field(() => CategoryRef, { nullable: true })
categoryRef: CategoryRef;

@Field({ nullable: false })
versionID: number;
}
7 changes: 7 additions & 0 deletions src/domain-services/categories/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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;
};
Loading

0 comments on commit 3d597bc

Please sign in to comment.