src/app/core/entity/database-indexing/database-indexing.service.ts
Manage database query index creation and use, working as a facade in front of the Database service. This allows to track pending indexing processes and also show them to users in the UI.
Methods |
|
Accessors |
constructor(dbResolver: DatabaseResolverService, entitySchemaService: EntitySchemaService)
|
|||||||||
Parameters :
|
Async createIndex | |||||||||||||||
createIndex(designDoc: any, db: string)
|
|||||||||||||||
Register a new database query to be created/updated and indexed. This also triggers updates to the observable
Parameters :
Returns :
Promise<void>
|
generateIndexOnProperty | ||||||||||||||||||||
generateIndexOnProperty(indexId: string, entity: EntityConstructor<E>, referenceProperty: REF, secondaryIndex?: SEC)
|
||||||||||||||||||||
Type parameters :
|
||||||||||||||||||||
Generate and save a new database query index for the given entity type and property. This allows you to efficiently query documents of that entity type based on values of the reference property, e.g. query all Notes (entityType=Note) that are related to a certain user (referenceProperty="authors"). Query this index using the given indexId like this: generateIndexOnProperty("myIndex", Note, "category"); queryIndexDocs(Note, "myIndex/by_category")
Parameters :
Returns :
Promise<void>
|
Async queryIndexDocs | ||||||||||||||||||||
queryIndexDocs(entityConstructor: EntityConstructor<T>, indexName: string, options: QueryOptions | string)
|
||||||||||||||||||||
Type parameters :
|
||||||||||||||||||||
Load data from the Database through the given, previously created index.
Parameters :
Returns :
Promise<T[]>
|
Async queryIndexDocsRange | ||||||||||||||||||||
queryIndexDocsRange(entityConstructor: EntityConstructor<T>, indexName: string, startkey: string | any[], endkey?: string | any[])
|
||||||||||||||||||||
Type parameters :
|
||||||||||||||||||||
Load data from the Database through the given, previously created index for a key range.
Parameters :
Returns :
Promise<T[]>
|
Async queryIndexRaw | |||||||||||||||||||||||||
queryIndexRaw(indexName: string, options: QueryOptions, doNotWaitForIndexCreation?: boolean, db: string)
|
|||||||||||||||||||||||||
Run a query on the database. If the required index does not exist (yet) this blocks the request by default and only runs and returns once the index is available. Example :
Parameters :
Returns :
Promise<any>
|
queryIndexStats | ||||||||||||
queryIndexStats(indexName: string, options: QueryOptions)
|
||||||||||||
Parameters :
Returns :
Promise<any>
|
indicesRegistered |
getindicesRegistered()
|
All currently registered indices with their status
Returns :
Observable<BackgroundProcessState[]>
|
import { Injectable } from "@angular/core";
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.
* This allows to track pending indexing processes and also show them to users in the UI.
*/
@Injectable({
providedIn: "root",
})
export class DatabaseIndexingService {
private _indicesRegistered = new BehaviorSubject<BackgroundProcessState[]>(
[],
);
/** All currently registered indices with their status */
get indicesRegistered(): Observable<BackgroundProcessState[]> {
return this._indicesRegistered.asObservable();
}
constructor(
private dbResolver: DatabaseResolverService,
private entitySchemaService: EntitySchemaService,
) {}
/**
* Register a new database query to be created/updated and indexed.
*
* This also triggers updates to the observable `indicesRegistered`.
*
* @param designDoc The design document (see @link{Database}) describing the query/index.
*/
async createIndex(
designDoc: any,
db: string = Entity.DATABASE,
): Promise<void> {
const indexDetails = designDoc._id.replace(/_design\//, "");
const indexState: BackgroundProcessState = {
title: $localize`Preparing data (Indexing)`,
details: indexDetails,
pending: true,
};
const indexCreationPromise = this.dbResolver
.getDatabase(db)
.saveDatabaseIndex(designDoc);
this._indicesRegistered.next([
...this._indicesRegistered.value.filter(
(state) => state.details !== indexDetails,
),
indexState,
]);
try {
await indexCreationPromise;
} catch (err) {
indexState.pending = false;
indexState.error = err;
this._indicesRegistered.next(this._indicesRegistered.value);
throw err;
}
indexState.pending = false;
this._indicesRegistered.next(this._indicesRegistered.value);
}
/**
* Generate and save a new database query index for the given entity type and property.
*
* This allows you to efficiently query documents of that entity type based on values of the reference property,
* e.g. query all Notes (entityType=Note) that are related to a certain user (referenceProperty="authors").
*
* Query this index using the given indexId like this:
* generateIndexOnProperty("myIndex", Note, "category");
* queryIndexDocs(Note, "myIndex/by_category")
*
* @param indexId id to query this index after creation (--> {indexId}/by_{referenceProperty})
* @param entity entity type to limit the documents included in this index
* @param referenceProperty property key on the documents whose value is indexed as a query key
* @param secondaryIndex (optional) additional property to emit as a secondary index to narrow queries further
*/
generateIndexOnProperty<
E extends Entity,
REF extends keyof E & string,
SEC extends keyof E & string,
>(
indexId: string,
entity: EntityConstructor<E>,
referenceProperty: REF,
secondaryIndex?: SEC,
): Promise<void> {
const emitParamFormatter = (primaryParam) => {
if (secondaryIndex) {
return `emit([${primaryParam}, doc.${secondaryIndex}]);`;
} else {
return `emit(${primaryParam});`;
}
};
const simpleEmit = emitParamFormatter("doc." + referenceProperty);
const arrayEmit = `
if (!Array.isArray(doc.${referenceProperty})) return;
doc.${referenceProperty}.forEach((relatedEntity) => {
${emitParamFormatter("relatedEntity")}
});`;
const designDoc = {
_id: "_design/" + indexId,
views: {
[`by_${referenceProperty}`]: {
map: `(doc) => {
if (!doc._id.startsWith("${entity.ENTITY_TYPE}")) return;
${
entity.schema.get(referenceProperty).isArray
? arrayEmit
: simpleEmit
}
}`,
},
},
};
return this.createIndex(designDoc);
}
/**
* Load data from the Database through the given, previously created index.
* @param entityConstructor
* @param indexName The name of the previously created index to be queried.
* @param options (Optional) additional query options object or a simple value used as the exact key to retrieve
*/
async queryIndexDocs<T extends Entity>(
entityConstructor: EntityConstructor<T>,
indexName: string,
options: QueryOptions | string = {},
): Promise<T[]> {
if (typeof options === "string") {
options = { key: options };
}
options.include_docs = true;
const rawResults = await this.queryIndexRaw(indexName, options);
return rawResults.rows.map((loadedRecord) => {
const entity = new entityConstructor("");
this.entitySchemaService.loadDataIntoEntity(entity, loadedRecord.doc);
return entity;
});
}
/**
* Load data from the Database through the given, previously created index for a key range.
* @param entityConstructor
* @param indexName The name of the previously created index to be queried.
* @param startkey start id of range to query
* @param endkey end id of range to query (inclusive)
*/
async queryIndexDocsRange<T extends Entity>(
entityConstructor: EntityConstructor<T>,
indexName: string,
startkey: string | any[],
endkey?: string | any[],
): Promise<T[]> {
if (Array.isArray(endkey)) {
endkey = [...endkey, {}];
} else {
endkey = endkey + "\ufff0";
}
return this.queryIndexDocs(entityConstructor, indexName, {
startkey: startkey,
endkey: endkey,
});
}
queryIndexStats(
indexName: string,
options: QueryOptions = {
reduce: true,
group: true,
},
): Promise<any> {
return this.queryIndexRaw(indexName, options);
}
/**
* Run a query on the database.
* If the required index does not exist (yet) this blocks the request by default
* and only runs and returns once the index is available.
*
* @param indexName key of the database index to be used
* @param options additional options for the request
* @param doNotWaitForIndexCreation (Optional) flag to *not* block the query if the index doesn't exist yet.
* If no index exists this may result in an error (e.g. 404)
*/
async queryIndexRaw(
indexName: string,
options: QueryOptions,
doNotWaitForIndexCreation?: boolean,
db: string = Entity.DATABASE,
): Promise<any> {
if (!doNotWaitForIndexCreation) {
await this.waitForIndexAvailable(indexName);
}
return this.dbResolver.getDatabase(db).query(indexName, options);
}
/**
* If the index is not created yet, wait until it is ready to avoid 404 errors.
* Returns immediately if index is already created.
* @param indexName
* @private
*/
private async waitForIndexAvailable(indexName: string): Promise<void> {
function relevantIndexIsReady(processes, requiredIndexName) {
const relevantProcess = processes.find(
(process) =>
process.details === requiredIndexName ||
requiredIndexName.startsWith(process.details + "/"),
);
return relevantProcess && !relevantProcess.pending;
}
if (relevantIndexIsReady(this._indicesRegistered.value, indexName)) {
return;
}
await firstValueFrom(
this._indicesRegistered.pipe(
first((processes) => relevantIndexIsReady(processes, indexName)),
),
);
}
}