Merge branch 'master' into embedded-objects-fixes

Conflicts:
	src/app/core/cache/builders/remote-data-build.service.ts
This commit is contained in:
Lotte Hofstede
2018-05-28 10:43:47 +02:00
23 changed files with 972 additions and 170 deletions

View File

@@ -1,5 +1,60 @@
# 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
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.

View File

@@ -23,6 +23,7 @@ import { RequestService } from '../../core/data/request.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { configureRequest } from '../../core/shared/operators';
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
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(
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(
@@ -188,7 +189,7 @@ export class SearchService implements OnDestroy {
}
});
}),
tap((request: RestRequest) => this.requestService.configure(request)),
configureRequest(this.requestService)
);
const requestEntryObs = requestObs.pipe(

View File

@@ -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 { 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 { 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', () => {
let scheduler: TestScheduler;
let service: BrowseService;
let responseCache: ResponseCacheService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
const browsesEndpointURL = 'https://rest.api/browses';
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
const browseDefinitions = [
Object.assign(new BrowseDefinition(), {
id: 'date',
metadataBrowse: false,
sortOptions: [
{
@@ -45,6 +49,7 @@ describe('BrowseService', () => {
}
}),
Object.assign(new BrowseDefinition(), {
id: 'author',
metadataBrowse: true,
sortOptions: [
{
@@ -80,7 +85,7 @@ describe('BrowseService', () => {
b: {
response: {
isSuccessful,
browseDefinitions,
payload: browseDefinitions,
}
}
}));
@@ -91,7 +96,8 @@ describe('BrowseService', () => {
return new BrowseService(
responseCache,
requestService,
halService
halService,
rdbService
);
}
@@ -99,15 +105,99 @@ describe('BrowseService', () => {
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('if getEndpoint fires', () => {
describe('if getBrowseDefinitions fires', () => {
beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(halService, 'getEndpoint').and
.returnValue(hot('--a-', { a: browsesEndpointURL }));
spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('--a-', { a: {
payload: browseDefinitions
}}));
});
it('should return the URL for the given metadatumKey and linkPath', () => {
@@ -152,26 +242,15 @@ describe('BrowseService', () => {
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', () => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(halService, 'getEndpoint').and
spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('----'));
const metadatumKey = 'dc.date.issued';
@@ -182,22 +261,5 @@ describe('BrowseService', () => {
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);
});
});
});
});

View File

@@ -1,15 +1,34 @@
import { Inject, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import {
ensureArrayHasValue,
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 { 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 { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import {
configureRequest,
filterSuccessfulResponses,
getRemoteDataPayload,
getRequestFromSelflink,
getResponseFromSelflink
} from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner';
@Injectable()
export class BrowseService {
@@ -31,42 +50,106 @@ export class BrowseService {
constructor(
protected responseCache: ResponseCacheService,
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> {
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
return this.halService.getEndpoint(this.linkPath)
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: RestRequest) => this.requestService.configure(request))
.flatMap((request: RestRequest) => {
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`Couldn't retrieve the browses endpoint`))),
successResponse
.filter((response: BrowseSuccessResponse) => isNotEmpty(response.browseDefinitions))
.map((response: BrowseSuccessResponse) => response.browseDefinitions)
.map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.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();
return this.getBrowseDefinitions().pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.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()
);
}
}

View File

@@ -1,10 +1,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { map, tap } from 'rxjs/operators';
import { NormalizedSearchResult } from '../../../+search-page/normalized-search-result.model';
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 { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
import { RemoteDataError } from '../../data/remote-data-error';
@@ -12,13 +9,19 @@ import { GetRequest } from '../../data/request.models';
import { RequestEntry } from '../../data/request.reducer';
import { RequestService } from '../../data/request.service';
import { NormalizedObject } from '../models/normalized-object.model';
import { ObjectCacheService } from '../object-cache.service';
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
import { ResponseCacheEntry } from '../response-cache.reducer';
import { ResponseCacheService } from '../response-cache.service';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { NormalizedObject } from '../models/normalized-object.model';
import { PageInfo } from '../../shared/page-info.model';
import {
getRequestFromSelflink,
getResourceLinksFromResponse,
getResponseFromSelflink,
filterSuccessfulResponses
} from '../../shared/operators';
@Injectable()
export class RemoteDataBuildService {
@@ -27,43 +30,42 @@ export class RemoteDataBuildService {
protected requestService: RequestService) {
}
buildSingle<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<TDomain>> {
if (typeof hrefObs === 'string') {
hrefObs = Observable.of(hrefObs);
buildSingle<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain>> {
if (typeof href$ === 'string') {
href$ = Observable.of(href$);
}
const requestHrefObs = hrefObs.flatMap((href: string) =>
this.objectCache.getRequestHrefBySelfLink(href));
const requestHref$ = href$.pipe(flatMap((href: string) =>
this.objectCache.getRequestHrefBySelfLink(href)));
const requestEntryObs = Observable.race(
hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
.filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) =>
this.requestService.getByHref(requestHref)).filter((entry) => hasValue(entry))
const requestEntry$ = Observable.race(
href$.pipe(getRequestFromSelflink(this.requestService)),
requestHref$.pipe(getRequestFromSelflink(this.requestService))
);
const responseCacheObs = Observable.race(
hrefObs.flatMap((href: string) => this.responseCache.get(href))
.filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
const responseCache$ = Observable.race(
href$.pipe(getResponseFromSelflink(this.responseCache)),
requestHref$.pipe(getResponseFromSelflink(this.responseCache))
);
// always use self link if that is cached, only if it isn't, get it via the response.
const payloadObs =
const payload$ =
Observable.combineLatest(
hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href))
.startWith(undefined),
responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
.flatMap((resourceSelfLinks: string[]) => {
href$.pipe(
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
startWith(undefined)
),
responseCache$.pipe(
getResourceLinksFromResponse(),
flatMap((resourceSelfLinks: string[]) => {
if (isNotEmpty(resourceSelfLinks)) {
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
} else {
return Observable.of(undefined);
}
})
.distinctUntilChanged()
.startWith(undefined),
}),
distinctUntilChanged(),
startWith(undefined)
),
(fromSelfLink, fromResponse) => {
if (hasValue(fromSelfLink)) {
return fromSelfLink;
@@ -71,18 +73,19 @@ export class RemoteDataBuildService {
return fromResponse;
}
}
).filter((normalized) => hasValue(normalized))
.map((normalized: TNormalized) => {
).pipe(
hasValueOperator(),
map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized);
})
.startWith(undefined)
.distinctUntilChanged();
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
}),
startWith(undefined),
distinctUntilChanged()
);
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
}
toRemoteDataObservable<T>(requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<T>) {
return Observable.combineLatest(requestEntryObs, responseCacheObs.startWith(undefined), payloadObs,
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
return Observable.combineLatest(requestEntry$, responseCache$.startWith(undefined), payload$,
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
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>>> {
if (typeof hrefObs === 'string') {
hrefObs = Observable.of(hrefObs);
buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<PaginatedList<TDomain>>> {
if (typeof href$ === 'string') {
href$ = Observable.of(href$);
}
const requestEntryObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
.filter((entry) => hasValue(entry));
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
.filter((entry) => hasValue(entry));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const tDomainListObs = responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
.flatMap((resourceUUIDs: string[]) => {
const tDomainList$ = responseCache$.pipe(
getResourceLinksFromResponse(),
flatMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs)
.map((normList: TNormalized[]) => {
return normList.map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized);
});
});
})
.startWith([])
.distinctUntilChanged();
}),
startWith([]),
distinctUntilChanged()
);
const pageInfoObs = responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => {
const pageInfo$ = responseCache$.pipe(
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => {
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
@@ -140,13 +141,14 @@ export class RemoteDataBuildService {
return resPageInfo;
}
}
});
})
);
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
const payload$ = Observable.combineLatest(tDomainList$, pageInfo$, (tDomainList, pageInfo) => {
return new PaginatedList(pageInfo, tDomainList);
});
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
}
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {

View File

@@ -1,5 +1,6 @@
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { RequestError } from '../data/request.models';
import { BrowseEntry } from '../shared/browse-entry.model';
import { PageInfo } from '../shared/page-info.model';
import { BrowseDefinition } from '../shared/browse-definition.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(
public browseDefinitions: BrowseDefinition[],
public statusCode: string
public payload: T,
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}

View File

@@ -15,6 +15,7 @@ import { coreReducers } from './core.reducers';
import { isNotEmpty } from '../shared/empty.util';
import { ApiService } from '../shared/api.service';
import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service';
import { CollectionDataService } from './data/collection-data.service';
import { CommunityDataService } from './data/community-data.service';
import { DebugResponseParsingService } from './data/debug-response-parsing.service';
@@ -83,6 +84,7 @@ const PROVIDERS = [
SearchResponseParsingService,
ServerResponseService,
BrowseResponseParsingService,
BrowseEntriesResponseParsingService,
BrowseService,
ConfigResponseParsingService,
RouteService,

View File

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

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

View File

@@ -1,6 +1,6 @@
import { BrowseResponseParsingService } from './browse-response-parsing.service';
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 { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
@@ -12,7 +12,7 @@ describe('BrowseResponseParsingService', () => {
});
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 = {
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);
expect(response.constructor).toBe(BrowseSuccessResponse);
expect(response.constructor).toBe(GenericSuccessResponse);
});
it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => {
@@ -155,9 +155,9 @@ describe('BrowseResponseParsingService', () => {
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);
expect((response as BrowseSuccessResponse).browseDefinitions).toEqual(definitions);
expect((response as GenericSuccessResponse<BrowseDefinition[]>).payload).toEqual(definitions);
});
});

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
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 { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
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]])) {
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
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 {
return new ErrorResponse(
Object.assign(

View File

@@ -2,6 +2,7 @@ import { SortOptions } from '../cache/models/sort-options.model';
import { GenericConstructor } from '../shared/generic-constructor';
import { GlobalConfig } from '../../../config/global-config.interface';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './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 {
constructor(uuid: string, href: string) {
super(uuid, href);

View File

@@ -2,6 +2,9 @@ import { autoserialize, autoserializeAs } from 'cerialize';
import { SortOption } from './sort-option.model';
export class BrowseDefinition {
@autoserialize
id: string;
@autoserialize
metadataBrowse: boolean;

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

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

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

View File

@@ -1,6 +1,16 @@
import { cold, hot } from 'jasmine-marbles';
import {
isEmpty, hasNoValue, hasValue, isNotEmpty, isNull, isNotNull,
isUndefined, isNotUndefined
ensureArrayHasValue,
hasNoValue,
hasValue,
hasValueOperator,
isEmpty,
isNotEmpty,
isNotEmptyOperator,
isNotNull,
isNotUndefined,
isNull,
isUndefined
} from './empty.util';
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', () => {
it('should return true for null', () => {
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$);
});
});
});

View File

@@ -1,3 +1,6 @@
import { Observable } from 'rxjs/Observable';
import { filter, map } from 'rxjs/operators';
/**
* Returns true if the passed value is null.
* isNull(); // false
@@ -82,6 +85,14 @@ export function hasValue(obj?: any): boolean {
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,
* or empty function.
@@ -148,3 +159,21 @@ export function isEmpty(obj?: any): boolean {
export function isNotEmpty(obj?: any): boolean {
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 : []));

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

View File

@@ -1,10 +1,11 @@
import { Observable } from 'rxjs/Observable';
import { RequestService } from '../../core/data/request.service';
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', {
configure: () => false,
generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
getByHref: (uuid: string) => new RequestEntry()
configure: false,
generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
getByHref: getByHref$
});
}

View File

@@ -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 { 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', {
add: (key: string, response: RestResponse, msToLive: number) => new ResponseCacheEntry(),
get: (key: string) => new ResponseCacheEntry(),
has: (key: string) => false,
add: add$,
get: get$,
has,
});
}

View File

@@ -4,6 +4,7 @@ import { InjectionToken } from '@angular/core';
import { Config } from './config/config.interface';
import { ServerConfig } from './config/server-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');
@@ -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();
// set config for whether running in production

View File

@@ -21,8 +21,6 @@ import { ENV_CONFIG } from './config';
export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
const app = express();
const port = ENV_CONFIG.ui.port ? ENV_CONFIG.ui.port : 80;
if (ENV_CONFIG.production) {
enableProdMode();
app.use(compression());
@@ -90,7 +88,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
https.createServer({
key: keys.serviceKey,
cert: keys.certificate
}, app).listen(port, ENV_CONFIG.ui.host, () => {
}, app).listen(ENV_CONFIG.ui.port, ENV_CONFIG.ui.host, () => {
serverStarted();
});
}
@@ -127,7 +125,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
});
}
} else {
app.listen(port, ENV_CONFIG.ui.host, () => {
app.listen(ENV_CONFIG.ui.port, ENV_CONFIG.ui.host, () => {
serverStarted();
});
}}