common-ui/src/lib/listing/services/entities.service.ts
2022-07-22 10:52:26 +03:00

114 lines
3.8 KiB
TypeScript

import { Injectable } 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';
import { Id } from '../models/trackable';
@Injectable()
/**
* E for Entity,
* I for Interface.
* By default, if no interface is provided, I = E
*/
export class EntitiesService<I, E extends I & IListable = I & IListable> extends GenericService<I> {
readonly noData$: Observable<boolean>;
readonly all$: Observable<E[]>;
readonly allLength$: Observable<number>;
protected readonly _defaultModelPath: string = '';
protected readonly _entityClass?: new (entityInterface: I, ...args: unknown[]) => E;
protected readonly _entityChanged$ = new Subject<E>();
protected readonly _entityDeleted$ = new Subject<E>();
readonly #all$ = new BehaviorSubject<E[]>([]);
constructor() {
super();
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());
}
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 => (this._entityClass ? new this._entityClass(entity) : (entity as E))),
tap((entities: E[]) => this.setEntities(entities)),
);
}
getEntityChanged$(entityId: Id): Observable<E | undefined> {
return this._entityChanged$.pipe(
filter(entity => entity.id === entityId),
startWith(this.find(entityId)),
shareLast(),
);
}
getEntityDeleted$(entityId: Id): 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);
}
}
remove(id: Id) {
const entity = this.all.find(item => item.id === id);
if (entity) {
this.#all$.next(this.all.filter(item => item.id !== id));
this._entityDeleted$.next(entity);
}
}
find(id: Id): E | undefined {
return this.all.find(entity => entity.id === id);
}
has(id: Id): 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);
}
}