Skip to content
This repository has been archived by the owner on Mar 14, 2024. It is now read-only.

Detecting Report Changes: further improvements and cleanup #25

Merged
merged 3 commits into from
Feb 19, 2024
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: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ DATABASE_USER=admin
DATABASE_PASSWORD=admin
QUERY_URL=http://127.0.0.1:4984
SCHEMA_CONFIG_ID=_design/sqlite:config
REPORT_DATABASE_URL=http://127.0.0.1:5984
REPORT_DATABASE_NAME=app

CHANGES_POLL_INTERVAL=60000
2 changes: 2 additions & 0 deletions src/couchdb/dtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ export interface CouchDbChangeResult {
seq: string;

doc?: any;

deleted?: boolean;
}
2 changes: 1 addition & 1 deletion src/domain/report-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class ReportData {
asHash(): string {
return crypto
.createHash('sha256')
.update(JSON.stringify(this))
.update(JSON.stringify(this.data))
sleidig marked this conversation as resolved.
Show resolved Hide resolved
.digest('hex');
}
}
12 changes: 8 additions & 4 deletions src/report-changes/core/report-change-detector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { EntityDoc, ReportChangeDetector } from './report-change-detector';
import { ReportChangeDetector } from './report-change-detector';
import { Report } from '../../domain/report';
import { DocChangeDetails } from './report-changes.service';

import {
DocChangeDetails,
EntityDoc,
} from '../storage/database-changes.service';

describe('ReportChangeDetector', () => {
function testReportChangeDetection(
Expand All @@ -21,8 +25,8 @@ describe('ReportChangeDetector', () => {
changes: [],
seq: '',
},
new: newDoc,
previous: newDoc,
newDoc: newDoc,
previousDoc: newDoc,
};
expect(service.affectsReport(mockedDocChange)).toBe(expectedResult);
}
Expand Down
12 changes: 2 additions & 10 deletions src/report-changes/core/report-change-detector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Report } from '../../domain/report';
import { DocChangeDetails } from './report-changes.service';

import { DocChangeDetails } from '../storage/database-changes.service';

/**
* Simple class encapsulating the logic to determine if a specific report is affected by a change in the database.
Expand Down Expand Up @@ -43,12 +44,3 @@ export class ReportChangeDetector {
return true;
}
}

/**
* A doc in the database representing an entity managed in the frontend.
*/
export interface EntityDoc {
_id: string;

[key: string]: any;
}
124 changes: 62 additions & 62 deletions src/report-changes/core/report-changes.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Injectable } from '@nestjs/common';
import { EntityDoc, ReportChangeDetector } from './report-change-detector';
import { ReportChangeDetector } from './report-change-detector';
import { NotificationService } from '../../notification/core/notification.service';
import { Reference } from '../../domain/reference';
import { ReportDataChangeEvent } from '../../domain/report-data-change-event';
import { ReportCalculationOutcomeSuccess } from '../../domain/report-calculation';
import { Report } from '../../domain/report';
import { CouchdbChangesService } from '../storage/couchdb-changes.service';
import { DefaultReportStorage } from '../../report/storage/report-storage.service';
import { filter, map, mergeAll, Observable, switchMap, tap, zip } from 'rxjs';
import { filter, map, Observable, switchMap, tap, zip } from 'rxjs';
import {
CreateReportCalculationFailed,
CreateReportCalculationUseCase,
} from '../../report/core/use-cases/create-report-calculation-use-case.service';
import { DatabaseChangeResult } from '../storage/database-changes.service';
import {
DatabaseChangeResult,
DocChangeDetails,
} from '../storage/database-changes.service';

@Injectable()
export class ReportChangesService {
Expand Down Expand Up @@ -76,13 +79,11 @@ export class ReportChangesService {

monitorCouchDbChanges() {
this.couchdbChangesRepository
.subscribeToAllNewChanges()
.subscribeToAllNewChangesWithDocs()
.pipe(
mergeAll(),
tap((change: DatabaseChangeResult) =>
this.checkReportConfigUpdate(change),
tap((change: DocChangeDetails) =>
this.checkReportConfigUpdate(change.change),
),
map((c: DatabaseChangeResult) => this.getChangeDetails(c)),
switchMap((change: DocChangeDetails) =>
this.changeIsAffectingReport(change),
),
Expand All @@ -96,23 +97,6 @@ export class ReportChangesService {
});
}

/**
* Load current and previous doc for advanced change detection across all reports.
* @param change
* @private
*/
private getChangeDetails(change: DatabaseChangeResult): DocChangeDetails {
// TODO: storage to get any doc from DB (for a _rev also!)
// until then, only the .change with the id can be used in ReportChangeDetector
// can also use ?include_docs=true in the changes request to get the latest doc

return {
change: change,
previous: { _id: '' }, // cache this here to avoid requests?
new: { _id: '' },
};
}

private changeIsAffectingReport(
docChange: DocChangeDetails,
): Observable<ReportDataChangeEvent[]> {
Expand All @@ -123,48 +107,64 @@ export class ReportChangesService {
continue;
}

const reportChangeEventObservable = this.createReportCalculation
.startReportCalculation(changeDetector.report)
.pipe(
switchMap((outcome) => {
if (outcome instanceof CreateReportCalculationFailed) {
// TODO: what do we do here in case of failure?
throw new Error('Report calculation failed');
}

return this.createReportCalculation.getCompletedReportCalculation(
new Reference(outcome.result.id),
);
}),
filter(
(calcUpdate) =>
(calcUpdate.outcome as ReportCalculationOutcomeSuccess)
?.result_hash !== changeDetector.lastCalculationHash,
),
tap(
(calcUpdate) =>
(changeDetector.lastCalculationHash = (
calcUpdate.outcome as ReportCalculationOutcomeSuccess
)?.result_hash),
),
map(
(result) =>
({
report: result.report,
calculation: result,
} as ReportDataChangeEvent),
),
);
const reportChangeEventObservable = this.calculateNewReportData(
changeDetector,
docChange,
);

affectedReports.push(reportChangeEventObservable);
}

return zip(affectedReports);
}
}

export interface DocChangeDetails {
change: DatabaseChangeResult;
previous: EntityDoc;
new: EntityDoc;
private calculateNewReportData(
changeDetector: ReportChangeDetector,
docChange: DocChangeDetails,
) {
return this.createReportCalculation
.startReportCalculation(changeDetector.report)
.pipe(
switchMap((outcome) => {
if (outcome instanceof CreateReportCalculationFailed) {
const err = new Error('Report calculation failed');
console.error(err);
// TODO: what do we do here in case of failure?
throw err;
}

return this.createReportCalculation.getCompletedReportCalculation(
new Reference(outcome.result.id),
);
}),
filter((calcUpdate) => {
if (
(calcUpdate.outcome as ReportCalculationOutcomeSuccess)
?.result_hash !== changeDetector.lastCalculationHash
) {
return true;
} else {
console.log(
'Report calculation did not change from doc',
changeDetector.report,
docChange,
);
return false;
}
}),
tap(
(calcUpdate) =>
(changeDetector.lastCalculationHash = (
calcUpdate.outcome as ReportCalculationOutcomeSuccess
)?.result_hash),
),
map(
(result) =>
({
report: result.report,
calculation: result,
} as ReportDataChangeEvent),
),
);
}
}
Loading
Loading