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

WIP: multiple database services #2758

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 5 additions & 5 deletions doc/compodoc_sources/concepts/overall-architecture.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Overall Structure

This is a rough sketch of the architecture of the core system under discussion. The modules and classes shown here are included in the ndb-core repository.
![](../../images/architecture_core.png)

An actual, specific software system to be used will be based on the core and extend it:
![](../../images/architecture_concrete-project.png)
This is a rough sketch of the architecture of the core system under discussion.
The modules and classes shown here are included in the ndb-core repository.
Note that some components and services are explicitly designed to be used and extended when developing feature modules,
while others will usually not need to be touched or understood when extending the system:
![](../../images/architecture_modules.png)

## Folder Structure

Expand Down
Binary file removed doc/images/architecture_concrete-project.png
Binary file not shown.
Binary file removed doc/images/architecture_core.png
Binary file not shown.
Binary file added doc/images/architecture_modules.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import { ConfigurableEnumModule } from "./core/basic-datatypes/configurable-enum
import { FaIconLibrary } from "@fortawesome/angular-fontawesome";
import { fas } from "@fortawesome/free-solid-svg-icons";
import { far } from "@fortawesome/free-regular-svg-icons";
import { DatabaseModule } from "./core/database/database.module";
import { Angulartics2Matomo, Angulartics2Module } from "angulartics2";
import {
DEFAULT_LANGUAGE,
Expand Down Expand Up @@ -111,7 +110,6 @@ import { SkillModule } from "./features/skill/skill.module";
// Core modules
CoreModule,
ConfigurableEnumModule,
DatabaseModule,
LanguageModule,
LatestChangesModule,
PermissionsModule,
Expand Down
7 changes: 4 additions & 3 deletions src/app/core/admin/admin-overview/admin-overview.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { MatSnackBar } from "@angular/material/snack-bar";
import { ConfigService } from "../../config/config.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { readFile } from "../../../utils/utils";
import { Database } from "../../database/database";
import { ExtendedAlertConfig } from "../../alerts/alert-config";
import { MatButtonModule } from "@angular/material/button";
import { RouterLink } from "@angular/router";
Expand All @@ -15,6 +14,7 @@ import { DownloadService } from "../../export/download-service/download.service"
import { MatListModule } from "@angular/material/list";
import { RouteTarget } from "../../../route-target";
import { AdminOverviewService } from "./admin-overview.service";
import { DatabaseResolverService } from "../../database/database-resolver.service";

/**
* Admin GUI giving administrative users different options/actions.
Expand All @@ -36,7 +36,7 @@ export class AdminOverviewComponent implements OnInit {
private alertService: AlertService,
private backupService: BackupService,
private downloadService: DownloadService,
private db: Database,
private dbResolver: DatabaseResolverService,
private confirmationDialog: ConfirmationDialogService,
private snackBar: MatSnackBar,
private configService: ConfigService,
Expand All @@ -51,7 +51,8 @@ export class AdminOverviewComponent implements OnInit {
* Send a reference of the PouchDB to the browser's developer console for real-time debugging.
*/
debugDatabase() {
console.log(this.db);
console.log(this.dbResolver);
console.log(this.dbResolver.getDatabase());
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/app/core/admin/backup/backup.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from "@angular/core";
import { Database } from "../../database/database";
import { Config } from "../../config/config";
import { DatabaseResolverService } from "../../database/database-resolver.service";

/**
* Create and load backups of the database.
Expand All @@ -9,7 +10,12 @@ import { Config } from "../../config/config";
providedIn: "root",
})
export class BackupService {
constructor(private db: Database) {}
private db: Database;

constructor(private dbResolver: DatabaseResolverService) {
this.db = this.dbResolver.getDatabase();
// TODO: backup other databases?
}

/**
* Creates an array holding all elements of the database.
Expand Down
30 changes: 30 additions & 0 deletions src/app/core/database/database-factory.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { inject, Injectable } from "@angular/core";
import { Database } from "./database";
import { PouchDatabase } from "./pouch-database";
import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
import { environment } from "../../../environments/environment";
import { SessionType } from "../session/session-type";

/**
* Provides a method to generate Database instances.
*
* (can be re-implemented to cater to different Database implementations).
*/
@Injectable({
providedIn: "root",
})
export class DatabaseFactoryService {
private authService = inject(KeycloakAuthService);

createDatabase(dbName: string): Database {
const db = new PouchDatabase(this.authService);

if (environment.session_type === SessionType.mock) {
db.initInMemoryDB(dbName);
} else {
db.initIndexedDB(dbName);
}

return db;
}
}
16 changes: 16 additions & 0 deletions src/app/core/database/database-resolver.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { DatabaseResolverService } from './database-resolver.service';

describe('DatabaseResolverService', () => {
let service: DatabaseResolverService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DatabaseResolverService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
38 changes: 38 additions & 0 deletions src/app/core/database/database-resolver.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { inject, Injectable } from "@angular/core";
import { Database } from "./database";
import { SessionInfo } from "../session/auth/session-info";
import { environment } from "../../../environments/environment";
import { DatabaseFactoryService } from "./database-factory.service";

/**
* Manages access to individual databases,
* as data may be stored across multiple different instances.
*/
@Injectable({
providedIn: "root",
})
export class DatabaseResolverService {
static readonly DEFAULT_DB = "app";

private databases: Map<string, Database> = new Map();
private databaseFactory = inject(DatabaseFactoryService);

getDatabase(dbName: string = DatabaseResolverService.DEFAULT_DB): Database {
return this.databases.get(dbName);
}

resetDatabases() {}

async initDatabasesForSession(session: SessionInfo) {
this.initializeAppDatabaseForCurrentUser(session);
// TODO: init other DBs
}

private initializeAppDatabaseForCurrentUser(user: SessionInfo) {
const userDBName = `${user.name}-${environment.DB_NAME}`;
const userDb = this.databaseFactory.createDatabase(userDBName);
this.databases.set(environment.DB_NAME, userDb);

// TODO: have removed fallback to old "app" IndexedDB database here; check sentry if this may cause larger impact
}
}
15 changes: 0 additions & 15 deletions src/app/core/database/database.module.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/app/core/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { Observable } from "rxjs";
/**
* An implementation of this abstract class provides functions for direct database access.
* This interface is an extension of the [PouchDB API](https://pouchdb.com/api.html).
*
* PLEASE NOTE:
* Direct access to the Database layer is rarely necessary, and you should probably use EntityMapperService instead.
* Database is not an Angular Service and has to be accessed through the DatabaseResolverService.
*/
export abstract class Database {
/**
Expand Down
8 changes: 5 additions & 3 deletions src/app/core/database/pouch-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { Logging } from "../logging/logging.service";
import PouchDB from "pouchdb-browser";
import memory from "pouchdb-adapter-memory";
import { PerformanceAnalysisLogging } from "../../utils/performance-analysis-logging";
import { Injectable, Optional } from "@angular/core";
import { firstValueFrom, Observable, Subject } from "rxjs";
import { filter } from "rxjs/operators";
import { HttpStatusCode } from "@angular/common/http";
Expand All @@ -34,7 +33,6 @@ import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.serv
* Additional convenience functions on top of the PouchDB API
* should be implemented in the abstract {@link Database}.
*/
@Injectable()
export class PouchDatabase extends Database {
/**
* Small helper function which creates a database with in-memory PouchDB initialized
Expand Down Expand Up @@ -66,9 +64,13 @@ export class PouchDatabase extends Database {

/**
* Create a PouchDB database manager.
* @param authService (Optional) the authentication service that may require additional auth headers for remote DB access
*/
constructor(@Optional() private authService?: KeycloakAuthService) {
constructor(private authService?: KeycloakAuthService) {
super();

// TODO: refactor to run the init method directly from constructor and remove databaseInitialized
// now that DatabaseResolver handles creation and provider we don't have to separate creation and initialization anymore
}

/**
Expand Down
18 changes: 6 additions & 12 deletions src/app/core/demo-data/demo-data-initializer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import { DemoDataGeneratingProgressDialogComponent } from "./demo-data-generatin
import { SessionManagerService } from "../session/session-service/session-manager.service";
import { LocalAuthService } from "../session/auth/local/local-auth.service";
import { SessionInfo, SessionSubject } from "../session/auth/session-info";
import { Logging } from "../logging/logging.service";
import { Database } from "../database/database";
import { PouchDatabase } from "../database/pouch-database";
import { environment } from "../../../environments/environment";
import { LoginState } from "../session/session-states/login-state.enum";
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";

/**
* This service handles everything related to the demo-mode
Expand All @@ -25,7 +24,6 @@ import PouchDB from "pouchdb-browser";
@Injectable()
export class DemoDataInitializerService {
private liveSyncHandle: PouchDB.Replication.Sync<any>;
private pouchDatabase: PouchDatabase;
private readonly normalUser: SessionInfo = {
name: DemoUserGeneratorService.DEFAULT_USERNAME,
id: DemoUserGeneratorService.DEFAULT_USERNAME,
Expand All @@ -38,12 +36,13 @@ export class DemoDataInitializerService {
roles: ["user_app", "admin_app"],
entityId: `User:${DemoUserGeneratorService.ADMIN_USERNAME}`,
};

constructor(
private demoDataService: DemoDataService,
private localAuthService: LocalAuthService,
private sessionManager: SessionManagerService,
private dialog: MatDialog,
private database: Database,
private dbResolver: DatabaseResolverService,
private loginState: LoginStateSubject,
private sessionInfo: SessionSubject,
) {}
Expand All @@ -52,13 +51,6 @@ export class DemoDataInitializerService {
const dialogRef = this.dialog.open(
DemoDataGeneratingProgressDialogComponent,
);
if (this.database instanceof PouchDatabase) {
this.pouchDatabase = this.database;
} else {
Logging.warn(
"Cannot create demo data with session: " + environment.session_type,
);
}

this.localAuthService.saveUser(this.normalUser);
this.localAuthService.saveUser(this.adminUser);
Expand Down Expand Up @@ -95,7 +87,9 @@ export class DemoDataInitializerService {
} else {
demoUserDB = new PouchDB(dbName);
}
const currentUserDB = this.pouchDatabase.getPouchDB();
const currentUserDB = (
this.dbResolver.getDatabase() as PouchDatabase
).getPouchDB();
await currentUserDB.sync(demoUserDB, { batch_size: 500 });
this.liveSyncHandle = currentUserDB.sync(demoUserDB, {
live: true,
Expand Down
6 changes: 3 additions & 3 deletions src/app/core/demo-data/demo-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "@angular/core";
import { DemoDataGenerator } from "./demo-data-generator";
import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service";
import { Database } from "../database/database";
import { DatabaseResolverService } from "../database/database-resolver.service";

/**
* General config object to pass all initially register DemoDataGenerators
Expand Down Expand Up @@ -58,7 +58,7 @@ export class DemoDataService {
private entityMapper: EntityMapperService,
private injector: Injector,
private config: DemoDataServiceConfig,
private database: Database,
private dbResolver: DatabaseResolverService,
) {}

private registerAllProvidedDemoDataGenerators() {
Expand All @@ -75,7 +75,7 @@ export class DemoDataService {
* and add all the generated entities to the Database.
*/
async publishDemoData() {
if (!(await this.database.isEmpty())) {
if (!(await this.dbResolver.getDatabase().isEmpty())) {
return;
}
this.registerAllProvidedDemoDataGenerators();
Expand Down
17 changes: 12 additions & 5 deletions src/app/core/entity/database-indexing/database-indexing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
*/

import { Injectable } from "@angular/core";
import { Database, QueryOptions } from "../../database/database";
import { QueryOptions } from "../../database/database";
import { BehaviorSubject, firstValueFrom, Observable } from "rxjs";
import { BackgroundProcessState } from "../../ui/sync-status/background-process-state.interface";
import { Entity, EntityConstructor } from "../model/entity";
import { EntitySchemaService } from "../schema/entity-schema.service";
import { first } from "rxjs/operators";
import { DatabaseResolverService } from "../../database/database-resolver.service";

/**
* Manage database query index creation and use, working as a facade in front of the Database service.
Expand All @@ -41,7 +42,7 @@ export class DatabaseIndexingService {
}

constructor(
private db: Database,
private dbResolver: DatabaseResolverService,
private entitySchemaService: EntitySchemaService,
) {}

Expand All @@ -52,15 +53,20 @@ export class DatabaseIndexingService {
*
* @param designDoc The design document (see @link{Database}) describing the query/index.
*/
async createIndex(designDoc: any): Promise<void> {
async createIndex(
designDoc: any,
db: string = DatabaseResolverService.DEFAULT_DB,
): Promise<void> {
const indexDetails = designDoc._id.replace(/_design\//, "");
const indexState: BackgroundProcessState = {
title: $localize`Preparing data (Indexing)`,
details: indexDetails,
pending: true,
};

const indexCreationPromise = this.db.saveDatabaseIndex(designDoc);
const indexCreationPromise = this.dbResolver
.getDatabase(db)
.saveDatabaseIndex(designDoc);
this._indicesRegistered.next([
...this._indicesRegistered.value.filter(
(state) => state.details !== indexDetails,
Expand Down Expand Up @@ -213,12 +219,13 @@ export class DatabaseIndexingService {
indexName: string,
options: QueryOptions,
doNotWaitForIndexCreation?: boolean,
db: string = DatabaseResolverService.DEFAULT_DB,
): Promise<any> {
if (!doNotWaitForIndexCreation) {
await this.waitForIndexAvailable(indexName);
}

return this.db.query(indexName, options);
return this.dbResolver.getDatabase(db).query(indexName, options);
}

/**
Expand Down
Loading
Loading