Skip to content

Commit

Permalink
feat(api): Add BaseWidget data retrieval for map visualization mode
Browse files Browse the repository at this point in the history
  • Loading branch information
alepefe committed Oct 31, 2024
1 parent 2513608 commit d707336
Show file tree
Hide file tree
Showing 12 changed files with 1,270 additions and 286 deletions.
165 changes: 165 additions & 0 deletions api/src/infrastructure/postgres-survey-answers.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { DataSource, getMetadataArgsStorage, Repository } from 'typeorm';
import { ISurveyAnswerRepository } from '@api/infrastructure/survey-answer-repository.interface';
import { Inject, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { WidgetDataFilters } from '@shared/dto/widgets/widget-data-filter';
import { SectionWithDataWidget } from '@shared/dto/sections/section.entity';
import { SQLAdapter } from '@api/infrastructure/sql-adapter';
import {
BaseWidgetWithData,
WidgetChartData,
WidgetData,
} from '@shared/dto/widgets/base-widget-data.interface';
import { WidgetUtils } from '@shared/dto/widgets/widget.utils';
import { SurveyAnswer } from '@shared/dto/surveys/survey-answer.entity';

export class PostgresSurveyAnswerRepository
extends Repository<SurveyAnswer>
implements ISurveyAnswerRepository
{
private readonly answersTable: string;
private readonly edgeCasesMethodNameMap: Record<string, string> = {
'total-surveys': this.addTotalSurveysDataToWidget.name,
'total-countries': this.addTotalCountriesDataToWidget.name,
};

public constructor(
private readonly logger: Logger,
@Inject(SQLAdapter) private readonly sqlAdapter: SQLAdapter,
@InjectDataSource() private readonly dataSource: DataSource,
) {
super(SurveyAnswer, dataSource.manager);
this.answersTable = this.getAnswersTableName();
}

private getAnswersTableName(): string {
const ormMetadata = getMetadataArgsStorage();
const tableMetadata = ormMetadata.tables.find(
(table) => table.target === SurveyAnswer,
);
if (tableMetadata === undefined) {
throw new Error(`Table metadata for ${SurveyAnswer.name} not found`);
}
return tableMetadata.name;
}

public async addSurveyDataToSections(
sections: SectionWithDataWidget[],
filters?: WidgetDataFilters,
): Promise<SectionWithDataWidget[]> {
let filterClause: string;
if (filters !== undefined) {
filterClause = this.sqlAdapter.generateSqlFromWidgetDataFilters(filters);
}

const widgetDataPromises = [];
for (let sectionIdx = 0; sectionIdx < sections.length; sectionIdx++) {
const section = sections[sectionIdx];
const baseWidgets = section.baseWidgets;
for (let widgetIdx = 0; widgetIdx < baseWidgets.length; widgetIdx++) {
const widget = baseWidgets[widgetIdx];
widgetDataPromises.push(
this.appendBaseWidgetData(widget, filterClause),
);
}
}

await Promise.all(widgetDataPromises);
return sections;
}

public async addSurveyDataToBaseWidget(
widget: BaseWidgetWithData,
filters?: WidgetDataFilters,
): Promise<BaseWidgetWithData> {
let filterClause: string;
if (filters !== undefined) {
filterClause = this.sqlAdapter.generateSqlFromWidgetDataFilters(filters);
}

await this.appendBaseWidgetData(widget, filterClause);
return widget;
}

private async appendBaseWidgetData(
widget: BaseWidgetWithData,
filterClause: string,
): Promise<void> {
const { indicator } = widget;

// Check if the indicator is an edge case
const methodName = this.edgeCasesMethodNameMap[indicator];
if (methodName !== undefined) {
return this[methodName](widget, filterClause);
}

const widgetData: WidgetData = {};
const [
supportsChart,
supportsSingleValue,
supportsMap,
supportsNavigation,
] = WidgetUtils.getSupportedVisualizations(widget);

if (supportsChart === true) {
const totalsSql = `SELECT answer as "key", count(answer)::integer as "count", SUM(COUNT(answer)) OVER ()::integer AS total
FROM ${this.answersTable} ${this.sqlAdapter.appendExpressionToFilterClause(filterClause, `question_indicator = '${indicator}'`)} GROUP BY answer`;
const totalsResult: { key: string; count: number }[] =
await this.dataSource.query(totalsSql);

const arr: WidgetChartData = [];
for (let rowIdx = 0; rowIdx < totalsResult.length; rowIdx++) {
const res = totalsResult[rowIdx];
arr.push({ label: res.key, value: res.count, total: res.count });
}

widgetData.chart = arr;
}

if (supportsSingleValue === true) {
// TODO: Add WidgetCounterData
}

if (supportsMap === true) {
const mapSql = `SELECT country_code as country, COUNT(survey_id)::integer AS "count"
FROM ${this.answersTable}
GROUP BY country_code, question, answer
HAVING question = '${widget.question}' AND answer = 'Yes'`;

const result = await this.dataSource.query(mapSql);
widgetData.map = result;
}

if (supportsNavigation === true) {
// TODO: Add WidgetNavigationData
}

widget.data = widgetData;
}

private async addTotalSurveysDataToWidget(
widget: BaseWidgetWithData,
filterClause: string,
): Promise<void> {
const filteredCount = `SELECT COUNT(count)::integer as count FROM (SELECT COUNT(DISTINCT survey_id) FROM ${this.answersTable} ${filterClause} GROUP BY survey_id) AS survey_count`;
const totalCount = `SELECT COUNT(count)::integer as count FROM (SELECT COUNT(DISTINCT survey_id) FROM ${this.answersTable} GROUP BY survey_id) AS survey_count`;
const [[{ count: value }], [{ count: total }]] = await Promise.all([
this.dataSource.query(filteredCount),
this.dataSource.query(totalCount),
]);
widget.data = { counter: { value, total } };
}

private async addTotalCountriesDataToWidget(
widget: BaseWidgetWithData,
filterClause: string,
): Promise<void> {
const filteredCount = `SELECT COUNT(DISTINCT country_code)::integer as "count" FROM ${this.answersTable} ${filterClause}`;
const totalCount = `SELECT COUNT(DISTINCT country_code)::integer as "count" FROM ${this.answersTable};`;
const [[{ count: value }], [{ count: total }]] = await Promise.all([
this.dataSource.query(filteredCount),
this.dataSource.query(totalCount),
]);
widget.data = { counter: { value, total } };
}
}
131 changes: 0 additions & 131 deletions api/src/infrastructure/postgres-survey-data.repository.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
Section,
SectionWithDataWidget,
} from '@shared/dto/sections/section.entity';
import { SurveyAnswer } from '@shared/dto/surveys/survey-answer.entity';
import { BaseWidgetWithData } from '@shared/dto/widgets/base-widget-data.interface';
import { BaseWidget } from '@shared/dto/widgets/base-widget.entity';
import { WidgetDataFilter } from '@shared/dto/widgets/widget-data-filter';
import { Repository } from 'typeorm';

export const SurveyDataRepository = Symbol('ISurveyDataRepository');
export const SurveyAnswerRepository = Symbol('ISurveyAnswerRepository');

export interface ISurveyDataRepository {
export interface ISurveyAnswerRepository extends Repository<SurveyAnswer> {
addSurveyDataToSections(
sections: Partial<Section>[],
filters?: WidgetDataFilter[],
Expand Down
9 changes: 6 additions & 3 deletions api/src/modules/sections/sections.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { SectionsService } from '@api/modules/sections/sections.service';
import { Section } from '@shared/dto/sections/section.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '@api/modules/auth/auth.module';
import { PostgresSurveyDataRepository } from '@api/infrastructure/postgres-survey-data.repository';
import { PostgresSurveyAnswerRepository } from '@api/infrastructure/postgres-survey-answers.repository';
import { SQLAdapter } from '@api/infrastructure/sql-adapter';
import { SurveyDataRepository } from '@api/infrastructure/survey-data-repository.interface';
import { SurveyAnswerRepository } from '@api/infrastructure/survey-answer-repository.interface';

@Module({
imports: [TypeOrmModule.forFeature([Section]), forwardRef(() => AuthModule)],
providers: [
SQLAdapter,
{ provide: SurveyDataRepository, useClass: PostgresSurveyDataRepository },
{
provide: SurveyAnswerRepository,
useClass: PostgresSurveyAnswerRepository,
},
SectionsService,
],
controllers: [SectionsController],
Expand Down
14 changes: 7 additions & 7 deletions api/src/modules/sections/sections.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
SectionWithDataWidget,
} from '@shared/dto/sections/section.entity';
import { FetchSpecification } from 'nestjs-base-service';
import {
ISurveyDataRepository,
SurveyDataRepository,
} from '@api/infrastructure/survey-data-repository.interface';
import { WidgetDataFiltersSchema } from '@shared/schemas/widget-data-filters.schema';
import {
ISurveyAnswerRepository,
SurveyAnswerRepository,
} from '@api/infrastructure/survey-answer-repository.interface';

@Injectable()
export class SectionsService extends AppBaseService<
Expand All @@ -24,8 +24,8 @@ export class SectionsService extends AppBaseService<
public constructor(
@InjectRepository(Section)
private readonly sectionsRepository: Repository<SectionWithDataWidget>,
@Inject(SurveyDataRepository)
private readonly surveyDataRepository: ISurveyDataRepository,
@Inject(SurveyAnswerRepository)
private readonly surveyAnswerRepository: ISurveyAnswerRepository,
) {
super(sectionsRepository, 'section', 'sections');
}
Expand All @@ -42,7 +42,7 @@ export class SectionsService extends AppBaseService<
const { filters } = query;

const sectionsWithData =
await this.surveyDataRepository.addSurveyDataToSections(
await this.surveyAnswerRepository.addSurveyDataToSections(
sections,
filters,
);
Expand Down
4 changes: 2 additions & 2 deletions api/src/modules/widgets/widgets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class WidgetsController {

@Public()
@TsRestHandler(c.getWidgets)
async getWidgets(): Promise<ControllerResponse> {
public async getWidgets(): Promise<ControllerResponse> {
return tsRestHandler(c.getWidgets, async ({ query }) => {
const widgets = await this.widgetsService.findAllPaginated(query);
return { body: widgets, status: HttpStatus.OK };
Expand All @@ -20,7 +20,7 @@ export class WidgetsController {

@Public()
@TsRestHandler(c.getWidget)
async getWidget(): Promise<any> {
public async getWidget(): Promise<ControllerResponse> {
return tsRestHandler(c.getWidget, async ({ params: { id }, query }) => {
const widget = await this.widgetsService.findWidgetWithDataById(
id,
Expand Down
Loading

0 comments on commit d707336

Please sign in to comment.