Skip to content

[TM-2084] Move demographics new controller and support querying #194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4f2c676
[TM-2084] feat: add demographics controller and service to app module
Scriptmatico Jun 3, 2025
e02f183
[TM-2084] feat: implement DemographicDtoV2 and update demographics se…
Scriptmatico Jun 4, 2025
263bb2f
[TM-2084] feat: enhance demographic service and query DTO to support …
Scriptmatico Jun 4, 2025
b1f9b5b
[TM-2084] feat: add setup script and policy for demographics, and con…
Scriptmatico Jun 4, 2025
802b849
[TM-2084] add demographic.service.spec.ts unit test
Scriptmatico Jun 5, 2025
ff5ac96
[TM-2084] feat: update demographic service to support project and sit…
Scriptmatico Jun 5, 2025
d09c44b
[TM-2084] feat: enhance demographic service to validate filters and s…
Scriptmatico Jun 5, 2025
179d289
[TM-2084] Merge branch 'staging' into feat/TM-2084-Move-demographics-…
Scriptmatico Jun 6, 2025
e6e848b
[TM-2084] feat: refactor demographics module to improve structure and…
Scriptmatico Jun 6, 2025
2ab0c23
[TM-2084] feat: refactor demographic DTO and service for improved que…
Scriptmatico Jun 8, 2025
f2de920
[TM-2084] feat: restructure demographic service and query DTO for imp…
Scriptmatico Jun 10, 2025
f057aae
[TM-2084] Merge branch 'staging' into feat/TM-2084-Move-demographics-…
Scriptmatico Jun 11, 2025
cbfbfc6
[TM-2084] Add LARAVEL_MODELS constant and update DemographicDto const…
Scriptmatico Jun 11, 2025
f654add
[TM-2084] Fix syntax error in POLICIES array by adding missing comma …
Scriptmatico Jun 11, 2025
58f267e
[TM-2084] fix laravel types import
Scriptmatico Jun 11, 2025
7a5826d
[TM-2084] Update demographic tests to include demographicalType in mo…
Scriptmatico Jun 12, 2025
484e110
[TM-2084] Update demographic tests to include demographicalId in mock…
Scriptmatico Jun 12, 2025
918faf5
[TM-2084] Merge branch 'staging' into feat/TM-2084-Move-demographics-…
Scriptmatico Jun 17, 2025
47a8360
[TM-2084] Enhance DemographicsController with error handling and logg…
Scriptmatico Jun 17, 2025
0127b24
[TM-2084] Refactor DemographicsController to utilize LARAVEL_MODEL_TY…
Scriptmatico Jun 17, 2025
5058d78
[TM-2084] Remove unused import for MediaOwnerType in DemographicsCont…
Scriptmatico Jun 17, 2025
251e7e8
[TM-2084] Refactor DemographicsController and tests to utilize Projec…
Scriptmatico Jun 18, 2025
e178818
[TM-2084] Refactor association types in DTO and constants to include …
Scriptmatico Jun 18, 2025
1820aee
[TM-2084] Remove unused import for Media in laravel-types.ts
Scriptmatico Jun 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/entity-service/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ 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";
import { ImpactStoriesController } from "./entities/impact-stories.controller";
import { ImpactStoryService } from "./entities/impact-story.service";

@Module({
imports: [SentryModule.forRoot(), CommonModule, HealthModule, DataApiModule],
Expand Down
15 changes: 13 additions & 2 deletions apps/entity-service/src/entities/demographic.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe("DemographicsController", () => {
let controller: DemographicsController;
let demographicService: DeepMocked<DemographicService>;
let policyService: DeepMocked<PolicyService>;
const LARAVEL_TYPE = "App\\Models\\V2\\Projects\\Project";

beforeEach(async () => {
const module = await Test.createTestingModule({
Expand Down Expand Up @@ -48,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" } as Demographic),
new Demographic({ uuid: "2", type: "type 2" } 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
Expand Down
40 changes: 20 additions & 20 deletions apps/entity-service/src/entities/demographic.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,31 @@ type DemographicFilter<T extends Model = Model> = {
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)) {
Expand Down
8 changes: 7 additions & 1 deletion apps/entity-service/src/entities/demographics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/laravel-types";

@Controller("entities/v3/demographics")
export class DemographicsController {
Expand All @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This demographicData isn't the right shape. I'm a little surprised it compiles... it's been tricky getting the type safety guards right on those DTOs. It should look like this (example from the association processor that is also creating this DTO):

const additionalProps = { entityType: this.entityType, entityUuid: this.entityUuid };

I'm also surprised it's not complaining that model could be null. In this case, something like this should be right:

const { demographicalType: entityType, demographicalId } = demographic;
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);
}
}
Expand Down
5 changes: 2 additions & 3 deletions apps/entity-service/src/entities/dto/demographic-query.dto.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
17 changes: 5 additions & 12 deletions apps/entity-service/src/entities/dto/demographic.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,12 @@ export class DemographicEntryDto {

@JsonApiDto({ type: "demographics" })
export class DemographicDto extends AssociationDto {
constructor(demographic: Demographic, additional: AdditionalProps<DemographicDto, Demographic>);
constructor(demographic: Demographic);
constructor(demographic: Demographic, additional?: AdditionalProps<DemographicDto, Demographic>) {
constructor(demographic: Demographic, additional: AdditionalProps<DemographicDto, Demographic>) {
super();

if (additional != null) {
populateDto<DemographicDto, Omit<Demographic, "entries">>(this, demographic, {
...additional,
entries: demographic.entries?.map(entry => new DemographicEntryDto(entry)) ?? []
});
} else {
populateDto<DemographicDto>(this, demographic as unknown as DemographicDto);
}
populateDto<DemographicDto, Omit<Demographic, "entries">>(this, demographic, {
...additional,
entries: demographic.entries?.map(entry => new DemographicEntryDto(entry)) ?? []
});
}

@ApiProperty()
Expand Down
14 changes: 13 additions & 1 deletion libs/common/src/lib/policies/policy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ import { TMLogger } from "../util/tm-logger";
import { ProjectPitchPolicy } from "./project-pitch.policy";
import { TaskPolicy } from "./task.policy";
import { DemographicPolicy } from "./demographic.policy";
import { AuditStatusPolicy } from "./audit-status.policy";
import { FinancialIndicatorPolicy } from "./financial-indicator.policy";
import { FormPolicy } from "./form.policy";
import { FormQuestionOptionPolicy } from "./form-question-option.policy";
import { FundingProgrammePolicy } from "./funding-programme.policy";
import { ImpactStoryPolicy } from "./impact-story.policy";

type EntityClass = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -58,7 +64,13 @@ const POLICIES: [EntityClass, PolicyClass][] = [
[User, UserPolicy],
[ProjectPitch, ProjectPitchPolicy],
[Task, TaskPolicy],
[Demographic, DemographicPolicy]
[Demographic, DemographicPolicy],
[AuditStatus, AuditStatusPolicy],
[FinancialIndicator, FinancialIndicatorPolicy],
[Form, FormPolicy],
[FormQuestionOption, FormQuestionOptionPolicy],
[FundingProgramme, FundingProgrammePolicy],
[ImpactStory, ImpactStoryPolicy]
];

/**
Expand Down
9 changes: 9 additions & 0 deletions libs/database/src/lib/constants/laravel-types.ts
Original file line number Diff line number Diff line change
@@ -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
};