From 4f2c6765cb0cb2e6110048bb1f1a04a2bc35ad62 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Tue, 3 Jun 2025 11:46:46 -0600 Subject: [PATCH 01/21] [TM-2084] feat: add demographics controller and service to app module --- apps/entity-service/src/app.module.ts | 6 ++- .../src/entities/demographic.service.ts | 34 ++++++++++++++ .../src/entities/demographics.controller.ts | 44 +++++++++++++++++++ .../src/entities/dto/demographic-query.dto.ts | 29 ++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 apps/entity-service/src/entities/demographic.service.ts create mode 100644 apps/entity-service/src/entities/demographics.controller.ts create mode 100644 apps/entity-service/src/entities/dto/demographic-query.dto.ts diff --git a/apps/entity-service/src/app.module.ts b/apps/entity-service/src/app.module.ts index 422b757e..14086fb4 100644 --- a/apps/entity-service/src/app.module.ts +++ b/apps/entity-service/src/app.module.ts @@ -15,6 +15,8 @@ import { TasksService } from "./entities/tasks.service"; import { BoundingBoxController } from "./bounding-boxes/bounding-box.controller"; import { BoundingBoxService } from "./bounding-boxes/bounding-box.service"; import { DataApiModule } from "@terramatch-microservices/data-api"; +import { DemographicsController } from "./entities/demographics.controller"; +import { DemographicService } from "./entities/demographic.service"; @Module({ imports: [SentryModule.forRoot(), CommonModule, HealthModule, DataApiModule], @@ -24,6 +26,7 @@ import { DataApiModule } from "@terramatch-microservices/data-api"; controllers: [ ProjectPitchesController, TasksController, + DemographicsController, EntitiesController, EntityAssociationsController, TreesController, @@ -38,7 +41,8 @@ import { DataApiModule } from "@terramatch-microservices/data-api"; TreeService, ProjectPitchService, BoundingBoxService, - TasksService + TasksService, + DemographicService ] }) export class AppModule {} diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts new file mode 100644 index 00000000..031b65bf --- /dev/null +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -0,0 +1,34 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { ProjectPitch } from "@terramatch-microservices/database/entities"; +import { Includeable, Op } from "sequelize"; +import { ProjectPitchQueryDto } from "./dto/project-pitch-query.dto"; +import { PaginatedQueryBuilder } from "@terramatch-microservices/common/util/paginated-query.builder"; +import { DemographicQueryDto } from "./dto/demographic-query.dto"; + +@Injectable() +export class DemographicService { + async getDemographics(query: DemographicQueryDto) { + const organisationAssociation: Includeable = { + association: "organisation", + attributes: ["uuid", "name"] + }; + const builder = PaginatedQueryBuilder.forNumberPage(ProjectPitch, query, [organisationAssociation]); + + if (query.sort?.field != null) { + if ( + ["id", "organisationId", "projectName", "projectCountry", "restorationInterventionTypes", "createdAt"].includes( + query.sort.field + ) + ) { + builder.order([query.sort.field, query.sort.direction ?? "ASC"]); + } else { + throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); + } + } + return { + data: await builder.execute(), + paginationTotal: await builder.paginationTotal(), + pageNumber: query.page?.number ?? 1 + }; + } +} diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts new file mode 100644 index 00000000..d24606c3 --- /dev/null +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -0,0 +1,44 @@ +import { BadRequestException, Controller, Get, HttpStatus, NotFoundException, Param, Query } from "@nestjs/common"; +import { buildJsonApi, getStableRequestQuery } from "@terramatch-microservices/common/util"; +import { ApiOperation } from "@nestjs/swagger"; +import { ExceptionResponse, JsonApiResponse } from "@terramatch-microservices/common/decorators"; +import { ProjectPitchDto } from "./dto/project-pitch.dto"; +import { PolicyService } from "@terramatch-microservices/common"; +import { DemographicDto } from "./dto/demographic.dto"; +import { DemographicQueryDto } from "./dto/demographic-query.dto"; +import { DemographicService } from "./demographic.service"; + +@Controller("entities/v3/demographics") +export class DemographicsController { + constructor(private readonly demographicService: DemographicService, private readonly policyService: PolicyService) {} + + @Get() + @ApiOperation({ + operationId: "projectPitchIndex", + summary: "Get projects pitches." + }) + @JsonApiResponse([{ data: DemographicDto, pagination: "number" }]) + @ExceptionResponse(BadRequestException, { description: "Param types invalid" }) + @ExceptionResponse(NotFoundException, { description: "Records not found" }) + async demographicsIndex(@Query() params: DemographicQueryDto) { + const { data, paginationTotal, pageNumber } = await this.demographicService.getDemographics(params); + const document = buildJsonApi(ProjectPitchDto, { pagination: "number" }); + const indexIds: string[] = []; + if (data.length !== 0) { + await this.policyService.authorize("read", data); + for (const pitch of data) { + indexIds.push(pitch.uuid); + const pitchDto = new ProjectPitchDto(pitch); + document.addData(pitchDto.uuid, pitchDto); + } + } + document.addIndexData({ + resource: "demographics", + requestPath: `/entities/v3/demographics${getStableRequestQuery(params)}`, + ids: indexIds, + total: paginationTotal, + pageNumber: pageNumber + }); + return document.serialize(); + } +} diff --git a/apps/entity-service/src/entities/dto/demographic-query.dto.ts b/apps/entity-service/src/entities/dto/demographic-query.dto.ts new file mode 100644 index 00000000..b55a105b --- /dev/null +++ b/apps/entity-service/src/entities/dto/demographic-query.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty, IntersectionType } from "@nestjs/swagger"; +import { IsArray, IsEnum, IsOptional, ValidateNested } from "class-validator"; +import { NumberPage } from "@terramatch-microservices/common/dto/page.dto"; + +class QuerySort { + @ApiProperty({ name: "sort[field]", required: false }) + @IsOptional() + field?: string; + + @ApiProperty({ name: "sort[direction]", required: false, enum: ["ASC", "DESC"], default: "ASC" }) + @IsEnum(["ASC", "DESC"]) + @IsOptional() + direction?: "ASC" | "DESC"; +} + +export class DemographicQueryDto extends IntersectionType(QuerySort, NumberPage) { + @ValidateNested() + @IsOptional() + page?: NumberPage; + + @ValidateNested() + @IsOptional() + sort?: QuerySort; + + @ApiProperty({ required: false, isArray: true, description: "projectReport uuid array" }) + @IsOptional() + @IsArray() + projectReportUuid?: string[]; +} From e02f183faf5bb4a90a50b86ad07d9b9fae203b0c Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Tue, 3 Jun 2025 21:18:10 -0600 Subject: [PATCH 02/21] [TM-2084] feat: implement DemographicDtoV2 and update demographics service and controller for new structure --- .../src/entities/demographic.service.ts | 10 ++------- .../src/entities/demographics.controller.ts | 13 ++++++------ .../src/entities/dto/demographic.dto.ts | 21 ++++++++++++++++++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts index 031b65bf..d052e810 100644 --- a/apps/entity-service/src/entities/demographic.service.ts +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -1,18 +1,12 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { ProjectPitch } from "@terramatch-microservices/database/entities"; -import { Includeable, Op } from "sequelize"; -import { ProjectPitchQueryDto } from "./dto/project-pitch-query.dto"; +import { Demographic, ProjectPitch } from "@terramatch-microservices/database/entities"; import { PaginatedQueryBuilder } from "@terramatch-microservices/common/util/paginated-query.builder"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; @Injectable() export class DemographicService { async getDemographics(query: DemographicQueryDto) { - const organisationAssociation: Includeable = { - association: "organisation", - attributes: ["uuid", "name"] - }; - const builder = PaginatedQueryBuilder.forNumberPage(ProjectPitch, query, [organisationAssociation]); + const builder = PaginatedQueryBuilder.forNumberPage(Demographic, query); if (query.sort?.field != null) { if ( diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index d24606c3..cc0eb15a 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -2,9 +2,8 @@ import { BadRequestException, Controller, Get, HttpStatus, NotFoundException, Pa import { buildJsonApi, getStableRequestQuery } from "@terramatch-microservices/common/util"; import { ApiOperation } from "@nestjs/swagger"; import { ExceptionResponse, JsonApiResponse } from "@terramatch-microservices/common/decorators"; -import { ProjectPitchDto } from "./dto/project-pitch.dto"; import { PolicyService } from "@terramatch-microservices/common"; -import { DemographicDto } from "./dto/demographic.dto"; +import { DemographicDto, DemographicDtoV2 } from "./dto/demographic.dto"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { DemographicService } from "./demographic.service"; @@ -22,14 +21,14 @@ export class DemographicsController { @ExceptionResponse(NotFoundException, { description: "Records not found" }) async demographicsIndex(@Query() params: DemographicQueryDto) { const { data, paginationTotal, pageNumber } = await this.demographicService.getDemographics(params); - const document = buildJsonApi(ProjectPitchDto, { pagination: "number" }); + const document = buildJsonApi(DemographicDto, { pagination: "number" }); const indexIds: string[] = []; if (data.length !== 0) { await this.policyService.authorize("read", data); - for (const pitch of data) { - indexIds.push(pitch.uuid); - const pitchDto = new ProjectPitchDto(pitch); - document.addData(pitchDto.uuid, pitchDto); + for (const demographic of data) { + indexIds.push(demographic.uuid); + const demographicDto = new DemographicDtoV2(demographic); + document.addData(demographicDto.uuid, demographicDto); } } document.addIndexData({ diff --git a/apps/entity-service/src/entities/dto/demographic.dto.ts b/apps/entity-service/src/entities/dto/demographic.dto.ts index be2af281..a8ae2fed 100644 --- a/apps/entity-service/src/entities/dto/demographic.dto.ts +++ b/apps/entity-service/src/entities/dto/demographic.dto.ts @@ -1,5 +1,5 @@ import { AssociationDto } from "./association.dto"; -import { Demographic, DemographicEntry } from "@terramatch-microservices/database/entities"; +import { Demographic, DemographicEntry, ProjectPitch } from "@terramatch-microservices/database/entities"; import { ApiProperty } from "@nestjs/swagger"; import { AdditionalProps, populateDto } from "@terramatch-microservices/common/dto/json-api-attributes"; import { JsonApiDto } from "@terramatch-microservices/common/decorators"; @@ -95,3 +95,22 @@ export class DemographicDto extends AssociationDto { @ApiProperty({ type: () => DemographicEntryDto, isArray: true }) entries: DemographicEntryDto[]; } + +@JsonApiDto({ type: "demographics" }) +export class DemographicDtoV2 { + constructor(data: Demographic) { + populateDto(this, data as DemographicDtoV2); + } + + @ApiProperty() + uuid: string; + + @ApiProperty({ enum: Demographic.VALID_TYPES }) + type: string; + + @ApiProperty() + collection: string; + + @ApiProperty({ type: () => DemographicEntryDto, isArray: true }) + entries: DemographicEntryDto[]; +} From 263bb2f6a70b28bdeac9972b055b42fe06ce458a Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 4 Jun 2025 16:17:49 -0600 Subject: [PATCH 03/21] [TM-2084] feat: enhance demographic service and query DTO to support project report filtering --- .../src/entities/demographic.service.ts | 19 +++++++++++++------ .../src/entities/dto/demographic-query.dto.ts | 10 ++++++++++ .../common/src/lib/policies/policy.service.ts | 5 ++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts index d052e810..bf684dec 100644 --- a/apps/entity-service/src/entities/demographic.service.ts +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { Demographic, ProjectPitch } from "@terramatch-microservices/database/entities"; +import { Demographic, ProjectPitch, ProjectReport } from "@terramatch-microservices/database/entities"; import { PaginatedQueryBuilder } from "@terramatch-microservices/common/util/paginated-query.builder"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; @@ -9,16 +9,23 @@ export class DemographicService { const builder = PaginatedQueryBuilder.forNumberPage(Demographic, query); if (query.sort?.field != null) { - if ( - ["id", "organisationId", "projectName", "projectCountry", "restorationInterventionTypes", "createdAt"].includes( - query.sort.field - ) - ) { + if (["id", "type"].includes(query.sort.field)) { builder.order([query.sort.field, query.sort.direction ?? "ASC"]); } else { throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); } } + if (query.projectReportUuid != null && query.projectReportUuid.length > 0) { + const projectReports = await ProjectReport.findAll({ + where: { uuid: query.projectReportUuid } + }); + if (projectReports.length === 0) { + throw new NotFoundException("No project reports found for the provided UUIDs"); + } + builder.where({ + projectPitchId: projectReports.map(pitch => pitch.id) + }); + } return { data: await builder.execute(), paginationTotal: await builder.paginationTotal(), diff --git a/apps/entity-service/src/entities/dto/demographic-query.dto.ts b/apps/entity-service/src/entities/dto/demographic-query.dto.ts index b55a105b..e0e85dbe 100644 --- a/apps/entity-service/src/entities/dto/demographic-query.dto.ts +++ b/apps/entity-service/src/entities/dto/demographic-query.dto.ts @@ -22,8 +22,18 @@ export class DemographicQueryDto extends IntersectionType(QuerySort, NumberPage) @IsOptional() sort?: QuerySort; + @ApiProperty({ required: false, isArray: true, description: "project uuid array" }) + @IsOptional() + @IsArray() + projectUuid?: string[]; + @ApiProperty({ required: false, isArray: true, description: "projectReport uuid array" }) @IsOptional() @IsArray() projectReportUuid?: string[]; + + @ApiProperty({ required: false, isArray: true, description: "siteReport uuid array" }) + @IsOptional() + @IsArray() + siteReportUuid?: string[]; } diff --git a/libs/common/src/lib/policies/policy.service.ts b/libs/common/src/lib/policies/policy.service.ts index efbac162..8026209a 100644 --- a/libs/common/src/lib/policies/policy.service.ts +++ b/libs/common/src/lib/policies/policy.service.ts @@ -2,6 +2,7 @@ import { Injectable, Scope, UnauthorizedException } from "@nestjs/common"; import { RequestContext } from "nestjs-request-context"; import { UserPolicy } from "./user.policy"; import { + Demographic, Nursery, NurseryReport, Permission, @@ -28,6 +29,7 @@ import { NurseryPolicy } from "./nursery.policy"; import { TMLogger } from "../util/tm-logger"; import { ProjectPitchPolicy } from "./project-pitch.policy"; import { TaskPolicy } from "./task.policy"; +import { DemographicPolicy } from "@terramatch-microservices/common/policies/demographic.policy"; type EntityClass = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -49,7 +51,8 @@ const POLICIES: [EntityClass, PolicyClass][] = [ [SiteReport, SiteReportPolicy], [User, UserPolicy], [ProjectPitch, ProjectPitchPolicy], - [Task, TaskPolicy] + [Task, TaskPolicy], + [Demographic, DemographicPolicy] ]; /** From b1f9b5ba4c8a02ce617398fa754761b0f96f7012 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 4 Jun 2025 16:21:25 -0600 Subject: [PATCH 04/21] [TM-2084] feat: add setup script and policy for demographics, and configure yarn settings --- libs/common/src/lib/policies/demographic.policy.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 libs/common/src/lib/policies/demographic.policy.ts diff --git a/libs/common/src/lib/policies/demographic.policy.ts b/libs/common/src/lib/policies/demographic.policy.ts new file mode 100644 index 00000000..96bdb4e6 --- /dev/null +++ b/libs/common/src/lib/policies/demographic.policy.ts @@ -0,0 +1,11 @@ +import { Demographic } from "@terramatch-microservices/database/entities"; +import { UserPermissionsPolicy } from "./user-permissions.policy"; + +export class DemographicPolicy extends UserPermissionsPolicy { + async addRules() { + if (this.frameworks.length > 0) { + this.builder.can("read", Demographic); + return; + } + } +} From 802b8492e3423faa958c2dd9707ceb8911cc3b07 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Thu, 5 Jun 2025 09:59:03 -0600 Subject: [PATCH 05/21] [TM-2084] add demographic.service.spec.ts unit test --- .../src/entities/demographic.service.spec.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 apps/entity-service/src/entities/demographic.service.spec.ts diff --git a/apps/entity-service/src/entities/demographic.service.spec.ts b/apps/entity-service/src/entities/demographic.service.spec.ts new file mode 100644 index 00000000..e5cb145b --- /dev/null +++ b/apps/entity-service/src/entities/demographic.service.spec.ts @@ -0,0 +1,80 @@ +import { Test } from "@nestjs/testing"; +import { Demographic, ProjectPitch, User } from "@terramatch-microservices/database/entities"; +import { NumberPage } from "@terramatch-microservices/common/dto/page.dto"; +import { DemographicService } from "./demographic.service"; +import { DemographicQueryDto } from "./dto/demographic-query.dto"; + +function getDefaultPagination() { + const params = new DemographicQueryDto(); + params.page = new NumberPage(); + params.page.number = 2; + params.page.size = 10; + return params; +} + +describe("DemographicService", () => { + let service: DemographicService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [DemographicService] + }).compile(); + + service = module.get(DemographicService); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("Get Demographics", () => { + it("returns paginated demographics", async () => { + const demographics = [ + new Demographic({ uuid: "uuid y", type: "type 1" } as Demographic), + new Demographic({ uuid: "uuid x", type: "type 2" } as Demographic) + ]; + jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); + + const params = getDefaultPagination(); + + const result = await service.getDemographics(params); + + expect(result.data).toHaveLength(2); + expect(result.data[0].uuid).toBe("uuid y"); + expect(result.data[1].uuid).toBe("uuid x"); + expect(result.pageNumber).toBe(1); + }); + + it("applies projectReportUuid filter", async () => { + const demographics = [new Demographic({ uuid: "uuid1", type: "Filtered" } as Demographic)]; + jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); + + const params = getDefaultPagination(); + params.projectReportUuid = ["uuid1", "uid2"]; + + const result = await service.getDemographics(params); + expect(result.data).toHaveLength(1); + }); + + it("deny orders fields", async () => { + const demographics = [new Demographic({ uuid: "uuid1", type: "Filtered" } as Demographic)]; + jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); + + const params = getDefaultPagination(); + params.sort = { field: "no_exist_column", direction: "ASC" }; + + await expect(service.getDemographics(params)).rejects.toThrow("Invalid sort field: no_exist_column"); + }); + + it("applies order correctly", async () => { + const demographics = [new Demographic({ uuid: "uuid1", type: "Filtered" } as Demographic)]; + jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); + + const params = getDefaultPagination(); + params.sort = { field: "type", direction: "ASC" }; + + const result = await service.getDemographics(params); + expect(result.data).toHaveLength(1); + }); + }); +}); From ff5ac96603a7d410e67ad97de62619ac868a6867 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Thu, 5 Jun 2025 11:58:43 -0600 Subject: [PATCH 06/21] [TM-2084] feat: update demographic service to support project and site report filtering --- .../entities/demographic.controller.spec.ts | 78 +++++++++++++++++++ .../src/entities/demographic.service.spec.ts | 2 +- .../src/entities/demographic.service.ts | 62 +++++++++++++-- .../src/lib/entities/demographic.entity.ts | 13 +++- 4 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 apps/entity-service/src/entities/demographic.controller.spec.ts diff --git a/apps/entity-service/src/entities/demographic.controller.spec.ts b/apps/entity-service/src/entities/demographic.controller.spec.ts new file mode 100644 index 00000000..f29b39ef --- /dev/null +++ b/apps/entity-service/src/entities/demographic.controller.spec.ts @@ -0,0 +1,78 @@ +import { Demographic } from "@terramatch-microservices/database/entities"; +import { createMock, DeepMocked } from "@golevelup/ts-jest"; +import { Test } from "@nestjs/testing"; +import { BadRequestException } from "@nestjs/common"; +import { PolicyService } from "@terramatch-microservices/common"; +import { ProjectPitchQueryDto } from "./dto/project-pitch-query.dto"; +import { DemographicsController } from "./demographics.controller"; +import { DemographicService } from "./demographic.service"; +import { DemographicQueryDto } from "./dto/demographic-query.dto"; + +describe("DemographicsController", () => { + let controller: DemographicsController; + let demographicService: DeepMocked; + let policyService: DeepMocked; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + controllers: [DemographicsController], + providers: [ + { provide: DemographicService, useValue: (demographicService = createMock()) }, + { provide: PolicyService, useValue: (policyService = createMock()) } + ] + }).compile(); + + controller = module.get(DemographicsController); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("Demographics Index", () => { + it("should return demographics successfully", async () => { + const mockResponse = { + data: [], + paginationTotal: 0, + pageNumber: 1 + }; + policyService.getPermissions.mockResolvedValue(["framework-ppc"]); + + demographicService.getDemographics.mockResolvedValue(mockResponse); + + const result = await controller.demographicsIndex(new DemographicQueryDto()); + expect(demographicService.getDemographics).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + }); + + it("should return an array of 3 demographics successfully", async () => { + const mockResponse = { + data: [ + new Demographic({ uuid: "1", type: "type 1" } as Demographic), + new Demographic({ uuid: "2", type: "type 2" } as Demographic) + ], + paginationTotal: 3, + pageNumber: 1 + }; + policyService.getPermissions.mockResolvedValue(["framework-ppc"]); + demographicService.getDemographics.mockResolvedValue(mockResponse); + + const result = await controller.demographicsIndex(new DemographicQueryDto()); + expect(result).toBeDefined(); + expect(Array.isArray(result.data) ? result.data.length : 0).toBe(2); + expect(demographicService.getDemographics).toHaveBeenCalledTimes(1); + }); + + it("should throw BadRequestException for invalid parameters", async () => { + demographicService.getDemographics.mockRejectedValue(new BadRequestException("Invalid parameters")); + + await expect(controller.demographicsIndex(new ProjectPitchQueryDto())).rejects.toThrow(BadRequestException); + }); + + it("should handle unexpected errors gracefully", async () => { + demographicService.getDemographics.mockRejectedValue(new Error("Unexpected error")); + + await expect(controller.demographicsIndex(new ProjectPitchQueryDto())).rejects.toThrow(Error); + }); + }); +}); diff --git a/apps/entity-service/src/entities/demographic.service.spec.ts b/apps/entity-service/src/entities/demographic.service.spec.ts index e5cb145b..21d48211 100644 --- a/apps/entity-service/src/entities/demographic.service.spec.ts +++ b/apps/entity-service/src/entities/demographic.service.spec.ts @@ -7,7 +7,7 @@ import { DemographicQueryDto } from "./dto/demographic-query.dto"; function getDefaultPagination() { const params = new DemographicQueryDto(); params.page = new NumberPage(); - params.page.number = 2; + params.page.number = 1; params.page.size = 10; return params; } diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts index bf684dec..ecb9449f 100644 --- a/apps/entity-service/src/entities/demographic.service.ts +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -1,7 +1,15 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { Demographic, ProjectPitch, ProjectReport } from "@terramatch-microservices/database/entities"; +import { + Demographic, + Project, + ProjectPitch, + ProjectReport, + ProjectUser, + SiteReport +} from "@terramatch-microservices/database/entities"; import { PaginatedQueryBuilder } from "@terramatch-microservices/common/util/paginated-query.builder"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; +import { Op } from "sequelize"; @Injectable() export class DemographicService { @@ -15,16 +23,56 @@ export class DemographicService { throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); } } + if (query.projectUuid != null && query.projectUuid.length > 0) { + const project = await Project.findAll({ + attributes: ["id"], + where: { uuid: { [Op.in]: query.projectReportUuid } } + }); + + if (project.length > 0) { + const projectDemographics = Demographic.idsSubquery( + project.map(report => report.id), + Project.LARAVEL_TYPE + ); + + builder.where({ + id: { [Op.in]: projectDemographics } + }); + } + } if (query.projectReportUuid != null && query.projectReportUuid.length > 0) { - const projectReports = await ProjectReport.findAll({ - where: { uuid: query.projectReportUuid } + const projectReport = await ProjectReport.findAll({ + attributes: ["id"], + where: { uuid: { [Op.in]: query.projectReportUuid } } }); - if (projectReports.length === 0) { - throw new NotFoundException("No project reports found for the provided UUIDs"); + + if (projectReport.length > 0) { + const projectReportDemographics = Demographic.idsSubquery( + projectReport.map(report => report.id), + ProjectReport.LARAVEL_TYPE + ); + + builder.where({ + id: { [Op.in]: projectReportDemographics } + }); } - builder.where({ - projectPitchId: projectReports.map(pitch => pitch.id) + } + if (query.siteReportUuid != null && query.siteReportUuid.length > 0) { + const siteReports = await SiteReport.findAll({ + attributes: ["id"], + where: { uuid: { [Op.in]: query.siteReportUuid } } }); + + if (siteReports.length > 0) { + const siteReportDemographic = Demographic.idsSubquery( + siteReports.map(report => report.id), + SiteReport.LARAVEL_TYPE + ); + + builder.where({ + id: { [Op.in]: siteReportDemographic } + }); + } } return { data: await builder.execute(), diff --git a/libs/database/src/lib/entities/demographic.entity.ts b/libs/database/src/lib/entities/demographic.entity.ts index 28cedf18..de1f320d 100644 --- a/libs/database/src/lib/entities/demographic.entity.ts +++ b/libs/database/src/lib/entities/demographic.entity.ts @@ -36,12 +36,17 @@ export class Demographic extends Model { Demographic.INDIRECT_BENEFICIARIES_TYPE ] as const; - static idsSubquery(demographicalIds: Literal | number[], demographicalType: string, type: DemographicType) { - return Subquery.select(Demographic, "id") + static idsSubquery(demographicalIds: Literal | number[], demographicalType: string, type?: DemographicType) { + const query = Subquery.select(Demographic, "id") .eq("demographicalType", demographicalType) .in("demographicalId", demographicalIds) - .eq("hidden", false) - .eq("type", type).literal; + .eq("hidden", false); + + if (type != null) { + query.eq("type", type); + } + + return query.literal; } @PrimaryKey From d09c44b2ec2d211e9fe5c1faf497cdb16aec6026 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Thu, 5 Jun 2025 16:45:45 -0600 Subject: [PATCH 07/21] [TM-2084] feat: enhance demographic service to validate filters and support querying by project and site reports --- .../src/entities/demographic.service.spec.ts | 34 +++++- .../src/entities/demographic.service.ts | 110 +++++++++--------- 2 files changed, 85 insertions(+), 59 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.service.spec.ts b/apps/entity-service/src/entities/demographic.service.spec.ts index 21d48211..9bc0cede 100644 --- a/apps/entity-service/src/entities/demographic.service.spec.ts +++ b/apps/entity-service/src/entities/demographic.service.spec.ts @@ -1,5 +1,5 @@ import { Test } from "@nestjs/testing"; -import { Demographic, ProjectPitch, User } from "@terramatch-microservices/database/entities"; +import { Demographic } from "@terramatch-microservices/database/entities"; import { NumberPage } from "@terramatch-microservices/common/dto/page.dto"; import { DemographicService } from "./demographic.service"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; @@ -45,6 +45,27 @@ describe("DemographicService", () => { expect(result.pageNumber).toBe(1); }); + it("deny unexpected filter", async () => { + const demographics = [new Demographic({ uuid: "uuid10", type: "Filtered" } as Demographic)]; + jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); + + const params = getDefaultPagination(); + params["unexpectedFilter"] = ["value1", "value2"]; + + await expect(service.getDemographics(params)).rejects.toThrow("Invalid filter key: unexpectedFilter"); + }); + + it("applies projectUuid filter", async () => { + const demographics = [new Demographic({ uuid: "uuid10", type: "Filtered" } as Demographic)]; + jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); + + const params = getDefaultPagination(); + params.projectReportUuid = ["uuid10", "uid20"]; + + const result = await service.getDemographics(params); + expect(result.data).toHaveLength(1); + }); + it("applies projectReportUuid filter", async () => { const demographics = [new Demographic({ uuid: "uuid1", type: "Filtered" } as Demographic)]; jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); @@ -56,6 +77,17 @@ describe("DemographicService", () => { expect(result.data).toHaveLength(1); }); + it("applies siteReportUuid filter", async () => { + const demographics = [new Demographic({ uuid: "uuid44", type: "Filtered" } as Demographic)]; + jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); + + const params = getDefaultPagination(); + params.projectReportUuid = ["uuid44", "uid45"]; + + const result = await service.getDemographics(params); + expect(result.data).toHaveLength(1); + }); + it("deny orders fields", async () => { const demographics = [new Demographic({ uuid: "uuid1", type: "Filtered" } as Demographic)]; jest.spyOn(Demographic, "findAll").mockImplementation(() => Promise.resolve(demographics)); diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts index ecb9449f..d12d320e 100644 --- a/apps/entity-service/src/entities/demographic.service.ts +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -1,79 +1,73 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { - Demographic, - Project, - ProjectPitch, - ProjectReport, - ProjectUser, - SiteReport -} from "@terramatch-microservices/database/entities"; +import { BadRequestException, Injectable } from "@nestjs/common"; +import { Demographic, Project, ProjectReport, SiteReport } from "@terramatch-microservices/database/entities"; import { PaginatedQueryBuilder } from "@terramatch-microservices/common/util/paginated-query.builder"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; -import { Op } from "sequelize"; +import { Model, ModelStatic, Op } from "sequelize"; @Injectable() export class DemographicService { async getDemographics(query: DemographicQueryDto) { const builder = PaginatedQueryBuilder.forNumberPage(Demographic, query); - if (query.sort?.field != null) { - if (["id", "type"].includes(query.sort.field)) { - builder.order([query.sort.field, query.sort.direction ?? "ASC"]); - } else { - throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); - } - } - if (query.projectUuid != null && query.projectUuid.length > 0) { - const project = await Project.findAll({ - attributes: ["id"], - where: { uuid: { [Op.in]: query.projectReportUuid } } - }); + type DemographicFilter = { + uuidKey: string; + model: ModelStatic>; + laravelType: string; + }; - if (project.length > 0) { - const projectDemographics = Demographic.idsSubquery( - project.map(report => report.id), - Project.LARAVEL_TYPE - ); + const demographicFilters: DemographicFilter[] = [ + { + uuidKey: "projectUuid", + model: Project, + laravelType: Project.LARAVEL_TYPE + }, + { + uuidKey: "projectReportUuid", + model: ProjectReport, + laravelType: ProjectReport.LARAVEL_TYPE + }, + { + uuidKey: "siteReportUuid", + model: SiteReport, + laravelType: SiteReport.LARAVEL_TYPE + } + ]; - builder.where({ - id: { [Op.in]: projectDemographics } - }); + Object.entries(query).forEach(([key]) => { + if (key === "page" || key === "sort") return; + if (!demographicFilters.map(d => d.uuidKey).includes(key)) { + throw new BadRequestException(`Invalid filter key: ${key}`); } - } - if (query.projectReportUuid != null && query.projectReportUuid.length > 0) { - const projectReport = await ProjectReport.findAll({ - attributes: ["id"], - where: { uuid: { [Op.in]: query.projectReportUuid } } - }); + }); - if (projectReport.length > 0) { - const projectReportDemographics = Demographic.idsSubquery( - projectReport.map(report => report.id), - ProjectReport.LARAVEL_TYPE - ); + for (const { uuidKey, model, laravelType } of demographicFilters) { + const uuids = query[uuidKey]; + if (uuids != null && uuids.length > 0) { + const records = (await model.findAll({ + attributes: ["id"], + where: { uuid: { [Op.in]: uuids } } + })) as unknown as { id: number }[]; - builder.where({ - id: { [Op.in]: projectReportDemographics } - }); + if (records.length > 0) { + const demographicIds = Demographic.idsSubquery( + records.map(record => record.id), + laravelType + ); + builder.where({ + id: { [Op.in]: demographicIds } + }); + } } } - if (query.siteReportUuid != null && query.siteReportUuid.length > 0) { - const siteReports = await SiteReport.findAll({ - attributes: ["id"], - where: { uuid: { [Op.in]: query.siteReportUuid } } - }); - - if (siteReports.length > 0) { - const siteReportDemographic = Demographic.idsSubquery( - siteReports.map(report => report.id), - SiteReport.LARAVEL_TYPE - ); - builder.where({ - id: { [Op.in]: siteReportDemographic } - }); + if (query.sort?.field != null) { + if (["id", "type"].includes(query.sort.field)) { + builder.order([query.sort.field, query.sort.direction ?? "ASC"]); + } else { + throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); } } + return { data: await builder.execute(), paginationTotal: await builder.paginationTotal(), From e6e848b44b402d2c7cdb2f29777db6ae9eed6b27 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Fri, 6 Jun 2025 12:30:48 -0600 Subject: [PATCH 08/21] [TM-2084] feat: refactor demographics module to improve structure and enhance query capabilities --- apps/entity-service/src/entities/demographic.service.ts | 4 ++-- apps/entity-service/src/entities/demographics.controller.ts | 6 +++--- apps/entity-service/src/entities/dto/demographic.dto.ts | 2 +- libs/common/src/lib/policies/policy.service.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts index d12d320e..5b5efeff 100644 --- a/apps/entity-service/src/entities/demographic.service.ts +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -9,9 +9,9 @@ export class DemographicService { async getDemographics(query: DemographicQueryDto) { const builder = PaginatedQueryBuilder.forNumberPage(Demographic, query); - type DemographicFilter = { + type DemographicFilter = { uuidKey: string; - model: ModelStatic>; + model: ModelStatic; laravelType: string; }; diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index cc0eb15a..9b414865 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Controller, Get, HttpStatus, NotFoundException, Param, Query } from "@nestjs/common"; +import { BadRequestException, Controller, Get, NotFoundException, Query } from "@nestjs/common"; import { buildJsonApi, getStableRequestQuery } from "@terramatch-microservices/common/util"; import { ApiOperation } from "@nestjs/swagger"; import { ExceptionResponse, JsonApiResponse } from "@terramatch-microservices/common/decorators"; @@ -13,8 +13,8 @@ export class DemographicsController { @Get() @ApiOperation({ - operationId: "projectPitchIndex", - summary: "Get projects pitches." + operationId: "demographicsIndex", + summary: "Get demographics." }) @JsonApiResponse([{ data: DemographicDto, pagination: "number" }]) @ExceptionResponse(BadRequestException, { description: "Param types invalid" }) diff --git a/apps/entity-service/src/entities/dto/demographic.dto.ts b/apps/entity-service/src/entities/dto/demographic.dto.ts index a8ae2fed..f8d7f889 100644 --- a/apps/entity-service/src/entities/dto/demographic.dto.ts +++ b/apps/entity-service/src/entities/dto/demographic.dto.ts @@ -1,5 +1,5 @@ import { AssociationDto } from "./association.dto"; -import { Demographic, DemographicEntry, ProjectPitch } from "@terramatch-microservices/database/entities"; +import { Demographic, DemographicEntry } from "@terramatch-microservices/database/entities"; import { ApiProperty } from "@nestjs/swagger"; import { AdditionalProps, populateDto } from "@terramatch-microservices/common/dto/json-api-attributes"; import { JsonApiDto } from "@terramatch-microservices/common/decorators"; diff --git a/libs/common/src/lib/policies/policy.service.ts b/libs/common/src/lib/policies/policy.service.ts index 8026209a..b258634f 100644 --- a/libs/common/src/lib/policies/policy.service.ts +++ b/libs/common/src/lib/policies/policy.service.ts @@ -29,7 +29,7 @@ import { NurseryPolicy } from "./nursery.policy"; import { TMLogger } from "../util/tm-logger"; import { ProjectPitchPolicy } from "./project-pitch.policy"; import { TaskPolicy } from "./task.policy"; -import { DemographicPolicy } from "@terramatch-microservices/common/policies/demographic.policy"; +import { DemographicPolicy } from "./demographic.policy"; type EntityClass = { // eslint-disable-next-line @typescript-eslint/no-explicit-any From 2ab0c2346b6daea8c9a3cb11186521a52db226a1 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Sun, 8 Jun 2025 15:16:45 -0600 Subject: [PATCH 09/21] [TM-2084] feat: refactor demographic DTO and service for improved querying and validation --- .../src/entities/demographic.service.ts | 28 ++++++++------- .../src/entities/demographics.controller.ts | 4 +-- .../src/entities/dto/demographic-query.dto.ts | 24 ++----------- .../src/entities/dto/demographic.dto.ts | 34 ++++++------------- 4 files changed, 31 insertions(+), 59 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts index 5b5efeff..d55539c7 100644 --- a/apps/entity-service/src/entities/demographic.service.ts +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -4,18 +4,16 @@ import { PaginatedQueryBuilder } from "@terramatch-microservices/common/util/pag import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { Model, ModelStatic, Op } from "sequelize"; +type DemographicFilter = { + uuidKey: string; + model: ModelStatic; + laravelType: string; +}; + @Injectable() export class DemographicService { async getDemographics(query: DemographicQueryDto) { - const builder = PaginatedQueryBuilder.forNumberPage(Demographic, query); - - type DemographicFilter = { - uuidKey: string; - model: ModelStatic; - laravelType: string; - }; - - const demographicFilters: DemographicFilter[] = [ + const DEMOGRAPHIC_FILTERS: DemographicFilter[] = [ { uuidKey: "projectUuid", model: Project, @@ -31,16 +29,20 @@ export class DemographicService { model: SiteReport, laravelType: SiteReport.LARAVEL_TYPE } - ]; + ] as const; + + const builder = PaginatedQueryBuilder.forNumberPage(Demographic, query); + + const VALID_FILTER_KEYS = DEMOGRAPHIC_FILTERS.map(({ uuidKey }) => uuidKey); - Object.entries(query).forEach(([key]) => { + Object.keys(query).forEach(key => { if (key === "page" || key === "sort") return; - if (!demographicFilters.map(d => d.uuidKey).includes(key)) { + if (!VALID_FILTER_KEYS.includes(key)) { throw new BadRequestException(`Invalid filter key: ${key}`); } }); - for (const { uuidKey, model, laravelType } of demographicFilters) { + for (const { uuidKey, model, laravelType } of DEMOGRAPHIC_FILTERS) { const uuids = query[uuidKey]; if (uuids != null && uuids.length > 0) { const records = (await model.findAll({ diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index 9b414865..af25644a 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -3,7 +3,7 @@ import { buildJsonApi, getStableRequestQuery } from "@terramatch-microservices/c import { ApiOperation } from "@nestjs/swagger"; import { ExceptionResponse, JsonApiResponse } from "@terramatch-microservices/common/decorators"; import { PolicyService } from "@terramatch-microservices/common"; -import { DemographicDto, DemographicDtoV2 } from "./dto/demographic.dto"; +import { DemographicDto } from "./dto/demographic.dto"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { DemographicService } from "./demographic.service"; @@ -27,7 +27,7 @@ export class DemographicsController { await this.policyService.authorize("read", data); for (const demographic of data) { indexIds.push(demographic.uuid); - const demographicDto = new DemographicDtoV2(demographic); + const demographicDto = new DemographicDto(demographic); document.addData(demographicDto.uuid, demographicDto); } } diff --git a/apps/entity-service/src/entities/dto/demographic-query.dto.ts b/apps/entity-service/src/entities/dto/demographic-query.dto.ts index e0e85dbe..a028234b 100644 --- a/apps/entity-service/src/entities/dto/demographic-query.dto.ts +++ b/apps/entity-service/src/entities/dto/demographic-query.dto.ts @@ -1,27 +1,9 @@ import { ApiProperty, IntersectionType } from "@nestjs/swagger"; -import { IsArray, IsEnum, IsOptional, ValidateNested } from "class-validator"; +import { IsArray, IsOptional } from "class-validator"; import { NumberPage } from "@terramatch-microservices/common/dto/page.dto"; +import { IndexQueryDto } from "./index-query.dto"; -class QuerySort { - @ApiProperty({ name: "sort[field]", required: false }) - @IsOptional() - field?: string; - - @ApiProperty({ name: "sort[direction]", required: false, enum: ["ASC", "DESC"], default: "ASC" }) - @IsEnum(["ASC", "DESC"]) - @IsOptional() - direction?: "ASC" | "DESC"; -} - -export class DemographicQueryDto extends IntersectionType(QuerySort, NumberPage) { - @ValidateNested() - @IsOptional() - page?: NumberPage; - - @ValidateNested() - @IsOptional() - sort?: QuerySort; - +export class DemographicQueryDto extends IntersectionType(IndexQueryDto, NumberPage) { @ApiProperty({ required: false, isArray: true, description: "project uuid array" }) @IsOptional() @IsArray() diff --git a/apps/entity-service/src/entities/dto/demographic.dto.ts b/apps/entity-service/src/entities/dto/demographic.dto.ts index f8d7f889..08a7d826 100644 --- a/apps/entity-service/src/entities/dto/demographic.dto.ts +++ b/apps/entity-service/src/entities/dto/demographic.dto.ts @@ -75,31 +75,19 @@ export class DemographicEntryDto { @JsonApiDto({ type: "demographics" }) export class DemographicDto extends AssociationDto { - constructor(demographic: Demographic, additional: AdditionalProps) { + constructor(demographic: Demographic, additional: AdditionalProps); + constructor(demographic: Demographic); + constructor(demographic: Demographic, additional?: AdditionalProps) { super(); - populateDto>(this, demographic, { - ...additional, - entries: demographic.entries?.map(entry => new DemographicEntryDto(entry)) ?? [] - }); - } - - @ApiProperty() - uuid: string; - @ApiProperty({ enum: Demographic.VALID_TYPES }) - type: string; - - @ApiProperty() - collection: string; - - @ApiProperty({ type: () => DemographicEntryDto, isArray: true }) - entries: DemographicEntryDto[]; -} - -@JsonApiDto({ type: "demographics" }) -export class DemographicDtoV2 { - constructor(data: Demographic) { - populateDto(this, data as DemographicDtoV2); + if (additional != null) { + populateDto>(this, demographic, { + ...additional, + entries: demographic.entries?.map(entry => new DemographicEntryDto(entry)) ?? [] + }); + } else { + populateDto(this, demographic as unknown as DemographicDto); + } } @ApiProperty() From f2de92024f9307b77f6719ca98d82646f0e04f6d Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Tue, 10 Jun 2025 08:46:45 -0600 Subject: [PATCH 10/21] [TM-2084] feat: restructure demographic service and query DTO for improved readability and maintainability --- .../src/entities/demographic.service.ts | 40 +++++++++---------- .../src/entities/dto/demographic-query.dto.ts | 5 +-- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.service.ts b/apps/entity-service/src/entities/demographic.service.ts index d55539c7..3d3eaf14 100644 --- a/apps/entity-service/src/entities/demographic.service.ts +++ b/apps/entity-service/src/entities/demographic.service.ts @@ -10,31 +10,31 @@ type DemographicFilter = { laravelType: string; }; +const DEMOGRAPHIC_FILTERS: DemographicFilter[] = [ + { + uuidKey: "projectUuid", + model: Project, + laravelType: Project.LARAVEL_TYPE + }, + { + uuidKey: "projectReportUuid", + model: ProjectReport, + laravelType: ProjectReport.LARAVEL_TYPE + }, + { + uuidKey: "siteReportUuid", + model: SiteReport, + laravelType: SiteReport.LARAVEL_TYPE + } +] as const; + +const VALID_FILTER_KEYS = DEMOGRAPHIC_FILTERS.map(({ uuidKey }) => uuidKey); + @Injectable() export class DemographicService { async getDemographics(query: DemographicQueryDto) { - const DEMOGRAPHIC_FILTERS: DemographicFilter[] = [ - { - uuidKey: "projectUuid", - model: Project, - laravelType: Project.LARAVEL_TYPE - }, - { - uuidKey: "projectReportUuid", - model: ProjectReport, - laravelType: ProjectReport.LARAVEL_TYPE - }, - { - uuidKey: "siteReportUuid", - model: SiteReport, - laravelType: SiteReport.LARAVEL_TYPE - } - ] as const; - const builder = PaginatedQueryBuilder.forNumberPage(Demographic, query); - const VALID_FILTER_KEYS = DEMOGRAPHIC_FILTERS.map(({ uuidKey }) => uuidKey); - Object.keys(query).forEach(key => { if (key === "page" || key === "sort") return; if (!VALID_FILTER_KEYS.includes(key)) { diff --git a/apps/entity-service/src/entities/dto/demographic-query.dto.ts b/apps/entity-service/src/entities/dto/demographic-query.dto.ts index a028234b..5710b0af 100644 --- a/apps/entity-service/src/entities/dto/demographic-query.dto.ts +++ b/apps/entity-service/src/entities/dto/demographic-query.dto.ts @@ -1,9 +1,8 @@ -import { ApiProperty, IntersectionType } from "@nestjs/swagger"; +import { ApiProperty } from "@nestjs/swagger"; import { IsArray, IsOptional } from "class-validator"; -import { NumberPage } from "@terramatch-microservices/common/dto/page.dto"; import { IndexQueryDto } from "./index-query.dto"; -export class DemographicQueryDto extends IntersectionType(IndexQueryDto, NumberPage) { +export class DemographicQueryDto extends IndexQueryDto { @ApiProperty({ required: false, isArray: true, description: "project uuid array" }) @IsOptional() @IsArray() From cbfbfc6a22c6181c21d9bf256800777525482caf Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 11 Jun 2025 16:32:19 -0600 Subject: [PATCH 11/21] [TM-2084] Add LARAVEL_MODELS constant and update DemographicDto constructor for additional data retrieval --- .../src/entities/demographics.controller.ts | 8 +++++++- .../src/entities/dto/demographic.dto.ts | 17 +++++------------ libs/database/src/lib/constants/index.ts | 1 + .../database/src/lib/constants/laravel-types.ts | 9 +++++++++ 4 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 libs/database/src/lib/constants/laravel-types.ts diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index af25644a..5d372cf2 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -6,6 +6,7 @@ import { PolicyService } from "@terramatch-microservices/common"; import { DemographicDto } from "./dto/demographic.dto"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { DemographicService } from "./demographic.service"; +import { LARAVEL_MODELS } from "@terramatch-microservices/database/constants"; @Controller("entities/v3/demographics") export class DemographicsController { @@ -27,7 +28,12 @@ export class DemographicsController { await this.policyService.authorize("read", data); for (const demographic of data) { indexIds.push(demographic.uuid); - const demographicDto = new DemographicDto(demographic); + const model = LARAVEL_MODELS[demographic.demographicalType]; + const demographicData = await model.findOne({ + where: { id: demographic.demographicalId }, + attributes: ["id", "uuid"] + }); + const demographicDto = new DemographicDto(demographic, demographicData); document.addData(demographicDto.uuid, demographicDto); } } diff --git a/apps/entity-service/src/entities/dto/demographic.dto.ts b/apps/entity-service/src/entities/dto/demographic.dto.ts index 08a7d826..be2af281 100644 --- a/apps/entity-service/src/entities/dto/demographic.dto.ts +++ b/apps/entity-service/src/entities/dto/demographic.dto.ts @@ -75,19 +75,12 @@ export class DemographicEntryDto { @JsonApiDto({ type: "demographics" }) export class DemographicDto extends AssociationDto { - constructor(demographic: Demographic, additional: AdditionalProps); - constructor(demographic: Demographic); - constructor(demographic: Demographic, additional?: AdditionalProps) { + constructor(demographic: Demographic, additional: AdditionalProps) { super(); - - if (additional != null) { - populateDto>(this, demographic, { - ...additional, - entries: demographic.entries?.map(entry => new DemographicEntryDto(entry)) ?? [] - }); - } else { - populateDto(this, demographic as unknown as DemographicDto); - } + populateDto>(this, demographic, { + ...additional, + entries: demographic.entries?.map(entry => new DemographicEntryDto(entry)) ?? [] + }); } @ApiProperty() diff --git a/libs/database/src/lib/constants/index.ts b/libs/database/src/lib/constants/index.ts index 5e2003e7..4a65386f 100644 --- a/libs/database/src/lib/constants/index.ts +++ b/libs/database/src/lib/constants/index.ts @@ -3,3 +3,4 @@ export * from "./polygon-status"; export * from "./landscapes"; export * from "./organisations"; export * from "./framework-tf"; +export * from "./laravel-types"; diff --git a/libs/database/src/lib/constants/laravel-types.ts b/libs/database/src/lib/constants/laravel-types.ts new file mode 100644 index 00000000..2c910946 --- /dev/null +++ b/libs/database/src/lib/constants/laravel-types.ts @@ -0,0 +1,9 @@ +import { Organisation, Project, ProjectPitch, ProjectReport, SiteReport } from "../entities"; + +export const LARAVEL_MODELS = { + [Organisation.LARAVEL_TYPE]: Organisation, + [Project.LARAVEL_TYPE]: Project, + [ProjectPitch.LARAVEL_TYPE]: ProjectPitch, + [ProjectReport.LARAVEL_TYPE]: ProjectReport, + [SiteReport.LARAVEL_TYPE]: SiteReport +}; From f654add6c8b239a8ca5d3d0cbf3e06a227c456cc Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 11 Jun 2025 16:33:50 -0600 Subject: [PATCH 12/21] [TM-2084] Fix syntax error in POLICIES array by adding missing comma for DemographicPolicy entry --- libs/common/src/lib/policies/policy.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/lib/policies/policy.service.ts b/libs/common/src/lib/policies/policy.service.ts index b07402e6..9667dd93 100644 --- a/libs/common/src/lib/policies/policy.service.ts +++ b/libs/common/src/lib/policies/policy.service.ts @@ -64,7 +64,7 @@ const POLICIES: [EntityClass, PolicyClass][] = [ [User, UserPolicy], [ProjectPitch, ProjectPitchPolicy], [Task, TaskPolicy], - [Demographic, DemographicPolicy][(Task, TaskPolicy)], + [Demographic, DemographicPolicy], [AuditStatus, AuditStatusPolicy], [FinancialIndicator, FinancialIndicatorPolicy], [Form, FormPolicy], From 58f267ed9b416bc0e492654ea7e71d9b7c0ee851 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 11 Jun 2025 16:48:27 -0600 Subject: [PATCH 13/21] [TM-2084] fix laravel types import --- apps/entity-service/src/entities/demographics.controller.ts | 2 +- libs/database/src/lib/constants/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index 5d372cf2..80d20c68 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -6,7 +6,7 @@ import { PolicyService } from "@terramatch-microservices/common"; import { DemographicDto } from "./dto/demographic.dto"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { DemographicService } from "./demographic.service"; -import { LARAVEL_MODELS } from "@terramatch-microservices/database/constants"; +import { LARAVEL_MODELS } from "@terramatch-microservices/database/constants/laravel-types"; @Controller("entities/v3/demographics") export class DemographicsController { diff --git a/libs/database/src/lib/constants/index.ts b/libs/database/src/lib/constants/index.ts index 4a65386f..5e2003e7 100644 --- a/libs/database/src/lib/constants/index.ts +++ b/libs/database/src/lib/constants/index.ts @@ -3,4 +3,3 @@ export * from "./polygon-status"; export * from "./landscapes"; export * from "./organisations"; export * from "./framework-tf"; -export * from "./laravel-types"; From 7a5826d52c4e8ceb15b1d16780d2b275241a0250 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 11 Jun 2025 21:22:32 -0600 Subject: [PATCH 14/21] [TM-2084] Update demographic tests to include demographicalType in mock data --- .../src/entities/demographic.controller.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.controller.spec.ts b/apps/entity-service/src/entities/demographic.controller.spec.ts index f29b39ef..2138385c 100644 --- a/apps/entity-service/src/entities/demographic.controller.spec.ts +++ b/apps/entity-service/src/entities/demographic.controller.spec.ts @@ -12,6 +12,7 @@ describe("DemographicsController", () => { let controller: DemographicsController; let demographicService: DeepMocked; let policyService: DeepMocked; + const LARAVEL_TYPE = "App\\Models\\V2\\Projects\\Project"; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -48,8 +49,8 @@ describe("DemographicsController", () => { it("should return an array of 3 demographics successfully", async () => { const mockResponse = { data: [ - new Demographic({ uuid: "1", type: "type 1" } as Demographic), - new Demographic({ uuid: "2", type: "type 2" } as Demographic) + new Demographic({ uuid: "1", type: "type 1", demographicalType: LARAVEL_TYPE } as Demographic), + new Demographic({ uuid: "2", type: "type 2", demographicalType: LARAVEL_TYPE } as Demographic) ], paginationTotal: 3, pageNumber: 1 From 484e110f87996381a37599b420dd72e0da3890cb Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 11 Jun 2025 21:32:24 -0600 Subject: [PATCH 15/21] [TM-2084] Update demographic tests to include demographicalId in mock data --- .../src/entities/demographic.controller.spec.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.controller.spec.ts b/apps/entity-service/src/entities/demographic.controller.spec.ts index 2138385c..8a167365 100644 --- a/apps/entity-service/src/entities/demographic.controller.spec.ts +++ b/apps/entity-service/src/entities/demographic.controller.spec.ts @@ -49,8 +49,18 @@ describe("DemographicsController", () => { it("should return an array of 3 demographics successfully", async () => { const mockResponse = { data: [ - new Demographic({ uuid: "1", type: "type 1", demographicalType: LARAVEL_TYPE } as Demographic), - new Demographic({ uuid: "2", type: "type 2", demographicalType: LARAVEL_TYPE } as Demographic) + new Demographic({ + uuid: "1", + type: "type 1", + demographicalType: LARAVEL_TYPE, + demographicalId: 1 + } as Demographic), + new Demographic({ + uuid: "2", + type: "type 2", + demographicalType: LARAVEL_TYPE, + demographicalId: 2 + } as Demographic) ], paginationTotal: 3, pageNumber: 1 From 47a83608b7ab781e295b129fda869efe074e1386 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Tue, 17 Jun 2025 14:14:28 -0600 Subject: [PATCH 16/21] [TM-2084] Enhance DemographicsController with error handling and logging for demographic entity retrieval --- .../src/entities/demographics.controller.ts | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index 80d20c68..85b9c49a 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -1,4 +1,11 @@ -import { BadRequestException, Controller, Get, NotFoundException, Query } from "@nestjs/common"; +import { + BadRequestException, + Controller, + Get, + InternalServerErrorException, + NotFoundException, + Query +} from "@nestjs/common"; import { buildJsonApi, getStableRequestQuery } from "@terramatch-microservices/common/util"; import { ApiOperation } from "@nestjs/swagger"; import { ExceptionResponse, JsonApiResponse } from "@terramatch-microservices/common/decorators"; @@ -7,9 +14,13 @@ import { DemographicDto } from "./dto/demographic.dto"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { DemographicService } from "./demographic.service"; import { LARAVEL_MODELS } from "@terramatch-microservices/database/constants/laravel-types"; +import { TMLogger } from "@terramatch-microservices/common/util/tm-logger"; +import { MediaOwnerType } from "@terramatch-microservices/database/constants/media-owners"; @Controller("entities/v3/demographics") export class DemographicsController { + private logger = new TMLogger(DemographicsController.name); + constructor(private readonly demographicService: DemographicService, private readonly policyService: PolicyService) {} @Get() @@ -28,12 +39,20 @@ export class DemographicsController { await this.policyService.authorize("read", data); for (const demographic of data) { indexIds.push(demographic.uuid); - const model = LARAVEL_MODELS[demographic.demographicalType]; - const demographicData = await model.findOne({ - where: { id: demographic.demographicalId }, - attributes: ["id", "uuid"] - }); - const demographicDto = new DemographicDto(demographic, demographicData); + const { demographicalType, demographicalId } = demographic; + const entityType = demographicalType as MediaOwnerType; + const model = LARAVEL_MODELS[entityType]; + if (model == null) { + this.logger.error("Unknown model type", entityType); + throw new InternalServerErrorException("Unexpected demographic association type"); + } + const entity = await model.findOne({ where: { id: demographicalId }, attributes: ["uuid"] }); + if (entity == null) { + this.logger.error("Demographic parent entity not found", { entityType, id: demographicalId }); + throw new NotFoundException(); + } + const additionalProps = { entityType, entityUuid: entity.uuid }; + const demographicDto = new DemographicDto(demographic, additionalProps); document.addData(demographicDto.uuid, demographicDto); } } From 0127b24bf79469d97c702b700ec57f56b0b6adee Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Tue, 17 Jun 2025 17:07:42 -0600 Subject: [PATCH 17/21] [TM-2084] Refactor DemographicsController to utilize LARAVEL_MODEL_TYPES for demographicalType mapping and add Project to Demographic import --- .../src/entities/demographic.controller.spec.ts | 7 +++---- .../src/entities/demographics.controller.ts | 4 ++-- libs/database/src/lib/constants/laravel-types.ts | 8 ++++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.controller.spec.ts b/apps/entity-service/src/entities/demographic.controller.spec.ts index 8a167365..55bd7d6e 100644 --- a/apps/entity-service/src/entities/demographic.controller.spec.ts +++ b/apps/entity-service/src/entities/demographic.controller.spec.ts @@ -1,4 +1,4 @@ -import { Demographic } from "@terramatch-microservices/database/entities"; +import { Demographic, Project } from "@terramatch-microservices/database/entities"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import { Test } from "@nestjs/testing"; import { BadRequestException } from "@nestjs/common"; @@ -12,7 +12,6 @@ describe("DemographicsController", () => { let controller: DemographicsController; let demographicService: DeepMocked; let policyService: DeepMocked; - const LARAVEL_TYPE = "App\\Models\\V2\\Projects\\Project"; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -52,13 +51,13 @@ describe("DemographicsController", () => { new Demographic({ uuid: "1", type: "type 1", - demographicalType: LARAVEL_TYPE, + demographicalType: Project.LARAVEL_TYPE, demographicalId: 1 } as Demographic), new Demographic({ uuid: "2", type: "type 2", - demographicalType: LARAVEL_TYPE, + demographicalType: Project.LARAVEL_TYPE, demographicalId: 2 } as Demographic) ], diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index 85b9c49a..0867b2e4 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -13,7 +13,7 @@ import { PolicyService } from "@terramatch-microservices/common"; import { DemographicDto } from "./dto/demographic.dto"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { DemographicService } from "./demographic.service"; -import { LARAVEL_MODELS } from "@terramatch-microservices/database/constants/laravel-types"; +import { LARAVEL_MODEL_TYPES, LARAVEL_MODELS } from "@terramatch-microservices/database/constants/laravel-types"; import { TMLogger } from "@terramatch-microservices/common/util/tm-logger"; import { MediaOwnerType } from "@terramatch-microservices/database/constants/media-owners"; @@ -40,7 +40,7 @@ export class DemographicsController { for (const demographic of data) { indexIds.push(demographic.uuid); const { demographicalType, demographicalId } = demographic; - const entityType = demographicalType as MediaOwnerType; + const entityType = LARAVEL_MODEL_TYPES[demographicalType]; const model = LARAVEL_MODELS[entityType]; if (model == null) { this.logger.error("Unknown model type", entityType); diff --git a/libs/database/src/lib/constants/laravel-types.ts b/libs/database/src/lib/constants/laravel-types.ts index 2c910946..2c7f2e34 100644 --- a/libs/database/src/lib/constants/laravel-types.ts +++ b/libs/database/src/lib/constants/laravel-types.ts @@ -7,3 +7,11 @@ export const LARAVEL_MODELS = { [ProjectReport.LARAVEL_TYPE]: ProjectReport, [SiteReport.LARAVEL_TYPE]: SiteReport }; + +export const LARAVEL_MODEL_TYPES = { + [Organisation.LARAVEL_TYPE]: "organisation", + [Project.LARAVEL_TYPE]: "project", + [ProjectPitch.LARAVEL_TYPE]: "project-pitch", + [ProjectReport.LARAVEL_TYPE]: "project-report", + [SiteReport.LARAVEL_TYPE]: "site-report" +}; From 5058d78ead14ed1a78d98434dea47b63e902649f Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Tue, 17 Jun 2025 17:17:18 -0600 Subject: [PATCH 18/21] [TM-2084] Remove unused import for MediaOwnerType in DemographicsController --- apps/entity-service/src/entities/demographics.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index 0867b2e4..3b635b22 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -15,7 +15,6 @@ import { DemographicQueryDto } from "./dto/demographic-query.dto"; import { DemographicService } from "./demographic.service"; import { LARAVEL_MODEL_TYPES, LARAVEL_MODELS } from "@terramatch-microservices/database/constants/laravel-types"; import { TMLogger } from "@terramatch-microservices/common/util/tm-logger"; -import { MediaOwnerType } from "@terramatch-microservices/database/constants/media-owners"; @Controller("entities/v3/demographics") export class DemographicsController { From 251e7e894189447c99dedf6d619532b03436358d Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Tue, 17 Jun 2025 20:02:15 -0600 Subject: [PATCH 19/21] [TM-2084] Refactor DemographicsController and tests to utilize ProjectFactory for demographic ID assignment and improve error logging --- .../src/entities/demographic.controller.spec.ts | 6 ++++-- .../src/entities/demographics.controller.ts | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/entity-service/src/entities/demographic.controller.spec.ts b/apps/entity-service/src/entities/demographic.controller.spec.ts index 55bd7d6e..99e588b7 100644 --- a/apps/entity-service/src/entities/demographic.controller.spec.ts +++ b/apps/entity-service/src/entities/demographic.controller.spec.ts @@ -7,6 +7,7 @@ import { ProjectPitchQueryDto } from "./dto/project-pitch-query.dto"; import { DemographicsController } from "./demographics.controller"; import { DemographicService } from "./demographic.service"; import { DemographicQueryDto } from "./dto/demographic-query.dto"; +import { ProjectFactory } from "@terramatch-microservices/database/factories"; describe("DemographicsController", () => { let controller: DemographicsController; @@ -46,19 +47,20 @@ describe("DemographicsController", () => { }); it("should return an array of 3 demographics successfully", async () => { + const project = await ProjectFactory.create(); const mockResponse = { data: [ new Demographic({ uuid: "1", type: "type 1", demographicalType: Project.LARAVEL_TYPE, - demographicalId: 1 + demographicalId: project.id } as Demographic), new Demographic({ uuid: "2", type: "type 2", demographicalType: Project.LARAVEL_TYPE, - demographicalId: 2 + demographicalId: project.id } as Demographic) ], paginationTotal: 3, diff --git a/apps/entity-service/src/entities/demographics.controller.ts b/apps/entity-service/src/entities/demographics.controller.ts index 3b635b22..a9a57155 100644 --- a/apps/entity-service/src/entities/demographics.controller.ts +++ b/apps/entity-service/src/entities/demographics.controller.ts @@ -38,18 +38,18 @@ export class DemographicsController { await this.policyService.authorize("read", data); for (const demographic of data) { indexIds.push(demographic.uuid); - const { demographicalType, demographicalId } = demographic; - const entityType = LARAVEL_MODEL_TYPES[demographicalType]; - const model = LARAVEL_MODELS[entityType]; + const { demographicalType: laravelType, demographicalId } = demographic; + const model = LARAVEL_MODELS[laravelType]; if (model == null) { - this.logger.error("Unknown model type", entityType); + this.logger.error("Unknown model type", model); throw new InternalServerErrorException("Unexpected demographic association type"); } const entity = await model.findOne({ where: { id: demographicalId }, attributes: ["uuid"] }); if (entity == null) { - this.logger.error("Demographic parent entity not found", { entityType, id: demographicalId }); + this.logger.error("Demographic parent entity not found", { model, id: demographicalId }); throw new NotFoundException(); } + const entityType = LARAVEL_MODEL_TYPES[laravelType]; const additionalProps = { entityType, entityUuid: entity.uuid }; const demographicDto = new DemographicDto(demographic, additionalProps); document.addData(demographicDto.uuid, demographicDto); From e1788189180985b4a18511b64559831d4b3c6c18 Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 18 Jun 2025 14:45:27 -0600 Subject: [PATCH 20/21] [TM-2084] Refactor association types in DTO and constants to include demographic associations and improve type definitions --- .../src/entities/dto/association.dto.ts | 18 +++++++++++++----- .../src/lib/constants/laravel-types.ts | 17 +++++++++-------- libs/database/src/lib/types/demographic.ts | 8 ++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/apps/entity-service/src/entities/dto/association.dto.ts b/apps/entity-service/src/entities/dto/association.dto.ts index a2e24ba5..6692244a 100644 --- a/apps/entity-service/src/entities/dto/association.dto.ts +++ b/apps/entity-service/src/entities/dto/association.dto.ts @@ -1,15 +1,23 @@ import { ApiProperty } from "@nestjs/swagger"; -import { ENTITY_TYPES } from "@terramatch-microservices/database/constants/entities"; -import { MediaOwnerType } from "@terramatch-microservices/database/constants/media-owners"; +import { ENTITY_TYPES, EntityType } from "@terramatch-microservices/database/constants/entities"; +import { MEDIA_OWNER_TYPES, MediaOwnerType } from "@terramatch-microservices/database/constants/media-owners"; +import { + DEMOGRAPHIC_ASSOCIATION_TYPES, + DemographicAssociationType +} from "@terramatch-microservices/database/types/demographic"; +import { uniq } from "lodash"; + +const ASSOCIATION_TYPES = uniq([...ENTITY_TYPES, ...MEDIA_OWNER_TYPES, DEMOGRAPHIC_ASSOCIATION_TYPES]); +type AssociationEntityType = EntityType | MediaOwnerType | DemographicAssociationType; export type AssociationDtoAdditionalProps = { - entityType: MediaOwnerType; + entityType: AssociationEntityType; entityUuid: string; }; export abstract class AssociationDto { - @ApiProperty({ enum: ENTITY_TYPES, description: "The entity type this resource is associated with." }) - entityType: MediaOwnerType; + @ApiProperty({ enum: ASSOCIATION_TYPES, description: "The entity type this resource is associated with." }) + entityType: AssociationEntityType; @ApiProperty({ description: "The entity UUID this resource is associated with." }) entityUuid: string; diff --git a/libs/database/src/lib/constants/laravel-types.ts b/libs/database/src/lib/constants/laravel-types.ts index 2c7f2e34..04fbd8a6 100644 --- a/libs/database/src/lib/constants/laravel-types.ts +++ b/libs/database/src/lib/constants/laravel-types.ts @@ -1,4 +1,5 @@ -import { Organisation, Project, ProjectPitch, ProjectReport, SiteReport } from "../entities"; +import { Media, Organisation, Project, ProjectPitch, ProjectReport, SiteReport } from "../entities"; +import { DemographicAssociationType } from "../types/demographic"; export const LARAVEL_MODELS = { [Organisation.LARAVEL_TYPE]: Organisation, @@ -8,10 +9,10 @@ export const LARAVEL_MODELS = { [SiteReport.LARAVEL_TYPE]: SiteReport }; -export const LARAVEL_MODEL_TYPES = { - [Organisation.LARAVEL_TYPE]: "organisation", - [Project.LARAVEL_TYPE]: "project", - [ProjectPitch.LARAVEL_TYPE]: "project-pitch", - [ProjectReport.LARAVEL_TYPE]: "project-report", - [SiteReport.LARAVEL_TYPE]: "site-report" -}; +export const LARAVEL_MODEL_TYPES: Record = { + [Organisation.LARAVEL_TYPE]: "organisations", + [Project.LARAVEL_TYPE]: "projects", + [ProjectPitch.LARAVEL_TYPE]: "projectPitches", + [ProjectReport.LARAVEL_TYPE]: "projectReports", + [SiteReport.LARAVEL_TYPE]: "siteReports" +} as const; diff --git a/libs/database/src/lib/types/demographic.ts b/libs/database/src/lib/types/demographic.ts index 56def9c9..2166b308 100644 --- a/libs/database/src/lib/types/demographic.ts +++ b/libs/database/src/lib/types/demographic.ts @@ -1,3 +1,11 @@ import { Demographic } from "../entities"; export type DemographicType = (typeof Demographic.VALID_TYPES)[number]; +export const DEMOGRAPHIC_ASSOCIATION_TYPES = [ + "organisations", + "projectPitches", + "projects", + "projectReports", + "siteReports" +] as const; +export type DemographicAssociationType = (typeof DEMOGRAPHIC_ASSOCIATION_TYPES)[number]; From 1820aee8013ba9b46e2226557ffa8d92349a08da Mon Sep 17 00:00:00 2001 From: Ismael Martinez Date: Wed, 18 Jun 2025 14:49:23 -0600 Subject: [PATCH 21/21] [TM-2084] Remove unused import for Media in laravel-types.ts --- libs/database/src/lib/constants/laravel-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/database/src/lib/constants/laravel-types.ts b/libs/database/src/lib/constants/laravel-types.ts index 04fbd8a6..df6125af 100644 --- a/libs/database/src/lib/constants/laravel-types.ts +++ b/libs/database/src/lib/constants/laravel-types.ts @@ -1,4 +1,4 @@ -import { Media, Organisation, Project, ProjectPitch, ProjectReport, SiteReport } from "../entities"; +import { Organisation, Project, ProjectPitch, ProjectReport, SiteReport } from "../entities"; import { DemographicAssociationType } from "../types/demographic"; export const LARAVEL_MODELS = {