mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
#255 Fetching and parsing browse entries
This commit is contained in:
@@ -23,6 +23,7 @@ import { RequestService } from '../../core/data/request.service';
|
|||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { configureRequest } from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -78,7 +79,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
@@ -153,7 +154,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
@@ -188,7 +189,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
|
@@ -1,24 +1,28 @@
|
|||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||||
import { BrowseService } from './browse.service';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { hot, cold, getTestScheduler } from 'jasmine-marbles';
|
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
|
||||||
import { BrowseEndpointRequest } from '../data/request.models';
|
|
||||||
import { TestScheduler } from 'rxjs/Rx';
|
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { BrowseEndpointRequest, BrowseEntriesRequest } from '../data/request.models';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { BrowseService } from './browse.service';
|
||||||
|
|
||||||
describe('BrowseService', () => {
|
describe('BrowseService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: BrowseService;
|
let service: BrowseService;
|
||||||
let responseCache: ResponseCacheService;
|
let responseCache: ResponseCacheService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
const browsesEndpointURL = 'https://rest.api/browses';
|
const browsesEndpointURL = 'https://rest.api/browses';
|
||||||
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
||||||
const browseDefinitions = [
|
const browseDefinitions = [
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
id: 'date',
|
||||||
metadataBrowse: false,
|
metadataBrowse: false,
|
||||||
sortOptions: [
|
sortOptions: [
|
||||||
{
|
{
|
||||||
@@ -45,6 +49,7 @@ describe('BrowseService', () => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
id: 'author',
|
||||||
metadataBrowse: true,
|
metadataBrowse: true,
|
||||||
sortOptions: [
|
sortOptions: [
|
||||||
{
|
{
|
||||||
@@ -80,7 +85,7 @@ describe('BrowseService', () => {
|
|||||||
b: {
|
b: {
|
||||||
response: {
|
response: {
|
||||||
isSuccessful,
|
isSuccessful,
|
||||||
browseDefinitions,
|
payload: browseDefinitions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -91,7 +96,8 @@ describe('BrowseService', () => {
|
|||||||
return new BrowseService(
|
return new BrowseService(
|
||||||
responseCache,
|
responseCache,
|
||||||
requestService,
|
requestService,
|
||||||
halService
|
halService,
|
||||||
|
rdbService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,15 +105,99 @@ describe('BrowseService', () => {
|
|||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getBrowseDefinitions', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
responseCache = initMockResponseCacheService(true);
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(halService, 'getEndpoint').and
|
||||||
|
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
||||||
|
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a new BrowseEndpointRequest', () => {
|
||||||
|
const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getBrowseDefinitions().subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
|
service.getBrowseDefinitions();
|
||||||
|
|
||||||
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData object containing the correct BrowseDefinition[]', () => {
|
||||||
|
const expected = cold('--a-', { a: {
|
||||||
|
payload: browseDefinitions
|
||||||
|
}});
|
||||||
|
|
||||||
|
expect(service.getBrowseDefinitions()).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBrowseEntriesFor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
responseCache = initMockResponseCacheService(true);
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
|
.returnValue(hot('--a-', { a: {
|
||||||
|
payload: browseDefinitions
|
||||||
|
}}));
|
||||||
|
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with a valid browse definition id', () => {
|
||||||
|
it('should configure a new BrowseEntriesRequest', () => {
|
||||||
|
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getBrowseEntriesFor(browseDefinitions[1].id).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
|
service.getBrowseEntriesFor(browseDefinitions[1].id);
|
||||||
|
|
||||||
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with an invalid browse definition id', () => {
|
||||||
|
it('should throw an Error', () => {
|
||||||
|
|
||||||
|
const definitionID = 'invalidID';
|
||||||
|
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
||||||
|
|
||||||
|
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getBrowseURLFor', () => {
|
describe('getBrowseURLFor', () => {
|
||||||
|
|
||||||
describe('if getEndpoint fires', () => {
|
describe('if getBrowseDefinitions fires', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(halService, 'getEndpoint').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
.returnValue(hot('--a-', { a: {
|
||||||
|
payload: browseDefinitions
|
||||||
|
}}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the URL for the given metadatumKey and linkPath', () => {
|
it('should return the URL for the given metadatumKey and linkPath', () => {
|
||||||
@@ -152,26 +242,15 @@ describe('BrowseService', () => {
|
|||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure a new BrowseEndpointRequest', () => {
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
|
||||||
const linkPath = 'items';
|
|
||||||
const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
|
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkPath).subscribe());
|
|
||||||
scheduler.flush();
|
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if getEndpoint doesn\'t fire', () => {
|
describe('if getBrowseDefinitions doesn\'t fire', () => {
|
||||||
it('should return undefined', () => {
|
it('should return undefined', () => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(halService, 'getEndpoint').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
.returnValue(hot('----'));
|
.returnValue(hot('----'));
|
||||||
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadatumKey = 'dc.date.issued';
|
||||||
@@ -182,22 +261,5 @@ describe('BrowseService', () => {
|
|||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if the browses endpoint can\'t be retrieved', () => {
|
|
||||||
it('should throw an error', () => {
|
|
||||||
responseCache = initMockResponseCacheService(false);
|
|
||||||
requestService = getMockRequestService();
|
|
||||||
service = initTestService();
|
|
||||||
spyOn(halService, 'getEndpoint').and
|
|
||||||
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
|
||||||
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
|
||||||
const linkPath = 'items';
|
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
|
||||||
const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`));
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,15 +1,34 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import {
|
||||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
ensureArrayHasValue,
|
||||||
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
hasValueOperator,
|
||||||
|
isEmpty,
|
||||||
|
isNotEmpty,
|
||||||
|
isNotEmptyOperator
|
||||||
|
} from '../../shared/empty.util';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { SortOptions } from '../cache/models/sort-options.model';
|
||||||
|
import { GenericSuccessResponse } from '../cache/response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { BrowseEndpointRequest, RestRequest } from '../data/request.models';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { BrowseEndpointRequest, BrowseEntriesRequest, RestRequest } from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import {
|
||||||
|
configureRequest,
|
||||||
|
filterSuccessfulResponses,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
getRequestFromSelflink,
|
||||||
|
getResponseFromSelflink
|
||||||
|
} from '../shared/operators';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseService {
|
export class BrowseService {
|
||||||
@@ -31,42 +50,106 @@ export class BrowseService {
|
|||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService) {
|
protected halService: HALEndpointService,
|
||||||
|
private rdb: RemoteDataBuildService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseDefinitions(): Observable<RemoteData<BrowseDefinition[]>> {
|
||||||
|
const request$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
|
||||||
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
|
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
||||||
|
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
||||||
|
const payload$ = responseCache$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
|
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
||||||
|
ensureArrayHasValue(),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseEntriesFor(definitionID: string, options: {
|
||||||
|
pagination?: PaginationComponentOptions;
|
||||||
|
sort?: SortOptions;
|
||||||
|
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
|
const request$ = this.getBrowseDefinitions().pipe(
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
|
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
|
||||||
|
),
|
||||||
|
map((def: BrowseDefinition) => {
|
||||||
|
if (isNotEmpty(def)) {
|
||||||
|
return def._links;
|
||||||
|
} else {
|
||||||
|
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
hasValueOperator(),
|
||||||
|
map((_links: any) => _links.entries),
|
||||||
|
hasValueOperator(),
|
||||||
|
map((href: string) => {
|
||||||
|
// TODO nearly identical to PaginatedSearchOptions => refactor
|
||||||
|
const args = [];
|
||||||
|
if (isNotEmpty(options.sort)) {
|
||||||
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(options.pagination)) {
|
||||||
|
args.push(`page=${options.pagination.currentPage - 1}`);
|
||||||
|
args.push(`size=${options.pagination.pageSize}`);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
}
|
||||||
|
return href;
|
||||||
|
}),
|
||||||
|
map((endpointURL: string) => new BrowseEntriesRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
|
||||||
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
|
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
||||||
|
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
||||||
|
|
||||||
|
const payload$ = responseCache$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
|
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
||||||
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
||||||
return this.halService.getEndpoint(this.linkPath)
|
return this.getBrowseDefinitions().pipe(
|
||||||
.filter((href: string) => isNotEmpty(href))
|
getRemoteDataPayload(),
|
||||||
.distinctUntilChanged()
|
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
.map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL))
|
.find((def: BrowseDefinition) => {
|
||||||
.do((request: RestRequest) => this.requestService.configure(request))
|
const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
||||||
.flatMap((request: RestRequest) => {
|
return isNotEmpty(matchingKeys);
|
||||||
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
})
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
),
|
||||||
.partition((response: RestResponse) => response.isSuccessful);
|
map((def: BrowseDefinition) => {
|
||||||
|
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
||||||
return Observable.merge(
|
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
|
||||||
errorResponse.flatMap((response: ErrorResponse) =>
|
} else {
|
||||||
Observable.throw(new Error(`Couldn't retrieve the browses endpoint`))),
|
return def._links[linkPath];
|
||||||
successResponse
|
}
|
||||||
.filter((response: BrowseSuccessResponse) => isNotEmpty(response.browseDefinitions))
|
}),
|
||||||
.map((response: BrowseSuccessResponse) => response.browseDefinitions)
|
startWith(undefined),
|
||||||
.map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
distinctUntilChanged()
|
||||||
.find((def: BrowseDefinition) => {
|
);
|
||||||
const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
|
||||||
return isNotEmpty(matchingKeys);
|
|
||||||
})
|
|
||||||
).map((def: BrowseDefinition) => {
|
|
||||||
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
|
||||||
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
|
|
||||||
} else {
|
|
||||||
return def._links[linkPath];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}).startWith(undefined)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,28 +1,25 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { map, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
|
||||||
import { NormalizedSearchResult } from '../../../+search-page/normalized-search-result.model';
|
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
|
||||||
import { SearchQueryResponse } from '../../../+search-page/search-service/search-query-response.model';
|
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
|
||||||
import { PaginatedList } from '../../data/paginated-list';
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
import { RemoteDataError } from '../../data/remote-data-error';
|
import { RemoteDataError } from '../../data/remote-data-error';
|
||||||
import { GetRequest, RestRequest } from '../../data/request.models';
|
import { GetRequest } from '../../data/request.models';
|
||||||
import { RequestEntry } from '../../data/request.reducer';
|
import { RequestEntry } from '../../data/request.reducer';
|
||||||
import { RequestService } from '../../data/request.service';
|
import { RequestService } from '../../data/request.service';
|
||||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
import { NormalizedObject } from '../models/normalized-object.model';
|
||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
|
||||||
import { NormalizedDSpaceObject } from '../models/normalized-dspace-object.model';
|
|
||||||
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
|
||||||
|
|
||||||
import { CacheableObject } from '../object-cache.reducer';
|
|
||||||
import { ObjectCacheService } from '../object-cache.service';
|
import { ObjectCacheService } from '../object-cache.service';
|
||||||
import { DSOSuccessResponse, ErrorResponse, SearchSuccessResponse } from '../response-cache.models';
|
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../response-cache.reducer';
|
import { ResponseCacheEntry } from '../response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../response-cache.service';
|
import { ResponseCacheService } from '../response-cache.service';
|
||||||
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
||||||
import { NormalizedObject } from '../models/normalized-object.model';
|
import {
|
||||||
|
getRequestFromSelflink,
|
||||||
|
getResourceLinksFromResponse,
|
||||||
|
getResponseFromSelflink,
|
||||||
|
filterSuccessfulResponses
|
||||||
|
} from '../../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteDataBuildService {
|
export class RemoteDataBuildService {
|
||||||
@@ -31,43 +28,42 @@ export class RemoteDataBuildService {
|
|||||||
protected requestService: RequestService) {
|
protected requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSingle<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<TDomain>> {
|
buildSingle<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain>> {
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof href$ === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
href$ = Observable.of(href$);
|
||||||
}
|
}
|
||||||
const requestHrefObs = hrefObs.flatMap((href: string) =>
|
const requestHref$ = href$.pipe(flatMap((href: string) =>
|
||||||
this.objectCache.getRequestHrefBySelfLink(href));
|
this.objectCache.getRequestHrefBySelfLink(href)));
|
||||||
|
|
||||||
const requestEntryObs = Observable.race(
|
const requestEntry$ = Observable.race(
|
||||||
hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
href$.pipe(getRequestFromSelflink(this.requestService)),
|
||||||
.filter((entry) => hasValue(entry)),
|
requestHref$.pipe(getRequestFromSelflink(this.requestService))
|
||||||
requestHrefObs.flatMap((requestHref) =>
|
|
||||||
this.requestService.getByHref(requestHref)).filter((entry) => hasValue(entry))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const responseCacheObs = Observable.race(
|
const responseCache$ = Observable.race(
|
||||||
hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
href$.pipe(getResponseFromSelflink(this.responseCache)),
|
||||||
.filter((entry) => hasValue(entry)),
|
requestHref$.pipe(getResponseFromSelflink(this.responseCache))
|
||||||
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// always use self link if that is cached, only if it isn't, get it via the response.
|
// always use self link if that is cached, only if it isn't, get it via the response.
|
||||||
const payloadObs =
|
const payload$ =
|
||||||
Observable.combineLatest(
|
Observable.combineLatest(
|
||||||
hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href))
|
href$.pipe(
|
||||||
.startWith(undefined),
|
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
|
||||||
responseCacheObs
|
startWith(undefined)
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
),
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
responseCache$.pipe(
|
||||||
.flatMap((resourceSelfLinks: string[]) => {
|
getResourceLinksFromResponse(),
|
||||||
|
flatMap((resourceSelfLinks: string[]) => {
|
||||||
if (isNotEmpty(resourceSelfLinks)) {
|
if (isNotEmpty(resourceSelfLinks)) {
|
||||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
||||||
} else {
|
} else {
|
||||||
return Observable.of(undefined);
|
return Observable.of(undefined);
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
.distinctUntilChanged()
|
distinctUntilChanged(),
|
||||||
.startWith(undefined),
|
startWith(undefined)
|
||||||
|
),
|
||||||
(fromSelfLink, fromResponse) => {
|
(fromSelfLink, fromResponse) => {
|
||||||
if (hasValue(fromSelfLink)) {
|
if (hasValue(fromSelfLink)) {
|
||||||
return fromSelfLink;
|
return fromSelfLink;
|
||||||
@@ -75,17 +71,19 @@ export class RemoteDataBuildService {
|
|||||||
return fromResponse;
|
return fromResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).filter((normalized) => hasValue(normalized))
|
).pipe(
|
||||||
.map((normalized: TNormalized) => {
|
hasValueOperator(),
|
||||||
|
map((normalized: TNormalized) => {
|
||||||
return this.build<TNormalized, TDomain>(normalized);
|
return this.build<TNormalized, TDomain>(normalized);
|
||||||
})
|
}),
|
||||||
.startWith(undefined)
|
startWith(undefined),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
);
|
||||||
|
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemoteDataObservable<T>(requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<T>) {
|
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
|
||||||
return Observable.combineLatest(requestEntryObs, responseCacheObs.startWith(undefined), payloadObs,
|
return Observable.combineLatest(requestEntry$, responseCache$.startWith(undefined), payload$,
|
||||||
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
||||||
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
||||||
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||||
@@ -109,33 +107,31 @@ export class RemoteDataBuildService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buildList<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> {
|
buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> {
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof href$ === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
href$ = Observable.of(href$);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestEntryObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
||||||
.filter((entry) => hasValue(entry));
|
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
||||||
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
|
||||||
.filter((entry) => hasValue(entry));
|
|
||||||
|
|
||||||
const tDomainListObs = responseCacheObs
|
const tDomainList$ = responseCache$.pipe(
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
getResourceLinksFromResponse(),
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
flatMap((resourceUUIDs: string[]) => {
|
||||||
.flatMap((resourceUUIDs: string[]) => {
|
|
||||||
return this.objectCache.getList(resourceUUIDs)
|
return this.objectCache.getList(resourceUUIDs)
|
||||||
.map((normList: TNormalized[]) => {
|
.map((normList: TNormalized[]) => {
|
||||||
return normList.map((normalized: TNormalized) => {
|
return normList.map((normalized: TNormalized) => {
|
||||||
return this.build<TNormalized, TDomain>(normalized);
|
return this.build<TNormalized, TDomain>(normalized);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
.startWith([])
|
startWith([]),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
const pageInfoObs = responseCacheObs
|
const pageInfo$ = responseCache$.pipe(
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
filterSuccessfulResponses(),
|
||||||
.map((entry: ResponseCacheEntry) => {
|
map((entry: ResponseCacheEntry) => {
|
||||||
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
|
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
|
||||||
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
|
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
|
||||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
||||||
@@ -144,9 +140,10 @@ export class RemoteDataBuildService {
|
|||||||
return resPageInfo;
|
return resPageInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
|
const payload$ = Observable.combineLatest(tDomainList$, pageInfo$, (tDomainList, pageInfo) => {
|
||||||
if (hasValue(pageInfo)) {
|
if (hasValue(pageInfo)) {
|
||||||
return new PaginatedList(pageInfo, tDomainList);
|
return new PaginatedList(pageInfo, tDomainList);
|
||||||
} else {
|
} else {
|
||||||
@@ -154,7 +151,7 @@ export class RemoteDataBuildService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
||||||
|
8
src/app/core/cache/response-cache.models.ts
vendored
8
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||||
import { RequestError } from '../data/request.models';
|
import { RequestError } from '../data/request.models';
|
||||||
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { ConfigObject } from '../shared/config/config.model';
|
import { ConfigObject } from '../shared/config/config.model';
|
||||||
@@ -78,10 +79,11 @@ export class EndpointMapSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BrowseSuccessResponse extends RestResponse {
|
export class GenericSuccessResponse<T> extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public browseDefinitions: BrowseDefinition[],
|
public payload: T,
|
||||||
public statusCode: string
|
public statusCode: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
) {
|
) {
|
||||||
super(true, statusCode);
|
super(true, statusCode);
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ import { coreReducers } from './core.reducers';
|
|||||||
import { isNotEmpty } from '../shared/empty.util';
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
|
|
||||||
import { ApiService } from '../shared/api.service';
|
import { ApiService } from '../shared/api.service';
|
||||||
|
import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service';
|
||||||
import { CollectionDataService } from './data/collection-data.service';
|
import { CollectionDataService } from './data/collection-data.service';
|
||||||
import { CommunityDataService } from './data/community-data.service';
|
import { CommunityDataService } from './data/community-data.service';
|
||||||
import { DebugResponseParsingService } from './data/debug-response-parsing.service';
|
import { DebugResponseParsingService } from './data/debug-response-parsing.service';
|
||||||
@@ -83,6 +84,7 @@ const PROVIDERS = [
|
|||||||
SearchResponseParsingService,
|
SearchResponseParsingService,
|
||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
BrowseResponseParsingService,
|
BrowseResponseParsingService,
|
||||||
|
BrowseEntriesResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
ConfigResponseParsingService,
|
ConfigResponseParsingService,
|
||||||
RouteService,
|
RouteService,
|
||||||
|
48
src/app/core/data/browse-entries-response-parsing.service.ts
Normal file
48
src/app/core/data/browse-entries-response-parsing.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import {
|
||||||
|
ErrorResponse,
|
||||||
|
GenericSuccessResponse,
|
||||||
|
RestResponse
|
||||||
|
} from '../cache/response-cache.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseEntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
protected objectFactory = {
|
||||||
|
getConstructor: () => BrowseEntry
|
||||||
|
};
|
||||||
|
protected toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) { super();
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
||||||
|
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
|
||||||
|
const browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
|
return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
|
||||||
|
} else {
|
||||||
|
return new ErrorResponse(
|
||||||
|
Object.assign(
|
||||||
|
new Error('Unexpected response from browse endpoint'),
|
||||||
|
{ statusText: data.statusCode }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
import { BrowseEndpointRequest } from './request.models';
|
import { BrowseEndpointRequest } from './request.models';
|
||||||
import { BrowseSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
import { GenericSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
|
||||||
@@ -138,9 +138,9 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
it('should return a BrowseSuccessResponse if data contains a valid browse endpoint response', () => {
|
it('should return a GenericSuccessResponse if data contains a valid browse endpoint response', () => {
|
||||||
const response = service.parse(validRequest, validResponse);
|
const response = service.parse(validRequest, validResponse);
|
||||||
expect(response.constructor).toBe(BrowseSuccessResponse);
|
expect(response.constructor).toBe(GenericSuccessResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => {
|
it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => {
|
||||||
@@ -155,9 +155,9 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
expect(response.constructor).toBe(ErrorResponse);
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a BrowseSuccessResponse with the BrowseDefinitions in data', () => {
|
it('should return a GenericSuccessResponse with the BrowseDefinitions in data', () => {
|
||||||
const response = service.parse(validRequest, validResponse);
|
const response = service.parse(validRequest, validResponse);
|
||||||
expect((response as BrowseSuccessResponse).browseDefinitions).toEqual(definitions);
|
expect((response as GenericSuccessResponse<BrowseDefinition[]>).payload).toEqual(definitions);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { RestRequest } from './request.models';
|
import { RestRequest } from './request.models';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
import { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
@@ -15,7 +15,7 @@ export class BrowseResponseParsingService implements ResponseParsingService {
|
|||||||
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
|
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
|
||||||
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
return new BrowseSuccessResponse(browseDefinitions, data.statusCode);
|
return new GenericSuccessResponse(browseDefinitions, data.statusCode);
|
||||||
} else {
|
} else {
|
||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
@@ -2,6 +2,7 @@ import { SortOptions } from '../cache/models/sort-options.model';
|
|||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
|
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
||||||
@@ -164,6 +165,12 @@ export class BrowseEndpointRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BrowseEntriesRequest extends GetRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return BrowseEntriesResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigRequest extends GetRequest {
|
export class ConfigRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string) {
|
constructor(uuid: string, href: string) {
|
||||||
super(uuid, href);
|
super(uuid, href);
|
||||||
|
@@ -2,6 +2,9 @@ import { autoserialize, autoserializeAs } from 'cerialize';
|
|||||||
import { SortOption } from './sort-option.model';
|
import { SortOption } from './sort-option.model';
|
||||||
|
|
||||||
export class BrowseDefinition {
|
export class BrowseDefinition {
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
metadataBrowse: boolean;
|
metadataBrowse: boolean;
|
||||||
|
|
||||||
|
20
src/app/core/shared/browse-entry.model.ts
Normal file
20
src/app/core/shared/browse-entry.model.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
|
||||||
|
export class BrowseEntry {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
authority: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
@autoserializeAs('valueLang')
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
}
|
47
src/app/core/shared/operators.ts
Normal file
47
src/app/core/shared/operators.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
||||||
|
import { hasValueOperator } from '../../shared/empty.util';
|
||||||
|
import { DSOSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { RestRequest } from '../data/request.models';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file contains custom RxJS operators that can be used in multiple places
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getRequestFromSelflink = (requestService: RequestService) =>
|
||||||
|
(source: Observable<string>): Observable<RequestEntry> =>
|
||||||
|
source.pipe(
|
||||||
|
flatMap((href: string) => requestService.getByHref(href)),
|
||||||
|
hasValueOperator()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getResponseFromSelflink = (responseCache: ResponseCacheService) =>
|
||||||
|
(source: Observable<string>): Observable<ResponseCacheEntry> =>
|
||||||
|
source.pipe(
|
||||||
|
flatMap((href: string) => responseCache.get(href)),
|
||||||
|
hasValueOperator()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const filterSuccessfulResponses = () =>
|
||||||
|
(source: Observable<ResponseCacheEntry>): Observable<ResponseCacheEntry> =>
|
||||||
|
source.pipe(filter((entry: ResponseCacheEntry) => entry.response.isSuccessful));
|
||||||
|
|
||||||
|
export const getResourceLinksFromResponse = () =>
|
||||||
|
(source: Observable<ResponseCacheEntry>): Observable<string[]> =>
|
||||||
|
source.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const configureRequest = (requestService: RequestService) =>
|
||||||
|
(source: Observable<RestRequest>): Observable<RestRequest> =>
|
||||||
|
source.pipe(tap((request: RestRequest) => requestService.configure(request)));
|
||||||
|
|
||||||
|
export const getRemoteDataPayload = () =>
|
||||||
|
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||||
|
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
|
@@ -1,6 +1,16 @@
|
|||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
import {
|
import {
|
||||||
isEmpty, hasNoValue, hasValue, isNotEmpty, isNull, isNotNull,
|
ensureArrayHasValue,
|
||||||
isUndefined, isNotUndefined
|
hasNoValue,
|
||||||
|
hasValue,
|
||||||
|
hasValueOperator,
|
||||||
|
isEmpty,
|
||||||
|
isNotEmpty,
|
||||||
|
isNotEmptyOperator,
|
||||||
|
isNotNull,
|
||||||
|
isNotUndefined,
|
||||||
|
isNull,
|
||||||
|
isUndefined
|
||||||
} from './empty.util';
|
} from './empty.util';
|
||||||
|
|
||||||
describe('Empty Utils', () => {
|
describe('Empty Utils', () => {
|
||||||
@@ -274,6 +284,25 @@ describe('Empty Utils', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hasValueOperator', () => {
|
||||||
|
it('should only include items from the source observable for which hasValue is true, and omit all others', () => {
|
||||||
|
const testData = {
|
||||||
|
a: null,
|
||||||
|
b: 'test',
|
||||||
|
c: true,
|
||||||
|
d: undefined,
|
||||||
|
e: 1,
|
||||||
|
f: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const source$ = hot('abcdef', testData);
|
||||||
|
const expected$ = cold('-bc-ef', testData);
|
||||||
|
const result$ = source$.pipe(hasValueOperator());
|
||||||
|
|
||||||
|
expect(result$).toBeObservable(expected$);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isEmpty', () => {
|
describe('isEmpty', () => {
|
||||||
it('should return true for null', () => {
|
it('should return true for null', () => {
|
||||||
expect(isEmpty(null)).toBe(true);
|
expect(isEmpty(null)).toBe(true);
|
||||||
@@ -393,4 +422,56 @@ describe('Empty Utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isNotEmptyOperator', () => {
|
||||||
|
it('should only include items from the source observable for which isNotEmpty is true, and omit all others', () => {
|
||||||
|
const testData = {
|
||||||
|
a: null,
|
||||||
|
b: 'test',
|
||||||
|
c: true,
|
||||||
|
d: undefined,
|
||||||
|
e: 1,
|
||||||
|
f: {},
|
||||||
|
g: '',
|
||||||
|
h: ' '
|
||||||
|
};
|
||||||
|
|
||||||
|
const source$ = hot('abcdefgh', testData);
|
||||||
|
const expected$ = cold('-bc-e--h', testData);
|
||||||
|
const result$ = source$.pipe(isNotEmptyOperator());
|
||||||
|
|
||||||
|
expect(result$).toBeObservable(expected$);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ensureArrayHasValue', () => {
|
||||||
|
it('should let all arrays pass unchanged, and turn everything else in to empty arrays', () => {
|
||||||
|
const sourceData = {
|
||||||
|
a: { a: 'b' },
|
||||||
|
b: ['a', 'b', 'c'],
|
||||||
|
c: null,
|
||||||
|
d: [1],
|
||||||
|
e: undefined,
|
||||||
|
f: [],
|
||||||
|
g: () => true,
|
||||||
|
h: {},
|
||||||
|
i: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedData = Object.assign({}, sourceData, {
|
||||||
|
a: [],
|
||||||
|
c: [],
|
||||||
|
e: [],
|
||||||
|
g: [],
|
||||||
|
h: [],
|
||||||
|
i: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const source$ = hot('abcdefghi', sourceData);
|
||||||
|
const expected$ = cold('abcdefghi', expectedData);
|
||||||
|
const result$ = source$.pipe(ensureArrayHasValue());
|
||||||
|
|
||||||
|
expect(result$).toBeObservable(expected$);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { filter, map } from 'rxjs/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the passed value is null.
|
* Returns true if the passed value is null.
|
||||||
* isNull(); // false
|
* isNull(); // false
|
||||||
@@ -82,6 +85,14 @@ export function hasValue(obj?: any): boolean {
|
|||||||
return isNotUndefined(obj) && isNotNull(obj);
|
return isNotUndefined(obj) && isNotNull(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter items emitted by the source Observable by only emitting those for
|
||||||
|
* which hasValue is true
|
||||||
|
*/
|
||||||
|
export const hasValueOperator = () =>
|
||||||
|
<T>(source: Observable<T>): Observable<T> =>
|
||||||
|
source.pipe(filter((obj: T) => hasValue(obj)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that a value is `null` or an empty string, empty array,
|
* Verifies that a value is `null` or an empty string, empty array,
|
||||||
* or empty function.
|
* or empty function.
|
||||||
@@ -148,3 +159,21 @@ export function isEmpty(obj?: any): boolean {
|
|||||||
export function isNotEmpty(obj?: any): boolean {
|
export function isNotEmpty(obj?: any): boolean {
|
||||||
return !isEmpty(obj);
|
return !isEmpty(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter items emitted by the source Observable by only emitting those for
|
||||||
|
* which isNotEmpty is true
|
||||||
|
*/
|
||||||
|
export const isNotEmptyOperator = () =>
|
||||||
|
<T>(source: Observable<T>): Observable<T> =>
|
||||||
|
source.pipe(filter((obj: T) => isNotEmpty(obj)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests each value emitted by the source Observable,
|
||||||
|
* let's arrays pass through, turns other values in to
|
||||||
|
* empty arrays. Used to be able to chain array operators
|
||||||
|
* on something that may not have a value
|
||||||
|
*/
|
||||||
|
export const ensureArrayHasValue = () =>
|
||||||
|
<T>(source: Observable<T[]>): Observable<T[]> =>
|
||||||
|
source.pipe(map((arr: T[]): T[] => Array.isArray(arr) ? arr : []));
|
||||||
|
23
src/app/shared/mocks/mock-remote-data-build.service.ts
Normal file
23
src/app/shared/mocks/mock-remote-data-build.service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
|
||||||
|
export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService {
|
||||||
|
return {
|
||||||
|
toRemoteDataObservable: (requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<any>) => {
|
||||||
|
|
||||||
|
if (hasValue(toRemoteDataObservable$)) {
|
||||||
|
return toRemoteDataObservable$;
|
||||||
|
} else {
|
||||||
|
return payload$.pipe(map((payload) => ({
|
||||||
|
payload
|
||||||
|
} as RemoteData<any>)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as RemoteDataBuildService;
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user