Select Git revision
test_env.py
resource.service.ts 6.08 KiB
import {
BehaviorSubject,
Observable,
catchError,
combineLatest,
filter,
map,
mergeMap,
of,
startWith,
tap,
throwError,
} from 'rxjs';
import { ResourceServiceConfig } from './resource.model';
import {
StateResource,
createEmptyStateResource,
createErrorStateResource,
createStateResource,
isLoadingRequired,
throwErrorOn,
} from './resource.util';
import { Resource, getUrl, hasLink } from '@ngxp/rest';
import { ResourceRepository } from './resource.repository';
import { isEqual, isNull } from 'lodash-es';
import { isNotNull } from '../tech.util';
import { HttpErrorResponse } from '@angular/common/http';
import { isUnprocessableEntity } from '../http.util';
import { HttpError, ProblemDetail } from '../tech.model';
import { isDevMode } from '@angular/core';
/**
* B = Type of baseresource
* T = Type of the resource which is working on
*/
export class ResourceService<B extends Resource, T extends Resource> {
readonly stateResource: BehaviorSubject<StateResource<T>> = new BehaviorSubject(
createEmptyStateResource(),
);
configResource: B;
constructor(
private config: ResourceServiceConfig<B>,
private repository: ResourceRepository,
) {}
public get(): Observable<StateResource<T>> {
return combineLatest([this.stateResource.asObservable(), this.getConfigResource()]).pipe(
tap(([, configResource]) => this.handleConfigResourceChanged(configResource)),
tap(([, configResource]) => this.handleNullConfigResource(configResource)),
tap(([stateResource, configResource]) =>
this.handleConfigResource(stateResource, configResource),
),
filter(([stateResource]) => !this.shouldFilter(stateResource)),
map(([stateResource]) => stateResource),
startWith(createEmptyStateResource<T>(true)),
);
}
private getConfigResource(): Observable<B> {
return this.config.resource.pipe(
filter((stateResource) => stateResource.loaded && !stateResource.loading),
map((stateResource) => stateResource.resource),
);
}
handleConfigResourceChanged(configResource: B): void {
if (!isEqual(this.configResource, configResource)) {
this.configResource = configResource;
this.stateResource.next({ ...this.stateResource.value, reload: true });
}
}
handleNullConfigResource(configResource: B): void {
if (this.shouldClearStateResource(configResource)) {
this.stateResource.next(createEmptyStateResource());
}
}
shouldClearStateResource(configResource: B): boolean {
return isNull(configResource) && !isEqual(this.stateResource.value, createEmptyStateResource());
}
handleConfigResource(stateResource: StateResource<T>, configResource: B): void {
if (this.shouldLoadResource(stateResource, configResource)) {
this.loadResource(configResource);
}
}
shouldLoadResource(stateResource: StateResource<T>, configResource: B): boolean {
return isNotNull(configResource) && isLoadingRequired(stateResource);
}
loadResource(configResource: B): void {
this.verifyGetLink(configResource);
this.setStateResourceLoading();
this.doLoadResource(configResource);
}
handleError(errorResponse: HttpErrorResponse): Observable<StateResource<HttpError>> {
if (isUnprocessableEntity(errorResponse.status)) {
return of(createErrorStateResource((<any>errorResponse) as ProblemDetail));
}
return throwError(() => errorResponse);
}
private verifyGetLink(configResource: B): void {
throwErrorOn(
!hasLink(configResource, this.config.getLinkRel),
'No get link exists on configresource.',
);
}
setStateResourceLoading(): void {
this.stateResource.next({ ...createEmptyStateResource(true), reload: false });
}
doLoadResource(configResource: B): void {
this.repository
.getResource(getUrl(configResource, this.config.getLinkRel))
.pipe()
.subscribe((loadedResource: T) => this.updateStateResource(loadedResource));
}
updateStateResource(resource: T): void {
this.stateResource.next(createStateResource(resource));
}
shouldFilter(stateResource: StateResource<Resource>): boolean {
return (
stateResource.reload || this.isInvalidResourceCombination(stateResource, this.configResource)
);
}
isInvalidResourceCombination(
stateResource: StateResource<Resource>,
baseResource: Resource,
): boolean {
return (
!stateResource.loading &&
((!stateResource.loaded && isNotNull(baseResource)) ||
(stateResource.loaded && isNull(baseResource)))
);
}
public save(toSave: unknown): Observable<StateResource<T | HttpError>> {
this.verifyEditLinkRel();
return this.stateResource.asObservable().pipe(
mergeMap((selectedResource: StateResource<T>) =>
this.doSave(selectedResource.resource, toSave),
),
map((loadedResource: T) => createStateResource(loadedResource)),
catchError((errorResponse: HttpErrorResponse) => this.handleError(errorResponse)),
);
}
private verifyEditLinkRel(): void {
throwErrorOn(
!this.hasLinkRel(this.config.editLinkRel),
'No edit link exists on current stateresource.',
);
}
private doSave(resource: T, toSave: unknown): Observable<T> {
return <Observable<T>>this.repository.save({
resource,
linkRel: this.config.editLinkRel,
toSave,
});
}
public refresh(): void {
this.stateResource.next({ ...this.stateResource.value, reload: true });
}
public canEdit(): boolean {
return this.hasLinkRel(this.config.editLinkRel);
}
public canDelete(): boolean {
return this.hasLinkRel(this.config.deleteLinkRel);
}
private hasLinkRel(linkRel: string): boolean {
return hasLink(this.getStateResource(), linkRel);
}
private getStateResource(): T {
return this.stateResource.value.resource;
}
public delete(): Observable<Resource> {
this.verifyDeleteLinkRel();
return this.repository.delete(this.getStateResource(), this.config.deleteLinkRel);
}
private verifyDeleteLinkRel(): void {
throwErrorOn(
!this.hasLinkRel(this.config.deleteLinkRel),
'No delete link exists on current stateresource.',
);
}
}