Skip to content

Commit db04fdd

Browse files
authored
Release 3.39.1
2 parents d3f5dc5 + 68c4aae commit db04fdd

File tree

12 files changed

+154
-123
lines changed

12 files changed

+154
-123
lines changed

src/app/core/database/pouch-database.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717

1818
import { PouchDatabase } from "./pouch-database";
19+
import { fakeAsync, tick } from "@angular/core/testing";
20+
import PouchDB from "pouchdb-browser";
21+
import { HttpStatusCode } from "@angular/common/http";
1922

2023
describe("PouchDatabase tests", () => {
2124
let database: PouchDatabase;
@@ -284,4 +287,36 @@ describe("PouchDatabase tests", () => {
284287

285288
await expectAsync(database.isEmpty()).toBeResolvedTo(false);
286289
});
290+
291+
it("should try auto-login if fetch fails and fetch again", fakeAsync(() => {
292+
const mockAuthService = jasmine.createSpyObj(["login", "addAuthHeader"]);
293+
database = new PouchDatabase(mockAuthService);
294+
database.initRemoteDB("");
295+
296+
mockAuthService.login.and.resolveTo();
297+
// providing "valid" token on second call
298+
let calls = 0;
299+
mockAuthService.addAuthHeader.and.callFake((headers) => {
300+
headers.Authorization = calls % 2 === 1 ? "valid" : "invalid";
301+
});
302+
spyOn(PouchDB, "fetch").and.callFake(async (url, opts) => {
303+
calls++;
304+
if (opts.headers["Authorization"] === "valid") {
305+
return new Response('{ "_id": "foo" }', { status: HttpStatusCode.Ok });
306+
} else {
307+
return {
308+
status: HttpStatusCode.Unauthorized,
309+
ok: false,
310+
} as Response;
311+
}
312+
});
313+
314+
database.get("Entity:ABC");
315+
tick();
316+
tick();
317+
318+
expect(PouchDB.fetch).toHaveBeenCalledTimes(2);
319+
expect(mockAuthService.login).toHaveBeenCalled();
320+
expect(mockAuthService.addAuthHeader).toHaveBeenCalledTimes(2);
321+
}));
287322
});

src/app/core/database/pouch-database.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import { Logging } from "../logging/logging.service";
2020
import PouchDB from "pouchdb-browser";
2121
import memory from "pouchdb-adapter-memory";
2222
import { PerformanceAnalysisLogging } from "../../utils/performance-analysis-logging";
23-
import { Injectable } from "@angular/core";
23+
import { Injectable, Optional } from "@angular/core";
2424
import { firstValueFrom, Observable, Subject } from "rxjs";
2525
import { filter } from "rxjs/operators";
2626
import { HttpStatusCode } from "@angular/common/http";
2727
import { environment } from "../../../environments/environment";
28+
import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
2829

2930
/**
3031
* Wrapper for a PouchDB instance to decouple the code from
@@ -66,7 +67,7 @@ export class PouchDatabase extends Database {
6667
/**
6768
* Create a PouchDB database manager.
6869
*/
69-
constructor() {
70+
constructor(@Optional() private authService?: KeycloakAuthService) {
7071
super();
7172
}
7273

@@ -105,27 +106,55 @@ export class PouchDatabase extends Database {
105106
*/
106107
initRemoteDB(
107108
dbName = `${environment.DB_PROXY_PREFIX}/${environment.DB_NAME}`,
108-
fetch = this.defaultFetch,
109109
): PouchDatabase {
110110
const options = {
111111
adapter: "http",
112112
skip_setup: true,
113-
fetch,
113+
fetch: (url: string | Request, opts: RequestInit) =>
114+
this.defaultFetch(url, opts),
114115
};
115116
this.pouchDB = new PouchDB(dbName, options);
116117
this.databaseInitialized.complete();
117118
return this;
118119
}
119120

120-
private defaultFetch(url, opts: any) {
121+
private defaultFetch: Fetch = async (url: string | Request, opts: any) => {
121122
if (typeof url !== "string") {
122-
return;
123+
const err = new Error("PouchDatabase.fetch: url is not a string");
124+
err["details"] = url;
125+
throw err;
123126
}
124127

125128
const remoteUrl =
126129
environment.DB_PROXY_PREFIX + url.split(environment.DB_PROXY_PREFIX)[1];
127-
return PouchDB.fetch(remoteUrl, opts);
128-
}
130+
this.authService.addAuthHeader(opts.headers);
131+
132+
let result: Response;
133+
try {
134+
result = await PouchDB.fetch(remoteUrl, opts);
135+
} catch (err) {
136+
Logging.warn("Failed to fetch from DB", err);
137+
}
138+
139+
// retry login if request failed with unauthorized
140+
if (!result || result.status === HttpStatusCode.Unauthorized) {
141+
try {
142+
await this.authService.login();
143+
this.authService.addAuthHeader(opts.headers);
144+
result = await PouchDB.fetch(remoteUrl, opts);
145+
} catch (err) {
146+
Logging.warn("Failed to fetch from DB", err);
147+
}
148+
}
149+
150+
if (!result || result.status >= 500) {
151+
throw new DatabaseException({
152+
error: "Failed to fetch from DB",
153+
actualResponse: result,
154+
});
155+
}
156+
return result;
157+
};
129158

130159
async getPouchDBOnceReady(): Promise<PouchDB.Database> {
131160
await firstValueFrom(this.databaseInitialized, {
@@ -438,7 +467,10 @@ export class PouchDatabase extends Database {
438467
* This overwrites PouchDB's error class which only logs limited information
439468
*/
440469
class DatabaseException extends Error {
441-
constructor(error: PouchDB.Core.Error, entityId?: string) {
470+
constructor(
471+
error: PouchDB.Core.Error | { [key: string]: any },
472+
entityId?: string,
473+
) {
442474
super();
443475

444476
if (entityId) {

src/app/core/database/sync.service.spec.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { Database } from "./database";
66
import { LoginStateSubject, SyncStateSubject } from "../session/session-type";
77
import { LoginState } from "../session/session-states/login-state.enum";
88
import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
9-
import { HttpStatusCode } from "@angular/common/http";
10-
import PouchDB from "pouchdb-browser";
119
import { Subject } from "rxjs";
1210

1311
describe("SyncService", () => {
@@ -72,47 +70,6 @@ describe("SyncService", () => {
7270
stopPeriodicTimer();
7371
}));
7472

75-
it("should try auto-login if fetch fails and fetch again", fakeAsync(() => {
76-
// Make sync call pass
77-
spyOn(
78-
TestBed.inject(Database) as PouchDatabase,
79-
"getPouchDB",
80-
).and.returnValues({ sync: () => Promise.resolve() } as any);
81-
spyOn(PouchDB, "fetch").and.returnValues(
82-
Promise.resolve({
83-
status: HttpStatusCode.Unauthorized,
84-
ok: false,
85-
} as Response),
86-
Promise.resolve({ status: HttpStatusCode.Ok, ok: true } as Response),
87-
);
88-
// providing "valid" token on second call
89-
let calls = 0;
90-
mockAuthService.addAuthHeader.and.callFake((headers) => {
91-
headers.Authorization = calls++ === 1 ? "valid" : "invalid";
92-
});
93-
mockAuthService.login.and.resolveTo();
94-
const initSpy = spyOn(service["remoteDatabase"], "initRemoteDB");
95-
service.startSync();
96-
tick();
97-
// taking fetch function from init call
98-
const fetch = initSpy.calls.mostRecent().args[1];
99-
100-
const url = "/db/_changes";
101-
const opts = { headers: {} };
102-
let fetchResult;
103-
fetch(url, opts).then((res) => (fetchResult = res));
104-
tick();
105-
expect(fetchResult).toBeDefined();
106-
107-
expect(PouchDB.fetch).toHaveBeenCalledTimes(2);
108-
expect(PouchDB.fetch).toHaveBeenCalledWith(url, opts);
109-
expect(opts.headers).toEqual({ Authorization: "valid" });
110-
expect(mockAuthService.login).toHaveBeenCalled();
111-
expect(mockAuthService.addAuthHeader).toHaveBeenCalledTimes(2);
112-
113-
stopPeriodicTimer();
114-
}));
115-
11673
it("should sync immediately when local db has changes", fakeAsync(() => {
11774
const mockLocalDb = jasmine.createSpyObj(["sync"]);
11875
const db = TestBed.inject(Database) as PouchDatabase;

src/app/core/database/sync.service.ts

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { Injectable } from "@angular/core";
22
import { Database } from "./database";
33
import { PouchDatabase } from "./pouch-database";
44
import { Logging } from "../logging/logging.service";
5-
import { HttpStatusCode } from "@angular/common/http";
6-
import PouchDB from "pouchdb-browser";
75
import { SyncState } from "../session/session-states/sync-state.enum";
86
import { SyncStateSubject } from "../session/session-type";
97
import {
@@ -30,7 +28,7 @@ export class SyncService {
3028
private readonly POUCHDB_SYNC_BATCH_SIZE = 500;
3129
static readonly SYNC_INTERVAL = 30000;
3230

33-
private remoteDatabase = new PouchDatabase();
31+
private remoteDatabase: PouchDatabase;
3432
private remoteDB: PouchDB.Database;
3533
private localDB: PouchDB.Database;
3634

@@ -39,6 +37,8 @@ export class SyncService {
3937
private authService: KeycloakAuthService,
4038
private syncStateSubject: SyncStateSubject,
4139
) {
40+
this.remoteDatabase = new PouchDatabase(this.authService);
41+
4242
this.logSyncContext();
4343

4444
this.syncStateSubject
@@ -78,45 +78,13 @@ export class SyncService {
7878
private initDatabases() {
7979
this.remoteDatabase.initRemoteDB(
8080
`${environment.DB_PROXY_PREFIX}/${environment.DB_NAME}`,
81-
this.fetch.bind(this),
8281
);
8382
this.remoteDB = this.remoteDatabase.getPouchDB();
8483
if (this.database instanceof PouchDatabase) {
8584
this.localDB = this.database.getPouchDB();
8685
}
8786
}
8887

89-
private async fetch(url: string, opts: any) {
90-
// TODO: merge this with PouchDatabase.defaultFetch, which is very similar
91-
92-
if (typeof url !== "string") {
93-
return;
94-
}
95-
96-
const remoteUrl =
97-
environment.DB_PROXY_PREFIX + url.split(environment.DB_PROXY_PREFIX)[1];
98-
const initialRes = await this.sendRequest(remoteUrl, opts);
99-
100-
// retry login if request failed with unauthorized
101-
if (initialRes.status === HttpStatusCode.Unauthorized) {
102-
return (
103-
this.authService
104-
.login()
105-
.then(() => this.sendRequest(remoteUrl, opts))
106-
// return initial response if request failed again
107-
.then((newRes) => (newRes.ok ? newRes : initialRes))
108-
.catch(() => initialRes)
109-
);
110-
} else {
111-
return initialRes;
112-
}
113-
}
114-
115-
private sendRequest(url: string, opts) {
116-
this.authService.addAuthHeader(opts.headers);
117-
return PouchDB.fetch(url, opts);
118-
}
119-
12088
/**
12189
* Execute a (one-time) sync between the local and server database.
12290
*/

src/app/features/location/geo.service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ export class GeoService {
6969
private reformatDisplayName(result: OpenStreetMapsSearchResult): GeoResult {
7070
const addr = result?.address;
7171
if (addr) {
72+
const city = addr.city ?? addr.town;
73+
7274
result.display_name = [
7375
addr.amenity ?? addr.office,
7476
addr.road ? addr.road + " " + addr.house_number : undefined,
75-
addr.postcode ? addr.postcode + " " + addr.city : addr.city,
77+
addr.postcode ? addr.postcode + " " + city : city,
7678
]
7779
.filter((x) => !!x)
7880
.join(", ");
@@ -110,6 +112,7 @@ type OpenStreetMapsSearchResult = GeoResult & {
110112
suburb?: string;
111113
borough?: string;
112114
city?: string;
115+
town?: string;
113116
postcode?: number;
114117
country?: string;
115118
country_code?: string;

src/app/features/location/map/map.component.spec.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MatDialog } from "@angular/material/dialog";
1515
import { MapPopupConfig } from "../map-popup/map-popup.component";
1616
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
1717
import { EMPTY, of, Subject } from "rxjs";
18+
import { GeoLocation } from "../location.datatype";
1819

1920
describe("MapComponent", () => {
2021
let component: MapComponent;
@@ -23,6 +24,11 @@ describe("MapComponent", () => {
2324
const config: MapConfig = { start: [52, 13] };
2425
let map: L.Map;
2526

27+
const TEST_LOCATION: GeoLocation = {
28+
locationString: "test address",
29+
geoLookup: { lat: 1, lon: 1, display_name: "test address" },
30+
};
31+
2632
beforeEach(async () => {
2733
mockDialog = jasmine.createSpyObj(["open"]);
2834
mockDialog.open.and.returnValue({ afterClosed: () => EMPTY } as any);
@@ -78,7 +84,7 @@ describe("MapComponent", () => {
7884
it("should create markers for entities and emit entity when marker is clicked", (done) => {
7985
Child.schema.set("address", { dataType: "location" });
8086
const child = new Child();
81-
child["address"] = { lat: 1, lon: 1 };
87+
child["address"] = TEST_LOCATION;
8288
component.entities = [child];
8389

8490
const marker = getEntityMarkers()[0];
@@ -111,8 +117,10 @@ describe("MapComponent", () => {
111117
Child.schema.set("address", { dataType: "location" });
112118
Child.schema.set("otherAddress", { dataType: "location" });
113119
const child = new Child();
114-
child["address"] = { lat: 1, lon: 1 };
115-
child["otherAddress"] = { lat: 1, lon: 2 };
120+
child["address"] = TEST_LOCATION;
121+
child["otherAddress"] = {
122+
geoLookup: { lon: 99, lat: 99, display_name: "other address" },
123+
} as GeoLocation;
116124

117125
component.entities = [child];
118126

src/app/features/location/map/map.component.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
LocationProperties,
2525
MapPropertiesPopupComponent,
2626
} from "./map-properties-popup/map-properties-popup.component";
27+
import { GeoResult } from "../geo.service";
2728

2829
@Component({
2930
selector: "app-map",
@@ -158,9 +159,10 @@ export class MapComponent implements AfterViewInit {
158159
.filter((entity) => !!entity)
159160
.forEach((entity) => {
160161
this.getMapProperties(entity)
161-
.filter((prop) => !!entity[prop])
162-
.forEach((prop) => {
163-
const marker = L.marker([entity[prop].lat, entity[prop].lon]);
162+
.map((prop) => entity[prop]?.geoLookup)
163+
.filter((loc: GeoResult) => !!loc)
164+
.forEach((loc: GeoResult) => {
165+
const marker = L.marker([loc.lat, loc.lon]);
164166
marker.bindTooltip(entity.toString());
165167
marker.on("click", () => this.entityClick.emit(entity));
166168
marker["entity"] = entity;

src/app/features/location/view-distance/view-distance.component.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ViewDistanceComponent } from "./view-distance.component";
33
import { Child } from "../../../child-dev-project/children/model/child";
44
import { Subject } from "rxjs";
55
import { Coordinates } from "../coordinates";
6+
import { GeoLocation } from "../location.datatype";
67

78
describe("ViewDistanceComponent", () => {
89
let component: ViewDistanceComponent;
@@ -17,7 +18,7 @@ describe("ViewDistanceComponent", () => {
1718
fixture = TestBed.createComponent(ViewDistanceComponent);
1819

1920
entity = new Child();
20-
entity["address"] = { lat: 52, lon: 13 };
21+
entity["address"] = { geoLookup: { lat: 52, lon: 13 } } as GeoLocation;
2122
compareCoordinates = new Subject();
2223
component = fixture.componentInstance;
2324
component.id = "distance";
@@ -58,7 +59,7 @@ describe("ViewDistanceComponent", () => {
5859
});
5960

6061
it("should display the shortest distance", () => {
61-
entity["otherAddress"] = { lat: 52, lon: 14 };
62+
entity["otherAddress"] = { geoLookup: { lat: 52, lon: 14 } } as GeoLocation;
6263
const c1 = { lat: 53, lon: 14 };
6364
const c2 = { lat: 52.0001, lon: 14 };
6465
compareCoordinates.next([c1, c2]);

0 commit comments

Comments
 (0)