From ec528c6323afe10364f626fdd57daf9e0da2ddff Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Wed, 12 Feb 2025 09:44:00 +0100 Subject: [PATCH 1/4] use Entity.DATABASE instead of environment database name --- src/app/core/database/database-resolver.service.ts | 3 +-- src/app/core/demo-data/demo-data-initializer.service.spec.ts | 5 +++-- src/app/core/demo-data/demo-data-initializer.service.ts | 3 ++- .../session/session-service/session-manager.service.spec.ts | 2 +- .../core/user/user-security/user-security.component.spec.ts | 2 +- src/app/core/user/user-security/user-security.component.ts | 4 +++- src/app/features/file/couchdb-file.service.spec.ts | 2 +- src/app/features/file/couchdb-file.service.ts | 2 +- src/environments/environment.prod.ts | 1 - src/environments/environment.spec.ts | 1 - src/environments/environment.ts | 3 --- 11 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/app/core/database/database-resolver.service.ts b/src/app/core/database/database-resolver.service.ts index 35b517db07..ab09a03923 100644 --- a/src/app/core/database/database-resolver.service.ts +++ b/src/app/core/database/database-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from "@angular/core"; import { Database, DatabaseDocChange } from "./database"; import { SessionInfo } from "../session/auth/session-info"; -import { environment } from "../../../environments/environment"; import { DatabaseFactoryService } from "./database-factory.service"; import { Entity } from "../entity/model/entity"; import { Observable, Subject } from "rxjs"; @@ -69,7 +68,7 @@ export class DatabaseResolverService { } private initializeAppDatabaseForCurrentUser(user: SessionInfo) { - const userDBName = `${user.name}-${environment.DB_NAME}`; + const userDBName = `${user.name}-${Entity.DATABASE}`; this.getDatabase(Entity.DATABASE).init(userDBName); } diff --git a/src/app/core/demo-data/demo-data-initializer.service.spec.ts b/src/app/core/demo-data/demo-data-initializer.service.spec.ts index ef96419701..4b4d036f30 100644 --- a/src/app/core/demo-data/demo-data-initializer.service.spec.ts +++ b/src/app/core/demo-data/demo-data-initializer.service.spec.ts @@ -14,6 +14,7 @@ import { PouchDatabase } from "../database/pouchdb/pouch-database"; import { LoginState } from "../session/session-states/login-state.enum"; import { DatabaseResolverService } from "../database/database-resolver.service"; import { MemoryPouchDatabase } from "../database/pouchdb/memory-pouch-database"; +import { Entity } from "../entity/model/entity"; describe("DemoDataInitializerService", () => { const normalUser: SessionInfo = { @@ -40,8 +41,8 @@ describe("DemoDataInitializerService", () => { beforeEach(() => { environment.session_type = SessionType.mock; - demoUserDBName = `${DemoUserGeneratorService.DEFAULT_USERNAME}-${environment.DB_NAME}`; - adminDBName = `${DemoUserGeneratorService.ADMIN_USERNAME}-${environment.DB_NAME}`; + demoUserDBName = `${DemoUserGeneratorService.DEFAULT_USERNAME}-${Entity.DATABASE}`; + adminDBName = `${DemoUserGeneratorService.ADMIN_USERNAME}-${Entity.DATABASE}`; mockDemoDataService = jasmine.createSpyObj(["publishDemoData"]); mockDemoDataService.publishDemoData.and.resolveTo(); mockDialog = jasmine.createSpyObj(["open"]); diff --git a/src/app/core/demo-data/demo-data-initializer.service.ts b/src/app/core/demo-data/demo-data-initializer.service.ts index 410c37aeeb..af7a6b6c2b 100644 --- a/src/app/core/demo-data/demo-data-initializer.service.ts +++ b/src/app/core/demo-data/demo-data-initializer.service.ts @@ -13,6 +13,7 @@ import { LoginStateSubject, SessionType } from "../session/session-type"; import memory from "pouchdb-adapter-memory"; import PouchDB from "pouchdb-browser"; import { DatabaseResolverService } from "../database/database-resolver.service"; +import { Entity } from "../entity/model/entity"; /** * This service handles everything related to the demo-mode @@ -79,7 +80,7 @@ export class DemoDataInitializerService { } private async syncWithDemoUserDB() { - const dbName = `${DemoUserGeneratorService.DEFAULT_USERNAME}-${environment.DB_NAME}`; + const dbName = `${DemoUserGeneratorService.DEFAULT_USERNAME}-${Entity.DATABASE}`; let demoUserDB: PouchDB.Database; if (environment.session_type === SessionType.mock) { PouchDB.plugin(memory); diff --git a/src/app/core/session/session-service/session-manager.service.spec.ts b/src/app/core/session/session-service/session-manager.service.spec.ts index c2efc98734..45c114a98f 100644 --- a/src/app/core/session/session-service/session-manager.service.spec.ts +++ b/src/app/core/session/session-service/session-manager.service.spec.ts @@ -30,7 +30,7 @@ describe("SessionManagerService", () => { let mockKeycloak: jasmine.SpyObj; let mockNavigator: { onLine: boolean }; let dbUser: SessionInfo; - const userDBName = `${TEST_USER}-${environment.DB_NAME}`; + const userDBName = `${TEST_USER}-app`; let mockDatabaseResolver: jasmine.SpyObj; beforeEach(waitForAsync(() => { diff --git a/src/app/core/user/user-security/user-security.component.spec.ts b/src/app/core/user/user-security/user-security.component.spec.ts index 55d97e08bf..757502864a 100644 --- a/src/app/core/user/user-security/user-security.component.spec.ts +++ b/src/app/core/user/user-security/user-security.component.spec.ts @@ -166,7 +166,7 @@ describe("UserSecurityComponent", () => { tick(); expect(mockHttp.post).toHaveBeenCalledWith( - `${environment.DB_PROXY_PREFIX}/${environment.DB_NAME}/clear_local`, + `${environment.DB_PROXY_PREFIX}/${Entity.DATABASE}/clear_local`, undefined, ); flush(); diff --git a/src/app/core/user/user-security/user-security.component.ts b/src/app/core/user/user-security/user-security.component.ts index c1d9fa1e03..cef95260d8 100644 --- a/src/app/core/user/user-security/user-security.component.ts +++ b/src/app/core/user/user-security/user-security.component.ts @@ -215,9 +215,11 @@ export class UserSecurityComponent implements OnInit { } private triggerSyncReset() { + // TODO: does this need to be triggered for other CouchDBs as well? + this.http .post( - `${environment.DB_PROXY_PREFIX}/${environment.DB_NAME}/clear_local`, + `${environment.DB_PROXY_PREFIX}/${Entity.DATABASE}/clear_local`, undefined, ) .subscribe({ diff --git a/src/app/features/file/couchdb-file.service.spec.ts b/src/app/features/file/couchdb-file.service.spec.ts index cd7d08e3db..7ef10739a6 100644 --- a/src/app/features/file/couchdb-file.service.spec.ts +++ b/src/app/features/file/couchdb-file.service.spec.ts @@ -44,7 +44,7 @@ describe("CouchdbFileService", () => { let mockSnackbar: jasmine.SpyObj; let dismiss: jasmine.Spy; let updates: Subject>; - const attachmentUrlPrefix = `${environment.DB_PROXY_PREFIX}/${environment.DB_NAME}-attachments`; + const attachmentUrlPrefix = `${environment.DB_PROXY_PREFIX}/${Entity.DATABASE}-attachments`; let mockNavigator; beforeEach(() => { diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index 96210263e5..0a5b6905b2 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -31,7 +31,7 @@ import { SyncedPouchDatabase } from "app/core/database/pouchdb/synced-pouch-data */ @Injectable() export class CouchdbFileService extends FileService { - private attachmentsUrl = `${environment.DB_PROXY_PREFIX}/${environment.DB_NAME}-attachments`; + private attachmentsUrl = `${environment.DB_PROXY_PREFIX}/${Entity.DATABASE}-attachments`; // TODO it seems like failed requests are executed again when a new one is done private requestQueue = new ObservableQueue(); private cache: { [key: string]: Observable } = {}; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index c9ef6f22ad..cc7eb90016 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -14,7 +14,6 @@ export const environment = { account_url: "https://keycloak.aam-digital.net", email: undefined, DB_PROXY_PREFIX: "/db", - DB_NAME: "app", enableNotificationModule: true, }; diff --git a/src/environments/environment.spec.ts b/src/environments/environment.spec.ts index 3088a944c3..36d0af1799 100644 --- a/src/environments/environment.spec.ts +++ b/src/environments/environment.spec.ts @@ -13,7 +13,6 @@ export const environment = { account_url: "https://accounts.aam-digital.net", email: undefined, DB_PROXY_PREFIX: "/db", - DB_NAME: "app", enableNotificationModule: false, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index ea7d2fe9d9..995437938e 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -40,8 +40,5 @@ export const environment = { /** Path for the reverse proxy that forwards to the database - configured in `proxy.conf.json` and `default.conf` */ DB_PROXY_PREFIX: "/db", - /** Name of the database that is used */ - DB_NAME: "app", - enableNotificationModule: true, }; From df77e30d917269e177b524bf0bcf5b231c9d1f4e Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Wed, 12 Feb 2025 09:44:13 +0100 Subject: [PATCH 2/4] auto-create DB stubs --- src/app/core/database/database-resolver.service.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/app/core/database/database-resolver.service.ts b/src/app/core/database/database-resolver.service.ts index ab09a03923..1bb2a2ab05 100644 --- a/src/app/core/database/database-resolver.service.ts +++ b/src/app/core/database/database-resolver.service.ts @@ -27,16 +27,6 @@ export class DatabaseResolverService { constructor(private databaseFactory: DatabaseFactoryService) { this._changesFeed = new Subject(); - this.initDatabaseStubs(); - } - - /** - * Generate Database objects so that change subscriptions and other operations - * can already be performed during bootstrap. - * @private - */ - private initDatabaseStubs() { - this.registerDatabase(Entity.DATABASE); } private registerDatabase(dbName: string) { @@ -46,6 +36,10 @@ export class DatabaseResolverService { } getDatabase(dbName: string = Entity.DATABASE): Database { + if (!this.databases.has(dbName)) { + this.registerDatabase(dbName); + } + let db = this.databases.get(dbName); return db; } From bbb3a2635dcda9301006ab2ce770677d6a65bd78 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Wed, 12 Feb 2025 12:23:14 +0100 Subject: [PATCH 3/4] switch to separate notifications CouchDB for NotificationEvent entities --- .../database/database-resolver.service.ts | 15 ++++++++++- .../core/database/pouchdb/pouch-database.ts | 5 +++- .../database/pouchdb/synced-pouch-database.ts | 27 ++++++++++++------- .../notification/model/notification-event.ts | 3 +++ 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/app/core/database/database-resolver.service.ts b/src/app/core/database/database-resolver.service.ts index 1bb2a2ab05..1b123b44be 100644 --- a/src/app/core/database/database-resolver.service.ts +++ b/src/app/core/database/database-resolver.service.ts @@ -4,6 +4,8 @@ import { SessionInfo } from "../session/auth/session-info"; import { DatabaseFactoryService } from "./database-factory.service"; import { Entity } from "../entity/model/entity"; import { Observable, Subject } from "rxjs"; +import { NotificationEvent } from "app/features/notification/model/notification-event"; +import { SyncedPouchDatabase } from "./pouchdb/synced-pouch-database"; /** * Manages access to individual databases, @@ -58,7 +60,7 @@ export class DatabaseResolverService { async initDatabasesForSession(session: SessionInfo) { this.initializeAppDatabaseForCurrentUser(session); - // ... in future initialize additional DBs here + this.initializeNotificationsDatabaseForCurrentUser(session); } private initializeAppDatabaseForCurrentUser(user: SessionInfo) { @@ -66,6 +68,17 @@ export class DatabaseResolverService { this.getDatabase(Entity.DATABASE).init(userDBName); } + private initializeNotificationsDatabaseForCurrentUser(user: SessionInfo) { + const db = this.getDatabase(NotificationEvent.DATABASE); + const serverDbName = `${NotificationEvent.DATABASE}_${user.id}`; + const browserDbName = serverDbName; + if (db instanceof SyncedPouchDatabase) { + db.init(browserDbName, serverDbName); + } else { + db.init(browserDbName); + } + } + initDatabasesForAnonymous() { if (!this.getDatabase(Entity.DATABASE).isInitialized()) { this.getDatabase(Entity.DATABASE).init(null); diff --git a/src/app/core/database/pouchdb/pouch-database.ts b/src/app/core/database/pouchdb/pouch-database.ts index 28ab35bc98..c45c35ff3e 100644 --- a/src/app/core/database/pouchdb/pouch-database.ts +++ b/src/app/core/database/pouchdb/pouch-database.ts @@ -40,7 +40,10 @@ export class PouchDatabase extends Database { * @param dbName the name for the database under which the IndexedDB entries will be created * @param options PouchDB options which are directly passed to the constructor */ - init(dbName?: string, options?: PouchDB.Configuration.DatabaseConfiguration) { + init( + dbName?: string, + options?: PouchDB.Configuration.DatabaseConfiguration | any, + ) { this.pouchDB = new PouchDB(dbName ?? this.dbName, options); this.databaseInitialized.complete(); } diff --git a/src/app/core/database/pouchdb/synced-pouch-database.ts b/src/app/core/database/pouchdb/synced-pouch-database.ts index 046c321c58..1d6b520fc0 100644 --- a/src/app/core/database/pouchdb/synced-pouch-database.ts +++ b/src/app/core/database/pouchdb/synced-pouch-database.ts @@ -36,6 +36,7 @@ export class SyncedPouchDatabase extends PouchDatabase { SYNC_INTERVAL = 30000; private remoteDatabase: RemotePouchDatabase; + private syncState: SyncStateSubject = new SyncStateSubject(); /** trigger to unsubscribe any internal subscriptions */ private destroy$ = new Subject(); @@ -43,7 +44,7 @@ export class SyncedPouchDatabase extends PouchDatabase { constructor( dbName: string, authService: KeycloakAuthService, - private syncStateSubject: SyncStateSubject, + private globalSyncState: SyncStateSubject, private navigator: Navigator, private loginStateSubject: LoginStateSubject, ) { @@ -52,7 +53,7 @@ export class SyncedPouchDatabase extends PouchDatabase { this.remoteDatabase = new RemotePouchDatabase(dbName, authService); this.logSyncContext(); - this.syncStateSubject + this.syncState .pipe( takeUntil(this.destroy$), filter((state) => state === SyncState.COMPLETED), @@ -63,6 +64,11 @@ export class SyncedPouchDatabase extends PouchDatabase { this.logSyncContext(); }); + // forward sync state to global sync state (combining state from all synced databases) + this.syncState + .pipe(takeUntil(this.destroy$)) + .subscribe((state: SyncState) => this.globalSyncState.next(state)); + this.loginStateSubject .pipe( takeUntil(this.destroy$), @@ -75,8 +81,9 @@ export class SyncedPouchDatabase extends PouchDatabase { * Initializes the PouchDB with local indexeddb as well as a remote server connection for syncing. * @param dbName local database name (for the current user); * if explicitly passed as `null`, a remote-only, anonymous session is initialized + * @param remoteDbName (optional) remote database name (if different from local browser database name) */ - override init(dbName?: string | null) { + override init(dbName?: string | null, remoteDbName?: string) { if (dbName === null) { this.remoteDatabase.init(); // use the remote database as internal database driver @@ -86,7 +93,7 @@ export class SyncedPouchDatabase extends PouchDatabase { super.init(dbName ?? this.dbName); // keep remote database on default name (e.g. "app" instead of "user_uuid-app") - this.remoteDatabase.init(); + this.remoteDatabase.init(remoteDbName); } } @@ -103,24 +110,26 @@ export class SyncedPouchDatabase extends PouchDatabase { sync(): Promise { if (!this.navigator.onLine) { Logging.debug("Not syncing because offline"); - this.syncStateSubject.next(SyncState.UNSYNCED); + this.syncState.next(SyncState.UNSYNCED); return Promise.resolve({}); } - this.syncStateSubject.next(SyncState.STARTED); + this.syncState.next(SyncState.STARTED); return this.getPouchDB() .sync(this.remoteDatabase.getPouchDB(), { batch_size: this.POUCHDB_SYNC_BATCH_SIZE, }) .then((res) => { + if (res) res["dbName"] = this.dbName; Logging.debug("sync completed", res); - this.syncStateSubject.next(SyncState.COMPLETED); + + this.syncState.next(SyncState.COMPLETED); return res as SyncResult; }) .catch((err) => { Logging.debug("sync error", err); - this.syncStateSubject.next(SyncState.FAILED); + this.syncState.next(SyncState.FAILED); throw err; }); } @@ -144,7 +153,7 @@ export class SyncedPouchDatabase extends PouchDatabase { .pipe( debounceTime(500), mergeMap(() => { - if (this.syncStateSubject.value == SyncState.STARTED) { + if (this.syncState.value == SyncState.STARTED) { return of(); } else { return from(this.sync()); diff --git a/src/app/features/notification/model/notification-event.ts b/src/app/features/notification/model/notification-event.ts index 41bae3c90b..9251323e9b 100644 --- a/src/app/features/notification/model/notification-event.ts +++ b/src/app/features/notification/model/notification-event.ts @@ -10,6 +10,9 @@ import { EntityNotificationContext } from "./entity-notification-context"; */ @DatabaseEntity("NotificationEvent") export class NotificationEvent extends Entity { + // notification events are stored in a separate, user-specific database + static override DATABASE = "notifications"; + /* * The title of the notification. */ From b271f3f5b7498c8f890f69ea3b33cb85527fe4c3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 28 Feb 2025 13:28:55 +0100 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Abhishek Negi <108608673+Abhinegi2@users.noreply.github.com> --- src/app/core/database/database-resolver.service.ts | 5 +++++ src/app/core/database/pouchdb/synced-pouch-database.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/core/database/database-resolver.service.ts b/src/app/core/database/database-resolver.service.ts index 1b123b44be..f7a6002578 100644 --- a/src/app/core/database/database-resolver.service.ts +++ b/src/app/core/database/database-resolver.service.ts @@ -58,6 +58,11 @@ export class DatabaseResolverService { } } + /** + * Connect the database(s) for the current user's "session", + * i.e. configuring the access for that account after login + * (especially for local and remote database modes) + */ async initDatabasesForSession(session: SessionInfo) { this.initializeAppDatabaseForCurrentUser(session); this.initializeNotificationsDatabaseForCurrentUser(session); diff --git a/src/app/core/database/pouchdb/synced-pouch-database.ts b/src/app/core/database/pouchdb/synced-pouch-database.ts index 1d6b520fc0..5c2883f611 100644 --- a/src/app/core/database/pouchdb/synced-pouch-database.ts +++ b/src/app/core/database/pouchdb/synced-pouch-database.ts @@ -121,9 +121,8 @@ export class SyncedPouchDatabase extends PouchDatabase { batch_size: this.POUCHDB_SYNC_BATCH_SIZE, }) .then((res) => { - if (res) res["dbName"] = this.dbName; + if (res) res["dbName"] = this.dbName; // add for debugging information Logging.debug("sync completed", res); - this.syncState.next(SyncState.COMPLETED); return res as SyncResult; })