mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 07:23:03 +00:00
Merge branch 'master' into embedded-objects-fixes
Conflicts: src/app/core/cache/builders/remote-data-build.service.ts
This commit is contained in:
@@ -1,5 +1,60 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
|
Default configuration file is located in `config/` folder. All configuration options should be listed in the default configuration file `config/environment.default.js`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change:
|
||||||
|
|
||||||
|
- Create a new `environment.dev.js` file in `config/` for `devel` environment;
|
||||||
|
- Create a new `environment.prod.js` file in `config/` for `production` environment;
|
||||||
|
|
||||||
|
Some few configuration options can be overridden by setting environment variables. These and the variable names are listed below.
|
||||||
|
|
||||||
|
## Nodejs server
|
||||||
|
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itsself to, and if ssl should be enabled not. By default it listens on `localhost:3000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
||||||
|
|
||||||
|
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
||||||
|
```
|
||||||
|
module.exports = {
|
||||||
|
// Angular Universal server settings.
|
||||||
|
ui: {
|
||||||
|
ssl: false,
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3000,
|
||||||
|
nameSpace: '/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
|
```
|
||||||
|
DSPACE_SSL=true
|
||||||
|
DSPACE_HOST=localhost
|
||||||
|
DSPACE_PORT=3000
|
||||||
|
DSPACE_NAMESPACE=/
|
||||||
|
```
|
||||||
|
|
||||||
|
## DSpace's REST endpoint
|
||||||
|
dspace-angular connects to your DSpace installation by using its REST endpoint. To do so, you have to define the ip address, port and if ssl should be enabled. You can do this in a configuration file (see above) by adding the following options:
|
||||||
|
|
||||||
|
```
|
||||||
|
module.exports = {
|
||||||
|
// The REST API server settings.
|
||||||
|
rest: {
|
||||||
|
ssl: true,
|
||||||
|
host: 'dspace7.4science.it',
|
||||||
|
port: 443,
|
||||||
|
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
|
nameSpace: '/dspace-spring-rest/api'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
|
```
|
||||||
|
DSPACE_REST_SSL=true
|
||||||
|
DSPACE_REST_HOST=localhost
|
||||||
|
DSPACE_REST_PORT=3000
|
||||||
|
DSPACE_REST_NAMESPACE=/
|
||||||
|
```
|
||||||
|
|
||||||
## Supporting analytics services other than Google Analytics
|
## Supporting analytics services other than Google Analytics
|
||||||
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
||||||
|
|
||||||
|
@@ -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,10 +1,7 @@
|
|||||||
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';
|
||||||
@@ -12,13 +9,19 @@ 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 { NormalizedObject } from '../models/normalized-object.model';
|
||||||
import { ObjectCacheService } from '../object-cache.service';
|
import { ObjectCacheService } from '../object-cache.service';
|
||||||
import { DSOSuccessResponse, ErrorResponse } 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 { PageInfo } from '../../shared/page-info.model';
|
import { PageInfo } from '../../shared/page-info.model';
|
||||||
|
import {
|
||||||
|
getRequestFromSelflink,
|
||||||
|
getResourceLinksFromResponse,
|
||||||
|
getResponseFromSelflink,
|
||||||
|
filterSuccessfulResponses
|
||||||
|
} from '../../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteDataBuildService {
|
export class RemoteDataBuildService {
|
||||||
@@ -27,43 +30,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;
|
||||||
@@ -71,18 +73,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(requestEntry$, responseCache$.startWith(undefined), payload$,
|
||||||
return Observable.combineLatest(requestEntryObs, responseCacheObs.startWith(undefined), payloadObs,
|
|
||||||
(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;
|
||||||
@@ -105,33 +108,31 @@ export class RemoteDataBuildService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buildList<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<PaginatedList<TDomain>>> {
|
buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<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) {
|
||||||
@@ -140,13 +141,14 @@ export class RemoteDataBuildService {
|
|||||||
return resPageInfo;
|
return resPageInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
|
const payload$ = Observable.combineLatest(tDomainList$, pageInfo$, (tDomainList, pageInfo) => {
|
||||||
return new PaginatedList(pageInfo, tDomainList);
|
return new PaginatedList(pageInfo, tDomainList);
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
||||||
|
@@ -0,0 +1,146 @@
|
|||||||
|
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
||||||
|
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
|
||||||
|
import { BrowseEntriesRequest } from './request.models';
|
||||||
|
|
||||||
|
describe('BrowseEntriesResponseParsingService', () => {
|
||||||
|
let service: BrowseEntriesResponseParsingService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new BrowseEntriesResponseParsingService(undefined, getMockObjectCacheService());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
const request = new BrowseEntriesRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/entries');
|
||||||
|
|
||||||
|
const validResponse = {
|
||||||
|
payload: {
|
||||||
|
_embedded: {
|
||||||
|
browseEntries: [
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Arulmozhiyal, Ramaswamy',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Bastida-Jumilla, Ma Consuelo',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Bastida-Jumilla, Ma Consuelo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Cao, Binggang',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Cao, Binggang'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Castelli, Mauro',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Castelli, Mauro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Cat, Lily',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Cat, Lily'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
first: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries?page=0&size=5'
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries'
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries?page=1&size=5'
|
||||||
|
},
|
||||||
|
last: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries?page=9&size=5'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
size: 5,
|
||||||
|
totalElements: 50,
|
||||||
|
totalPages: 10,
|
||||||
|
number: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusCode: '200'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
const invalidResponseNotAList = {
|
||||||
|
payload: {
|
||||||
|
authority: null,
|
||||||
|
value: 'Arulmozhiyal, Ramaswamy',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries'
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statusCode: '200'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
const invalidResponseStatusCode = {
|
||||||
|
payload: {}, statusCode: '500'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
it('should return a GenericSuccessResponse if data contains a valid browse entries response', () => {
|
||||||
|
const response = service.parse(request, validResponse);
|
||||||
|
expect(response.constructor).toBe(GenericSuccessResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains an invalid browse entries response', () => {
|
||||||
|
const response = service.parse(request, invalidResponseNotAList);
|
||||||
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains a statuscode other than 200', () => {
|
||||||
|
const response = service.parse(request, invalidResponseStatusCode);
|
||||||
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
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';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('parse', () => {
|
describe('parse', () => {
|
||||||
const validRequest = new BrowseEndpointRequest('clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
|
const validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
|
||||||
|
|
||||||
const validResponse = {
|
const validResponse = {
|
||||||
payload: {
|
payload: {
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
152
src/app/core/shared/operators.spec.ts
Normal file
152
src/app/core/shared/operators.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from '../../../../node_modules/rxjs';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { GetRequest, RestRequest } from '../data/request.models';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import {
|
||||||
|
configureRequest,
|
||||||
|
filterSuccessfulResponses, getRemoteDataPayload,
|
||||||
|
getRequestFromSelflink, getResourceLinksFromResponse,
|
||||||
|
getResponseFromSelflink
|
||||||
|
} from './operators';
|
||||||
|
|
||||||
|
describe('Core Module - RxJS Operators', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let requestService: RequestService;
|
||||||
|
const testSelfLink = 'https://rest.api/';
|
||||||
|
|
||||||
|
const testRCEs = {
|
||||||
|
a: { response: { isSuccessful: true, resourceSelfLinks: ['a', 'b', 'c', 'd'] } },
|
||||||
|
b: { response: { isSuccessful: false, resourceSelfLinks: ['e', 'f'] } },
|
||||||
|
c: { response: { isSuccessful: undefined, resourceSelfLinks: ['g', 'h', 'i'] } },
|
||||||
|
d: { response: { isSuccessful: true, resourceSelfLinks: ['j', 'k', 'l', 'm', 'n'] } },
|
||||||
|
e: { response: { isSuccessful: 1, resourceSelfLinks: [] } }
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRequestFromSelflink', () => {
|
||||||
|
|
||||||
|
it('should return the RequestEntry corresponding to the self link in the source', () => {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getRequestFromSelflink(requestService));
|
||||||
|
const expected = cold('a', { a: new RequestEntry()});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the requestService to fetch the request by its self link', () => {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
scheduler.schedule(() => source.pipe(getRequestFromSelflink(requestService)).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.getByHref).toHaveBeenCalledWith(testSelfLink)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldn\'t return anything if there is no request matching the self link', () => {
|
||||||
|
requestService = getMockRequestService(cold('a', { a: undefined }));
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getRequestFromSelflink(requestService));
|
||||||
|
const expected = cold('-');
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResponseFromSelflink', () => {
|
||||||
|
let responseCacheService: ResponseCacheService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the ResponseCacheEntry corresponding to the self link in the source', () => {
|
||||||
|
responseCacheService = getMockResponseCacheService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
||||||
|
const expected = cold('a', { a: new ResponseCacheEntry()});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the responseCacheService to fetch the response by the request\'s link', () => {
|
||||||
|
responseCacheService = getMockResponseCacheService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
scheduler.schedule(() => source.pipe(getResponseFromSelflink(responseCacheService)).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(responseCacheService.get).toHaveBeenCalledWith(testSelfLink)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldn\'t return anything if there is no response matching the request\'s link', () => {
|
||||||
|
responseCacheService = getMockResponseCacheService(undefined, cold('a', { a: undefined }));
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
||||||
|
const expected = cold('-');
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filterSuccessfulResponses', () => {
|
||||||
|
it('should only return responses for which isSuccessful === true', () => {
|
||||||
|
const source = hot('abcde', testRCEs);
|
||||||
|
const result = source.pipe(filterSuccessfulResponses());
|
||||||
|
const expected = cold('a--d-', testRCEs);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResourceLinksFromResponse', () => {
|
||||||
|
it('should return the resourceSelfLinks for all successful responses', () => {
|
||||||
|
const source = hot('abcde', testRCEs);
|
||||||
|
const result = source.pipe(getResourceLinksFromResponse());
|
||||||
|
const expected = cold('a--d-', {
|
||||||
|
a: testRCEs.a.response.resourceSelfLinks,
|
||||||
|
d: testRCEs.d.response.resourceSelfLinks
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('configureRequest', () => {
|
||||||
|
it('should call requestService.configure with the source request', () => {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
const testRequest = new GetRequest('6b789e31-f026-4ff8-8993-4eb3b730c841', testSelfLink);
|
||||||
|
const source = hot('a', { a: testRequest });
|
||||||
|
scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(testRequest)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRemoteDataPayload', () => {
|
||||||
|
it('should return the payload of the source RemoteData', () => {
|
||||||
|
const testRD = { a: { payload: 'a' } };
|
||||||
|
const source = hot('a', testRD);
|
||||||
|
const result = source.pipe(getRemoteDataPayload());
|
||||||
|
const expected = cold('a', {
|
||||||
|
a: testRD.a.payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
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 === true));
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
@@ -1,10 +1,11 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
import { RequestEntry } from '../../core/data/request.reducer';
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
|
|
||||||
export function getMockRequestService(): RequestService {
|
export function getMockRequestService(getByHref$: Observable<RequestEntry> = Observable.of(new RequestEntry())): RequestService {
|
||||||
return jasmine.createSpyObj('requestService', {
|
return jasmine.createSpyObj('requestService', {
|
||||||
configure: () => false,
|
configure: false,
|
||||||
generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
||||||
getByHref: (uuid: string) => new RequestEntry()
|
getByHref: getByHref$
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||||
import { RestResponse } from '../../core/cache/response-cache.models';
|
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
||||||
|
|
||||||
export function getMockResponseCacheService(): ResponseCacheService {
|
export function getMockResponseCacheService(
|
||||||
|
add$: Observable<ResponseCacheEntry> = Observable.of(new ResponseCacheEntry()),
|
||||||
|
get$: Observable<ResponseCacheEntry> = Observable.of(new ResponseCacheEntry()),
|
||||||
|
has: boolean = false
|
||||||
|
): ResponseCacheService {
|
||||||
return jasmine.createSpyObj('ResponseCacheService', {
|
return jasmine.createSpyObj('ResponseCacheService', {
|
||||||
add: (key: string, response: RestResponse, msToLive: number) => new ResponseCacheEntry(),
|
add: add$,
|
||||||
get: (key: string) => new ResponseCacheEntry(),
|
get: get$,
|
||||||
has: (key: string) => false,
|
has,
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import { InjectionToken } from '@angular/core';
|
|||||||
import { Config } from './config/config.interface';
|
import { Config } from './config/config.interface';
|
||||||
import { ServerConfig } from './config/server-config.interface';
|
import { ServerConfig } from './config/server-config.interface';
|
||||||
import { GlobalConfig } from './config/global-config.interface';
|
import { GlobalConfig } from './config/global-config.interface';
|
||||||
|
import { hasValue } from './app/shared/empty.util';
|
||||||
|
|
||||||
const GLOBAL_CONFIG: InjectionToken<GlobalConfig> = new InjectionToken<GlobalConfig>('config');
|
const GLOBAL_CONFIG: InjectionToken<GlobalConfig> = new InjectionToken<GlobalConfig>('config');
|
||||||
|
|
||||||
@@ -55,6 +56,41 @@ if (envConfigFile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow to override a few important options by environment variables
|
||||||
|
function createServerConfig(host?: string, port?: string, nameSpace?: string, ssl?: string): ServerConfig {
|
||||||
|
const result = { host, nameSpace } as any;
|
||||||
|
|
||||||
|
if (hasValue(port)) {
|
||||||
|
result.port = Number(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(ssl)) {
|
||||||
|
result.ssl = ssl.trim().match(/^(true|1|yes)$/i) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processEnv = {
|
||||||
|
ui: createServerConfig(
|
||||||
|
process.env.DSPACE_HOST,
|
||||||
|
process.env.DSPACE_PORT,
|
||||||
|
process.env.DSPACE_NAMESPACE,
|
||||||
|
process.env.DSPACE_SSL),
|
||||||
|
rest: createServerConfig(
|
||||||
|
process.env.DSPACE_REST_HOST,
|
||||||
|
process.env.DSPACE_REST_PORT,
|
||||||
|
process.env.DSPACE_REST_NAMESPACE,
|
||||||
|
process.env.DSPACE_REST_SSL)
|
||||||
|
} as GlobalConfig;
|
||||||
|
|
||||||
|
// merge the environment variables with our configuration.
|
||||||
|
try {
|
||||||
|
merge(processEnv)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Unable to merge environment variable into the configuration')
|
||||||
|
}
|
||||||
|
|
||||||
buildBaseUrls();
|
buildBaseUrls();
|
||||||
|
|
||||||
// set config for whether running in production
|
// set config for whether running in production
|
||||||
|
@@ -21,8 +21,6 @@ import { ENV_CONFIG } from './config';
|
|||||||
export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const port = ENV_CONFIG.ui.port ? ENV_CONFIG.ui.port : 80;
|
|
||||||
|
|
||||||
if (ENV_CONFIG.production) {
|
if (ENV_CONFIG.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
@@ -90,7 +88,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
|||||||
https.createServer({
|
https.createServer({
|
||||||
key: keys.serviceKey,
|
key: keys.serviceKey,
|
||||||
cert: keys.certificate
|
cert: keys.certificate
|
||||||
}, app).listen(port, ENV_CONFIG.ui.host, () => {
|
}, app).listen(ENV_CONFIG.ui.port, ENV_CONFIG.ui.host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,7 +125,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.listen(port, ENV_CONFIG.ui.host, () => {
|
app.listen(ENV_CONFIG.ui.port, ENV_CONFIG.ui.host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
Reference in New Issue
Block a user