get browse endpoints from hal links

This commit is contained in:
Art Lowel
2017-10-12 12:51:58 +02:00
parent 3580fe4ffb
commit a84eb533be
22 changed files with 346 additions and 80 deletions

View File

@@ -22,6 +22,7 @@ import { Observable } from 'rxjs/Observable';
selector: 'ds-collection-page',
styleUrls: ['./collection-page.component.scss'],
templateUrl: './collection-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CollectionPageComponent implements OnInit, OnDestroy {
collectionData: RemoteData<Collection>;
@@ -37,8 +38,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
private route: ActivatedRoute) {
this.paginationConfig = new PaginationComponentOptions();
this.paginationConfig.id = 'collection-page-pagination';
this.paginationConfig.pageSizeOptions = [4];
this.paginationConfig.pageSize = 4;
this.paginationConfig.pageSize = 5;
this.paginationConfig.currentPage = 1;
this.sortConfig = new SortOptions();
}

View File

@@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
@@ -13,6 +13,7 @@ import { hasValue } from '../shared/empty.util';
selector: 'ds-community-page',
styleUrls: ['./community-page.component.scss'],
templateUrl: './community-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommunityPageComponent implements OnInit, OnDestroy {
communityData: RemoteData<Community>;

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'ds-home',
styleUrls: ['./home.component.scss'],
templateUrl: './home.component.html'
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent {

View File

@@ -40,5 +40,7 @@ export class TopLevelCommunityListComponent {
elementsPerPage: data.pageSize,
sort: { field: data.sortField, direction: data.sortDirection }
});
this.cds.getScopedEndpoint('7669c72a-3f2a-451f-a3b9-9210e7a4c02f')
.subscribe((c) => console.log('communities', c))
}
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@@ -16,6 +16,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
selector: 'ds-item-page',
styleUrls: ['./item-page.component.scss'],
templateUrl: './item-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemPageComponent implements OnInit {

View File

@@ -40,6 +40,7 @@ export function createTranslateLoader(http: HttpClient) {
// forRoot ensures the providers are only created once
IdlePreloadModule.forRoot(),
RouterModule.forRoot([], {
// enableTracing: true,
useHash: false,
preloadingStrategy:
IdlePreload

View File

@@ -0,0 +1,62 @@
import { Inject, Injectable } from '@angular/core';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../config';
import { BrowseEndpointRequest, RestRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { BrowseSuccessResponse } from '../cache/response-cache.models';
import { isNotEmpty } from '../../shared/empty.util';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class BrowseService extends HALEndpointService {
protected linkName = 'browses';
private static toSearchKeyArray(metadatumKey: string): string[] {
const keyParts = metadatumKey.split('.');
const searchFor = [];
searchFor.push('*');
for (let i = 0; i < keyParts.length - 1; i++) {
const prevParts = keyParts.slice(0, i + 1);
const nextPart = [...prevParts, '*'].join('.');
searchFor.push(nextPart);
}
searchFor.push(metadatumKey);
return searchFor;
}
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
super();
}
getBrowseURLFor(metadatumKey: string, linkName: string): Observable<string> {
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
return this.getEndpoint()
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => new BrowseEndpointRequest(endpointURL))
.do((request: RestRequest) => {
setTimeout(() => {
this.requestService.configure(request);
}, 0);
})
.flatMap((request: RestRequest) => this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.filter((response: BrowseSuccessResponse) => isNotEmpty(response) && 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 matchingKeys.length > 0
})
).map((def: BrowseDefinition) => def._links[linkName])
);
}
}

View File

@@ -66,4 +66,14 @@ export abstract class NormalizedDSpaceObject extends NormalizedObject {
@autoserialize
owner: string;
/**
* The links to all related resources returned by the rest api.
*
* Repeated here to make the serialization work,
* inheritSerialization doesn't seem to work for more than one level
*/
@autoserialize
_links: {
[name: string]: string
}
}

View File

@@ -17,4 +17,8 @@ export abstract class NormalizedObject implements CacheableObject {
@autoserialize
uuid: string;
@autoserialize
_links: {
[name: string]: string
}
}

View File

@@ -1,5 +1,6 @@
import { RequestError } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model';
import { BrowseDefinition } from '../shared/browse-definition.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
@@ -32,6 +33,15 @@ export class RootSuccessResponse extends RestResponse {
}
}
export class BrowseSuccessResponse extends RestResponse {
constructor(
public browseDefinitions: BrowseDefinition[],
public statusCode: string
) {
super(true, statusCode);
}
}
export class ErrorResponse extends RestResponse {
errorMessage: string;

View File

@@ -24,6 +24,8 @@ import { HostWindowService } from '../shared/host-window.service';
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
import { ServerResponseService } from '../shared/server-response.service';
import { BrowseService } from './browse/browse.service';
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
const IMPORTS = [
CommonModule,
@@ -54,6 +56,8 @@ const PROVIDERS = [
ResponseCacheService,
RootResponseParsingService,
ServerResponseService,
BrowseResponseParsingService,
BrowseService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
];

View File

@@ -0,0 +1,28 @@
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 { isNotEmpty } from '../../shared/empty.util';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BrowseDefinition } from '../shared/browse-definition.model';
@Injectable()
export class BrowseResponseParsingService implements ResponseParsingService {
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(BrowseDefinition);
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
return new BrowseSuccessResponse(browseDefinitions, data.statusCode);
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from browse endpoint'),
{ statusText: data.statusCode }
)
);
}
}
}

View File

@@ -9,20 +9,54 @@ import { CoreState } from '../core.reducers';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { Observable } from 'rxjs/Observable';
import { CommunityDataService } from './community-data.service';
import { FindByIDRequest } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { isNotEmpty } from '../../shared/empty.util';
@Injectable()
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
protected linkName = 'collections';
protected browseEndpoint = '/discover/browses/dateissued/collections';
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private cds: CommunityDataService,
protected objectCache: ObjectCacheService
) {
super(NormalizedCollection, EnvConfig);
super(NormalizedCollection);
}
/**
* Get the scoped endpoint URL by fetching the object with
* the given scopeID and returning its HAL link with this
* data-service's linkName
*
* @param {string} scopeID
* the id of the scope object
* @return { Observable<string> }
* an Observable<string> containing the scoped URL
*/
public getScopedEndpoint(scopeID: string): Observable<string> {
this.cds.getEndpoint()
.map((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID))
.filter((href: string) => isNotEmpty(href))
.take(1)
.subscribe((href: string) => {
const request = new FindByIDRequest(href, scopeID);
setTimeout(() => {
this.requestService.configure(request);
}, 0);
});
return this.objectCache.getByUUID(scopeID, NormalizedCommunity)
.map((nc: NormalizedCommunity) => nc._links[this.linkName])
.filter((href) => isNotEmpty(href))
.distinctUntilChanged();
}
}

View File

@@ -10,20 +10,52 @@ import { CoreState } from '../core.reducers';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { Observable } from 'rxjs/Observable';
import { isNotEmpty } from '../../shared/empty.util';
import { FindByIDRequest } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service';
@Injectable()
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
protected linkName = 'communities';
protected browseEndpoint = '/discover/browses/dateissued/communities';
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService
) {
super(NormalizedCommunity, EnvConfig);
super(NormalizedCommunity);
}
/**
* Get the scoped endpoint URL by fetching the object with
* the given scopeID and returning its HAL link with this
* data-service's linkName
*
* @param {string} scopeID
* the id of the scope object
* @return { Observable<string> }
* an Observable<string> containing the scoped URL
*/
public getScopedEndpoint(scopeID: string): Observable<string> {
this.getEndpoint()
.map((endpoint: string) => this.getFindByIDHref(endpoint, scopeID))
.filter((href: string) => isNotEmpty(href))
.take(1)
.subscribe((href: string) => {
const request = new FindByIDRequest(href, scopeID);
setTimeout(() => {
this.requestService.configure(request);
}, 0);
});
return this.objectCache.getByUUID(scopeID, NormalizedCommunity)
.map((nc: NormalizedCommunity) => nc._links[this.linkName])
.filter((href) => isNotEmpty(href))
.distinctUntilChanged();
}
}

View File

@@ -1,71 +1,41 @@
import { ResponseCacheService } from '../cache/response-cache.service';
import { CacheableObject } from '../cache/object-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteData } from './remote-data';
import {
FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest,
RootEndpointRequest
} from './request.models';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { GenericConstructor } from '../shared/generic-constructor';
import { GlobalConfig } from '../../../config';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { Observable } from 'rxjs/Observable';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
import { GlobalConfig } from '../../../config';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CacheableObject } from '../cache/object-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers';
import { GenericConstructor } from '../shared/generic-constructor';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteData } from './remote-data';
import { FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest } from './request.models';
import { RequestService } from './request.service';
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
export abstract class DataService<TNormalized extends CacheableObject, TDomain> extends HALEndpointService {
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService;
protected abstract rdbService: RemoteDataBuildService;
protected abstract store: Store<CoreState>;
protected abstract linkName: string;
protected abstract browseEndpoint: string;
protected abstract EnvConfig: GlobalConfig
constructor(
private normalizedResourceType: GenericConstructor<TNormalized>,
protected EnvConfig: GlobalConfig
) {
super();
}
private getEndpointMap(): Observable<EndpointMap> {
const request = new RootEndpointRequest(this.EnvConfig);
this.requestService.configure(request);
return this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
.map((response: RootSuccessResponse) => response.endpointMap)
.distinctUntilChanged();
}
public abstract getScopedEndpoint(scope: string): Observable<string>
public getEndpoint(): Observable<string> {
const request = new RootEndpointRequest(this.EnvConfig);
this.requestService.configure(request);
return this.getEndpointMap()
.map((map: EndpointMap) => map[this.linkName])
.distinctUntilChanged();
}
public isEnabledOnRestApi(): Observable<boolean> {
return this.getEndpointMap()
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
.startWith(undefined)
.distinctUntilChanged();
}
protected getFindAllHref(endpoint, options: FindAllOptions = {}): string {
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> {
let result;
const args = [];
if (hasValue(options.scopeID)) {
result = new RESTURLCombiner(this.EnvConfig, this.browseEndpoint).toString();
args.push(`scope=${options.scopeID}`);
result = this.getScopedEndpoint(options.scopeID);
} else {
result = endpoint;
result = Observable.of(endpoint);
}
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
@@ -86,25 +56,28 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
}
if (isNotEmpty(args)) {
result = `${result}?${args.join('&')}`;
return result.map((href: string) => `${href}?${args.join('&')}`);
} else {
return result;
}
return result;
}
findAll(options: FindAllOptions = {}): RemoteData<TDomain[]> {
const hrefObs = this.getEndpoint()
.map((endpoint: string) => this.getFindAllHref(endpoint, options));
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
hrefObs
.subscribe((href: string) => {
const request = new FindAllRequest(href, options);
this.requestService.configure(request);
setTimeout(() => {
this.requestService.configure(request);
}, 0);
});
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
}
protected getFindByIDHref(endpoint, resourceID): string {
getFindByIDHref(endpoint, resourceID): string {
return `${endpoint}/${resourceID}`;
}
@@ -115,14 +88,18 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
hrefObs
.subscribe((href: string) => {
const request = new FindByIDRequest(href, id);
this.requestService.configure(request);
setTimeout(() => {
this.requestService.configure(request);
}, 0);
});
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
}
findByHref(href: string): RemoteData<TDomain> {
this.requestService.configure(new RestRequest(href));
setTimeout(() => {
this.requestService.configure(new RestRequest(href));
}, 0);
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
// return this.rdbService.buildSingle(href));
}

View File

@@ -10,19 +10,29 @@ import { NormalizedItem } from '../cache/models/normalized-item.model';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { Observable } from 'rxjs/Observable';
import { BrowseService } from '../browse/browse.service';
import { isNotEmpty } from '../../shared/empty.util';
@Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> {
protected linkName = 'items';
protected browseEndpoint = '/discover/browses/dateissued/items';
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private bs: BrowseService
) {
super(NormalizedItem, EnvConfig);
super(NormalizedItem);
}
public getScopedEndpoint(scopeID: string): Observable<string> {
return this.bs.getBrowseURLFor('dc.date.issued', this.linkName)
.filter((href) => isNotEmpty(href))
.distinctUntilChanged();
}
}

View File

@@ -5,6 +5,7 @@ import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RootResponseParsingService } from './root-response-parsing.service';
import { BrowseResponseParsingService } from './browse-response-parsing.service';
/* tslint:disable:max-classes-per-file */
export class RestRequest {
@@ -53,6 +54,16 @@ export class RootEndpointRequest extends RestRequest {
}
}
export class BrowseEndpointRequest extends RestRequest {
constructor(href: string) {
super(href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return BrowseResponseParsingService;
}
}
export class RequestError extends Error {
statusText: string;
}

View File

@@ -6,7 +6,7 @@ import { Observable } from 'rxjs/Observable';
import { hasValue } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOSuccessResponse } from '../cache/response-cache.models';
import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers';
@@ -47,16 +47,20 @@ export class RequestService {
configure<T extends CacheableObject>(request: RestRequest): void {
let isCached = this.objectCache.hasBySelfLink(request.href);
// console.log('request.href', request.href);
if (!isCached && this.responseCache.has(request.href)) {
const [dsoSuccessResponse, otherSuccessResponse] = this.responseCache.get(request.href)
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.take(1)
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => entry.response)
.share()
.partition((response: RestResponse) => response.isSuccessful);
const [dsoSuccessResponse, otherSuccessResponse] = successResponse
.share()
.partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks));
Observable.merge(
errorResponse.map(() => true), // TODO add a configurable number of retries in case of an error.
otherSuccessResponse.map(() => true),
dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached
.map((response: DSOSuccessResponse) => response.resourceSelfLinks)

View File

@@ -19,12 +19,7 @@ export class RootResponseParsingService implements ResponseParsingService {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
const links = data.payload._links;
for (const link of Object.keys(links)) {
let href = links[link].href;
// TODO temporary workaround as these endpoint paths are relative, but should be absolute
if (isNotEmpty(href) && !href.startsWith('http')) {
href = new RESTURLCombiner(this.EnvConfig, href.substring(this.EnvConfig.rest.nameSpace.length)).toString();
}
links[link] = href;
links[link] = links[link].href;
}
return new RootSuccessResponse(links, data.statusCode);
} else {

View File

@@ -0,0 +1,24 @@
import { autoserialize, autoserializeAs } from 'cerialize';
import { SortOption } from './sort-option.model';
export class BrowseDefinition {
@autoserialize
metadataBrowse: boolean;
@autoserialize
sortOptions: SortOption[];
@autoserializeAs('order')
defaultSortOrder: string;
@autoserialize
type: string;
@autoserializeAs('metadata')
metadataKeys: string[];
@autoserialize
_links: {
[name: string]: string
}
}

View File

@@ -0,0 +1,46 @@
import { Observable } from 'rxjs/Observable';
import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
import { RootEndpointRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { isNotEmpty } from '../../shared/empty.util';
export abstract class HALEndpointService {
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService;
protected abstract linkName: string;
protected abstract EnvConfig: GlobalConfig;
protected getEndpointMap(): Observable<EndpointMap> {
const request = new RootEndpointRequest(this.EnvConfig);
setTimeout(() => {
this.requestService.configure(request);
}, 0);
return this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
.map((response: RootSuccessResponse) => response.endpointMap)
.distinctUntilChanged();
}
public getEndpoint(): Observable<string> {
return this.getEndpointMap()
.do((map: EndpointMap) => {
if (!this.linkName) {
console.log('map', this)
}
})
.map((map: EndpointMap) => map[this.linkName])
.distinctUntilChanged();
}
public isEnabledOnRestApi(): Observable<boolean> {
return this.getEndpointMap()
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
.startWith(undefined)
.distinctUntilChanged();
}
}

View File

@@ -0,0 +1,9 @@
import { autoserialize } from 'cerialize';
export class SortOption {
@autoserialize
name: string;
@autoserialize
metadata: string;
}