Cache redesign part 2

This commit is contained in:
Art Lowel
2021-01-20 14:34:08 +01:00
parent 64ba6293c9
commit c66de4fe91
155 changed files with 1610 additions and 1637 deletions

View File

@@ -15,8 +15,8 @@ import { EpersonDtoModel } from '../../../core/eperson/models/eperson-dto.model'
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { import {
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData,
getFirstCompletedRemoteData getAllSucceededRemoteData
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@@ -39,7 +39,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
/** /**
* A list of all the current EPeople within the repository or the result of the search * A list of all the current EPeople within the repository or the result of the search
*/ */
ePeople$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject<RemoteData<PaginatedList<EPerson>>>({} as any); ePeople$: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject(buildPaginatedList<EPerson>(new PageInfo(), []));
/** /**
* A BehaviorSubject with the list of EpersonDtoModel objects made from the EPeople in the repository or * A BehaviorSubject with the list of EpersonDtoModel objects made from the EPeople in the repository or
* as the result of the search * as the result of the search
@@ -72,6 +72,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
currentSearchQuery: string; currentSearchQuery: string;
currentSearchScope: string; currentSearchScope: string;
/**
* The subscription for the search method
*/
searchSub: Subscription;
/** /**
* List of subscriptions * List of subscriptions
*/ */
@@ -108,6 +113,29 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.isEPersonFormShown = true; this.isEPersonFormShown = true;
} }
})); }));
this.subs.push(this.ePeople$.pipe(
switchMap((epeople: PaginatedList<EPerson>) => {
if (epeople.pageInfo.totalElements > 0) {
return combineLatest(...epeople.page.map((eperson) => {
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
map((authorized) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.ableToDelete = authorized;
epersonDtoModel.eperson = eperson;
return epersonDtoModel;
})
);
})).pipe(map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epeople.pageInfo, dtos);
}));
} else {
// if it's empty, simply forward the empty list
return [epeople];
}
})).subscribe((value: PaginatedList<EpersonDtoModel>) => {
this.ePeopleDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
}));
} }
/** /**
@@ -138,34 +166,21 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.currentSearchScope = scope; this.currentSearchScope = scope;
this.config.currentPage = 1; this.config.currentPage = 1;
} }
this.subs.push(this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { if (hasValue(this.searchSub)) {
this.searchSub.unsubscribe();
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
}
this.searchSub = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: this.config.currentPage, currentPage: this.config.currentPage,
elementsPerPage: this.config.pageSize elementsPerPage: this.config.pageSize
}).subscribe((peopleRD) => { }).pipe(
this.ePeople$.next(peopleRD); getAllSucceededRemoteData(),
).subscribe((peopleRD) => {
this.ePeople$.next(peopleRD.payload);
this.pageInfoState$.next(peopleRD.payload.pageInfo); this.pageInfoState$.next(peopleRD.payload.pageInfo);
} }
));
this.subs.push(this.ePeople$.pipe(
getAllSucceededRemoteDataPayload(),
switchMap((epeople) => {
return combineLatest(...epeople.page.map((eperson) => {
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
map((authorized) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.ableToDelete = authorized;
epersonDtoModel.eperson = eperson;
return epersonDtoModel;
})
); );
})).pipe(map((dtos: EpersonDtoModel[]) => { this.subs.push(this.searchSub);
return buildPaginatedList(epeople.pageInfo, dtos);
}));
})).subscribe((value) => {
this.ePeopleDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
}));
} }
/** /**
@@ -224,7 +239,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
} }
}); });
}} }
}
}); });
} }
} }
@@ -261,16 +277,13 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
} }
/** /**
* This method will ensure that the page gets reset and that the cache is cleared * This method will set everything to stale, which will cause the lists on this page to update.
*/ */
reset() { reset() {
this.epersonService.getBrowseEndpoint().pipe( this.epersonService.getBrowseEndpoint().pipe(
switchMap((href) => this.requestService.removeByHrefSubstring(href)),
filter((isCached) => isCached),
take(1) take(1)
).subscribe(() => { ).subscribe((href: string) => {
this.cleanupSubscribes(); this.requestService.setStaleByHrefSubstring(href);
this.initialisePage();
}); });
} }
} }

View File

@@ -345,7 +345,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup === null) { if (activeGroup === null) {
this.groupDataService.cancelEditGroup(); this.groupDataService.cancelEditGroup();
this.groupDataService.findByHref(groupSelfLink, false, followLink('subgroups'), followLink('epersons'), followLink('object')) this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
.pipe( .pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload()) getRemoteDataPayload())

View File

@@ -160,7 +160,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
if (group != null) { if (group != null) {
return this.ePersonDataService.findAllByHref(group._links.epersons.href, { return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
currentPage: 0, currentPage: 0,
elementsPerPage: Number.MAX_SAFE_INTEGER elementsPerPage: 9999
}) })
.pipe( .pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
@@ -206,12 +206,11 @@ export class MembersListComponent implements OnInit, OnDestroy {
if (ePersonToUpdate != null) { if (ePersonToUpdate != null) {
this.ePersonDataService.clearLinkRequests(ePersonToUpdate._links.groups.href); this.ePersonDataService.clearLinkRequests(ePersonToUpdate._links.groups.href);
} }
this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup)); this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup));
this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, { this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, {
currentPage: this.configSearch.currentPage, currentPage: this.configSearch.currentPage,
elementsPerPage: this.configSearch.pageSize elementsPerPage: this.configSearch.pageSize
}); }, false);
} }
/** /**

View File

@@ -125,7 +125,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
} else { } else {
return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
currentPage: 0, currentPage: 0,
elementsPerPage: Number.MAX_SAFE_INTEGER elementsPerPage: 9999
}) })
.pipe( .pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
@@ -212,12 +212,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
* @param activeGroup Group currently being edited * @param activeGroup Group currently being edited
*/ */
public forceUpdateGroups(activeGroup: Group) { public forceUpdateGroups(activeGroup: Group) {
this.groupDataService.clearGroupLinkRequests(activeGroup._links.subgroups.href);
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup)); this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup));
this.subgroupsOfGroup = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { this.subgroupsOfGroup = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
currentPage: this.config.currentPage, currentPage: this.config.currentPage,
elementsPerPage: this.config.pageSize elementsPerPage: this.config.pageSize
}); }, false);
} }
/** /**

View File

@@ -89,8 +89,9 @@ export class MetadataSchemaComponent implements OnInit {
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe( this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
switchMap(([schema, update]: [MetadataSchema, boolean]) => { switchMap(([schema, update]: [MetadataSchema, boolean]) => {
if (update) { if (update) {
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), true, followLink('schema')); this.needsUpdate$.next(false);
} }
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), !update, false, followLink('schema'));
}) })
); );
} }

View File

@@ -23,7 +23,7 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Bitstream>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Bitstream>> {
return this.bitstreamService.findById(route.params.id, false, ...this.followLinks) return this.bitstreamService.findById(route.params.id, true, false, ...this.followLinks)
.pipe( .pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );
@@ -35,7 +35,7 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
*/ */
get followLinks(): FollowLinkConfig<Bitstream>[] { get followLinks(): FollowLinkConfig<Bitstream>[] {
return [ return [
followLink('bundle', undefined, true, followLink('item')), followLink('bundle', undefined, true, true, true, followLink('item')),
followLink('format') followLink('format')
]; ];
} }

View File

@@ -57,6 +57,9 @@ describe('CollectionItemMapperComponent', () => {
id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4',
name: 'test-collection', name: 'test-collection',
_links: { _links: {
mappedItems: {
href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4/mappedItems'
},
self: { self: {
href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4' href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4'
} }
@@ -82,8 +85,10 @@ describe('CollectionItemMapperComponent', () => {
const searchConfigServiceStub = { const searchConfigServiceStub = {
paginatedSearchOptions: mockSearchOptions paginatedSearchOptions: mockSearchOptions
}; };
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
const itemDataServiceStub = { const itemDataServiceStub = {
mapToCollection: () => createSuccessfulRemoteDataObject$({}) mapToCollection: () => createSuccessfulRemoteDataObject$({}),
findAllByHref: () => of(emptyList)
}; };
const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD }); const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD });
const translateServiceStub = { const translateServiceStub = {
@@ -92,7 +97,6 @@ describe('CollectionItemMapperComponent', () => {
onTranslationChange: new EventEmitter(), onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter() onDefaultLangChange: new EventEmitter()
}; };
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
const searchServiceStub = Object.assign(new SearchServiceStub(), { const searchServiceStub = Object.assign(new SearchServiceStub(), {
search: () => of(emptyList), search: () => of(emptyList),
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */

View File

@@ -124,10 +124,11 @@ export class CollectionItemMapperComponent implements OnInit {
this.collectionItemsRD$ = collectionAndOptions$.pipe( this.collectionItemsRD$ = collectionAndOptions$.pipe(
switchMap(([collectionRD, options, shouldUpdate]) => { switchMap(([collectionRD, options, shouldUpdate]) => {
if (shouldUpdate) { if (shouldUpdate) {
return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, { this.shouldUpdate$.next(false);
sort: this.defaultSortOptions
}),followLink('owningCollection'));
} }
return this.itemDataService.findAllByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, {
sort: this.defaultSortOptions
}),!shouldUpdate, true, followLink('owningCollection'));
}) })
); );
this.mappedItemsRD$ = collectionAndOptions$.pipe( this.mappedItemsRD$ = collectionAndOptions$.pipe(

View File

@@ -23,7 +23,7 @@ export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
return this.collectionService.findById(route.params.id, false, followLink('logo')).pipe( return this.collectionService.findById(route.params.id, true, false, followLink('logo')).pipe(
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
); );
} }

View File

@@ -23,7 +23,7 @@ export class ItemTemplatePageResolver implements Resolve<RemoteData<Item>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemTemplateService.findByCollectionID(route.params.id, false, followLink('templateItemOf')).pipe( return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );
} }

View File

@@ -25,6 +25,7 @@ export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
return this.communityService.findById( return this.communityService.findById(
route.params.id, route.params.id,
true,
false, false,
followLink('logo'), followLink('logo'),
followLink('subcommunities'), followLink('subcommunities'),

View File

@@ -79,7 +79,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
getFirstSucceededRemoteDataWithNotEmptyPayload(), getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((item: Item) => this.linkService.resolveLink( map((item: Item) => this.linkService.resolveLink(
item, item,
followLink('bundles', new FindListOptions(), true, followLink('bitstreams')) followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams'))
)) ))
) as Observable<Item>; ) as Observable<Item>;

View File

@@ -81,11 +81,15 @@ describe('ItemCollectionMapperComponent', () => {
const itemDataServiceStub = { const itemDataServiceStub = {
mapToCollection: () => createSuccessfulRemoteDataObject$({}), mapToCollection: () => createSuccessfulRemoteDataObject$({}),
removeMappingFromCollection: () => createSuccessfulRemoteDataObject$({}), removeMappingFromCollection: () => createSuccessfulRemoteDataObject$({}),
getMappedCollectionsEndpoint: () => of('rest/api/mappedCollectionsEndpoint'),
getMappedCollections: () => of(mockCollectionsRD), getMappedCollections: () => of(mockCollectionsRD),
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
clearMappedCollectionsRequests: () => {} clearMappedCollectionsRequests: () => {}
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
}; };
const collectionDataServiceStub = {
findAllByHref: () => of(mockCollectionsRD)
};
const searchServiceStub = Object.assign(new SearchServiceStub(), { const searchServiceStub = Object.assign(new SearchServiceStub(), {
search: () => of(mockCollectionsRD), search: () => of(mockCollectionsRD),
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
@@ -114,7 +118,7 @@ describe('ItemCollectionMapperComponent', () => {
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
{ provide: TranslateService, useValue: translateServiceStub }, { provide: TranslateService, useValue: translateServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: CollectionDataService, useValue: {} } { provide: CollectionDataService, useValue: collectionDataServiceStub }
] ]
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -11,7 +11,8 @@ import {
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getRemoteDataPayload, getRemoteDataPayload,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
toDSpaceObjectListRD toDSpaceObjectListRD,
getAllSucceededRemoteData
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { map, startWith, switchMap, take } from 'rxjs/operators'; import { map, startWith, switchMap, take } from 'rxjs/operators';
@@ -101,14 +102,22 @@ export class ItemCollectionMapperComponent implements OnInit {
* Load mappedCollectionsRD$ to only obtain collections that don't own this item * Load mappedCollectionsRD$ to only obtain collections that don't own this item
*/ */
loadCollectionLists() { loadCollectionLists() {
console.log('loadCollectionLists');
this.shouldUpdate$ = new BehaviorSubject<boolean>(true); this.shouldUpdate$ = new BehaviorSubject<boolean>(true);
this.itemCollectionsRD$ = observableCombineLatest(this.itemRD$, this.shouldUpdate$).pipe( this.itemCollectionsRD$ = observableCombineLatest(this.itemRD$.pipe(getFirstSucceededRemoteDataPayload()), this.shouldUpdate$).pipe(
map(([itemRD, shouldUpdate]) => { switchMap(([item, shouldUpdate]) => {
if (shouldUpdate) { if (shouldUpdate === true) {
return itemRD.payload; this.shouldUpdate$.next(false);
} }
return this.collectionDataService.findAllByHref(
this.itemDataService.getMappedCollectionsEndpoint(item.id),
undefined,
!shouldUpdate,
false
).pipe(
getAllSucceededRemoteData()
);
}), }),
switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id))
); );
const owningCollectionRD$ = this.itemRD$.pipe( const owningCollectionRD$ = this.itemRD$.pipe(
@@ -141,13 +150,11 @@ export class ItemCollectionMapperComponent implements OnInit {
const itemIdAndExcludingIds$ = observableCombineLatest([ const itemIdAndExcludingIds$ = observableCombineLatest([
this.itemRD$.pipe( this.itemRD$.pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
take(1),
map((rd: RemoteData<Item>) => rd.payload), map((rd: RemoteData<Item>) => rd.payload),
map((item: Item) => item.id) map((item: Item) => item.id)
), ),
this.itemCollectionsRD$.pipe( this.itemCollectionsRD$.pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
take(1),
map((rd: RemoteData<PaginatedList<Collection>>) => rd.payload.page), map((rd: RemoteData<PaginatedList<Collection>>) => rd.payload.page),
map((collections: Collection[]) => collections.map((collection: Collection) => collection.id)) map((collections: Collection[]) => collections.map((collection: Collection) => collection.id))
) )
@@ -203,6 +210,7 @@ export class ItemCollectionMapperComponent implements OnInit {
successMessages.subscribe(([head, content]) => { successMessages.subscribe(([head, content]) => {
this.notificationsService.success(head, content); this.notificationsService.success(head, content);
}); });
this.shouldUpdate$.next(true);
} }
if (unsuccessful.length > 0) { if (unsuccessful.length > 0) {
const unsuccessMessages = observableCombineLatest([ const unsuccessMessages = observableCombineLatest([
@@ -214,8 +222,6 @@ export class ItemCollectionMapperComponent implements OnInit {
this.notificationsService.error(head, content); this.notificationsService.error(head, content);
}); });
} }
// Force an update on all lists and switch back to the first tab
this.shouldUpdate$.next(true);
this.switchToFirstTab(); this.switchToFirstTab();
}); });
} }

View File

@@ -228,7 +228,7 @@ describe('EditInPlaceFieldComponent', () => {
})); }));
it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => { it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => {
expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, false, followLink('schema')); expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, true, false, followLink('schema'));
}); });
it('it should set metadataFieldSuggestions to the right value', () => { it('it should set metadataFieldSuggestions to the right value', () => {

View File

@@ -127,7 +127,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
*/ */
findMetadataFieldSuggestions(query: string) { findMetadataFieldSuggestions(query: string) {
if (isNotEmpty(query)) { if (isNotEmpty(query)) {
return this.registryService.queryMetadataFields(query, null, false, followLink('schema')).pipe( return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
metadataFieldsToString(), metadataFieldsToString(),
).subscribe((fieldNames: string[]) => { ).subscribe((fieldNames: string[]) => {

View File

@@ -11,7 +11,7 @@ import {
} from '../../../../core/data/object-updates/object-updates.reducer'; } from '../../../../core/data/object-updates/object-updates.reducer';
import { RelationshipService } from '../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../core/data/relationship.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { defaultIfEmpty, map, mergeMap, switchMap, take, } from 'rxjs/operators'; import { defaultIfEmpty, map, mergeMap, switchMap, take, startWith } from 'rxjs/operators';
import { hasValue, hasValueOperator } from '../../../../shared/empty.util'; import { hasValue, hasValueOperator } from '../../../../shared/empty.util';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
@@ -311,12 +311,13 @@ export class EditRelationshipListComponent implements OnInit {
return fieldUpdatesFiltered; return fieldUpdatesFiltered;
}), }),
)), )),
startWith({}),
); );
} }
private getItemRelationships() { private getItemRelationships() {
this.linkService.resolveLink(this.item, this.linkService.resolveLink(this.item,
followLink('relationships', undefined, true, followLink('relationships', undefined, true, true, true,
followLink('relationshipType'), followLink('relationshipType'),
followLink('leftItem'), followLink('leftItem'),
followLink('rightItem'), followLink('rightItem'),

View File

@@ -84,6 +84,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
switchMap(() => this.itemService.findById( switchMap(() => this.itemService.findById(
this.item.uuid, this.item.uuid,
true, true,
true,
followLink('owningCollection'), followLink('owningCollection'),
followLink('bundles'), followLink('bundles'),
followLink('relationships')), followLink('relationships')),
@@ -118,6 +119,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
switchMap((entityType) => switchMap((entityType) =>
this.entityTypeService.getEntityTypeRelationships( this.entityTypeService.getEntityTypeRelationships(
entityType.id, entityType.id,
true,
true,
followLink('leftType'), followLink('leftType'),
followLink('rightType')) followLink('rightType'))
.pipe( .pipe(

View File

@@ -67,6 +67,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
'ORIGINAL', 'ORIGINAL',
{elementsPerPage: this.pageSize, currentPage: pageNumber}, {elementsPerPage: this.pageSize, currentPage: pageNumber},
true, true,
true,
followLink('format') followLink('format')
)), )),
tap((rd: RemoteData<PaginatedList<Bitstream>>) => { tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
@@ -83,6 +84,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
'LICENSE', 'LICENSE',
{elementsPerPage: this.pageSize, currentPage: pageNumber}, {elementsPerPage: this.pageSize, currentPage: pageNumber},
true, true,
true,
followLink('format') followLink('format')
)), )),
tap((rd: RemoteData<PaginatedList<Bitstream>>) => { tap((rd: RemoteData<PaginatedList<Bitstream>>) => {

View File

@@ -25,11 +25,12 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id, return this.itemService.findById(route.params.id,
true,
false, false,
followLink('owningCollection'), followLink('owningCollection'),
followLink('bundles', new FindListOptions(), true, followLink('bitstreams')), followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')),
followLink('relationships'), followLink('relationships'),
followLink('version', undefined, true, followLink('versionhistory')), followLink('version', undefined, true, true, true, followLink('versionhistory')),
).pipe( ).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );

View File

@@ -1,6 +1,11 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs'; import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
zip as observableZip
} from 'rxjs';
import { RelationshipService } from '../../../core/data/relationship.service'; import { RelationshipService } from '../../../core/data/relationship.service';
import { MetadataValue } from '../../../core/shared/metadata.models'; import { MetadataValue } from '../../../core/shared/metadata.models';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
@@ -82,7 +87,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum)) .map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => { .map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) { if (metadatum.isVirtual) {
return this.relationshipService.findById(metadatum.virtualValue, false, followLink('leftItem'), followLink('rightItem')).pipe( return this.relationshipService.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
switchMap((relRD: RemoteData<Relationship>) => switchMap((relRD: RemoteData<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe( observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(

View File

@@ -1,10 +1,11 @@
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FindByIDRequest, IdentifierType } from '../core/data/request.models'; import { IdentifierType } from '../core/data/request.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service'; import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service';
import { DSpaceObject } from '../core/shared/dspace-object.model';
interface LookupParams { interface LookupParams {
type: IdentifierType; type: IdentifierType;
@@ -20,7 +21,7 @@ export class LookupGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = this.getLookupParams(route); const params = this.getLookupParams(route);
return this.dsoService.findByIdAndIDType(params.id, params.type).pipe( return this.dsoService.findByIdAndIDType(params.id, params.type).pipe(
map((response: RemoteData<FindByIDRequest>) => response.hasFailed) map((response: RemoteData<DSpaceObject>) => response.hasFailed)
); );
} }

View File

@@ -24,6 +24,7 @@ export class WorkflowItemPageResolver implements Resolve<RemoteData<WorkflowItem
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<WorkflowItem>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<WorkflowItem>> {
return this.workflowItemService.findById(route.params.id, return this.workflowItemService.findById(route.params.id,
true,
false, false,
followLink('item'), followLink('item'),
).pipe( ).pipe(

View File

@@ -45,11 +45,7 @@ export class AuthRequestService {
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, body, options)), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, body, options)),
map ((request: PostRequest) => { tap((request: PostRequest) => this.requestService.send(request)),
request.responseMsToLive = 10 * 1000;
return request;
}),
tap((request: PostRequest) => this.requestService.configure(request)),
mergeMap((request: PostRequest) => this.fetchRequest(request)), mergeMap((request: PostRequest) => this.fetchRequest(request)),
distinctUntilChanged()); distinctUntilChanged());
} }
@@ -60,11 +56,7 @@ export class AuthRequestService {
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)), map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)),
map ((request: GetRequest) => { tap((request: GetRequest) => this.requestService.send(request)),
request.forceBypassCache = true;
return request;
}),
tap((request: GetRequest) => this.requestService.configure(request)),
mergeMap((request: GetRequest) => this.fetchRequest(request)), mergeMap((request: GetRequest) => this.fetchRequest(request)),
distinctUntilChanged()); distinctUntilChanged());
} }
@@ -78,7 +70,7 @@ export class AuthRequestService {
distinctUntilChanged(), distinctUntilChanged(),
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()), map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
tap((request: PostRequest) => this.requestService.configure(request)), tap((request: PostRequest) => this.requestService.send(request)),
switchMap((request: PostRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)), switchMap((request: PostRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((response: RemoteData<ShortLivedToken>) => { map((response: RemoteData<ShortLivedToken>) => {

View File

@@ -23,7 +23,7 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collecti
*/ */
get followLinks(): FollowLinkConfig<Collection>[] { get followLinks(): FollowLinkConfig<Collection>[] {
return [ return [
followLink('parentCommunity', undefined, true, followLink('parentCommunity', undefined, true, true, true,
followLink('parentCommunity') followLink('parentCommunity')
) )
]; ];

View File

@@ -29,7 +29,7 @@ export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceO
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> {
const uuid = route.params.id; const uuid = route.params.id;
return this.dataService.findById(uuid, false, ...this.followLinks).pipe( return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
map((object: T) => { map((object: T) => {

View File

@@ -23,8 +23,8 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
*/ */
get followLinks(): FollowLinkConfig<Item>[] { get followLinks(): FollowLinkConfig<Item>[] {
return [ return [
followLink('owningCollection', undefined, true, followLink('owningCollection', undefined, true, true, true,
followLink('parentCommunity', undefined, true, followLink('parentCommunity', undefined, true, true, true,
followLink('parentCommunity')) followLink('parentCommunity'))
), ),
followLink('bundles'), followLink('bundles'),

View File

@@ -25,22 +25,22 @@ describe(`BrowseDefinitionDataService`, () => {
describe(`findAll`, () => { describe(`findAll`, () => {
it(`should call findAll on DataServiceImpl`, () => { it(`should call findAll on DataServiceImpl`, () => {
service.findAll(options, false, ...linksToFollow); service.findAll(options, true, false, ...linksToFollow);
expect(dataServiceImplSpy.findAll).toHaveBeenCalledWith(options, false, ...linksToFollow); expect(dataServiceImplSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow);
}); });
}); });
describe(`findByHref`, () => { describe(`findByHref`, () => {
it(`should call findByHref on DataServiceImpl`, () => { it(`should call findByHref on DataServiceImpl`, () => {
service.findByHref(hrefSingle, false, ...linksToFollow); service.findByHref(hrefSingle, true, false, ...linksToFollow);
expect(dataServiceImplSpy.findByHref).toHaveBeenCalledWith(hrefSingle, false, ...linksToFollow); expect(dataServiceImplSpy.findByHref).toHaveBeenCalledWith(hrefSingle, true, false, ...linksToFollow);
}); });
}); });
describe(`findAllByHref`, () => { describe(`findAllByHref`, () => {
it(`should call findAllByHref on DataServiceImpl`, () => { it(`should call findAllByHref on DataServiceImpl`, () => {
service.findAllByHref(hrefAll, options, false, ...linksToFollow); service.findAllByHref(hrefAll, options, true, false, ...linksToFollow);
expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(hrefAll, options, false, ...linksToFollow); expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(hrefAll, options, true, false, ...linksToFollow);
}); });
}); });

View File

@@ -63,42 +63,48 @@ export class BrowseDefinitionDataService {
* info should be added to the objects * info should be added to the objects
* *
* @param options Find list options object * @param options Find list options object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<BrowseDefinition>>>} * @return {Observable<RemoteData<PaginatedList<BrowseDefinition>>>}
* Return an observable that emits object list * Return an observable that emits object list
*/ */
findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> { findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.dataService.findAll(options, reRequestOnStale, ...linksToFollow); return this.dataService.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
* Returns an observable of {@link RemoteData} of an {@link BrowseDefinition}, based on an href, with a list of {@link FollowLinkConfig}, * Returns an observable of {@link RemoteData} of an {@link BrowseDefinition}, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link BrowseDefinition} * to automatically resolve {@link HALLink}s of the {@link BrowseDefinition}
* @param href The url of {@link BrowseDefinition} we want to retrieve * @param href The url of {@link BrowseDefinition} we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<BrowseDefinition>> { findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<BrowseDefinition>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
* Returns a list of observables of {@link RemoteData} of {@link BrowseDefinition}s, based on an href, with a list of {@link FollowLinkConfig}, * Returns a list of observables of {@link RemoteData} of {@link BrowseDefinition}s, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link BrowseDefinition} * to automatically resolve {@link HALLink}s of the {@link BrowseDefinition}
* @param href The url of the {@link BrowseDefinition} we want to retrieve * @param href The url of object we want to retrieve
* @param findListOptions Find list options object * @param findListOptions Find list options object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> { findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
} }

View File

@@ -11,8 +11,8 @@ import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { BrowseService } from './browse.service'; import { BrowseService } from './browse.service';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList, getFirstUsedArgumentOfSpyMethod } from '../../shared/testing/utils.test';
import { GetRequest } from '../data/request.models'; import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
describe('BrowseService', () => { describe('BrowseService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -82,6 +82,7 @@ describe('BrowseService', () => {
]; ];
let browseDefinitionDataService; let browseDefinitionDataService;
let hrefOnlyDataService;
const getRequestEntry$ = (successful: boolean) => { const getRequestEntry$ = (successful: boolean) => {
return observableOf({ return observableOf({
@@ -93,10 +94,12 @@ describe('BrowseService', () => {
browseDefinitionDataService = jasmine.createSpyObj('browseDefinitionDataService', { browseDefinitionDataService = jasmine.createSpyObj('browseDefinitionDataService', {
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions)) findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions))
}); });
hrefOnlyDataService = getMockHrefOnlyDataService();
return new BrowseService( return new BrowseService(
requestService, requestService,
halService, halService,
browseDefinitionDataService, browseDefinitionDataService,
hrefOnlyDataService,
rdbService rdbService
); );
} }
@@ -123,54 +126,40 @@ describe('BrowseService', () => {
}); });
}); });
describe('getBrowseEntriesFor and getBrowseItemsFor', () => { describe('getBrowseEntriesFor and findList', () => {
const mockAuthorName = 'Donald Smith'; const mockAuthorName = 'Donald Smith';
beforeEach(() => { beforeEach(() => {
requestService = getMockRequestService(getRequestEntry$(true)); requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('--a-', {
a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions))
}));
spyOn(rdbService, 'buildList').and.callThrough(); spyOn(rdbService, 'buildList').and.callThrough();
}); });
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
it('should configure a new BrowseEntriesRequest', () => { it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
const expected = new GetRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries.href); const expected = browseDefinitions[1]._links.entries.href;
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
}); a: expected
}));
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id));
expect(rdbService.buildList).toHaveBeenCalled();
}); });
}); });
describe('when getBrowseItemsFor is called with a valid browse definition id', () => { describe('when findList is called with a valid browse definition id', () => {
it('should configure a new BrowseItemsRequest', () => { it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
const expected = new GetRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName); const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName;
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
}); a: expected
}));
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id));
expect(rdbService.buildList).toHaveBeenCalled();
}); });
}); });
@@ -256,29 +245,19 @@ describe('BrowseService', () => {
requestService = getMockRequestService(); requestService = getMockRequestService();
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('--a-', {
a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions))
}));
spyOn(rdbService, 'buildList').and.callThrough(); spyOn(rdbService, 'buildList').and.callThrough();
}); });
describe('when getFirstItemFor is called with a valid browse definition id', () => { describe('when getFirstItemFor is called with a valid browse definition id', () => {
const expectedURL = browseDefinitions[1]._links.items.href + '?page=0&size=1'; const expectedURL = browseDefinitions[1]._links.items.href + '?page=0&size=1';
it('should configure a new BrowseItemsRequest', () => { it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
const expected = new GetRequest(requestService.generateRequestId(), expectedURL);
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe()); scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
}); a: expectedURL
}));
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getFirstItemFor(browseDefinitions[1].id);
expect(rdbService.buildList).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,11 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of as observableOf } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, startWith, take } from 'rxjs/operators'; import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { PaginatedList } from '../data/paginated-list.model'; import { PaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { GetRequest } from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model'; import { BrowseEntry } from '../shared/browse-entry.model';
@@ -21,6 +20,7 @@ import {
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { BrowseDefinitionDataService } from './browse-definition-data.service';
import { HrefOnlyDataService } from '../data/href-only-data.service';
/** /**
* The service handling all browse requests * The service handling all browse requests
@@ -46,6 +46,7 @@ export class BrowseService {
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
private browseDefinitionDataService: BrowseDefinitionDataService, private browseDefinitionDataService: BrowseDefinitionDataService,
private hrefOnlyDataService: HrefOnlyDataService,
private rdb: RemoteDataBuildService, private rdb: RemoteDataBuildService,
) { ) {
} }
@@ -65,7 +66,7 @@ export class BrowseService {
* @param options * @param options
*/ */
getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> { getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
return this.getBrowseDefinitions().pipe( const href$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(options.metadataDefinition), getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(), hasValueOperator(),
map((_links: any) => { map((_links: any) => {
@@ -93,9 +94,9 @@ export class BrowseService {
href = new URLCombiner(href, `?${args.join('&')}`).toString(); href = new URLCombiner(href, `?${args.join('&')}`).toString();
} }
return href; return href;
}), })
getBrowseEntriesFor(this.requestService, this.rdb)
); );
return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(href$);
} }
/** /**
@@ -105,7 +106,7 @@ export class BrowseService {
* @returns {Observable<RemoteData<PaginatedList<Item>>>} * @returns {Observable<RemoteData<PaginatedList<Item>>>}
*/ */
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> { getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getBrowseDefinitions().pipe( const href$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(options.metadataDefinition), getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(), hasValueOperator(),
map((_links: any) => { map((_links: any) => {
@@ -136,8 +137,8 @@ export class BrowseService {
} }
return href; return href;
}), }),
getBrowseItemsFor(this.requestService, this.rdb)
); );
return this.hrefOnlyDataService.findAllByHref<Item>(href$);
} }
/** /**
@@ -146,7 +147,7 @@ export class BrowseService {
* @param scope * @param scope
*/ */
getFirstItemFor(definition: string, scope?: string): Observable<RemoteData<Item>> { getFirstItemFor(definition: string, scope?: string): Observable<RemoteData<Item>> {
return this.getBrowseDefinitions().pipe( const href$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definition), getBrowseDefinitionLinks(definition),
hasValueOperator(), hasValueOperator(),
map((_links: any) => { map((_links: any) => {
@@ -165,11 +166,14 @@ export class BrowseService {
href = new URLCombiner(href, `?${args.join('&')}`).toString(); href = new URLCombiner(href, `?${args.join('&')}`).toString();
} }
return href; return href;
}), })
getBrowseItemsFor(this.requestService, this.rdb), );
return this.hrefOnlyDataService.findAllByHref<Item>(href$).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getFirstOccurrence() getFirstOccurrence()
); );
} }
/** /**
@@ -177,9 +181,7 @@ export class BrowseService {
* @param items * @param items
*/ */
getPrevBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> { getPrevBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
return observableOf(items.payload.prev).pipe( return this.hrefOnlyDataService.findAllByHref<Item>(items.payload.prev);
getBrowseItemsFor(this.requestService, this.rdb)
);
} }
/** /**
@@ -187,9 +189,7 @@ export class BrowseService {
* @param items * @param items
*/ */
getNextBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> { getNextBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
return observableOf(items.payload.next).pipe( return this.hrefOnlyDataService.findAllByHref<Item>(items.payload.next);
getBrowseItemsFor(this.requestService, this.rdb)
);
} }
/** /**
@@ -197,9 +197,7 @@ export class BrowseService {
* @param entries * @param entries
*/ */
getPrevBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> { getPrevBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
return observableOf(entries.payload.prev).pipe( return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(entries.payload.prev);
getBrowseEntriesFor(this.requestService, this.rdb)
);
} }
/** /**
@@ -207,9 +205,7 @@ export class BrowseService {
* @param entries * @param entries
*/ */
getNextBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> { getNextBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
return observableOf(entries.payload.next).pipe( return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(entries.payload.next);
getBrowseEntriesFor(this.requestService, this.rdb)
);
} }
/** /**
@@ -241,39 +237,3 @@ export class BrowseService {
} }
} }
/**
* Operator for turning a href into a PaginatedList of BrowseEntries
* @param requestService
* @param responseCache
* @param rdb
*/
export const getBrowseEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
(source: Observable<string>): Observable<RemoteData<PaginatedList<BrowseEntry>>> => {
const requestId = requestService.generateRequestId();
source.pipe(take(1)).subscribe((href: string) => {
const request = new GetRequest(requestId, href);
requestService.configure(request);
});
return rdb.buildList(source);
};
/**
* Operator for turning a href into a PaginatedList of Items
* @param requestService
* @param responseCache
* @param rdb
*/
export const getBrowseItemsFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
(source: Observable<string>): Observable<RemoteData<PaginatedList<Item>>> => {
const requestId = requestService.generateRequestId();
source.pipe(take(1)).subscribe((href: string) => {
const request = new GetRequest(requestId, href);
requestService.configure(request);
});
return rdb.buildList(source);
};

View File

@@ -38,11 +38,11 @@ class TestModel implements HALResource {
@Injectable() @Injectable()
class TestDataService { class TestDataService {
findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) { findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
return 'findAllByHref'; return 'findAllByHref';
} }
findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) { findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
return 'findByHref'; return 'findByHref';
} }
} }
@@ -90,10 +90,10 @@ xdescribe('LinkService', () => {
propertyName: 'predecessor' propertyName: 'predecessor'
}); });
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
}); });
it('should call dataservice.findByHref with the correct href and nested links', () => { it('should call dataservice.findByHref with the correct href and nested links', () => {
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, followLink('successor')); expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor'));
}); });
}); });
describe(`when the linkdefinition concerns a list`, () => { describe(`when the linkdefinition concerns a list`, () => {
@@ -105,10 +105,10 @@ xdescribe('LinkService', () => {
isList: true isList: true
}); });
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, followLink('successor'))); service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor')));
}); });
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => { it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, followLink('successor')); expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
}); });
}); });
describe('either way', () => { describe('either way', () => {
@@ -119,7 +119,7 @@ xdescribe('LinkService', () => {
propertyName: 'predecessor' propertyName: 'predecessor'
}); });
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
result = service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
}); });
it('should call getLinkDefinition with the correct model and link', () => { it('should call getLinkDefinition with the correct model and link', () => {
@@ -144,7 +144,7 @@ xdescribe('LinkService', () => {
}); });
it('should throw an error', () => { it('should throw an error', () => {
expect(() => { expect(() => {
service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
}).toThrow(); }).toThrow();
}); });
}); });
@@ -160,7 +160,7 @@ xdescribe('LinkService', () => {
}); });
it('should throw an error', () => { it('should throw an error', () => {
expect(() => { expect(() => {
service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
}).toThrow(); }).toThrow();
}); });
}); });

View File

@@ -61,9 +61,9 @@ export class LinkService {
try { try {
if (matchingLinkDef.isList) { if (matchingLinkDef.isList) {
model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, true, ...linkToFollow.linksToFollow); model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
} else { } else {
model[linkToFollow.name] = service.findByHref(href, true, ...linkToFollow.linksToFollow); model[linkToFollow.name] = service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
} }
} catch (e) { } catch (e) {
console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`); console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`);

View File

@@ -523,7 +523,7 @@ describe('RemoteDataBuildService', () => {
let paginatedLinksToFollow; let paginatedLinksToFollow;
beforeEach(() => { beforeEach(() => {
paginatedLinksToFollow = [ paginatedLinksToFollow = [
followLink('page', undefined, true, ...linksToFollow), followLink('page', undefined, true, true, true, ...linksToFollow),
...linksToFollow ...linksToFollow
]; ];
}); });

View File

@@ -271,7 +271,7 @@ export class RemoteDataBuildService {
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/ */
buildList<T extends HALResource>(href$: string | Observable<string>, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> { buildList<T extends HALResource>(href$: string | Observable<string>, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', undefined, false, ...linksToFollow)); return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', undefined, false, true, true, ...linksToFollow));
} }
/** /**

View File

@@ -94,14 +94,16 @@ export class ServerSyncBufferEffects {
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched * @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
*/ */
private applyPatch(href: string): Observable<Action> { private applyPatch(href: string): Observable<Action> {
const patchObject = this.objectCache.getByHref(href).pipe(take(1)); const patchObject = this.objectCache.getByHref(href).pipe(
take(1)
);
return patchObject.pipe( return patchObject.pipe(
map((entry: ObjectCacheEntry) => { map((entry: ObjectCacheEntry) => {
if (isNotEmpty(entry.patches)) { if (isNotEmpty(entry.patches)) {
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
if (isNotEmpty(flatPatch)) { if (isNotEmpty(flatPatch)) {
this.requestService.configure(new PatchRequest(this.requestService.generateRequestId(), href, flatPatch)); this.requestService.send(new PatchRequest(this.requestService.generateRequestId(), href, flatPatch));
} }
} }
return new ApplyPatchObjectCacheAction(href); return new ApplyPatchObjectCacheAction(href);

View File

@@ -58,12 +58,12 @@ describe('ConfigService', () => {
describe('findByHref', () => { describe('findByHref', () => {
it('should configure a new GetRequest', () => { it('should send a new GetRequest', () => {
const expected = new GetRequest(requestService.generateRequestId(), scopedEndpoint); const expected = new GetRequest(requestService.generateRequestId(), scopedEndpoint);
scheduler.schedule(() => service.findByHref(scopedEndpoint).subscribe()); scheduler.schedule(() => service.findByHref(scopedEndpoint).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected, true);
}); });
}); });
}); });

View File

@@ -52,8 +52,8 @@ export abstract class ConfigService {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, this.linkPath); this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, this.linkPath);
} }
public findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ConfigObject>[]): Observable<RemoteData<ConfigObject>> { public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ConfigObject>[]): Observable<RemoteData<ConfigObject>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow).pipe( return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((rd: RemoteData<ConfigObject>) => { map((rd: RemoteData<ConfigObject>) => {
if (rd.hasFailed) { if (rd.hasFailed) {

View File

@@ -34,7 +34,7 @@ export class SubmissionFormsConfigService extends ConfigService {
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionforms'); super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionforms');
} }
public findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SubmissionFormsModel>[]): Observable<RemoteData<SubmissionFormsModel>> { public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SubmissionFormsModel>[]): Observable<RemoteData<SubmissionFormsModel>> {
return super.findByHref(href, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionFormsModel>>; return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionFormsModel>>;
} }
} }

View File

@@ -36,7 +36,7 @@ export class SubmissionUploadsConfigService extends ConfigService {
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionuploads'); super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionuploads');
} }
findByHref(href: string, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionUploadsModel>> { findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionUploadsModel>> {
return super.findByHref(href, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionUploadsModel>>; return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionUploadsModel>>;
} }
} }

View File

@@ -2,7 +2,11 @@ import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import {
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { Action, StoreConfig, StoreModule } from '@ngrx/store';
@@ -51,14 +55,12 @@ import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
import { DSOResponseParsingService } from './data/dso-response-parsing.service'; import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service';
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
import { ItemTypeDataService } from './data/entity-type-data.service';
import { EntityTypeService } from './data/entity-type.service'; import { EntityTypeService } from './data/entity-type.service';
import { ExternalSourceService } from './data/external-source.service'; import { ExternalSourceService } from './data/external-source.service';
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service'; import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service';
import { ItemDataService } from './data/item-data.service'; import { ItemDataService } from './data/item-data.service';
import { LicenseDataService } from './data/license-data.service';
import { LookupRelationService } from './data/lookup-relation.service'; import { LookupRelationService } from './data/lookup-relation.service';
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
@@ -263,8 +265,6 @@ const PROVIDERS = [
LookupRelationService, LookupRelationService,
VersionDataService, VersionDataService,
VersionHistoryDataService, VersionHistoryDataService,
LicenseDataService,
ItemTypeDataService,
WorkflowActionDataService, WorkflowActionDataService,
ProcessDataService, ProcessDataService,
ScriptDataService, ScriptDataService,

View File

@@ -55,8 +55,8 @@ describe('BitstreamDataService', () => {
service.updateFormat(bitstream, format); service.updateFormat(bitstream, format);
}); });
it('should configure a put request', () => { it('should send a put request', () => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
}); });
}); });
}); });

View File

@@ -25,7 +25,7 @@ import { RequestService } from './request.service';
import { BitstreamFormatDataService } from './bitstream-format-data.service'; import { BitstreamFormatDataService } from './bitstream-format-data.service';
import { BitstreamFormat } from '../shared/bitstream-format.model'; import { BitstreamFormat } from '../shared/bitstream-format.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { configureRequest } from '../shared/operators'; import { sendRequest } from '../shared/operators';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { RequestEntryState } from './request.reducer'; import { RequestEntryState } from './request.reducer';
@@ -64,13 +64,15 @@ export class BitstreamDataService extends DataService<Bitstream> {
* *
* @param bundle the bundle to retrieve bitstreams from * @param bundle the bundle to retrieve bitstreams from
* @param options options for the find all request * @param options options for the find all request
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findAllByBundle(bundle: Bundle, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> { findAllByBundle(bundle: Bundle, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
return this.findAllByHref(bundle._links.bitstreams.href, options, reRequestOnStale, ...linksToFollow); return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -120,7 +122,7 @@ export class BitstreamDataService extends DataService<Bitstream> {
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe( return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
switchMap((bundleRD: RemoteData<Bundle>) => { switchMap((bundleRD: RemoteData<Bundle>) => {
if (isNotEmpty(bundleRD.payload)) { if (isNotEmpty(bundleRD.payload)) {
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 9999 }).pipe(
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => { map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) { if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) => const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) =>
@@ -166,18 +168,21 @@ export class BitstreamDataService extends DataService<Bitstream> {
* in all current use cases, and having it simplifies this method * in all current use cases, and having it simplifies this method
* *
* @param item the {@link Item} the {@link Bundle} is a part of * @param item the {@link Item} the {@link Bundle} is a part of
* @param bundleName the name of the {@link Bundle} we want to find {@link Bitstream}s for * @param bundleName the name of the {@link Bundle} we want to find
* {@link Bitstream}s for
* @param options the {@link FindListOptions} for the request * @param options the {@link FindListOptions} for the request
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> { public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
return this.bundleService.findByItemAndName(item, bundleName).pipe( return this.bundleService.findByItemAndName(item, bundleName).pipe(
switchMap((bundleRD: RemoteData<Bundle>) => { switchMap((bundleRD: RemoteData<Bundle>) => {
if (bundleRD.hasSucceeded && hasValue(bundleRD.payload)) { if (bundleRD.hasSucceeded && hasValue(bundleRD.payload)) {
return this.findAllByBundle(bundleRD.payload, options, reRequestOnStale, ...linksToFollow); return this.findAllByBundle(bundleRD.payload, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} else if (!bundleRD.hasSucceeded && bundleRD.statusCode === 404) { } else if (!bundleRD.hasSucceeded && bundleRD.statusCode === 404) {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []), new Date().getTime()); return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []), new Date().getTime());
} else { } else {
@@ -209,7 +214,7 @@ export class BitstreamDataService extends DataService<Bitstream> {
options.headers = headers; options.headers = headers;
return new PutRequest(requestId, bitstreamHref, formatHref, options); return new PutRequest(requestId, bitstreamHref, formatHref, options);
}), }),
configureRequest(this.requestService), sendRequest(this.requestService),
take(1) take(1)
).subscribe(() => { ).subscribe(() => {
this.requestService.removeByHrefSubstring(bitstream.self + '/format'); this.requestService.removeByHrefSubstring(bitstream.self + '/format');

View File

@@ -73,7 +73,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -93,7 +93,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -115,7 +115,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -136,7 +136,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -160,7 +160,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -183,7 +183,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -206,7 +206,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -228,7 +228,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -250,7 +250,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: cold('a', { a: responseCacheEntry }), getByUUID: cold('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',
@@ -270,7 +270,7 @@ describe('BitstreamFormatDataService', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
configure: {}, send: {},
getByHref: observableOf(responseCacheEntry), getByHref: observableOf(responseCacheEntry),
getByUUID: hot('a', { a: responseCacheEntry }), getByUUID: hot('a', { a: responseCacheEntry }),
generateRequestId: 'request-id', generateRequestId: 'request-id',

View File

@@ -19,7 +19,7 @@ import { BitstreamFormat } from '../shared/bitstream-format.model';
import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type';
import { Bitstream } from '../shared/bitstream.model'; import { Bitstream } from '../shared/bitstream.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { configureRequest } from '../shared/operators'; import { sendRequest } from '../shared/operators';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
@@ -82,7 +82,7 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => map((endpointURL: string) =>
new PutRequest(requestId, endpointURL, bitstreamFormat)), new PutRequest(requestId, endpointURL, bitstreamFormat)),
configureRequest(this.requestService)).subscribe(); sendRequest(this.requestService)).subscribe();
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
@@ -99,7 +99,7 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
map((endpointURL: string) => { map((endpointURL: string) => {
return new PostRequest(requestId, endpointURL, bitstreamFormat); return new PostRequest(requestId, endpointURL, bitstreamFormat);
}), }),
configureRequest(this.requestService) sendRequest(this.requestService)
).subscribe(); ).subscribe();
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);

View File

@@ -81,7 +81,7 @@ describe('BundleDataService', () => {
}); });
it('should call findAllByHref with the item\'s bundles link', () => { it('should call findAllByHref with the item\'s bundles link', () => {
expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined, true); expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined, true, true);
}); });
}); });

View File

@@ -52,12 +52,15 @@ export class BundleDataService extends DataService<Bundle> {
* *
* @param item the {@link Item} the {@link Bundle}s are a part of * @param item the {@link Item} the {@link Bundle}s are a part of
* @param options the {@link FindListOptions} for the request * @param options the {@link FindListOptions} for the request
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow the {@link FollowLinkConfig}s for the request * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findAllByItem(item: Item, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<PaginatedList<Bundle>>> { findAllByItem(item: Item, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<PaginatedList<Bundle>>> {
return this.findAllByHref(item._links.bundles.href, options, reRequestOnStale, ...linksToFollow); return this.findAllByHref(item._links.bundles.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -65,13 +68,16 @@ export class BundleDataService extends DataService<Bundle> {
* *
* @param item the {@link Item} the {@link Bundle}s are a part of * @param item the {@link Item} the {@link Bundle}s are a part of
* @param bundleName the name of the {@link Bundle} to retrieve * @param bundleName the name of the {@link Bundle} to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow the {@link FollowLinkConfig}s for the request * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
// TODO should be implemented rest side // TODO should be implemented rest side
findByItemAndName(item: Item, bundleName: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> { findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }, reRequestOnStale, ...linksToFollow).pipe( return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
map((rd: RemoteData<PaginatedList<Bundle>>) => { map((rd: RemoteData<PaginatedList<Bundle>>) => {
if (hasValue(rd.payload) && hasValue(rd.payload.page)) { if (hasValue(rd.payload) && hasValue(rd.payload.page)) {
const matchingBundle = rd.payload.page.find((bundle: Bundle) => const matchingBundle = rd.payload.page.find((bundle: Bundle) =>
@@ -129,7 +135,7 @@ export class BundleDataService extends DataService<Bundle> {
take(1) take(1)
).subscribe((href) => { ).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href); const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request); this.requestService.send(request, true);
}); });
return this.rdbService.buildList<Bitstream>(hrefObs, ...linksToFollow); return this.rdbService.buildList<Bitstream>(hrefObs, ...linksToFollow);

View File

@@ -6,7 +6,7 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { fakeAsync, tick } from '@angular/core/testing'; import { fakeAsync, tick } from '@angular/core/testing';
import { ContentSourceRequest, GetRequest, UpdateContentSourceRequest } from './request.models'; import { ContentSourceRequest, UpdateContentSourceRequest } from './request.models';
import { ContentSource } from '../shared/content-source.model'; import { ContentSource } from '../shared/content-source.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -87,10 +87,10 @@ describe('CollectionDataService', () => {
contentSource$ = service.getContentSource(collectionId); contentSource$ = service.getContentSource(collectionId);
}); });
it('should configure a new ContentSourceRequest', fakeAsync(() => { it('should send a new ContentSourceRequest', fakeAsync(() => {
contentSource$.subscribe(); contentSource$.subscribe();
tick(); tick();
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(ContentSourceRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(ContentSourceRequest), true);
})); }));
}); });
@@ -103,25 +103,13 @@ describe('CollectionDataService', () => {
returnedContentSource$ = service.updateContentSource(collectionId, contentSource); returnedContentSource$ = service.updateContentSource(collectionId, contentSource);
}); });
it('should configure a new UpdateContentSourceRequest', fakeAsync(() => { it('should send a new UpdateContentSourceRequest', fakeAsync(() => {
returnedContentSource$.subscribe(); returnedContentSource$.subscribe();
tick(); tick();
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest));
})); }));
}); });
describe('getMappedItems', () => {
let result;
beforeEach(() => {
result = service.getMappedItems('collection-id');
});
it('should configure a GET request', () => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
});
});
describe('when calling getAuthorizedCollection', () => { describe('when calling getAuthorizedCollection', () => {
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
@@ -175,10 +163,10 @@ describe('CollectionDataService', () => {
returnedContentSource$ = service.updateContentSource(collectionId, contentSource); returnedContentSource$ = service.updateContentSource(collectionId, contentSource);
}); });
it('should configure a new UpdateContentSourceRequest', fakeAsync(() => { it('should send a new UpdateContentSourceRequest', fakeAsync(() => {
returnedContentSource$.subscribe(); returnedContentSource$.subscribe();
tick(); tick();
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest));
})); }));
it('should display an error notification', fakeAsync(() => { it('should display an error notification', fakeAsync(() => {

View File

@@ -3,12 +3,11 @@ import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
import { INotification } from '../../shared/notifications/models/notification.model'; import { INotification } from '../../shared/notifications/models/notification.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -20,26 +19,19 @@ import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { COLLECTION } from '../shared/collection.resource-type'; import { COLLECTION } from '../shared/collection.resource-type';
import { ContentSource } from '../shared/content-source.model'; import { ContentSource } from '../shared/content-source.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { GenericConstructor } from '../shared/generic-constructor';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { import { getFirstCompletedRemoteData } from '../shared/operators';
configureRequest,
getFirstCompletedRemoteData
} from '../shared/operators';
import { ComColDataService } from './comcol-data.service'; import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { ResponseParsingService } from './parsing.service';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { import {
ContentSourceRequest, ContentSourceRequest,
FindListOptions, FindListOptions,
GetRequest, UpdateContentSourceRequest,
UpdateContentSourceRequest RestRequest
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { BitstreamDataService } from './bitstream-data.service'; import { BitstreamDataService } from './bitstream-data.service';
@@ -70,20 +62,25 @@ export class CollectionDataService extends ComColDataService<Collection> {
/** /**
* Get all collections the user is authorized to submit to * Get all collections the user is authorized to submit to
* *
* @param query limit the returned collection to those with metadata values matching the query terms. * @param query limit the returned collection to those with metadata values
* matching the query terms.
* @param options The [[FindListOptions]] object * @param options The [[FindListOptions]] object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return Observable<RemoteData<PaginatedList<Collection>>> * @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list * collection list
*/ */
getAuthorizedCollection(query: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> { getAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findSubmitAuthorized'; const searchHref = 'findSubmitAuthorized';
options = Object.assign({}, options, { options = Object.assign({}, options, {
searchParams: [new RequestParam('query', query)] searchParams: [new RequestParam('query', query)]
}); });
return this.searchBy(searchHref, options, reRequestOnStale, ...linksToFollow).pipe( return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending)); filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
} }
@@ -149,7 +146,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
href$.subscribe((href: string) => { href$.subscribe((href: string) => {
const request = new ContentSourceRequest(this.requestService.generateRequestId(), href); const request = new ContentSourceRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request); this.requestService.send(request, true);
}); });
return this.rdbService.buildSingle<ContentSource>(href$); return this.rdbService.buildSingle<ContentSource>(href$);
@@ -175,9 +172,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
); );
// Execute the post/put request // Execute the post/put request
request$.pipe( request$.subscribe((request: RestRequest) => this.requestService.send(request));
configureRequest(this.requestService)
).subscribe();
// Return updated ContentSource // Return updated ContentSource
return this.rdbService.buildFromRequestUUID<ContentSource>(requestId).pipe( return this.rdbService.buildFromRequestUUID<ContentSource>(requestId).pipe(
@@ -205,48 +200,6 @@ export class CollectionDataService extends ComColDataService<Collection> {
); );
} }
/**
* Fetches the endpoint used for mapping items to a collection
* @param collectionId The id of the collection to map items to
*/
getMappedItemsEndpoint(collectionId): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
map((endpoint: string) => this.getIDHref(endpoint, collectionId)),
map((endpoint: string) => `${endpoint}/mappedItems`)
);
}
/**
* Fetches a list of items that are mapped to a collection
* @param collectionId The id of the collection
* @param searchOptions Search options to sort or filter out items
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
getMappedItems(collectionId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
const requestUuid = this.requestService.generateRequestId();
const href$ = this.getMappedItemsEndpoint(collectionId).pipe(
isNotEmptyOperator(),
distinctUntilChanged(),
map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint)
);
href$.pipe(
map((endpoint: string) => {
const request = new GetRequest(requestUuid, endpoint);
return Object.assign(request, {
responseMsToLive: 0,
getResponseParser(): GenericConstructor<ResponseParsingService> {
return DSOResponseParsingService;
}
});
}),
configureRequest(this.requestService)
).subscribe();
return this.rdbService.buildList(href$, ...linksToFollow);
}
protected getFindByParentHref(parentUUID: string): Observable<string> { protected getFindByParentHref(parentUUID: string): Observable<string> {
return this.halService.getEndpoint('communities').pipe( return this.halService.getEndpoint('communities').pipe(
switchMap((communityEndpointHref: string) => switchMap((communityEndpointHref: string) =>
@@ -261,5 +214,4 @@ export class CollectionDataService extends ComColDataService<Collection> {
findOwningCollectionFor(item: Item): Observable<RemoteData<Collection>> { findOwningCollectionFor(item: Item): Observable<RemoteData<Collection>> {
return this.findByHref(item._links.owningCollection.href); return this.findByHref(item._links.owningCollection.href);
} }
} }

View File

@@ -13,11 +13,12 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ComColDataService } from './comcol-data.service'; import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { FindByIDRequest, FindListOptions } from './request.models'; import { FindListOptions, GetRequest } from './request.models';
import { RequestEntry } from './request.reducer'; import { RequestEntry } from './request.reducer';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { import {
createFailedRemoteDataObject$, createNoContentRemoteDataObject$, createFailedRemoteDataObject$,
createNoContentRemoteDataObject$,
createSuccessfulRemoteDataObject$ createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { BitstreamDataService } from './bitstream-data.service'; import { BitstreamDataService } from './bitstream-data.service';
@@ -147,18 +148,18 @@ describe('ComColDataService', () => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
}); });
it('should configure a new FindByIDRequest for the scope Community', () => { it('should send a new FindByIDRequest for the scope Community', () => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = getMockRequestService(getRequestEntry$(true)); requestService = getMockRequestService(getRequestEntry$(true));
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
service = initTestService(); service = initTestService();
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); const expected = new GetRequest(requestService.generateRequestId(), communityEndpoint);
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected, true);
}); });
describe('if the scope Community can\'t be found', () => { describe('if the scope Community can\'t be found', () => {

View File

@@ -1,4 +1,4 @@
import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
@@ -7,7 +7,7 @@ import { HALLink } from '../shared/hal-link.model';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { FindByIDRequest, FindListOptions } from './request.models'; import { FindListOptions } from './request.models';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -42,11 +42,10 @@ export abstract class ComColDataService<T extends Community | Collection> extend
const scopeCommunityHrefObs = this.cds.getEndpoint().pipe( const scopeCommunityHrefObs = this.cds.getEndpoint().pipe(
map((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)), map((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)),
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
take(1), take(1)
tap((href: string) => { );
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID);
this.requestService.configure(request); this.createAndSendGetRequest(scopeCommunityHrefObs, true);
}));
return scopeCommunityHrefObs.pipe( return scopeCommunityHrefObs.pipe(
switchMap((href: string) => this.rdbService.buildSingle<Community>(href)), switchMap((href: string) => this.rdbService.buildSingle<Community>(href)),
@@ -71,7 +70,7 @@ export abstract class ComColDataService<T extends Community | Collection> extend
const href$ = this.getFindByParentHref(parentUUID).pipe( const href$ = this.getFindByParentHref(parentUUID).pipe(
map((href: string) => this.buildHrefFromFindOptions(href, options)) map((href: string) => this.buildHrefFromFindOptions(href, options))
); );
return this.findList(href$, options); return this.findAllByHref(href$);
} }
/** /**

View File

@@ -3,8 +3,7 @@ import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, switchMap, take } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -17,7 +16,7 @@ import { ComColDataService } from './comcol-data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { FindListOptions, FindListRequest } from './request.models'; import { FindListOptions } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { BitstreamDataService } from './bitstream-data.service'; import { BitstreamDataService } from './bitstream-data.service';
@@ -48,15 +47,7 @@ export class CommunityDataService extends ComColDataService<Community> {
findTop(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Community>>> { findTop(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Community>>> {
const hrefObs = this.getFindAllHref(options, this.topLinkPath); const hrefObs = this.getFindAllHref(options, this.topLinkPath);
hrefObs.pipe( return this.findAllByHref(hrefObs, undefined);
filter((href: string) => hasValue(href)),
take(1))
.subscribe((href: string) => {
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request);
});
return this.rdbService.buildList<Community>(hrefObs) as Observable<RemoteData<PaginatedList<Community>>>;
} }
protected getFindByParentHref(parentUUID: string): Observable<string> { protected getFindByParentHref(parentUUID: string): Observable<string> {

View File

@@ -2,7 +2,7 @@ import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindByIDRequest } from './request.models'; import { GetRequest } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -34,7 +34,7 @@ describe('ConfigurationDataService', () => {
}); });
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID, generateRequestId: requestUUID,
configure: true send: true
}); });
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: cold('a', { buildSingle: cold('a', {
@@ -67,11 +67,11 @@ describe('ConfigurationDataService', () => {
expect(halService.getEndpoint).toHaveBeenCalledWith('properties'); expect(halService.getEndpoint).toHaveBeenCalledWith('properties');
}); });
it('should configure the proper FindByIDRequest', () => { it('should send the proper FindByIDRequest', () => {
scheduler.schedule(() => service.findByPropertyName(testObject.name)); scheduler.schedule(() => service.findByPropertyName(testObject.name));
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.name)); expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL), true);
}); });
it('should return a RemoteData<ConfigurationProperty> for the object with the given name', () => { it('should return a RemoteData<ConfigurationProperty> for the object with the given name', () => {

View File

@@ -212,7 +212,7 @@ describe('DataService', () => {
it('should include nested linksToFollow 3lvl', () => { it('should include nested linksToFollow 3lvl', () => {
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`; const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
(service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))).subscribe((value) => { (service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships')))).subscribe((value) => {
expect(value).toBe(expected); expect(value).toBe(expected);
}); });
}); });
@@ -247,7 +247,7 @@ describe('DataService', () => {
it('should include nested linksToFollow 3lvl', () => { it('should include nested linksToFollow 3lvl', () => {
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`; const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))); const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, true, true,followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
}); });
@@ -268,8 +268,8 @@ describe('DataService', () => {
service.patch(dso, operations); service.patch(dso, operations);
}); });
it('should configure a PatchRequest', () => { it('should send a PatchRequest', () => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PatchRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PatchRequest));
}); });
}); });

View File

@@ -1,16 +1,17 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { import {
distinctUntilChanged, distinctUntilChanged,
filter, filter,
find, find,
first,
map, map,
mergeMap, mergeMap,
take, take,
takeWhile, switchMap, tap, takeWhile,
switchMap,
tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
@@ -25,22 +26,18 @@ import { CoreState } from '../core.reducers';
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { import { getRemoteDataPayload, getFirstSucceededRemoteData, } from '../shared/operators';
getRemoteDataPayload,
getFirstSucceededRemoteData,
} from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { ChangeAnalyzer } from './change-analyzer'; import { ChangeAnalyzer } from './change-analyzer';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { import {
CreateRequest, CreateRequest,
FindByIDRequest,
FindListOptions,
FindListRequest,
GetRequest, GetRequest,
FindListOptions,
PatchRequest, PatchRequest,
PutRequest, DeleteRequest PutRequest,
DeleteRequest
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
@@ -159,23 +156,23 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
args = [...args, `page=${options.currentPage - 1}`]; args = this.addHrefArg(href, args, `page=${options.currentPage - 1}`);
} }
if (hasValue(options.elementsPerPage)) { if (hasValue(options.elementsPerPage)) {
args = [...args, `size=${options.elementsPerPage}`]; args = this.addHrefArg(href, args, `size=${options.elementsPerPage}`);
} }
if (hasValue(options.sort)) { if (hasValue(options.sort)) {
args = [...args, `sort=${options.sort.field},${options.sort.direction}`]; args = this.addHrefArg(href, args, `sort=${options.sort.field},${options.sort.direction}`);
} }
if (hasValue(options.startsWith)) { if (hasValue(options.startsWith)) {
args = [...args, `startsWith=${options.startsWith}`]; args = this.addHrefArg(href, args, `startsWith=${options.startsWith}`);
} }
if (hasValue(options.searchParams)) { if (hasValue(options.searchParams)) {
options.searchParams.forEach((param: RequestParam) => { options.searchParams.forEach((param: RequestParam) => {
args = [...args, `${param.fieldName}=${param.fieldValue}`]; args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
}); });
} }
args = this.addEmbedParams(args, ...linksToFollow); args = this.addEmbedParams(href, args, ...linksToFollow);
if (isNotEmpty(args)) { if (isNotEmpty(args)) {
return new URLCombiner(href, `?${args.join('&')}`).toString(); return new URLCombiner(href, `?${args.join('&')}`).toString();
} else { } else {
@@ -198,11 +195,11 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
let args = []; let args = [];
if (hasValue(params)) { if (hasValue(params)) {
params.forEach((param: RequestParam) => { params.forEach((param: RequestParam) => {
args.push(`${param.fieldName}=${param.fieldValue}`); args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
}); });
} }
args = this.addEmbedParams(args, ...linksToFollow); args = this.addEmbedParams(href, args, ...linksToFollow);
if (isNotEmpty(args)) { if (isNotEmpty(args)) {
return new URLCombiner(href, `?${args.join('&')}`).toString(); return new URLCombiner(href, `?${args.join('&')}`).toString();
@@ -212,20 +209,39 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
} }
/** /**
* Adds the embed options to the link for the request * Adds the embed options to the link for the request
* @param href The href the params are to be added to
* @param args params for the query string * @param args params for the query string
* @param linksToFollow links we want to embed in query string if shouldEmbed is true * @param linksToFollow links we want to embed in query string if shouldEmbed is true
*/ */
protected addEmbedParams(args: string[], ...linksToFollow: FollowLinkConfig<T>[]) { protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]) {
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => { linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
if (linkToFollow !== undefined && linkToFollow.shouldEmbed) { if (linkToFollow !== undefined && linkToFollow.shouldEmbed) {
const embedString = 'embed=' + String(linkToFollow.name); const embedString = 'embed=' + String(linkToFollow.name);
const embedWithNestedString = this.addNestedEmbeds(embedString, ...linkToFollow.linksToFollow); const embedWithNestedString = this.addNestedEmbeds(embedString, ...linkToFollow.linksToFollow);
args = [...args, embedWithNestedString]; args = this.addHrefArg(href, args, embedWithNestedString);
} }
}); });
return args; return args;
} }
/**
* Add a new argument to the list of arguments, only if it doesn't already exist in the given href,
* or the current list of arguments
*
* @param href The href the arguments are to be added to
* @param currentArgs The current list of arguments
* @param newArg The new argument to add
* @return The next list of arguments, with newArg included if it wasn't already.
* Note this function will not modify any of the input params.
*/
protected addHrefArg(href: string, currentArgs: string[], newArg: string): string[] {
if (href.includes(newArg) || currentArgs.includes(newArg)) {
return [...currentArgs];
} else {
return [...currentArgs, newArg];
}
}
/** /**
* Add the nested followLinks to the embed param, recursively, separated by a / * Add the nested followLinks to the embed param, recursively, separated by a /
* @param embedString embedString so far (recursive) * @param embedString embedString so far (recursive)
@@ -249,44 +265,17 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
* info should be added to the objects * info should be added to the objects
* *
* @param options Find list options object * @param options Find list options object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>>} * @return {Observable<RemoteData<PaginatedList<T>>>}
* Return an observable that emits object list * Return an observable that emits object list
*/ */
findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> { findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
return this.findList(this.getFindAllHref(options), options, reRequestOnStale, ...linksToFollow); return this.findAllByHref(this.getFindAllHref(options), options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Returns an observable of {@link RemoteData} of an object, based on href observable,
* with a list of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
* @param href$ Observable of href of object we want to retrieve
* @param options Find list options object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
* the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s
* should be automatically resolved
*/
protected findList(href$, options: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]) {
const requestId = this.requestService.generateRequestId();
href$.pipe(
first((href: string) => hasValue(href)))
.subscribe((href: string) => {
const request = new FindListRequest(requestId, href, options);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request);
});
return this.rdbService.buildList<T>(href$, ...linksToFollow).pipe(
reRequestStaleRemoteData(reRequestOnStale, () =>
this.findList(href$, options, reRequestOnStale, ...linksToFollow))
) as Observable<RemoteData<PaginatedList<T>>>;
} }
/** /**
@@ -313,78 +302,107 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
* @param id ID of object we want to retrieve * @param id ID of object we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findById(id: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> { findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
const requestId = this.requestService.generateRequestId(); const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow);
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow).pipe(
isNotEmptyOperator(),
take(1)
);
href$.subscribe((href: string) => {
const request = new FindByIDRequest(requestId, href, id);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request);
});
return this.rdbService.buildSingle<T>(href$, ...linksToFollow).pipe(
reRequestStaleRemoteData(reRequestOnStale, () =>
this.findById(id, reRequestOnStale, ...linksToFollow))
);
} }
/** /**
* Returns an observable of {@link RemoteData} of an object, based on an href, with a list of * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
* @param href The url of object we want to retrieve * @param href$ The url of object we want to retrieve. Can be a string or
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * an Observable<string>
* the response becomes stale * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * no valid cached version. Defaults to true
* should be automatically resolved * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> { findByHref(href$: string | Observable<string>, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
const requestHref = this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow); if (typeof href$ === 'string') {
const requestId = this.requestService.generateRequestId(); href$ = observableOf(href$);
const request = new GetRequest(requestId, requestHref);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
} }
this.requestService.configure(request);
return this.rdbService.buildSingle<T>(href, ...linksToFollow).pipe( const requestHref$ = href$.pipe(
isNotEmptyOperator(),
take(1),
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow))
);
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
return this.rdbService.buildSingle<T>(href$, ...linksToFollow).pipe(
reRequestStaleRemoteData(reRequestOnStale, () => reRequestStaleRemoteData(reRequestOnStale, () =>
this.findByHref(href, reRequestOnStale, ...linksToFollow)) this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow))
); );
} }
/** /**
* Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list
* of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object * of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
* @param href The url of object we want to retrieve * @param href$ The url of object we want to retrieve. Can be a string or
* an Observable<string>
* @param findListOptions Find list options object * @param findListOptions Find list options object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> { findAllByHref(href$: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
const requestHref = this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow); if (typeof href$ === 'string') {
href$ = observableOf(href$);
}
const requestHref$ = href$.pipe(
isNotEmptyOperator(),
take(1),
map((href: string) => this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow))
);
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
return this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
reRequestStaleRemoteData(reRequestOnStale, () =>
this.findAllByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow))
);
}
/**
* Create a GET request for the given href, and send it.
*
* @param href$ The url of object we want to retrieve. Can be a string or
* an Observable<string>
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
*/
protected createAndSendGetRequest(href$: string | Observable<string>, useCachedVersionIfAvailable = true): void {
if (isNotEmpty(href$)) {
if (typeof href$ === 'string') {
href$ = observableOf(href$);
}
href$.pipe(
isNotEmptyOperator(),
take(1)
).subscribe((href: string) => {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const request = new GetRequest(requestId, requestHref); const request = new GetRequest(requestId, href);
if (hasValue(this.responseMsToLive)) { if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive; request.responseMsToLive = this.responseMsToLive;
} }
this.requestService.configure(request); this.requestService.send(request, useCachedVersionIfAvailable);
return this.rdbService.buildList<T>(requestHref, ...linksToFollow).pipe( });
reRequestStaleRemoteData(reRequestOnStale, () => }
this.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow))
);
} }
/** /**
@@ -403,30 +421,19 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
* *
* @param searchMethod The search method for the object * @param searchMethod The search method for the object
* @param options The [[FindListOptions]] object * @param options The [[FindListOptions]] object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow The array of [[FollowLinkConfig]] * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>} * @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server * Return an observable that emits response from the server
*/ */
searchBy(searchMethod: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> { searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
const requestId = this.requestService.generateRequestId();
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
hrefObs.pipe( return this.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
find((href: string) => hasValue(href))
).subscribe((href: string) => {
const request = new FindListRequest(requestId, href, options);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request);
});
return this.rdbService.buildList(hrefObs, ...linksToFollow).pipe(
reRequestStaleRemoteData(reRequestOnStale, () =>
this.searchBy(searchMethod, options, reRequestOnStale, ...linksToFollow))
);
} }
/** /**
@@ -447,14 +454,14 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
if (hasValue(this.responseMsToLive)) { if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive; request.responseMsToLive = this.responseMsToLive;
} }
this.requestService.configure(request); this.requestService.send(request);
}); });
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
createPatchFromCache(object: T): Observable<Operation[]> { createPatchFromCache(object: T): Observable<Operation[]> {
const oldVersion$ = this.findByHref(object._links.self.href, false); const oldVersion$ = this.findByHref(object._links.self.href, true, false);
return oldVersion$.pipe( return oldVersion$.pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
@@ -475,7 +482,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
request.responseMsToLive = this.responseMsToLive; request.responseMsToLive = this.responseMsToLive;
} }
this.requestService.configure(request); this.requestService.send(request);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
@@ -492,7 +499,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
if (isNotEmpty(operations)) { if (isNotEmpty(operations)) {
this.objectCache.addPatch(object._links.self.href, operations); this.objectCache.addPatch(object._links.self.href, operations);
} }
return this.findByHref(object._links.self.href, true); return this.findByHref(object._links.self.href, true, true);
} }
) )
); );
@@ -524,7 +531,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
if (hasValue(this.responseMsToLive)) { if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive; request.responseMsToLive = this.responseMsToLive;
} }
this.requestService.configure(request); this.requestService.send(request);
}); });
const result$ = this.rdbService.buildFromRequestUUID<T>(requestId); const result$ = this.rdbService.buildFromRequestUUID<T>(requestId);
@@ -579,7 +586,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
if (hasValue(this.responseMsToLive)) { if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive; request.responseMsToLive = this.responseMsToLive;
} }
this.requestService.configure(request); this.requestService.send(request);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }

View File

@@ -9,7 +9,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DsoRedirectDataService } from './dso-redirect-data.service'; import { DsoRedirectDataService } from './dso-redirect-data.service';
import { FindByIDRequest, IdentifierType } from './request.models'; import { GetRequest, IdentifierType } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
@@ -42,7 +42,7 @@ describe('DsoRedirectDataService', () => {
}); });
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID, generateRequestId: requestUUID,
configure: true send: true
}); });
router = { router = {
navigate: jasmine.createSpy('navigate') navigate: jasmine.createSpy('navigate')
@@ -93,18 +93,18 @@ describe('DsoRedirectDataService', () => {
expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); expect(halService.getEndpoint).toHaveBeenCalledWith('dso');
}); });
it('should configure the proper FindByIDRequest for uuid', () => { it('should send the proper FindByIDRequest for uuid', () => {
scheduler.schedule(() => service.findByIdAndIDType(dsoUUID, IdentifierType.UUID)); scheduler.schedule(() => service.findByIdAndIDType(dsoUUID, IdentifierType.UUID));
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestUUIDURL, dsoUUID)); expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestUUIDURL), true);
}); });
it('should configure the proper FindByIDRequest for handle', () => { it('should send the proper FindByIDRequest for handle', () => {
scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE)); scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE));
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle)); expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestHandleURL), true);
}); });
it('should navigate to item route', () => { it('should navigate to item route', () => {
@@ -162,7 +162,7 @@ describe('DsoRedirectDataService', () => {
it('should include nested linksToFollow 3lvl', () => { it('should include nested linksToFollow 3lvl', () => {
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`; const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))); const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
}); });

View File

@@ -14,9 +14,10 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { FindByIDRequest, IdentifierType } from './request.models'; import { IdentifierType } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { getFirstCompletedRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
import { DSpaceObject } from '../shared/dspace-object.model';
@Injectable() @Injectable()
export class DsoRedirectDataService extends DataService<any> { export class DsoRedirectDataService extends DataService<any> {
@@ -53,7 +54,7 @@ export class DsoRedirectDataService extends DataService<any> {
{}, [], ...linksToFollow); {}, [], ...linksToFollow);
} }
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> { findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<DSpaceObject>> {
this.setLinkPath(identifierType); this.setLinkPath(identifierType);
return this.findById(id).pipe( return this.findById(id).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),

View File

@@ -3,7 +3,7 @@ import { TestScheduler } from 'rxjs/testing';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindByIDRequest } from './request.models'; import { GetRequest } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { DSpaceObjectDataService } from './dspace-object-data.service'; import { DSpaceObjectDataService } from './dspace-object-data.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
@@ -32,7 +32,7 @@ describe('DSpaceObjectDataService', () => {
}); });
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID, generateRequestId: requestUUID,
configure: true send: true
}); });
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: cold('a', { buildSingle: cold('a', {
@@ -65,11 +65,11 @@ describe('DSpaceObjectDataService', () => {
expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); expect(halService.getEndpoint).toHaveBeenCalledWith('dso');
}); });
it('should configure the proper FindByIDRequest', () => { it('should send the proper FindByIDRequest', () => {
scheduler.schedule(() => service.findById(testObject.uuid)); scheduler.schedule(() => service.findById(testObject.uuid));
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid)); expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL), true);
}); });
it('should return a RemoteData<DSpaceObject> for the object with the given ID', () => { it('should return a RemoteData<DSpaceObject> for the object with the given ID', () => {

View File

@@ -61,24 +61,30 @@ export class DSpaceObjectDataService {
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of {@link FollowLinkConfig}, * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the object * to automatically resolve {@link HALLink}s of the object
* @param id ID of object we want to retrieve * @param id ID of object we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findById(id: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> { findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
return this.dataService.findById(id, reRequestOnStale, ...linksToFollow); return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
* Returns an observable of {@link RemoteData} of an object, based on an href, with a list of {@link FollowLinkConfig}, * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the object * to automatically resolve {@link HALLink}s of the object
* @param href The url of object we want to retrieve * @param href The url of object we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> { findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -86,12 +92,15 @@ export class DSpaceObjectDataService {
* to automatically resolve {@link HALLink}s of the object * to automatically resolve {@link HALLink}s of the object
* @param href The url of object we want to retrieve * @param href The url of object we want to retrieve
* @param findListOptions Find list options object * @param findListOptions Find list options object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<PaginatedList<DSpaceObject>>> { findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
} }

View File

@@ -1,89 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { 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 { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ItemType } from '../shared/item-relationships/item-type.model';
import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type';
import { DataService } from './data.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import { FindListOptions } from './request.models';
import { RequestService } from './request.service';
/* tslint:disable:max-classes-per-file */
/**
* A private DataService implementation to delegate specific methods to.
*/
class DataServiceImpl extends DataService<ItemType> {
protected linkPath = 'entitytypes';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ItemType>) {
super();
}
}
/**
* A service to retrieve {@link ItemType}s from the REST API.
*/
@Injectable()
@dataService(ITEM_TYPE)
export class ItemTypeDataService {
/**
* A private DataService instance to delegate specific methods to.
*/
private dataService: DataServiceImpl;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ItemType>) {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
}
/**
* Returns an observable of {@link RemoteData} of an {@link ItemType}, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link ItemType}
* @param href The url of {@link ItemType} we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
* the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ItemType>[]): Observable<RemoteData<ItemType>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow);
}
/**
* Returns a list of observables of {@link RemoteData} of {@link ItemType}s, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link ItemType}
* @param href The url of the {@link ItemType} we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
* the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findByAllHref(href: string, reRequestOnStale = true, findListOptions: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<ItemType>[]): Observable<RemoteData<PaginatedList<ItemType>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -10,14 +10,14 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { GetRequest } from './request.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap, take, map } from 'rxjs/operators'; import { switchMap, take, map } from 'rxjs/operators';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import {PaginatedList} from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { ItemType } from '../shared/item-relationships/item-type.model'; import { ItemType } from '../shared/item-relationships/item-type.model';
import {getRemoteDataPayload, getFirstSucceededRemoteData} from '../shared/operators'; import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../shared/operators';
import { RelationshipTypeService } from './relationship-type.service';
/** /**
* Service handling all ItemType requests * Service handling all ItemType requests
@@ -33,6 +33,7 @@ export class EntityTypeService extends DataService<ItemType> {
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected relationshipTypeService: RelationshipTypeService,
protected http: HttpClient, protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ItemType>) { protected comparator: DefaultChangeAnalyzer<ItemType>) {
super(); super();
@@ -69,18 +70,16 @@ export class EntityTypeService extends DataService<ItemType> {
/** /**
* Get the allowed relationship types for an entity type * Get the allowed relationship types for an entity type
* @param entityTypeId * @param entityTypeId
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
getEntityTypeRelationships(entityTypeId: string, ...linksToFollow: FollowLinkConfig<RelationshipType>[]): Observable<RemoteData<PaginatedList<RelationshipType>>> { getEntityTypeRelationships(entityTypeId: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<RelationshipType>[]): Observable<RemoteData<PaginatedList<RelationshipType>>> {
const href$ = this.getRelationshipTypesEndpoint(entityTypeId); const href$ = this.getRelationshipTypesEndpoint(entityTypeId);
return this.relationshipTypeService.findAllByHref(href$, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
href$.pipe(take(1)).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
});
return this.rdbService.buildList(href$, ...linksToFollow);
} }
/** /**

View File

@@ -31,7 +31,7 @@ describe('EpersonRegistrationService', () => {
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: 'request-id', generateRequestId: 'request-id',
configure: {}, send: {},
getByUUID: cold('a', getByUUID: cold('a',
{ a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) }) { a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) })
}); });
@@ -70,7 +70,7 @@ describe('EpersonRegistrationService', () => {
const expected = service.registerEmail('test@mail.org'); const expected = service.registerEmail('test@mail.org');
expect(requestService.configure).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration));
expect(expected).toBeObservable(cold('(a|)', { a: rd })); expect(expected).toBeObservable(cold('(a|)', { a: rd }));
}); });
}); });

View File

@@ -66,7 +66,7 @@ export class EpersonRegistrationService {
find((href: string) => hasValue(href)), find((href: string) => hasValue(href)),
map((href: string) => { map((href: string) => {
const request = new PostRequest(requestId, href, registration); const request = new PostRequest(requestId, href, registration);
this.requestService.configure(request); this.requestService.send(request);
}) })
).subscribe(); ).subscribe();
@@ -93,7 +93,7 @@ export class EpersonRegistrationService {
return RegistrationResponseParsingService; return RegistrationResponseParsingService;
} }
}); });
this.requestService.configure(request); this.requestService.send(request, true);
}) })
).subscribe(); ).subscribe();

View File

@@ -42,7 +42,7 @@ describe('ExternalSourceService', () => {
function init() { function init() {
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: 'request-uuid', generateRequestId: 'request-uuid',
configure: {} send: {}
}); });
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries))
@@ -64,8 +64,8 @@ describe('ExternalSourceService', () => {
result = service.getExternalSourceEntries('test'); result = service.getExternalSourceEntries('test');
}); });
it('should configure a GetRequest', () => { it('should send a GetRequest', () => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), true);
}); });
it('should return the entries', () => { it('should return the entries', () => {

View File

@@ -9,16 +9,16 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { FindListOptions, GetRequest } from './request.models'; import { FindListOptions } from './request.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
import { configureRequest } from '../shared/operators';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/** /**
* A service handling all external source requests * A service handling all external source requests
@@ -63,21 +63,22 @@ export class ExternalSourceService extends DataService<ExternalSource> {
* Get the entries for an external source * Get the entries for an external source
* @param externalSourceId The id of the external source to fetch entries for * @param externalSourceId The id of the external source to fetch entries for
* @param searchOptions The search options to limit results to * @param searchOptions The search options to limit results to
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<ExternalSourceEntry>>> { getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ExternalSourceEntry>[]): Observable<RemoteData<PaginatedList<ExternalSourceEntry>>> {
const requestUuid = this.requestService.generateRequestId();
const href$ = this.getEntriesEndpoint(externalSourceId).pipe( const href$ = this.getEntriesEndpoint(externalSourceId).pipe(
isNotEmptyOperator(), isNotEmptyOperator(),
distinctUntilChanged(), distinctUntilChanged(),
map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint),
take(1)
); );
href$.pipe( // TODO create a dedicated ExternalSourceEntryDataService and move this entire method to it. Then the "as any"s won't be necessary
map((endpoint: string) => new GetRequest(requestUuid, endpoint)), return this.findAllByHref(href$, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as any) as any;
configureRequest(this.requestService)
).subscribe();
return this.rdbService.buildList(href$);
} }
} }

View File

@@ -68,7 +68,7 @@ describe('AuthorizationDataService', () => {
}); });
it('should call searchBy with the site\'s url', () => { it('should call searchBy with the site\'s url', () => {
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self), true); expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self), true, true);
}); });
}); });
@@ -78,7 +78,7 @@ describe('AuthorizationDataService', () => {
}); });
it('should call searchBy with the site\'s url and the feature', () => { it('should call searchBy with the site\'s url and the feature', () => {
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, null, FeatureID.LoginOnBehalfOf), true); expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, null, FeatureID.LoginOnBehalfOf), true, true);
}); });
}); });
@@ -88,7 +88,7 @@ describe('AuthorizationDataService', () => {
}); });
it('should call searchBy with the object\'s url and the feature', () => { it('should call searchBy with the object\'s url and the feature', () => {
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, null, FeatureID.LoginOnBehalfOf), true); expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, null, FeatureID.LoginOnBehalfOf), true, true);
}); });
}); });
@@ -98,7 +98,7 @@ describe('AuthorizationDataService', () => {
}); });
it('should call searchBy with the object\'s url, user\'s uuid and the feature', () => { it('should call searchBy with the object\'s url, user\'s uuid and the feature', () => {
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf), true); expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf), true, true);
}); });
}); });
}); });

View File

@@ -60,7 +60,7 @@ export class AuthorizationDataService extends DataService<Authorization> {
* @param featureId ID of the {@link Feature} to check {@link Authorization} for * @param featureId ID of the {@link Feature} to check {@link Authorization} for
*/ */
isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string): Observable<boolean> { isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string): Observable<boolean> {
return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, followLink('feature')).pipe( return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, true, true, followLink('feature')).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((authorizationRD) => { map((authorizationRD) => {
if (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)) { if (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)) {
@@ -83,13 +83,18 @@ export class AuthorizationDataService extends DataService<Authorization> {
* If not provided, the UUID of the currently authenticated {@link EPerson} will be used. * If not provided, the UUID of the currently authenticated {@link EPerson} will be used.
* @param featureId ID of the {@link Feature} to search {@link Authorization}s for * @param featureId ID of the {@link Feature} to search {@link Authorization}s for
* @param options {@link FindListOptions} to provide pagination and/or additional arguments * @param options {@link FindListOptions} to provide pagination and/or additional arguments
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<Authorization>[]): Observable<RemoteData<PaginatedList<Authorization>>> { searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Authorization>[]): Observable<RemoteData<PaginatedList<Authorization>>> {
return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe( return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe(
addSiteObjectUrlIfEmpty(this.siteService), addSiteObjectUrlIfEmpty(this.siteService),
switchMap((params: AuthorizationSearchParams) => { switchMap((params: AuthorizationSearchParams) => {
return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), true, ...linksToFollow); return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}) })
); );
} }

View File

@@ -0,0 +1,82 @@
import { HrefOnlyDataService } from './href-only-data.service';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { FindListOptions } from './request.models';
import { DataService } from './data.service';
describe(`HrefOnlyDataService`, () => {
let service: HrefOnlyDataService;
let href: string;
let spy: jasmine.Spy;
let followLinks: FollowLinkConfig<any>[];
let findListOptions: FindListOptions;
beforeEach(() => {
href = 'https://rest.api/server/api/core/items/de7fa215-4a25-43a7-a4d7-17534a09fdfc';
followLinks = [ followLink('link1'), followLink('link2') ];
findListOptions = new FindListOptions();
service = new HrefOnlyDataService(null, null, null, null, null, null, null, null);
});
it(`should instantiate a private DataService`, () => {
expect((service as any).dataService).toBeDefined();
expect((service as any).dataService).toBeInstanceOf(DataService);
});
describe(`findByHref`, () => {
beforeEach(() => {
spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
});
it(`should delegate to findByHref on the internal DataService`, () => {
service.findByHref(href, false, false, ...followLinks);
expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks);
});
describe(`when useCachedVersionIfAvailable is omitted`, () => {
it(`should call findByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => {
service.findByHref(href);
expect(spy).toHaveBeenCalledWith(jasmine.anything(), true, jasmine.anything());
});
});
describe(`when reRequestOnStale is omitted`, () => {
it(`should call findByHref on the internal DataService with reRequestOnStale = true`, () => {
service.findByHref(href);
expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true);
});
});
});
describe(`findAllByHref`, () => {
beforeEach(() => {
spy = spyOn((service as any).dataService, 'findAllByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
});
it(`should delegate to findAllByHref on the internal DataService`, () => {
service.findAllByHref(href, findListOptions, false, false, ...followLinks);
expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks);
});
describe(`when findListOptions is omitted`, () => {
it(`should call findAllByHref on the internal DataService with findListOptions = {}`, () => {
service.findAllByHref(href);
expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything());
});
});
describe(`when useCachedVersionIfAvailable is omitted`, () => {
it(`should call findAllByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => {
service.findAllByHref(href);
expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything());
});
});
describe(`when reRequestOnStale is omitted`, () => {
it(`should call findAllByHref on the internal DataService with reRequestOnStale = true`, () => {
service.findAllByHref(href);
expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true);
});
});
});
});

View File

@@ -0,0 +1,99 @@
import { DataService } from './data.service';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { Injectable } from '@angular/core';
import { VOCABULARY_ENTRY } from '../submission/vocabularies/models/vocabularies.resource-type';
import { FindListOptions } from './request.models';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators';
import { RemoteData } from './remote-data';
import { Observable } from 'rxjs/internal/Observable';
import { PaginatedList } from './paginated-list.model';
import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type';
import { LICENSE } from '../shared/license.resource-type';
import { CacheableObject } from '../cache/object-cache.reducer';
/* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService<any> {
// linkPath isn't used if we're only searching by href.
protected linkPath = undefined;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<any>) {
super();
}
}
/**
* A DataService with only findByHref methods. Its purpose is to be used for resources that don't
* need to be retrieved by ID, or have any way to update them, but require a DataService in order
* for their links to be resolved by the LinkService.
*
* an @dataService annotation can be added for any number of these resource types
*/
@Injectable({
providedIn: 'root'
})
@dataService(VOCABULARY_ENTRY)
@dataService(ITEM_TYPE)
@dataService(LICENSE)
export class HrefOnlyDataService {
private dataService: DataServiceImpl;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<any>) {
this.dataService = new DataServiceImpl(requestService, rdbService, store, objectCache, halService, notificationsService, http, comparator);
}
/**
* Returns an observable of {@link RemoteData} of an object, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the object
* @param href The url of object we want to retrieve
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
findByHref<T extends CacheableObject>(href: string | Observable<string>, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the object
* @param href The url of object we want to retrieve
* @param findListOptions Find list options object
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
findAllByHref<T extends CacheableObject>(href: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -126,8 +126,8 @@ describe('ItemDataService', () => {
result = service.removeMappingFromCollection('item-id', 'collection-id'); result = service.removeMappingFromCollection('item-id', 'collection-id');
}); });
it('should configure a DELETE request', () => { it('should send a DELETE request', () => {
result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(DeleteRequest))); result.subscribe(() => expect(requestService.send).toHaveBeenCalledWith(jasmine.any(DeleteRequest)));
}); });
}); });
@@ -139,8 +139,8 @@ describe('ItemDataService', () => {
result = service.mapToCollection('item-id', 'collection-href'); result = service.mapToCollection('item-id', 'collection-href');
}); });
it('should configure a POST request', () => { it('should send a POST request', () => {
result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); result.subscribe(() => expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PostRequest)));
}); });
}); });
@@ -158,9 +158,9 @@ describe('ItemDataService', () => {
result = service.importExternalSourceEntry(externalSourceEntry, 'collection-id'); result = service.importExternalSourceEntry(externalSourceEntry, 'collection-id');
}); });
it('should configure a POST request', (done) => { it('should send a POST request', (done) => {
result.subscribe(() => { result.subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PostRequest));
done(); done();
}); });
}); });
@@ -176,9 +176,9 @@ describe('ItemDataService', () => {
result = service.createBundle(itemId, bundleName); result = service.createBundle(itemId, bundleName);
}); });
it('should configure a POST request', (done) => { it('should send a POST request', (done) => {
result.subscribe(() => { result.subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PostRequest));
done(); done();
}); });
}); });

View File

@@ -16,7 +16,7 @@ import { ExternalSourceEntry } from '../shared/external-source-entry.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { ITEM } from '../shared/item.resource-type'; import { ITEM } from '../shared/item.resource-type';
import { configureRequest } from '../shared/operators'; import { sendRequest } from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { DataService } from './data.service'; import { DataService } from './data.service';
@@ -99,7 +99,7 @@ export class ItemDataService extends DataService<Item> {
isNotEmptyOperator(), isNotEmptyOperator(),
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
configureRequest(this.requestService), sendRequest(this.requestService),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)),
); );
} }
@@ -120,29 +120,11 @@ export class ItemDataService extends DataService<Item> {
options.headers = headers; options.headers = headers;
return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options); return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options);
}), }),
configureRequest(this.requestService), sendRequest(this.requestService),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)) switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid))
); );
} }
/**
* Fetches all collections the item is mapped to
* @param itemId The item's id
*/
public getMappedCollections(itemId: string): Observable<RemoteData<PaginatedList<Collection>>> {
const href$ = this.getMappedCollectionsEndpoint(itemId).pipe(
isNotEmptyOperator(),
take(1)
);
href$.subscribe((href: string) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
});
return this.rdbService.buildList(href$);
}
/** /**
* Set the isWithdrawn state of an item to a specified state * Set the isWithdrawn state of an item to a specified state
* @param item * @param item
@@ -196,7 +178,7 @@ export class ItemDataService extends DataService<Item> {
take(1) take(1)
).subscribe((href) => { ).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href); const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request); this.requestService.send(request);
}); });
return this.rdbService.buildList<Bundle>(hrefObs); return this.rdbService.buildList<Bundle>(hrefObs);
@@ -225,7 +207,7 @@ export class ItemDataService extends DataService<Item> {
headers = headers.append('Content-Type', 'application/json'); headers = headers.append('Content-Type', 'application/json');
options.headers = headers; options.headers = headers;
const request = new PostRequest(requestId, href, JSON.stringify(bundleJson), options); const request = new PostRequest(requestId, href, JSON.stringify(bundleJson), options);
this.requestService.configure(request); this.requestService.send(request);
}); });
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
@@ -260,7 +242,7 @@ export class ItemDataService extends DataService<Item> {
find((href: string) => hasValue(href)), find((href: string) => hasValue(href)),
map((href: string) => { map((href: string) => {
const request = new PutRequest(requestId, href, collection._links.self.href, options); const request = new PutRequest(requestId, href, collection._links.self.href, options);
this.requestService.configure(request); this.requestService.send(request);
}) })
).subscribe(); ).subscribe();
@@ -285,7 +267,7 @@ export class ItemDataService extends DataService<Item> {
find((href: string) => hasValue(href)), find((href: string) => hasValue(href)),
map((href: string) => { map((href: string) => {
const request = new PostRequest(requestId, href, externalSourceEntry._links.self.href, options); const request = new PostRequest(requestId, href, externalSourceEntry._links.self.href, options);
this.requestService.configure(request); this.requestService.send(request);
}) })
).subscribe(); ).subscribe();

View File

@@ -28,7 +28,7 @@ describe('ItemTemplateDataService', () => {
generateRequestId(): string { generateRequestId(): string {
return scopeID; return scopeID;
}, },
configure(request: RestRequest) { send(request: RestRequest) {
// Do nothing // Do nothing
}, },
getByHref(requestHref: string) { getByHref(requestHref: string) {

View File

@@ -100,13 +100,16 @@ class DataServiceImpl extends ItemDataService {
/** /**
* Set the collection ID and send a find by ID request * Set the collection ID and send a find by ID request
* @param collectionID * @param collectionID
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findByCollectionID(collectionID: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> { findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> {
this.setCollectionEndpoint(collectionID); this.setCollectionEndpoint(collectionID);
return super.findById(collectionID, reRequestOnStale, ...linksToFollow); return super.findById(collectionID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -178,12 +181,15 @@ export class ItemTemplateDataService implements UpdateDataService<Item> {
/** /**
* Find an item template by collection ID * Find an item template by collection ID
* @param collectionID * @param collectionID
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findByCollectionID(collectionID: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> { findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> {
return this.dataService.findByCollectionID(collectionID, reRequestOnStale, ...linksToFollow); return this.dataService.findByCollectionID(collectionID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**

View File

@@ -1,89 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { 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 { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { License } from '../shared/license.model';
import { LICENSE } from '../shared/license.resource-type';
import { DataService } from './data.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import { FindListOptions } from './request.models';
import { RequestService } from './request.service';
/* tslint:disable:max-classes-per-file */
/**
* A private DataService implementation to delegate specific methods to.
*/
class DataServiceImpl extends DataService<License> {
protected linkPath = '';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<License>) {
super();
}
}
/**
* A service to retrieve {@link License}s from the REST API.
*/
@Injectable()
@dataService(LICENSE)
export class LicenseDataService {
/**
* A private DataService instance to delegate specific methods to.
*/
private dataService: DataServiceImpl;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<License>) {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
}
/**
* Returns an observable of {@link RemoteData} of a {@link License}, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link License}
* @param href The URL of object we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
* the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<License>[]): Observable<RemoteData<License>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow);
}
/**
* Returns a list of observables of {@link RemoteData} of {@link License}s, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link License}
* @param href The URL of object we want to retrieve
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
* the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findByAllHref(href: string, reRequestOnStale = true, findListOptions: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<License>[]): Observable<RemoteData<PaginatedList<License>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -31,7 +31,7 @@ describe('MetadataFieldDataService', () => {
}); });
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
configure: {}, send: {},
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }), getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
removeByHrefSubstring: {} removeByHrefSubstring: {}
}); });
@@ -59,7 +59,7 @@ describe('MetadataFieldDataService', () => {
const expectedOptions = Object.assign(new FindListOptions(), { const expectedOptions = Object.assign(new FindListOptions(), {
searchParams: [new RequestParam('schema', schema.prefix)] searchParams: [new RequestParam('schema', schema.prefix)]
}); });
expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions, true); expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions, true, true);
}); });
}); });

View File

@@ -48,15 +48,18 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
* Find metadata fields belonging to a metadata schema * Find metadata fields belonging to a metadata schema
* @param schema The metadata schema to list fields for * @param schema The metadata schema to list fields for
* @param options The options info used to retrieve the fields * @param options The options info used to retrieve the fields
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
findBySchema(schema: MetadataSchema, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<MetadataField>[]) { findBySchema(schema: MetadataSchema, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<MetadataField>[]) {
const optionsWithSchema = Object.assign(new FindListOptions(), options, { const optionsWithSchema = Object.assign(new FindListOptions(), options, {
searchParams: [new RequestParam('schema', schema.prefix)] searchParams: [new RequestParam('schema', schema.prefix)]
}); });
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, reRequestOnStale, ...linksToFollow); return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -71,10 +74,11 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
* schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value * schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value
* if there's an exact match * if there's an exact match
* @param options The options info used to retrieve the fields * @param options The options info used to retrieve the fields
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-requested after the response becomes stale * @param reRequestOnStale Whether or not the request should automatically be re-requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/ */
searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<MetadataField>[]): Observable<RemoteData<PaginatedList<MetadataField>>> { searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<MetadataField>[]): Observable<RemoteData<PaginatedList<MetadataField>>> {
const optionParams = Object.assign(new FindListOptions(), options, { const optionParams = Object.assign(new FindListOptions(), options, {
searchParams: [ searchParams: [
new RequestParam('schema', hasValue(schema) ? schema : ''), new RequestParam('schema', hasValue(schema) ? schema : ''),
@@ -84,7 +88,7 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
new RequestParam('exactName', hasValue(exactName) ? exactName : '') new RequestParam('exactName', hasValue(exactName) ? exactName : '')
] ]
}); });
return this.searchBy(this.searchByFieldNameLinkPath, optionParams, reRequestOnStale, ...linksToFollow); return this.searchBy(this.searchByFieldNameLinkPath, optionParams, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**

View File

@@ -22,7 +22,7 @@ describe('MetadataSchemaDataService', () => {
function init() { function init() {
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
configure: {}, send: {},
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }), getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
removeByHrefSubstring: {} removeByHrefSubstring: {}
}); });
@@ -54,7 +54,7 @@ describe('MetadataSchemaDataService', () => {
describe('called with a new metadata schema', () => { describe('called with a new metadata schema', () => {
it('should send a CreateRequest', (done) => { it('should send a CreateRequest', (done) => {
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => { metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(CreateRequest));
done(); done();
}); });
}); });
@@ -69,7 +69,7 @@ describe('MetadataSchemaDataService', () => {
it('should send a PutRequest', (done) => { it('should send a PutRequest', (done) => {
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => { metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
done(); done();
}); });
}); });

View File

@@ -13,12 +13,11 @@ import { Process } from '../../../process-page/processes/process.model';
import { dataService } from '../../cache/builders/build-decorators'; import { dataService } from '../../cache/builders/build-decorators';
import { PROCESS } from '../../../process-page/processes/process.resource-type'; import { PROCESS } from '../../../process-page/processes/process.resource-type';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { GetRequest } from '../request.models';
import { PaginatedList } from '../paginated-list.model'; import { PaginatedList } from '../paginated-list.model';
import { Bitstream } from '../../shared/bitstream.model'; import { Bitstream } from '../../shared/bitstream.model';
import { RemoteData } from '../remote-data'; import { RemoteData } from '../remote-data';
import { isNotEmptyOperator } from '../../../shared/empty.util'; import { BitstreamDataService } from '../bitstream-data.service';
@Injectable() @Injectable()
@dataService(PROCESS) @dataService(PROCESS)
@@ -32,6 +31,7 @@ export class ProcessDataService extends DataService<Process> {
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected bitstreamDataService: BitstreamDataService,
protected http: HttpClient, protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Process>) { protected comparator: DefaultChangeAnalyzer<Process>) {
super(); super();
@@ -48,20 +48,11 @@ export class ProcessDataService extends DataService<Process> {
} }
/** /**
* Get a process his output files * Get a process' output files
* @param processId The ID of the process * @param processId The ID of the process
*/ */
getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> { getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> {
const href$ = this.getFilesEndpoint(processId).pipe( const href$ = this.getFilesEndpoint(processId);
isNotEmptyOperator(), return this.bitstreamDataService.findAllByHref(href$);
take(1)
);
href$.subscribe((href: string) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
});
return this.rdbService.buildList(href$);
} }
} }

View File

@@ -49,7 +49,7 @@ export class ScriptDataService extends DataService<Script> {
const body = this.getInvocationFormData(parameters, files); const body = this.getInvocationFormData(parameters, files);
return new MultipartPostRequest(requestId, endpoint, body); return new MultipartPostRequest(requestId, endpoint, body);
}) })
).subscribe((request: RestRequest) => this.requestService.configure(request)); ).subscribe((request: RestRequest) => this.requestService.send(request));
return this.rdbService.buildFromRequestUUID<Process>(requestId); return this.rdbService.buildFromRequestUUID<Process>(requestId);
} }

View File

@@ -2,7 +2,10 @@ import { of as observableOf } from 'rxjs';
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import {
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ItemType } from '../shared/item-relationships/item-type.model'; import { ItemType } from '../shared/item-relationships/item-type.model';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
@@ -94,17 +97,6 @@ describe('RelationshipTypeService', () => {
service = initTestService(); service = initTestService();
}); });
describe('getAllRelationshipTypes', () => {
it('should return all relationshipTypes', (done) => {
const expected = service.getAllRelationshipTypes({});
expected.subscribe((e) => {
expect(e).toBe(buildList);
done();
});
});
});
describe('getRelationshipTypeByLabelAndTypes', () => { describe('getRelationshipTypeByLabelAndTypes', () => {
it('should return the type filtered by label and type strings', (done) => { it('should return the type filtered by label and type strings', (done) => {

View File

@@ -15,13 +15,12 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ItemType } from '../shared/item-relationships/item-type.model'; import { ItemType } from '../shared/item-relationships/item-type.model';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { RELATIONSHIP_TYPE } from '../shared/item-relationships/relationship-type.resource-type'; import { RELATIONSHIP_TYPE } from '../shared/item-relationships/relationship-type.resource-type';
import { configureRequest, getFirstSucceededRemoteData } from '../shared/operators'; import { getFirstSucceededRemoteData } from '../shared/operators';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { ItemDataService } from './item-data.service'; import { ItemDataService } from './item-data.service';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { FindListOptions, FindListRequest } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
/** /**
@@ -45,32 +44,12 @@ export class RelationshipTypeService extends DataService<RelationshipType> {
super(); super();
} }
/**
* Get the endpoint for a relationship type by ID
* @param id
*/
getRelationshipTypeEndpoint(id: number) {
return this.halService.getEndpoint(this.linkPath).pipe(
map((href: string) => `${href}/${id}`)
);
}
getAllRelationshipTypes(options: FindListOptions): Observable<RemoteData<PaginatedList<RelationshipType>>> {
const link$ = this.halService.getEndpoint(this.linkPath);
return link$
.pipe(
map((endpointURL: string) => new FindListRequest(this.requestService.generateRequestId(), endpointURL, options)),
configureRequest(this.requestService),
switchMap(() => this.rdbService.buildList<RelationshipType>(link$, followLink('leftType'), followLink('rightType')))
) as Observable<RemoteData<PaginatedList<RelationshipType>>>;
}
/** /**
* Get the RelationshipType for a relationship type by label * Get the RelationshipType for a relationship type by label
* @param label * @param label
*/ */
getRelationshipTypeByLabelAndTypes(label: string, firstType: string, secondType: string): Observable<RelationshipType> { getRelationshipTypeByLabelAndTypes(label: string, firstType: string, secondType: string): Observable<RelationshipType> {
return this.getAllRelationshipTypes({ currentPage: 1, elementsPerPage: Number.MAX_VALUE }) return this.findAll({ currentPage: 1, elementsPerPage: 9999 }, true, true, followLink('leftType'), followLink('rightType'))
.pipe( .pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
/* Flatten the page so we can treat it like an observable */ /* Flatten the page so we can treat it like an observable */

View File

@@ -156,7 +156,7 @@ xdescribe('RelationshipService', () => {
it('should send a DeleteRequest', () => { it('should send a DeleteRequest', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid + '?copyVirtualMetadata=right'); const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid + '?copyVirtualMetadata=right');
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should clear the cache of the related items', () => { it('should clear the cache of the related items', () => {
@@ -210,6 +210,7 @@ xdescribe('RelationshipService', () => {
mockLabel, mockLabel,
mockOptions, mockOptions,
true, true,
true,
followLink('leftItem'), followLink('leftItem'),
followLink('rightItem'), followLink('rightItem'),
followLink('relationshipType') followLink('relationshipType')

View File

@@ -30,7 +30,7 @@ import { Relationship } from '../shared/item-relationships/relationship.model';
import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type'; import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { import {
configureRequest, sendRequest,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
@@ -113,7 +113,7 @@ export class RelationshipService extends DataService<Relationship> {
map((endpointURL: string) => map((endpointURL: string) =>
new DeleteRequest(this.requestService.generateRequestId(), endpointURL + '?copyVirtualMetadata=' + copyVirtualMetadata) new DeleteRequest(this.requestService.generateRequestId(), endpointURL + '?copyVirtualMetadata=' + copyVirtualMetadata)
), ),
configureRequest(this.requestService), sendRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)), switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)), tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)),
@@ -140,7 +140,7 @@ export class RelationshipService extends DataService<Relationship> {
map((endpointUrl: string) => isNotEmpty(leftwardValue) ? `${endpointUrl}&leftwardValue=${leftwardValue}` : endpointUrl), map((endpointUrl: string) => isNotEmpty(leftwardValue) ? `${endpointUrl}&leftwardValue=${leftwardValue}` : endpointUrl),
map((endpointUrl: string) => isNotEmpty(rightwardValue) ? `${endpointUrl}&rightwardValue=${rightwardValue}` : 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)), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, `${item1.self} \n ${item2.self}`, options)),
configureRequest(this.requestService), sendRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)), switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
tap(() => this.refreshRelationshipItemsInCache(item1)), tap(() => this.refreshRelationshipItemsInCache(item1)),
@@ -153,7 +153,7 @@ export class RelationshipService extends DataService<Relationship> {
* @param relationshipId The identifier of the relationship * @param relationshipId The identifier of the relationship
*/ */
private refreshRelationshipItemsInCacheByRelationship(relationshipId: string) { private refreshRelationshipItemsInCacheByRelationship(relationshipId: string) {
this.findById(relationshipId, false, followLink('leftItem'), followLink('rightItem')).pipe( this.findById(relationshipId, true, false, followLink('leftItem'), followLink('rightItem')).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
switchMap((rel: Relationship) => observableCombineLatest( switchMap((rel: Relationship) => observableCombineLatest(
@@ -181,8 +181,7 @@ export class RelationshipService extends DataService<Relationship> {
]).pipe( ]).pipe(
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
take(1), take(1),
switchMap(() => this.itemService.findByHref(item._links.self.href).pipe(take(1))) ).subscribe(() => this.itemService.findByHref(item._links.self.href, false));
).subscribe();
} }
/** /**
@@ -193,7 +192,7 @@ export class RelationshipService extends DataService<Relationship> {
* should be automatically resolved * should be automatically resolved
*/ */
getItemRelationshipsArray(item: Item, ...linksToFollow: FollowLinkConfig<Relationship>[]): Observable<Relationship[]> { getItemRelationshipsArray(item: Item, ...linksToFollow: FollowLinkConfig<Relationship>[]): Observable<Relationship[]> {
return this.findAllByHref(item._links.relationships.href, undefined, false, ...linksToFollow).pipe( return this.findAllByHref(item._links.relationships.href, undefined, true, false, ...linksToFollow).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
map((rels: PaginatedList<Relationship>) => rels.page), map((rels: PaginatedList<Relationship>) => rels.page),
@@ -255,7 +254,7 @@ export class RelationshipService extends DataService<Relationship> {
* @param options * @param options
*/ */
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> { getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getItemRelationshipsByLabel(item, label, options, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid)); return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid));
} }
/** /**
@@ -265,12 +264,14 @@ export class RelationshipService extends DataService<Relationship> {
* @param item * @param item
* @param label * @param label
* @param options * @param options
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
getItemRelationshipsByLabel(item: Item, label: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Relationship>[]): Observable<RemoteData<PaginatedList<Relationship>>> { getItemRelationshipsByLabel(item: Item, label: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Relationship>[]): Observable<RemoteData<PaginatedList<Relationship>>> {
let findListOptions = new FindListOptions(); let findListOptions = new FindListOptions();
if (options) { if (options) {
findListOptions = Object.assign(new FindListOptions(), options); findListOptions = Object.assign(new FindListOptions(), options);
@@ -281,7 +282,7 @@ export class RelationshipService extends DataService<Relationship> {
} else { } else {
findListOptions.searchParams = searchParams; findListOptions.searchParams = searchParams;
} }
return this.searchBy('byLabel', findListOptions, reRequestOnStale, ...linksToFollow); return this.searchBy('byLabel', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -326,6 +327,7 @@ export class RelationshipService extends DataService<Relationship> {
item1, item1,
label, label,
options, options,
true,
false, false,
followLink('relationshipType'), followLink('relationshipType'),
followLink('leftItem'), followLink('leftItem'),

View File

@@ -21,7 +21,6 @@ export enum IdentifierType {
export abstract class RestRequest { export abstract class RestRequest {
public responseMsToLive = environment.cache.msToLive.default; public responseMsToLive = environment.cache.msToLive.default;
public forceBypassCache = false;
public isMultipart = false; public isMultipart = false;
constructor( constructor(
@@ -130,16 +129,6 @@ export class PatchRequest extends RestRequest {
} }
} }
export class FindByIDRequest extends GetRequest {
constructor(
uuid: string,
href: string,
public resourceID: string
) {
super(uuid, href);
}
}
export class FindListOptions { export class FindListOptions {
scopeID?: string; scopeID?: string;
elementsPerPage?: number; elementsPerPage?: number;
@@ -169,7 +158,6 @@ export class EndpointMapRequest extends GetRequest {
* Class representing a submission HTTP GET request object * Class representing a submission HTTP GET request object
*/ */
export class SubmissionRequest extends GetRequest { export class SubmissionRequest extends GetRequest {
forceBypassCache = true;
constructor(uuid: string, href: string) { constructor(uuid: string, href: string) {
super(uuid, href); super(uuid, href);
} }

View File

@@ -271,6 +271,9 @@ function expireRequest(storeState: RequestState, action: RequestStaleAction): Re
return storeState; return storeState;
} else { } else {
const prevEntry = storeState[action.payload.uuid]; const prevEntry = storeState[action.payload.uuid];
if (isStale(prevEntry.state)) {
return storeState;
} else {
return Object.assign({}, storeState, { return Object.assign({}, storeState, {
[action.payload.uuid]: Object.assign({}, prevEntry, { [action.payload.uuid]: Object.assign({}, prevEntry, {
state: hasSucceeded(prevEntry.state) ? RequestEntryState.SuccessStale : RequestEntryState.ErrorStale, state: hasSucceeded(prevEntry.state) ? RequestEntryState.SuccessStale : RequestEntryState.ErrorStale,
@@ -278,6 +281,7 @@ function expireRequest(storeState: RequestState, action: RequestStaleAction): Re
}) })
}); });
} }
}
} }
/** /**

View File

@@ -24,6 +24,7 @@ import { RequestService } from './request.service';
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { storeModuleConfig } from '../../app.reducer'; import { storeModuleConfig } from '../../app.reducer';
import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { createRequestEntry$ } from '../../shared/testing/utils.test';
describe('RequestService', () => { describe('RequestService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -209,46 +210,6 @@ describe('RequestService', () => {
}); });
}); });
describe(`if the request with the specified UUID wasn't sent, because it was already cached`, () => {
let entry;
beforeEach(() => {
entry = {
state: RequestEntryState.Success,
response: {
timeCompleted: new Date().getTime()
},
request: new GetRequest('request-uuid', 'request-href')
};
// No direct hit in the request cache with that UUID
// A hit in the index, which returns the uuid of the cached request
// the call to retrieve the cached request using the UUID from the index
const state = Object.assign({}, initialState, {
core: Object.assign({}, initialState.core, {
'data/request': {
'otherRequestUUID': entry
},
'index': {
'get-request/configured-to-cache-uuid': {
'5f2a0d2a-effa-4d54-bd54-5663b960f9eb': 'otherRequestUUID'
}
}
})
});
mockStore.setState(state);
});
it(`it should return the cached request`, () => {
const result = service.getByUUID(testUUID);
const expected = cold('c', {
c: entry
});
expect(result).toBeObservable(expected);
});
});
}); });
describe('getByHref', () => { describe('getByHref', () => {
@@ -309,7 +270,7 @@ describe('RequestService', () => {
}); });
}); });
describe('configure', () => { describe('send', () => {
beforeEach(() => { beforeEach(() => {
spyOn(serviceAsAny, 'dispatchRequest'); spyOn(serviceAsAny, 'dispatchRequest');
}); });
@@ -324,7 +285,7 @@ describe('RequestService', () => {
it('should track it on it\'s way to the store', () => { it('should track it on it\'s way to the store', () => {
spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore'); spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
spyOn(serviceAsAny, 'isCachedOrPending').and.returnValue(false); spyOn(serviceAsAny, 'isCachedOrPending').and.returnValue(false);
service.configure(request); service.send(request);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).toHaveBeenCalledWith(request); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).toHaveBeenCalledWith(request);
}); });
describe('and it isn\'t cached or pending', () => { describe('and it isn\'t cached or pending', () => {
@@ -333,7 +294,7 @@ describe('RequestService', () => {
}); });
it('should dispatch the request', () => { it('should dispatch the request', () => {
scheduler.schedule(() => service.configure(request)); scheduler.schedule(() => service.send(request, true));
scheduler.flush(); scheduler.flush();
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request);
}); });
@@ -344,7 +305,7 @@ describe('RequestService', () => {
}); });
it('shouldn\'t dispatch the request', () => { it('shouldn\'t dispatch the request', () => {
service.configure(request); service.send(request, true);
expect(serviceAsAny.dispatchRequest).not.toHaveBeenCalled(); expect(serviceAsAny.dispatchRequest).not.toHaveBeenCalled();
}); });
}); });
@@ -352,22 +313,22 @@ describe('RequestService', () => {
describe('when the request isn\'t a GET request', () => { describe('when the request isn\'t a GET request', () => {
it('should dispatch the request', () => { it('should dispatch the request', () => {
service.configure(testPostRequest); service.send(testPostRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest);
service.configure(testPutRequest); service.send(testPutRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest);
service.configure(testDeleteRequest); service.send(testDeleteRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest);
service.configure(testOptionsRequest); service.send(testOptionsRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest);
service.configure(testHeadRequest); service.send(testHeadRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest);
service.configure(testPatchRequest); service.send(testPatchRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest);
}); });
}); });
@@ -435,20 +396,25 @@ describe('RequestService', () => {
}); });
describe('dispatchRequest', () => { describe('dispatchRequest', () => {
let dispatchSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
spyOn(store, 'dispatch'); dispatchSpy = spyOn(store, 'dispatch');
}); });
it('should dispatch a RequestConfigureAction', () => { it('should dispatch a RequestConfigureAction', () => {
const request = testGetRequest; const request = testGetRequest;
serviceAsAny.dispatchRequest(request); serviceAsAny.dispatchRequest(request);
expect(store.dispatch).toHaveBeenCalledWith(new RequestConfigureAction(request)); const firstAction = dispatchSpy.calls.argsFor(0)[0];
expect(firstAction).toBeInstanceOf(RequestConfigureAction);
expect(firstAction.payload).toEqual(request);
}); });
it('should dispatch a RequestExecuteAction', () => { it('should dispatch a RequestExecuteAction', () => {
const request = testGetRequest; const request = testGetRequest;
serviceAsAny.dispatchRequest(request); serviceAsAny.dispatchRequest(request);
expect(store.dispatch).toHaveBeenCalledWith(new RequestExecuteAction(request.uuid)); const secondAction = dispatchSpy.calls.argsFor(1)[0];
expect(secondAction).toBeInstanceOf(RequestExecuteAction);
expect(secondAction.payload).toEqual(request.uuid);
}); });
describe('when it\'s not a GET request', () => { describe('when it\'s not a GET request', () => {
@@ -501,7 +467,7 @@ describe('RequestService', () => {
describe('when the request is added to the store', () => { describe('when the request is added to the store', () => {
it('should stop tracking the request', () => { it('should stop tracking the request', () => {
spyOn(serviceAsAny, 'getByUUID').and.returnValue(observableOf(entry)); spyOn(serviceAsAny, 'getByHref').and.returnValue(observableOf(entry));
serviceAsAny.trackRequestsOnTheirWayToTheStore(request); serviceAsAny.trackRequestsOnTheirWayToTheStore(request);
expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy(); expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy();
}); });

View File

@@ -2,20 +2,15 @@ import { Injectable } from '@angular/core';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, map, mergeMap, take, switchMap, tap } from 'rxjs/operators'; import { filter, map, take, tap } from 'rxjs/operators';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
import { CacheableObject, ObjectCacheEntry } from '../cache/object-cache.reducer'; import { ObjectCacheEntry } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { IndexName, IndexState, MetaIndexState } from '../index/index.reducer'; import { IndexState, MetaIndexState } from '../index/index.reducer';
import { import { requestIndexSelector, getUrlWithoutEmbedParams } from '../index/index.selectors';
originalRequestUUIDFromRequestUUIDSelector,
requestIndexSelector,
uuidFromHrefSelector,
getUrlWithoutEmbedParams
} from '../index/index.selectors';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { import {
RequestConfigureAction, RequestConfigureAction,
@@ -23,10 +18,9 @@ import {
RequestStaleAction RequestStaleAction
} from './request.actions'; } from './request.actions';
import { GetRequest, RestRequest } from './request.models'; import { GetRequest, RestRequest } from './request.models';
import { RequestEntry, RequestState, hasCompleted, isStale, isLoading } from './request.reducer'; import { RequestEntry, RequestState, isStale, isLoading } from './request.reducer';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
import { AddToIndexAction } from '../index/index.actions';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';
/** /**
@@ -38,7 +32,7 @@ const requestCacheSelector = createSelector(
); );
/** /**
* Selector function to select a request entry by uuid from the cache * Selector function to select a request entry by uuid from the store
* @param uuid The uuid of the request * @param uuid The uuid of the request
*/ */
const entryFromUUIDSelector = (uuid: string): MemoizedSelector<CoreState, RequestEntry> => createSelector( const entryFromUUIDSelector = (uuid: string): MemoizedSelector<CoreState, RequestEntry> => createSelector(
@@ -48,6 +42,29 @@ const entryFromUUIDSelector = (uuid: string): MemoizedSelector<CoreState, Reques
} }
); );
/**
* Selector function to select a request entry by href from the store
* @param href The href of the request
*/
const entryFromHrefSelector = (href: string): MemoizedSelector<CoreState, RequestEntry> => createSelector(
requestIndexSelector,
requestCacheSelector,
(indexState: IndexState, requestState: RequestState) => {
let uuid: any;
if (hasValue(indexState)) {
uuid = indexState[getUrlWithoutEmbedParams(href)];
} else {
return undefined;
}
if (hasValue(requestState)) {
return requestState[uuid];
} else {
return undefined;
}
}
);
/** /**
* Create a selector that fetches a list of request UUIDs from a given index substate of which the request href * Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
* contains a given substring * contains a given substring
@@ -138,7 +155,7 @@ export class RequestService {
this.getByHref(request.href).pipe( this.getByHref(request.href).pipe(
take(1)) take(1))
.subscribe((re: RequestEntry) => { .subscribe((re: RequestEntry) => {
isPending = (hasValue(re) && !hasCompleted(re.state)); isPending = (hasValue(re) && isLoading(re.state));
}); });
return isPending; return isPending;
} }
@@ -147,20 +164,21 @@ export class RequestService {
* Retrieve a RequestEntry based on their uuid * Retrieve a RequestEntry based on their uuid
*/ */
getByUUID(uuid: string): Observable<RequestEntry> { getByUUID(uuid: string): Observable<RequestEntry> {
return observableCombineLatest([ return this.store.pipe(
this.store.pipe( select(entryFromUUIDSelector(uuid)),
select(entryFromUUIDSelector(uuid)) this.fixRequestHeaders(),
), this.checkStale()
this.store.pipe( );
select(originalRequestUUIDFromRequestUUIDSelector(uuid)), }
switchMap((originalUUID) => {
return this.store.pipe(select(entryFromUUIDSelector(originalUUID))); /**
}, * Operator that turns the request headers back in to an HttpHeaders instance after an entry has
), * been retrieved from the ngrx store
), * @private
]).pipe( */
map((entries: RequestEntry[]) => entries.find((entry: RequestEntry) => hasValue(entry))), private fixRequestHeaders() {
map((entry: RequestEntry) => { return (source: Observable<RequestEntry>): Observable<RequestEntry> => {
return source.pipe(map((entry: RequestEntry) => {
// Headers break after being retrieved from the store (because of lazy initialization) // Headers break after being retrieved from the store (because of lazy initialization)
// Combining them with a new object fixes this issue // Combining them with a new object fixes this issue
if (hasValue(entry) && hasValue(entry.request) && hasValue(entry.request.options) && hasValue(entry.request.options.headers)) { if (hasValue(entry) && hasValue(entry.request) && hasValue(entry.request.options) && hasValue(entry.request.options.headers)) {
@@ -168,51 +186,60 @@ export class RequestService {
entry.request.options.headers = Object.assign(new HttpHeaders(), entry.request.options.headers); entry.request.options.headers = Object.assign(new HttpHeaders(), entry.request.options.headers);
} }
return entry; return entry;
}),
tap((entry: RequestEntry) => {
if (hasValue(entry) && !isStale(entry.state) && !isValid(entry)) {
this.store.dispatch(new RequestStaleAction(uuid));
}
}) })
); );
};
} }
/** /**
* Retrieve a RequestEntry based on their href * Operator that will check if an entry should be stale, and will dispatch an action to set it to
* stale if it should
* @private
*/
private checkStale() {
return (source: Observable<RequestEntry>): Observable<RequestEntry> => {
return source.pipe(
tap((entry: RequestEntry) => {
if (hasValue(entry) && hasValue(entry.request) && !isStale(entry.state) && !isValid(entry)) {
this.store.dispatch(new RequestStaleAction(entry.request.uuid));
}
})
);
};
}
/**
* Retrieve a RequestEntry based on its href
*/ */
getByHref(href: string): Observable<RequestEntry> { getByHref(href: string): Observable<RequestEntry> {
return this.store.pipe( return this.store.pipe(
select(uuidFromHrefSelector(href)), select(entryFromHrefSelector(href)),
mergeMap((uuid: string) => { this.fixRequestHeaders(),
if (isNotEmpty(uuid)) { this.checkStale()
return this.getByUUID(uuid);
} else {
return [undefined];
}
})
); );
} }
/** /**
* Configure a certain request * Add the given request to the ngrx store, and send it to the rest api
* Used to make sure a request is in the cache *
* @param {RestRequest} request The request to send out * @param request The request to send out
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to false
* @returns true if the request was sent, false otherwise
*/ */
configure<T extends CacheableObject>(request: RestRequest): void { send(request: RestRequest, useCachedVersionIfAvailable = false): boolean {
const isGetRequest = request.method === RestRequestMethod.GET; if (useCachedVersionIfAvailable && request.method !== RestRequestMethod.GET) {
if (!isGetRequest || request.forceBypassCache || !this.isCachedOrPending(request)) { console.warn(`${JSON.stringify(request, null, 2)} is not a GET request. In general only GET requests should reuse cached data.`);
}
if (!useCachedVersionIfAvailable || !this.isCachedOrPending(request)) {
this.dispatchRequest(request); this.dispatchRequest(request);
if (isGetRequest) { if (request.method === RestRequestMethod.GET) {
this.trackRequestsOnTheirWayToTheStore(request); this.trackRequestsOnTheirWayToTheStore(request);
} }
return true;
} else { } else {
this.getByHref(request.href).pipe( return false;
filter((entry) => hasValue(entry)),
take(1)
).subscribe((entry) => {
return this.store.dispatch(new AddToIndexAction(IndexName.UUID_MAPPING, request.uuid, entry.request.uuid));
}
);
} }
} }
@@ -308,15 +335,15 @@ export class RequestService {
/** /**
* ngrx action dispatches are asynchronous. But this.isPending needs to return true as soon as the * ngrx action dispatches are asynchronous. But this.isPending needs to return true as soon as the
* configure method for a GET request has been executed, otherwise certain requests will happen multiple times. * send method for a GET request has been executed, otherwise certain requests will happen multiple times.
* *
* This method will store the href of every GET request that gets configured in a local variable, and * This method will store the href of every GET request that gets configured in a local variable, and
* remove it as soon as it can be found in the store. * remove it as soon as it can be found in the store.
*/ */
private trackRequestsOnTheirWayToTheStore(request: GetRequest) { private trackRequestsOnTheirWayToTheStore(request: GetRequest) {
this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href]; this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href];
this.getByUUID(request.uuid).pipe( this.getByHref(request.href).pipe(
filter((re: RequestEntry) => hasValue(re)), filter((re: RequestEntry) => hasValue(re) && hasValue(re.request) && re.request.uuid === request.uuid),
take(1) take(1)
).subscribe((re: RequestEntry) => { ).subscribe((re: RequestEntry) => {
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href); this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href);

View File

@@ -39,7 +39,7 @@ describe('SiteDataService', () => {
}); });
requestService = jasmine.createSpyObj('requestService', { requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID, generateRequestId: requestUUID,
configure: true, send: true,
}); });
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
buildList: cold('a', { buildList: cold('a', {

View File

@@ -5,7 +5,7 @@ import { VersionHistoryDataService } from './version-history-data.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { GetRequest } from './request.models'; import { VersionDataService } from './version-data.service';
const url = 'fake-url'; const url = 'fake-url';
@@ -16,6 +16,7 @@ describe('VersionHistoryDataService', () => {
let notificationsService: any; let notificationsService: any;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService; let objectCache: ObjectCacheService;
let versionService: VersionDataService;
let halService: any; let halService: any;
beforeEach(() => { beforeEach(() => {
@@ -29,8 +30,8 @@ describe('VersionHistoryDataService', () => {
result = service.getVersions('1'); result = service.getVersions('1');
}); });
it('should configure a GET request', () => { it('should call versionService.findAllByHref', () => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); expect(versionService.findAllByHref).toHaveBeenCalled();
}); });
}); });
@@ -46,9 +47,12 @@ describe('VersionHistoryDataService', () => {
objectCache = jasmine.createSpyObj('objectCache', { objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove') remove: jasmine.createSpy('remove')
}); });
versionService = jasmine.createSpyObj('objectCache', {
findAllByHref: jasmine.createSpy('findAllByHref')
});
halService = new HALEndpointServiceStub(url); halService = new HALEndpointServiceStub(url);
notificationsService = new NotificationsServiceStub(); notificationsService = new NotificationsServiceStub();
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, null, null); service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, versionService, null, null);
} }
}); });

View File

@@ -10,16 +10,17 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FindListOptions, GetRequest } from './request.models'; import { FindListOptions } from './request.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { Version } from '../shared/version.model'; import { Version } from '../shared/version.model';
import { map, switchMap, take } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { VERSION_HISTORY } from '../shared/version-history.resource-type'; import { VERSION_HISTORY } from '../shared/version-history.resource-type';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { VersionDataService } from './version-data.service';
/** /**
* Service responsible for handling requests related to the VersionHistory object * Service responsible for handling requests related to the VersionHistory object
@@ -37,6 +38,7 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected versionDataService: VersionDataService,
protected http: HttpClient, protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<VersionHistory>) { protected comparator: DefaultChangeAnalyzer<VersionHistory>) {
super(); super();
@@ -63,19 +65,18 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
* Get a version history's versions using paginated search options * Get a version history's versions using paginated search options
* @param versionHistoryId The version history's ID * @param versionHistoryId The version history's ID
* @param searchOptions The search options to use * @param searchOptions The search options to use
* @param linksToFollow HAL Links to follow on the Versions * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
getVersions(versionHistoryId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: FollowLinkConfig<Version>[]): Observable<RemoteData<PaginatedList<Version>>> { getVersions(versionHistoryId: string, searchOptions?: PaginatedSearchOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Version>[]): Observable<RemoteData<PaginatedList<Version>>> {
const hrefObs = this.getVersionsEndpoint(versionHistoryId).pipe( const hrefObs = this.getVersionsEndpoint(versionHistoryId).pipe(
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
); );
hrefObs.pipe(
take(1)
).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
});
return this.rdbService.buildList<Version>(hrefObs, ...linksToFollow); return this.versionDataService.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
} }

View File

@@ -96,33 +96,47 @@ describe('EPersonDataService', () => {
it('search by default scope (byMetadata) and no query', () => { it('search by default scope (byMetadata) and no query', () => {
service.searchByScope(null, ''); service.searchByScope(null, '');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', ''))] searchParams: [Object.assign(new RequestParam('query', encodeURIComponent('')))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true);
}); });
it('search metadata scope and no query', () => { it('search metadata scope and no query', () => {
service.searchByScope('metadata', ''); service.searchByScope('metadata', '');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', ''))] searchParams: [Object.assign(new RequestParam('query', encodeURIComponent('')))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true);
}); });
it('search metadata scope and with query', () => { it('search metadata scope and with query', () => {
service.searchByScope('metadata', 'test'); service.searchByScope('metadata', 'test');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', 'test'))] searchParams: [Object.assign(new RequestParam('query', encodeURIComponent('test')))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true);
}); });
it('search email scope and no query', () => { it('search email scope and no query', () => {
spyOn(service, 'getSearchByHref').and.returnValue(epersonsEndpoint);
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
service.searchByScope('email', ''); service.searchByScope('email', '');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('email', ''))] searchParams: [Object.assign(new RequestParam('email', encodeURIComponent('')))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byEmail', options, true); expect(service.getSearchByHref).toHaveBeenCalledWith('byEmail', options);
expect(service.findByHref).toHaveBeenCalledWith(epersonsEndpoint, true, true);
});
it('search email scope with a query', () => {
spyOn(service, 'getSearchByHref').and.returnValue(epersonsEndpoint);
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock));
service.searchByScope('email', EPersonMock.email);
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('email', encodeURIComponent(EPersonMock.email)))]
});
expect(service.getSearchByHref).toHaveBeenCalledWith('byEmail', options);
expect(service.findByHref).toHaveBeenCalledWith(epersonsEndpoint, true, true);
}); });
}); });
@@ -147,7 +161,7 @@ describe('EPersonDataService', () => {
it('should send PatchRequest with replace email operation', () => { it('should send PatchRequest with replace email operation', () => {
const operations = [{ op: 'replace', path: '/email', value: newEmail }]; const operations = [{ op: 'replace', path: '/email', value: newEmail }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations); const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
@@ -166,7 +180,7 @@ describe('EPersonDataService', () => {
it('should send PatchRequest with replace certificate operation', () => { it('should send PatchRequest with replace certificate operation', () => {
const operations = [{ op: 'replace', path: '/certificate', value: !EPersonMock.requireCertificate }]; const operations = [{ op: 'replace', path: '/certificate', value: !EPersonMock.requireCertificate }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations); const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
@@ -185,7 +199,7 @@ describe('EPersonDataService', () => {
it('should send PatchRequest with replace canLogIn operation', () => { it('should send PatchRequest with replace canLogIn operation', () => {
const operations = [{ op: 'replace', path: '/canLogIn', value: !EPersonMock.canLogIn }]; const operations = [{ op: 'replace', path: '/canLogIn', value: !EPersonMock.canLogIn }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations); const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
@@ -219,7 +233,7 @@ describe('EPersonDataService', () => {
{ op: 'replace', path: '/eperson.lastname/0/value', value: newLastName }, { op: 'replace', path: '/eperson.lastname/0/value', value: newLastName },
{ op: 'replace', path: '/eperson.firstname/0/value', value: newFirstName }]; { op: 'replace', path: '/eperson.firstname/0/value', value: newFirstName }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations); const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
}); });
@@ -278,7 +292,7 @@ describe('EPersonDataService', () => {
it('should send DeleteRequest', () => { it('should send DeleteRequest', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid); const expected = new DeleteRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
@@ -287,7 +301,7 @@ describe('EPersonDataService', () => {
service.createEPersonForToken(EPersonMock, 'test-token'); service.createEPersonForToken(EPersonMock, 'test-token');
const expected = new PostRequest(requestService.generateRequestId(), epersonsEndpoint + '?token=test-token', EPersonMock); const expected = new PostRequest(requestService.generateRequestId(), epersonsEndpoint + '?token=test-token', EPersonMock);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
describe('patchPasswordWithToken', () => { describe('patchPasswordWithToken', () => {
@@ -297,7 +311,7 @@ describe('EPersonDataService', () => {
const operation = Object.assign({ op: 'add', path: '/password', value: 'test-password' }); const operation = Object.assign({ op: 'add', path: '/password', value: 'test-password' });
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]); const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });

View File

@@ -3,14 +3,14 @@ import { Injectable } from '@angular/core';
import { createSelector, select, Store } from '@ngrx/store'; import { createSelector, select, Store } from '@ngrx/store';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, find, map, take } from 'rxjs/operators'; import { find, map, take } from 'rxjs/operators';
import { import {
EPeopleRegistryCancelEPersonAction, EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction EPeopleRegistryEditEPersonAction
} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions';
import { EPeopleRegistryState } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; import { EPeopleRegistryState } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.reducers';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { hasValue } from '../../shared/empty.util'; import { hasValue, hasNoValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
@@ -19,20 +19,16 @@ import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DataService } from '../data/data.service'; import { DataService } from '../data/data.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { PaginatedList } from '../data/paginated-list.model'; import { PaginatedList, buildPaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { import { FindListOptions, PatchRequest, PostRequest, } from '../data/request.models';
FindListOptions,
FindListRequest,
PatchRequest,
PostRequest,
} from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getRemoteDataPayload, getFirstSucceededRemoteData, } from '../shared/operators'; import { getRemoteDataPayload, getFirstSucceededRemoteData, } from '../shared/operators';
import { EPerson } from './models/eperson.model'; import { EPerson } from './models/eperson.model';
import { EPERSON } from './models/eperson.resource-type'; import { EPERSON } from './models/eperson.resource-type';
import { NoContent } from '../shared/NoContent.model'; import { NoContent } from '../shared/NoContent.model';
import { PageInfo } from '../shared/page-info.model';
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry; const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson); const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
@@ -61,23 +57,6 @@ export class EPersonDataService extends DataService<EPerson> {
super(); super();
} }
/**
* Retrieves all EPeople
* @param options The options info used to retrieve the EPeople
*/
public getEPeople(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
const hrefObs = this.getFindAllHref(options, this.linkPath);
hrefObs.pipe(
filter((href: string) => hasValue(href)),
take(1))
.subscribe((href: string) => {
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request);
});
return this.rdbService.buildList<EPerson>(hrefObs) as Observable<RemoteData<PaginatedList<EPerson>>>;
}
/** /**
* Search the EPeople with a given scope and query * Search the EPeople with a given scope and query
* @param scope Scope of the EPeople search, default byMetadata * @param scope Scope of the EPeople search, default byMetadata
@@ -89,38 +68,75 @@ export class EPersonDataService extends DataService<EPerson> {
case 'metadata': case 'metadata':
return this.getEpeopleByMetadata(query.trim(), options); return this.getEpeopleByMetadata(query.trim(), options);
case 'email': case 'email':
return this.getEpeopleByEmail(query.trim(), options); return this.getEPersonByEmail(query.trim()).pipe(
map((rd: RemoteData<EPerson | NoContent>) => {
if (rd.hasSucceeded) {
// Turn the single EPerson or NoContent in to a PaginatedList<EPerson>
let page;
if (rd.statusCode === 204 || hasNoValue(rd.payload)) {
page = [];
} else {
page = [rd.payload];
}
return new RemoteData<PaginatedList<EPerson>>(
rd.timeCompleted,
rd.msToLive,
rd.lastUpdated,
rd.state,
rd.errorMessage,
buildPaginatedList(new PageInfo({
elementsPerPage: options.elementsPerPage,
totalElements: page.length,
totalPages: page.length,
currentPage: 1
}), page),
rd.statusCode
);
} else {
// If it hasn't succeeded, there can be no payload, so we can re-cast the existing
// RemoteData object
return rd as RemoteData<PaginatedList<EPerson>>;
}
})
);
default: default:
return this.getEpeopleByMetadata(query.trim(), options); return this.getEpeopleByMetadata(query.trim(), options);
} }
} }
/** /**
* Returns a search result list of EPeople, by email query (/eperson/epersons/search/{@link searchByEmailPath}?email=<>) * Returns a single EPerson, by email query (/eperson/epersons/search/{@link searchByEmailPath}?email=<>). If it can be found
* NoContent otherwise
*
* @param query email query * @param query email query
* @param options * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * no valid cached version. Defaults to true
* the response becomes stale * @param reRequestOnStale Whether or not the request should automatically be re-
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * requested after the response becomes stale
* should be automatically resolved * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
private getEpeopleByEmail(query: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<PaginatedList<EPerson>>> { public getEPersonByEmail(query: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<EPerson | NoContent>> {
const searchParams = [new RequestParam('email', query)]; const findListOptions = new FindListOptions();
return this.getEPeopleBy(searchParams, this.searchByEmailPath, options, reRequestOnStale, ...linksToFollow); findListOptions.searchParams = [new RequestParam('email', encodeURIComponent(query))];
const href$ = this.getSearchByHref(this.searchByEmailPath, findListOptions, ...linksToFollow);
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
* Returns a search result list of EPeople, by metadata query (/eperson/epersons/search/{@link searchByMetadataPath}?query=<>) * Returns a search result list of EPeople, by metadata query (/eperson/epersons/search/{@link searchByMetadataPath}?query=<>)
* @param query metadata query * @param query metadata query
* @param options * @param options
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
private getEpeopleByMetadata(query: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<PaginatedList<EPerson>>> { private getEpeopleByMetadata(query: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<PaginatedList<EPerson>>> {
const searchParams = [new RequestParam('query', query)]; const searchParams = [new RequestParam('query', encodeURIComponent(query))];
return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, reRequestOnStale, ...linksToFollow); return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -128,12 +144,14 @@ export class EPersonDataService extends DataService<EPerson> {
* @param searchParams query parameters in the search * @param searchParams query parameters in the search
* @param searchMethod searchBy path * @param searchMethod searchBy path
* @param options * @param options
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
private getEPeopleBy(searchParams: RequestParam[], searchMethod: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<PaginatedList<EPerson>>> { private getEPeopleBy(searchParams: RequestParam[], searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<PaginatedList<EPerson>>> {
let findListOptions = new FindListOptions(); let findListOptions = new FindListOptions();
if (options) { if (options) {
findListOptions = Object.assign(new FindListOptions(), options); findListOptions = Object.assign(new FindListOptions(), options);
@@ -143,7 +161,7 @@ export class EPersonDataService extends DataService<EPerson> {
} else { } else {
findListOptions.searchParams = searchParams; findListOptions.searchParams = searchParams;
} }
return this.searchBy(searchMethod, findListOptions, reRequestOnStale, ...linksToFollow); return this.searchBy(searchMethod, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -153,17 +171,16 @@ export class EPersonDataService extends DataService<EPerson> {
*/ */
public updateEPerson(ePerson: EPerson): Observable<RemoteData<EPerson>> { public updateEPerson(ePerson: EPerson): Observable<RemoteData<EPerson>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const oldVersion$ = this.findByHref(ePerson._links.self.href); const oldVersion$ = this.findByHref(ePerson._links.self.href, true, false);
oldVersion$.pipe( oldVersion$.pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
map((oldEPerson: EPerson) => { take(1)
).subscribe((oldEPerson: EPerson) => {
const operations = this.generateOperations(oldEPerson, ePerson); const operations = this.generateOperations(oldEPerson, ePerson);
const patchRequest = new PatchRequest(requestId, ePerson._links.self.href, operations); const patchRequest = new PatchRequest(requestId, ePerson._links.self.href, operations);
return this.requestService.configure(patchRequest); return this.requestService.send(patchRequest);
}), });
take(1)
).subscribe();
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
@@ -274,11 +291,10 @@ export class EPersonDataService extends DataService<EPerson> {
map((href: string) => `${href}?token=${token}`)); map((href: string) => `${href}?token=${token}`));
hrefObs.pipe( hrefObs.pipe(
find((href: string) => hasValue(href)), find((href: string) => hasValue(href)),
map((href: string) => { ).subscribe((href: string) => {
const request = new PostRequest(requestId, href, eperson); const request = new PostRequest(requestId, href, eperson);
this.requestService.configure(request); this.requestService.send(request);
}) });
).subscribe();
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
@@ -301,11 +317,10 @@ export class EPersonDataService extends DataService<EPerson> {
hrefObs.pipe( hrefObs.pipe(
find((href: string) => hasValue(href)), find((href: string) => hasValue(href)),
map((href: string) => { ).subscribe((href: string) => {
const request = new PatchRequest(requestId, href, [operation]); const request = new PatchRequest(requestId, href, [operation]);
this.requestService.configure(request); this.requestService.send(request);
}) });
).subscribe();
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }

View File

@@ -94,7 +94,7 @@ describe('GroupDataService', () => {
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', ''))] searchParams: [Object.assign(new RequestParam('query', ''))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true);
}); });
it('search with query', () => { it('search with query', () => {
@@ -102,7 +102,7 @@ describe('GroupDataService', () => {
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', 'test'))] searchParams: [Object.assign(new RequestParam('query', 'test'))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true);
}); });
}); });
@@ -116,7 +116,7 @@ describe('GroupDataService', () => {
headers = headers.append('Content-Type', 'text/uri-list'); headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers; options.headers = headers;
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options); const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
@@ -126,7 +126,7 @@ describe('GroupDataService', () => {
}); });
it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => { it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id); const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
@@ -140,7 +140,7 @@ describe('GroupDataService', () => {
headers = headers.append('Content-Type', 'text/uri-list'); headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers; options.headers = headers;
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options); const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });
@@ -150,7 +150,7 @@ describe('GroupDataService', () => {
}); });
it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => { it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id); const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
}); });

View File

@@ -10,7 +10,6 @@ import {
} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; } from '../../+admin/admin-access-control/group-registry/group-registry.actions';
import { GroupRegistryState } from '../../+admin/admin-access-control/group-registry/group-registry.reducers'; import { GroupRegistryState } from '../../+admin/admin-access-control/group-registry/group-registry.reducers';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -20,13 +19,7 @@ import { DataService } from '../data/data.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { PaginatedList } from '../data/paginated-list.model'; import { PaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { import { CreateRequest, DeleteRequest, FindListOptions, PostRequest } from '../data/request.models';
CreateRequest,
DeleteRequest,
FindListOptions,
FindListRequest,
PostRequest
} from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
@@ -71,34 +64,19 @@ export class GroupDataService extends DataService<Group> {
super(); super();
} }
/**
* Retrieves all groups
* @param pagination The pagination info used to retrieve the groups
*/
public getGroups(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<Group>[]): Observable<RemoteData<PaginatedList<Group>>> {
const hrefObs = this.getFindAllHref(options, this.linkPath, ...linksToFollow);
hrefObs.pipe(
filter((href: string) => hasValue(href)),
take(1))
.subscribe((href: string) => {
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request);
});
return this.rdbService.buildList<Group>(hrefObs) as Observable<RemoteData<PaginatedList<Group>>>;
}
/** /**
* Returns a search result list of groups, with certain query (searches in group name and by exact uuid) * Returns a search result list of groups, with certain query (searches in group name and by exact uuid)
* Endpoint used: /eperson/groups/search/byMetadata?query=<:name> * Endpoint used: /eperson/groups/search/byMetadata?query=<:name>
* @param query search query param * @param query search query param
* @param options * @param options
* @param reRequestOnStale Whether or not the request should automatically be re-requested after * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* the response becomes stale * no valid cached version. Defaults to true
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * @param reRequestOnStale Whether or not the request should automatically be re-
* should be automatically resolved * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/ */
public searchGroups(query: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Group>[]): Observable<RemoteData<PaginatedList<Group>>> { public searchGroups(query: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Group>[]): Observable<RemoteData<PaginatedList<Group>>> {
const searchParams = [new RequestParam('query', query)]; const searchParams = [new RequestParam('query', query)];
let findListOptions = new FindListOptions(); let findListOptions = new FindListOptions();
if (options) { if (options) {
@@ -109,7 +87,7 @@ export class GroupDataService extends DataService<Group> {
} else { } else {
findListOptions.searchParams = searchParams; findListOptions.searchParams = searchParams;
} }
return this.searchBy('byMetadata', findListOptions, reRequestOnStale, ...linksToFollow); return this.searchBy('byMetadata', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**
@@ -144,7 +122,7 @@ export class GroupDataService extends DataService<Group> {
headers = headers.append('Content-Type', 'text/uri-list'); headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers; options.headers = headers;
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options);
this.requestService.configure(postRequest); this.requestService.send(postRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
@@ -157,7 +135,7 @@ export class GroupDataService extends DataService<Group> {
deleteSubGroupFromGroup(activeGroup: Group, subgroup: Group): Observable<RemoteData<Group>> { deleteSubGroupFromGroup(activeGroup: Group, subgroup: Group): Observable<RemoteData<Group>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id);
this.requestService.configure(deleteRequest); this.requestService.send(deleteRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
@@ -174,7 +152,7 @@ export class GroupDataService extends DataService<Group> {
headers = headers.append('Content-Type', 'text/uri-list'); headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers; options.headers = headers;
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options);
this.requestService.configure(postRequest); this.requestService.send(postRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
@@ -187,7 +165,7 @@ export class GroupDataService extends DataService<Group> {
deleteMemberFromGroup(activeGroup: Group, ePerson: EPerson): Observable<RemoteData<Group>> { deleteMemberFromGroup(activeGroup: Group, ePerson: EPerson): Observable<RemoteData<Group>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id);
this.requestService.configure(deleteRequest); this.requestService.send(deleteRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
@@ -298,7 +276,7 @@ export class GroupDataService extends DataService<Group> {
}, },
}); });
this.requestService.configure( this.requestService.send(
new CreateRequest( new CreateRequest(
requestId, requestId,
link, link,
@@ -327,7 +305,7 @@ export class GroupDataService extends DataService<Group> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
this.requestService.configure( this.requestService.send(
new DeleteRequest( new DeleteRequest(
requestId, requestId,
link, link,

View File

@@ -7,6 +7,7 @@ import { AddToObjectCacheAction } from '../cache/object-cache.actions';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { AddToIndexAction } from './index.actions'; import { AddToIndexAction } from './index.actions';
import { IndexName } from './index.reducer'; import { IndexName } from './index.reducer';
import { provideMockStore } from '@ngrx/store/testing';
describe('ObjectUpdatesEffects', () => { describe('ObjectUpdatesEffects', () => {
let indexEffects: UUIDIndexEffects; let indexEffects: UUIDIndexEffects;
@@ -18,6 +19,7 @@ describe('ObjectUpdatesEffects', () => {
let alternativeLink; let alternativeLink;
let selfLink; let selfLink;
let otherLink; let otherLink;
let initialState;
function init() { function init() {
selfLink = 'rest.org/items/6ca6549c-3db2-4288-8ce4-4a3bce011860'; selfLink = 'rest.org/items/6ca6549c-3db2-4288-8ce4-4a3bce011860';
@@ -34,6 +36,15 @@ describe('ObjectUpdatesEffects', () => {
msToLive = 90000; msToLive = 90000;
requestUUID = '324e5a5c-06f7-428d-b3ba-cc322c5dde39'; requestUUID = '324e5a5c-06f7-428d-b3ba-cc322c5dde39';
alternativeLink = 'rest.org/alternative-link/1234'; alternativeLink = 'rest.org/alternative-link/1234';
initialState = {
core: {
index: {
[IndexName.REQUEST]: {
[selfLink]: requestUUID
}
}
}
};
} }
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
@@ -42,6 +53,7 @@ describe('ObjectUpdatesEffects', () => {
providers: [ providers: [
UUIDIndexEffects, UUIDIndexEffects,
provideMockActions(() => actions), provideMockActions(() => actions),
provideMockStore({ initialState }),
], ],
}); });
})); }));

View File

@@ -1,14 +1,24 @@
import { filter, map } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
import { AddToObjectCacheAction, ObjectCacheActionTypes, RemoveFromObjectCacheAction } from '../cache/object-cache.actions'; import {
import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions'; AddToObjectCacheAction,
ObjectCacheActionTypes,
RemoveFromObjectCacheAction
} from '../cache/object-cache.actions';
import {
RequestActionTypes,
RequestConfigureAction,
RequestStaleAction
} from '../data/request.actions';
import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'; import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { IndexName } from './index.reducer'; import { IndexName } from './index.reducer';
import { RestRequestMethod } from '../data/rest-request-method'; import { RestRequestMethod } from '../data/rest-request-method';
import { getUrlWithoutEmbedParams } from './index.selectors'; import { getUrlWithoutEmbedParams, uuidFromHrefSelector } from './index.selectors';
import { Store, select } from '@ngrx/store';
import { CoreState } from '../core.reducers';
@Injectable() @Injectable()
export class UUIDIndexEffects { export class UUIDIndexEffects {
@@ -63,16 +73,30 @@ export class UUIDIndexEffects {
.pipe( .pipe(
ofType(RequestActionTypes.CONFIGURE), ofType(RequestActionTypes.CONFIGURE),
filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.GET), filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.GET),
map((action: RequestConfigureAction) => { switchMap((action: RequestConfigureAction) => {
return new AddToIndexAction( const href = getUrlWithoutEmbedParams(action.payload.href);
return this.store.pipe(
select(uuidFromHrefSelector(href)),
take(1),
map((uuid: string) => [action, uuid])
);
}
),
switchMap(([action, uuid]: [RequestConfigureAction, string]) => {
let actions = [];
if (hasValue(uuid)) {
actions = [new RequestStaleAction(uuid)];
}
actions = [...actions, new AddToIndexAction(
IndexName.REQUEST, IndexName.REQUEST,
getUrlWithoutEmbedParams(action.payload.href), getUrlWithoutEmbedParams(action.payload.href),
action.payload.uuid action.payload.uuid
); )];
return actions;
}) })
); );
constructor(private actions$: Actions) { constructor(private actions$: Actions, private store: Store<CoreState>) {
} }

View File

@@ -1,7 +1,11 @@
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { IndexName, indexReducer, MetaIndexState } from './index.reducer'; import { IndexName, indexReducer, MetaIndexState } from './index.reducer';
import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions'; import {
AddToIndexAction,
RemoveFromIndexBySubstringAction,
RemoveFromIndexByValueAction
} from './index.actions';
class NullAction extends AddToIndexAction { class NullAction extends AddToIndexAction {
type = null; type = null;
@@ -24,8 +28,6 @@ describe('requestReducer', () => {
[key1]: val1 [key1]: val1
}, [IndexName.REQUEST]: { }, [IndexName.REQUEST]: {
[key1]: val1 [key1]: val1
}, [IndexName.UUID_MAPPING]: {
[key1]: val1
} }
}; };
deepFreeze(testState); deepFreeze(testState);

View File

@@ -16,13 +16,6 @@ export enum IndexName {
// contains all requests in the request cache indexed by UUID // contains all requests in the request cache indexed by UUID
REQUEST = 'get-request/href-to-uuid', REQUEST = 'get-request/href-to-uuid',
/**
* Contains the UUIDs of requests that were sent to the server and
* have their responses cached, indexed by the UUIDs of requests that
* weren't sent because the response they requested was already cached
*/
UUID_MAPPING = 'get-request/configured-to-cache-uuid',
/** /**
* Contains the alternative link for an objects * Contains the alternative link for an objects
* Maps these link on to their matching self link in the object cache * Maps these link on to their matching self link in the object cache

View File

@@ -82,17 +82,6 @@ export const alternativeLinkIndexSelector: MemoizedSelector<CoreState, IndexStat
(state: MetaIndexState) => state[IndexName.ALTERNATIVE_OBJECT_LINK] (state: MetaIndexState) => state[IndexName.ALTERNATIVE_OBJECT_LINK]
); );
/**
* Return the request UUID mapping index based on the MetaIndexState
*
* @returns
* a MemoizedSelector to select the request UUID mapping
*/
export const requestUUIDIndexSelector: MemoizedSelector<CoreState, IndexState> = createSelector(
metaIndexSelector,
(state: MetaIndexState) => state[IndexName.UUID_MAPPING]
);
/** /**
* Return the self link of an object in the object-cache based on its UUID * Return the self link of an object in the object-cache based on its UUID
* *
@@ -121,21 +110,6 @@ export const uuidFromHrefSelector =
(state: IndexState) => hasValue(state) ? state[getUrlWithoutEmbedParams(href)] : undefined (state: IndexState) => hasValue(state) ? state[getUrlWithoutEmbedParams(href)] : undefined
); );
/**
* Return the UUID of a cached request based on the UUID of a request
* that wasn't sent because the response was already cached
*
* @param uuid
* The UUID of the new request
* @returns
* a MemoizedSelector to select the UUID of the cached request
*/
export const originalRequestUUIDFromRequestUUIDSelector =
(uuid: string): MemoizedSelector<CoreState, string> => createSelector(
requestUUIDIndexSelector,
(state: IndexState) => hasValue(state) ? state[uuid] : undefined
);
/** /**
* Return the self link of an object based on its alternative link * Return the self link of an object based on its alternative link
* *

View File

@@ -145,12 +145,12 @@ describe('JsonPatchOperationsService test suite', () => {
expect((service as any).submitJsonPatchOperations).toHaveBeenCalled(); expect((service as any).submitJsonPatchOperations).toHaveBeenCalled();
}); });
it('should configure a new SubmissionPatchRequest', () => { it('should send a new SubmissionPatchRequest', () => {
const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody); const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody);
scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should dispatch a new StartTransactionPatchOperationsAction', () => { it('should dispatch a new StartTransactionPatchOperationsAction', () => {
@@ -235,12 +235,12 @@ describe('JsonPatchOperationsService test suite', () => {
expect((service as any).submitJsonPatchOperations).toHaveBeenCalled(); expect((service as any).submitJsonPatchOperations).toHaveBeenCalled();
}); });
it('should configure a new SubmissionPatchRequest', () => { it('should send a new SubmissionPatchRequest', () => {
const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody); const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody);
scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should dispatch a new StartTransactionPatchOperationsAction', () => { it('should dispatch a new StartTransactionPatchOperationsAction', () => {

Some files were not shown because too many files have changed in this diff Show More