mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into collection-thumbnail-embed
This commit is contained in:
16
.gitattributes
vendored
16
.gitattributes
vendored
@@ -1,2 +1,16 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
# By default, auto detect text files and perform LF normalization
|
||||||
|
# This ensures code is always checked in with LF line endings
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
|
# JS and TS files must always use LF for Angular tools to work
|
||||||
|
# Some Angular tools expect LF line endings, even on Windows.
|
||||||
|
# This ensures Windows always checks out these files with LF line endings
|
||||||
|
# We've copied many of these rules from https://github.com/angular/angular-cli/
|
||||||
|
*.js eol=lf
|
||||||
|
*.ts eol=lf
|
||||||
|
*.json eol=lf
|
||||||
|
*.json5 eol=lf
|
||||||
|
*.css eol=lf
|
||||||
|
*.scss eol=lf
|
||||||
|
*.html eol=lf
|
||||||
|
*.svg eol=lf
|
@@ -260,3 +260,10 @@ mediaViewer:
|
|||||||
info:
|
info:
|
||||||
enableEndUserAgreement: true
|
enableEndUserAgreement: true
|
||||||
enablePrivacyStatement: true
|
enablePrivacyStatement: true
|
||||||
|
# Home Page
|
||||||
|
homePage:
|
||||||
|
recentSubmissions:
|
||||||
|
# The number of item showing in recent submission components
|
||||||
|
pageSize: 5
|
||||||
|
# Sort record of recent submission
|
||||||
|
sortField: 'dc.date.accessioned'
|
||||||
|
1
cypress/.gitignore
vendored
1
cypress/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
screenshots/
|
screenshots/
|
||||||
videos/
|
videos/
|
||||||
|
downloads/
|
||||||
|
@@ -7,8 +7,9 @@ const appConfig: AppConfig = buildAppConfig();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
|
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
|
||||||
|
* Any CLI arguments given to this script are patched through to `ng serve` as well.
|
||||||
*/
|
*/
|
||||||
child.spawn(
|
child.spawn(
|
||||||
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl}`,
|
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')}`,
|
||||||
{ stdio: 'inherit', shell: true }
|
{ stdio: 'inherit', shell: true }
|
||||||
);
|
);
|
||||||
|
0
scripts/sync-i18n-files.ts
Executable file → Normal file
0
scripts/sync-i18n-files.ts
Executable file → Normal file
@@ -22,7 +22,13 @@ console.log(`...Testing connection to REST API at ${restUrl}...\n`);
|
|||||||
if (appConfig.rest.ssl) {
|
if (appConfig.rest.ssl) {
|
||||||
const req = https.request(restUrl, (res) => {
|
const req = https.request(restUrl, (res) => {
|
||||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||||
res.on('data', (data) => {
|
// We will keep reading data until the 'end' event fires.
|
||||||
|
// This ensures we don't just read the first chunk.
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
checkJSONResponse(data);
|
checkJSONResponse(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -35,7 +41,13 @@ if (appConfig.rest.ssl) {
|
|||||||
} else {
|
} else {
|
||||||
const req = http.request(restUrl, (res) => {
|
const req = http.request(restUrl, (res) => {
|
||||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||||
res.on('data', (data) => {
|
// We will keep reading data until the 'end' event fires.
|
||||||
|
// This ensures we don't just read the first chunk.
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
checkJSONResponse(data);
|
checkJSONResponse(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -177,7 +177,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
getGroupRegistryRouterLink: ''
|
getGroupRegistryRouterLink: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -265,7 +265,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
if (eperson != null) {
|
if (eperson != null) {
|
||||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, {
|
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: this.config.pageSize
|
elementsPerPage: this.config.pageSize
|
||||||
});
|
});
|
||||||
@@ -297,7 +297,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
switchMap(([eperson, findListOptions]) => {
|
switchMap(([eperson, findListOptions]) => {
|
||||||
if (eperson != null) {
|
if (eperson != null) {
|
||||||
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||||
}
|
}
|
||||||
return observableOf(undefined);
|
return observableOf(undefined);
|
||||||
})
|
})
|
||||||
@@ -554,7 +554,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private updateGroups(options) {
|
private updateGroups(options) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
|
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,7 +53,7 @@ describe('MembersListComponent', () => {
|
|||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
epersonMembers: epersonMembers,
|
epersonMembers: epersonMembers,
|
||||||
subgroupMembers: subgroupMembers,
|
subgroupMembers: subgroupMembers,
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||||
},
|
},
|
||||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
@@ -129,7 +129,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.set(SubKey.MembersDTO,
|
this.subs.set(SubKey.MembersDTO,
|
||||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
switchMap((currentPagination) => {
|
switchMap((currentPagination) => {
|
||||||
return this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
|
||||||
currentPage: currentPagination.currentPage,
|
currentPage: currentPagination.currentPage,
|
||||||
elementsPerPage: currentPagination.pageSize
|
elementsPerPage: currentPagination.pageSize
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||||
mergeMap((group: Group) => {
|
mergeMap((group: Group) => {
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
|
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: 9999
|
elementsPerPage: 9999
|
||||||
}, false)
|
}, false)
|
||||||
|
@@ -65,7 +65,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
getSubgroups(): Group {
|
getSubgroups(): Group {
|
||||||
return this.activeGroup;
|
return this.activeGroup;
|
||||||
},
|
},
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.subgroups$.pipe(
|
return this.subgroups$.pipe(
|
||||||
map((currentGroups: Group[]) => {
|
map((currentGroups: Group[]) => {
|
||||||
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
||||||
|
@@ -115,7 +115,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.set(
|
this.subs.set(
|
||||||
SubKey.Members,
|
SubKey.Members,
|
||||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
switchMap((config) => this.groupDataService.findListByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||||
currentPage: config.currentPage,
|
currentPage: config.currentPage,
|
||||||
elementsPerPage: config.pageSize
|
elementsPerPage: config.pageSize
|
||||||
},
|
},
|
||||||
@@ -139,7 +139,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
||||||
return observableOf(false);
|
return observableOf(false);
|
||||||
} else {
|
} else {
|
||||||
return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
|
return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: 9999
|
elementsPerPage: 9999
|
||||||
})
|
})
|
||||||
|
@@ -69,7 +69,7 @@ describe('GroupRegistryComponent', () => {
|
|||||||
mockGroups = [GroupMock, GroupMock2];
|
mockGroups = [GroupMock, GroupMock2];
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
switch (href) {
|
switch (href) {
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
||||||
@@ -97,7 +97,7 @@ describe('GroupRegistryComponent', () => {
|
|||||||
};
|
};
|
||||||
groupsDataServiceStub = {
|
groupsDataServiceStub = {
|
||||||
allGroups: mockGroups,
|
allGroups: mockGroups,
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
switch (href) {
|
switch (href) {
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
||||||
|
@@ -213,7 +213,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,7 +221,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.groupService.findAllByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<nav @slideHorizontal class="navbar navbar-dark p-0"
|
<nav class="navbar navbar-dark p-0"
|
||||||
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
||||||
[@slideSidebar]="{
|
[@slideSidebar]="{
|
||||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||||
|
@@ -2,7 +2,7 @@ import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
|||||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||||
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
import { slideSidebar } from '../../shared/animations/slide';
|
||||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
@@ -18,7 +18,7 @@ import { ThemeService } from '../../shared/theme-support/theme.service';
|
|||||||
selector: 'ds-admin-sidebar',
|
selector: 'ds-admin-sidebar',
|
||||||
templateUrl: './admin-sidebar.component.html',
|
templateUrl: './admin-sidebar.component.html',
|
||||||
styleUrls: ['./admin-sidebar.component.scss'],
|
styleUrls: ['./admin-sidebar.component.scss'],
|
||||||
animations: [slideHorizontal, slideSidebar]
|
animations: [slideSidebar]
|
||||||
})
|
})
|
||||||
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
|
0
src/app/app.module.ts
Executable file → Normal file
0
src/app/app.module.ts
Executable file → Normal file
@@ -16,7 +16,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
|||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||||
import { EntityTypeService } from '../../core/data/entity-type.service';
|
import { EntityTypeDataService } from '../../core/data/entity-type-data.service';
|
||||||
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
|
||||||
import { MetadataValue } from '../../core/shared/metadata.models';
|
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||||
import { getFirstSucceededRemoteListPayload } from '../../core/shared/operators';
|
import { getFirstSucceededRemoteListPayload } from '../../core/shared/operators';
|
||||||
@@ -61,7 +61,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
|||||||
protected dsoService: CommunityDataService,
|
protected dsoService: CommunityDataService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected entityTypeService: EntityTypeService) {
|
protected entityTypeService: EntityTypeDataService) {
|
||||||
super(formService, translate, notificationsService, authService, requestService, objectCache);
|
super(formService, translate, notificationsService, authService, requestService, objectCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -94,7 +94,7 @@ describe('CollectionItemMapperComponent', () => {
|
|||||||
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
|
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
|
||||||
const itemDataServiceStub = {
|
const itemDataServiceStub = {
|
||||||
mapToCollection: () => createSuccessfulRemoteDataObject$({}),
|
mapToCollection: () => createSuccessfulRemoteDataObject$({}),
|
||||||
findAllByHref: () => observableOf(emptyList)
|
findListByHref: () => observableOf(emptyList),
|
||||||
};
|
};
|
||||||
const activatedRouteStub = {
|
const activatedRouteStub = {
|
||||||
parent: {
|
parent: {
|
||||||
@@ -152,7 +152,7 @@ describe('CollectionItemMapperComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
||||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
getGroupRegistryRouterLink: '',
|
getGroupRegistryRouterLink: '',
|
||||||
getUUIDFromString: '',
|
getUUIDFromString: '',
|
||||||
});
|
});
|
||||||
|
@@ -143,7 +143,7 @@ export class CollectionItemMapperComponent implements OnInit {
|
|||||||
if (shouldUpdate === true) {
|
if (shouldUpdate === true) {
|
||||||
this.shouldUpdate$.next(false);
|
this.shouldUpdate$.next(false);
|
||||||
}
|
}
|
||||||
return this.itemDataService.findAllByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, {
|
return this.itemDataService.findListByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, {
|
||||||
sort: this.defaultSortOptions
|
sort: this.defaultSortOptions
|
||||||
}),!shouldUpdate, false, followLink('owningCollection')).pipe(
|
}),!shouldUpdate, false, followLink('owningCollection')).pipe(
|
||||||
getAllSucceededRemoteData()
|
getAllSucceededRemoteData()
|
||||||
|
@@ -13,7 +13,7 @@ import { Item } from '../../../core/shared/item.model';
|
|||||||
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
|
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||||
|
|
||||||
describe('CollectionMetadataComponent', () => {
|
describe('CollectionMetadataComponent', () => {
|
||||||
@@ -39,8 +39,8 @@ describe('CollectionMetadataComponent', () => {
|
|||||||
|
|
||||||
const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', {
|
const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', {
|
||||||
findByCollectionID: createSuccessfulRemoteDataObject$(template),
|
findByCollectionID: createSuccessfulRemoteDataObject$(template),
|
||||||
create: createSuccessfulRemoteDataObject$(template),
|
createByCollectionID: createSuccessfulRemoteDataObject$(template),
|
||||||
deleteByCollectionID: observableOf(true),
|
delete: observableOf(true),
|
||||||
getCollectionEndpoint: observableOf(collectionTemplateHref),
|
getCollectionEndpoint: observableOf(collectionTemplateHref),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,12 +91,12 @@ describe('CollectionMetadataComponent', () => {
|
|||||||
|
|
||||||
describe('deleteItemTemplate', () => {
|
describe('deleteItemTemplate', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true));
|
(itemTemplateService.delete as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
comp.deleteItemTemplate();
|
comp.deleteItemTemplate();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call ItemTemplateService.deleteByCollectionID', () => {
|
it('should call ItemTemplateService.delete', () => {
|
||||||
expect(itemTemplateService.deleteByCollectionID).toHaveBeenCalledWith(template, 'collection-id');
|
expect(itemTemplateService.delete).toHaveBeenCalledWith(template.uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when delete returns a success', () => {
|
describe('when delete returns a success', () => {
|
||||||
@@ -107,7 +107,7 @@ describe('CollectionMetadataComponent', () => {
|
|||||||
|
|
||||||
describe('when delete returns a failure', () => {
|
describe('when delete returns a failure', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(false));
|
(itemTemplateService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||||
comp.deleteItemTemplate();
|
comp.deleteItemTemplate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -7,12 +7,14 @@ import { ItemTemplateDataService } from '../../../core/data/item-template-data.s
|
|||||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||||
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for editing a collection's metadata
|
* Component for editing a collection's metadata
|
||||||
@@ -65,7 +67,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
const template$ = collection$.pipe(
|
const template$ = collection$.pipe(
|
||||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid).pipe(
|
switchMap((collection: Collection) => this.itemTemplateService.createByCollectionID(new Item(), collection.uuid).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -83,18 +85,15 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
|||||||
* Delete the item template from the collection
|
* Delete the item template from the collection
|
||||||
*/
|
*/
|
||||||
deleteItemTemplate() {
|
deleteItemTemplate() {
|
||||||
const collection$ = this.dsoRD$.pipe(
|
this.dsoRD$.pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
);
|
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)),
|
||||||
const template$ = collection$.pipe(
|
getFirstSucceededRemoteDataPayload(),
|
||||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid).pipe(
|
switchMap((template) => {
|
||||||
getFirstSucceededRemoteDataPayload(),
|
return this.itemTemplateService.delete(template.uuid);
|
||||||
)),
|
}),
|
||||||
);
|
getFirstCompletedRemoteData(),
|
||||||
combineLatestObservable(collection$, template$).pipe(
|
map((response: RemoteData<NoContent>) => hasValue(response) && response.hasSucceeded),
|
||||||
switchMap(([collection, template]) => {
|
|
||||||
return this.itemTemplateService.deleteByCollectionID(template, collection.uuid);
|
|
||||||
})
|
|
||||||
).subscribe((success: boolean) => {
|
).subscribe((success: boolean) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success'));
|
this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success'));
|
||||||
|
@@ -135,7 +135,7 @@ describe('CommunityPageSubCollectionList Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
||||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
getGroupRegistryRouterLink: '',
|
getGroupRegistryRouterLink: '',
|
||||||
getUUIDFromString: '',
|
getUUIDFromString: '',
|
||||||
});
|
});
|
||||||
|
@@ -131,7 +131,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
||||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
getGroupRegistryRouterLink: '',
|
getGroupRegistryRouterLink: '',
|
||||||
getUUIDFromString: '',
|
getUUIDFromString: '',
|
||||||
});
|
});
|
||||||
|
@@ -7,57 +7,156 @@ import { TestScheduler } from 'rxjs/testing';
|
|||||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
import { ShortLivedToken } from './models/short-lived-token.model';
|
import { ShortLivedToken } from './models/short-lived-token.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import objectContaining = jasmine.objectContaining;
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
describe(`AuthRequestService`, () => {
|
describe(`AuthRequestService`, () => {
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let endpointURL: string;
|
let endpointURL: string;
|
||||||
|
let requestID: string;
|
||||||
let shortLivedToken: ShortLivedToken;
|
let shortLivedToken: ShortLivedToken;
|
||||||
let shortLivedTokenRD: RemoteData<ShortLivedToken>;
|
let shortLivedTokenRD: RemoteData<ShortLivedToken>;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let service: AuthRequestService;
|
let service;
|
||||||
let testScheduler;
|
let testScheduler;
|
||||||
|
|
||||||
class TestAuthRequestService extends AuthRequestService {
|
const status = new AuthStatus();
|
||||||
constructor(
|
|
||||||
hes: HALEndpointService,
|
|
||||||
rs: RequestService,
|
|
||||||
rdbs: RemoteDataBuildService
|
|
||||||
) {
|
|
||||||
super(hes, rs, rdbs);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
class TestAuthRequestService extends AuthRequestService {
|
||||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
constructor(
|
||||||
}
|
hes: HALEndpointService,
|
||||||
|
rs: RequestService,
|
||||||
|
rdbs: RemoteDataBuildService
|
||||||
|
) {
|
||||||
|
super(hes, rs, rdbs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const init = (cold: typeof TestScheduler.prototype.createColdObservable) => {
|
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||||
endpointURL = 'https://rest.api/auth';
|
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||||
shortLivedToken = Object.assign(new ShortLivedToken(), {
|
}
|
||||||
value: 'some-token'
|
}
|
||||||
});
|
|
||||||
shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken);
|
|
||||||
|
|
||||||
halService = jasmine.createSpyObj('halService', {
|
const init = (cold: typeof TestScheduler.prototype.createColdObservable) => {
|
||||||
'getEndpoint': cold('a', { a: endpointURL })
|
endpointURL = 'https://rest.api/auth';
|
||||||
});
|
requestID = 'requestID';
|
||||||
requestService = jasmine.createSpyObj('requestService', {
|
shortLivedToken = Object.assign(new ShortLivedToken(), {
|
||||||
'send': null
|
value: 'some-token'
|
||||||
});
|
});
|
||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken);
|
||||||
'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD })
|
|
||||||
});
|
|
||||||
|
|
||||||
service = new TestAuthRequestService(halService, requestService, rdbService);
|
halService = jasmine.createSpyObj('halService', {
|
||||||
};
|
'getEndpoint': cold('a', { a: endpointURL })
|
||||||
|
});
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
'generateRequestId': requestID,
|
||||||
|
'send': null,
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD })
|
||||||
|
});
|
||||||
|
|
||||||
|
service = new TestAuthRequestService(halService, requestService, rdbService);
|
||||||
|
|
||||||
|
spyOn(service as any, 'fetchRequest').and.returnValue(cold('a', { a: createSuccessfulRemoteDataObject(status) }));
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('REST request methods', () => {
|
||||||
|
let options: HttpOptions;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testScheduler = new TestScheduler((actual, expected) => {
|
options = Object.create({});
|
||||||
expect(actual).toEqual(expected);
|
});
|
||||||
|
|
||||||
|
describe('GET', () => {
|
||||||
|
it('should send a GET request to the right endpoint and return the auth status', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
init(cold);
|
||||||
|
|
||||||
|
expectObservable(service.getRequest('method', options)).toBe('a', {
|
||||||
|
a: objectContaining({ payload: status }),
|
||||||
|
});
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||||
|
uuid: requestID,
|
||||||
|
href: endpointURL + '/method',
|
||||||
|
method: RestRequestMethod.GET,
|
||||||
|
body: undefined,
|
||||||
|
options,
|
||||||
|
}));
|
||||||
|
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the request even if caller doesn\'t subscribe to the response', () => {
|
||||||
|
testScheduler.run(({ cold, flush }) => {
|
||||||
|
init(cold);
|
||||||
|
|
||||||
|
service.getRequest('method', options);
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||||
|
uuid: requestID,
|
||||||
|
href: endpointURL + '/method',
|
||||||
|
method: RestRequestMethod.GET,
|
||||||
|
body: undefined,
|
||||||
|
options,
|
||||||
|
}));
|
||||||
|
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST', () => {
|
||||||
|
it('should send a POST request to the right endpoint and return the auth status', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
init(cold);
|
||||||
|
|
||||||
|
expectObservable(service.postToEndpoint('method', { content: 'something' }, options)).toBe('a', {
|
||||||
|
a: objectContaining({ payload: status }),
|
||||||
|
});
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||||
|
uuid: requestID,
|
||||||
|
href: endpointURL + '/method',
|
||||||
|
method: RestRequestMethod.POST,
|
||||||
|
body: { content: 'something' },
|
||||||
|
options,
|
||||||
|
}));
|
||||||
|
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the request even if caller doesn\'t subscribe to the response', () => {
|
||||||
|
testScheduler.run(({ cold, flush }) => {
|
||||||
|
init(cold);
|
||||||
|
|
||||||
|
service.postToEndpoint('method', { content: 'something' }, options);
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||||
|
uuid: requestID,
|
||||||
|
href: endpointURL + '/method',
|
||||||
|
method: RestRequestMethod.POST,
|
||||||
|
body: { content: 'something' },
|
||||||
|
options,
|
||||||
|
}));
|
||||||
|
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe(`getShortlivedToken`, () => {
|
describe(`getShortlivedToken`, () => {
|
||||||
it(`should call createShortLivedTokenRequest with the url for the endpoint`, () => {
|
it(`should call createShortLivedTokenRequest with the url for the endpoint`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, switchMap, tap, take } from 'rxjs/operators';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
@@ -27,8 +27,13 @@ export abstract class AuthRequestService {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fetchRequest(request: RestRequest, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): Observable<RemoteData<AuthStatus>> {
|
/**
|
||||||
return this.rdbService.buildFromRequestUUID<AuthStatus>(request.uuid, ...linksToFollow).pipe(
|
* Fetch the response to a request from the cache, once it's completed.
|
||||||
|
* @param requestId the UUID of the request for which to retrieve the response
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected fetchRequest(requestId: string, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): Observable<RemoteData<AuthStatus>> {
|
||||||
|
return this.rdbService.buildFromRequestUUID<AuthStatus>(requestId, ...linksToFollow).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -44,28 +49,48 @@ export abstract class AuthRequestService {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST request to an authentication endpoint
|
||||||
|
* @param method the method to send to (e.g. 'status')
|
||||||
|
* @param body the data to send (optional)
|
||||||
|
* @param options the HTTP options for the request
|
||||||
|
*/
|
||||||
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
|
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
|
||||||
return this.halService.getEndpoint(this.linkName).pipe(
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
this.halService.getEndpoint(this.linkName).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
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(requestId, endpointURL, body, options)),
|
||||||
tap((request: PostRequest) => this.requestService.send(request)),
|
take(1)
|
||||||
mergeMap((request: PostRequest) => this.fetchRequest(request)),
|
).subscribe((request: PostRequest) => {
|
||||||
distinctUntilChanged());
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.fetchRequest(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a GET request to an authentication endpoint
|
||||||
|
* @param method the method to send to (e.g. 'status')
|
||||||
|
* @param options the HTTP options for the request
|
||||||
|
*/
|
||||||
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
|
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
|
||||||
return this.halService.getEndpoint(this.linkName).pipe(
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
this.halService.getEndpoint(this.linkName).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
|
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)),
|
map((endpointURL: string) => new GetRequest(requestId, endpointURL, undefined, options)),
|
||||||
tap((request: GetRequest) => this.requestService.send(request)),
|
take(1)
|
||||||
mergeMap((request: GetRequest) => this.fetchRequest(request, ...linksToFollow)),
|
).subscribe((request: GetRequest) => {
|
||||||
distinctUntilChanged());
|
this.requestService.send(request);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
return this.fetchRequest(requestId, ...linksToFollow);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
* Factory function to create the request object to send. This needs to be a POST client side and
|
||||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
||||||
|
@@ -17,6 +17,7 @@ import {
|
|||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { AuthMethodType } from './models/auth.method-type';
|
import { AuthMethodType } from './models/auth.method-type';
|
||||||
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auth state.
|
* The auth state.
|
||||||
@@ -251,6 +252,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
idle: false,
|
idle: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case StoreActionTypes.REHYDRATE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
blocking: true,
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@@ -2,23 +2,26 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
||||||
import { DataService } from '../data/data.service';
|
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators';
|
||||||
import { getRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators';
|
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { ChildHALResource } from '../shared/child-hal-resource.model';
|
import { ChildHALResource } from '../shared/child-hal-resource.model';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
|
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
|
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
|
||||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService<T>) {
|
protected constructor(
|
||||||
|
protected breadcrumbService: DSOBreadcrumbsService,
|
||||||
|
protected dataService: IdentifiableDataService<T>,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +39,7 @@ export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceO
|
|||||||
if (hasValue(object)) {
|
if (hasValue(object)) {
|
||||||
const fullPath = state.url;
|
const fullPath = state.url;
|
||||||
const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid;
|
const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid;
|
||||||
return {provider: this.breadcrumbService, key: object, url: url};
|
return { provider: this.breadcrumbService, key: object, url: url };
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@@ -5,15 +5,9 @@ import { FindListOptions } from '../data/find-list-options.model';
|
|||||||
|
|
||||||
describe(`BrowseDefinitionDataService`, () => {
|
describe(`BrowseDefinitionDataService`, () => {
|
||||||
let service: BrowseDefinitionDataService;
|
let service: BrowseDefinitionDataService;
|
||||||
const dataServiceImplSpy = jasmine.createSpyObj('dataService', {
|
const findAllDataSpy = jasmine.createSpyObj('findAllData', {
|
||||||
findAll: EMPTY,
|
findAll: EMPTY,
|
||||||
findByHref: EMPTY,
|
|
||||||
findAllByHref: EMPTY,
|
|
||||||
findById: EMPTY,
|
|
||||||
});
|
});
|
||||||
const hrefAll = 'https://rest.api/server/api/discover/browses';
|
|
||||||
const hrefSingle = 'https://rest.api/server/api/discover/browses/author';
|
|
||||||
const id = 'author';
|
|
||||||
const options = new FindListOptions();
|
const options = new FindListOptions();
|
||||||
const linksToFollow = [
|
const linksToFollow = [
|
||||||
followLink('entries'),
|
followLink('entries'),
|
||||||
@@ -21,35 +15,14 @@ describe(`BrowseDefinitionDataService`, () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new BrowseDefinitionDataService(null, null, null, null, null, null, null, null);
|
service = new BrowseDefinitionDataService(null, null, null, null);
|
||||||
(service as any).dataService = dataServiceImplSpy;
|
(service as any).findAllData = findAllDataSpy;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`findAll`, () => {
|
describe(`findAll`, () => {
|
||||||
it(`should call findAll on DataServiceImpl`, () => {
|
it(`should call findAll on findAllData`, () => {
|
||||||
service.findAll(options, true, false, ...linksToFollow);
|
service.findAll(options, true, false, ...linksToFollow);
|
||||||
expect(dataServiceImplSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow);
|
expect(findAllDataSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`findByHref`, () => {
|
|
||||||
it(`should call findByHref on DataServiceImpl`, () => {
|
|
||||||
service.findByHref(hrefSingle, true, false, ...linksToFollow);
|
|
||||||
expect(dataServiceImplSpy.findByHref).toHaveBeenCalledWith(hrefSingle, true, false, ...linksToFollow);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`findAllByHref`, () => {
|
|
||||||
it(`should call findAllByHref on DataServiceImpl`, () => {
|
|
||||||
service.findAllByHref(hrefAll, options, true, false, ...linksToFollow);
|
|
||||||
expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(hrefAll, options, true, false, ...linksToFollow);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`findById`, () => {
|
|
||||||
it(`should call findById on DataServiceImpl`, () => {
|
|
||||||
service.findAllByHref(id, options, true, false, ...linksToFollow);
|
|
||||||
expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(id, options, true, false, ...linksToFollow);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,125 +1,56 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
|
||||||
import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
|
import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
|
||||||
import { DataService } from '../data/data.service';
|
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
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 { HttpClient } from '@angular/common/http';
|
|
||||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { PaginatedList } from '../data/paginated-list.model';
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
||||||
|
import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
|
||||||
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
class DataServiceImpl extends DataService<BrowseDefinition> {
|
* Data service responsible for retrieving browse definitions from the REST server
|
||||||
protected linkPath = 'browses';
|
*/
|
||||||
|
|
||||||
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<BrowseDefinition>) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@dataService(BROWSE_DEFINITION)
|
@dataService(BROWSE_DEFINITION)
|
||||||
export class BrowseDefinitionDataService {
|
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition> {
|
||||||
/**
|
private findAllData: FindAllDataImpl<BrowseDefinition>;
|
||||||
* A private DataService instance to delegate specific methods to.
|
|
||||||
*/
|
|
||||||
private dataService: DataServiceImpl;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
) {
|
||||||
protected http: HttpClient,
|
super('browses', requestService, rdbService, objectCache, halService);
|
||||||
protected comparator: DefaultChangeAnalyzer<BrowseDefinition>) {
|
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
* 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 useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
* no valid cached version. Defaults to true
|
* no valid cached version. Defaults to true
|
||||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
* requested after the response becomes stale
|
* requested after the response becomes stale
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
* {@link HALLink}s should be automatically resolved
|
* {@link HALLink}s should be automatically resolved
|
||||||
* @return {Observable<RemoteData<PaginatedList<BrowseDefinition>>>}
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
* Return an observable that emits object list
|
* Return an observable that emits object list
|
||||||
*/
|
*/
|
||||||
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, 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, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findAllData.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},
|
|
||||||
* to automatically resolve {@link HALLink}s of the {@link BrowseDefinition}
|
|
||||||
* @param href The url of {@link BrowseDefinition} 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(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<BrowseDefinition>> {
|
|
||||||
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},
|
|
||||||
* to automatically resolve {@link HALLink}s of the {@link BrowseDefinition}
|
|
||||||
* @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(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
|
|
||||||
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @param id ID 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
|
|
||||||
*/
|
|
||||||
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<BrowseDefinition>> {
|
|
||||||
return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -139,13 +139,13 @@ describe('BrowseService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
|
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
|
||||||
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
it('should call hrefOnlyDataService.findListByHref with the expected href', () => {
|
||||||
const expected = 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(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
|
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
|
||||||
a: expected
|
a: expected
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -153,20 +153,20 @@ describe('BrowseService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when findList is called with a valid browse definition id', () => {
|
describe('when findList is called with a valid browse definition id', () => {
|
||||||
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
it('should call hrefOnlyDataService.findListByHref with the expected href', () => {
|
||||||
const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName);
|
const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName);
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, undefined, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, undefined, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
|
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
|
||||||
a: expected
|
a: expected
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
describe('when getBrowseItemsFor is called with a valid filter value and authority key', () => {
|
describe('when getBrowseItemsFor is called with a valid filter value and authority key', () => {
|
||||||
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
it('should call hrefOnlyDataService.findListByHref with the expected href', () => {
|
||||||
const expected = browseDefinitions[1]._links.items.href +
|
const expected = browseDefinitions[1]._links.items.href +
|
||||||
'?filterValue=' + encodeURIComponent(mockAuthorName) +
|
'?filterValue=' + encodeURIComponent(mockAuthorName) +
|
||||||
'&filterAuthority=' + encodeURIComponent(mockAuthorityKey);
|
'&filterAuthority=' + encodeURIComponent(mockAuthorityKey);
|
||||||
@@ -174,7 +174,7 @@ describe('BrowseService', () => {
|
|||||||
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, mockAuthorityKey, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, mockAuthorityKey, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
|
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
|
||||||
a: expected
|
a: expected
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -267,11 +267,11 @@ describe('BrowseService', () => {
|
|||||||
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 call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
it('should call hrefOnlyDataService.findListByHref with the expected href', () => {
|
||||||
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
|
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
|
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
|
||||||
a: expectedURL
|
a: expectedURL
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@@ -103,9 +103,9 @@ export class BrowseService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (options.embedThumbnail) {
|
if (options.embedThumbnail) {
|
||||||
return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
||||||
}
|
}
|
||||||
return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(href$);
|
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,9 +151,9 @@ export class BrowseService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (options.embedThumbnail) {
|
if (options.embedThumbnail) {
|
||||||
return this.hrefOnlyDataService.findAllByHref<Item>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
||||||
}
|
}
|
||||||
return this.hrefOnlyDataService.findAllByHref<Item>(href$);
|
return this.hrefOnlyDataService.findListByHref<Item>(href$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,7 +184,7 @@ export class BrowseService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.hrefOnlyDataService.findAllByHref<Item>(href$).pipe(
|
return this.hrefOnlyDataService.findListByHref<Item>(href$).pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getFirstOccurrence()
|
getFirstOccurrence()
|
||||||
);
|
);
|
||||||
@@ -196,7 +196,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 this.hrefOnlyDataService.findAllByHref<Item>(items.payload.prev);
|
return this.hrefOnlyDataService.findListByHref<Item>(items.payload.prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,7 +204,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 this.hrefOnlyDataService.findAllByHref<Item>(items.payload.next);
|
return this.hrefOnlyDataService.findListByHref<Item>(items.payload.next);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -212,7 +212,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 this.hrefOnlyDataService.findAllByHref<BrowseEntry>(entries.payload.prev);
|
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(entries.payload.prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,7 +220,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 this.hrefOnlyDataService.findAllByHref<BrowseEntry>(entries.payload.next);
|
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(entries.payload.next);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,14 +1,7 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import { dataService, getDataServiceFor, getLinkDefinition, link, } from './build-decorators';
|
import { getLinkDefinition, link } from './build-decorators';
|
||||||
|
|
||||||
class TestService {
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnotherTestService {
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestHALResource implements HALResource {
|
class TestHALResource implements HALResource {
|
||||||
_links: {
|
_links: {
|
||||||
@@ -25,31 +18,6 @@ describe('build decorators', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testType = new ResourceType('testType-' + new Date().getTime());
|
testType = new ResourceType('testType-' + new Date().getTime());
|
||||||
});
|
});
|
||||||
describe('@dataService/getDataServiceFor', () => {
|
|
||||||
|
|
||||||
it('should register a resourcetype for a dataservice', () => {
|
|
||||||
dataService(testType)(TestService);
|
|
||||||
expect(getDataServiceFor(testType)).toBe(TestService);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when the resource type isn't specified`, () => {
|
|
||||||
it(`should throw an error`, () => {
|
|
||||||
expect(() => {
|
|
||||||
dataService(undefined)(TestService);
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when there already is a registered dataservice for a resourcetype`, () => {
|
|
||||||
it(`should throw an error`, () => {
|
|
||||||
dataService(testType)(TestService);
|
|
||||||
expect(() => {
|
|
||||||
dataService(testType)(AnotherTestService);
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`@link/getLinkDefinitions`, () => {
|
describe(`@link/getLinkDefinitions`, () => {
|
||||||
it(`should register a link`, () => {
|
it(`should register a link`, () => {
|
||||||
|
45
src/app/core/cache/builders/build-decorators.ts
vendored
45
src/app/core/cache/builders/build-decorators.ts
vendored
@@ -3,20 +3,13 @@ import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
|||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import {
|
import { getResourceTypeValueFor } from '../object-cache.reducer';
|
||||||
getResourceTypeValueFor
|
|
||||||
} from '../object-cache.reducer';
|
|
||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
import { CacheableObject } from '../cacheable-object.model';
|
|
||||||
import { TypedObject } from '../typed-object.model';
|
import { TypedObject } from '../typed-object.model';
|
||||||
|
|
||||||
export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<any>>('getDataServiceFor', {
|
|
||||||
providedIn: 'root',
|
|
||||||
factory: () => getDataServiceFor
|
|
||||||
});
|
|
||||||
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
|
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => getLinkDefinition
|
factory: () => getLinkDefinition,
|
||||||
});
|
});
|
||||||
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
|
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -27,7 +20,6 @@ const resolvedLinkKey = Symbol('resolvedLink');
|
|||||||
|
|
||||||
const resolvedLinkMap = new Map();
|
const resolvedLinkMap = new Map();
|
||||||
const typeMap = new Map();
|
const typeMap = new Map();
|
||||||
const dataServiceMap = new Map();
|
|
||||||
const linkMap = new Map();
|
const linkMap = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,39 +38,6 @@ export function getClassForType(type: string | ResourceType) {
|
|||||||
return typeMap.get(getResourceTypeValueFor(type));
|
return typeMap.get(getResourceTypeValueFor(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A class decorator to indicate that this class is a dataservice
|
|
||||||
* for a given resource type.
|
|
||||||
*
|
|
||||||
* "dataservice" in this context means that it has findByHref and
|
|
||||||
* findAllByHref methods.
|
|
||||||
*
|
|
||||||
* @param resourceType the resource type the class is a dataservice for
|
|
||||||
*/
|
|
||||||
export function dataService(resourceType: ResourceType): any {
|
|
||||||
return (target: any) => {
|
|
||||||
if (hasNoValue(resourceType)) {
|
|
||||||
throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`);
|
|
||||||
}
|
|
||||||
const existingDataservice = dataServiceMap.get(resourceType.value);
|
|
||||||
|
|
||||||
if (hasValue(existingDataservice)) {
|
|
||||||
throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
dataServiceMap.set(resourceType.value, target);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the dataservice matching the given resource type
|
|
||||||
*
|
|
||||||
* @param resourceType the resource type you want the matching dataservice for
|
|
||||||
*/
|
|
||||||
export function getDataServiceFor<T extends CacheableObject>(resourceType: ResourceType) {
|
|
||||||
return dataServiceMap.get(resourceType.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to represent the data that can be set by the @link decorator
|
* A class to represent the data that can be set by the @link decorator
|
||||||
*/
|
*/
|
||||||
|
13
src/app/core/cache/builders/link.service.spec.ts
vendored
13
src/app/core/cache/builders/link.service.spec.ts
vendored
@@ -6,9 +6,10 @@ import { HALLink } from '../../shared/hal-link.model';
|
|||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import { LinkService } from './link.service';
|
import { LinkService } from './link.service';
|
||||||
import { DATA_SERVICE_FACTORY, LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators';
|
import { LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators';
|
||||||
import { isEmpty } from 'rxjs/operators';
|
import { isEmpty } from 'rxjs/operators';
|
||||||
import { FindListOptions } from '../../data/find-list-options.model';
|
import { FindListOptions } from '../../data/find-list-options.model';
|
||||||
|
import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator';
|
||||||
|
|
||||||
const TEST_MODEL = new ResourceType('testmodel');
|
const TEST_MODEL = new ResourceType('testmodel');
|
||||||
let result: any;
|
let result: any;
|
||||||
@@ -32,8 +33,8 @@ class TestModel implements HALResource {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class TestDataService {
|
class TestDataService {
|
||||||
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
|
findListByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
|
||||||
return 'findAllByHref';
|
return 'findListByHref';
|
||||||
}
|
}
|
||||||
|
|
||||||
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
|
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
|
||||||
@@ -64,7 +65,7 @@ describe('LinkService', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
testDataService = new TestDataService();
|
testDataService = new TestDataService();
|
||||||
spyOn(testDataService, 'findAllByHref').and.callThrough();
|
spyOn(testDataService, 'findListByHref').and.callThrough();
|
||||||
spyOn(testDataService, 'findByHref').and.callThrough();
|
spyOn(testDataService, 'findByHref').and.callThrough();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [LinkService, {
|
providers: [LinkService, {
|
||||||
@@ -118,8 +119,8 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor')));
|
||||||
});
|
});
|
||||||
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
it('should call dataservice.findListByHref with the correct href, findListOptions, and nested links', () => {
|
||||||
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
|
expect(testDataService.findListByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('either way', () => {
|
describe('either way', () => {
|
||||||
|
18
src/app/core/cache/builders/link.service.ts
vendored
18
src/app/core/cache/builders/link.service.ts
vendored
@@ -3,28 +3,30 @@ import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
|||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
|
import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator';
|
||||||
import {
|
import {
|
||||||
DATA_SERVICE_FACTORY,
|
|
||||||
LINK_DEFINITION_FACTORY,
|
LINK_DEFINITION_FACTORY,
|
||||||
LINK_DEFINITION_MAP_FACTORY,
|
LINK_DEFINITION_MAP_FACTORY,
|
||||||
LinkDefinition
|
LinkDefinition,
|
||||||
} from './build-decorators';
|
} from './build-decorators';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
import { EMPTY, Observable } from 'rxjs';
|
import { EMPTY, Observable } from 'rxjs';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { HALDataService } from '../../data/base/hal-data-service.interface';
|
||||||
|
import { PaginatedList } from '../../data/paginated-list.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Service to handle the resolving and removing
|
* A Service to handle the resolving and removing
|
||||||
* of resolved {@link HALLink}s on HALResources
|
* of resolved {@link HALLink}s on HALResources
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class LinkService {
|
export class LinkService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected parentInjector: Injector,
|
protected parentInjector: Injector,
|
||||||
@Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor<any>,
|
@Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor<HALDataService<any>>,
|
||||||
@Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>,
|
@Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>,
|
||||||
@Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: <T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>,
|
@Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: <T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>,
|
||||||
) {
|
) {
|
||||||
@@ -51,7 +53,7 @@ export class LinkService {
|
|||||||
* @param model the {@link HALResource} to resolve the link for
|
* @param model the {@link HALResource} to resolve the link for
|
||||||
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
||||||
*/
|
*/
|
||||||
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U | PaginatedList<U>>> {
|
||||||
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
|
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
|
||||||
|
|
||||||
if (hasValue(matchingLinkDef)) {
|
if (hasValue(matchingLinkDef)) {
|
||||||
@@ -61,9 +63,9 @@ export class LinkService {
|
|||||||
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
|
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = Injector.create({
|
const service: HALDataService<any> = Injector.create({
|
||||||
providers: [],
|
providers: [],
|
||||||
parent: this.parentInjector
|
parent: this.parentInjector,
|
||||||
}).get(provider);
|
}).get(provider);
|
||||||
|
|
||||||
const link = model._links[matchingLinkDef.linkName];
|
const link = model._links[matchingLinkDef.linkName];
|
||||||
@@ -72,7 +74,7 @@ export class LinkService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (matchingLinkDef.isList) {
|
if (matchingLinkDef.isList) {
|
||||||
return service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
return service.findListByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
||||||
} else {
|
} else {
|
||||||
return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigDataService } from './config-data.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GetRequest } from '../data/request.models';
|
import { GetRequest } from '../data/request.models';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
@@ -9,23 +9,26 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
|
|||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
const BROWSE = 'search/findByCollection';
|
const BROWSE = 'search/findByCollection';
|
||||||
|
|
||||||
class TestService extends ConfigService {
|
class TestService extends ConfigDataService {
|
||||||
protected linkPath = LINK_NAME;
|
protected linkPath = LINK_NAME;
|
||||||
protected browseEndpoint = BROWSE;
|
protected browseEndpoint = BROWSE;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected halService: HALEndpointService) {
|
protected objectCache: ObjectCacheService,
|
||||||
super(requestService, rdbService, null, null, halService, null, null, null, BROWSE);
|
protected halService: HALEndpointService,
|
||||||
|
) {
|
||||||
|
super(BROWSE, requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ConfigService', () => {
|
describe('ConfigDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: TestService;
|
let service: TestService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
@@ -45,7 +48,8 @@ describe('ConfigService', () => {
|
|||||||
return new TestService(
|
return new TestService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
halService
|
null,
|
||||||
|
halService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
40
src/app/core/config/config-data.service.ts
Normal file
40
src/app/core/config/config-data.service.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ConfigObject } from './models/config.model';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { BaseDataService } from '../data/base/base-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract data service to retrieve configuration objects from the REST server.
|
||||||
|
* Common logic for configuration objects should be implemented here.
|
||||||
|
*/
|
||||||
|
export abstract class ConfigDataService extends BaseDataService<ConfigObject> {
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Throws an error if a configuration object cannot be retrieved.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ConfigObject>[]): Observable<RemoteData<ConfigObject>> {
|
||||||
|
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<ConfigObject>) => {
|
||||||
|
if (rd.hasFailed) {
|
||||||
|
throw new Error(`Couldn't retrieve the config`);
|
||||||
|
} else {
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,67 +0,0 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { ConfigObject } from './models/config.model';
|
|
||||||
import { RemoteData } from '../data/remote-data';
|
|
||||||
import { DataService } from '../data/data.service';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
|
|
||||||
class DataServiceImpl extends DataService<ConfigObject> {
|
|
||||||
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<ConfigObject>,
|
|
||||||
protected linkPath: string
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class ConfigService {
|
|
||||||
/**
|
|
||||||
* 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<ConfigObject>,
|
|
||||||
protected linkPath: string
|
|
||||||
) {
|
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, this.linkPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ConfigObject>[]): Observable<RemoteData<ConfigObject>> {
|
|
||||||
return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
map((rd: RemoteData<ConfigObject>) => {
|
|
||||||
if (rd.hasFailed) {
|
|
||||||
throw new Error(`Couldn't retrieve the config`);
|
|
||||||
} else {
|
|
||||||
return rd;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,41 +1,46 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigDataService } from './config-data.service';
|
||||||
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 { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
|
||||||
import { SUBMISSION_ACCESSES_TYPE } from './models/config-type';
|
import { SUBMISSION_ACCESSES_TYPE } from './models/config-type';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
|
||||||
import { ConfigObject } from './models/config.model';
|
import { ConfigObject } from './models/config.model';
|
||||||
import { SubmissionAccessesModel } from './models/config-submission-accesses.model';
|
import { SubmissionAccessesModel } from './models/config-submission-accesses.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { CoreState } from '../core-state.model';
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
|
* Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(SUBMISSION_ACCESSES_TYPE)
|
@dataService(SUBMISSION_ACCESSES_TYPE)
|
||||||
export class SubmissionAccessesConfigService extends ConfigService {
|
export class SubmissionAccessesConfigDataService extends ConfigDataService {
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DefaultChangeAnalyzer<SubmissionAccessesModel>
|
|
||||||
) {
|
) {
|
||||||
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionaccessoptions');
|
super('submissionaccessoptions', requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Throws an error if a configuration object cannot be retrieved.
|
||||||
|
*
|
||||||
|
* @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(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionAccessesModel>> {
|
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionAccessesModel>> {
|
||||||
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionAccessesModel>>;
|
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionAccessesModel>>;
|
||||||
}
|
}
|
@@ -1,39 +1,46 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ConfigDataService } from './config-data.service';
|
||||||
import { ConfigService } from './config.service';
|
|
||||||
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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
|
||||||
import { ConfigObject } from './models/config.model';
|
import { ConfigObject } from './models/config.model';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
|
||||||
import { SUBMISSION_FORMS_TYPE } from './models/config-type';
|
import { SUBMISSION_FORMS_TYPE } from './models/config-type';
|
||||||
import { SubmissionFormsModel } from './models/config-submission-forms.model';
|
import { SubmissionFormsModel } from './models/config-submission-forms.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { CoreState } from '../core-state.model';
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data service to retrieve submission form configuration objects from the REST server.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(SUBMISSION_FORMS_TYPE)
|
@dataService(SUBMISSION_FORMS_TYPE)
|
||||||
export class SubmissionFormsConfigService extends ConfigService {
|
export class SubmissionFormsConfigDataService extends ConfigDataService {
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DefaultChangeAnalyzer<SubmissionFormsModel>
|
|
||||||
) {
|
) {
|
||||||
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionforms');
|
super('submissionforms', requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Throws an error if a configuration object cannot be retrieved.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
public findByHref(href: string, useCachedVersionIfAvailable = true, 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, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionFormsModel>>;
|
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionFormsModel>>;
|
||||||
}
|
}
|
@@ -1,39 +1,30 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigDataService } from './config-data.service';
|
||||||
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 { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
|
||||||
import { SUBMISSION_UPLOADS_TYPE } from './models/config-type';
|
import { SUBMISSION_UPLOADS_TYPE } from './models/config-type';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
|
||||||
import { ConfigObject } from './models/config.model';
|
import { ConfigObject } from './models/config.model';
|
||||||
import { SubmissionUploadsModel } from './models/config-submission-uploads.model';
|
import { SubmissionUploadsModel } from './models/config-submission-uploads.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { CoreState } from '../core-state.model';
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
|
* Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(SUBMISSION_UPLOADS_TYPE)
|
@dataService(SUBMISSION_UPLOADS_TYPE)
|
||||||
export class SubmissionUploadsConfigService extends ConfigService {
|
export class SubmissionUploadsConfigDataService extends ConfigDataService {
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DefaultChangeAnalyzer<SubmissionUploadsModel>
|
|
||||||
) {
|
) {
|
||||||
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionuploads');
|
super('submissionuploads', requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
|
|
||||||
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionUploadsModel>> {
|
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionUploadsModel>> {
|
@@ -36,7 +36,7 @@ import { SubmissionDefinitionsModel } from './config/models/config-submission-de
|
|||||||
import { SubmissionFormsModel } from './config/models/config-submission-forms.model';
|
import { SubmissionFormsModel } from './config/models/config-submission-forms.model';
|
||||||
import { SubmissionSectionModel } from './config/models/config-submission-section.model';
|
import { SubmissionSectionModel } from './config/models/config-submission-section.model';
|
||||||
import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model';
|
import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model';
|
||||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
import { SubmissionFormsConfigDataService } from './config/submission-forms-config-data.service';
|
||||||
import { coreEffects } from './core.effects';
|
import { coreEffects } from './core.effects';
|
||||||
import { coreReducers } from './core.reducers';
|
import { coreReducers } from './core.reducers';
|
||||||
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
||||||
@@ -49,8 +49,8 @@ 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 { EntityTypeService } from './data/entity-type.service';
|
import { EntityTypeDataService } from './data/entity-type-data.service';
|
||||||
import { ExternalSourceService } from './data/external-source.service';
|
import { ExternalSourceDataService } from './data/external-source-data.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';
|
||||||
@@ -58,9 +58,9 @@ import { ItemDataService } from './data/item-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';
|
||||||
import { RelationshipTypeService } from './data/relationship-type.service';
|
import { RelationshipTypeDataService } from './data/relationship-type-data.service';
|
||||||
import { RelationshipService } from './data/relationship.service';
|
import { RelationshipDataService } from './data/relationship-data.service';
|
||||||
import { ResourcePolicyService } from './resource-policy/resource-policy.service';
|
import { ResourcePolicyDataService } from './resource-policy/resource-policy-data.service';
|
||||||
import { SearchResponseParsingService } from './data/search-response-parsing.service';
|
import { SearchResponseParsingService } from './data/search-response-parsing.service';
|
||||||
import { SiteDataService } from './data/site-data.service';
|
import { SiteDataService } from './data/site-data.service';
|
||||||
import { DspaceRestService } from './dspace-rest/dspace-rest.service';
|
import { DspaceRestService } from './dspace-rest/dspace-rest.service';
|
||||||
@@ -170,14 +170,16 @@ import { SubmissionAccessesModel } from './config/models/config-submission-acces
|
|||||||
import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model';
|
import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model';
|
||||||
import { AccessStatusDataService } from './data/access-status-data.service';
|
import { AccessStatusDataService } from './data/access-status-data.service';
|
||||||
import { LinkHeadService } from './services/link-head.service';
|
import { LinkHeadService } from './services/link-head.service';
|
||||||
import { ResearcherProfileService } from './profile/researcher-profile.service';
|
import { ResearcherProfileDataService } from './profile/researcher-profile-data.service';
|
||||||
import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service';
|
import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service';
|
||||||
import { ResearcherProfile } from './profile/model/researcher-profile.model';
|
import { ResearcherProfile } from './profile/model/researcher-profile.model';
|
||||||
import { OrcidQueueService } from './orcid/orcid-queue.service';
|
import { OrcidQueueDataService } from './orcid/orcid-queue-data.service';
|
||||||
import { OrcidHistoryDataService } from './orcid/orcid-history-data.service';
|
import { OrcidHistoryDataService } from './orcid/orcid-history-data.service';
|
||||||
import { OrcidQueue } from './orcid/model/orcid-queue.model';
|
import { OrcidQueue } from './orcid/model/orcid-queue.model';
|
||||||
import { OrcidHistory } from './orcid/model/orcid-history.model';
|
import { OrcidHistory } from './orcid/model/orcid-history.model';
|
||||||
import { OrcidAuthService } from './orcid/orcid-auth.service';
|
import { OrcidAuthService } from './orcid/orcid-auth.service';
|
||||||
|
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
|
||||||
|
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -223,7 +225,7 @@ const PROVIDERS = [
|
|||||||
MetadataService,
|
MetadataService,
|
||||||
ObjectCacheService,
|
ObjectCacheService,
|
||||||
PaginationComponentOptions,
|
PaginationComponentOptions,
|
||||||
ResourcePolicyService,
|
ResourcePolicyDataService,
|
||||||
RegistryService,
|
RegistryService,
|
||||||
BitstreamFormatDataService,
|
BitstreamFormatDataService,
|
||||||
RemoteDataBuildService,
|
RemoteDataBuildService,
|
||||||
@@ -238,7 +240,7 @@ const PROVIDERS = [
|
|||||||
AccessStatusDataService,
|
AccessStatusDataService,
|
||||||
SubmissionCcLicenseDataService,
|
SubmissionCcLicenseDataService,
|
||||||
SubmissionCcLicenseUrlDataService,
|
SubmissionCcLicenseUrlDataService,
|
||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigDataService,
|
||||||
SubmissionRestService,
|
SubmissionRestService,
|
||||||
SubmissionResponseParsingService,
|
SubmissionResponseParsingService,
|
||||||
SubmissionJsonPatchOperationsService,
|
SubmissionJsonPatchOperationsService,
|
||||||
@@ -259,7 +261,7 @@ const PROVIDERS = [
|
|||||||
MenuService,
|
MenuService,
|
||||||
ObjectUpdatesService,
|
ObjectUpdatesService,
|
||||||
SearchService,
|
SearchService,
|
||||||
RelationshipService,
|
RelationshipDataService,
|
||||||
MyDSpaceGuard,
|
MyDSpaceGuard,
|
||||||
RoleService,
|
RoleService,
|
||||||
TaskResponseParsingService,
|
TaskResponseParsingService,
|
||||||
@@ -267,7 +269,7 @@ const PROVIDERS = [
|
|||||||
PoolTaskDataService,
|
PoolTaskDataService,
|
||||||
BitstreamDataService,
|
BitstreamDataService,
|
||||||
DsDynamicTypeBindRelationService,
|
DsDynamicTypeBindRelationService,
|
||||||
EntityTypeService,
|
EntityTypeDataService,
|
||||||
ContentSourceResponseParsingService,
|
ContentSourceResponseParsingService,
|
||||||
ItemTemplateDataService,
|
ItemTemplateDataService,
|
||||||
SearchService,
|
SearchService,
|
||||||
@@ -276,8 +278,8 @@ const PROVIDERS = [
|
|||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
SearchConfigurationService,
|
SearchConfigurationService,
|
||||||
SelectableListService,
|
SelectableListService,
|
||||||
RelationshipTypeService,
|
RelationshipTypeDataService,
|
||||||
ExternalSourceService,
|
ExternalSourceDataService,
|
||||||
LookupRelationService,
|
LookupRelationService,
|
||||||
VersionDataService,
|
VersionDataService,
|
||||||
VersionHistoryDataService,
|
VersionHistoryDataService,
|
||||||
@@ -300,14 +302,16 @@ const PROVIDERS = [
|
|||||||
FilteredDiscoveryPageResponseParsingService,
|
FilteredDiscoveryPageResponseParsingService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
||||||
VocabularyService,
|
VocabularyService,
|
||||||
|
VocabularyDataService,
|
||||||
|
VocabularyEntryDetailsDataService,
|
||||||
VocabularyTreeviewService,
|
VocabularyTreeviewService,
|
||||||
SequenceService,
|
SequenceService,
|
||||||
GroupDataService,
|
GroupDataService,
|
||||||
FeedbackDataService,
|
FeedbackDataService,
|
||||||
ResearcherProfileService,
|
ResearcherProfileDataService,
|
||||||
ProfileClaimService,
|
ProfileClaimService,
|
||||||
OrcidAuthService,
|
OrcidAuthService,
|
||||||
OrcidQueueService,
|
OrcidQueueDataService,
|
||||||
OrcidHistoryDataService,
|
OrcidHistoryDataService,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -76,6 +76,6 @@ describe('AccessStatusDataService', () => {
|
|||||||
});
|
});
|
||||||
halService = new HALEndpointServiceStub(url);
|
halService = new HALEndpointServiceStub(url);
|
||||||
notificationsService = new NotificationsServiceStub();
|
notificationsService = new NotificationsServiceStub();
|
||||||
service = new AccessStatusDataService(null, halService, null, notificationsService, objectCache, rdbService, requestService, null);
|
service = new AccessStatusDataService(requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,43 +1,35 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
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';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
|
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
|
||||||
import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type';
|
import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
|
import { BaseDataService } from './base/base-data.service';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data service responsible for retrieving the access status of Items
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(ACCESS_STATUS)
|
@dataService(ACCESS_STATUS)
|
||||||
export class AccessStatusDataService extends DataService<AccessStatusObject> {
|
export class AccessStatusDataService extends BaseDataService<AccessStatusObject> {
|
||||||
|
|
||||||
protected linkPath = 'accessStatus';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected comparator: DefaultChangeAnalyzer<AccessStatusObject>,
|
|
||||||
protected halService: HALEndpointService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
|
||||||
protected rdbService: RemoteDataBuildService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected store: Store<CoreState>,
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
) {
|
) {
|
||||||
super();
|
super('accessStatus', requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {@link RemoteData} of {@link AccessStatusObject} that is the access status of the given item
|
* Returns {@link RemoteData} of {@link AccessStatusObject} that is the access status of the given item
|
||||||
* @param item Item we want the access status of
|
* @param item Item we want the access status of
|
||||||
*/
|
*/
|
||||||
findAccessStatusFor(item: Item): Observable<RemoteData<AccessStatusObject>> {
|
findAccessStatusFor(item: Item): Observable<RemoteData<AccessStatusObject>> {
|
||||||
return this.findByHref(item._links.accessStatus.href);
|
return this.findByHref(item._links.accessStatus.href);
|
||||||
|
@@ -1,54 +1,38 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/**
|
||||||
import { HttpClient } from '@angular/common/http';
|
* The contents of this file are subject to the license and copyright
|
||||||
import { Store } from '@ngrx/store';
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
import { compare, Operation } from 'fast-json-patch';
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { Item } from '../shared/item.model';
|
|
||||||
import {
|
|
||||||
createFailedRemoteDataObject,
|
|
||||||
createSuccessfulRemoteDataObject,
|
|
||||||
createSuccessfulRemoteDataObject$,
|
|
||||||
} from '../../shared/remote-data.utils';
|
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { PatchRequest } from './request.models';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
import { RequestEntryState } from './request-entry-state.model';
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
|
||||||
import { fakeAsync, tick } from '@angular/core/testing';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
import { BaseDataService } from './base-data.service';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
const BOOLEAN = { f: false, t: true };
|
const BOOLEAN = { f: false, t: true };
|
||||||
|
|
||||||
class TestService extends DataService<any> {
|
class TestService extends BaseDataService<any> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected linkPath: string,
|
|
||||||
protected halService: HALEndpointService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected notificationsService: NotificationsService,
|
protected halService: HALEndpointService,
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: ChangeAnalyzer<Item>
|
|
||||||
) {
|
) {
|
||||||
super();
|
super(undefined, requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
@@ -56,24 +40,12 @@ class TestService extends DataService<any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
describe('BaseDataService', () => {
|
||||||
diff(object1: Item, object2: Item): Operation[] {
|
|
||||||
return compare((object1 as any).metadata, (object2 as any).metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DataService', () => {
|
|
||||||
let service: TestService;
|
let service: TestService;
|
||||||
let options: FindListOptions;
|
|
||||||
let requestService;
|
let requestService;
|
||||||
let halService;
|
let halService;
|
||||||
let rdbService;
|
let rdbService;
|
||||||
let notificationsService;
|
|
||||||
let http;
|
|
||||||
let comparator;
|
|
||||||
let objectCache;
|
let objectCache;
|
||||||
let store;
|
|
||||||
let selfLink;
|
let selfLink;
|
||||||
let linksToFollow;
|
let linksToFollow;
|
||||||
let testScheduler;
|
let testScheduler;
|
||||||
@@ -83,9 +55,6 @@ describe('DataService', () => {
|
|||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
halService = new HALEndpointServiceStub('url') as any;
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
notificationsService = {} as NotificationsService;
|
|
||||||
http = {} as HttpClient;
|
|
||||||
comparator = new DummyChangeAnalyzer() as any;
|
|
||||||
objectCache = {
|
objectCache = {
|
||||||
|
|
||||||
addPatch: () => {
|
addPatch: () => {
|
||||||
@@ -98,7 +67,6 @@ describe('DataService', () => {
|
|||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
} as any;
|
} as any;
|
||||||
store = {} as Store<CoreState>;
|
|
||||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
linksToFollow = [
|
linksToFollow = [
|
||||||
followLink('a'),
|
followLink('a'),
|
||||||
@@ -126,17 +94,11 @@ describe('DataService', () => {
|
|||||||
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return new TestService(
|
return new TestService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
store,
|
|
||||||
endpoint,
|
|
||||||
halService,
|
|
||||||
objectCache,
|
objectCache,
|
||||||
notificationsService,
|
halService,
|
||||||
http,
|
|
||||||
comparator,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,259 +106,6 @@ describe('DataService', () => {
|
|||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getFindAllHref', () => {
|
|
||||||
|
|
||||||
it('should return an observable with the endpoint', () => {
|
|
||||||
options = {};
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
|
||||||
expect(value).toBe(endpoint);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include page in href if currentPage provided in options', () => {
|
|
||||||
options = { currentPage: 2 };
|
|
||||||
const expected = `${endpoint}?page=${options.currentPage - 1}`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include size in href if elementsPerPage provided in options', () => {
|
|
||||||
options = { elementsPerPage: 5 };
|
|
||||||
const expected = `${endpoint}?size=${options.elementsPerPage}`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include sort href if SortOptions provided in options', () => {
|
|
||||||
const sortOptions = new SortOptions('field1', SortDirection.ASC);
|
|
||||||
options = { sort: sortOptions };
|
|
||||||
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include startsWith in href if startsWith provided in options', () => {
|
|
||||||
options = { startsWith: 'ab' };
|
|
||||||
const expected = `${endpoint}?startsWith=${options.startsWith}`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include all provided options in href', () => {
|
|
||||||
const sortOptions = new SortOptions('field1', SortDirection.DESC);
|
|
||||||
options = {
|
|
||||||
currentPage: 6,
|
|
||||||
elementsPerPage: 10,
|
|
||||||
sort: sortOptions,
|
|
||||||
startsWith: 'ab',
|
|
||||||
|
|
||||||
};
|
|
||||||
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
|
||||||
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include all searchParams in href if any provided in options', () => {
|
|
||||||
options = {
|
|
||||||
searchParams: [
|
|
||||||
new RequestParam('param1', 'test'),
|
|
||||||
new RequestParam('param2', 'test2'),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
const expected = `${endpoint}?param1=test¶m2=test2`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include linkPath in href if any provided', () => {
|
|
||||||
const expected = `${endpoint}/test/entries`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, 'test/entries').subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include single linksToFollow as embed', () => {
|
|
||||||
const expected = `${endpoint}?embed=bundles`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles')).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include single linksToFollow as embed and its size', () => {
|
|
||||||
const expected = `${endpoint}?embed.size=bundles=5&embed=bundles`;
|
|
||||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
|
||||||
elementsPerPage: 5
|
|
||||||
});
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles', { findListOptions: config })).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include multiple linksToFollow as embed', () => {
|
|
||||||
const expected = `${endpoint}?embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf')).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include multiple linksToFollow as embed and its sizes if given', () => {
|
|
||||||
const expected = `${endpoint}?embed=bundles&embed.size=owningCollection=2&embed=owningCollection&embed=templateItemOf`;
|
|
||||||
|
|
||||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
|
||||||
elementsPerPage: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', { findListOptions: config }), followLink('templateItemOf')).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
|
||||||
const expected = `${endpoint}?embed=templateItemOf`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref(
|
|
||||||
{},
|
|
||||||
null,
|
|
||||||
followLink('bundles', { shouldEmbed: false }),
|
|
||||||
followLink('owningCollection', { shouldEmbed: false }),
|
|
||||||
followLink('templateItemOf')
|
|
||||||
).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include nested linksToFollow 3lvl', () => {
|
|
||||||
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include nested linksToFollow 2lvl and nested embed\'s size', () => {
|
|
||||||
const expected = `${endpoint}?embed.size=owningCollection/itemtemplate=4&embed=owningCollection/itemtemplate`;
|
|
||||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
|
||||||
elementsPerPage: 4
|
|
||||||
});
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', { findListOptions: config }))).subscribe((value) => {
|
|
||||||
expect(value).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getIDHref', () => {
|
|
||||||
const endpointMock = 'https://dspace7-internal.atmire.com/server/api/core/items';
|
|
||||||
const resourceIdMock = '003c99b4-d4fe-44b0-a945-e12182a7ca89';
|
|
||||||
|
|
||||||
it('should return endpoint', () => {
|
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock);
|
|
||||||
expect(result).toEqual(endpointMock + '/' + resourceIdMock);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include single linksToFollow as embed', () => {
|
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=bundles`;
|
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles'));
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include multiple linksToFollow as embed', () => {
|
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf'));
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
|
|
||||||
const result = (service as any).getIDHref(
|
|
||||||
endpointMock,
|
|
||||||
resourceIdMock,
|
|
||||||
followLink('bundles', { shouldEmbed: false }),
|
|
||||||
followLink('owningCollection', { shouldEmbed: false }),
|
|
||||||
followLink('templateItemOf')
|
|
||||||
);
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include nested linksToFollow 3lvl', () => {
|
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships'))));
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('patch', () => {
|
|
||||||
const dso = {
|
|
||||||
uuid: 'dso-uuid'
|
|
||||||
};
|
|
||||||
const operations = [
|
|
||||||
Object.assign({
|
|
||||||
op: 'move',
|
|
||||||
from: '/1',
|
|
||||||
path: '/5'
|
|
||||||
}) as Operation
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
service.patch(dso, operations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send a PatchRequest', () => {
|
|
||||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PatchRequest));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
let operations;
|
|
||||||
let dso;
|
|
||||||
let dso2;
|
|
||||||
const name1 = 'random string';
|
|
||||||
const name2 = 'another random string';
|
|
||||||
beforeEach(() => {
|
|
||||||
operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation];
|
|
||||||
|
|
||||||
dso = Object.assign(new DSpaceObject(), {
|
|
||||||
_links: { self: { href: selfLink } },
|
|
||||||
metadata: [{ key: 'dc.title', value: name1 }]
|
|
||||||
});
|
|
||||||
|
|
||||||
dso2 = Object.assign(new DSpaceObject(), {
|
|
||||||
_links: { self: { href: selfLink } },
|
|
||||||
metadata: [{ key: 'dc.title', value: name2 }]
|
|
||||||
});
|
|
||||||
|
|
||||||
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso));
|
|
||||||
spyOn(objectCache, 'addPatch');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call addPatch on the object cache with the right parameters when there are differences', () => {
|
|
||||||
service.update(dso2).subscribe();
|
|
||||||
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not call addPatch on the object cache with the right parameters when there are no differences', () => {
|
|
||||||
service.update(dso).subscribe();
|
|
||||||
expect(objectCache.addPatch).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`reRequestStaleRemoteData`, () => {
|
describe(`reRequestStaleRemoteData`, () => {
|
||||||
let callback: jasmine.Spy<jasmine.Func>;
|
let callback: jasmine.Spy<jasmine.Func>;
|
||||||
|
|
||||||
@@ -661,7 +370,7 @@ describe('DataService', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`findAllByHref`, () => {
|
describe(`findListByHref`, () => {
|
||||||
let findListOptions;
|
let findListOptions;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
findListOptions = { currentPage: 5 };
|
findListOptions = { currentPage: 5 };
|
||||||
@@ -674,7 +383,7 @@ describe('DataService', () => {
|
|||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||||
|
|
||||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow);
|
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -685,11 +394,11 @@ describe('DataService', () => {
|
|||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||||
|
|
||||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true);
|
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true);
|
||||||
expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||||
|
|
||||||
service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow);
|
||||||
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), false);
|
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), false);
|
||||||
expectObservable(rdbService.buildList.calls.argsFor(1)[0]).toBe('(a|)', { a: 'bingo!' });
|
expectObservable(rdbService.buildList.calls.argsFor(1)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||||
});
|
});
|
||||||
@@ -701,29 +410,29 @@ describe('DataService', () => {
|
|||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||||
|
|
||||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow);
|
expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow);
|
||||||
expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findAllByHref call as a callback`, () => {
|
it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findListByHref call as a callback`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||||
|
|
||||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue();
|
expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue();
|
||||||
spyOn(service, 'findAllByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
spyOn(service, 'findListByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||||
// prove that the spy we just added hasn't been called yet
|
// prove that the spy we just added hasn't been called yet
|
||||||
expect(service.findAllByHref).not.toHaveBeenCalled();
|
expect(service.findListByHref).not.toHaveBeenCalled();
|
||||||
// call the callback passed to reRequestStaleRemoteData
|
// call the callback passed to reRequestStaleRemoteData
|
||||||
(service as any).reRequestStaleRemoteData.calls.argsFor(0)[1]();
|
(service as any).reRequestStaleRemoteData.calls.argsFor(0)[1]();
|
||||||
// verify that findAllByHref _has_ been called now, with the same params as the original call
|
// verify that findListByHref _has_ been called now, with the same params as the original call
|
||||||
expect(service.findAllByHref).toHaveBeenCalledWith(jasmine.anything(), findListOptions, true, true, ...linksToFollow);
|
expect(service.findListByHref).toHaveBeenCalledWith(jasmine.anything(), findListOptions, true, true, ...linksToFollow);
|
||||||
// ... except for selflink, which will have been turned in to an observable.
|
// ... except for selflink, which will have been turned in to an observable.
|
||||||
expectObservable((service.findAllByHref as jasmine.Spy).calls.argsFor(0)[0]).toBe('(a|)', { a: selfLink });
|
expectObservable((service.findListByHref as jasmine.Spy).calls.argsFor(0)[0]).toBe('(a|)', { a: selfLink });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -737,7 +446,7 @@ describe('DataService', () => {
|
|||||||
a: 'bingo!',
|
a: 'bingo!',
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -765,7 +474,7 @@ describe('DataService', () => {
|
|||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -786,7 +495,7 @@ describe('DataService', () => {
|
|||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -816,7 +525,7 @@ describe('DataService', () => {
|
|||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -837,7 +546,7 @@ describe('DataService', () => {
|
|||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -915,97 +624,4 @@ describe('DataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
let MOCK_SUCCEEDED_RD;
|
|
||||||
let MOCK_FAILED_RD;
|
|
||||||
|
|
||||||
let invalidateByHrefSpy: jasmine.Spy;
|
|
||||||
let buildFromRequestUUIDSpy: jasmine.Spy;
|
|
||||||
let getIDHrefObsSpy: jasmine.Spy;
|
|
||||||
let deleteByHrefSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
|
|
||||||
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough();
|
|
||||||
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
|
|
||||||
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
|
|
||||||
|
|
||||||
MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
|
|
||||||
MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should retrieve href by ID and call deleteByHref', () => {
|
|
||||||
getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
|
||||||
|
|
||||||
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
|
|
||||||
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
|
|
||||||
expect(deleteByHrefSpy).toHaveBeenCalledWith('some-href', ['a', 'b', 'c']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteByHref', () => {
|
|
||||||
it('should call invalidateByHref if the DELETE request succeeds', (done) => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
|
||||||
|
|
||||||
service.deleteByHref('some-href').subscribe(rd => {
|
|
||||||
expect(rd).toBe(MOCK_SUCCEEDED_RD);
|
|
||||||
expect(invalidateByHrefSpy).toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
|
||||||
|
|
||||||
service.deleteByHref('some-href');
|
|
||||||
tick();
|
|
||||||
|
|
||||||
expect(invalidateByHrefSpy).toHaveBeenCalled();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not call invalidateByHref if the DELETE request fails', (done) => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
|
|
||||||
|
|
||||||
service.deleteByHref('some-href').subscribe(rd => {
|
|
||||||
expect(rd).toBe(MOCK_FAILED_RD);
|
|
||||||
expect(invalidateByHrefSpy).not.toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should wait for invalidateByHref before emitting', () => {
|
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(
|
|
||||||
cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away
|
|
||||||
);
|
|
||||||
invalidateByHrefSpy.and.returnValue(
|
|
||||||
cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer
|
|
||||||
);
|
|
||||||
|
|
||||||
const done$ = service.deleteByHref('some-href');
|
|
||||||
expectObservable(done$).toBe(
|
|
||||||
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should wait for the DELETE request to resolve before emitting', () => {
|
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
|
||||||
buildFromRequestUUIDSpy.and.returnValue(
|
|
||||||
cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while
|
|
||||||
);
|
|
||||||
invalidateByHrefSpy.and.returnValue(
|
|
||||||
cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner
|
|
||||||
); // e.g.: maybe already stale before this call?
|
|
||||||
|
|
||||||
const done$ = service.deleteByHref('some-href');
|
|
||||||
expectObservable(done$).toBe(
|
|
||||||
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
374
src/app/core/data/base/base-data.service.ts
Normal file
374
src/app/core/data/base/base-data.service.ts
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AsyncSubject, from as observableFrom, Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { map, mergeMap, skipWhile, switchMap, take, tap, toArray } from 'rxjs/operators';
|
||||||
|
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { GetRequest } from '../request.models';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
|
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALDataService } from './hal-data-service.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common functionality for data services.
|
||||||
|
* Specific functionality that not all services would need
|
||||||
|
* is implemented in "DataService feature" classes (e.g. {@link CreateData}
|
||||||
|
*
|
||||||
|
* All DataService (or DataService feature) classes must
|
||||||
|
* - extend this class (or {@link IdentifiableDataService})
|
||||||
|
* - implement any DataService features it requires in order to forward calls to it
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* export class SomeDataService extends BaseDataService<Something> implements CreateData<Something>, SearchData<Something> {
|
||||||
|
* private createData: CreateData<Something>;
|
||||||
|
* private searchData: SearchDataData<Something>;
|
||||||
|
*
|
||||||
|
* create(...) {
|
||||||
|
* return this.createData.create(...);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* searchBy(...) {
|
||||||
|
* return this.searchData.searchBy(...);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class BaseDataService<T extends CacheableObject> implements HALDataService<T> {
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected responseMsToLive?: number,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows subclasses to reset the response cache time.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for browsing
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param linkPath The link path for the object
|
||||||
|
* @returns {Observable<string>}
|
||||||
|
*/
|
||||||
|
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||||
|
return this.getEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base endpoint for all requests
|
||||||
|
*/
|
||||||
|
protected getEndpoint(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn an options object into a query string and combine it with the given HREF
|
||||||
|
*
|
||||||
|
* @param href The HREF to which the query string should be appended
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param extraArgs Array with additional params to combine with query string
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: FollowLinkConfig<T>[]): string {
|
||||||
|
let args = [...extraArgs];
|
||||||
|
|
||||||
|
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 */
|
||||||
|
args = this.addHrefArg(href, args, `page=${options.currentPage - 1}`);
|
||||||
|
}
|
||||||
|
if (hasValue(options.elementsPerPage)) {
|
||||||
|
args = this.addHrefArg(href, args, `size=${options.elementsPerPage}`);
|
||||||
|
}
|
||||||
|
if (hasValue(options.sort)) {
|
||||||
|
args = this.addHrefArg(href, args, `sort=${options.sort.field},${options.sort.direction}`);
|
||||||
|
}
|
||||||
|
if (hasValue(options.startsWith)) {
|
||||||
|
args = this.addHrefArg(href, args, `startsWith=${options.startsWith}`);
|
||||||
|
}
|
||||||
|
if (hasValue(options.searchParams)) {
|
||||||
|
options.searchParams.forEach((param: RequestParam) => {
|
||||||
|
args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
args = this.addEmbedParams(href, args, ...linksToFollow);
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
} else {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn an array of RequestParam into a query string and combine it with the given HREF
|
||||||
|
*
|
||||||
|
* @param href The HREF to which the query string should be appended
|
||||||
|
* @param params Array with additional params to combine with query string
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
*/
|
||||||
|
buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: FollowLinkConfig<T>[]): string {
|
||||||
|
|
||||||
|
let args = [];
|
||||||
|
if (hasValue(params)) {
|
||||||
|
params.forEach((param: RequestParam) => {
|
||||||
|
args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
args = this.addEmbedParams(href, args, ...linksToFollow);
|
||||||
|
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
} else {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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 linksToFollow links we want to embed in query string if shouldEmbed is true
|
||||||
|
*/
|
||||||
|
protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]) {
|
||||||
|
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||||
|
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
|
||||||
|
const embedString = 'embed=' + String(linkToFollow.name);
|
||||||
|
// Add the embeds size if given in the FollowLinkConfig.FindListOptions
|
||||||
|
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
|
||||||
|
args = this.addHrefArg(href, args,
|
||||||
|
'embed.size=' + String(linkToFollow.name) + '=' + linkToFollow.findListOptions.elementsPerPage);
|
||||||
|
}
|
||||||
|
// Adds the nested embeds and their size if given
|
||||||
|
if (isNotEmpty(linkToFollow.linksToFollow)) {
|
||||||
|
args = this.addNestedEmbeds(embedString, href, args, ...linkToFollow.linksToFollow);
|
||||||
|
} else {
|
||||||
|
args = this.addHrefArg(href, args, embedString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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, separated by a /, and their sizes, recursively
|
||||||
|
* @param embedString embedString so far (recursive)
|
||||||
|
* @param href The href the params are to be added to
|
||||||
|
* @param args params for the query string
|
||||||
|
* @param linksToFollow links we want to embed in query string if shouldEmbed is true
|
||||||
|
*/
|
||||||
|
protected addNestedEmbeds(embedString: string, href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]): string[] {
|
||||||
|
let nestEmbed = embedString;
|
||||||
|
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||||
|
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
|
||||||
|
nestEmbed = nestEmbed + '/' + String(linkToFollow.name);
|
||||||
|
// Add the nested embeds size if given in the FollowLinkConfig.FindListOptions
|
||||||
|
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
|
||||||
|
const nestedEmbedSize = 'embed.size=' + nestEmbed.split('=')[1] + '=' + linkToFollow.findListOptions.elementsPerPage;
|
||||||
|
args = this.addHrefArg(href, args, nestedEmbedSize);
|
||||||
|
}
|
||||||
|
if (hasValue(linkToFollow.linksToFollow) && isNotEmpty(linkToFollow.linksToFollow)) {
|
||||||
|
args = this.addNestedEmbeds(nestEmbed, href, args, ...linkToFollow.linksToFollow);
|
||||||
|
} else {
|
||||||
|
args = this.addHrefArg(href, args, nestEmbed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An operator that will call the given function if the incoming RemoteData is stale and
|
||||||
|
* shouldReRequest is true
|
||||||
|
*
|
||||||
|
* @param shouldReRequest Whether or not to call the re-request function if the RemoteData is stale
|
||||||
|
* @param requestFn The function to call if the RemoteData is stale and shouldReRequest is
|
||||||
|
* true
|
||||||
|
*/
|
||||||
|
protected reRequestStaleRemoteData<O>(shouldReRequest: boolean, requestFn: () => Observable<RemoteData<O>>) {
|
||||||
|
return (source: Observable<RemoteData<O>>): Observable<RemoteData<O>> => {
|
||||||
|
if (shouldReRequest === true) {
|
||||||
|
return source.pipe(
|
||||||
|
tap((remoteData: RemoteData<O>) => {
|
||||||
|
if (hasValue(remoteData) && remoteData.isStale) {
|
||||||
|
requestFn();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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. 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
|
||||||
|
* @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 | Observable<string>, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
||||||
|
if (typeof href$ === 'string') {
|
||||||
|
href$ = observableOf(href$);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestHref$ = href$.pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
take(1),
|
||||||
|
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||||
|
|
||||||
|
return this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
||||||
|
// This skip ensures that if a stale object is present in the cache when you do a
|
||||||
|
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||||
|
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||||
|
// cached completed object
|
||||||
|
skipWhile((rd: RemoteData<T>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
||||||
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
|
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Observable of a {@link RemoteData} of a {@link PaginatedList} 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 list we want to retrieve. Can be a string or an Observable<string>
|
||||||
|
* @param options
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's no valid cached version.
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
findListByHref(href$: string | Observable<string>, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
if (typeof href$ === 'string') {
|
||||||
|
href$ = observableOf(href$);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestHref$ = href$.pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
take(1),
|
||||||
|
map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||||
|
|
||||||
|
return this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
|
||||||
|
// This skip ensures that if a stale object is present in the cache when you do a
|
||||||
|
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||||
|
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||||
|
// cached completed object
|
||||||
|
skipWhile((rd: RemoteData<PaginatedList<T>>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
||||||
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
|
this.findListByHref(href$, options, 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 request = new GetRequest(requestId, href);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
this.requestService.send(request, useCachedVersionIfAvailable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the links to traverse from the root of the api to the
|
||||||
|
* endpoint this DataService represents
|
||||||
|
*
|
||||||
|
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
|
||||||
|
* links to 'bar' the linkPath for the BarDataService would be
|
||||||
|
* 'foo/bar'
|
||||||
|
*/
|
||||||
|
getLinkPath(): string {
|
||||||
|
return this.linkPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate a cached object by its href
|
||||||
|
* @param href the href to invalidate
|
||||||
|
*/
|
||||||
|
public invalidateByHref(href: string): Observable<boolean> {
|
||||||
|
const done$ = new AsyncSubject<boolean>();
|
||||||
|
|
||||||
|
this.objectCache.getByHref(href).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe(
|
||||||
|
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
|
||||||
|
toArray(),
|
||||||
|
)),
|
||||||
|
).subscribe(() => {
|
||||||
|
done$.next(true);
|
||||||
|
done$.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
return done$;
|
||||||
|
}
|
||||||
|
}
|
225
src/app/core/data/base/create-data.spec.ts
Normal file
225
src/app/core/data/base/create-data.spec.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { CreateData, CreateDataImpl } from './create-data';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
|
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { RestRequestMethod } from '../rest-request-method';
|
||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether calls to `CreateData` methods are correctly patched through in a concrete data service that implements it
|
||||||
|
*/
|
||||||
|
export function testCreateDataImplementation(serviceFactory: () => CreateData<any>) {
|
||||||
|
let service;
|
||||||
|
|
||||||
|
describe('CreateData implementation', () => {
|
||||||
|
const OBJ = Object.assign(new DSpaceObject(), {
|
||||||
|
uuid: '08eec68f-45e4-47a3-80c5-f0beb5627079',
|
||||||
|
});
|
||||||
|
const PARAMS = [
|
||||||
|
new RequestParam('abc', 123), new RequestParam('def', 456),
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
service = serviceFactory();
|
||||||
|
|
||||||
|
(service as any).createData = jasmine.createSpyObj('createData', {
|
||||||
|
create: 'TEST create',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to create', () => {
|
||||||
|
const out: any = service.create(OBJ, ...PARAMS);
|
||||||
|
|
||||||
|
expect((service as any).createData.create).toHaveBeenCalledWith(OBJ, ...PARAMS);
|
||||||
|
expect(out).toBe('TEST create');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
|
class TestService extends CreateDataImpl<any> {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
) {
|
||||||
|
super('test', requestService, rdbService, objectCache, halService, notificationsService, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return observableOf(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CreateDataImpl', () => {
|
||||||
|
let service: TestService;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let objectCache;
|
||||||
|
let notificationsService;
|
||||||
|
let remoteDataMocks;
|
||||||
|
let obj;
|
||||||
|
|
||||||
|
let MOCK_SUCCEEDED_RD;
|
||||||
|
let MOCK_FAILED_RD;
|
||||||
|
|
||||||
|
let buildFromRequestUUIDSpy: jasmine.Spy;
|
||||||
|
let createOnEndpointSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
objectCache = {
|
||||||
|
|
||||||
|
addPatch: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getObjectBySelfLink: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getByHref: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
obj = {
|
||||||
|
uuid: '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7',
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeStamp = new Date().getTime();
|
||||||
|
const msToLive = 15 * 60 * 1000;
|
||||||
|
const payload = { foo: 'bar' };
|
||||||
|
const statusCodeSuccess = 200;
|
||||||
|
const statusCodeError = 404;
|
||||||
|
const errorMessage = 'not found';
|
||||||
|
remoteDataMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||||
|
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||||
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new TestService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough();
|
||||||
|
createOnEndpointSpy = spyOn(service, 'createOnEndpoint').and.callThrough();
|
||||||
|
|
||||||
|
MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
|
||||||
|
MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should POST the object to the root endpoint with the given parameters and return the remote data', (done) => {
|
||||||
|
const params = [
|
||||||
|
new RequestParam('abc', 123), new RequestParam('def', 456)
|
||||||
|
];
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Success));
|
||||||
|
|
||||||
|
service.create(obj, ...params).subscribe(out => {
|
||||||
|
expect(createOnEndpointSpy).toHaveBeenCalledWith(obj, jasmine.anything());
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.POST,
|
||||||
|
uuid: requestService.generateRequestId(),
|
||||||
|
href: 'https://rest.api/core?abc=123&def=456',
|
||||||
|
body: JSON.stringify(obj),
|
||||||
|
}));
|
||||||
|
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId());
|
||||||
|
expect(out).toEqual(remoteDataMocks.Success);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOnEndpoint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Success));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a POST request with the object as JSON', (done) => {
|
||||||
|
service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.POST,
|
||||||
|
body: JSON.stringify(obj),
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the POST request to the given endpoint', (done) => {
|
||||||
|
|
||||||
|
service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.POST,
|
||||||
|
href: 'https://rest.api/core/custom?search',
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the remote data for the sent request', (done) => {
|
||||||
|
service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.POST,
|
||||||
|
uuid: requestService.generateRequestId(),
|
||||||
|
}));
|
||||||
|
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId());
|
||||||
|
expect(notificationsService.error).not.toHaveBeenCalled();
|
||||||
|
expect(out).toEqual(remoteDataMocks.Success);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show an error notification if the request fails', (done) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Error));
|
||||||
|
|
||||||
|
service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.POST,
|
||||||
|
uuid: requestService.generateRequestId(),
|
||||||
|
}));
|
||||||
|
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId());
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(out).toEqual(remoteDataMocks.Error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
107
src/app/core/data/base/create-data.ts
Normal file
107
src/app/core/data/base/create-data.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { BaseDataService } from './base-data.service';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
|
import { distinctUntilChanged, map, take, takeWhile } from 'rxjs/operators';
|
||||||
|
import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer';
|
||||||
|
import { getClassForType } from '../../cache/builders/build-decorators';
|
||||||
|
import { CreateRequest } from '../request.models';
|
||||||
|
import { NotificationOptions } from '../../../shared/notifications/models/notification-options.model';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a data service that can create objects.
|
||||||
|
*/
|
||||||
|
export interface CreateData<T extends CacheableObject> {
|
||||||
|
/**
|
||||||
|
* Create a new DSpaceObject on the server, and store the response
|
||||||
|
* in the object cache
|
||||||
|
*
|
||||||
|
* @param object The object to create
|
||||||
|
* @param params Array with additional params to combine with query string
|
||||||
|
*/
|
||||||
|
create(object: T, ...params: RequestParam[]): Observable<RemoteData<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DataService feature to create objects.
|
||||||
|
*
|
||||||
|
* Concrete data services can use this feature by implementing {@link CreateData}
|
||||||
|
* and delegating its method to an inner instance of this class.
|
||||||
|
*/
|
||||||
|
export class CreateDataImpl<T extends CacheableObject> extends BaseDataService<T> implements CreateData<T> {
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new object on the server, and store the response in the object cache
|
||||||
|
*
|
||||||
|
* @param object The object to create
|
||||||
|
* @param params Array with additional params to combine with query string
|
||||||
|
*/
|
||||||
|
create(object: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
||||||
|
const endpoint$ = this.getEndpoint().pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpoint: string) => this.buildHrefWithParams(endpoint, params)),
|
||||||
|
);
|
||||||
|
return this.createOnEndpoint(object, endpoint$);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST request to create a new resource to a specific endpoint.
|
||||||
|
* Use this method if the endpoint needs to be adjusted. In most cases {@link create} should be sufficient.
|
||||||
|
* @param object the object to create
|
||||||
|
* @param endpoint$ the endpoint to send the POST request to
|
||||||
|
*/
|
||||||
|
createOnEndpoint(object: T, endpoint$: Observable<string>): Observable<RemoteData<T>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const serializedObject = new DSpaceSerializer(getClassForType(object.type)).serialize(object);
|
||||||
|
|
||||||
|
endpoint$.pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe((endpoint: string) => {
|
||||||
|
const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedObject));
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result$ = this.rdbService.buildFromRequestUUID<T>(requestId);
|
||||||
|
|
||||||
|
// TODO a dataservice is not the best place to show a notification,
|
||||||
|
// this should move up to the components that use this method
|
||||||
|
result$.pipe(
|
||||||
|
takeWhile((rd: RemoteData<T>) => rd.isLoading, true)
|
||||||
|
).subscribe((rd: RemoteData<T>) => {
|
||||||
|
if (rd.hasFailed) {
|
||||||
|
this.notificationsService.error('Server Error:', rd.errorMessage, new NotificationOptions(-1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result$;
|
||||||
|
}
|
||||||
|
}
|
55
src/app/core/data/base/data-service.decorator.spec.ts
Normal file
55
src/app/core/data/base/data-service.decorator.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { BaseDataService } from './base-data.service';
|
||||||
|
import { HALDataService } from './hal-data-service.interface';
|
||||||
|
import { dataService, getDataServiceFor } from './data-service.decorator';
|
||||||
|
|
||||||
|
class TestService extends BaseDataService<any> {
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnotherTestService implements HALDataService<any> {
|
||||||
|
public findListByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let testType;
|
||||||
|
|
||||||
|
describe('@dataService/getDataServiceFor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
testType = new ResourceType('testType-' + new Date().getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a resourcetype for a dataservice', () => {
|
||||||
|
dataService(testType)(TestService);
|
||||||
|
expect(getDataServiceFor(testType)).toBe(TestService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`when the resource type isn't specified`, () => {
|
||||||
|
it(`should throw an error`, () => {
|
||||||
|
expect(() => {
|
||||||
|
dataService(undefined)(TestService);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`when there already is a registered dataservice for a resourcetype`, () => {
|
||||||
|
it(`should throw an error`, () => {
|
||||||
|
dataService(testType)(TestService);
|
||||||
|
expect(() => {
|
||||||
|
dataService(testType)(AnotherTestService);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
51
src/app/core/data/base/data-service.decorator.ts
Normal file
51
src/app/core/data/base/data-service.decorator.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
|
import { HALDataService } from './hal-data-service.interface';
|
||||||
|
|
||||||
|
export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<HALDataService<any>>>('getDataServiceFor', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => getDataServiceFor,
|
||||||
|
});
|
||||||
|
const dataServiceMap = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class decorator to indicate that this class is a data service for a given HAL resource type.
|
||||||
|
*
|
||||||
|
* In most cases, a data service should extend {@link BaseDataService}.
|
||||||
|
* At the very least it must implement {@link HALDataService} in order for it to work with {@link LinkService}.
|
||||||
|
*
|
||||||
|
* @param resourceType the resource type the class is a dataservice for
|
||||||
|
*/
|
||||||
|
export function dataService(resourceType: ResourceType) {
|
||||||
|
return (target: GenericConstructor<HALDataService<any>>): void => {
|
||||||
|
if (hasNoValue(resourceType)) {
|
||||||
|
throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`);
|
||||||
|
}
|
||||||
|
const existingDataservice = dataServiceMap.get(resourceType.value);
|
||||||
|
|
||||||
|
if (hasValue(existingDataservice)) {
|
||||||
|
throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataServiceMap.set(resourceType.value, target);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the dataservice matching the given resource type
|
||||||
|
*
|
||||||
|
* @param resourceType the resource type you want the matching dataservice for
|
||||||
|
*/
|
||||||
|
export function getDataServiceFor<T extends CacheableObject>(resourceType: ResourceType): GenericConstructor<HALDataService<any>> {
|
||||||
|
return dataServiceMap.get(resourceType.value);
|
||||||
|
}
|
245
src/app/core/data/base/delete-data.spec.ts
Normal file
245
src/app/core/data/base/delete-data.spec.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { constructIdEndpointDefault } from './identifiable-data.service';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
|
import { DeleteData, DeleteDataImpl } from './delete-data';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it
|
||||||
|
*/
|
||||||
|
export function testDeleteDataImplementation(serviceFactory: () => DeleteData<any>) {
|
||||||
|
let service;
|
||||||
|
|
||||||
|
describe('DeleteData implementation', () => {
|
||||||
|
const ID = '2ce78f3a-791b-4d70-b5eb-753d587bbadd';
|
||||||
|
const HREF = 'https://rest.api/core/items/' + ID;
|
||||||
|
const COPY_VIRTUAL_METADATA = [
|
||||||
|
'a', 'b', 'c'
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
service = serviceFactory();
|
||||||
|
(service as any).deleteData = jasmine.createSpyObj('deleteData', {
|
||||||
|
delete: 'TEST delete',
|
||||||
|
deleteByHref: 'TEST deleteByHref',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to delete', () => {
|
||||||
|
const out: any = service.delete(ID, COPY_VIRTUAL_METADATA);
|
||||||
|
|
||||||
|
expect((service as any).deleteData.delete).toHaveBeenCalledWith(ID, COPY_VIRTUAL_METADATA);
|
||||||
|
expect(out).toBe('TEST delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to deleteByHref', () => {
|
||||||
|
const out: any = service.deleteByHref(HREF, COPY_VIRTUAL_METADATA);
|
||||||
|
|
||||||
|
expect((service as any).deleteData.deleteByHref).toHaveBeenCalledWith(HREF, COPY_VIRTUAL_METADATA);
|
||||||
|
expect(out).toBe('TEST deleteByHref');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
|
const BOOLEAN = { f: false, t: true };
|
||||||
|
|
||||||
|
class TestService extends DeleteDataImpl<any> {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
) {
|
||||||
|
super(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined, constructIdEndpointDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return observableOf(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DeleteDataImpl', () => {
|
||||||
|
let service: TestService;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let objectCache;
|
||||||
|
let notificationsService;
|
||||||
|
let selfLink;
|
||||||
|
let linksToFollow;
|
||||||
|
let testScheduler;
|
||||||
|
let remoteDataMocks;
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
objectCache = {
|
||||||
|
|
||||||
|
addPatch: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getObjectBySelfLink: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getByHref: () => {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
notificationsService = {} as NotificationsService;
|
||||||
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
linksToFollow = [
|
||||||
|
followLink('a'),
|
||||||
|
followLink('b')
|
||||||
|
];
|
||||||
|
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
// asserting the two objects are equal
|
||||||
|
// e.g. using chai.
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeStamp = new Date().getTime();
|
||||||
|
const msToLive = 15 * 60 * 1000;
|
||||||
|
const payload = { foo: 'bar' };
|
||||||
|
const statusCodeSuccess = 200;
|
||||||
|
const statusCodeError = 404;
|
||||||
|
const errorMessage = 'not found';
|
||||||
|
remoteDataMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||||
|
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||||
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new TestService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
let MOCK_SUCCEEDED_RD;
|
||||||
|
let MOCK_FAILED_RD;
|
||||||
|
|
||||||
|
let invalidateByHrefSpy: jasmine.Spy;
|
||||||
|
let buildFromRequestUUIDSpy: jasmine.Spy;
|
||||||
|
let getIDHrefObsSpy: jasmine.Spy;
|
||||||
|
let deleteByHrefSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
|
||||||
|
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough();
|
||||||
|
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
|
||||||
|
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
|
||||||
|
|
||||||
|
MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
|
||||||
|
MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve href by ID and call deleteByHref', () => {
|
||||||
|
getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
|
|
||||||
|
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
|
||||||
|
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
|
||||||
|
expect(deleteByHrefSpy).toHaveBeenCalledWith('some-href', ['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteByHref', () => {
|
||||||
|
it('should call invalidateByHref if the DELETE request succeeds', (done) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
|
||||||
|
service.deleteByHref('some-href').subscribe(rd => {
|
||||||
|
expect(rd).toBe(MOCK_SUCCEEDED_RD);
|
||||||
|
expect(invalidateByHrefSpy).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
|
||||||
|
|
||||||
|
service.deleteByHref('some-href');
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(invalidateByHrefSpy).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call invalidateByHref if the DELETE request fails', (done) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
|
||||||
|
|
||||||
|
service.deleteByHref('some-href').subscribe(rd => {
|
||||||
|
expect(rd).toBe(MOCK_FAILED_RD);
|
||||||
|
expect(invalidateByHrefSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wait for invalidateByHref before emitting', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(
|
||||||
|
cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away
|
||||||
|
);
|
||||||
|
invalidateByHrefSpy.and.returnValue(
|
||||||
|
cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer
|
||||||
|
);
|
||||||
|
|
||||||
|
const done$ = service.deleteByHref('some-href');
|
||||||
|
expectObservable(done$).toBe(
|
||||||
|
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wait for the DELETE request to resolve before emitting', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
|
buildFromRequestUUIDSpy.and.returnValue(
|
||||||
|
cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while
|
||||||
|
);
|
||||||
|
invalidateByHrefSpy.and.returnValue(
|
||||||
|
cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner
|
||||||
|
); // e.g.: maybe already stale before this call?
|
||||||
|
|
||||||
|
const done$ = service.deleteByHref('some-href');
|
||||||
|
expectObservable(done$).toBe(
|
||||||
|
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
108
src/app/core/data/base/delete-data.ts
Normal file
108
src/app/core/data/base/delete-data.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { AsyncSubject, combineLatest, Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { NoContent } from '../../shared/NoContent.model';
|
||||||
|
import { filter, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { DeleteRequest } from '../request.models';
|
||||||
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { ConstructIdEndpoint, IdentifiableDataService } from './identifiable-data.service';
|
||||||
|
|
||||||
|
export interface DeleteData<T extends CacheableObject> {
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param objectId The id of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
*/
|
||||||
|
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param href The self link of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
* Only emits once all request related to the DSO has been invalidated.
|
||||||
|
*/
|
||||||
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeleteDataImpl<T extends CacheableObject> extends IdentifiableDataService<T> implements DeleteData<T> {
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
protected constructIdEndpoint: ConstructIdEndpoint,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive, constructIdEndpoint);
|
||||||
|
if (hasNoValue(constructIdEndpoint)) {
|
||||||
|
throw new Error(`DeleteDataImpl initialized without a constructIdEndpoint method (linkPath: ${linkPath})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.getIDHrefObs(objectId).pipe(
|
||||||
|
switchMap((href: string) => this.deleteByHref(href, copyVirtualMetadata)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
if (copyVirtualMetadata) {
|
||||||
|
copyVirtualMetadata.forEach((id) =>
|
||||||
|
href += (href.includes('?') ? '&' : '?')
|
||||||
|
+ 'copyVirtualMetadata='
|
||||||
|
+ id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new DeleteRequest(requestId, href);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
this.requestService.send(request);
|
||||||
|
|
||||||
|
const response$ = this.rdbService.buildFromRequestUUID(requestId);
|
||||||
|
|
||||||
|
const invalidated$ = new AsyncSubject<boolean>();
|
||||||
|
response$.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<NoContent>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
return this.invalidateByHref(href);
|
||||||
|
} else {
|
||||||
|
return [true];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe(() => {
|
||||||
|
invalidated$.next(true);
|
||||||
|
invalidated$.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
return combineLatest([response$, invalidated$]).pipe(
|
||||||
|
filter(([_, invalidated]) => invalidated),
|
||||||
|
map(([response, _]) => response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
296
src/app/core/data/base/find-all-data.spec.ts
Normal file
296
src/app/core/data/base/find-all-data.spec.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { FindAllData, FindAllDataImpl } from './find-all-data';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it
|
||||||
|
*/
|
||||||
|
export function testFindAllDataImplementation(serviceFactory: () => FindAllData<any>) {
|
||||||
|
let service;
|
||||||
|
|
||||||
|
describe('FindAllData implementation', () => {
|
||||||
|
const OPTIONS = Object.assign(new FindListOptions(), { elementsPerPage: 10, currentPage: 3 });
|
||||||
|
const FOLLOWLINKS = [
|
||||||
|
followLink('test'),
|
||||||
|
followLink('something'),
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
service = serviceFactory();
|
||||||
|
(service as any).findAllData = jasmine.createSpyObj('findAllData', {
|
||||||
|
findAll: 'TEST findAll',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to findAll', () => {
|
||||||
|
const out: any = service.findAll(OPTIONS, false, true, ...FOLLOWLINKS);
|
||||||
|
|
||||||
|
expect((service as any).findAllData.findAll).toHaveBeenCalledWith(OPTIONS, false, true, ...FOLLOWLINKS);
|
||||||
|
expect(out).toBe('TEST findAll');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
|
class TestService extends FindAllDataImpl<any> {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
) {
|
||||||
|
super(undefined, requestService, rdbService, objectCache, halService, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return observableOf(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FindAllDataImpl', () => {
|
||||||
|
let service: TestService;
|
||||||
|
let options: FindListOptions;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let objectCache;
|
||||||
|
let selfLink;
|
||||||
|
let linksToFollow;
|
||||||
|
let testScheduler;
|
||||||
|
let remoteDataMocks;
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
objectCache = {
|
||||||
|
|
||||||
|
addPatch: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getObjectBySelfLink: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getByHref: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
linksToFollow = [
|
||||||
|
followLink('a'),
|
||||||
|
followLink('b'),
|
||||||
|
];
|
||||||
|
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
// asserting the two objects are equal
|
||||||
|
// e.g. using chai.
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeStamp = new Date().getTime();
|
||||||
|
const msToLive = 15 * 60 * 1000;
|
||||||
|
const payload = { foo: 'bar' };
|
||||||
|
const statusCodeSuccess = 200;
|
||||||
|
const statusCodeError = 404;
|
||||||
|
const errorMessage = 'not found';
|
||||||
|
remoteDataMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||||
|
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||||
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new TestService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFindAllHref', () => {
|
||||||
|
|
||||||
|
it('should return an observable with the endpoint', () => {
|
||||||
|
options = {};
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(endpoint);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include page in href if currentPage provided in options', () => {
|
||||||
|
options = { currentPage: 2 };
|
||||||
|
const expected = `${endpoint}?page=${options.currentPage - 1}`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include size in href if elementsPerPage provided in options', () => {
|
||||||
|
options = { elementsPerPage: 5 };
|
||||||
|
const expected = `${endpoint}?size=${options.elementsPerPage}`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include sort href if SortOptions provided in options', () => {
|
||||||
|
const sortOptions = new SortOptions('field1', SortDirection.ASC);
|
||||||
|
options = { sort: sortOptions };
|
||||||
|
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include startsWith in href if startsWith provided in options', () => {
|
||||||
|
options = { startsWith: 'ab' };
|
||||||
|
const expected = `${endpoint}?startsWith=${options.startsWith}`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include all provided options in href', () => {
|
||||||
|
const sortOptions = new SortOptions('field1', SortDirection.DESC);
|
||||||
|
options = {
|
||||||
|
currentPage: 6,
|
||||||
|
elementsPerPage: 10,
|
||||||
|
sort: sortOptions,
|
||||||
|
startsWith: 'ab',
|
||||||
|
|
||||||
|
};
|
||||||
|
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
||||||
|
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include all searchParams in href if any provided in options', () => {
|
||||||
|
options = {
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('param1', 'test'),
|
||||||
|
new RequestParam('param2', 'test2'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = `${endpoint}?param1=test¶m2=test2`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include linkPath in href if any provided', () => {
|
||||||
|
const expected = `${endpoint}/test/entries`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref({}, 'test/entries').subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include single linksToFollow as embed', () => {
|
||||||
|
const expected = `${endpoint}?embed=bundles`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref({}, null, followLink('bundles')).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include single linksToFollow as embed and its size', () => {
|
||||||
|
const expected = `${endpoint}?embed.size=bundles=5&embed=bundles`;
|
||||||
|
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
|
elementsPerPage: 5,
|
||||||
|
});
|
||||||
|
(service as any).getFindAllHref({}, null, followLink('bundles', { findListOptions: config })).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include multiple linksToFollow as embed', () => {
|
||||||
|
const expected = `${endpoint}?embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf')).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include multiple linksToFollow as embed and its sizes if given', () => {
|
||||||
|
const expected = `${endpoint}?embed=bundles&embed.size=owningCollection=2&embed=owningCollection&embed=templateItemOf`;
|
||||||
|
|
||||||
|
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
|
elementsPerPage: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', { findListOptions: config }), followLink('templateItemOf')).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
|
const expected = `${endpoint}?embed=templateItemOf`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(
|
||||||
|
{},
|
||||||
|
null,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf'),
|
||||||
|
).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
|
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include nested linksToFollow 2lvl and nested embed\'s size', () => {
|
||||||
|
const expected = `${endpoint}?embed.size=owningCollection/itemtemplate=4&embed=owningCollection/itemtemplate`;
|
||||||
|
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
|
elementsPerPage: 4,
|
||||||
|
});
|
||||||
|
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', { findListOptions: config }))).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
101
src/app/core/data/base/find-all-data.ts
Normal file
101
src/app/core/data/base/find-all-data.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { BaseDataService } from './base-data.service';
|
||||||
|
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
||||||
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a data service that list all of its objects.
|
||||||
|
*/
|
||||||
|
export interface FindAllData<T extends CacheableObject> {
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DataService feature to list all objects.
|
||||||
|
*
|
||||||
|
* Concrete data services can use this feature by implementing {@link FindAllData}
|
||||||
|
* and delegating its method to an inner instance of this class.
|
||||||
|
*/
|
||||||
|
export class FindAllDataImpl<T extends CacheableObject> extends BaseDataService<T> implements FindAllData<T> {
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
return this.findListByHref(this.getFindAllHref(options), options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF with given options object
|
||||||
|
*
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param linkPath The link path for the object
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
||||||
|
let endpoint$: Observable<string>;
|
||||||
|
const args = [];
|
||||||
|
|
||||||
|
endpoint$ = this.getBrowseEndpoint(options).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return endpoint$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
||||||
|
}
|
||||||
|
}
|
41
src/app/core/data/base/hal-data-service.interface.ts
Normal file
41
src/app/core/data/base/hal-data-service.interface.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface defining the minimum functionality needed for a data service to resolve HAL resources.
|
||||||
|
*/
|
||||||
|
export interface HALDataService<T extends HALResource> {
|
||||||
|
/**
|
||||||
|
* 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. 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.
|
||||||
|
* @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 | Observable<string>, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Observable of a {@link RemoteData} of a {@link PaginatedList} 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 list we want to retrieve. Can be a string or an Observable<string>
|
||||||
|
* @param findListOptions The options for to use for this find list request.
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's no valid cached version.
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
findListByHref(href$: string | Observable<string>, findListOptions?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>>;
|
||||||
|
}
|
145
src/app/core/data/base/identifiable-data.service.spec.ts
Normal file
145
src/app/core/data/base/identifiable-data.service.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { IdentifiableDataService } from './identifiable-data.service';
|
||||||
|
|
||||||
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
|
class TestService extends IdentifiableDataService<any> {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
) {
|
||||||
|
super(undefined, requestService, rdbService, objectCache, halService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return observableOf(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('IdentifiableDataService', () => {
|
||||||
|
let service: TestService;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let objectCache;
|
||||||
|
let selfLink;
|
||||||
|
let linksToFollow;
|
||||||
|
let testScheduler;
|
||||||
|
let remoteDataMocks;
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
objectCache = {
|
||||||
|
|
||||||
|
addPatch: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getObjectBySelfLink: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getByHref: () => {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
linksToFollow = [
|
||||||
|
followLink('a'),
|
||||||
|
followLink('b')
|
||||||
|
];
|
||||||
|
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
// asserting the two objects are equal
|
||||||
|
// e.g. using chai.
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeStamp = new Date().getTime();
|
||||||
|
const msToLive = 15 * 60 * 1000;
|
||||||
|
const payload = { foo: 'bar' };
|
||||||
|
const statusCodeSuccess = 200;
|
||||||
|
const statusCodeError = 404;
|
||||||
|
const errorMessage = 'not found';
|
||||||
|
remoteDataMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||||
|
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||||
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new TestService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getIDHref', () => {
|
||||||
|
const endpointMock = 'https://dspace7-internal.atmire.com/server/api/core/items';
|
||||||
|
const resourceIdMock = '003c99b4-d4fe-44b0-a945-e12182a7ca89';
|
||||||
|
|
||||||
|
it('should return endpoint', () => {
|
||||||
|
const result = (service as any).getIDHref(endpointMock, resourceIdMock);
|
||||||
|
expect(result).toEqual(endpointMock + '/' + resourceIdMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include single linksToFollow as embed', () => {
|
||||||
|
const expected = `${endpointMock}/${resourceIdMock}?embed=bundles`;
|
||||||
|
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles'));
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include multiple linksToFollow as embed', () => {
|
||||||
|
const expected = `${endpointMock}/${resourceIdMock}?embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
||||||
|
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf'));
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
|
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
|
||||||
|
const result = (service as any).getIDHref(
|
||||||
|
endpointMock,
|
||||||
|
resourceIdMock,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf')
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
|
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
||||||
|
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships'))));
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
83
src/app/core/data/base/identifiable-data.service.ts
Normal file
83
src/app/core/data/base/identifiable-data.service.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { BaseDataService } from './base-data.service';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type for the method to construct an ID endpoint.
|
||||||
|
*/
|
||||||
|
export type ConstructIdEndpoint = (endpoint: string, resourceID: string) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default method to construct an ID endpoint
|
||||||
|
*/
|
||||||
|
export const constructIdEndpointDefault = (endpoint, resourceID) => `${endpoint}/${resourceID}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of data service that deals with objects that have an ID.
|
||||||
|
*
|
||||||
|
* The effective endpoint to use for the ID can be adjusted by providing a different {@link ConstructIdEndpoint} method.
|
||||||
|
* This method is passed as an argument so that it can be set on data service features without having to override them.
|
||||||
|
*/
|
||||||
|
export class IdentifiableDataService<T extends CacheableObject> extends BaseDataService<T> {
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected responseMsToLive?: number,
|
||||||
|
protected constructIdEndpoint: ConstructIdEndpoint = constructIdEndpointDefault,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @param id ID 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
|
||||||
|
*/
|
||||||
|
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
||||||
|
const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow);
|
||||||
|
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF for a specific object based on its identifier; with possible embed query params based on linksToFollow
|
||||||
|
* @param endpoint The base endpoint for the type of object
|
||||||
|
* @param resourceID The identifier for the object
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
getIDHref(endpoint, resourceID, ...linksToFollow: FollowLinkConfig<T>[]): string {
|
||||||
|
return this.buildHrefFromFindOptions(this.constructIdEndpoint(endpoint, resourceID), {}, [], ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an observable for the HREF of a specific object based on its identifier
|
||||||
|
* @param resourceID The identifier for the object
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
getIDHrefObs(resourceID: string, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
||||||
|
return this.getEndpoint().pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow)));
|
||||||
|
}
|
||||||
|
}
|
235
src/app/core/data/base/patch-data.spec.ts
Normal file
235
src/app/core/data/base/patch-data.spec.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
|
import { PatchData, PatchDataImpl } from './patch-data';
|
||||||
|
import { ChangeAnalyzer } from '../change-analyzer';
|
||||||
|
import { Item } from '../../shared/item.model';
|
||||||
|
import { compare, Operation } from 'fast-json-patch';
|
||||||
|
import { PatchRequest } from '../request.models';
|
||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { constructIdEndpointDefault } from './identifiable-data.service';
|
||||||
|
import { RestRequestMethod } from '../rest-request-method';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether calls to `PatchData` methods are correctly patched through in a concrete data service that implements it
|
||||||
|
*/
|
||||||
|
export function testPatchDataImplementation(serviceFactory: () => PatchData<any>) {
|
||||||
|
let service;
|
||||||
|
|
||||||
|
describe('PatchData implementation', () => {
|
||||||
|
const OBJ = Object.assign(new DSpaceObject(), {
|
||||||
|
uuid: '08eec68f-45e4-47a3-80c5-f0beb5627079',
|
||||||
|
});
|
||||||
|
const OPERATIONS = [
|
||||||
|
{ op: 'replace', path: '/0/value', value: 'test' },
|
||||||
|
{ op: 'add', path: '/2/value', value: 'test2' },
|
||||||
|
] as Operation[];
|
||||||
|
const METHOD = RestRequestMethod.POST;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
service = serviceFactory();
|
||||||
|
(service as any).patchData = jasmine.createSpyObj('patchData', {
|
||||||
|
patch: 'TEST patch',
|
||||||
|
update: 'TEST update',
|
||||||
|
commitUpdates: undefined,
|
||||||
|
createPatchFromCache: 'TEST createPatchFromCache',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to patch', () => {
|
||||||
|
const out: any = service.patch(OBJ, OPERATIONS);
|
||||||
|
|
||||||
|
expect((service as any).patchData.patch).toHaveBeenCalledWith(OBJ, OPERATIONS);
|
||||||
|
expect(out).toBe('TEST patch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to update', () => {
|
||||||
|
const out: any = service.update(OBJ);
|
||||||
|
|
||||||
|
expect((service as any).patchData.update).toHaveBeenCalledWith(OBJ);
|
||||||
|
expect(out).toBe('TEST update');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to commitUpdates', () => {
|
||||||
|
service.commitUpdates(METHOD);
|
||||||
|
expect((service as any).patchData.commitUpdates).toHaveBeenCalledWith(METHOD);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to createPatchFromCache', () => {
|
||||||
|
const out: any = service.createPatchFromCache(OBJ);
|
||||||
|
|
||||||
|
expect((service as any).patchData.createPatchFromCache).toHaveBeenCalledWith(OBJ);
|
||||||
|
expect(out).toBe('TEST createPatchFromCache');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
|
class TestService extends PatchDataImpl<any> {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected comparator: ChangeAnalyzer<Item>,
|
||||||
|
) {
|
||||||
|
super(undefined, requestService, rdbService, objectCache, halService, comparator, undefined, constructIdEndpointDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return observableOf(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
||||||
|
diff(object1: Item, object2: Item): Operation[] {
|
||||||
|
return compare((object1 as any).metadata, (object2 as any).metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PatchDataImpl', () => {
|
||||||
|
let service: TestService;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let comparator;
|
||||||
|
let objectCache;
|
||||||
|
let selfLink;
|
||||||
|
let linksToFollow;
|
||||||
|
let testScheduler;
|
||||||
|
let remoteDataMocks;
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
comparator = new DummyChangeAnalyzer() as any;
|
||||||
|
objectCache = {
|
||||||
|
|
||||||
|
addPatch: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getObjectBySelfLink: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getByHref: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
linksToFollow = [
|
||||||
|
followLink('a'),
|
||||||
|
followLink('b'),
|
||||||
|
];
|
||||||
|
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
// asserting the two objects are equal
|
||||||
|
// e.g. using chai.
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeStamp = new Date().getTime();
|
||||||
|
const msToLive = 15 * 60 * 1000;
|
||||||
|
const payload = { foo: 'bar' };
|
||||||
|
const statusCodeSuccess = 200;
|
||||||
|
const statusCodeError = 404;
|
||||||
|
const errorMessage = 'not found';
|
||||||
|
remoteDataMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||||
|
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||||
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new TestService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
comparator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('patch', () => {
|
||||||
|
const dso = {
|
||||||
|
uuid: 'dso-uuid'
|
||||||
|
};
|
||||||
|
const operations = [
|
||||||
|
Object.assign({
|
||||||
|
op: 'move',
|
||||||
|
from: '/1',
|
||||||
|
path: '/5'
|
||||||
|
}) as Operation
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
service.patch(dso, operations).subscribe(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a PatchRequest', () => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PatchRequest));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
let operations;
|
||||||
|
let dso;
|
||||||
|
let dso2;
|
||||||
|
const name1 = 'random string';
|
||||||
|
const name2 = 'another random string';
|
||||||
|
beforeEach(() => {
|
||||||
|
operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation];
|
||||||
|
|
||||||
|
dso = Object.assign(new DSpaceObject(), {
|
||||||
|
_links: { self: { href: selfLink } },
|
||||||
|
metadata: [{ key: 'dc.title', value: name1 }]
|
||||||
|
});
|
||||||
|
|
||||||
|
dso2 = Object.assign(new DSpaceObject(), {
|
||||||
|
_links: { self: { href: selfLink } },
|
||||||
|
metadata: [{ key: 'dc.title', value: name2 }]
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso));
|
||||||
|
spyOn(objectCache, 'addPatch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call addPatch on the object cache with the right parameters when there are differences', () => {
|
||||||
|
service.update(dso2).subscribe();
|
||||||
|
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call addPatch on the object cache with the right parameters when there are no differences', () => {
|
||||||
|
service.update(dso).subscribe();
|
||||||
|
expect(objectCache.addPatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
143
src/app/core/data/base/patch-data.ts
Normal file
143
src/app/core/data/base/patch-data.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { find, map, mergeMap } from 'rxjs/operators';
|
||||||
|
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { PatchRequest } from '../request.models';
|
||||||
|
import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../shared/operators';
|
||||||
|
import { ChangeAnalyzer } from '../change-analyzer';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { RestRequestMethod } from '../rest-request-method';
|
||||||
|
import { ConstructIdEndpoint, IdentifiableDataService } from './identifiable-data.service';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a data service that can patch and update objects.
|
||||||
|
*/
|
||||||
|
export interface PatchData<T extends CacheableObject> {
|
||||||
|
/**
|
||||||
|
* Send a patch request for a specified object
|
||||||
|
* @param {T} object The object to send a patch request for
|
||||||
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
|
*/
|
||||||
|
patch(object: T, operations: Operation[]): Observable<RemoteData<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
|
* @param {DSpaceObject} object The given object
|
||||||
|
*/
|
||||||
|
update(object: T): Observable<RemoteData<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
commitUpdates(method?: RestRequestMethod): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of operations representing the difference between an object and its latest value in the cache.
|
||||||
|
* @param object the object to resolve to a list of patch operations
|
||||||
|
*/
|
||||||
|
createPatchFromCache(object: T): Observable<Operation[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DataService feature to patch and update objects.
|
||||||
|
*
|
||||||
|
* Concrete data services can use this feature by implementing {@link PatchData}
|
||||||
|
* and delegating its method to an inner instance of this class.
|
||||||
|
*
|
||||||
|
* Note that this feature requires the object in question to have an ID.
|
||||||
|
* Make sure to use the same {@link ConstructIdEndpoint} as in the parent data service.
|
||||||
|
*/
|
||||||
|
export class PatchDataImpl<T extends CacheableObject> extends IdentifiableDataService<T> implements PatchData<T> {
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected comparator: ChangeAnalyzer<T>,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
protected constructIdEndpoint: ConstructIdEndpoint,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive, constructIdEndpoint);
|
||||||
|
if (hasNoValue(constructIdEndpoint)) {
|
||||||
|
throw new Error(`PatchDataImpl initialized without a constructIdEndpoint method (linkPath: ${linkPath})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a patch request for a specified object
|
||||||
|
* @param {T} object The object to send a patch request for
|
||||||
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
|
*/
|
||||||
|
patch(object: T, operations: Operation[]): Observable<RemoteData<T>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, object.uuid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
).subscribe((href: string) => {
|
||||||
|
const request = new PatchRequest(requestId, href, operations);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
|
* @param {DSpaceObject} object The given object
|
||||||
|
*/
|
||||||
|
update(object: T): Observable<RemoteData<T>> {
|
||||||
|
return this.createPatchFromCache(object).pipe(
|
||||||
|
mergeMap((operations: Operation[]) => {
|
||||||
|
if (isNotEmpty(operations)) {
|
||||||
|
this.objectCache.addPatch(object._links.self.href, operations);
|
||||||
|
}
|
||||||
|
return this.findByHref(object._links.self.href, true, true);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
commitUpdates(method?: RestRequestMethod): void {
|
||||||
|
this.requestService.commit(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of operations representing the difference between an object and its latest value in the cache.
|
||||||
|
* @param object the object to resolve to a list of patch operations
|
||||||
|
*/
|
||||||
|
createPatchFromCache(object: T): Observable<Operation[]> {
|
||||||
|
const oldVersion$ = this.findByHref(object._links.self.href, true, false);
|
||||||
|
return oldVersion$.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((oldVersion: T) => this.comparator.diff(oldVersion, object)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
176
src/app/core/data/base/put-data.spec.ts
Normal file
176
src/app/core/data/base/put-data.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
|
import { PutData, PutDataImpl } from './put-data';
|
||||||
|
import { RestRequestMethod } from '../rest-request-method';
|
||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether calls to `PutData` methods are correctly patched through in a concrete data service that implements it
|
||||||
|
*/
|
||||||
|
export function testPutDataImplementation(serviceFactory: () => PutData<any>) {
|
||||||
|
let service;
|
||||||
|
|
||||||
|
describe('PutData implementation', () => {
|
||||||
|
const OBJ = Object.assign(new DSpaceObject(), {
|
||||||
|
uuid: '08eec68f-45e4-47a3-80c5-f0beb5627079',
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
service = serviceFactory();
|
||||||
|
(service as any).putData = jasmine.createSpyObj('putData', {
|
||||||
|
put: 'TEST put',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to put', () => {
|
||||||
|
const out: any = service.put(OBJ);
|
||||||
|
|
||||||
|
expect((service as any).putData.put).toHaveBeenCalledWith(OBJ);
|
||||||
|
expect(out).toBe('TEST put');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
|
class TestService extends PutDataImpl<any> {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
) {
|
||||||
|
super(undefined, requestService, rdbService, objectCache, halService, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return observableOf(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PutDataImpl', () => {
|
||||||
|
let service: TestService;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let objectCache;
|
||||||
|
let selfLink;
|
||||||
|
let remoteDataMocks;
|
||||||
|
|
||||||
|
let obj;
|
||||||
|
let buildFromRequestUUIDSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
objectCache = {
|
||||||
|
|
||||||
|
addPatch: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getObjectBySelfLink: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getByHref: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
|
||||||
|
const timeStamp = new Date().getTime();
|
||||||
|
const msToLive = 15 * 60 * 1000;
|
||||||
|
const payload = { foo: 'bar' };
|
||||||
|
const statusCodeSuccess = 200;
|
||||||
|
const statusCodeError = 404;
|
||||||
|
const errorMessage = 'not found';
|
||||||
|
remoteDataMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||||
|
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||||
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new TestService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
obj = Object.assign(new DSpaceObject(), {
|
||||||
|
uuid: '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7',
|
||||||
|
metadata: { // recognized properties will be serialized
|
||||||
|
['dc.title']: [
|
||||||
|
{ language: 'en', value: 'some object' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
data: [ 1, 2, 3, 4 ], // unrecognized properties won't be serialized
|
||||||
|
_links: { self: { href: selfLink } },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.returnValue(observableOf(remoteDataMocks.Success));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('put', () => {
|
||||||
|
it('should send a PUT request with the serialized object', (done) => {
|
||||||
|
service.put(obj).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.PUT,
|
||||||
|
body: { // _links are not serialized
|
||||||
|
uuid: obj.uuid,
|
||||||
|
metadata: obj.metadata
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the PUT request to the object\'s self link', (done) => {
|
||||||
|
service.put(obj).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.PUT,
|
||||||
|
href: selfLink,
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the remote data for the sent request', (done) => {
|
||||||
|
service.put(obj).subscribe(out => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.PUT,
|
||||||
|
uuid: requestService.generateRequestId(),
|
||||||
|
}));
|
||||||
|
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId());
|
||||||
|
expect(out).toEqual(remoteDataMocks.Success);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
69
src/app/core/data/base/put-data.ts
Normal file
69
src/app/core/data/base/put-data.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { BaseDataService } from './base-data.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer';
|
||||||
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
|
import { PutRequest } from '../request.models';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a data service that can send PUT requests.
|
||||||
|
*/
|
||||||
|
export interface PutData<T extends CacheableObject> {
|
||||||
|
/**
|
||||||
|
* Send a PUT request for the specified object
|
||||||
|
*
|
||||||
|
* @param object The object to send a put request for.
|
||||||
|
*/
|
||||||
|
put(object: T): Observable<RemoteData<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DataService feature to send PUT requests.
|
||||||
|
*
|
||||||
|
* Concrete data services can use this feature by implementing {@link PutData}
|
||||||
|
* and delegating its method to an inner instance of this class.
|
||||||
|
*/
|
||||||
|
export class PutDataImpl<T extends CacheableObject> extends BaseDataService<T> implements PutData<T> {
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a PUT request for the specified object
|
||||||
|
*
|
||||||
|
* @param object The object to send a put request for.
|
||||||
|
*/
|
||||||
|
put(object: T): Observable<RemoteData<T>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||||
|
const request = new PutRequest(requestId, object._links.self.href, serializedObject);
|
||||||
|
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestService.send(request);
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
|
}
|
||||||
|
}
|
141
src/app/core/data/base/search-data.spec.ts
Normal file
141
src/app/core/data/base/search-data.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { constructSearchEndpointDefault, SearchData, SearchDataImpl } from './search-data';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether calls to `SearchData` methods are correctly patched through in a concrete data service that implements it
|
||||||
|
*/
|
||||||
|
export function testSearchDataImplementation(serviceFactory: () => SearchData<any>) {
|
||||||
|
let service;
|
||||||
|
|
||||||
|
describe('SearchData implementation', () => {
|
||||||
|
const OPTIONS = Object.assign(new FindListOptions(), { elementsPerPage: 10, currentPage: 3 });
|
||||||
|
const FOLLOWLINKS = [
|
||||||
|
followLink('test'),
|
||||||
|
followLink('something'),
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
service = serviceFactory();
|
||||||
|
(service as any).searchData = jasmine.createSpyObj('searchData', {
|
||||||
|
searchBy: 'TEST searchBy',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle calls to searchBy', () => {
|
||||||
|
const out: any = service.searchBy('searchMethod', OPTIONS, false, true, ...FOLLOWLINKS);
|
||||||
|
|
||||||
|
expect((service as any).searchData.searchBy).toHaveBeenCalledWith('searchMethod', OPTIONS, false, true, ...FOLLOWLINKS);
|
||||||
|
expect(out).toBe('TEST searchBy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
|
describe('SearchDataImpl', () => {
|
||||||
|
let service: SearchDataImpl<any>;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let linksToFollow;
|
||||||
|
|
||||||
|
let constructSearchEndpointSpy;
|
||||||
|
let options;
|
||||||
|
|
||||||
|
function initTestService(): SearchDataImpl<any> {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: observableOf(endpoint),
|
||||||
|
});
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
linksToFollow = [
|
||||||
|
followLink('a'),
|
||||||
|
followLink('b'),
|
||||||
|
];
|
||||||
|
|
||||||
|
constructSearchEndpointSpy = jasmine.createSpy('constructSearchEndpointSpy').and.callFake(constructSearchEndpointDefault);
|
||||||
|
|
||||||
|
options = Object.assign(new FindListOptions(), {
|
||||||
|
elementsPerPage: 5,
|
||||||
|
currentPage: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new SearchDataImpl(
|
||||||
|
'test',
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
undefined,
|
||||||
|
halService,
|
||||||
|
undefined,
|
||||||
|
constructSearchEndpointSpy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSearchEndpoint', () => {
|
||||||
|
it('should return the search endpoint for the given method', (done) => {
|
||||||
|
(service as any).getSearchEndpoint('testMethod').subscribe(searchEndpoint => {
|
||||||
|
expect(halService.getEndpoint).toHaveBeenCalledWith('test');
|
||||||
|
expect(searchEndpoint).toBe('https://rest.api/core/search/testMethod');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use constructSearchEndpoint to construct the search endpoint', (done) => {
|
||||||
|
(service as any).getSearchEndpoint('testMethod').subscribe(() => {
|
||||||
|
expect(constructSearchEndpointSpy).toHaveBeenCalledWith('https://rest.api/core', 'testMethod');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSearchByHref', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service as any, 'getSearchEndpoint').and.callThrough();
|
||||||
|
spyOn(service, 'buildHrefFromFindOptions').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the search endpoint with additional query parameters', (done) => {
|
||||||
|
service.getSearchByHref('testMethod', options, ...linksToFollow).subscribe(href => {
|
||||||
|
expect((service as any).getSearchEndpoint).toHaveBeenCalledWith('testMethod');
|
||||||
|
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(
|
||||||
|
'https://rest.api/core/search/testMethod',
|
||||||
|
options,
|
||||||
|
[],
|
||||||
|
...linksToFollow,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(href).toBe('https://rest.api/core/search/testMethod?page=2&size=5&embed=a&embed=b');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchBy', () => {
|
||||||
|
it('should patch getSearchEndpoint into findListByHref and return the result', () => {
|
||||||
|
spyOn(service, 'getSearchByHref').and.returnValue('endpoint' as any);
|
||||||
|
spyOn(service, 'findListByHref').and.returnValue('resulting remote data' as any);
|
||||||
|
|
||||||
|
const out: any = service.searchBy('testMethod', options, false, true, ...linksToFollow);
|
||||||
|
|
||||||
|
expect(service.getSearchByHref).toHaveBeenCalledWith('testMethod', options, ...linksToFollow);
|
||||||
|
expect(service.findListByHref).toHaveBeenCalledWith('endpoint', undefined, false, true, ...linksToFollow);
|
||||||
|
expect(out).toBe('resulting remote data');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
134
src/app/core/data/base/search-data.ts
Normal file
134
src/app/core/data/base/search-data.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { BaseDataService } from './base-data.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { filter, map } from 'rxjs/operators';
|
||||||
|
import { hasNoValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type for method to construct a search endpoint
|
||||||
|
*/
|
||||||
|
export type ConstructSearchEndpoint = (href: string, searchMethod: string) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default method to construct a search endpoint
|
||||||
|
*/
|
||||||
|
export const constructSearchEndpointDefault = (href: string, searchMethod: string): string => `${href}/search/${searchMethod}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a data service that can search for objects.
|
||||||
|
*/
|
||||||
|
export interface SearchData<T extends CacheableObject> {
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DataService feature to search for objects.
|
||||||
|
*
|
||||||
|
* Concrete data services can use this feature by implementing {@link SearchData}
|
||||||
|
* and delegating its method to an inner instance of this class.
|
||||||
|
*/
|
||||||
|
export class SearchDataImpl<T extends CacheableObject> extends BaseDataService<T> implements SearchData<T> {
|
||||||
|
/**
|
||||||
|
* @param linkPath
|
||||||
|
* @param requestService
|
||||||
|
* @param rdbService
|
||||||
|
* @param objectCache
|
||||||
|
* @param halService
|
||||||
|
* @param responseMsToLive
|
||||||
|
* @param constructSearchEndpoint an optional method to construct the search endpoint, passed as an argument so it can be
|
||||||
|
* modified without extending this class. Defaults to `${href}/search/${searchMethod}`
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
private constructSearchEndpoint: ConstructSearchEndpoint = constructSearchEndpointDefault,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive);
|
||||||
|
if (hasNoValue(constructSearchEndpoint)) {
|
||||||
|
throw new Error(`SearchDataImpl initialized without a constructSearchEndpoint method (linkPath: ${linkPath})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
|
||||||
|
return this.findListByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF for a specific object's search method with given options object
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
||||||
|
let result$: Observable<string>;
|
||||||
|
const args = [];
|
||||||
|
|
||||||
|
result$ = this.getSearchEndpoint(searchMethod);
|
||||||
|
|
||||||
|
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return object search endpoint by given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
*/
|
||||||
|
private getSearchEndpoint(searchMethod: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
map(href => this.constructSearchEndpoint(href, searchMethod)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -12,6 +12,9 @@ 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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
|
||||||
describe('BitstreamDataService', () => {
|
describe('BitstreamDataService', () => {
|
||||||
let service: BitstreamDataService;
|
let service: BitstreamDataService;
|
||||||
@@ -47,7 +50,14 @@ describe('BitstreamDataService', () => {
|
|||||||
});
|
});
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
|
||||||
service = new BitstreamDataService(requestService, rdbService, null, objectCache, halService, null, null, null, null, bitstreamFormatService);
|
service = new BitstreamDataService(requestService, rdbService, objectCache, halService, null, bitstreamFormatService, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new BitstreamDataService(null, null, null, null, null, null, null, null);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
testPatchDataImplementation(initService);
|
||||||
|
testDeleteDataImplementation(initService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when updating the bitstream\'s format', () => {
|
describe('when updating the bitstream\'s format', () => {
|
||||||
|
@@ -1,12 +1,9 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
@@ -15,8 +12,6 @@ import { Bundle } from '../shared/bundle.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 { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
|
||||||
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PutRequest } from './request.models';
|
import { PutRequest } from './request.models';
|
||||||
@@ -28,36 +23,45 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { sendRequest } from '../shared/request.operators';
|
import { sendRequest } from '../shared/request.operators';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { SearchData, SearchDataImpl } from './base/search-data';
|
||||||
|
import { PatchData, PatchDataImpl } from './base/patch-data';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { DeleteData, DeleteDataImpl } from './base/delete-data';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Bitstream}s from the REST API
|
* A service to retrieve {@link Bitstream}s from the REST API
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@dataService(BITSTREAM)
|
@dataService(BITSTREAM)
|
||||||
export class BitstreamDataService extends DataService<Bitstream> {
|
export class BitstreamDataService extends IdentifiableDataService<Bitstream> implements SearchData<Bitstream>, PatchData<Bitstream>, DeleteData<Bitstream> {
|
||||||
|
private searchData: SearchDataImpl<Bitstream>;
|
||||||
/**
|
private patchData: PatchDataImpl<Bitstream>;
|
||||||
* The HAL path to the bitstream endpoint
|
private deleteData: DeleteDataImpl<Bitstream>;
|
||||||
*/
|
|
||||||
protected linkPath = 'bitstreams';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DSOChangeAnalyzer<Bitstream>,
|
|
||||||
protected bundleService: BundleDataService,
|
protected bundleService: BundleDataService,
|
||||||
protected bitstreamFormatService: BitstreamFormatDataService
|
protected bitstreamFormatService: BitstreamFormatDataService,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Bitstream>,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
) {
|
) {
|
||||||
super();
|
super('bitstreams', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.patchData = new PatchDataImpl<Bitstream>(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +77,7 @@ export class BitstreamDataService extends DataService<Bitstream> {
|
|||||||
* {@link HALLink}s should be automatically resolved
|
* {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
findAllByBundle(bundle: Bundle, options?: FindListOptions, useCachedVersionIfAvailable = true, 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, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findListByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,8 +184,97 @@ export class BitstreamDataService extends DataService<Bitstream> {
|
|||||||
hrefObs,
|
hrefObs,
|
||||||
useCachedVersionIfAvailable,
|
useCachedVersionIfAvailable,
|
||||||
reRequestOnStale,
|
reRequestOnStale,
|
||||||
...linksToFollow
|
...linksToFollow,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF for a specific object's search method with given options object
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<string> {
|
||||||
|
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
public commitUpdates(method?: RestRequestMethod): void {
|
||||||
|
this.patchData.commitUpdates(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a patch request for a specified object
|
||||||
|
* @param {T} object The object to send a patch request for
|
||||||
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
|
*/
|
||||||
|
public patch(object: Bitstream, operations: []): Observable<RemoteData<Bitstream>> {
|
||||||
|
return this.patchData.patch(object, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
|
* @param {DSpaceObject} object The given object
|
||||||
|
*/
|
||||||
|
public update(object: Bitstream): Observable<RemoteData<Bitstream>> {
|
||||||
|
return this.patchData.update(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of operations representing the difference between an object and its latest value in the cache.
|
||||||
|
* @param object the object to resolve to a list of patch operations
|
||||||
|
*/
|
||||||
|
public createPatchFromCache(object: Bitstream): Observable<Operation[]> {
|
||||||
|
return this.patchData.createPatchFromCache(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param objectId The id of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
*/
|
||||||
|
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param href The self link of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
* Only emits once all request related to the DSO has been invalidated.
|
||||||
|
*/
|
||||||
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,19 +6,16 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
|||||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||||
import { waitForAsync } from '@angular/core/testing';
|
import { waitForAsync } from '@angular/core/testing';
|
||||||
import {
|
import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
|
||||||
BitstreamFormatsRegistryDeselectAction,
|
|
||||||
BitstreamFormatsRegistryDeselectAllAction,
|
|
||||||
BitstreamFormatsRegistrySelectAction
|
|
||||||
} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
|
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { RequestEntry } from './request-entry.model';
|
import { RequestEntry } from './request-entry.model';
|
||||||
|
import { testFindAllDataImplementation } from './base/find-all-data.spec';
|
||||||
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
|
||||||
describe('BitstreamFormatDataService', () => {
|
describe('BitstreamFormatDataService', () => {
|
||||||
let service: BitstreamFormatDataService;
|
let service: BitstreamFormatDataService;
|
||||||
@@ -50,8 +47,6 @@ describe('BitstreamFormatDataService', () => {
|
|||||||
} as HALEndpointService;
|
} as HALEndpointService;
|
||||||
|
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
|
||||||
const comparator = {} as any;
|
|
||||||
|
|
||||||
let rd;
|
let rd;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
@@ -65,15 +60,19 @@ describe('BitstreamFormatDataService', () => {
|
|||||||
return new BitstreamFormatDataService(
|
return new BitstreamFormatDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
store,
|
|
||||||
objectCache,
|
objectCache,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
http,
|
store,
|
||||||
comparator
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new BitstreamFormatDataService(null, null, null, null, null, null);
|
||||||
|
testFindAllDataImplementation(initService);
|
||||||
|
testDeleteDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getBrowseEndpoint', () => {
|
describe('getBrowseEndpoint', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
|
@@ -1,16 +1,10 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
|
||||||
import {
|
import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
|
||||||
BitstreamFormatsRegistryDeselectAction,
|
|
||||||
BitstreamFormatsRegistryDeselectAllAction,
|
|
||||||
BitstreamFormatsRegistrySelectAction
|
|
||||||
} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
|
|
||||||
import { BitstreamFormatRegistryState } from '../../admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
import { BitstreamFormatRegistryState } from '../../admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
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';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
@@ -18,40 +12,53 @@ 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 { DataService } from './data.service';
|
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PostRequest, PutRequest } from './request.models';
|
import { PostRequest, PutRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { sendRequest } from '../shared/request.operators';
|
import { sendRequest } from '../shared/request.operators';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { DeleteData, DeleteDataImpl } from './base/delete-data';
|
||||||
|
import { FindAllData, FindAllDataImpl } from './base/find-all-data';
|
||||||
|
import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model';
|
||||||
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { PaginatedList } from './paginated-list.model';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
|
||||||
const bitstreamFormatsStateSelector = createSelector(
|
const bitstreamFormatsStateSelector = createSelector(
|
||||||
coreSelector,
|
coreSelector,
|
||||||
(state: CoreState) => state.bitstreamFormats
|
(state: CoreState) => state.bitstreamFormats,
|
||||||
|
);
|
||||||
|
const selectedBitstreamFormatSelector = createSelector(
|
||||||
|
bitstreamFormatsStateSelector,
|
||||||
|
(bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats,
|
||||||
);
|
);
|
||||||
const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector,
|
|
||||||
(bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint
|
* A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(BITSTREAM_FORMAT)
|
@dataService(BITSTREAM_FORMAT)
|
||||||
export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
export class BitstreamFormatDataService extends IdentifiableDataService<BitstreamFormat> implements FindAllData<BitstreamFormat>, DeleteData<BitstreamFormat> {
|
||||||
|
|
||||||
protected linkPath = 'bitstreamformats';
|
protected linkPath = 'bitstreamformats';
|
||||||
|
|
||||||
|
private findAllData: FindAllDataImpl<BitstreamFormat>;
|
||||||
|
private deleteData: DeleteDataImpl<BitstreamFormat>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected store: Store<CoreState>,
|
||||||
protected comparator: DefaultChangeAnalyzer<BitstreamFormat>) {
|
) {
|
||||||
super();
|
super('bitstreamformats', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +67,7 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
|||||||
*/
|
*/
|
||||||
public getUpdateEndpoint(formatId: string): Observable<string> {
|
public getUpdateEndpoint(formatId: string): Observable<string> {
|
||||||
return this.getBrowseEndpoint().pipe(
|
return this.getBrowseEndpoint().pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, formatId))
|
map((endpoint: string) => this.getIDHref(endpoint, formatId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,4 +154,47 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
|||||||
findByBitstream(bitstream: Bitstream): Observable<RemoteData<BitstreamFormat>> {
|
findByBitstream(bitstream: Bitstream): Observable<RemoteData<BitstreamFormat>> {
|
||||||
return this.findByHref(bitstream._links.format.href);
|
return this.findByHref(bitstream._links.format.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<BitstreamFormat>[]): Observable<RemoteData<PaginatedList<BitstreamFormat>>> {
|
||||||
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param objectId The id of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
*/
|
||||||
|
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param href The self link of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
* Only emits once all request related to the DSO has been invalidated.
|
||||||
|
*/
|
||||||
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
|
|||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { Bundle } from '../shared/bundle.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
|
|
||||||
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
||||||
diff(object1: Item, object2: Item): Operation[] {
|
diff(object1: Item, object2: Item): Operation[] {
|
||||||
@@ -64,9 +65,6 @@ describe('BundleDataService', () => {
|
|||||||
store,
|
store,
|
||||||
objectCache,
|
objectCache,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
|
||||||
http,
|
|
||||||
comparator,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,14 +72,20 @@ describe('BundleDataService', () => {
|
|||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new BundleDataService(null, null, null, null, null);
|
||||||
|
|
||||||
|
testPatchDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
describe('findAllByItem', () => {
|
describe('findAllByItem', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(service, 'findAllByHref');
|
spyOn(service, 'findListByHref');
|
||||||
service.findAllByItem(item);
|
service.findAllByItem(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call findAllByHref with the item\'s bundles link', () => {
|
it('should call findListByHref with the item\'s bundles link', () => {
|
||||||
expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined, true, true);
|
expect(service.findListByHref).toHaveBeenCalledWith(bundleLink, undefined, true, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,20 +1,14 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Bundle } from '../shared/bundle.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
import { BUNDLE } from '../shared/bundle.resource-type';
|
import { BUNDLE } from '../shared/bundle.resource-type';
|
||||||
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 { DataService } from './data.service';
|
|
||||||
import { DefaultChangeAnalyzer } from './default-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 { GetRequest } from './request.models';
|
import { GetRequest } from './request.models';
|
||||||
@@ -22,30 +16,36 @@ import { RequestService } from './request.service';
|
|||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
import { RequestEntryState } from './request-entry-state.model';
|
import { RequestEntryState } from './request-entry-state.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { PatchData, PatchDataImpl } from './base/patch-data';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Bundle}s from the REST API
|
* A service to retrieve {@link Bundle}s from the REST API
|
||||||
*/
|
*/
|
||||||
@Injectable(
|
@Injectable(
|
||||||
{providedIn: 'root'}
|
{ providedIn: 'root' },
|
||||||
)
|
)
|
||||||
@dataService(BUNDLE)
|
@dataService(BUNDLE)
|
||||||
export class BundleDataService extends DataService<Bundle> {
|
export class BundleDataService extends IdentifiableDataService<Bundle> implements PatchData<Bundle> {
|
||||||
protected linkPath = 'bundles';
|
private bitstreamsEndpoint = 'bitstreams';
|
||||||
protected bitstreamsEndpoint = 'bitstreams';
|
|
||||||
|
private patchData: PatchDataImpl<Bundle>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected comparator: DSOChangeAnalyzer<Bundle>,
|
||||||
protected http: HttpClient,
|
) {
|
||||||
protected comparator: DefaultChangeAnalyzer<Bundle>) {
|
super('bundles', requestService, rdbService, objectCache, halService);
|
||||||
super();
|
|
||||||
|
this.patchData = new PatchDataImpl<Bundle>(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +61,7 @@ export class BundleDataService extends DataService<Bundle> {
|
|||||||
* {@link HALLink}s should be automatically resolved
|
* {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
findAllByItem(item: Item, options?: FindListOptions, useCachedVersionIfAvailable = true, 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, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findListByHref(item._links.bundles.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,7 +133,7 @@ export class BundleDataService extends DataService<Bundle> {
|
|||||||
const hrefObs = this.getBitstreamsEndpoint(bundleId, searchOptions);
|
const hrefObs = this.getBitstreamsEndpoint(bundleId, searchOptions);
|
||||||
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
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.send(request, true);
|
this.requestService.send(request, true);
|
||||||
@@ -141,4 +141,38 @@ export class BundleDataService extends DataService<Bundle> {
|
|||||||
|
|
||||||
return this.rdbService.buildList<Bitstream>(hrefObs, ...linksToFollow);
|
return this.rdbService.buildList<Bitstream>(hrefObs, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
public commitUpdates(method?: RestRequestMethod): void {
|
||||||
|
this.patchData.commitUpdates(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a patch request for a specified object
|
||||||
|
* @param {T} object The object to send a patch request for
|
||||||
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
|
*/
|
||||||
|
public patch(object: Bundle, operations: Operation[]): Observable<RemoteData<Bundle>> {
|
||||||
|
return this.patchData.patch(object, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
|
* @param {DSpaceObject} object The given object
|
||||||
|
*/
|
||||||
|
public update(object: Bundle): Observable<RemoteData<Bundle>> {
|
||||||
|
return this.patchData.update(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of operations representing the difference between an object and its latest value in the cache.
|
||||||
|
* @param object the object to resolve to a list of patch operations
|
||||||
|
*/
|
||||||
|
public createPatchFromCache(object: Bundle): Observable<Operation[]> {
|
||||||
|
return this.patchData.createPatchFromCache(object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,16 +13,17 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
|||||||
import { Collection } from '../shared/collection.model';
|
import { Collection } from '../shared/collection.model';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { buildPaginatedList } from './paginated-list.model';
|
import { buildPaginatedList } from './paginated-list.model';
|
||||||
import {
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
createFailedRemoteDataObject$,
|
|
||||||
createSuccessfulRemoteDataObject,
|
|
||||||
createSuccessfulRemoteDataObject$
|
|
||||||
} from '../../shared/remote-data.utils';
|
|
||||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
import { hasNoValue } from '../../shared/empty.util';
|
||||||
|
import { testCreateDataImplementation } from './base/create-data.spec';
|
||||||
|
import { testFindAllDataImplementation } from './base/find-all-data.spec';
|
||||||
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
|
||||||
const url = 'fake-url';
|
const url = 'fake-url';
|
||||||
const collectionId = 'fake-collection-id';
|
const collectionId = 'fake-collection-id';
|
||||||
@@ -75,6 +76,16 @@ describe('CollectionDataService', () => {
|
|||||||
const paginatedList = buildPaginatedList(pageInfo, array);
|
const paginatedList = buildPaginatedList(pageInfo, array);
|
||||||
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new CollectionDataService(null, null, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
testCreateDataImplementation(initService);
|
||||||
|
testFindAllDataImplementation(initService);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
testPatchDataImplementation(initService);
|
||||||
|
testDeleteDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
describe('when the requests are successful', () => {
|
describe('when the requests are successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createService();
|
createService();
|
||||||
@@ -201,7 +212,7 @@ describe('CollectionDataService', () => {
|
|||||||
notificationsService = new NotificationsServiceStub();
|
notificationsService = new NotificationsServiceStub();
|
||||||
translate = getMockTranslateService();
|
translate = getMockTranslateService();
|
||||||
|
|
||||||
service = new CollectionDataService(requestService, rdbService, null, null, objectCache, halService, notificationsService, null, null, null, translate);
|
service = new CollectionDataService(requestService, rdbService, objectCache, halService, null, notificationsService, null, null, translate);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
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 { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
@@ -9,7 +8,6 @@ import { NotificationOptions } from '../../shared/notifications/models/notificat
|
|||||||
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 { 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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
@@ -33,30 +31,28 @@ import {
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { BitstreamDataService } from './bitstream-data.service';
|
import { BitstreamDataService } from './bitstream-data.service';
|
||||||
import { RestRequest } from './rest-request.model';
|
import { RestRequest } from './rest-request.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { Community } from '../shared/community.model';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(COLLECTION)
|
@dataService(COLLECTION)
|
||||||
export class CollectionDataService extends ComColDataService<Collection> {
|
export class CollectionDataService extends ComColDataService<Collection> {
|
||||||
protected linkPath = 'collections';
|
|
||||||
protected errorTitle = 'collection.source.update.notifications.error.title';
|
protected errorTitle = 'collection.source.update.notifications.error.title';
|
||||||
protected contentSourceError = 'collection.source.update.notifications.error.content';
|
protected contentSourceError = 'collection.source.update.notifications.error.content';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected cds: CommunityDataService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Community>,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
protected comparator: DSOChangeAnalyzer<Collection>,
|
protected communityDataService: CommunityDataService,
|
||||||
protected translate: TranslateService
|
protected translate: TranslateService,
|
||||||
) {
|
) {
|
||||||
super();
|
super('collections', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,15 +280,15 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
* @param findListOptions Pagination and search options.
|
* @param findListOptions Pagination and search options.
|
||||||
*/
|
*/
|
||||||
findMappedCollectionsFor(item: Item, findListOptions?: FindListOptions): Observable<RemoteData<PaginatedList<Collection>>> {
|
findMappedCollectionsFor(item: Item, findListOptions?: FindListOptions): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
return this.findAllByHref(item._links.mappedCollections.href, findListOptions);
|
return this.findListByHref(item._links.mappedCollections.href, findListOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected getScopeCommunityHref(options: FindListOptions) {
|
protected getScopeCommunityHref(options: FindListOptions) {
|
||||||
return this.cds.getEndpoint().pipe(
|
return this.communityDataService.getEndpoint().pipe(
|
||||||
map((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)),
|
map((endpoint: string) => this.communityDataService.getIDHref(endpoint, options.scopeID)),
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
take(1)
|
take(1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,16 +13,16 @@ 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 { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import {
|
import { createFailedRemoteDataObject, createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
createFailedRemoteDataObject$,
|
|
||||||
createSuccessfulRemoteDataObject$,
|
|
||||||
createFailedRemoteDataObject,
|
|
||||||
createSuccessfulRemoteDataObject
|
|
||||||
} from '../../shared/remote-data.utils';
|
|
||||||
import { BitstreamDataService } from './bitstream-data.service';
|
import { BitstreamDataService } from './bitstream-data.service';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
|
import { testCreateDataImplementation } from './base/create-data.spec';
|
||||||
|
import { testFindAllDataImplementation } from './base/find-all-data.spec';
|
||||||
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class TestService extends ComColDataService<any> {
|
|||||||
protected comparator: DSOChangeAnalyzer<Community>,
|
protected comparator: DSOChangeAnalyzer<Community>,
|
||||||
protected linkPath: string
|
protected linkPath: string
|
||||||
) {
|
) {
|
||||||
super();
|
super('something', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getFindByParentHref(parentUUID: string): Observable<string> {
|
protected getFindByParentHref(parentUUID: string): Observable<string> {
|
||||||
@@ -154,6 +154,15 @@ describe('ComColDataService', () => {
|
|||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new TestService(null, null, null, null, null, null, null, null, null, null, null);
|
||||||
|
testCreateDataImplementation(initService);
|
||||||
|
testFindAllDataImplementation(initService);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
testPatchDataImplementation(initService);
|
||||||
|
testDeleteDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getBrowseEndpoint', () => {
|
describe('getBrowseEndpoint', () => {
|
||||||
it(`should call createAndSendGetRequest with the scope Community's self link`, () => {
|
it(`should call createAndSendGetRequest with the scope Community's self link`, () => {
|
||||||
testScheduler.run(({ cold, flush, expectObservable }) => {
|
testScheduler.run(({ cold, flush, expectObservable }) => {
|
||||||
|
@@ -4,7 +4,6 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Community } from '../shared/community.model';
|
import { Community } from '../shared/community.model';
|
||||||
import { HALLink } from '../shared/hal-link.model';
|
import { HALLink } from '../shared/hal-link.model';
|
||||||
import { DataService } from './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 { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
@@ -17,11 +16,45 @@ import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { PatchData, PatchDataImpl } from './base/patch-data';
|
||||||
|
import { DeleteData, DeleteDataImpl } from './base/delete-data';
|
||||||
|
import { FindAllData, FindAllDataImpl } from './base/find-all-data';
|
||||||
|
import { SearchData, SearchDataImpl } from './base/search-data';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { CreateData, CreateDataImpl } from './base/create-data';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
export abstract class ComColDataService<T extends Community | Collection> extends DataService<T> {
|
export abstract class ComColDataService<T extends Community | Collection> extends IdentifiableDataService<T> implements CreateData<T>, FindAllData<T>, SearchData<T>, PatchData<T>, DeleteData<T> {
|
||||||
protected abstract objectCache: ObjectCacheService;
|
private createData: CreateData<T>;
|
||||||
protected abstract halService: HALEndpointService;
|
private findAllData: FindAllData<T>;
|
||||||
protected abstract bitstreamDataService: BitstreamDataService;
|
private searchData: SearchData<T>;
|
||||||
|
private patchData: PatchData<T>;
|
||||||
|
private deleteData: DeleteData<T>;
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected comparator: DSOChangeAnalyzer<T>,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
|
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
|
||||||
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.searchData = new SearchDataImpl<T>(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.patchData = new PatchDataImpl<T>(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the scoped endpoint URL by fetching the object with
|
* Get the scoped endpoint URL by fetching the object with
|
||||||
@@ -66,7 +99,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.findAllByHref(href$, options, true, true, ...linksToFollow);
|
return this.findListByHref(href$, options, true, true, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,4 +162,110 @@ export abstract class ComColDataService<T extends Community | Collection> extend
|
|||||||
const parentCommunity = dso._links.parentCommunity;
|
const parentCommunity = dso._links.parentCommunity;
|
||||||
return isNotEmpty(parentCommunity) ? parentCommunity.href : null;
|
return isNotEmpty(parentCommunity) ? parentCommunity.href : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new object on the server, and store the response in the object cache
|
||||||
|
*
|
||||||
|
* @param object The object to create
|
||||||
|
* @param params Array with additional params to combine with query string
|
||||||
|
*/
|
||||||
|
create(object: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
||||||
|
return this.createData.create(object, ...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
public commitUpdates(method?: RestRequestMethod): void {
|
||||||
|
this.patchData.commitUpdates(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a patch request for a specified object
|
||||||
|
* @param {T} object The object to send a patch request for
|
||||||
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
|
*/
|
||||||
|
public patch(object: T, operations: []): Observable<RemoteData<T>> {
|
||||||
|
return this.patchData.patch(object, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
|
* @param {DSpaceObject} object The given object
|
||||||
|
*/
|
||||||
|
public update(object: T): Observable<RemoteData<T>> {
|
||||||
|
return this.patchData.update(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of operations representing the difference between an object and its latest value in the cache.
|
||||||
|
* @param object the object to resolve to a list of patch operations
|
||||||
|
*/
|
||||||
|
public createPatchFromCache(object: T): Observable<Operation[]> {
|
||||||
|
return this.patchData.createPatchFromCache(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param objectId The id of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
*/
|
||||||
|
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param href The self link of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
* Only emits once all request related to the DSO has been invalidated.
|
||||||
|
*/
|
||||||
|
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,8 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
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';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Community } from '../shared/community.model';
|
import { Community } from '../shared/community.model';
|
||||||
@@ -19,36 +16,36 @@ import { RequestService } from './request.service';
|
|||||||
import { BitstreamDataService } from './bitstream-data.service';
|
import { BitstreamDataService } from './bitstream-data.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(COMMUNITY)
|
@dataService(COMMUNITY)
|
||||||
export class CommunityDataService extends ComColDataService<Community> {
|
export class CommunityDataService extends ComColDataService<Community> {
|
||||||
protected linkPath = 'communities';
|
|
||||||
protected topLinkPath = 'search/top';
|
protected topLinkPath = 'search/top';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Community>,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DSOChangeAnalyzer<Community>
|
|
||||||
) {
|
) {
|
||||||
super();
|
super('communities', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this method is overridden in order to make it public
|
||||||
getEndpoint() {
|
getEndpoint() {
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
findTop(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<Community>[]): Observable<RemoteData<PaginatedList<Community>>> {
|
findTop(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<Community>[]): Observable<RemoteData<PaginatedList<Community>>> {
|
||||||
const hrefObs = this.getFindAllHref(options, this.topLinkPath);
|
return this.getEndpoint().pipe(
|
||||||
return this.findAllByHref(hrefObs, undefined, true, true, ...linksToFollow);
|
map(href => `${href}/search/top`),
|
||||||
|
switchMap(href => this.findListByHref(href, options, true, true, ...linksToFollow))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getFindByParentHref(parentUUID: string): Observable<string> {
|
protected getFindByParentHref(parentUUID: string): Observable<string> {
|
||||||
|
@@ -5,8 +5,6 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { GetRequest } 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 { HttpClient } from '@angular/common/http';
|
|
||||||
import { ConfigurationDataService } from './configuration-data.service';
|
import { ConfigurationDataService } from './configuration-data.service';
|
||||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
|
||||||
@@ -44,18 +42,12 @@ describe('ConfigurationDataService', () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = {} as ObjectCacheService;
|
||||||
const notificationsService = {} as NotificationsService;
|
|
||||||
const http = {} as HttpClient;
|
|
||||||
const comparator = {} as any;
|
|
||||||
|
|
||||||
service = new ConfigurationDataService(
|
service = new ConfigurationDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
|
||||||
http,
|
|
||||||
comparator
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,55 +1,30 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
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';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
|
||||||
import { CONFIG_PROPERTY } from '../shared/config-property.resource-type';
|
import { CONFIG_PROPERTY } from '../shared/config-property.resource-type';
|
||||||
import { CoreState } from '../core-state.model';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
class DataServiceImpl extends DataService<ConfigurationProperty> {
|
|
||||||
protected linkPath = 'properties';
|
|
||||||
|
|
||||||
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<ConfigurationProperty>) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(CONFIG_PROPERTY)
|
@dataService(CONFIG_PROPERTY)
|
||||||
/**
|
/**
|
||||||
* Data Service responsible for retrieving Configuration properties
|
* Data Service responsible for retrieving Configuration properties
|
||||||
*/
|
*/
|
||||||
export class ConfigurationDataService {
|
export class ConfigurationDataService extends IdentifiableDataService<ConfigurationProperty> {
|
||||||
protected linkPath = 'properties';
|
|
||||||
private dataService: DataServiceImpl;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
) {
|
||||||
protected http: HttpClient,
|
super('properties', requestService, rdbService, objectCache, halService);
|
||||||
protected comparator: DefaultChangeAnalyzer<ConfigurationProperty>) {
|
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,6 +32,6 @@ export class ConfigurationDataService {
|
|||||||
* @param name
|
* @param name
|
||||||
*/
|
*/
|
||||||
findByPropertyName(name: string): Observable<RemoteData<ConfigurationProperty>> {
|
findByPropertyName(name: string): Observable<RemoteData<ConfigurationProperty>> {
|
||||||
return this.dataService.findById(name);
|
return this.findById(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,693 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { Operation } from 'fast-json-patch';
|
|
||||||
import { AsyncSubject, combineLatest, from as observableFrom, Observable, of as observableOf } from 'rxjs';
|
|
||||||
import {
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
find,
|
|
||||||
map,
|
|
||||||
mergeMap,
|
|
||||||
skipWhile,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
takeWhile,
|
|
||||||
tap,
|
|
||||||
toArray
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
|
||||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
|
||||||
import { getClassForType } from '../cache/builders/build-decorators';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
|
||||||
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
|
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
|
||||||
import { PaginatedList } from './paginated-list.model';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import { CreateRequest, DeleteRequest, GetRequest, PatchRequest, PutRequest } from './request.models';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
|
||||||
import { UpdateDataService } from './update-data.service';
|
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
|
||||||
import { CacheableObject } from '../cache/cacheable-object.model';
|
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
|
||||||
|
|
||||||
export abstract class DataService<T extends CacheableObject> implements UpdateDataService<T> {
|
|
||||||
protected abstract requestService: RequestService;
|
|
||||||
protected abstract rdbService: RemoteDataBuildService;
|
|
||||||
protected abstract store: Store<CoreState>;
|
|
||||||
protected abstract linkPath: string;
|
|
||||||
protected abstract halService: HALEndpointService;
|
|
||||||
protected abstract objectCache: ObjectCacheService;
|
|
||||||
protected abstract notificationsService: NotificationsService;
|
|
||||||
protected abstract http: HttpClient;
|
|
||||||
protected abstract comparator: ChangeAnalyzer<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows subclasses to reset the response cache time.
|
|
||||||
*/
|
|
||||||
protected responseMsToLive: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the endpoint for browsing
|
|
||||||
* @param options The [[FindListOptions]] object
|
|
||||||
* @param linkPath The link path for the object
|
|
||||||
* @returns {Observable<string>}
|
|
||||||
*/
|
|
||||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
|
||||||
return this.getEndpoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the base endpoint for all requests
|
|
||||||
*/
|
|
||||||
protected getEndpoint(): Observable<string> {
|
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the HREF with given options object
|
|
||||||
*
|
|
||||||
* @param options The [[FindListOptions]] object
|
|
||||||
* @param linkPath The link path for the object
|
|
||||||
* @return {Observable<string>}
|
|
||||||
* Return an observable that emits created HREF
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
*/
|
|
||||||
public getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
|
||||||
let endpoint$: Observable<string>;
|
|
||||||
const args = [];
|
|
||||||
|
|
||||||
endpoint$ = this.getBrowseEndpoint(options).pipe(
|
|
||||||
filter((href: string) => isNotEmpty(href)),
|
|
||||||
map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href),
|
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
|
|
||||||
return endpoint$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the HREF for a specific object's search method with given options object
|
|
||||||
*
|
|
||||||
* @param searchMethod The search method for the object
|
|
||||||
* @param options The [[FindListOptions]] object
|
|
||||||
* @return {Observable<string>}
|
|
||||||
* Return an observable that emits created HREF
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
*/
|
|
||||||
public getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
|
||||||
let result$: Observable<string>;
|
|
||||||
const args = [];
|
|
||||||
|
|
||||||
result$ = this.getSearchEndpoint(searchMethod);
|
|
||||||
|
|
||||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn an options object into a query string and combine it with the given HREF
|
|
||||||
*
|
|
||||||
* @param href The HREF to which the query string should be appended
|
|
||||||
* @param options The [[FindListOptions]] object
|
|
||||||
* @param extraArgs Array with additional params to combine with query string
|
|
||||||
* @return {Observable<string>}
|
|
||||||
* Return an observable that emits created HREF
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
*/
|
|
||||||
public buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: FollowLinkConfig<T>[]): string {
|
|
||||||
let args = [...extraArgs];
|
|
||||||
|
|
||||||
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 */
|
|
||||||
args = this.addHrefArg(href, args, `page=${options.currentPage - 1}`);
|
|
||||||
}
|
|
||||||
if (hasValue(options.elementsPerPage)) {
|
|
||||||
args = this.addHrefArg(href, args, `size=${options.elementsPerPage}`);
|
|
||||||
}
|
|
||||||
if (hasValue(options.sort)) {
|
|
||||||
args = this.addHrefArg(href, args, `sort=${options.sort.field},${options.sort.direction}`);
|
|
||||||
}
|
|
||||||
if (hasValue(options.startsWith)) {
|
|
||||||
args = this.addHrefArg(href, args, `startsWith=${options.startsWith}`);
|
|
||||||
}
|
|
||||||
if (hasValue(options.searchParams)) {
|
|
||||||
options.searchParams.forEach((param: RequestParam) => {
|
|
||||||
args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
args = this.addEmbedParams(href, args, ...linksToFollow);
|
|
||||||
if (isNotEmpty(args)) {
|
|
||||||
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
|
||||||
} else {
|
|
||||||
return href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn an array of RequestParam into a query string and combine it with the given HREF
|
|
||||||
*
|
|
||||||
* @param href The HREF to which the query string should be appended
|
|
||||||
* @param params Array with additional params to combine with query string
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
*
|
|
||||||
* @return {Observable<string>}
|
|
||||||
* Return an observable that emits created HREF
|
|
||||||
*/
|
|
||||||
buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: FollowLinkConfig<T>[]): string {
|
|
||||||
|
|
||||||
let args = [];
|
|
||||||
if (hasValue(params)) {
|
|
||||||
params.forEach((param: RequestParam) => {
|
|
||||||
args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
args = this.addEmbedParams(href, args, ...linksToFollow);
|
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
|
||||||
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
|
||||||
} else {
|
|
||||||
return href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 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 linksToFollow links we want to embed in query string if shouldEmbed is true
|
|
||||||
*/
|
|
||||||
protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]) {
|
|
||||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
|
||||||
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
|
|
||||||
const embedString = 'embed=' + String(linkToFollow.name);
|
|
||||||
// Add the embeds size if given in the FollowLinkConfig.FindListOptions
|
|
||||||
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
|
|
||||||
args = this.addHrefArg(href, args,
|
|
||||||
'embed.size=' + String(linkToFollow.name) + '=' + linkToFollow.findListOptions.elementsPerPage);
|
|
||||||
}
|
|
||||||
// Adds the nested embeds and their size if given
|
|
||||||
if (isNotEmpty(linkToFollow.linksToFollow)) {
|
|
||||||
args = this.addNestedEmbeds(embedString, href, args, ...linkToFollow.linksToFollow);
|
|
||||||
} else {
|
|
||||||
args = this.addHrefArg(href, args, embedString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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, separated by a /, and their sizes, recursively
|
|
||||||
* @param embedString embedString so far (recursive)
|
|
||||||
* @param href The href the params are to be added to
|
|
||||||
* @param args params for the query string
|
|
||||||
* @param linksToFollow links we want to embed in query string if shouldEmbed is true
|
|
||||||
*/
|
|
||||||
protected addNestedEmbeds(embedString: string, href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]): string[] {
|
|
||||||
let nestEmbed = embedString;
|
|
||||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
|
||||||
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
|
|
||||||
nestEmbed = nestEmbed + '/' + String(linkToFollow.name);
|
|
||||||
// Add the nested embeds size if given in the FollowLinkConfig.FindListOptions
|
|
||||||
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
|
|
||||||
const nestedEmbedSize = 'embed.size=' + nestEmbed.split('=')[1] + '=' + linkToFollow.findListOptions.elementsPerPage;
|
|
||||||
args = this.addHrefArg(href, args, nestedEmbedSize);
|
|
||||||
}
|
|
||||||
if (hasValue(linkToFollow.linksToFollow) && isNotEmpty(linkToFollow.linksToFollow)) {
|
|
||||||
args = this.addNestedEmbeds(nestEmbed, href, args, ...linkToFollow.linksToFollow);
|
|
||||||
} else {
|
|
||||||
args = this.addHrefArg(href, args, nestEmbed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
|
||||||
* info should be added to the objects
|
|
||||||
*
|
|
||||||
* @param options 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
|
|
||||||
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
|
||||||
* Return an observable that emits object list
|
|
||||||
*/
|
|
||||||
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
|
||||||
return this.findAllByHref(this.getFindAllHref(options), options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the HREF for a specific object based on its identifier; with possible embed query params based on linksToFollow
|
|
||||||
* @param endpoint The base endpoint for the type of object
|
|
||||||
* @param resourceID The identifier for the object
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
*/
|
|
||||||
getIDHref(endpoint, resourceID, ...linksToFollow: FollowLinkConfig<T>[]): string {
|
|
||||||
return this.buildHrefFromFindOptions(endpoint + '/' + resourceID, {}, [], ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an observable for the HREF of a specific object based on its identifier
|
|
||||||
* @param resourceID The identifier for the object
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
*/
|
|
||||||
getIDHrefObs(resourceID: string, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
|
||||||
return this.getEndpoint().pipe(
|
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @param id ID 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
|
|
||||||
*/
|
|
||||||
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
|
||||||
const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow);
|
|
||||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An operator that will call the given function if the incoming RemoteData is stale and
|
|
||||||
* shouldReRequest is true
|
|
||||||
*
|
|
||||||
* @param shouldReRequest Whether or not to call the re-request function if the RemoteData is stale
|
|
||||||
* @param requestFn The function to call if the RemoteData is stale and shouldReRequest is
|
|
||||||
* true
|
|
||||||
*/
|
|
||||||
protected reRequestStaleRemoteData<O>(shouldReRequest: boolean, requestFn: () => Observable<RemoteData<O>>) {
|
|
||||||
return (source: Observable<RemoteData<O>>): Observable<RemoteData<O>> => {
|
|
||||||
if (shouldReRequest === true) {
|
|
||||||
return source.pipe(
|
|
||||||
tap((remoteData: RemoteData<O>) => {
|
|
||||||
if (hasValue(remoteData) && remoteData.isStale) {
|
|
||||||
requestFn();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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. 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
|
|
||||||
* @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 | Observable<string>, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
|
||||||
if (typeof href$ === 'string') {
|
|
||||||
href$ = observableOf(href$);
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestHref$ = href$.pipe(
|
|
||||||
isNotEmptyOperator(),
|
|
||||||
take(1),
|
|
||||||
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
|
||||||
|
|
||||||
return this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
|
||||||
// This skip ensures that if a stale object is present in the cache when you do a
|
|
||||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
|
||||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
|
||||||
// cached completed object
|
|
||||||
skipWhile((rd: RemoteData<T>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
|
||||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
|
||||||
this.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. Can be a string or
|
|
||||||
* an Observable<string>
|
|
||||||
* @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(href$: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
|
||||||
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(
|
|
||||||
// This skip ensures that if a stale object is present in the cache when you do a
|
|
||||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
|
||||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
|
||||||
// cached completed object
|
|
||||||
skipWhile((rd: RemoteData<PaginatedList<T>>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
|
||||||
this.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 request = new GetRequest(requestId, href);
|
|
||||||
if (hasValue(this.responseMsToLive)) {
|
|
||||||
request.responseMsToLive = this.responseMsToLive;
|
|
||||||
}
|
|
||||||
this.requestService.send(request, useCachedVersionIfAvailable);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return object search endpoint by given search method
|
|
||||||
*
|
|
||||||
* @param searchMethod The search method for the object
|
|
||||||
*/
|
|
||||||
protected getSearchEndpoint(searchMethod: string): Observable<string> {
|
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
|
||||||
filter((href: string) => isNotEmpty(href)),
|
|
||||||
map((href: string) => `${href}/search/${searchMethod}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a new FindListRequest with given search method
|
|
||||||
*
|
|
||||||
* @param searchMethod The search method for the object
|
|
||||||
* @param options The [[FindListOptions]] 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
|
|
||||||
* @return {Observable<RemoteData<PaginatedList<T>>}
|
|
||||||
* Return an observable that emits response from the server
|
|
||||||
*/
|
|
||||||
searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
|
||||||
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
|
||||||
|
|
||||||
return this.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a patch request for a specified object
|
|
||||||
* @param {T} object The object to send a patch request for
|
|
||||||
* @param {Operation[]} operations The patch operations to be performed
|
|
||||||
*/
|
|
||||||
patch(object: T, operations: Operation[]): Observable<RemoteData<T>> {
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, object.uuid)));
|
|
||||||
|
|
||||||
hrefObs.pipe(
|
|
||||||
find((href: string) => hasValue(href)),
|
|
||||||
).subscribe((href: string) => {
|
|
||||||
const request = new PatchRequest(requestId, href, operations);
|
|
||||||
if (hasValue(this.responseMsToLive)) {
|
|
||||||
request.responseMsToLive = this.responseMsToLive;
|
|
||||||
}
|
|
||||||
this.requestService.send(request);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
createPatchFromCache(object: T): Observable<Operation[]> {
|
|
||||||
const oldVersion$ = this.findByHref(object._links.self.href, true, false);
|
|
||||||
return oldVersion$.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
map((oldVersion: T) => this.comparator.diff(oldVersion, object)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a PUT request for the specified object
|
|
||||||
*
|
|
||||||
* @param object The object to send a put request for.
|
|
||||||
*/
|
|
||||||
put(object: T): Observable<RemoteData<T>> {
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
|
||||||
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
|
||||||
const request = new PutRequest(requestId, object._links.self.href, serializedObject);
|
|
||||||
|
|
||||||
if (hasValue(this.responseMsToLive)) {
|
|
||||||
request.responseMsToLive = this.responseMsToLive;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestService.send(request);
|
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new patch to the object cache
|
|
||||||
* The patch is derived from the differences between the given object and its version in the object cache
|
|
||||||
* @param {DSpaceObject} object The given object
|
|
||||||
*/
|
|
||||||
update(object: T): Observable<RemoteData<T>> {
|
|
||||||
return this.createPatchFromCache(object)
|
|
||||||
.pipe(
|
|
||||||
mergeMap((operations: Operation[]) => {
|
|
||||||
if (isNotEmpty(operations)) {
|
|
||||||
this.objectCache.addPatch(object._links.self.href, operations);
|
|
||||||
}
|
|
||||||
return this.findByHref(object._links.self.href, true, true);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new DSpaceObject on the server, and store the response
|
|
||||||
* in the object cache
|
|
||||||
*
|
|
||||||
* @param {CacheableObject} object
|
|
||||||
* The object to create
|
|
||||||
* @param {RequestParam[]} params
|
|
||||||
* Array with additional params to combine with query string
|
|
||||||
*/
|
|
||||||
create(object: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
|
||||||
const endpoint$ = this.getEndpoint().pipe(
|
|
||||||
isNotEmptyOperator(),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((endpoint: string) => this.buildHrefWithParams(endpoint, params))
|
|
||||||
);
|
|
||||||
|
|
||||||
const serializedObject = new DSpaceSerializer(getClassForType(object.type)).serialize(object);
|
|
||||||
|
|
||||||
endpoint$.pipe(
|
|
||||||
take(1)
|
|
||||||
).subscribe((endpoint: string) => {
|
|
||||||
const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedObject));
|
|
||||||
if (hasValue(this.responseMsToLive)) {
|
|
||||||
request.responseMsToLive = this.responseMsToLive;
|
|
||||||
}
|
|
||||||
this.requestService.send(request);
|
|
||||||
});
|
|
||||||
|
|
||||||
const result$ = this.rdbService.buildFromRequestUUID<T>(requestId);
|
|
||||||
|
|
||||||
// TODO a dataservice is not the best place to show a notification,
|
|
||||||
// this should move up to the components that use this method
|
|
||||||
result$.pipe(
|
|
||||||
takeWhile((rd: RemoteData<T>) => rd.isLoading, true)
|
|
||||||
).subscribe((rd: RemoteData<T>) => {
|
|
||||||
if (rd.hasFailed) {
|
|
||||||
this.notificationsService.error('Server Error:', rd.errorMessage, new NotificationOptions(-1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate an existing DSpaceObject by marking all requests it is included in as stale
|
|
||||||
* @param objectId The id of the object to be invalidated
|
|
||||||
* @return An Observable that will emit `true` once all requests are stale
|
|
||||||
*/
|
|
||||||
invalidate(objectId: string): Observable<boolean> {
|
|
||||||
return this.getIDHrefObs(objectId).pipe(
|
|
||||||
switchMap((href: string) => this.invalidateByHref(href))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate an existing DSpaceObject by marking all requests it is included in as stale
|
|
||||||
* @param href The self link of the object to be invalidated
|
|
||||||
* @return An Observable that will emit `true` once all requests are stale
|
|
||||||
*/
|
|
||||||
invalidateByHref(href: string): Observable<boolean> {
|
|
||||||
const done$ = new AsyncSubject<boolean>();
|
|
||||||
|
|
||||||
this.objectCache.getByHref(href).pipe(
|
|
||||||
take(1),
|
|
||||||
switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe(
|
|
||||||
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
|
|
||||||
toArray(),
|
|
||||||
)),
|
|
||||||
).subscribe(() => {
|
|
||||||
done$.next(true);
|
|
||||||
done$.complete();
|
|
||||||
});
|
|
||||||
|
|
||||||
return done$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an existing DSpace Object on the server
|
|
||||||
* @param objectId The id of the object to be removed
|
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
|
||||||
* metadata should be saved as real metadata
|
|
||||||
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
|
||||||
* errorMessage, timeCompleted, etc
|
|
||||||
*/
|
|
||||||
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
|
||||||
return this.getIDHrefObs(objectId).pipe(
|
|
||||||
switchMap((href: string) => this.deleteByHref(href, copyVirtualMetadata))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an existing DSpace Object on the server
|
|
||||||
* @param href The self link of the object to be removed
|
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
|
||||||
* metadata should be saved as real metadata
|
|
||||||
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
|
||||||
* errorMessage, timeCompleted, etc
|
|
||||||
* Only emits once all request related to the DSO has been invalidated.
|
|
||||||
*/
|
|
||||||
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
|
||||||
|
|
||||||
if (copyVirtualMetadata) {
|
|
||||||
copyVirtualMetadata.forEach((id) =>
|
|
||||||
href += (href.includes('?') ? '&' : '?')
|
|
||||||
+ 'copyVirtualMetadata='
|
|
||||||
+ id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = new DeleteRequest(requestId, href);
|
|
||||||
if (hasValue(this.responseMsToLive)) {
|
|
||||||
request.responseMsToLive = this.responseMsToLive;
|
|
||||||
}
|
|
||||||
this.requestService.send(request);
|
|
||||||
|
|
||||||
const response$ = this.rdbService.buildFromRequestUUID(requestId);
|
|
||||||
|
|
||||||
const invalidated$ = new AsyncSubject<boolean>();
|
|
||||||
response$.pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
switchMap((rd: RemoteData<NoContent>) => {
|
|
||||||
if (rd.hasSucceeded) {
|
|
||||||
return this.invalidateByHref(href);
|
|
||||||
} else {
|
|
||||||
return [true];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).subscribe(() => {
|
|
||||||
invalidated$.next(true);
|
|
||||||
invalidated$.complete();
|
|
||||||
});
|
|
||||||
|
|
||||||
return combineLatest([response$, invalidated$]).pipe(
|
|
||||||
filter(([_, invalidated]) => invalidated),
|
|
||||||
map(([response, _]) => response),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Commit current object changes to the server
|
|
||||||
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
|
||||||
*/
|
|
||||||
commitUpdates(method?: RestRequestMethod) {
|
|
||||||
this.requestService.commit(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the links to traverse from the root of the api to the
|
|
||||||
* endpoint this DataService represents
|
|
||||||
*
|
|
||||||
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
|
|
||||||
* links to 'bar' the linkPath for the BarDataService would be
|
|
||||||
* 'foo/bar'
|
|
||||||
*/
|
|
||||||
getLinkPath(): string {
|
|
||||||
return this.linkPath;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,95 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { tap } from 'rxjs/operators';
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import { IdentifierType } from './request.models';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
|
||||||
import { Item } from '../shared/item.model';
|
|
||||||
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
|
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DsoRedirectDataService extends DataService<any> {
|
|
||||||
|
|
||||||
// Set the default link path to the identifier lookup endpoint.
|
|
||||||
protected linkPath = 'pid';
|
|
||||||
private uuidEndpoint = 'dso';
|
|
||||||
|
|
||||||
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: DSOChangeAnalyzer<any>,
|
|
||||||
private router: Router) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
setLinkPath(identifierType: IdentifierType) {
|
|
||||||
// The default 'pid' endpoint for identifiers does not support uuid lookups.
|
|
||||||
// For uuid lookups we need to change the linkPath.
|
|
||||||
if (identifierType === IdentifierType.UUID) {
|
|
||||||
this.linkPath = this.uuidEndpoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getIDHref(endpoint, resourceID, ...linksToFollow: FollowLinkConfig<any>[]): string {
|
|
||||||
// Supporting both identifier (pid) and uuid (dso) endpoints
|
|
||||||
return this.buildHrefFromFindOptions( endpoint.replace(/\{\?id\}/, `?id=${resourceID}`)
|
|
||||||
.replace(/\{\?uuid\}/, `?uuid=${resourceID}`),
|
|
||||||
{}, [], ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<DSpaceObject>> {
|
|
||||||
this.setLinkPath(identifierType);
|
|
||||||
return this.findById(id).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
tap((response) => {
|
|
||||||
if (response.hasSucceeded) {
|
|
||||||
const dso = response.payload;
|
|
||||||
const uuid = dso.uuid;
|
|
||||||
if (hasValue(uuid)) {
|
|
||||||
let newRoute = this.getEndpointFromDSOType(response.payload.type);
|
|
||||||
if (dso.type.startsWith('item')) {
|
|
||||||
newRoute = getItemPageRoute(dso as Item);
|
|
||||||
} else if (hasValue(newRoute)) {
|
|
||||||
newRoute += '/' + uuid;
|
|
||||||
}
|
|
||||||
if (hasValue(newRoute)) {
|
|
||||||
this.router.navigate([newRoute]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Is there an existing method somewhere else that converts dso type to route?
|
|
||||||
getEndpointFromDSOType(dsoType: string): string {
|
|
||||||
// Are there other types to consider?
|
|
||||||
if (dsoType.startsWith('item')) {
|
|
||||||
return 'items';
|
|
||||||
} else if (dsoType.startsWith('community')) {
|
|
||||||
return 'communities';
|
|
||||||
} else if (dsoType.startsWith('collection')) {
|
|
||||||
return 'collections';
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +1,18 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } 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';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { DsoRedirectDataService } from './dso-redirect-data.service';
|
import { DsoRedirectService } from './dso-redirect.service';
|
||||||
import { GetRequest, 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';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
|
|
||||||
describe('DsoRedirectDataService', () => {
|
describe('DsoRedirectService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: DsoRedirectDataService;
|
let service: DsoRedirectService;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
@@ -29,10 +25,6 @@ describe('DsoRedirectDataService', () => {
|
|||||||
const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`;
|
const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`;
|
||||||
const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`;
|
const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`;
|
||||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||||
const store = {} as Store<CoreState>;
|
|
||||||
const notificationsService = {} as NotificationsService;
|
|
||||||
const http = {} as HttpClient;
|
|
||||||
const comparator = {} as any;
|
|
||||||
const objectCache = {} as ObjectCacheService;
|
const objectCache = {} as ObjectCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -59,20 +51,16 @@ describe('DsoRedirectDataService', () => {
|
|||||||
a: remoteData
|
a: remoteData
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
service = new DsoRedirectDataService(
|
service = new DsoRedirectService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
store,
|
|
||||||
objectCache,
|
objectCache,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
router,
|
||||||
http,
|
|
||||||
comparator,
|
|
||||||
router
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findById', () => {
|
describe('findByIdAndIDType', () => {
|
||||||
it('should call HALEndpointService with the path to the pid endpoint', () => {
|
it('should call HALEndpointService with the path to the pid endpoint', () => {
|
||||||
scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE));
|
scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
@@ -141,7 +129,7 @@ describe('DsoRedirectDataService', () => {
|
|||||||
redir.subscribe();
|
redir.subscribe();
|
||||||
scheduler.schedule(() => redir);
|
scheduler.schedule(() => redir);
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]);
|
expect(router.navigate).toHaveBeenCalledWith(['/collections/' + remoteData.payload.uuid]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to communities route', () => {
|
it('should navigate to communities route', () => {
|
||||||
@@ -150,55 +138,58 @@ describe('DsoRedirectDataService', () => {
|
|||||||
redir.subscribe();
|
redir.subscribe();
|
||||||
scheduler.schedule(() => redir);
|
scheduler.schedule(() => redir);
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['communities/' + remoteData.payload.uuid]);
|
expect(router.navigate).toHaveBeenCalledWith(['/communities/' + remoteData.payload.uuid]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getIDHref', () => {
|
describe('DataService', () => { // todo: should only test the id/uuid interpolation thingy
|
||||||
it('should return endpoint', () => {
|
describe('getIDHref', () => { // todo: should be able to move this up to IdentifiableDataService?
|
||||||
const result = (service as any).getIDHref(pidLink, dsoUUID);
|
it('should return endpoint', () => {
|
||||||
expect(result).toEqual(requestUUIDURL);
|
const result = (service as any).dataService.getIDHref(pidLink, dsoUUID);
|
||||||
});
|
expect(result).toEqual(requestUUIDURL);
|
||||||
|
});
|
||||||
|
|
||||||
it('should include single linksToFollow as embed', () => {
|
it('should include single linksToFollow as embed', () => {
|
||||||
const expected = `${requestUUIDURL}&embed=bundles`;
|
const expected = `${requestUUIDURL}&embed=bundles`;
|
||||||
const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles'));
|
const result = (service as any).dataService.getIDHref(pidLink, dsoUUID, followLink('bundles'));
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include multiple linksToFollow as embed', () => {
|
it('should include multiple linksToFollow as embed', () => {
|
||||||
const expected = `${requestUUIDURL}&embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
const expected = `${requestUUIDURL}&embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
||||||
const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf'));
|
const result = (service as any).dataService.getIDHref(pidLink, dsoUUID, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf'));
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
const expected = `${requestUUIDURL}&embed=templateItemOf`;
|
const expected = `${requestUUIDURL}&embed=templateItemOf`;
|
||||||
const result = (service as any).getIDHref(
|
const result = (service as any).dataService.getIDHref(
|
||||||
pidLink,
|
pidLink,
|
||||||
dsoUUID,
|
dsoUUID,
|
||||||
followLink('bundles', { shouldEmbed: false }),
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
followLink('owningCollection', { shouldEmbed: false }),
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
followLink('templateItemOf')
|
followLink('templateItemOf'),
|
||||||
);
|
);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
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(
|
const result = (service as any).dataService.getIDHref(
|
||||||
pidLink,
|
pidLink,
|
||||||
dsoUUID,
|
dsoUUID,
|
||||||
followLink('owningCollection',
|
followLink(
|
||||||
{},
|
'owningCollection',
|
||||||
followLink('itemtemplate',
|
|
||||||
{},
|
{},
|
||||||
followLink('relationships')
|
followLink(
|
||||||
)
|
'itemtemplate',
|
||||||
)
|
{},
|
||||||
);
|
followLink('relationships'),
|
||||||
expect(result).toEqual(expected);
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
104
src/app/core/data/dso-redirect.service.ts
Normal file
104
src/app/core/data/dso-redirect.service.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { IdentifierType } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { getDSORoute } from '../../app-routing-paths';
|
||||||
|
|
||||||
|
const ID_ENDPOINT = 'pid';
|
||||||
|
const UUID_ENDPOINT = 'dso';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data service to retrieve DSpaceObjects by persistent identifier or UUID.
|
||||||
|
* Doesn't define a constant {@link linkPath} but switches between two endpoints on demand:
|
||||||
|
* {@link setLinkPath} must be called before each request.
|
||||||
|
*/
|
||||||
|
class DsoByIdOrUUIDDataService extends IdentifiableDataService<DSpaceObject> {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
undefined, requestService, rdbService, objectCache, halService, undefined,
|
||||||
|
// interpolate id/uuid as query parameter
|
||||||
|
(endpoint: string, resourceID: string): string => {
|
||||||
|
return endpoint.replace(/{\?id}/, `?id=${resourceID}`)
|
||||||
|
.replace(/{\?uuid}/, `?uuid=${resourceID}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default 'pid' endpoint for identifiers does not support uuid lookups.
|
||||||
|
* For uuid lookups we need to change the linkPath.
|
||||||
|
* @param identifierType
|
||||||
|
*/
|
||||||
|
setLinkPath(identifierType: IdentifierType) {
|
||||||
|
if (identifierType === IdentifierType.UUID) {
|
||||||
|
this.linkPath = UUID_ENDPOINT;
|
||||||
|
} else {
|
||||||
|
this.linkPath = ID_ENDPOINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service to handle redirects from identifier paths to DSO path
|
||||||
|
* e.g.: redirect from /handle/... to /items/...
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DsoRedirectService {
|
||||||
|
private dataService: DsoByIdOrUUIDDataService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
private router: Router,
|
||||||
|
) {
|
||||||
|
this.dataService = new DsoByIdOrUUIDDataService(requestService, rdbService, objectCache, halService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a DSpaceObject by
|
||||||
|
* @param id the identifier of the object to retrieve
|
||||||
|
* @param identifierType the type of the given identifier (defaults to UUID)
|
||||||
|
*/
|
||||||
|
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<DSpaceObject>> {
|
||||||
|
this.dataService.setLinkPath(identifierType);
|
||||||
|
return this.dataService.findById(id).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
tap((response) => {
|
||||||
|
if (response.hasSucceeded) {
|
||||||
|
const dso = response.payload;
|
||||||
|
if (hasValue(dso.uuid)) {
|
||||||
|
let newRoute = getDSORoute(dso);
|
||||||
|
if (hasValue(newRoute)) {
|
||||||
|
this.router.navigate([newRoute]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -7,8 +7,6 @@ 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';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
|
|
||||||
describe('DSpaceObjectDataService', () => {
|
describe('DSpaceObjectDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -42,18 +40,12 @@ describe('DSpaceObjectDataService', () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = {} as ObjectCacheService;
|
||||||
const notificationsService = {} as NotificationsService;
|
|
||||||
const http = {} as HttpClient;
|
|
||||||
const comparator = {} as any;
|
|
||||||
|
|
||||||
service = new DSpaceObjectDataService(
|
service = new DSpaceObjectDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
|
||||||
http,
|
|
||||||
comparator
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,106 +1,28 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { DSPACE_OBJECT } from '../shared/dspace-object.resource-type';
|
import { DSPACE_OBJECT } from '../shared/dspace-object.resource-type';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { PaginatedList } from './paginated-list.model';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
import { CoreState } from '../core-state.model';
|
import { dataService } from './base/data-service.decorator';
|
||||||
import { FindListOptions } from './find-list-options.model';
|
|
||||||
|
|
||||||
class DataServiceImpl extends DataService<DSpaceObject> {
|
|
||||||
protected linkPath = 'dso';
|
|
||||||
|
|
||||||
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: DSOChangeAnalyzer<DSpaceObject>) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
getIDHref(endpoint, resourceID, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): string {
|
|
||||||
return this.buildHrefFromFindOptions( endpoint.replace(/\{\?uuid\}/, `?uuid=${resourceID}`),
|
|
||||||
{}, [], ...linksToFollow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(DSPACE_OBJECT)
|
@dataService(DSPACE_OBJECT)
|
||||||
export class DSpaceObjectDataService {
|
export class DSpaceObjectDataService extends IdentifiableDataService<DSpaceObject> {
|
||||||
protected linkPath = 'dso';
|
|
||||||
private dataService: DataServiceImpl;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
) {
|
||||||
protected http: HttpClient,
|
super(
|
||||||
protected comparator: DSOChangeAnalyzer<DSpaceObject>) {
|
'dso', requestService, rdbService, objectCache, halService, undefined,
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
// interpolate uuid as query parameter
|
||||||
|
(endpoint: string, resourceID: string): string => {
|
||||||
|
return endpoint.replace(/{\?uuid}/, `?uuid=${resourceID}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @param id ID 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
|
|
||||||
*/
|
|
||||||
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
|
|
||||||
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},
|
|
||||||
* 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(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
|
|
||||||
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(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
|
|
||||||
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
19
src/app/core/data/entity-type-data.service.spec.ts
Normal file
19
src/app/core/data/entity-type-data.service.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { testFindAllDataImplementation } from './base/find-all-data.spec';
|
||||||
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
|
import { EntityTypeDataService } from './entity-type-data.service';
|
||||||
|
|
||||||
|
describe('EntityTypeDataService', () => {
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new EntityTypeDataService(null, null, null, null, null);
|
||||||
|
testFindAllDataImplementation(initService);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,13 +1,8 @@
|
|||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.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 { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
@@ -16,28 +11,31 @@ import { RelationshipType } from '../shared/item-relationships/relationship-type
|
|||||||
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 { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
|
import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
|
||||||
import { RelationshipTypeService } from './relationship-type.service';
|
import { RelationshipTypeDataService } from './relationship-type-data.service';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { BaseDataService } from './base/base-data.service';
|
||||||
|
import { SearchData, SearchDataImpl } from './base/search-data';
|
||||||
|
import { FindAllData, FindAllDataImpl } from './base/find-all-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service handling all ItemType requests
|
* Service handling all ItemType requests
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EntityTypeService extends DataService<ItemType> {
|
export class EntityTypeDataService extends BaseDataService<ItemType> implements FindAllData<ItemType>, SearchData<ItemType> {
|
||||||
|
private findAllData: FindAllData<ItemType>;
|
||||||
|
private searchData: SearchDataImpl<ItemType>;
|
||||||
|
|
||||||
protected linkPath = 'entitytypes';
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected relationshipTypeService: RelationshipTypeDataService,
|
||||||
|
) {
|
||||||
|
super('entitytypes', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
constructor(protected requestService: RequestService,
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
protected rdbService: RemoteDataBuildService,
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected halService: HALEndpointService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected relationshipTypeService: RelationshipTypeService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DefaultChangeAnalyzer<ItemType>) {
|
|
||||||
super();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseEndpoint(options, linkPath?: string): Observable<string> {
|
getBrowseEndpoint(options, linkPath?: string): Observable<string> {
|
||||||
@@ -147,7 +145,7 @@ export class EntityTypeService extends DataService<ItemType> {
|
|||||||
*/
|
*/
|
||||||
getEntityTypeRelationships(entityTypeId: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...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);
|
return this.relationshipTypeService.findListByHref(href$, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,7 +156,43 @@ export class EntityTypeService extends DataService<ItemType> {
|
|||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((endPoint: string) =>
|
switchMap((endPoint: string) =>
|
||||||
this.findByHref(endPoint + '/label/' + label))
|
this.findByHref(endPoint + '/label/' + label)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<ItemType>[]): Observable<RemoteData<PaginatedList<ItemType>>> {
|
||||||
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<ItemType>[]): Observable<RemoteData<PaginatedList<ItemType>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -13,11 +13,9 @@ import { RegistrationResponseParsingService } from './registration-response-pars
|
|||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
|
||||||
@Injectable(
|
@Injectable({
|
||||||
{
|
providedIn: 'root',
|
||||||
providedIn: 'root',
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
/**
|
/**
|
||||||
* Service that will register a new email address and request a token
|
* Service that will register a new email address and request a token
|
||||||
*/
|
*/
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
import { ExternalSourceService } from './external-source.service';
|
import { ExternalSourceDataService } from './external-source-data.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { GetRequest } from './request.models';
|
import { GetRequest } from './request.models';
|
||||||
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
|
|
||||||
describe('ExternalSourceService', () => {
|
describe('ExternalSourceService', () => {
|
||||||
let service: ExternalSourceService;
|
let service: ExternalSourceDataService;
|
||||||
|
|
||||||
let requestService;
|
let requestService;
|
||||||
let rdbService;
|
let rdbService;
|
||||||
@@ -48,15 +49,20 @@ describe('ExternalSourceService', () => {
|
|||||||
buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries))
|
buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries))
|
||||||
});
|
});
|
||||||
halService = jasmine.createSpyObj('halService', {
|
halService = jasmine.createSpyObj('halService', {
|
||||||
getEndpoint: observableOf('external-sources-REST-endpoint')
|
getEndpoint: observableOf('external-sources-REST-endpoint'),
|
||||||
});
|
});
|
||||||
service = new ExternalSourceService(requestService, rdbService, undefined, undefined, halService, undefined, undefined, undefined);
|
service = new ExternalSourceDataService(requestService, rdbService, undefined, halService);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new ExternalSourceDataService(null, null, null, null);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getExternalSourceEntries', () => {
|
describe('getExternalSourceEntries', () => {
|
||||||
let result;
|
let result;
|
||||||
|
|
@@ -1,13 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { ExternalSource } from '../shared/external-source.model';
|
import { ExternalSource } from '../shared/external-source.model';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
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 { HttpClient } from '@angular/common/http';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
|
import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
@@ -15,28 +11,27 @@ import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
|
|||||||
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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { SearchData, SearchDataImpl } from './base/search-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service handling all external source requests
|
* A service handling all external source requests
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExternalSourceService extends DataService<ExternalSource> {
|
export class ExternalSourceDataService extends IdentifiableDataService<ExternalSource> implements SearchData<ExternalSource> {
|
||||||
protected linkPath = 'externalsources';
|
private searchData: SearchData<ExternalSource>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
) {
|
||||||
protected http: HttpClient,
|
super('externalsources', requestService, rdbService, objectCache, halService);
|
||||||
protected comparator: DefaultChangeAnalyzer<ExternalSource>) {
|
|
||||||
super();
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,10 +70,28 @@ export class ExternalSourceService extends DataService<ExternalSource> {
|
|||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint),
|
map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint),
|
||||||
take(1)
|
take(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO create a dedicated ExternalSourceEntryDataService and move this entire method to it. Then the "as any"s won't be necessary
|
// TODO create a dedicated ExternalSourceEntryDataService and move this entire method to it. Then the "as any"s won't be necessary
|
||||||
return this.findAllByHref(href$, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as any) as any;
|
return this.findListByHref(href$, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as any) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<ExternalSource>[]): Observable<RemoteData<PaginatedList<ExternalSource>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,6 +1,5 @@
|
|||||||
import { AuthorizationDataService } from './authorization-data.service';
|
import { AuthorizationDataService } from './authorization-data.service';
|
||||||
import { SiteDataService } from '../site-data.service';
|
import { SiteDataService } from '../site-data.service';
|
||||||
import { AuthService } from '../../auth/auth.service';
|
|
||||||
import { Site } from '../../shared/site.model';
|
import { Site } from '../../shared/site.model';
|
||||||
import { EPerson } from '../../eperson/models/eperson.model';
|
import { EPerson } from '../../eperson/models/eperson.model';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
@@ -12,11 +11,11 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from
|
|||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { Feature } from '../../shared/feature.model';
|
import { Feature } from '../../shared/feature.model';
|
||||||
import { FindListOptions } from '../find-list-options.model';
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { testSearchDataImplementation } from '../base/search-data.spec';
|
||||||
|
|
||||||
describe('AuthorizationDataService', () => {
|
describe('AuthorizationDataService', () => {
|
||||||
let service: AuthorizationDataService;
|
let service: AuthorizationDataService;
|
||||||
let siteService: SiteDataService;
|
let siteService: SiteDataService;
|
||||||
let authService: AuthService;
|
|
||||||
|
|
||||||
let site: Site;
|
let site: Site;
|
||||||
let ePerson: EPerson;
|
let ePerson: EPerson;
|
||||||
@@ -37,13 +36,9 @@ describe('AuthorizationDataService', () => {
|
|||||||
uuid: 'test-eperson'
|
uuid: 'test-eperson'
|
||||||
});
|
});
|
||||||
siteService = jasmine.createSpyObj('siteService', {
|
siteService = jasmine.createSpyObj('siteService', {
|
||||||
find: observableOf(site)
|
find: observableOf(site),
|
||||||
});
|
});
|
||||||
authService = {
|
service = new AuthorizationDataService(requestService, undefined, undefined, undefined, siteService);
|
||||||
isAuthenticated: () => observableOf(true),
|
|
||||||
getAuthenticatedUserFromStore: () => observableOf(ePerson)
|
|
||||||
} as AuthService;
|
|
||||||
service = new AuthorizationDataService(requestService, undefined, undefined, undefined, undefined, undefined, undefined, undefined, authService, siteService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -51,6 +46,11 @@ describe('AuthorizationDataService', () => {
|
|||||||
spyOn(service, 'searchBy').and.returnValue(observableOf(undefined));
|
spyOn(service, 'searchBy').and.returnValue(observableOf(undefined));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new AuthorizationDataService(null, null, null, null, null);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
it('should call setStaleByHrefSubstring method', () => {
|
it('should call setStaleByHrefSubstring method', () => {
|
||||||
service.invalidateAuthorizationsRequestCache();
|
service.invalidateAuthorizationsRequestCache();
|
||||||
expect((service as any).requestService.setStaleByHrefSubstring).toHaveBeenCalledWith((service as any).linkPath);
|
expect((service as any).requestService.setStaleByHrefSubstring).toHaveBeenCalledWith((service as any).linkPath);
|
||||||
|
@@ -1,18 +1,11 @@
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AUTHORIZATION } from '../../shared/authorization.resource-type';
|
import { AUTHORIZATION } from '../../shared/authorization.resource-type';
|
||||||
import { dataService } from '../../cache/builders/build-decorators';
|
|
||||||
import { DataService } from '../data.service';
|
|
||||||
import { Authorization } from '../../shared/authorization.model';
|
import { Authorization } from '../../shared/authorization.model';
|
||||||
import { RequestService } from '../request.service';
|
import { RequestService } from '../request.service';
|
||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
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 { HttpClient } from '@angular/common/http';
|
|
||||||
import { DSOChangeAnalyzer } from '../dso-change-analyzer.service';
|
|
||||||
import { AuthService } from '../../auth/auth.service';
|
|
||||||
import { SiteDataService } from '../site-data.service';
|
import { SiteDataService } from '../site-data.service';
|
||||||
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteData } from '../remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
@@ -24,31 +17,32 @@ import { AuthorizationSearchParams } from './authorization-search-params';
|
|||||||
import { addSiteObjectUrlIfEmpty, oneAuthorizationMatchesFeature } from './authorization-utils';
|
import { addSiteObjectUrlIfEmpty, oneAuthorizationMatchesFeature } from './authorization-utils';
|
||||||
import { FeatureID } from './feature-id';
|
import { FeatureID } from './feature-id';
|
||||||
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
import { CoreState } from '../../core-state.model';
|
|
||||||
import { FindListOptions } from '../find-list-options.model';
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { BaseDataService } from '../base/base-data.service';
|
||||||
|
import { SearchData, SearchDataImpl } from '../base/search-data';
|
||||||
|
import { dataService } from '../base/data-service.decorator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Authorization}s from the REST API
|
* A service to retrieve {@link Authorization}s from the REST API
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(AUTHORIZATION)
|
@dataService(AUTHORIZATION)
|
||||||
export class AuthorizationDataService extends DataService<Authorization> {
|
export class AuthorizationDataService extends BaseDataService<Authorization> implements SearchData<Authorization> {
|
||||||
protected linkPath = 'authorizations';
|
protected linkPath = 'authorizations';
|
||||||
protected searchByObjectPath = 'object';
|
protected searchByObjectPath = 'object';
|
||||||
|
|
||||||
|
private searchData: SearchDataImpl<Authorization>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected siteService: SiteDataService,
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DSOChangeAnalyzer<Authorization>,
|
|
||||||
protected authService: AuthService,
|
|
||||||
protected siteService: SiteDataService
|
|
||||||
) {
|
) {
|
||||||
super();
|
super('authorizations', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,7 +124,25 @@ export class AuthorizationDataService extends DataService<Authorization> {
|
|||||||
params.push(new RequestParam('eperson', ePersonUuid));
|
params.push(new RequestParam('eperson', ePersonUuid));
|
||||||
}
|
}
|
||||||
return Object.assign(new FindListOptions(), options, {
|
return Object.assign(new FindListOptions(), options, {
|
||||||
searchParams: [...params]
|
searchParams: [...params],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] 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
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Authorization>[]): Observable<RemoteData<PaginatedList<Authorization>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,36 +1,27 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { FEATURE } from '../../shared/feature.resource-type';
|
import { FEATURE } from '../../shared/feature.resource-type';
|
||||||
import { dataService } from '../../cache/builders/build-decorators';
|
|
||||||
import { DataService } from '../data.service';
|
|
||||||
import { Feature } from '../../shared/feature.model';
|
import { Feature } from '../../shared/feature.model';
|
||||||
import { RequestService } from '../request.service';
|
import { RequestService } from '../request.service';
|
||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
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 { BaseDataService } from '../base/base-data.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { dataService } from '../base/data-service.decorator';
|
||||||
import { DSOChangeAnalyzer } from '../dso-change-analyzer.service';
|
|
||||||
import { CoreState } from '../../core-state.model';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Feature}s from the REST API
|
* A service to retrieve {@link Feature}s from the REST API
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(FEATURE)
|
@dataService(FEATURE)
|
||||||
export class FeatureDataService extends DataService<Feature> {
|
export class FeatureDataService extends BaseDataService<Feature> {
|
||||||
protected linkPath = 'features';
|
protected linkPath = 'features';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DSOChangeAnalyzer<Feature>
|
|
||||||
) {
|
) {
|
||||||
super();
|
super('features', requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { HrefOnlyDataService } from './href-only-data.service';
|
import { HrefOnlyDataService } from './href-only-data.service';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { BaseDataService } from './base/base-data.service';
|
||||||
|
|
||||||
describe(`HrefOnlyDataService`, () => {
|
describe(`HrefOnlyDataService`, () => {
|
||||||
let service: HrefOnlyDataService;
|
let service: HrefOnlyDataService;
|
||||||
@@ -15,12 +15,12 @@ describe(`HrefOnlyDataService`, () => {
|
|||||||
href = 'https://rest.api/server/api/core/items/de7fa215-4a25-43a7-a4d7-17534a09fdfc';
|
href = 'https://rest.api/server/api/core/items/de7fa215-4a25-43a7-a4d7-17534a09fdfc';
|
||||||
followLinks = [ followLink('link1'), followLink('link2') ];
|
followLinks = [ followLink('link1'), followLink('link2') ];
|
||||||
findListOptions = new FindListOptions();
|
findListOptions = new FindListOptions();
|
||||||
service = new HrefOnlyDataService(null, null, null, null, null, null, null, null);
|
service = new HrefOnlyDataService(null, null, null, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should instantiate a private DataService`, () => {
|
it(`should instantiate a private DataService`, () => {
|
||||||
expect((service as any).dataService).toBeDefined();
|
expect((service as any).dataService).toBeDefined();
|
||||||
expect((service as any).dataService).toBeInstanceOf(DataService);
|
expect((service as any).dataService).toBeInstanceOf(BaseDataService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`findByHref`, () => {
|
describe(`findByHref`, () => {
|
||||||
@@ -28,7 +28,7 @@ describe(`HrefOnlyDataService`, () => {
|
|||||||
spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
|
spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should delegate to findByHref on the internal DataService`, () => {
|
it(`should forward to findByHref on the internal DataService`, () => {
|
||||||
service.findByHref(href, false, false, ...followLinks);
|
service.findByHref(href, false, false, ...followLinks);
|
||||||
expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks);
|
expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks);
|
||||||
});
|
});
|
||||||
@@ -48,33 +48,33 @@ describe(`HrefOnlyDataService`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`findAllByHref`, () => {
|
describe(`findListByHref`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spy = spyOn((service as any).dataService, 'findAllByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
|
spy = spyOn((service as any).dataService, 'findListByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should delegate to findAllByHref on the internal DataService`, () => {
|
it(`should delegate to findListByHref on the internal DataService`, () => {
|
||||||
service.findAllByHref(href, findListOptions, false, false, ...followLinks);
|
service.findListByHref(href, findListOptions, false, false, ...followLinks);
|
||||||
expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks);
|
expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`when findListOptions is omitted`, () => {
|
describe(`when findListOptions is omitted`, () => {
|
||||||
it(`should call findAllByHref on the internal DataService with findListOptions = {}`, () => {
|
it(`should call findListByHref on the internal DataService with findListOptions = {}`, () => {
|
||||||
service.findAllByHref(href);
|
service.findListByHref(href);
|
||||||
expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything());
|
expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`when useCachedVersionIfAvailable is omitted`, () => {
|
describe(`when useCachedVersionIfAvailable is omitted`, () => {
|
||||||
it(`should call findAllByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => {
|
it(`should call findListByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => {
|
||||||
service.findAllByHref(href);
|
service.findListByHref(href);
|
||||||
expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything());
|
expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`when reRequestOnStale is omitted`, () => {
|
describe(`when reRequestOnStale is omitted`, () => {
|
||||||
it(`should call findAllByHref on the internal DataService with reRequestOnStale = true`, () => {
|
it(`should call findListByHref on the internal DataService with reRequestOnStale = true`, () => {
|
||||||
service.findAllByHref(href);
|
service.findListByHref(href);
|
||||||
expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true);
|
expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,42 +1,20 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
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 { HttpClient } from '@angular/common/http';
|
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { VOCABULARY_ENTRY } from '../submission/vocabularies/models/vocabularies.resource-type';
|
import { VOCABULARY_ENTRY } from '../submission/vocabularies/models/vocabularies.resource-type';
|
||||||
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 { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { PaginatedList } from './paginated-list.model';
|
import { PaginatedList } from './paginated-list.model';
|
||||||
import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type';
|
import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type';
|
||||||
import { LICENSE } from '../shared/license.resource-type';
|
import { LICENSE } from '../shared/license.resource-type';
|
||||||
import { CacheableObject } from '../cache/cacheable-object.model';
|
import { CacheableObject } from '../cache/cacheable-object.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { BaseDataService } from './base/base-data.service';
|
||||||
class DataServiceImpl extends DataService<any> {
|
import { HALDataService } from './base/hal-data-service.interface';
|
||||||
// linkPath isn't used if we're only searching by href.
|
import { dataService } from './base/data-service.decorator';
|
||||||
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
|
* A DataService with only findByHref methods. Its purpose is to be used for resources that don't
|
||||||
@@ -44,26 +22,37 @@ class DataServiceImpl extends DataService<any> {
|
|||||||
* for their links to be resolved by the LinkService.
|
* for their links to be resolved by the LinkService.
|
||||||
*
|
*
|
||||||
* an @dataService annotation can be added for any number of these resource types
|
* an @dataService annotation can be added for any number of these resource types
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Additionally, this service may be used to retrieve objects by `href` regardless of their type
|
||||||
|
* For example
|
||||||
|
* ```
|
||||||
|
* const items$: Observable<RemoteData<PaginatedList<Item>>> = hrefOnlyDataService.findListByHref<Item>(href);
|
||||||
|
* const sites$: Observable<RemoteData<PaginatedList<Site>>> = hrefOnlyDataService.findListByHref<Site>(href);
|
||||||
|
* ```
|
||||||
|
* This means we cannot extend from {@link BaseDataService} directly because the method signatures would not match.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@dataService(VOCABULARY_ENTRY)
|
@dataService(VOCABULARY_ENTRY)
|
||||||
@dataService(ITEM_TYPE)
|
@dataService(ITEM_TYPE)
|
||||||
@dataService(LICENSE)
|
@dataService(LICENSE)
|
||||||
export class HrefOnlyDataService {
|
export class HrefOnlyDataService implements HALDataService<any> {
|
||||||
private dataService: DataServiceImpl;
|
/**
|
||||||
|
* Works with a {@link BaseDataService} internally, but only exposes two of its methods
|
||||||
|
* with altered signatures to (optionally) constrain the arbitrary return type.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private dataService: BaseDataService<any>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
) {
|
||||||
protected http: HttpClient,
|
this.dataService = new BaseDataService(undefined, requestService, rdbService, objectCache, halService);
|
||||||
protected comparator: DefaultChangeAnalyzer<any>) {
|
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, store, objectCache, halService, notificationsService, http, comparator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,7 +82,7 @@ export class HrefOnlyDataService {
|
|||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
* {@link HALLink}s should be automatically resolved
|
* {@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>>> {
|
findListByHref<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);
|
return this.dataService.findListByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,18 +10,21 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
|||||||
import { RestResponse } from '../cache/response.models';
|
import { RestResponse } from '../cache/response.models';
|
||||||
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
import { DeleteRequest, GetRequest, PostRequest } from './request.models';
|
import { DeleteRequest, PostRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { RequestEntry } from './request-entry.model';
|
import { RequestEntry } from './request-entry.model';
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
import { HALEndpointServiceStub } from 'src/app/shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from 'src/app/shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { testCreateDataImplementation } from './base/create-data.spec';
|
||||||
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: ItemDataService;
|
let service: ItemDataService;
|
||||||
let bs: BrowseService;
|
let browseService: BrowseService;
|
||||||
const requestService = Object.assign(getMockRequestService(), {
|
const requestService = Object.assign(getMockRequestService(), {
|
||||||
generateRequestId(): string {
|
generateRequestId(): string {
|
||||||
return scopeID;
|
return scopeID;
|
||||||
@@ -78,24 +81,29 @@ describe('ItemDataService', () => {
|
|||||||
return new ItemDataService(
|
return new ItemDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
store,
|
|
||||||
bs,
|
|
||||||
objectCache,
|
objectCache,
|
||||||
halEndpointService,
|
halEndpointService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
http,
|
|
||||||
comparator,
|
comparator,
|
||||||
bundleService
|
browseService,
|
||||||
|
bundleService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new ItemDataService(null, null, null, null, null, null, null, null);
|
||||||
|
testCreateDataImplementation(initService);
|
||||||
|
testPatchDataImplementation(initService);
|
||||||
|
testDeleteDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getBrowseEndpoint', () => {
|
describe('getBrowseEndpoint', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the endpoint to fetch Items within the given scope and starting with the given string', () => {
|
it('should return the endpoint to fetch Items within the given scope and starting with the given string', () => {
|
||||||
bs = initMockBrowseService(true);
|
browseService = initMockBrowseService(true);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
|
||||||
const result = service.getBrowseEndpoint(options);
|
const result = service.getBrowseEndpoint(options);
|
||||||
@@ -106,7 +114,7 @@ describe('ItemDataService', () => {
|
|||||||
|
|
||||||
describe('if the dc.date.issue browse isn\'t configured for items', () => {
|
describe('if the dc.date.issue browse isn\'t configured for items', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
bs = initMockBrowseService(false);
|
browseService = initMockBrowseService(false);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
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';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
@@ -16,12 +22,10 @@ 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 { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
import { DataService } from './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 { DeleteRequest, GetRequest, PostRequest, PutRequest} from './request.models';
|
import { DeleteRequest, GetRequest, PostRequest, PutRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import { Bundle } from '../shared/bundle.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
@@ -34,27 +38,42 @@ import { ResponseParsingService } from './parsing.service';
|
|||||||
import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service';
|
import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service';
|
||||||
import { sendRequest } from '../shared/request.operators';
|
import { sendRequest } from '../shared/request.operators';
|
||||||
import { RestRequest } from './rest-request.model';
|
import { RestRequest } from './rest-request.model';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { ConstructIdEndpoint, IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { PatchData, PatchDataImpl } from './base/patch-data';
|
||||||
|
import { DeleteData, DeleteDataImpl } from './base/delete-data';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { CreateData, CreateDataImpl } from './base/create-data';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
|
||||||
@Injectable()
|
/**
|
||||||
@dataService(ITEM)
|
* An abstract service for CRUD operations on Items
|
||||||
export class ItemDataService extends DataService<Item> {
|
* Doesn't specify an endpoint because multiple endpoints support Item-like functionality (e.g. items, itemtemplates)
|
||||||
protected linkPath = 'items';
|
* Extend this class to implement data services for Items
|
||||||
|
*/
|
||||||
|
export abstract class BaseItemDataService extends IdentifiableDataService<Item> implements CreateData<Item>, PatchData<Item>, DeleteData<Item> {
|
||||||
|
private createData: CreateData<Item>;
|
||||||
|
private patchData: PatchData<Item>;
|
||||||
|
private deleteData: DeleteData<Item>;
|
||||||
|
|
||||||
constructor(
|
protected constructor(
|
||||||
|
protected linkPath,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected bs: BrowseService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DSOChangeAnalyzer<Item>,
|
protected comparator: DSOChangeAnalyzer<Item>,
|
||||||
protected bundleService: BundleDataService
|
protected browseService: BrowseService,
|
||||||
|
protected bundleService: BundleDataService,
|
||||||
|
protected constructIdEndpoint: ConstructIdEndpoint = (endpoint, resourceID) => `${endpoint}/${resourceID}`,
|
||||||
) {
|
) {
|
||||||
super();
|
super(linkPath, requestService, rdbService, objectCache, halService, undefined, constructIdEndpoint);
|
||||||
|
|
||||||
|
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
|
||||||
|
this.patchData = new PatchDataImpl<Item>(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,10 +88,11 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
if (options.sort && options.sort.field) {
|
if (options.sort && options.sort.field) {
|
||||||
field = options.sort.field;
|
field = options.sort.field;
|
||||||
}
|
}
|
||||||
return this.bs.getBrowseURLFor(field, linkPath).pipe(
|
return this.browseService.getBrowseURLFor(field, linkPath).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()),
|
map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()),
|
||||||
distinctUntilChanged(),);
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +104,7 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
public getMappedCollectionsEndpoint(itemId: string, collectionId?: string): Observable<string> {
|
public getMappedCollectionsEndpoint(itemId: string, collectionId?: string): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, itemId)),
|
map((endpoint: string) => this.getIDHref(endpoint, itemId)),
|
||||||
map((endpoint: string) => `${endpoint}/mappedCollections${collectionId ? `/${collectionId}` : ''}`)
|
map((endpoint: string) => `${endpoint}/mappedCollections${collectionId ? `/${collectionId}` : ''}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +239,7 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
public getMoveItemEndpoint(itemId: string): Observable<string> {
|
public getMoveItemEndpoint(itemId: string): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, itemId)),
|
map((endpoint: string) => this.getIDHref(endpoint, itemId)),
|
||||||
map((endpoint: string) => `${endpoint}/owningCollection`)
|
map((endpoint: string) => `${endpoint}/owningCollection`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,4 +319,93 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
this.requestService.setStaleByHrefSubstring('item/' + itemUUID);
|
this.requestService.setStaleByHrefSubstring('item/' + itemUUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
public commitUpdates(method?: RestRequestMethod): void {
|
||||||
|
this.patchData.commitUpdates(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a patch request for a specified object
|
||||||
|
* @param {T} object The object to send a patch request for
|
||||||
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
|
*/
|
||||||
|
public patch(object: Item, operations: Operation[]): Observable<RemoteData<Item>> {
|
||||||
|
return this.patchData.patch(object, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
|
* @param {DSpaceObject} object The given object
|
||||||
|
*/
|
||||||
|
public update(object: Item): Observable<RemoteData<Item>> {
|
||||||
|
return this.patchData.update(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of operations representing the difference between an object and its latest value in the cache.
|
||||||
|
* @param object the object to resolve to a list of patch operations
|
||||||
|
*/
|
||||||
|
public createPatchFromCache(object: Item): Observable<Operation[]> {
|
||||||
|
return this.patchData.createPatchFromCache(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param objectId The id of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
*/
|
||||||
|
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param href The self link of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
* Only emits once all request related to the DSO has been invalidated.
|
||||||
|
*/
|
||||||
|
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new object on the server, and store the response in the object cache
|
||||||
|
*
|
||||||
|
* @param object The object to create
|
||||||
|
* @param params Array with additional params to combine with query string
|
||||||
|
*/
|
||||||
|
public create(object: Item, ...params: RequestParam[]): Observable<RemoteData<Item>> {
|
||||||
|
return this.createData.create(object, ...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service for CRUD operations on Items
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(ITEM)
|
||||||
|
export class ItemDataService extends BaseItemDataService {
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Item>,
|
||||||
|
protected browseService: BrowseService,
|
||||||
|
protected bundleService: BundleDataService,
|
||||||
|
) {
|
||||||
|
super('items', requestService, rdbService, objectCache, halService, notificationsService, comparator, browseService, bundleService);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,7 @@ describe('ItemRequestDataService', () => {
|
|||||||
getEndpoint: observableOf(restApiEndpoint),
|
getEndpoint: observableOf(restApiEndpoint),
|
||||||
});
|
});
|
||||||
|
|
||||||
service = new ItemRequestDataService(requestService, rdbService, null, null, halService, null, null, null);
|
service = new ItemRequestDataService(requestService, rdbService, null, halService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('requestACopy', () => {
|
describe('requestACopy', () => {
|
||||||
|
@@ -9,40 +9,27 @@ import { PostRequest, PutRequest } from './request.models';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { ItemRequest } from '../shared/item-request.model';
|
import { ItemRequest } from '../shared/item-request.model';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
|
||||||
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
import { sendRequest } from '../shared/request.operators';
|
import { sendRequest } from '../shared/request.operators';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint
|
* A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint
|
||||||
*/
|
*/
|
||||||
@Injectable(
|
@Injectable({
|
||||||
{
|
providedIn: 'root',
|
||||||
providedIn: 'root',
|
})
|
||||||
}
|
export class ItemRequestDataService extends IdentifiableDataService<ItemRequest> {
|
||||||
)
|
|
||||||
export class ItemRequestDataService extends DataService<ItemRequest> {
|
|
||||||
|
|
||||||
protected linkPath = 'itemrequests';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DefaultChangeAnalyzer<ItemRequest>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super('itemrequests', requestService, rdbService, objectCache, halService);
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemRequestEndpoint(): Observable<string> {
|
getItemRequestEndpoint(): Observable<string> {
|
||||||
@@ -124,9 +111,9 @@ export class ItemRequestDataService extends DataService<ItemRequest> {
|
|||||||
suggestOpenAccess,
|
suggestOpenAccess,
|
||||||
}), options);
|
}), options);
|
||||||
}),
|
}),
|
||||||
sendRequest(this.requestService)).subscribe();
|
sendRequest(this.requestService),
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,17 +8,20 @@ import { BrowseService } from '../browse/browse.service';
|
|||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
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 { CollectionDataService } from './collection-data.service';
|
import { CollectionDataService } from './collection-data.service';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { RestRequest } from './rest-request.model';
|
import { RestRequest } from './rest-request.model';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { RequestEntry } from './request-entry.model';
|
import { RequestEntry } from './request-entry.model';
|
||||||
|
import { testCreateDataImplementation } from './base/create-data.spec';
|
||||||
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
import createSpyObj = jasmine.createSpyObj;
|
||||||
|
|
||||||
describe('ItemTemplateDataService', () => {
|
describe('ItemTemplateDataService', () => {
|
||||||
let service: ItemTemplateDataService;
|
let service: ItemTemplateDataService;
|
||||||
let itemService: any;
|
let byCollection: any;
|
||||||
|
|
||||||
const item = new Item();
|
const item = new Item();
|
||||||
const collectionEndpoint = 'https://rest.api/core/collections/4af28e99-6a9c-4036-a199-e1b587046d39';
|
const collectionEndpoint = 'https://rest.api/core/collections/4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||||
@@ -47,14 +50,14 @@ describe('ItemTemplateDataService', () => {
|
|||||||
} as RequestService;
|
} as RequestService;
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
const bs = {} as BrowseService;
|
const browseService = {} as BrowseService;
|
||||||
const objectCache = {
|
const objectCache = {
|
||||||
getObjectBySelfLink(self) {
|
getObjectBySelfLink(self) {
|
||||||
return observableOf({});
|
return observableOf({});
|
||||||
},
|
},
|
||||||
addPatch(self, operations) {
|
addPatch(self, operations) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
},
|
||||||
} as any;
|
} as any;
|
||||||
const halEndpointService = {
|
const halEndpointService = {
|
||||||
getEndpoint(linkPath: string): Observable<string> {
|
getEndpoint(linkPath: string): Observable<string> {
|
||||||
@@ -62,7 +65,6 @@ describe('ItemTemplateDataService', () => {
|
|||||||
}
|
}
|
||||||
} as HALEndpointService;
|
} as HALEndpointService;
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
|
||||||
const comparator = {
|
const comparator = {
|
||||||
diff(first, second) {
|
diff(first, second) {
|
||||||
return [{}];
|
return [{}];
|
||||||
@@ -78,60 +80,68 @@ describe('ItemTemplateDataService', () => {
|
|||||||
service = new ItemTemplateDataService(
|
service = new ItemTemplateDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
store,
|
|
||||||
bs,
|
|
||||||
objectCache,
|
objectCache,
|
||||||
halEndpointService,
|
halEndpointService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
http,
|
|
||||||
comparator,
|
comparator,
|
||||||
|
browseService,
|
||||||
undefined,
|
undefined,
|
||||||
collectionService
|
collectionService,
|
||||||
);
|
);
|
||||||
itemService = (service as any).dataService;
|
byCollection = (service as any).byCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initTestService();
|
initTestService();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('commitUpdates', () => {
|
describe('composition', () => {
|
||||||
it('should call commitUpdates on the item service implementation', () => {
|
const initService = () => new ItemTemplateDataService(null, null, null, null, null, null, null, null, null);
|
||||||
spyOn(itemService, 'commitUpdates');
|
testCreateDataImplementation(initService);
|
||||||
service.commitUpdates();
|
testPatchDataImplementation(initService);
|
||||||
expect(itemService.commitUpdates).toHaveBeenCalled();
|
testDeleteDataImplementation(initService);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should call update on the item service implementation', () => {
|
|
||||||
spyOn(itemService, 'update');
|
|
||||||
service.update(item);
|
|
||||||
expect(itemService.update).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findByCollectionID', () => {
|
describe('findByCollectionID', () => {
|
||||||
it('should call findByCollectionID on the item service implementation', () => {
|
it('should call findByCollectionID on the collection-based data service', () => {
|
||||||
spyOn(itemService, 'findByCollectionID');
|
spyOn(byCollection, 'findById');
|
||||||
service.findByCollectionID(scopeID);
|
service.findByCollectionID(scopeID);
|
||||||
expect(itemService.findByCollectionID).toHaveBeenCalled();
|
expect(byCollection.findById).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
describe('createByCollectionID', () => {
|
||||||
it('should call createTemplate on the item service implementation', () => {
|
it('should call createTemplate on the collection-based data service', () => {
|
||||||
spyOn(itemService, 'createTemplate');
|
spyOn(byCollection, 'createTemplate');
|
||||||
service.create(item, scopeID);
|
service.createByCollectionID(item, scopeID);
|
||||||
expect(itemService.createTemplate).toHaveBeenCalled();
|
expect(byCollection.createTemplate).toHaveBeenCalledWith(item, scopeID);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteByCollectionID', () => {
|
describe('byCollection', () => {
|
||||||
it('should call deleteByCollectionID on the item service implementation', () => {
|
beforeEach(() => {
|
||||||
spyOn(itemService, 'deleteByCollectionID');
|
byCollection.createData = createSpyObj('createData', {
|
||||||
service.deleteByCollectionID(item, scopeID);
|
createOnEndpoint: 'TEST createOnEndpoint',
|
||||||
expect(itemService.deleteByCollectionID).toHaveBeenCalled();
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getIDHrefObs', () => {
|
||||||
|
it('should point to the Item template of a given Collection', () => {
|
||||||
|
expect(byCollection.getIDHrefObs(scopeID)).toBeObservable(cold('a', { a: jasmine.stringMatching(`/collections/${scopeID}/itemtemplate`) }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createTemplate', () => {
|
||||||
|
it('should forward to CreateDataImpl.createOnEndpoint', () => {
|
||||||
|
spyOn(byCollection, 'getIDHrefObs').and.returnValue('TEST getIDHrefObs');
|
||||||
|
|
||||||
|
const out = byCollection.createTemplate(item, scopeID);
|
||||||
|
|
||||||
|
expect(byCollection.getIDHrefObs).toHaveBeenCalledWith(scopeID);
|
||||||
|
expect(byCollection.createData.createOnEndpoint).toHaveBeenCalledWith(item, 'TEST getIDHrefObs');
|
||||||
|
expect(out).toBe('TEST createOnEndpoint');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,147 +1,62 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ItemDataService } from './item-data.service';
|
import { BaseItemDataService } from './item-data.service';
|
||||||
import { UpdateDataService } from './update-data.service';
|
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
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 { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
import { CollectionDataService } from './collection-data.service';
|
import { CollectionDataService } from './collection-data.service';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { CreateDataImpl } from './base/create-data';
|
||||||
import { Operation } from 'fast-json-patch';
|
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
|
||||||
import { CoreState } from '../core-state.model';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom implementation of the ItemDataService, but for collection item templates
|
* Data service for interacting with Item templates via their Collection
|
||||||
* Makes sure to change the endpoint before sending out CRUD requests for the item template
|
|
||||||
*/
|
*/
|
||||||
class DataServiceImpl extends ItemDataService {
|
class CollectionItemTemplateDataService extends IdentifiableDataService<Item> {
|
||||||
protected collectionLinkPath = 'itemtemplate';
|
private createData: CreateDataImpl<Item>;
|
||||||
protected linkPath = 'itemtemplates';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Endpoint dynamically changing depending on what request we're sending
|
|
||||||
*/
|
|
||||||
private endpoint$: Observable<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the current endpoint based on a collection?
|
|
||||||
*/
|
|
||||||
private collectionEndpoint = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected bs: BrowseService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected collectionService: CollectionDataService,
|
||||||
protected comparator: DSOChangeAnalyzer<Item>,
|
) {
|
||||||
protected bundleService: BundleDataService,
|
super('itemtemplates', requestService, rdbService, objectCache, halService, undefined);
|
||||||
protected collectionService: CollectionDataService) {
|
|
||||||
super(requestService, rdbService, store, bs, objectCache, halService, notificationsService, http, comparator, bundleService);
|
// We only intend to use createOnEndpoint, so this inner data service feature doesn't need an endpoint at all
|
||||||
|
this.createData = new CreateDataImpl<Item>(undefined, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the endpoint based on a collection
|
* Create an observable for the HREF of a specific object based on its identifier
|
||||||
* @param collectionID The ID of the collection to base the endpoint on
|
*
|
||||||
|
* Overridden to ensure that {@link findById} works with Collection IDs and points to the template.
|
||||||
|
* @param collectionID the ID of a Collection
|
||||||
*/
|
*/
|
||||||
public getCollectionEndpoint(collectionID: string): Observable<string> {
|
public getIDHrefObs(collectionID: string): Observable<string> {
|
||||||
return this.collectionService.getIDHrefObs(collectionID).pipe(
|
return this.collectionService.getIDHrefObs(collectionID).pipe(
|
||||||
switchMap((href: string) => this.halService.getEndpoint(this.collectionLinkPath, href))
|
switchMap((href: string) => this.halService.getEndpoint('itemtemplate', href)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the endpoint to be based on a collection
|
* Create a new item template for a Collection by ID
|
||||||
* @param collectionID The ID of the collection to base the endpoint on
|
|
||||||
*/
|
|
||||||
private setCollectionEndpoint(collectionID: string) {
|
|
||||||
this.collectionEndpoint = true;
|
|
||||||
this.endpoint$ = this.getCollectionEndpoint(collectionID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the endpoint to the regular linkPath
|
|
||||||
*/
|
|
||||||
private setRegularEndpoint() {
|
|
||||||
this.collectionEndpoint = false;
|
|
||||||
this.endpoint$ = this.halService.getEndpoint(this.linkPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the base endpoint for all requests
|
|
||||||
* Uses the current collectionID to assemble a request endpoint for the collection's item template
|
|
||||||
*/
|
|
||||||
protected getEndpoint(): Observable<string> {
|
|
||||||
return this.endpoint$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the current endpoint is based on a collection, simply return the collection's template endpoint, otherwise
|
|
||||||
* create a regular template endpoint
|
|
||||||
* @param resourceID
|
|
||||||
*/
|
|
||||||
getIDHrefObs(resourceID: string): Observable<string> {
|
|
||||||
if (this.collectionEndpoint) {
|
|
||||||
return this.getEndpoint();
|
|
||||||
} else {
|
|
||||||
return super.getIDHrefObs(resourceID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the collection ID and send a find by ID request
|
|
||||||
* @param collectionID
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> {
|
|
||||||
this.setCollectionEndpoint(collectionID);
|
|
||||||
return super.findById(collectionID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the collection ID and send a create request
|
|
||||||
* @param item
|
* @param item
|
||||||
* @param collectionID
|
* @param collectionID
|
||||||
*/
|
*/
|
||||||
createTemplate(item: Item, collectionID: string): Observable<RemoteData<Item>> {
|
public createTemplate(item: Item, collectionID: string): Observable<RemoteData<Item>> {
|
||||||
this.setCollectionEndpoint(collectionID);
|
return this.createData.createOnEndpoint(item, this.getIDHrefObs(collectionID));
|
||||||
return super.create(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the collection ID and send a delete request
|
|
||||||
* @param item
|
|
||||||
* @param collectionID
|
|
||||||
*/
|
|
||||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
|
||||||
this.setRegularEndpoint();
|
|
||||||
return super.delete(item.uuid).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
map((response: RemoteData<NoContent>) => hasValue(response) && response.hasSucceeded)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,43 +64,23 @@ class DataServiceImpl extends ItemDataService {
|
|||||||
* A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplates endpoint
|
* A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplates endpoint
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemTemplateDataService implements UpdateDataService<Item> {
|
export class ItemTemplateDataService extends BaseItemDataService {
|
||||||
/**
|
private byCollection: CollectionItemTemplateDataService;
|
||||||
* The data service responsible for all CRUD actions on the item
|
|
||||||
*/
|
|
||||||
private dataService: DataServiceImpl;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected bs: BrowseService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: DSOChangeAnalyzer<Item>,
|
protected comparator: DSOChangeAnalyzer<Item>,
|
||||||
|
protected browseService: BrowseService,
|
||||||
protected bundleService: BundleDataService,
|
protected bundleService: BundleDataService,
|
||||||
protected collectionService: CollectionDataService) {
|
protected collectionService: CollectionDataService,
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, store, bs, objectCache, halService, notificationsService, http, comparator, bundleService, collectionService);
|
) {
|
||||||
}
|
super('itemtemplates', requestService, rdbService, objectCache, halService, notificationsService, comparator, browseService, bundleService);
|
||||||
|
|
||||||
/**
|
this.byCollection = new CollectionItemTemplateDataService(requestService, rdbService, objectCache, halService, notificationsService, collectionService);
|
||||||
* Commit current object changes to the server
|
|
||||||
*/
|
|
||||||
commitUpdates(method?: RestRequestMethod) {
|
|
||||||
this.dataService.commitUpdates(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new patch to the object cache
|
|
||||||
*/
|
|
||||||
update(object: Item): Observable<RemoteData<Item>> {
|
|
||||||
return this.dataService.update(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
patch(dso: Item, operations: Operation[]): Observable<RemoteData<Item>> {
|
|
||||||
return this.dataService.patch(dso, operations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,7 +94,7 @@ export class ItemTemplateDataService implements UpdateDataService<Item> {
|
|||||||
* {@link HALLink}s should be automatically resolved
|
* {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, 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, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.byCollection.findById(collectionID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,17 +102,8 @@ export class ItemTemplateDataService implements UpdateDataService<Item> {
|
|||||||
* @param item
|
* @param item
|
||||||
* @param collectionID
|
* @param collectionID
|
||||||
*/
|
*/
|
||||||
create(item: Item, collectionID: string): Observable<RemoteData<Item>> {
|
createByCollectionID(item: Item, collectionID: string): Observable<RemoteData<Item>> {
|
||||||
return this.dataService.createTemplate(item, collectionID);
|
return this.byCollection.createTemplate(item, collectionID);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a template item by collection ID
|
|
||||||
* @param item
|
|
||||||
* @param collectionID
|
|
||||||
*/
|
|
||||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
|
||||||
return this.dataService.deleteByCollectionID(item, collectionID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,6 +111,6 @@ export class ItemTemplateDataService implements UpdateDataService<Item> {
|
|||||||
* @param collectionID The ID of the collection to base the endpoint on
|
* @param collectionID The ID of the collection to base the endpoint on
|
||||||
*/
|
*/
|
||||||
getCollectionEndpoint(collectionID: string): Observable<string> {
|
getCollectionEndpoint(collectionID: string): Observable<string> {
|
||||||
return this.dataService.getCollectionEndpoint(collectionID);
|
return this.byCollection.getIDHrefObs(collectionID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { LookupRelationService } from './lookup-relation.service';
|
import { LookupRelationService } from './lookup-relation.service';
|
||||||
import { ExternalSourceService } from './external-source.service';
|
import { ExternalSourceDataService } from './external-source-data.service';
|
||||||
import { SearchService } from '../shared/search/search.service';
|
import { SearchService } from '../shared/search/search.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
@@ -16,7 +16,7 @@ import { of as observableOf } from 'rxjs';
|
|||||||
|
|
||||||
describe('LookupRelationService', () => {
|
describe('LookupRelationService', () => {
|
||||||
let service: LookupRelationService;
|
let service: LookupRelationService;
|
||||||
let externalSourceService: ExternalSourceService;
|
let externalSourceService: ExternalSourceDataService;
|
||||||
let searchService: SearchService;
|
let searchService: SearchService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user