Merge remote-tracking branch 'origin/main' into w2p-94060_Issue-1720_MyDSpace-support-entities

This commit is contained in:
lotte
2022-09-27 14:26:58 +02:00
448 changed files with 19938 additions and 6805 deletions

View File

@@ -156,9 +156,15 @@ languages:
- code: tr
label: Türkçe
active: true
- code: kk
label: Қазақ
active: true
- code: bn
label: বাংলা
active: true
- code: el
label: Ελληνικά
active: true
# Browse-By Pages
browseBy:
@@ -168,6 +174,27 @@ browseBy:
fiveYearLimit: 30
# The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
defaultLowerLimit: 1900
# If true, thumbnail images for items will be added to BOTH search and browse result lists.
showThumbnails: true
# The number of entries in a paginated browse results list.
# Rounded to the nearest size in the list of selectable sizes on the
# settings menu.
pageSize: 20
communityList:
# No. of communities to list per expansion (show more)
pageSize: 20
homePage:
recentSubmissions:
# The number of item showing in recent submission components
pageSize: 5
# Sort record of recent submission
sortField: 'dc.date.accessioned'
topLevelCommunityList:
# No. of communities to list per page on the home page
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
pageSize: 5
# Item Config
item:
@@ -243,7 +270,7 @@ themes:
# The default bundles that should always be displayed as suggestions when you upload a new bundle
bundle:
- standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
# For images, this enables a gallery viewer where you can zoom or page through images.

1
cypress/.gitignore vendored
View File

@@ -1,2 +1,3 @@
screenshots/
videos/
downloads/

View File

@@ -1,6 +1,6 @@
{
"name": "dspace-angular",
"version": "0.0.0",
"version": "7.4.0-next",
"scripts": {
"ng": "ng",
"config:watch": "nodemon",
@@ -10,7 +10,7 @@
"start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr",
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
"preserve": "yarn base-href",
"serve": "ng serve --configuration development",
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
"serve:ssr": "node dist/server/main",
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
"build": "ng build --configuration development",

View File

@@ -10,6 +10,6 @@ const appConfig: AppConfig = buildAppConfig();
* Any CLI arguments given to this script are patched through to `ng serve` as well.
*/
child.spawn(
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')}`,
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`,
{ stdio: 'inherit', shell: true }
);

View File

@@ -22,7 +22,13 @@ console.log(`...Testing connection to REST API at ${restUrl}...\n`);
if (appConfig.rest.ssl) {
const req = https.request(restUrl, (res) => {
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);
});
});
@@ -35,7 +41,13 @@ if (appConfig.rest.ssl) {
} else {
const req = http.request(restUrl, (res) => {
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);
});
});

View File

@@ -48,6 +48,7 @@ import { ServerAppModule } from './src/main.server';
import { buildAppConfig } from './src/config/config.server';
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
import { logStartupMessage } from './startup-message';
/*
* Set path for the browser application's dist folder
@@ -281,6 +282,8 @@ function run() {
}
function start() {
logStartupMessage(environment);
/*
* If SSL is enabled
* - Read credentials from configuration files

View File

@@ -177,7 +177,7 @@ describe('EPersonFormComponent', () => {
});
groupsDataService = jasmine.createSpyObj('groupsDataService', {
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: ''
});

View File

@@ -265,7 +265,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
if (eperson != null) {
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, {
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, {
currentPage: 1,
elementsPerPage: this.config.pageSize
});
@@ -297,7 +297,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
}),
switchMap(([eperson, findListOptions]) => {
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);
})
@@ -554,7 +554,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/
private updateGroups(options) {
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);
}));
}
}

View File

@@ -53,7 +53,7 @@ describe('MembersListComponent', () => {
activeGroup: activeGroup,
epersonMembers: epersonMembers,
subgroupMembers: subgroupMembers,
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
},
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {

View File

@@ -10,7 +10,7 @@ import {
combineLatest as observableCombineLatest,
ObservedValueOf,
} from 'rxjs';
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
@@ -129,7 +129,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
this.subs.set(SubKey.MembersDTO,
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
switchMap((currentPagination) => {
return this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
currentPage: currentPagination.currentPage,
elementsPerPage: currentPagination.pageSize
}
@@ -144,7 +144,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -153,8 +153,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
return epersonDtoModel;
});
return dto$;
}));
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}))
@@ -171,10 +171,10 @@ export class MembersListComponent implements OnInit, OnDestroy {
return this.groupDataService.getActiveGroup().pipe(take(1),
mergeMap((group: Group) => {
if (group != null) {
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
currentPage: 1,
elementsPerPage: 9999
}, false)
})
.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
@@ -274,7 +274,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -283,8 +283,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
return epersonDtoModel;
});
return dto$;
}));
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}))

View File

@@ -65,7 +65,7 @@ describe('SubgroupsListComponent', () => {
getSubgroups(): Group {
return this.activeGroup;
},
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
return this.subgroups$.pipe(
map((currentGroups: Group[]) => {
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));

View File

@@ -115,7 +115,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
this.subs.set(
SubKey.Members,
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,
elementsPerPage: config.pageSize
},
@@ -139,7 +139,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
if (activeGroup.uuid === possibleSubgroup.uuid) {
return observableOf(false);
} else {
return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, {
currentPage: 1,
elementsPerPage: 9999
})

View File

@@ -69,7 +69,7 @@ describe('GroupRegistryComponent', () => {
mockGroups = [GroupMock, GroupMock2];
mockEPeople = [EPersonMock, EPersonMock2];
ePersonDataServiceStub = {
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
switch (href) {
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
@@ -97,7 +97,7 @@ describe('GroupRegistryComponent', () => {
};
groupsDataServiceStub = {
allGroups: mockGroups,
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
switch (href) {
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({

View File

@@ -213,7 +213,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
* @param group
*/
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
*/
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());
}
/**

View File

@@ -14,6 +14,14 @@ import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { AuthService } from '../../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
import { FileService } from '../../../../../core/shared/file.service';
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
describe('CollectionAdminSearchResultGridElementComponent', () => {
let component: CollectionAdminSearchResultGridElementComponent;
@@ -45,7 +53,11 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} },
{ provide: LinkService, useValue: linkService }
{ provide: LinkService, useValue: linkService },
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: FileService, useClass: FileServiceStub },
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
{ provide: ThemeService, useValue: getMockThemeService() },
]
})
.compileComponents();

View File

@@ -16,6 +16,14 @@ import { CommunitySearchResult } from '../../../../../shared/object-collection/s
import { Community } from '../../../../../core/shared/community.model';
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { AuthService } from '../../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
import { FileService } from '../../../../../core/shared/file.service';
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
describe('CommunityAdminSearchResultGridElementComponent', () => {
let component: CommunityAdminSearchResultGridElementComponent;
@@ -47,7 +55,11 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} },
{ provide: LinkService, useValue: linkService }
{ provide: LinkService, useValue: linkService },
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: FileService, useClass: FileServiceStub },
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
{ provide: ThemeService, useValue: getMockThemeService() },
],
schemas: [NO_ERRORS_SCHEMA]
})

View File

@@ -20,6 +20,12 @@ import { getMockThemeService } from '../../../../../shared/mocks/theme-service.m
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
import { AuthService } from '../../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
import { FileService } from '../../../../../core/shared/file.service';
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
describe('ItemAdminSearchResultGridElementComponent', () => {
let component: ItemAdminSearchResultGridElementComponent;
@@ -64,6 +70,9 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: ThemeService, useValue: mockThemeService },
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: FileService, useClass: FileServiceStub },
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
})

View File

@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('CollectionAdminSearchResultListElementComponent', () => {
let component: CollectionAdminSearchResultListElementComponent;
@@ -36,7 +38,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
],
declarations: [CollectionAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }],
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();

View File

@@ -13,6 +13,8 @@ import { Community } from '../../../../../core/shared/community.model';
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('CommunityAdminSearchResultListElementComponent', () => {
let component: CommunityAdminSearchResultListElementComponent;
@@ -36,7 +38,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
],
declarations: [CommunityAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }],
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();

View File

@@ -10,6 +10,8 @@ import { ItemAdminSearchResultListElementComponent } from './item-admin-search-r
import { Item } from '../../../../../core/shared/item.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('ItemAdminSearchResultListElementComponent', () => {
let component: ItemAdminSearchResultListElementComponent;
@@ -33,7 +35,8 @@ describe('ItemAdminSearchResultListElementComponent', () => {
],
declarations: [ItemAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }],
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();

View File

@@ -18,6 +18,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
@@ -51,7 +53,8 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => {
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: LinkService, useValue: linkService },
{ provide: DSONameService, useClass: DSONameServiceMock }
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }
],
schemas: [NO_ERRORS_SCHEMA]
})

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, Inject, OnInit } from '@angular/core';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
@@ -13,6 +13,7 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
@Component({
@@ -32,9 +33,10 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
constructor(private linkService: LinkService,
protected truncatableService: TruncatableService,
protected dsoNameService: DSONameService
protected dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) {
super(truncatableService, dsoNameService);
super(truncatableService, dsoNameService, appConfig);
}
/**

View File

@@ -18,11 +18,10 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
describe('BrowseByDatePageComponent', () => {
let comp: BrowseByDatePageComponent;
@@ -83,7 +82,8 @@ describe('BrowseByDatePageComponent', () => {
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
{ provide: Router, useValue: new RouterMock() },
{ provide: PaginationService, useValue: paginationService },
{ provide: ChangeDetectorRef, useValue: mockCdRef }
{ provide: ChangeDetectorRef, useValue: mockCdRef },
{ provide: APP_CONFIG, useValue: environment }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -1,9 +1,8 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions
browseParamsToOptions, getBrowseSearchOptions
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model';
@@ -13,12 +12,12 @@ import { BrowseService } from '../../core/browse/browse.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { environment } from '../../../environments/environment';
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { isValidDate } from '../../shared/date.util';
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
@Component({
selector: 'ds-browse-by-date-page',
@@ -43,14 +42,16 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
protected dsoService: DSpaceObjectDataService,
protected router: Router,
protected paginationService: PaginationService,
protected cdRef: ChangeDetectorRef) {
super(route, browseService, dsoService, paginationService, router);
protected cdRef: ChangeDetectorRef,
@Inject(APP_CONFIG) public appConfig: AppConfig) {
super(route, browseService, dsoService, paginationService, router, appConfig);
}
ngOnInit(): void {
const sortConfig = new SortOptions('default', SortDirection.ASC);
this.startsWithType = StartsWithType.date;
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
// include the thumbnail configuration in browse search options
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
@@ -63,7 +64,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
this.browseId = params.id || this.defaultBrowseId;
this.startsWith = +params.startsWith || params.startsWith;
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
this.updatePageWithItems(searchOptions, this.value, undefined);
this.updateParent(params.scope);
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
@@ -83,7 +84,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
this.subs.push(
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
let lowerLimit = environment.browseBy.defaultLowerLimit;
let lowerLimit = this.appConfig.browseBy.defaultLowerLimit;
if (hasValue(firstItemRD.payload)) {
const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
if (isNotEmpty(date) && isValidDate(date)) {
@@ -94,8 +95,8 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
}
const options = [];
const currentYear = new Date().getUTCFullYear();
const oneYearBreak = Math.floor((currentYear - environment.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak = Math.floor((currentYear - environment.browseBy.fiveYearLimit) / 10) * 10;
const oneYearBreak = Math.floor((currentYear - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak = Math.floor((currentYear - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
if (lowerLimit <= fiveYearBreak) {
lowerLimit -= 10;
} else if (lowerLimit <= oneYearBreak) {

View File

@@ -1,4 +1,8 @@
import { BrowseByMetadataPageComponent, browseParamsToOptions } from './browse-by-metadata-page.component';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions,
getBrowseSearchOptions
} from './browse-by-metadata-page.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowseService } from '../../core/browse/browse.service';
import { CommonModule } from '@angular/common';
@@ -14,7 +18,7 @@ import { RemoteData } from '../../core/data/remote-data';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { SortDirection } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { Community } from '../../core/shared/community.model';
@@ -25,6 +29,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { APP_CONFIG } from '../../../config/app-config.interface';
describe('BrowseByMetadataPageComponent', () => {
let comp: BrowseByMetadataPageComponent;
@@ -43,6 +48,13 @@ describe('BrowseByMetadataPageComponent', () => {
]
});
const environmentMock = {
browseBy: {
showThumbnails: true,
pageSize: 10
}
};
const mockEntries = [
{
type: BrowseEntry.type,
@@ -97,7 +109,8 @@ describe('BrowseByMetadataPageComponent', () => {
{ provide: BrowseService, useValue: mockBrowseService },
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
{ provide: PaginationService, useValue: paginationService },
{ provide: Router, useValue: new RouterMock() }
{ provide: Router, useValue: new RouterMock() },
{ provide: APP_CONFIG, useValue: environmentMock }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -118,6 +131,10 @@ describe('BrowseByMetadataPageComponent', () => {
expect(comp.items$).toBeUndefined();
});
it('should set embed thumbnail property to true', () => {
expect(comp.fetchThumbnails).toBeTrue();
});
describe('when a value is provided', () => {
beforeEach(() => {
const paramsWithValue = {
@@ -145,14 +162,14 @@ describe('BrowseByMetadataPageComponent', () => {
};
const paginationOptions = Object.assign(new PaginationComponentOptions(), {
currentPage: 5,
pageSize: 10,
pageSize: comp.appConfig.browseBy.pageSize,
});
const sortOptions = {
direction: SortDirection.ASC,
field: 'fake-field',
};
result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author');
result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author', comp.fetchThumbnails);
});
it('should return BrowseEntrySearchOptions with the correct properties', () => {
@@ -163,6 +180,36 @@ describe('BrowseByMetadataPageComponent', () => {
expect(result.sort.direction).toEqual(SortDirection.ASC);
expect(result.sort.field).toEqual('fake-field');
expect(result.scope).toEqual('fake-scope');
expect(result.fetchThumbnail).toBeTrue();
});
});
describe('calling getBrowseSearchOptions', () => {
let result: BrowseEntrySearchOptions;
beforeEach(() => {
const paramsScope = {
scope: 'fake-scope'
};
const paginationOptions = Object.assign(new PaginationComponentOptions(), {
currentPage: 5,
pageSize: comp.appConfig.browseBy.pageSize,
});
const sortOptions = {
direction: SortDirection.ASC,
field: 'fake-field',
};
result = getBrowseSearchOptions('title', paginationOptions, sortOptions, comp.fetchThumbnails);
});
it('should return BrowseEntrySearchOptions with the correct properties', () => {
expect(result.metadataDefinition).toEqual('title');
expect(result.pagination.currentPage).toEqual(5);
expect(result.pagination.pageSize).toEqual(10);
expect(result.sort.direction).toEqual(SortDirection.ASC);
expect(result.sort.field).toEqual('fake-field');
expect(result.fetchThumbnail).toBeTrue();
});
});
});

View File

@@ -1,5 +1,5 @@
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { Component, Inject, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
@@ -17,6 +17,7 @@ import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
export const BBM_PAGINATION_ID = 'bbm';
@@ -26,9 +27,10 @@ export const BBM_PAGINATION_ID = 'bbm';
templateUrl: './browse-by-metadata-page.component.html'
})
/**
* Component for browsing (items) by metadata definition
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields.
* An example would be 'author' for 'dc.contributor.*'
* Component for browsing (items) by metadata definition.
* A metadata definition (a.k.a. browse id) is a short term used to describe one
* or multiple metadata fields. An example would be 'author' for
* 'dc.contributor.*'
*/
@rendersBrowseBy(BrowseByDataType.Metadata)
export class BrowseByMetadataPageComponent implements OnInit {
@@ -51,11 +53,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
/**
* The pagination config used to display the values
*/
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: BBM_PAGINATION_ID,
currentPage: 1,
pageSize: 20
});
paginationConfig: PaginationComponentOptions;
/**
* The pagination observable
@@ -111,16 +109,31 @@ export class BrowseByMetadataPageComponent implements OnInit {
*/
startsWith: string;
/**
* Determines whether to request embedded thumbnail.
*/
fetchThumbnails: boolean;
public constructor(protected route: ActivatedRoute,
protected browseService: BrowseService,
protected dsoService: DSpaceObjectDataService,
protected paginationService: PaginationService,
protected router: Router) {
protected router: Router,
@Inject(APP_CONFIG) public appConfig: AppConfig) {
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
id: BBM_PAGINATION_ID,
currentPage: 1,
pageSize: this.appConfig.browseBy.pageSize,
});
}
ngOnInit(): void {
const sortConfig = new SortOptions('default', SortDirection.ASC);
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
@@ -133,15 +146,16 @@ export class BrowseByMetadataPageComponent implements OnInit {
this.authority = params.authority;
this.value = +params.value || params.value || '';
this.startsWith = +params.startsWith || params.startsWith;
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value, this.authority);
this.updatePageWithItems(
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
} else {
this.updatePage(searchOptions);
this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
}
this.updateParent(params.scope);
}));
this.updateStartsWithTextOptions();
}
/**
@@ -228,22 +242,44 @@ export class BrowseByMetadataPageComponent implements OnInit {
}
/**
* Creates browse entry search options.
* @param defaultBrowseId the metadata definition to fetch entries or items for
* @param paginationConfig the required pagination configuration
* @param sortConfig the required sort configuration
* @param fetchThumbnails optional boolean for fetching thumbnails
* @returns BrowseEntrySearchOptions instance
*/
export function getBrowseSearchOptions(defaultBrowseId: string,
paginationConfig: PaginationComponentOptions,
sortConfig: SortOptions,
fetchThumbnails?: boolean) {
if (!hasValue(fetchThumbnails)) {
fetchThumbnails = false;
}
return new BrowseEntrySearchOptions(defaultBrowseId, paginationConfig, sortConfig, null,
null, fetchThumbnails);
}
/**
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items
* @param params URL and query parameters
* @param paginationConfig Pagination configuration
* @param sortConfig Sorting configuration
* @param metadata Optional metadata definition to fetch browse entries/items for
* @param fetchThumbnail Optional parameter for requesting thumbnail images
*/
export function browseParamsToOptions(params: any,
paginationConfig: PaginationComponentOptions,
sortConfig: SortOptions,
metadata?: string): BrowseEntrySearchOptions {
metadata?: string,
fetchThumbnail?: boolean): BrowseEntrySearchOptions {
return new BrowseEntrySearchOptions(
metadata,
paginationConfig,
sortConfig,
+params.startsWith || params.startsWith,
params.scope
params.scope,
fetchThumbnail
);
}

View File

@@ -18,11 +18,11 @@ import { BrowseService } from '../../core/browse/browse.service';
import { RouterMock } from '../../shared/mocks/router.mock';
import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
describe('BrowseByTitlePageComponent', () => {
let comp: BrowseByTitlePageComponent;
@@ -77,7 +77,8 @@ describe('BrowseByTitlePageComponent', () => {
{ provide: BrowseService, useValue: mockBrowseService },
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
{ provide: PaginationService, useValue: paginationService },
{ provide: Router, useValue: new RouterMock() }
{ provide: Router, useValue: new RouterMock() },
{ provide: APP_CONFIG, useValue: environment }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -1,12 +1,11 @@
import { combineLatest as observableCombineLatest } from 'rxjs';
import { Component } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions
browseParamsToOptions, getBrowseSearchOptions
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { BrowseService } from '../../core/browse/browse.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
@@ -14,6 +13,7 @@ import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
@Component({
selector: 'ds-browse-by-title-page',
@@ -30,13 +30,15 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
protected browseService: BrowseService,
protected dsoService: DSpaceObjectDataService,
protected paginationService: PaginationService,
protected router: Router) {
super(route, browseService, dsoService, paginationService, router);
protected router: Router,
@Inject(APP_CONFIG) public appConfig: AppConfig) {
super(route, browseService, dsoService, paginationService, router, appConfig);
}
ngOnInit(): void {
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
// include the thumbnail configuration in browse search options
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
@@ -47,7 +49,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
this.startsWith = +params.startsWith || params.startsWith;
this.browseId = params.id || this.defaultBrowseId;
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined);
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
this.updateParent(params.scope);
}));
this.updateStartsWithTextOptions();

View File

@@ -16,7 +16,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.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 { MetadataValue } from '../../core/shared/metadata.models';
import { getFirstSucceededRemoteListPayload } from '../../core/shared/operators';
@@ -61,7 +61,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
protected dsoService: CommunityDataService,
protected requestService: RequestService,
protected objectCache: ObjectCacheService,
protected entityTypeService: EntityTypeService) {
protected entityTypeService: EntityTypeDataService) {
super(formService, translate, notificationsService, authService, requestService, objectCache);
}

View File

@@ -94,7 +94,7 @@ describe('CollectionItemMapperComponent', () => {
const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([]));
const itemDataServiceStub = {
mapToCollection: () => createSuccessfulRemoteDataObject$({}),
findAllByHref: () => observableOf(emptyList)
findListByHref: () => observableOf(emptyList),
};
const activatedRouteStub = {
parent: {
@@ -152,7 +152,7 @@ describe('CollectionItemMapperComponent', () => {
});
const groupDataService = jasmine.createSpyObj('groupsDataService', {
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: '',
getUUIDFromString: '',
});

View File

@@ -143,7 +143,7 @@ export class CollectionItemMapperComponent implements OnInit {
if (shouldUpdate === true) {
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
}),!shouldUpdate, false, followLink('owningCollection')).pipe(
getAllSucceededRemoteData()

View File

@@ -28,6 +28,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCollectionPageRoute } from './collection-page-routing-paths';
import { redirectOn4xx } from '../core/shared/authorized.operators';
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
@Component({
selector: 'ds-collection-page',
@@ -74,6 +75,7 @@ export class CollectionPageComponent implements OnInit {
this.paginationConfig.pageSize = 5;
this.paginationConfig.currentPage = 1;
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
}
ngOnInit(): void {
@@ -102,13 +104,14 @@ export class CollectionPageComponent implements OnInit {
getFirstSucceededRemoteData(),
map((rd) => rd.payload.id),
switchMap((id: string) => {
return this.searchService.search(
return this.searchService.search<Item>(
new PaginatedSearchOptions({
scope: id,
pagination: currentPagination,
sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM]
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
}), null, true, true, ...BROWSE_LINKS_TO_FOLLOW)
.pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
}),
startWith(undefined) // Make sure switching pages shows loading component
)

View File

@@ -17,7 +17,7 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
followLink('parentCommunity', {},
followLink('parentCommunity')
),
followLink('logo')
followLink('logo'),
];
/**

View File

@@ -13,7 +13,7 @@ import { Item } from '../../../core/shared/item.model';
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
import { Collection } from '../../../core/shared/collection.model';
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';
describe('CollectionMetadataComponent', () => {
@@ -39,8 +39,8 @@ describe('CollectionMetadataComponent', () => {
const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', {
findByCollectionID: createSuccessfulRemoteDataObject$(template),
create: createSuccessfulRemoteDataObject$(template),
deleteByCollectionID: observableOf(true),
createByCollectionID: createSuccessfulRemoteDataObject$(template),
delete: observableOf(true),
getCollectionEndpoint: observableOf(collectionTemplateHref),
});
@@ -91,12 +91,12 @@ describe('CollectionMetadataComponent', () => {
describe('deleteItemTemplate', () => {
beforeEach(() => {
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true));
(itemTemplateService.delete as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({}));
comp.deleteItemTemplate();
});
it('should call ItemTemplateService.deleteByCollectionID', () => {
expect(itemTemplateService.deleteByCollectionID).toHaveBeenCalledWith(template, 'collection-id');
it('should call ItemTemplateService.delete', () => {
expect(itemTemplateService.delete).toHaveBeenCalledWith(template.uuid);
});
describe('when delete returns a success', () => {
@@ -107,7 +107,7 @@ describe('CollectionMetadataComponent', () => {
describe('when delete returns a failure', () => {
beforeEach(() => {
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(false));
(itemTemplateService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
comp.deleteItemTemplate();
});

View File

@@ -7,12 +7,14 @@ import { ItemTemplateDataService } from '../../../core/data/item-template-data.s
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { switchMap } from 'rxjs/operators';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { map, switchMap } from 'rxjs/operators';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { RequestService } from '../../../core/data/request.service';
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
@@ -65,7 +67,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
getFirstSucceededRemoteDataPayload(),
);
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(),
)),
);
@@ -83,18 +85,15 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
* Delete the item template from the collection
*/
deleteItemTemplate() {
const collection$ = this.dsoRD$.pipe(
this.dsoRD$.pipe(
getFirstSucceededRemoteDataPayload(),
);
const template$ = collection$.pipe(
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid).pipe(
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)),
getFirstSucceededRemoteDataPayload(),
)),
);
combineLatestObservable(collection$, template$).pipe(
switchMap(([collection, template]) => {
return this.itemTemplateService.deleteByCollectionID(template, collection.uuid);
})
switchMap((template) => {
return this.itemTemplateService.delete(template.uuid);
}),
getFirstCompletedRemoteData(),
map((response: RemoteData<NoContent>) => hasValue(response) && response.hasSucceeded),
).subscribe((success: boolean) => {
if (success) {
this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success'));

View File

@@ -15,6 +15,8 @@ import { Collection } from '../core/shared/collection.model';
import { PageInfo } from '../core/shared/page-info.model';
import { FlatNode } from './flat-node.model';
import { FindListOptions } from '../core/data/find-list-options.model';
import { APP_CONFIG } from 'src/config/app-config.interface';
import { environment } from 'src/environments/environment.test';
describe('CommunityListService', () => {
let store: StoreMock<AppState>;
@@ -191,13 +193,14 @@ describe('CommunityListService', () => {
};
TestBed.configureTestingModule({
providers: [CommunityListService,
{ provide: APP_CONFIG, useValue: environment },
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: Store, useValue: StoreMock },
],
});
store = TestBed.inject(Store as any);
service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store);
service = new CommunityListService(environment, communityDataServiceStub, collectionDataServiceStub, store);
});
it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => {

View File

@@ -1,5 +1,5 @@
/* eslint-disable max-classes-per-file */
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { createSelector, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
@@ -23,6 +23,7 @@ import { followLink } from '../shared/utils/follow-link-config.model';
import { FlatNode } from './flat-node.model';
import { ShowMoreFlatNode } from './show-more-flat-node.model';
import { FindListOptions } from '../core/data/find-list-options.model';
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
// Helper method to combine an flatten an array of observables of flatNode arrays
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
@@ -80,8 +81,6 @@ const communityListStateSelector = (state: AppState) => state.communityList;
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
export const MAX_COMCOLS_PER_PAGE = 20;
/**
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
* and connection to the store to retrieve and save the state of the community list
@@ -89,8 +88,15 @@ export const MAX_COMCOLS_PER_PAGE = 20;
@Injectable()
export class CommunityListService {
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
private store: Store<any>) {
private pageSize: number;
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
private communityDataService: CommunityDataService,
private collectionDataService: CollectionDataService,
private store: Store<any>
) {
this.pageSize = appConfig.communityList.pageSize;
}
private configOnePage: FindListOptions = Object.assign(new FindListOptions(), {
@@ -145,7 +151,7 @@ export class CommunityListService {
private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
return this.communityDataService.findTop({
currentPage: options.currentPage,
elementsPerPage: MAX_COMCOLS_PER_PAGE,
elementsPerPage: this.pageSize,
sort: {
field: options.sort.field,
direction: options.sort.direction
@@ -216,7 +222,7 @@ export class CommunityListService {
let subcoms = [];
for (let i = 1; i <= currentCommunityPage; i++) {
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
elementsPerPage: MAX_COMCOLS_PER_PAGE,
elementsPerPage: this.pageSize,
currentPage: i
},
followLink('subcommunities', { findListOptions: this.configOnePage }),
@@ -241,7 +247,7 @@ export class CommunityListService {
let collections = [];
for (let i = 1; i <= currentCollectionPage; i++) {
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
elementsPerPage: MAX_COMCOLS_PER_PAGE,
elementsPerPage: this.pageSize,
currentPage: i
})
.pipe(

View File

@@ -25,12 +25,13 @@
</div>
</div>
<section class="comcol-page-browse-section">
<!-- Browse-By Links -->
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
</ds-themed-comcol-page-browse-by>
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
<ds-themed-community-page-sub-community-list [community]="communityPayload"></ds-themed-community-page-sub-community-list>
<ds-themed-community-page-sub-collection-list [community]="communityPayload"></ds-themed-community-page-sub-collection-list>
</section>
<footer *ngIf="communityPayload.copyrightText" class="border-top my-5 pt-4">
<!-- Copyright -->

View File

@@ -13,10 +13,18 @@ import { StatisticsModule } from '../statistics/statistics.module';
import { CommunityFormModule } from './community-form/community-form.module';
import { ThemedCommunityPageComponent } from './themed-community-page.component';
import { ComcolModule } from '../shared/comcol/comcol.module';
import {
ThemedCommunityPageSubCommunityListComponent
} from './sub-community-list/themed-community-page-sub-community-list.component';
import {
ThemedCollectionPageSubCollectionListComponent
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
const DECLARATIONS = [CommunityPageComponent,
ThemedCommunityPageComponent,
ThemedCommunityPageSubCommunityListComponent,
CommunityPageSubCollectionListComponent,
ThemedCollectionPageSubCollectionListComponent,
CommunityPageSubCommunityListComponent,
CreateCommunityPageComponent,
DeleteCommunityPageComponent];

View File

@@ -135,7 +135,7 @@ describe('CommunityPageSubCollectionList Component', () => {
});
const groupDataService = jasmine.createSpyObj('groupsDataService', {
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: '',
getUUIDFromString: '',
});

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
@@ -12,6 +12,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
import { CollectionDataService } from '../../core/data/collection-data.service';
import { PaginationService } from '../../core/pagination/pagination.service';
import { switchMap } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
@Component({
selector: 'ds-community-page-sub-collection-list',
@@ -19,9 +20,15 @@ import { switchMap } from 'rxjs/operators';
templateUrl: './community-page-sub-collection-list.component.html',
animations:[fadeIn]
})
export class CommunityPageSubCollectionListComponent implements OnInit {
export class CommunityPageSubCollectionListComponent implements OnInit, OnDestroy {
@Input() community: Community;
/**
* Optional page size. Overrides communityList.pageSize configuration for this component.
* Value can be added in the themed version of the parent component.
*/
@Input() pageSize: number;
/**
* The pagination configuration
*/
@@ -50,7 +57,9 @@ export class CommunityPageSubCollectionListComponent implements OnInit {
ngOnInit(): void {
this.config = new PaginationComponentOptions();
this.config.id = this.pageId;
this.config.pageSize = 5;
if (hasValue(this.pageSize)) {
this.config.pageSize = this.pageSize;
}
this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.initPage();

View File

@@ -0,0 +1,28 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component';
import { Component, Input } from '@angular/core';
import { Community } from '../../core/shared/community.model';
@Component({
selector: 'ds-themed-community-page-sub-collection-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent<CommunityPageSubCollectionListComponent> {
@Input() community: Community;
@Input() pageSize: number;
protected inAndOutputNames: (keyof CommunityPageSubCollectionListComponent & keyof this)[] = ['community', 'pageSize'];
protected getComponentName(): string {
return 'CommunityPageSubCollectionListComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-page/sub-collection-list/community-page-sub-collection-list.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./community-page-sub-collection-list.component`);
}
}

View File

@@ -131,7 +131,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
});
const groupDataService = jasmine.createSpyObj('groupsDataService', {
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: '',
getUUIDFromString: '',
});

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
@@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { takeUntilCompletedRemoteData } from '../../core/shared/operators';
import { switchMap } from 'rxjs/operators';
import { PaginationService } from '../../core/pagination/pagination.service';
import { hasValue } from '../../shared/empty.util';
@Component({
selector: 'ds-community-page-sub-community-list',
@@ -22,9 +23,15 @@ import { PaginationService } from '../../core/pagination/pagination.service';
/**
* Component to render the sub-communities of a Community
*/
export class CommunityPageSubCommunityListComponent implements OnInit {
export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy {
@Input() community: Community;
/**
* Optional page size. Overrides communityList.pageSize configuration for this component.
* Value can be added in the themed version of the parent component.
*/
@Input() pageSize: number;
/**
* The pagination configuration
*/
@@ -53,7 +60,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit {
ngOnInit(): void {
this.config = new PaginationComponentOptions();
this.config.id = this.pageId;
this.config.pageSize = 5;
if (hasValue(this.pageSize)) {
this.config.pageSize = this.pageSize;
}
this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.initPage();

View File

@@ -0,0 +1,29 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component';
import { Component, Input } from '@angular/core';
import { Community } from '../../core/shared/community.model';
@Component({
selector: 'ds-themed-community-page-sub-community-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent<CommunityPageSubCommunityListComponent> {
@Input() community: Community;
@Input() pageSize: number;
protected inAndOutputNames: (keyof CommunityPageSubCommunityListComponent & keyof this)[] = ['community', 'pageSize'];
protected getComponentName(): string {
return 'CommunityPageSubCommunityListComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-page/sub-community-list/community-page-sub-community-list.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./community-page-sub-community-list.component`);
}
}

View File

@@ -111,7 +111,6 @@ describe(`AuthRequestService`, () => {
body: undefined,
options,
}));
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
});
});
});
@@ -151,7 +150,6 @@ describe(`AuthRequestService`, () => {
body: { content: 'something' },
options,
}));
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
});
});
});

View File

@@ -58,7 +58,9 @@ export abstract class AuthRequestService {
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
const requestId = this.requestService.generateRequestId();
this.halService.getEndpoint(this.linkName).pipe(
const endpoint$ = this.halService.getEndpoint(this.linkName);
endpoint$.pipe(
filter((href: string) => isNotEmpty(href)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
distinctUntilChanged(),
@@ -68,7 +70,9 @@ export abstract class AuthRequestService {
this.requestService.send(request);
});
return this.fetchRequest(requestId);
return endpoint$.pipe(
switchMap(() => this.fetchRequest(requestId)),
);
}
/**
@@ -79,7 +83,9 @@ export abstract class AuthRequestService {
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
const requestId = this.requestService.generateRequestId();
this.halService.getEndpoint(this.linkName).pipe(
const endpoint$ = this.halService.getEndpoint(this.linkName);
endpoint$.pipe(
filter((href: string) => isNotEmpty(href)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
distinctUntilChanged(),
@@ -89,7 +95,9 @@ export abstract class AuthRequestService {
this.requestService.send(request);
});
return this.fetchRequest(requestId, ...linksToFollow);
return endpoint$.pipe(
switchMap(() => this.fetchRequest(requestId, ...linksToFollow)),
);
}
/**
* Factory function to create the request object to send. This needs to be a POST client side and

View File

@@ -2,23 +2,26 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
import { DataService } from '../data/data.service';
import { getRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators';
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { DSpaceObject } from '../shared/dspace-object.model';
import { ChildHALResource } from '../shared/child-hal-resource.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { hasValue } from '../../shared/empty.util';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
/**
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
*/
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
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>,
) {
}
/**

View File

@@ -31,6 +31,8 @@ export class DSONameService {
const givenName = dso.firstMetadataValue('person.givenName');
if (isEmpty(familyName) && isEmpty(givenName)) {
return dso.firstMetadataValue('dc.title') || dso.name;
} else if (isEmpty(familyName) || isEmpty(givenName)) {
return familyName || givenName;
} else {
return `${familyName}, ${givenName}`;
}
@@ -55,11 +57,14 @@ export class DSONameService {
.filter((type) => typeof type === 'string')
.find((type: string) => Object.keys(this.factories).includes(type)) as string;
let name;
if (hasValue(match)) {
return this.factories[match](dso);
} else {
return this.factories.Default(dso);
name = this.factories[match](dso);
}
if (isEmpty(name)) {
name = this.factories.Default(dso);
}
return name;
}
}

View File

@@ -5,15 +5,9 @@ import { FindListOptions } from '../data/find-list-options.model';
describe(`BrowseDefinitionDataService`, () => {
let service: BrowseDefinitionDataService;
const dataServiceImplSpy = jasmine.createSpyObj('dataService', {
const findAllDataSpy = jasmine.createSpyObj('findAllData', {
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 linksToFollow = [
followLink('entries'),
@@ -21,35 +15,14 @@ describe(`BrowseDefinitionDataService`, () => {
];
beforeEach(() => {
service = new BrowseDefinitionDataService(null, null, null, null, null, null, null, null);
(service as any).dataService = dataServiceImplSpy;
service = new BrowseDefinitionDataService(null, null, null, null);
(service as any).findAllData = findAllDataSpy;
});
describe(`findAll`, () => {
it(`should call findAll on DataServiceImpl`, () => {
it(`should call findAll on findAllData`, () => {
service.findAll(options, true, false, ...linksToFollow);
expect(dataServiceImplSpy.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);
expect(findAllDataSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow);
});
});
});

View File

@@ -1,61 +1,38 @@
/* eslint-disable max-classes-per-file */
import { Injectable } from '@angular/core';
import { dataService } from '../cache/builders/build-decorators';
import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
import { DataService } from '../data/data.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { Observable } from 'rxjs';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list.model';
import { CoreState } from '../core-state.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> {
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();
}
}
/**
* Data service responsible for retrieving browse definitions from the REST server
*/
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
@dataService(BROWSE_DEFINITION)
export class BrowseDefinitionDataService {
/**
* A private DataService instance to delegate specific methods to.
*/
private dataService: DataServiceImpl;
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition> {
private findAllData: FindAllDataImpl<BrowseDefinition>;
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>) {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
) {
super('browses', requestService, rdbService, objectCache, halService);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
@@ -69,57 +46,11 @@ export class BrowseDefinitionDataService {
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<BrowseDefinition>>>}
* @return {Observable<RemoteData<PaginatedList<T>>>}
* Return an observable that emits object list
*/
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.dataService.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Returns an observable of {@link RemoteData} of an {@link BrowseDefinition}, based on an href, with a list of {@link FollowLinkConfig},
* 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);
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -6,13 +6,16 @@ import { SortOptions } from '../cache/models/sort-options.model';
* - metadataDefinition: The metadata definition to fetch entries or items for
* - pagination: Optional pagination options to use
* - sort: Optional sorting options to use
* - startsWith An optional value to use to filter the browse results
* - scope: An optional scope to limit the results within a specific collection or community
* - fetchThumbnail An optional boolean to request thumbnail for items
*/
export class BrowseEntrySearchOptions {
constructor(public metadataDefinition: string,
public pagination?: PaginationComponentOptions,
public sort?: SortOptions,
public startsWith?: string,
public scope?: string) {
public scope?: string,
public fetchThumbnail?: boolean) {
}
}

View File

@@ -139,13 +139,13 @@ describe('BrowseService', () => {
});
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;
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
a: expected
}));
});
@@ -153,20 +153,20 @@ describe('BrowseService', () => {
});
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);
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, undefined, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
a: expected
}));
});
});
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 +
'?filterValue=' + encodeURIComponent(mockAuthorName) +
'&filterAuthority=' + encodeURIComponent(mockAuthorityKey);
@@ -174,7 +174,7 @@ describe('BrowseService', () => {
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, mockAuthorityKey, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
a: expected
}));
});
@@ -267,11 +267,11 @@ describe('BrowseService', () => {
describe('when getFirstItemFor is called with a valid browse definition id', () => {
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.flush();
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', {
a: expectedURL
}));
});

View File

@@ -21,6 +21,12 @@ import { URLCombiner } from '../url-combiner/url-combiner';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { BrowseDefinitionDataService } from './browse-definition-data.service';
import { HrefOnlyDataService } from '../data/href-only-data.service';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
followLink('thumbnail')
];
/**
* The service handling all browse requests
@@ -96,7 +102,10 @@ export class BrowseService {
return href;
})
);
return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(href$);
if (options.fetchThumbnail ) {
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
}
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$);
}
/**
@@ -141,7 +150,10 @@ export class BrowseService {
return href;
}),
);
return this.hrefOnlyDataService.findAllByHref<Item>(href$);
if (options.fetchThumbnail) {
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
}
return this.hrefOnlyDataService.findListByHref<Item>(href$);
}
/**
@@ -172,7 +184,7 @@ export class BrowseService {
})
);
return this.hrefOnlyDataService.findAllByHref<Item>(href$).pipe(
return this.hrefOnlyDataService.findListByHref<Item>(href$).pipe(
getFirstSucceededRemoteData(),
getFirstOccurrence()
);
@@ -184,7 +196,7 @@ export class BrowseService {
* @param items
*/
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);
}
/**
@@ -192,7 +204,7 @@ export class BrowseService {
* @param items
*/
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);
}
/**
@@ -200,7 +212,7 @@ export class BrowseService {
* @param entries
*/
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);
}
/**
@@ -208,7 +220,7 @@ export class BrowseService {
* @param entries
*/
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);
}
/**

View File

@@ -1,14 +1,7 @@
/* eslint-disable max-classes-per-file */
import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import { dataService, getDataServiceFor, getLinkDefinition, link, } from './build-decorators';
class TestService {
}
class AnotherTestService {
}
import { getLinkDefinition, link } from './build-decorators';
class TestHALResource implements HALResource {
_links: {
@@ -25,31 +18,6 @@ describe('build decorators', () => {
beforeEach(() => {
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`, () => {
it(`should register a link`, () => {

View File

@@ -3,20 +3,13 @@ import { hasNoValue, hasValue } from '../../../shared/empty.util';
import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import {
getResourceTypeValueFor
} from '../object-cache.reducer';
import { getResourceTypeValueFor } from '../object-cache.reducer';
import { InjectionToken } from '@angular/core';
import { CacheableObject } from '../cacheable-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', {
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', {
providedIn: 'root',
@@ -27,7 +20,6 @@ const resolvedLinkKey = Symbol('resolvedLink');
const resolvedLinkMap = new Map();
const typeMap = new Map();
const dataServiceMap = new Map();
const linkMap = new Map();
/**
@@ -46,39 +38,6 @@ export function getClassForType(type: string | ResourceType) {
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
*/

View File

@@ -6,9 +6,10 @@ import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
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 { FindListOptions } from '../../data/find-list-options.model';
import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator';
const TEST_MODEL = new ResourceType('testmodel');
let result: any;
@@ -32,8 +33,8 @@ class TestModel implements HALResource {
@Injectable()
class TestDataService {
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
return 'findAllByHref';
findListByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
return 'findListByHref';
}
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
@@ -64,7 +65,7 @@ describe('LinkService', () => {
}
});
testDataService = new TestDataService();
spyOn(testDataService, 'findAllByHref').and.callThrough();
spyOn(testDataService, 'findListByHref').and.callThrough();
spyOn(testDataService, 'findByHref').and.callThrough();
TestBed.configureTestingModule({
providers: [LinkService, {
@@ -118,8 +119,8 @@ describe('LinkService', () => {
});
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', () => {
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
it('should call dataservice.findListByHref with the correct href, findListOptions, and nested links', () => {
expect(testDataService.findListByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
});
});
describe('either way', () => {

View File

@@ -3,28 +3,30 @@ import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model';
import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator';
import {
DATA_SERVICE_FACTORY,
LINK_DEFINITION_FACTORY,
LINK_DEFINITION_MAP_FACTORY,
LinkDefinition
LinkDefinition,
} from './build-decorators';
import { RemoteData } from '../../data/remote-data';
import { EMPTY, Observable } from 'rxjs';
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
* of resolved {@link HALLink}s on HALResources
*/
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class LinkService {
constructor(
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_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 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);
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`);
}
const service = Injector.create({
const service: HALDataService<any> = Injector.create({
providers: [],
parent: this.parentInjector
parent: this.parentInjector,
}).get(provider);
const link = model._links[matchingLinkDef.linkName];
@@ -72,7 +74,7 @@ export class LinkService {
try {
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 {
return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
}

View File

@@ -1,4 +1,4 @@
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model';
import { Item } from '../../shared/item.model';
import { PageInfo } from '../../shared/page-info.model';
@@ -18,6 +18,9 @@ import { take } from 'rxjs/operators';
import { HALLink } from '../../shared/hal-link.model';
import { RequestEntryState } from '../../data/request-entry-state.model';
import { RequestEntry } from '../../data/request-entry.model';
import { cold } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { fakeAsync, tick } from '@angular/core/testing';
describe('RemoteDataBuildService', () => {
let service: RemoteDataBuildService;
@@ -646,4 +649,211 @@ describe('RemoteDataBuildService', () => {
});
});
});
describe('buildFromHref', () => {
beforeEach(() => {
(objectCache.getRequestUUIDBySelfLink as jasmine.Spy).and.returnValue(cold('a', { a: 'request/uuid' }));
});
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID emit nothing', () => {
beforeEach(() => {
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
});
it('should not emit anything', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold(''));
});
});
describe('when one of getRequestFromRequestHref or getRequestFromRequestUUID emits nothing', () => {
let requestEntry: RequestEntry;
beforeEach(() => {
requestEntry = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the existing request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, {}, undefined),
}));
});
});
describe('when one of getRequestFromRequestHref or getRequestFromRequestUUID is stale', () => {
let requestEntry1: RequestEntry;
let requestEntry2: RequestEntry;
beforeEach(() => {
requestEntry1 = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
});
requestEntry2 = Object.assign(new RequestEntry(), {
state: RequestEntryState.SuccessStale,
request: {},
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the non-stale request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, {}, undefined),
}));
});
});
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID are stale', () => {
let requestEntry1: RequestEntry;
let requestEntry2: RequestEntry;
beforeEach(() => {
requestEntry1 = Object.assign(new RequestEntry(), {
state: RequestEntryState.SuccessStale,
request: {},
lastUpdated: 20,
});
requestEntry2 = Object.assign(new RequestEntry(), {
state: RequestEntryState.SuccessStale,
request: {},
lastUpdated: 10,
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the most up-to-date request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, 20, RequestEntryState.SuccessStale, undefined, {}, undefined),
}));
});
});
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID are not stale', () => {
let requestEntry1: RequestEntry;
let requestEntry2: RequestEntry;
beforeEach(() => {
requestEntry1 = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
lastUpdated: 25,
});
requestEntry2 = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
lastUpdated: 5,
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the most up-to-date request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, 25, RequestEntryState.Success, undefined, {}, undefined),
}));
});
});
});
describe('buildFromRequestUUIDAndAwait', () => {
let testScheduler;
let callback: jasmine.Spy;
let buildFromRequestUUIDSpy;
const BOOLEAN = { t: true, f: false };
const MOCK_PENDING_RD = createPendingRemoteDataObject();
const MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
const MOCK_FAILED_RD = createFailedRemoteDataObject('failed');
const RDs = {
p: MOCK_PENDING_RD,
s: MOCK_SUCCEEDED_RD,
f: MOCK_FAILED_RD,
};
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
callback = jasmine.createSpy('callback');
callback.and.returnValue(observableOf(undefined));
buildFromRequestUUIDSpy = spyOn(service, 'buildFromRequestUUID').and.callThrough();
});
it('should patch through href & followLinks to buildFromRequestUUID', () => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback, ...linksToFollow);
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith('some-href', ...linksToFollow);
});
it('should trigger the callback on successful RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_SUCCEEDED_RD);
expect(callback).toHaveBeenCalled();
done();
});
});
it('should trigger the callback on successful RD even if nothing subscribes to the returned Observable', fakeAsync(() => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback);
tick();
expect(callback).toHaveBeenCalled();
}));
it('should not trigger the callback on pending RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_PENDING_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_PENDING_RD);
expect(callback).not.toHaveBeenCalled();
done();
});
});
it('should not trigger the callback on failed RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_FAILED_RD);
expect(callback).not.toHaveBeenCalled();
done();
});
});
it('should only emit after the callback is done', () => {
testScheduler.run(({ cold: tsCold, expectObservable }) => {
buildFromRequestUUIDSpy.and.returnValue(
tsCold('-p----s', RDs)
);
callback.and.returnValue(
tsCold(' --t', BOOLEAN)
);
const done$ = service.buildFromRequestUUIDAndAwait('some-href', callback);
expectObservable(done$).toBe(
' -p------s', RDs // resulting duration between pending & successful includes the callback
);
});
});
});
});

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import {
AsyncSubject,
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
race as observableRace
} from 'rxjs';
import { map, switchMap, filter, distinctUntilKeyChanged } from 'rxjs/operators';
import { map, switchMap, filter, distinctUntilKeyChanged, startWith } from 'rxjs/operators';
import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model';
@@ -21,10 +21,11 @@ import { HALResource } from '../../shared/hal-resource.model';
import { PAGINATED_LIST } from '../../data/paginated-list.resource-type';
import { getUrlWithoutEmbedParams } from '../../index/index.selectors';
import { getResourceTypeValueFor } from '../object-cache.reducer';
import { hasSucceeded, RequestEntryState } from '../../data/request-entry-state.model';
import { hasSucceeded, isStale, RequestEntryState } from '../../data/request-entry-state.model';
import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators';
import { RequestEntry } from '../../data/request-entry.model';
import { ResponseState } from '../../data/response-state.model';
import { getFirstCompletedRemoteData } from '../../shared/operators';
@Injectable()
export class RemoteDataBuildService {
@@ -189,6 +190,49 @@ export class RemoteDataBuildService {
return this.toRemoteDataObservable<T>(requestEntry$, payload$);
}
/**
* Creates a {@link RemoteData} object for a rest request and its response
* and emits it only after the callback function is completed.
*
* @param requestUUID$ The UUID of the request we want to retrieve
* @param callback A function that returns an Observable. It will only be called once the request has succeeded.
* Then, the response will only be emitted after this callback function has emitted.
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
buildFromRequestUUIDAndAwait<T>(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<T>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<T>> {
const response$ = this.buildFromRequestUUID(requestUUID$, ...linksToFollow);
const callbackDone$ = new AsyncSubject<boolean>();
response$.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
// if the request succeeded, execute the callback
return callback(rd);
} else {
// otherwise, emit right away so the subscription doesn't stick around
return [true];
}
}),
).subscribe(() => {
callbackDone$.next(true);
callbackDone$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
// if the request succeeded, wait for the callback to finish
return callbackDone$.pipe(
map(() => rd),
);
} else {
return [rd];
}
})
);
}
/**
* Creates a {@link RemoteData} object for a rest request and its response
*
@@ -207,10 +251,27 @@ export class RemoteDataBuildService {
this.objectCache.getRequestUUIDBySelfLink(href)),
);
const requestEntry$ = observableRace(
href$.pipe(getRequestFromRequestHref(this.requestService)),
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)),
).pipe(
const requestEntry$ = observableCombineLatest([
href$.pipe(getRequestFromRequestHref(this.requestService), startWith(undefined)),
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService), startWith(undefined)),
]).pipe(
filter(([r1, r2]) => hasValue(r1) || hasValue(r2)),
map(([r1, r2]) => {
// If one of the two requests has no value, return the other (both is impossible due to the filter above)
if (hasNoValue(r2)) {
return r1;
} else if (hasNoValue(r1)) {
return r2;
}
if ((isStale(r1.state) && isStale(r2.state)) || (!isStale(r1.state) && !isStale(r2.state))) {
// Neither or both are stale, pick the most recent request
return r1.lastUpdated >= r2.lastUpdated ? r1 : r2;
} else {
// One of the two is stale, return the not stale request
return isStale(r2.state) ? r1 : r2;
}
}),
distinctUntilKeyChanged('lastUpdated')
);

View File

@@ -1,7 +1,7 @@
import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
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 { GetRequest } from '../data/request.models';
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 { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
import { FindListOptions } from '../data/find-list-options.model';
import { ObjectCacheService } from '../cache/object-cache.service';
const LINK_NAME = 'test';
const BROWSE = 'search/findByCollection';
class TestService extends ConfigService {
class TestService extends ConfigDataService {
protected linkPath = LINK_NAME;
protected browseEndpoint = BROWSE;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected halService: HALEndpointService) {
super(requestService, rdbService, null, null, halService, null, null, null, BROWSE);
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
) {
super(BROWSE, requestService, rdbService, objectCache, halService);
}
}
describe('ConfigService', () => {
describe('ConfigDataService', () => {
let scheduler: TestScheduler;
let service: TestService;
let requestService: RequestService;
@@ -45,7 +48,8 @@ describe('ConfigService', () => {
return new TestService(
requestService,
rdbService,
halService
null,
halService,
);
}

View 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;
}
}),
);
}
}

View File

@@ -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;
}
})
);
}
}

View File

@@ -1,41 +1,46 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { ConfigDataService } from './config-data.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { dataService } from '../cache/builders/build-decorators';
import { SUBMISSION_ACCESSES_TYPE } from './models/config-type';
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 { SubmissionAccessesModel } from './models/config-submission-accesses.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs';
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.
*/
@Injectable()
@dataService(SUBMISSION_ACCESSES_TYPE)
export class SubmissionAccessesConfigService extends ConfigService {
export class SubmissionAccessesConfigDataService extends ConfigDataService {
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<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>> {
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionAccessesModel>>;
}

View File

@@ -1,39 +1,46 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { ConfigDataService } from './config-data.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.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 { ConfigObject } from './models/config.model';
import { dataService } from '../cache/builders/build-decorators';
import { SUBMISSION_FORMS_TYPE } from './models/config-type';
import { SubmissionFormsModel } from './models/config-submission-forms.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs';
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()
@dataService(SUBMISSION_FORMS_TYPE)
export class SubmissionFormsConfigService extends ConfigService {
export class SubmissionFormsConfigDataService extends ConfigDataService {
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<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>> {
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionFormsModel>>;
}

View File

@@ -1,39 +1,30 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { ConfigDataService } from './config-data.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { dataService } from '../cache/builders/build-decorators';
import { SUBMISSION_UPLOADS_TYPE } from './models/config-type';
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 { SubmissionUploadsModel } from './models/config-submission-uploads.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs';
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.
*/
@Injectable()
@dataService(SUBMISSION_UPLOADS_TYPE)
export class SubmissionUploadsConfigService extends ConfigService {
export class SubmissionUploadsConfigDataService extends ConfigDataService {
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<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>> {

View File

@@ -36,7 +36,7 @@ import { SubmissionDefinitionsModel } from './config/models/config-submission-de
import { SubmissionFormsModel } from './config/models/config-submission-forms.model';
import { SubmissionSectionModel } from './config/models/config-submission-section.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 { coreReducers } from './core.reducers';
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 { DSpaceObjectDataService } from './data/dspace-object-data.service';
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
import { EntityTypeService } from './data/entity-type.service';
import { ExternalSourceService } from './data/external-source.service';
import { EntityTypeDataService } from './data/entity-type-data.service';
import { ExternalSourceDataService } from './data/external-source-data.service';
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
import { FacetValueResponseParsingService } from './data/facet-value-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 { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
import { RelationshipTypeService } from './data/relationship-type.service';
import { RelationshipService } from './data/relationship.service';
import { ResourcePolicyService } from './resource-policy/resource-policy.service';
import { RelationshipTypeDataService } from './data/relationship-type-data.service';
import { RelationshipDataService } from './data/relationship-data.service';
import { ResourcePolicyDataService } from './resource-policy/resource-policy-data.service';
import { SearchResponseParsingService } from './data/search-response-parsing.service';
import { SiteDataService } from './data/site-data.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 { AccessStatusDataService } from './data/access-status-data.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 { 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 { OrcidQueue } from './orcid/model/orcid-queue.model';
import { OrcidHistory } from './orcid/model/orcid-history.model';
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
@@ -223,7 +225,7 @@ const PROVIDERS = [
MetadataService,
ObjectCacheService,
PaginationComponentOptions,
ResourcePolicyService,
ResourcePolicyDataService,
RegistryService,
BitstreamFormatDataService,
RemoteDataBuildService,
@@ -238,7 +240,7 @@ const PROVIDERS = [
AccessStatusDataService,
SubmissionCcLicenseDataService,
SubmissionCcLicenseUrlDataService,
SubmissionFormsConfigService,
SubmissionFormsConfigDataService,
SubmissionRestService,
SubmissionResponseParsingService,
SubmissionJsonPatchOperationsService,
@@ -259,7 +261,7 @@ const PROVIDERS = [
MenuService,
ObjectUpdatesService,
SearchService,
RelationshipService,
RelationshipDataService,
MyDSpaceGuard,
RoleService,
TaskResponseParsingService,
@@ -267,7 +269,7 @@ const PROVIDERS = [
PoolTaskDataService,
BitstreamDataService,
DsDynamicTypeBindRelationService,
EntityTypeService,
EntityTypeDataService,
ContentSourceResponseParsingService,
ItemTemplateDataService,
SearchService,
@@ -276,8 +278,8 @@ const PROVIDERS = [
SearchFilterService,
SearchConfigurationService,
SelectableListService,
RelationshipTypeService,
ExternalSourceService,
RelationshipTypeDataService,
ExternalSourceDataService,
LookupRelationService,
VersionDataService,
VersionHistoryDataService,
@@ -300,14 +302,16 @@ const PROVIDERS = [
FilteredDiscoveryPageResponseParsingService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
VocabularyService,
VocabularyDataService,
VocabularyEntryDetailsDataService,
VocabularyTreeviewService,
SequenceService,
GroupDataService,
FeedbackDataService,
ResearcherProfileService,
ResearcherProfileDataService,
ProfileClaimService,
OrcidAuthService,
OrcidQueueService,
OrcidQueueDataService,
OrcidHistoryDataService,
];

View File

@@ -76,6 +76,6 @@ describe('AccessStatusDataService', () => {
});
halService = new HALEndpointServiceStub(url);
notificationsService = new NotificationsServiceStub();
service = new AccessStatusDataService(null, halService, null, notificationsService, objectCache, rdbService, requestService, null);
service = new AccessStatusDataService(requestService, rdbService, objectCache, halService);
}
});

View File

@@ -1,38 +1,30 @@
import { HttpClient } from '@angular/common/http';
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 { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DataService } from './data.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 { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
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()
@dataService(ACCESS_STATUS)
export class AccessStatusDataService extends DataService<AccessStatusObject> {
protected linkPath = 'accessStatus';
export class AccessStatusDataService extends BaseDataService<AccessStatusObject> {
constructor(
protected comparator: DefaultChangeAnalyzer<AccessStatusObject>,
protected halService: HALEndpointService,
protected http: HttpClient,
protected notificationsService: NotificationsService,
protected objectCache: ObjectCacheService,
protected rdbService: RemoteDataBuildService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
) {
super();
super('accessStatus', requestService, rdbService, objectCache, halService);
}
/**

View File

@@ -1,54 +1,38 @@
/* eslint-disable max-classes-per-file */
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { compare, Operation } from 'fast-json-patch';
/**
* 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 { 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 { NotificationsService } from '../../shared/notifications/notifications.service';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SortDirection, SortOptions } from '../cache/models/sort-options.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 { 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 { CoreState } from '../core-state.model';
import { FindListOptions } from './find-list-options.model';
import { RemoteData } from '../remote-data';
import { RequestEntryState } from '../request-entry-state.model';
import { fakeAsync, tick } from '@angular/core/testing';
import { BaseDataService } from './base-data.service';
const endpoint = 'https://rest.api/core';
const BOOLEAN = { f: false, t: true };
class TestService extends DataService<any> {
class TestService extends BaseDataService<any> {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected linkPath: string,
protected halService: HALEndpointService,
protected objectCache: ObjectCacheService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: ChangeAnalyzer<Item>
protected halService: HALEndpointService,
) {
super();
super(undefined, requestService, rdbService, objectCache, halService);
}
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
@@ -56,24 +40,12 @@ class TestService extends DataService<any> {
}
}
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
diff(object1: Item, object2: Item): Operation[] {
return compare((object1 as any).metadata, (object2 as any).metadata);
}
}
describe('DataService', () => {
describe('BaseDataService', () => {
let service: TestService;
let options: FindListOptions;
let requestService;
let halService;
let rdbService;
let notificationsService;
let http;
let comparator;
let objectCache;
let store;
let selfLink;
let linksToFollow;
let testScheduler;
@@ -83,9 +55,6 @@ describe('DataService', () => {
requestService = getMockRequestService();
halService = new HALEndpointServiceStub('url') as any;
rdbService = getMockRemoteDataBuildService();
notificationsService = {} as NotificationsService;
http = {} as HttpClient;
comparator = new DummyChangeAnalyzer() as any;
objectCache = {
addPatch: () => {
@@ -98,7 +67,6 @@ describe('DataService', () => {
/* empty */
}
} as any;
store = {} as Store<CoreState>;
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
linksToFollow = [
followLink('a'),
@@ -126,17 +94,11 @@ describe('DataService', () => {
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
};
return new TestService(
requestService,
rdbService,
store,
endpoint,
halService,
objectCache,
notificationsService,
http,
comparator,
halService,
);
}
@@ -144,259 +106,6 @@ describe('DataService', () => {
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&param2=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`, () => {
let callback: jasmine.Spy<jasmine.Func>;
@@ -661,7 +370,7 @@ describe('DataService', () => {
});
describe(`findAllByHref`, () => {
describe(`findListByHref`, () => {
let findListOptions;
beforeEach(() => {
findListOptions = { currentPage: 5 };
@@ -674,7 +383,7 @@ describe('DataService', () => {
spyOn(rdbService, 'buildList').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);
});
});
@@ -685,11 +394,11 @@ describe('DataService', () => {
spyOn(rdbService, 'buildList').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);
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);
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(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);
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 }) => {
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
spyOn(rdbService, 'buildList').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();
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
expect(service.findAllByHref).not.toHaveBeenCalled();
expect(service.findListByHref).not.toHaveBeenCalled();
// call the callback passed to reRequestStaleRemoteData
(service as any).reRequestStaleRemoteData.calls.argsFor(0)[1]();
// verify that findAllByHref _has_ been called now, with the same params as the original call
expect(service.findAllByHref).toHaveBeenCalledWith(jasmine.anything(), findListOptions, true, true, ...linksToFollow);
// verify that findListByHref _has_ been called now, with the same params as the original call
expect(service.findListByHref).toHaveBeenCalledWith(jasmine.anything(), findListOptions, true, true, ...linksToFollow);
// ... 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!',
};
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,
};
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,
};
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,
};
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,
};
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
);
});
});
});
});
});

View 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$;
}
}

View 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();
});
});
});
});

View 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$;
}
}

View 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();
});
});
});

View 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);
}

View File

@@ -0,0 +1,230 @@
/**
* 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 { RestRequestMethod } from '../rest-request-method';
/**
* 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';
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 buildFromRequestUUIDAndAwaitSpy: jasmine.Spy;
let getIDHrefObsSpy: jasmine.Spy;
let deleteByHrefSpy: jasmine.Spy;
beforeEach(() => {
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
buildFromRequestUUIDAndAwaitSpy = spyOn(rdbService, 'buildFromRequestUUIDAndAwait').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'));
buildFromRequestUUIDAndAwaitSpy.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 send a DELETE request', (done) => {
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.deleteByHref('some-href').subscribe(() => {
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
method: RestRequestMethod.DELETE,
href: 'some-href',
}));
done();
});
});
it('should include the virtual metadata to be copied in the DELETE request', (done) => {
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.deleteByHref('some-href', ['a', 'b', 'c']).subscribe(() => {
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
method: RestRequestMethod.DELETE,
href: 'some-href?copyVirtualMetadata=a&copyVirtualMetadata=b&copyVirtualMetadata=c',
}));
done();
});
});
it('should invalidate the currently cached object', (done) => {
service.deleteByHref('some-href').subscribe(() => {
expect(buildFromRequestUUIDAndAwaitSpy).toHaveBeenCalledWith(
requestService.generateRequestId(),
jasmine.anything(),
);
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
callback();
expect(service.invalidateByHref).toHaveBeenCalledWith('some-href');
done();
});
});
it('should return the RemoteData of the response', (done) => {
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.deleteByHref('some-href').subscribe(rd => {
expect(rd).toBe(MOCK_SUCCEEDED_RD);
done();
});
});
});
});
});

View File

@@ -0,0 +1,87 @@
/**
* 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 { Observable } from 'rxjs';
import { RemoteData } from '../remote-data';
import { NoContent } from '../../shared/NoContent.model';
import { switchMap } from 'rxjs/operators';
import { DeleteRequest } from '../request.models';
import { hasNoValue, hasValue } from '../../../shared/empty.util';
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);
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(href));
}
}

View 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&param2=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);
});
});
});
});

View 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)));
}
}

View 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>>>;
}

View 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);
});
});
});

View 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)));
}
}

View 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();
});
});
});

View 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)),
);
}
}

View 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();
});
});
});
});

View 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);
}
}

View 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');
});
});
});

View 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)),
);
}
}

View File

@@ -12,6 +12,9 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
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 { testSearchDataImplementation } from './base/search-data.spec';
import { testPatchDataImplementation } from './base/patch-data.spec';
import { testDeleteDataImplementation } from './base/delete-data.spec';
describe('BitstreamDataService', () => {
let service: BitstreamDataService;
@@ -47,7 +50,14 @@ describe('BitstreamDataService', () => {
});
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', () => {

View File

@@ -1,12 +1,9 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { map, switchMap, take } 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 { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Bitstream } from '../shared/bitstream.model';
@@ -15,8 +12,6 @@ import { Bundle } from '../shared/bundle.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
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 { RemoteData } from './remote-data';
import { PutRequest } from './request.models';
@@ -28,36 +23,45 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
import { PageInfo } from '../shared/page-info.model';
import { RequestParam } from '../cache/models/request-param.model';
import { sendRequest } from '../shared/request.operators';
import { CoreState } from '../core-state.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
*/
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
@dataService(BITSTREAM)
export class BitstreamDataService extends DataService<Bitstream> {
/**
* The HAL path to the bitstream endpoint
*/
protected linkPath = 'bitstreams';
export class BitstreamDataService extends IdentifiableDataService<Bitstream> implements SearchData<Bitstream>, PatchData<Bitstream>, DeleteData<Bitstream> {
private searchData: SearchDataImpl<Bitstream>;
private patchData: PatchDataImpl<Bitstream>;
private deleteData: DeleteDataImpl<Bitstream>;
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<Bitstream>,
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
*/
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,
useCachedVersionIfAvailable,
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);
}
}

View File

@@ -6,19 +6,16 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { BitstreamFormat } from '../shared/bitstream-format.model';
import { waitForAsync } from '@angular/core/testing';
import {
BitstreamFormatsRegistryDeselectAction,
BitstreamFormatsRegistryDeselectAllAction,
BitstreamFormatsRegistrySelectAction
} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
import { TestScheduler } from 'rxjs/testing';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { CoreState } from '../core-state.model';
import { RequestEntry } from './request-entry.model';
import { testFindAllDataImplementation } from './base/find-all-data.spec';
import { testDeleteDataImplementation } from './base/delete-data.spec';
describe('BitstreamFormatDataService', () => {
let service: BitstreamFormatDataService;
@@ -50,8 +47,6 @@ describe('BitstreamFormatDataService', () => {
} as HALEndpointService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
let rd;
let rdbService: RemoteDataBuildService;
@@ -59,21 +54,26 @@ describe('BitstreamFormatDataService', () => {
function initTestService(halService) {
rd = createSuccessfulRemoteDataObject({});
rdbService = jasmine.createSpyObj('rdbService', {
buildFromRequestUUID: observableOf(rd)
buildFromRequestUUID: observableOf(rd),
buildFromRequestUUIDAndAwait: observableOf(rd),
});
return new BitstreamFormatDataService(
requestService,
rdbService,
store,
objectCache,
halService,
notificationsService,
http,
comparator
store,
);
}
describe('composition', () => {
const initService = () => new BitstreamFormatDataService(null, null, null, null, null, null);
testFindAllDataImplementation(initService);
testDeleteDataImplementation(initService);
});
describe('getBrowseEndpoint', () => {
beforeEach(waitForAsync(() => {
scheduler = getTestScheduler();

View File

@@ -1,16 +1,10 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { createSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import {
BitstreamFormatsRegistryDeselectAction,
BitstreamFormatsRegistryDeselectAllAction,
BitstreamFormatsRegistrySelectAction
} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
import { BitstreamFormatRegistryState } from '../../admin/admin-registries/bitstream-formats/bitstream-format.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
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 } from '../shared/bitstream.model';
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 { PostRequest, PutRequest } from './request.models';
import { RequestService } from './request.service';
import { sendRequest } from '../shared/request.operators';
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(
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
*/
@Injectable()
@dataService(BITSTREAM_FORMAT)
export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
export class BitstreamFormatDataService extends IdentifiableDataService<BitstreamFormat> implements FindAllData<BitstreamFormat>, DeleteData<BitstreamFormat> {
protected linkPath = 'bitstreamformats';
private findAllData: FindAllDataImpl<BitstreamFormat>;
private deleteData: DeleteDataImpl<BitstreamFormat>;
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<BitstreamFormat>) {
super();
protected store: Store<CoreState>,
) {
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> {
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>> {
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);
}
}

View File

@@ -13,6 +13,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
import { createPaginatedList } from '../../shared/testing/utils.test';
import { Bundle } from '../shared/bundle.model';
import { CoreState } from '../core-state.model';
import { testPatchDataImplementation } from './base/patch-data.spec';
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
diff(object1: Item, object2: Item): Operation[] {
@@ -64,9 +65,6 @@ describe('BundleDataService', () => {
store,
objectCache,
halService,
notificationsService,
http,
comparator,
);
}
@@ -74,14 +72,20 @@ describe('BundleDataService', () => {
service = initTestService();
});
describe('composition', () => {
const initService = () => new BundleDataService(null, null, null, null, null);
testPatchDataImplementation(initService);
});
describe('findAllByItem', () => {
beforeEach(() => {
spyOn(service, 'findAllByHref');
spyOn(service, 'findListByHref');
service.findAllByItem(item);
});
it('should call findAllByHref with the item\'s bundles link', () => {
expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined, true, true);
it('should call findListByHref with the item\'s bundles link', () => {
expect(service.findListByHref).toHaveBeenCalledWith(bundleLink, undefined, true, true);
});
});

View File

@@ -1,20 +1,14 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, switchMap, take } 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 { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Bundle } from '../shared/bundle.model';
import { BUNDLE } from '../shared/bundle.resource-type';
import { HALEndpointService } from '../shared/hal-endpoint.service';
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 { RemoteData } from './remote-data';
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 { Bitstream } from '../shared/bitstream.model';
import { RequestEntryState } from './request-entry-state.model';
import { CoreState } from '../core-state.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
*/
@Injectable(
{providedIn: 'root'}
{ providedIn: 'root' },
)
@dataService(BUNDLE)
export class BundleDataService extends DataService<Bundle> {
protected linkPath = 'bundles';
protected bitstreamsEndpoint = 'bitstreams';
export class BundleDataService extends IdentifiableDataService<Bundle> implements PatchData<Bundle> {
private bitstreamsEndpoint = 'bitstreams';
private patchData: PatchDataImpl<Bundle>;
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<Bundle>) {
super();
protected comparator: DSOChangeAnalyzer<Bundle>,
) {
super('bundles', requestService, rdbService, objectCache, halService);
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
*/
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);
hrefObs.pipe(
take(1)
take(1),
).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.send(request, true);
@@ -141,4 +141,38 @@ export class BundleDataService extends DataService<Bundle> {
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);
}
}

View File

@@ -13,16 +13,17 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { Collection } from '../shared/collection.model';
import { PageInfo } from '../shared/page-info.model';
import { buildPaginatedList } from './paginated-list.model';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
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 collectionId = 'fake-collection-id';
@@ -75,6 +76,16 @@ describe('CollectionDataService', () => {
const paginatedList = buildPaginatedList(pageInfo, array);
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', () => {
beforeEach(() => {
createService();
@@ -201,7 +212,7 @@ describe('CollectionDataService', () => {
notificationsService = new NotificationsServiceStub();
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);
}
});

View File

@@ -1,6 +1,5 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
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 { 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 { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service';
@@ -33,30 +31,28 @@ import {
import { RequestService } from './request.service';
import { BitstreamDataService } from './bitstream-data.service';
import { RestRequest } from './rest-request.model';
import { CoreState } from '../core-state.model';
import { FindListOptions } from './find-list-options.model';
import { Community } from '../shared/community.model';
import { dataService } from './base/data-service.decorator';
@Injectable()
@dataService(COLLECTION)
export class CollectionDataService extends ComColDataService<Collection> {
protected linkPath = 'collections';
protected errorTitle = 'collection.source.update.notifications.error.title';
protected contentSourceError = 'collection.source.update.notifications.error.content';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected cds: CommunityDataService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected comparator: DSOChangeAnalyzer<Community>,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected bitstreamDataService: BitstreamDataService,
protected comparator: DSOChangeAnalyzer<Collection>,
protected translate: TranslateService
protected communityDataService: CommunityDataService,
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.
*/
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) {
return this.cds.getEndpoint().pipe(
map((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)),
return this.communityDataService.getEndpoint().pipe(
map((endpoint: string) => this.communityDataService.getIDHref(endpoint, options.scopeID)),
filter((href: string) => isNotEmpty(href)),
take(1)
take(1),
);
}
}

View File

@@ -13,16 +13,16 @@ import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { RequestService } from './request.service';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
createFailedRemoteDataObject,
createSuccessfulRemoteDataObject
} from '../../shared/remote-data.utils';
import { createFailedRemoteDataObject, createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { BitstreamDataService } from './bitstream-data.service';
import { CoreState } from '../core-state.model';
import { FindListOptions } from './find-list-options.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';
@@ -47,7 +47,7 @@ class TestService extends ComColDataService<any> {
protected comparator: DSOChangeAnalyzer<Community>,
protected linkPath: string
) {
super();
super('something', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService);
}
protected getFindByParentHref(parentUUID: string): Observable<string> {
@@ -154,6 +154,15 @@ describe('ComColDataService', () => {
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', () => {
it(`should call createAndSendGetRequest with the scope Community's self link`, () => {
testScheduler.run(({ cold, flush, expectObservable }) => {

View File

@@ -4,7 +4,6 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Community } from '../shared/community.model';
import { HALLink } from '../shared/hal-link.model';
import { DataService } from './data.service';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.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> {
protected abstract objectCache: ObjectCacheService;
protected abstract halService: HALEndpointService;
protected abstract bitstreamDataService: BitstreamDataService;
export abstract class ComColDataService<T extends Community | Collection> extends IdentifiableDataService<T> implements CreateData<T>, FindAllData<T>, SearchData<T>, PatchData<T>, DeleteData<T> {
private createData: CreateData<T>;
private findAllData: FindAllData<T>;
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
@@ -66,7 +99,7 @@ export abstract class ComColDataService<T extends Community | Collection> extend
const href$ = this.getFindByParentHref(parentUUID).pipe(
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;
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);
}
}

View File

@@ -1,11 +1,8 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Community } from '../shared/community.model';
@@ -19,36 +16,36 @@ import { RequestService } from './request.service';
import { BitstreamDataService } from './bitstream-data.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { isNotEmpty } from '../../shared/empty.util';
import { CoreState } from '../core-state.model';
import { FindListOptions } from './find-list-options.model';
import { dataService } from './base/data-service.decorator';
@Injectable()
@dataService(COMMUNITY)
export class CommunityDataService extends ComColDataService<Community> {
protected linkPath = 'communities';
protected topLinkPath = 'search/top';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected comparator: DSOChangeAnalyzer<Community>,
protected notificationsService: NotificationsService,
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() {
return this.halService.getEndpoint(this.linkPath);
}
findTop(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<Community>[]): Observable<RemoteData<PaginatedList<Community>>> {
const hrefObs = this.getFindAllHref(options, this.topLinkPath);
return this.findAllByHref(hrefObs, undefined, true, true, ...linksToFollow);
return this.getEndpoint().pipe(
map(href => `${href}/search/top`),
switchMap(href => this.findListByHref(href, options, true, true, ...linksToFollow))
);
}
protected getFindByParentHref(parentUUID: string): Observable<string> {

View File

@@ -5,8 +5,6 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { GetRequest } from './request.models';
import { RequestService } from './request.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 { ConfigurationProperty } from '../shared/configuration-property.model';
@@ -44,18 +42,12 @@ describe('ConfigurationDataService', () => {
})
});
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
service = new ConfigurationDataService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
http,
comparator
);
});

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