114 lines
3.8 KiB
TypeScript
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);
|
|
}
|
|
}
|