Skip to content
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

SQS Integration #2123

Merged
merged 14 commits into from
Dec 16, 2023
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Injectable } from "@angular/core";
import { DemoDataGenerator } from "../../core/demo-data/demo-data-generator";
import { ReportConfig, ReportType } from "./report-config";
import { ReportEntity } from "./report-config";
import { Child } from "../../child-dev-project/children/model/child";
import { School } from "../../child-dev-project/schools/model/school";
import { ChildSchoolRelation } from "../../child-dev-project/children/model/childSchoolRelation";
import { EventNote } from "../../child-dev-project/attendance/model/event-note";
import { EducationalMaterial } from "../../child-dev-project/children/educational-material/model/educational-material";

@Injectable()
export class DemoReportConfigGeneratorService extends DemoDataGenerator<ReportConfig> {
export class DemoReportConfigGeneratorService extends DemoDataGenerator<ReportEntity> {
static provider() {
return [
{
Expand All @@ -18,12 +18,14 @@ export class DemoReportConfigGeneratorService extends DemoDataGenerator<ReportCo
];
}

protected generateEntities(): ReportConfig[] {
return demoReports.map((report) => ReportConfig.create(report));
protected generateEntities(): ReportEntity[] {
return demoReports.map((report) =>
Object.assign(new ReportEntity(), report),
);
}
}

const demoReports: Partial<ReportType>[] = [
const demoReports: Partial<ReportEntity>[] = [
{
title: $localize`:Name of a report:Basic Report`,
aggregationDefinitions: [
Expand Down
36 changes: 20 additions & 16 deletions src/app/features/reporting/report-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity } from "../../core/entity/model/entity";
import { Entity, EntityConstructor } from "../../core/entity/model/entity";
import { DatabaseEntity } from "../../core/entity/database-entity.decorator";
import { Aggregation } from "./data-aggregation.service";
import { ExportColumnConfig } from "../../core/export/data-transformation-service/export-column-config";
Expand All @@ -7,38 +7,48 @@ import { DatabaseField } from "../../core/entity/database-field.decorator";
/**
* A report can be accessed by users to generate aggregated statistics or customized exports calculated from available data.
* "read" permission for a ReportConfig entity is also used to control which users can generate the report's results.
*
* This is the class which is saved to the database.
* However, when when using, use the {@link ReportEntity} instead which provides better type safety.
*/
@DatabaseEntity("ReportConfig")
export class ReportConfig extends Entity {
static create(data: Partial<ReportType>) {
return Object.assign(new ReportConfig(), data) as ReportType;
}

class ReportConfig extends Entity {
/** human-readable title of the report */
@DatabaseField() title: string;

/**
* (optional) mode of export.
* The {@link ReportEntity} holds the restriction on valid report modes.
* Default is "reporting"
*/
@DatabaseField() mode?: string;

/** the definitions to calculate the report's aggregations */
@DatabaseField() aggregationDefinitions?: any[];
@DatabaseField() aggregationDefinitions: any[] = [];
}

/**
* Union type to enable type safety for report configs.
* Use this instead of the {@class ReportConfig}
*/
export type ReportEntity = AggregationReport | ExportingReport | SqlReport;
/**
* This allows the `ReportEntity` to also be used as a constructor or in the `EntityMapper`
*/
export const ReportEntity = ReportConfig as EntityConstructor<ReportEntity>;

/**
* Reports handles by the {@class DataAggregationService}
*/
export class AggregationReport extends ReportConfig {
export interface AggregationReport extends ReportConfig {
mode: "reporting";
aggregationDefinitions: Aggregation[];
}

/**
* Reports handles by the {@class DataTransformationService}
*/
export class ExportingReport extends ReportConfig {
export interface ExportingReport extends ReportConfig {
/**
* If no mode is set, it will default to 'exporting'
*/
Expand All @@ -49,16 +59,10 @@ export class ExportingReport extends ReportConfig {
/**
* Reports handles by the {@class SqlReportService}
*/
export class SqlReport extends ReportConfig {
export interface SqlReport extends ReportConfig {
mode: "sql";
/**
* Array of valid SQL SELECT statements
*/
aggregationDefinitions: string[];
}

/**
* Union type to enable type safety for report configs.
* Use this instead of the {@class ReportConfig}
*/
export type ReportType = AggregationReport | ExportingReport | SqlReport;
24 changes: 6 additions & 18 deletions src/app/features/reporting/reporting/reporting.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testi
import { DataTransformationService } from "../../../core/export/data-transformation-service/data-transformation.service";
import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
import { mockEntityMapper } from "../../../core/entity/entity-mapper/mock-entity-mapper-service";
import { ReportConfig, SqlReport } from "../report-config";
import { ReportEntity, SqlReport } from "../report-config";
import { SqlReportService } from "../sql-report/sql-report.service";

describe("ReportingComponent", () => {
Expand All @@ -29,17 +29,7 @@ describe("ReportingComponent", () => {
let mockDataTransformationService: jasmine.SpyObj<DataTransformationService>;
let mockSqlReportService: jasmine.SpyObj<SqlReportService>;

const testReport = ReportConfig.create({
title: "test report",
aggregationDefinitions: [
{
query: "some query",
label: "some label",
groupBy: ["some", "values"],
aggregations: [],
},
],
});
const testReport = new ReportEntity();

beforeEach(async () => {
mockReportingService = jasmine.createSpyObj(["calculateReport"]);
Expand Down Expand Up @@ -204,12 +194,10 @@ describe("ReportingComponent", () => {
{ First: 3, Second: 4 },
];
mockDataTransformationService.queryAndTransformData.and.resolveTo(data);
const report = new ReportEntity();
report.mode = "exporting";

await component.calculateResults(
ReportConfig.create({ mode: "exporting", aggregationDefinitions: [] }),
new Date(),
new Date(),
);
await component.calculateResults(report, new Date(), new Date());

expect(
mockDataTransformationService.queryAndTransformData,
Expand All @@ -219,7 +207,7 @@ describe("ReportingComponent", () => {
});

it("should use the sql report service when report has mode 'sql'", async () => {
const report = new SqlReport();
const report = new ReportEntity() as SqlReport;
report.mode = "sql";

await component.calculateResults(
Expand Down
16 changes: 8 additions & 8 deletions src/app/features/reporting/reporting/reporting.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import {
GroupByDescription,
} from "../report-row";
import moment from "moment";
import { RouteTarget } from "../../../app.routing";
import { NgIf } from "@angular/common";
import { ViewTitleComponent } from "../../../core/common-components/view-title/view-title.component";
import { SelectReportComponent } from "./select-report/select-report.component";
import { ReportRowComponent } from "./report-row/report-row.component";
import { ObjectTableComponent } from "./object-table/object-table.component";
import { DataTransformationService } from "../../../core/export/data-transformation-service/data-transformation.service";
import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
import { ReportConfig, ReportType } from "../report-config";
import { ReportEntity } from "../report-config";
import { SqlReportService } from "../sql-report/sql-report.service";
import { RouteTarget } from "../../../route-target";

@RouteTarget("Reporting")
@Component({
Expand All @@ -31,8 +31,8 @@ import { SqlReportService } from "../sql-report/sql-report.service";
standalone: true,
})
export class ReportingComponent {
reports: ReportType[];
mode: ReportType["mode"];
reports: ReportEntity[];
mode: ReportEntity["mode"];
loading: boolean;

data: any[];
Expand All @@ -45,12 +45,12 @@ export class ReportingComponent {
private entityMapper: EntityMapperService,
) {
this.entityMapper
.loadType(ReportConfig)
.then((res) => (this.reports = res as ReportType[]));
.loadType(ReportEntity)
.then((res) => (this.reports = res));
}

async calculateResults(
selectedReport: ReportType,
selectedReport: ReportEntity,
fromDate: Date,
toDate: Date,
) {
Expand All @@ -73,7 +73,7 @@ export class ReportingComponent {
this.loading = false;
}

private getReportResults(report: ReportType, from: Date, to: Date) {
private getReportResults(report: ReportEntity, from: Date, to: Date) {
switch (report.mode) {
case "exporting":
return this.dataTransformationService.queryAndTransformData(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";

import { SelectReportComponent } from "./select-report.component";
import { ReportConfig } from "../../report-config";
import { ReportEntity } from "../../report-config";

describe("SelectReportComponent", () => {
let component: SelectReportComponent;
Expand All @@ -24,7 +24,7 @@ describe("SelectReportComponent", () => {
});

it("should select the first report if only one exists", () => {
const report = ReportConfig.create({ title: "someReport" });
const report = new ReportEntity();
component.reports = [report];

component.ngOnChanges({ reports: undefined });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ExportDataDirective } from "../../../../core/export/export-data-directi
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { MatTooltipModule } from "@angular/material/tooltip";
import { ReportType } from "../../report-config";
import { ReportEntity } from "../../report-config";

@Component({
selector: "app-select-report",
Expand All @@ -40,12 +40,12 @@ import { ReportType } from "../../report-config";
standalone: true,
})
export class SelectReportComponent implements OnChanges {
@Input() reports: ReportType[];
@Input() reports: ReportEntity[];
@Input() loading: boolean;
@Input() exportableData: any;
@Output() calculateClick = new EventEmitter<CalculateReportOptions>();

selectedReport: ReportType;
selectedReport: ReportEntity;
fromDate: Date;
toDate: Date;

Expand All @@ -59,7 +59,7 @@ export class SelectReportComponent implements OnChanges {
}

interface CalculateReportOptions {
report: ReportType;
report: ReportEntity;
from: Date;
to: Date;
}
22 changes: 13 additions & 9 deletions src/app/features/reporting/sql-report/sql-report.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "../../../core/entity/database-entity.decorator";
import { HttpClient } from "@angular/common/http";
import { of } from "rxjs";
import { SqlReport } from "../report-config";
import { ReportEntity, SqlReport } from "../report-config";
import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
import { mockEntityMapper } from "../../../core/entity/entity-mapper/mock-entity-mapper-service";
import { SqsSchema } from "./sqs-schema";
Expand Down Expand Up @@ -41,23 +41,25 @@ describe("SqlReportService", () => {
});

it("should query the external service with the provided report data", async () => {
const result = [{ some: "data" }];
mockHttpClient.post.and.returnValue(of(result));
const report = new SqlReport();
const mockResult = [{ some: "data" }];
mockHttpClient.post.and.returnValue(of(mockResult));
const report = new ReportEntity() as SqlReport;
report.mode = "sql";

await service.query(
const result = await service.query(
report,
moment("2023-01-01").toDate(),
moment("2024-01-01").toDate(),
);

expect(mockHttpClient.post).toHaveBeenCalledWith(
`${SqlReportService.QUERY_PROXY}/app/${report.getId(true)}`,
`${SqlReportService.QUERY_PROXY}/report/app/${report.getId(true)}`,
{
from: "2023-01-01",
to: "2024-01-01",
},
);
expect(result).toEqual(mockResult);
});

it("should generate a valid schema including all properties", () => {
Expand Down Expand Up @@ -105,19 +107,21 @@ describe("SqlReportService", () => {
mockEntities.add(SchemaTest.ENTITY_TYPE, SchemaTest);
const entityMapper = TestBed.inject(EntityMapperService);
const saveSpy = spyOn(entityMapper, "save").and.callThrough();
const report = new ReportEntity() as SqlReport;
report.mode = "sql";

await service.query(new SqlReport(), new Date(), new Date());
await service.query(report, new Date(), new Date());
expect(saveSpy).toHaveBeenCalledWith(jasmine.any(SqsSchema));

// SqsSchema exists and entity schema hasn't changed
saveSpy.calls.reset();
await service.query(new SqlReport(), new Date(), new Date());
await service.query(report, new Date(), new Date());
expect(saveSpy).not.toHaveBeenCalled();

// SqsSchema exists and entity schema changed
saveSpy.calls.reset();
SchemaTest.schema.set("test", { dataType: "string" });
await service.query(new SqlReport(), new Date(), new Date());
await service.query(report, new Date(), new Date());
expect(saveSpy).toHaveBeenCalledWith(jasmine.any(SqsSchema));

SchemaTest.schema.delete("test");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class SqlReportService {
await this.updateSchemaIfNecessary();
return firstValueFrom(
this.http.post<any[]>(
`${SqlReportService.QUERY_PROXY}/app/${report.getId(true)}`,
`${SqlReportService.QUERY_PROXY}/report/app/${report.getId(true)}`,
{
from: moment(from).format("YYYY-MM-DD"),
to: moment(to).format("YYYY-MM-DD"),
Expand Down