common-ui/src/lib/listing/services/entities.service.ts
2022-01-13 00:19:52 +02:00

119 lines
4.1 KiB
TypeScript

import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, map, startWith, tap } from 'rxjs/operators';
import { IListable } from '../models';
import { GenericService, QueryParam } from '../../services';
import { getLength, List, mapEach, shareDistinctLast, shareLast } from '../../utils';
/**
* This should be removed when refactoring is done
*/
const ENTITY_PATH = new InjectionToken<string>('This is here for compatibility while refactoring things.');
const ENTITY_CLASS = new InjectionToken<string>('This is here for compatibility while refactoring things.');
@Injectable()
/**
* E for Entity
* I for Interface
* By default, if no interface is provided, I = E
*/
export class EntitiesService<E extends IListable, I = E> extends GenericService<I> {
readonly noData$: Observable<boolean>;
readonly all$: Observable<E[]>;
readonly allLength$: Observable<number>;
protected readonly _entityChanged$ = new Subject<E>();
protected readonly _entityDeleted$ = new Subject<E>();
private readonly _all$ = new BehaviorSubject<E[]>([]);
constructor(
protected readonly _injector: Injector,
@Optional() @Inject(ENTITY_CLASS) private readonly _entityClass: new (entityInterface: I, ...args: unknown[]) => E,
@Optional() @Inject(ENTITY_PATH) protected readonly _defaultModelPath = '',
) {
super(_injector, _defaultModelPath);
this.all$ = this._all$.asObservable().pipe(shareDistinctLast());
this.allLength$ = this._all$.pipe(getLength, shareDistinctLast());
this.noData$ = this._noData$;
}
get all(): E[] {
return Object.values(this._all$.getValue());
}
private get _noData$(): Observable<boolean> {
return this.allLength$.pipe(
map(length => length === 0),
shareDistinctLast(),
);
}
loadAll(...args: unknown[]): Observable<E[]>;
loadAll(modelPath = this._defaultModelPath, queryParams?: List<QueryParam>): Observable<E[]> {
return this.getAll(modelPath, queryParams).pipe(
mapEach(entity => new this._entityClass(entity)),
tap((entities: E[]) => this.setEntities(entities)),
);
}
loadAllIfEmpty(modelPath = this._defaultModelPath, queryParams?: List<QueryParam>): Promise<unknown> | void {
if (!this.all.length) {
return this.loadAll(modelPath, queryParams).toPromise();
}
}
getEntityChanged$(entityId: string): Observable<E | undefined> {
return this._entityChanged$.pipe(
filter(entity => entity.id === entityId),
startWith(this.find(entityId)),
shareLast(),
);
}
getEntityDeleted$(entityId: string): Observable<E | undefined> {
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
setEntities(entities: E[]): void {
const changedEntities: E[] = [];
const deletedEntities = this.all.filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
const newEntities = entities.map(entity => {
const oldEntity = this.find(entity.id);
if (oldEntity && JSON.stringify(oldEntity) === JSON.stringify(entity)) {
return oldEntity;
}
changedEntities.push(entity);
return entity;
});
this._all$.next(newEntities);
// Emit observables only after entities have been updated
for (const entity of changedEntities) {
this._entityChanged$.next(entity);
}
for (const entity of deletedEntities) {
this._entityDeleted$.next(entity);
}
}
find(id: string): E | undefined {
return this.all.find(entity => entity.id === id);
}
has(id: string): boolean {
return this.all.some(entity => entity.id === id);
}
replace(entity: E): void {
const all = this.all.filter(item => item.id !== entity.id);
this.setEntities([...all, entity]);
this._entityChanged$.next(entity);
}
}