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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ COPY --from=builder /app/dist/ /usr/share/nginx/html
ENV PORT=80
# The url to the CouchDB database
ENV COUCHDB_URL="http://localhost"
# The url to the query backend, see https://github.com/Aam-Digital/query-backend
ENV QUERY_URL="http://localhost:3000"
# The url to a nominatim instance, see https://nominatim.org/
ENV NOMINATIM_URL="https://nominatim.openstreetmap.org"

Expand All @@ -91,5 +93,5 @@ ENV CSP="default-src 'self' 'unsafe-eval' data: https://*.tile.openstreetmap.org
### 'unsafe-eval' required for pouchdb https://github.com/pouchdb/pouchdb/issues/7853#issuecomment-535020600

# variables are inserted into the nginx config
CMD envsubst '$$PORT $$COUCHDB_URL $$NOMINATIM_URL $$CSP $$CSP_REPORT_URI' < /etc/nginx/templates/default.conf > /etc/nginx/conf.d/default.conf &&\
CMD envsubst '$$PORT $$COUCHDB_URL $$QUERY_URL $$NOMINATIM_URL $$CSP $$CSP_REPORT_URI' < /etc/nginx/templates/default.conf > /etc/nginx/conf.d/default.conf &&\
nginx -g 'daemon off;'
12 changes: 12 additions & 0 deletions build/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ server {
proxy_set_header X-Forwarded-Ssl on;
}

# The proxy path should be the same as in SqlReportService.QUERY_PROXY
location ^~ /query {
rewrite /query/(.*) /$1 break;
proxy_pass ${QUERY_URL};
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Ssl on;
}

# The proxy path should be the same as in GeoService.remoteUrl
location ^~ /nominatim {
rewrite /nominatim/(.*) /$1 break;
proxy_pass ${NOMINATIM_URL};
Expand Down
9 changes: 9 additions & 0 deletions proxy.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
"/db": ""
}
},
"/query": {
"target": "http://localhost:3000",
"secure": true,
"logLevel": "debug",
"changeOrigin": true,
"pathRewrite": {
"/query": ""
}
},
"/nominatim": {
"target": "https://nominatim.openstreetmap.org",
"secure": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ describe("EntityListComponent", () => {

it("should set the clicked column group", async () => {
createComponent();
// Test only works in desktop mode
component.isDesktop = true;
await initComponentInputs();
expect(component.selectedColumnGroupIndex).toBe(1);

Expand Down
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 } 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<ReportConfig>[] = [
const demoReports: Partial<ReportEntity>[] = [
{
title: $localize`:Name of a report:Basic Report`,
aggregationDefinitions: [
Expand Down
60 changes: 49 additions & 11 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,24 +7,62 @@ 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 using this in code, use the {@link ReportEntity} instead which provides better type safety.
*/
@DatabaseEntity("ReportConfig")
export class ReportConfig extends Entity {
static create(data: Partial<ReportConfig>) {
return Object.assign(new ReportConfig(), data);
}

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

/**
* (optional) mode whether the aggregation definitions are of type {@interface Aggregation} or {@interface ExportColumnConfig}
* (optional) mode of export.
* The {@link ReportEntity} holds the restriction on valid report modes.
* Default is "reporting"
*/
@DatabaseField() mode?: "reporting" | "exporting";
@DatabaseField() mode?: string;
TheSlimvReal marked this conversation as resolved.
Show resolved Hide resolved

/** the definitions to calculate the report's aggregations */
@DatabaseField() aggregationDefinitions?:
| Aggregation[]
| ExportColumnConfig[] = [];
@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 interface AggregationReport extends ReportConfig {
mode: "reporting";
aggregationDefinitions: Aggregation[];
}

/**
* Reports handles by the {@class DataTransformationService}
*/
export interface ExportingReport extends ReportConfig {
/**
* If no mode is set, it will default to 'exporting'
*/
mode?: "exporting";
aggregationDefinitions: ExportColumnConfig[];
}

/**
* Reports handles by the {@class SqlReportService}
*/
export interface SqlReport extends ReportConfig {
mode: "sql";
/**
* Array of valid SQL SELECT statements
*/
aggregationDefinitions: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
[rows]="data"
></app-report-row>
<app-object-table
*ngIf="data?.length > 0 && mode === 'exporting'"
*ngIf="data?.length > 0 && (mode === 'exporting' || mode === 'sql')"
[objects]="data"
></app-object-table>
46 changes: 28 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,32 +19,25 @@ 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 } from "../report-config";
import { ReportEntity, SqlReport } from "../report-config";
import { SqlReportService } from "../sql-report/sql-report.service";

describe("ReportingComponent", () => {
let component: ReportingComponent;
let fixture: ComponentFixture<ReportingComponent>;
let mockReportingService: jasmine.SpyObj<DataAggregationService>;
let mockDataTransformationService: jasmine.SpyObj<DataTransformationService>;
let mockSqlReportService: jasmine.SpyObj<SqlReportService>;

const testReport: ReportConfig = 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"]);
mockDataTransformationService = jasmine.createSpyObj([
"queryAndTransformData",
]);
mockReportingService.calculateReport.and.resolveTo([]);
mockSqlReportService = jasmine.createSpyObj(["query"]);
await TestBed.configureTestingModule({
imports: [
ReportingComponent,
Expand All @@ -59,6 +52,7 @@ describe("ReportingComponent", () => {
provide: DataTransformationService,
useValue: mockDataTransformationService,
},
{ provide: SqlReportService, useValue: mockSqlReportService },
{ provide: EntityMapperService, useValue: mockEntityMapper() },
],
}).compileComponents();
Expand Down Expand Up @@ -194,23 +188,39 @@ describe("ReportingComponent", () => {
]);
}));

it("should use the export service when aggregation has mode 'exporting'", async () => {
it("should use the export service when report has mode 'exporting'", async () => {
const data = [
{ First: 1, Second: 2 },
{ First: 3, Second: 4 },
];
mockDataTransformationService.queryAndTransformData.and.resolveTo(data);
const report = new ReportEntity();
report.mode = "exporting";

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

expect(
mockDataTransformationService.queryAndTransformData,
).toHaveBeenCalledWith([], jasmine.any(Date), jasmine.any(Date));
expect(component.data).toEqual(data);
expect(component.mode).toBe("exporting");
});

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

await component.calculateResults(
report,
new Date("2023-01-01"),
new Date("2023-12-31"),
);

expect(mockSqlReportService.query).toHaveBeenCalledWith(
report,
new Date("2023-01-01"),
// Next day (to date is exclusive
new Date("2024-01-01"),
);
});
});
Loading