Files
dspace-angular/src/app/core/data/relationship.service.ts

453 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { AppState, keySelector } from '../../app.reducer';
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { RestResponse } from '../cache/response.models';
import { CoreState } from '../core.reducers';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { Relationship } from '../shared/item-relationships/relationship.model';
import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type';
import { Item } from '../shared/item.model';
import { configureRequest, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
import { DataService } from './data.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { ItemDataService } from './item-data.service';
import { PaginatedList } from './paginated-list';
import { RemoteData, RemoteDataState } from './remote-data';
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
import { RequestService } from './request.service';
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
const relationshipListStateSelector = (listID: string): MemoizedSelector<AppState, NameVariantListState> => {
return keySelector<NameVariantListState>(listID, relationshipListsStateSelector);
};
const relationshipStateSelector = (listID: string, itemID: string): MemoizedSelector<AppState, string> => {
return keySelector<string>(itemID, relationshipListStateSelector(listID));
};
/**
* Return true if the Item in the payload of the source observable matches
* the given Item by UUID
*
* @param itemCheck the Item to compare with
*/
const compareItemsByUUID = (itemCheck: Item) =>
(source: Observable<RemoteData<Item>>): Observable<boolean> =>
source.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid === itemCheck.uuid)
);
/**
* The service handling all relationship requests
*/
@Injectable()
@dataService(RELATIONSHIP)
export class RelationshipService extends DataService<Relationship> {
protected linkPath = 'relationships';
protected responseMsToLive = 15 * 60 * 1000;
constructor(protected itemService: ItemDataService,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected halService: HALEndpointService,
protected objectCache: ObjectCacheService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Relationship>,
protected appStore: Store<AppState>) {
super();
}
/**
* Get the endpoint for a relationship by ID
* @param uuid
*/
getRelationshipEndpoint(uuid: string) {
return this.getBrowseEndpoint().pipe(
map((href: string) => `${href}/${uuid}`)
);
}
/**
* Send a delete request for a relationship by ID
* @param id
*/
deleteRelationship(id: string, copyVirtualMetadata: string): Observable<RestResponse> {
return this.getRelationshipEndpoint(id).pipe(
isNotEmptyOperator(),
take(1),
distinctUntilChanged(),
map((endpointURL: string) =>
new DeleteRequest(this.requestService.generateRequestId(), endpointURL + '?copyVirtualMetadata=' + copyVirtualMetadata)
),
configureRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)),
getResponseFromEntry(),
);
}
/**
* Method to create a new relationship
* @param typeId The identifier of the relationship type
* @param item1 The first item of the relationship
* @param item2 The second item of the relationship
* @param leftwardValue The leftward value of the relationship
* @param rightwardValue The rightward value of the relationship
*/
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string): Observable<RestResponse> {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;
return this.halService.getEndpoint(this.linkPath).pipe(
isNotEmptyOperator(),
take(1),
map((endpointUrl: string) => `${endpointUrl}?relationshipType=${typeId}`),
map((endpointUrl: string) => isNotEmpty(leftwardValue) ? `${endpointUrl}&leftwardValue=${leftwardValue}` : endpointUrl),
map((endpointUrl: string) => isNotEmpty(rightwardValue) ? `${endpointUrl}&rightwardValue=${rightwardValue}` : endpointUrl),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, `${item1.self} \n ${item2.self}`, options)),
configureRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
tap(() => this.refreshRelationshipItemsInCache(item1)),
tap(() => this.refreshRelationshipItemsInCache(item2)),
getResponseFromEntry(),
) as Observable<RestResponse>;
}
/**
* Method to remove two items of a relationship from the cache using the identifier of the relationship
* @param relationshipId The identifier of the relationship
*/
private refreshRelationshipItemsInCacheByRelationship(relationshipId: string) {
this.findById(relationshipId, followLink('leftItem'), followLink('rightItem')).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
switchMap((rel: Relationship) => observableCombineLatest(
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
)
),
take(1)
).subscribe(([item1, item2]) => {
this.refreshRelationshipItemsInCache(item1);
this.refreshRelationshipItemsInCache(item2);
})
}
/**
* Method to remove an item that's part of a relationship from the cache
* @param item The item to remove from the cache
*/
public refreshRelationshipItemsInCache(item) {
this.objectCache.remove(item._links.self.href);
this.requestService.removeByHrefSubstring(item.uuid);
observableCombineLatest([
this.objectCache.hasBySelfLinkObservable(item._links.self.href),
this.requestService.hasByHrefObservable(item.self)
]).pipe(
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
take(1),
switchMap(() => this.itemService.findByHref(item._links.self.href).pipe(take(1)))
).subscribe();
}
/**
* Get an item's relationships in the form of an array
*
* @param item The {@link Item} to get {@link Relationship}s for
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s
* should be automatically resolved
*/
getItemRelationshipsArray(item: Item, ...linksToFollow: Array<FollowLinkConfig<Relationship>>): Observable<Relationship[]> {
return this.findAllByHref(item._links.relationships.href, undefined, ...linksToFollow).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((rels: PaginatedList<Relationship>) => rels.page),
hasValueOperator(),
distinctUntilChanged(compareArraysUsingIds()),
);
}
/**
* Get an array of the labels of an items unique relationship types
* The array doesn't contain any duplicate labels
* @param item
*/
getRelationshipTypeLabelsByItem(item: Item): Observable<string[]> {
return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(
switchMap((relationships: Relationship[]) => observableCombineLatest(relationships.map((relationship: Relationship) => this.getRelationshipTypeLabelByRelationshipAndItem(relationship, item)))),
map((labels: string[]) => Array.from(new Set(labels)))
);
}
private getRelationshipTypeLabelByRelationshipAndItem(relationship: Relationship, item: Item): Observable<string> {
return relationship.leftItem.pipe(
getSucceededRemoteData(),
map((itemRD: RemoteData<Item>) => itemRD.payload),
switchMap((otherItem: Item) => relationship.relationshipType.pipe(
getSucceededRemoteData(),
map((relationshipTypeRD) => relationshipTypeRD.payload),
map((relationshipType: RelationshipType) => {
if (otherItem.uuid === item.uuid) {
return relationshipType.leftwardType;
} else {
return relationshipType.rightwardType;
}
})
)
))
}
/**
* Resolve a given item's relationships into related items and return the items as an array
* @param item
*/
getRelatedItems(item: Item): Observable<Item[]> {
return this.getItemRelationshipsArray(
item,
followLink('leftItem'),
followLink('rightItem'),
followLink('relationshipType')
).pipe(
relationsToItems(item.uuid)
);
}
/**
* Resolve a given item's relationships into related items, filtered by a relationship label
* and return the items as an array
* @param item
* @param label
* @param options
*/
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getItemRelationshipsByLabel(item, label, options, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid));
}
/**
* Resolve a given item's relationships by label
* This should move to the REST API.
*
* @param item
* @param label
* @param options
*/
getItemRelationshipsByLabel(item: Item, label: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<Relationship>>): Observable<RemoteData<PaginatedList<Relationship>>> {
let findListOptions = new FindListOptions();
if (options) {
findListOptions = Object.assign(new FindListOptions(), options);
}
const searchParams = [new RequestParam('label', label), new RequestParam('dso', item.id)];
if (findListOptions.searchParams) {
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
} else {
findListOptions.searchParams = searchParams;
}
return this.searchBy('byLabel', findListOptions, ...linksToFollow);
}
/**
* Method for fetching an item's relationships, but filtered by related item IDs (essentially performing a reverse lookup)
* Only relationships where leftItem or rightItem's ID is present in the list provided will be returned
* @param item
* @param uuids
*/
getRelationshipsByRelatedItemIds(item: Item, uuids: string[]): Observable<Relationship[]> {
return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem')).pipe(
switchMap((relationships: Relationship[]) => {
return observableCombineLatest(relationships.map((relationship: Relationship) => {
const isLeftItem$ = this.isItemInUUIDArray(relationship.leftItem, uuids);
const isRightItem$ = this.isItemInUUIDArray(relationship.rightItem, uuids);
return observableCombineLatest([isLeftItem$, isRightItem$]).pipe(
filter(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
map(() => relationship),
startWith(undefined)
);
}))
}),
map((relationships: Relationship[]) => relationships.filter(((relationship) => hasValue(relationship)))),
)
}
private isItemInUUIDArray(itemRD$: Observable<RemoteData<Item>>, uuids: string[]) {
return itemRD$.pipe(
getSucceededRemoteData(),
map((itemRD: RemoteData<Item>) => itemRD.payload),
map((item: Item) => uuids.includes(item.uuid))
);
}
/**
* Method to retrieve a relationship based on two items and a relationship type label
* @param item1 The first item in the relationship
* @param item2 The second item in the relationship
* @param label The rightward or leftward type of the relationship
*/
getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string, options?: FindListOptions): Observable<Relationship> {
return this.getItemRelationshipsByLabel(
item1,
label,
options,
followLink('relationshipType'),
followLink('leftItem'),
followLink('rightItem')
).pipe(
getSucceededRemoteData(),
// the mergemap below will emit all elements of the list as separate events
mergeMap((relationshipListRD: RemoteData<PaginatedList<Relationship>>) => relationshipListRD.payload.page),
mergeMap((relationship: Relationship) => {
return observableCombineLatest([
this.itemService.findByHref(relationship._links.leftItem.href).pipe(compareItemsByUUID(item2)),
this.itemService.findByHref(relationship._links.rightItem.href).pipe(compareItemsByUUID(item2))
]).pipe(
map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
map((isMatch) => isMatch ? relationship : undefined)
);
}),
filter((relationship) => hasValue(relationship)),
take(1)
)
}
/**
* Method to set the name variant for specific list and item
* @param listID The list for which to save the name variant
* @param itemID The item ID for which to save the name variant
* @param nameVariant The name variant to save
*/
public setNameVariant(listID: string, itemID: string, nameVariant: string) {
this.appStore.dispatch(new SetNameVariantAction(listID, itemID, nameVariant));
}
/**
* Method to retrieve the name variant for a specific list and item
* @param listID The list for which to retrieve the name variant
* @param itemID The item ID for which to retrieve the name variant
*/
public getNameVariant(listID: string, itemID: string): Observable<string> {
return this.appStore.pipe(
select(relationshipStateSelector(listID, itemID))
);
}
/**
* Method to remove the name variant for specific list and item
* @param listID The list for which to remove the name variant
* @param itemID The item ID for which to remove the name variant
*/
public removeNameVariant(listID: string, itemID: string) {
this.appStore.dispatch(new RemoveNameVariantAction(listID, itemID));
}
/**
* Method to retrieve all name variants for a single list
* @param listID The id of the list
*/
public getNameVariantsByListID(listID: string) {
return this.appStore.pipe(select(relationshipListStateSelector(listID)));
}
/**
* Method to update the name variant on the server
* @param item1 The first item of the relationship
* @param item2 The second item of the relationship
* @param relationshipLabel The leftward or rightward type of the relationship
* @param nameVariant The name variant to set for the matching relationship
*/
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
.pipe(
switchMap((relation: Relationship) =>
relation.relationshipType.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((type) => {
return { relation, type }
})
)
),
switchMap((relationshipAndType: { relation: Relationship, type: RelationshipType }) => {
const { relation, type } = relationshipAndType;
let updatedRelationship;
if (relationshipLabel === type.leftwardType) {
updatedRelationship = Object.assign(new Relationship(), relation, { rightwardValue: nameVariant });
} else {
updatedRelationship = Object.assign(new Relationship(), relation, { leftwardValue: nameVariant });
}
return this.update(updatedRelationship);
}),
);
}
/**
* Check whether a given item is the left item of a given relationship, as an observable boolean
* @param relationship the relationship for which to check whether the given item is the left item
* @param item the item for which to check whether it is the left item of the given relationship
*/
public isLeftItem(relationship: Relationship, item: Item): Observable<boolean> {
return relationship.leftItem.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
filter((leftItem: Item) => hasValue(leftItem) && isNotEmpty(leftItem.uuid)),
map((leftItem) => leftItem.uuid === item.uuid)
);
}
/**
* Method to update the the right or left place of a relationship
* The useLeftItem field in the reorderable relationship determines which place should be updated
* @param reoRel
*/
public updatePlace(reoRel: ReorderableRelationship): Observable<RemoteData<Relationship>> {
let updatedRelationship;
if (reoRel.useLeftItem) {
updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { rightPlace: reoRel.newIndex });
} else {
updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { leftPlace: reoRel.newIndex });
}
const update$ = this.update(updatedRelationship);
update$.pipe(
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.ResponsePending),
take(1),
).subscribe((relationshipRD: RemoteData<Relationship>) => {
if (relationshipRD.state === RemoteDataState.ResponsePending) {
this.refreshRelationshipItemsInCacheByRelationship(reoRel.relationship.id);
}
});
return update$;
}
/**
* Patch isn't supported on the relationship endpoint, so use put instead.
*
* @param object the {@link Relationship} to update
*/
update(object: Relationship): Observable<RemoteData<Relationship>> {
return this.put(object);
}
}