mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 10:34:15 +00:00
[CST-3088] Added vocabulary service and models
This commit is contained in:
@@ -16,8 +16,8 @@ import { MenuService } from '../shared/menu/menu.service';
|
|||||||
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
||||||
import {
|
import {
|
||||||
MOCK_RESPONSE_MAP,
|
MOCK_RESPONSE_MAP,
|
||||||
ResponseMapMock,
|
mockResponseMap,
|
||||||
mockResponseMap
|
ResponseMapMock
|
||||||
} from '../shared/mocks/dspace-rest-v2/mocks/response-map.mock';
|
} from '../shared/mocks/dspace-rest-v2/mocks/response-map.mock';
|
||||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
||||||
@@ -145,6 +145,9 @@ import { Version } from './shared/version.model';
|
|||||||
import { VersionHistory } from './shared/version-history.model';
|
import { VersionHistory } from './shared/version-history.model';
|
||||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
|
import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model';
|
||||||
|
import { Vocabulary } from './submission/vocabularies/models/vocabulary.model';
|
||||||
|
import { VocabularyEntriesResponseParsingService } from './submission/vocabularies/vocabulary-entries-response-parsing.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -272,7 +275,8 @@ const PROVIDERS = [
|
|||||||
},
|
},
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
FilteredDiscoveryPageResponseParsingService,
|
FilteredDiscoveryPageResponseParsingService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
||||||
|
VocabularyEntriesResponseParsingService
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -314,7 +318,9 @@ export const models =
|
|||||||
ExternalSourceEntry,
|
ExternalSourceEntry,
|
||||||
Version,
|
Version,
|
||||||
VersionHistory,
|
VersionHistory,
|
||||||
WorkflowAction
|
WorkflowAction,
|
||||||
|
Vocabulary,
|
||||||
|
VocabularyEntry
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -333,6 +339,12 @@ export const models =
|
|||||||
})
|
})
|
||||||
|
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
|
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
|
||||||
|
if (isNotEmpty(parentModule)) {
|
||||||
|
throw new Error('CoreModule is already loaded. Import it in the AppModule only');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static forRoot(): ModuleWithProviders {
|
static forRoot(): ModuleWithProviders {
|
||||||
return {
|
return {
|
||||||
ngModule: CoreModule,
|
ngModule: CoreModule,
|
||||||
@@ -341,10 +353,4 @@ export class CoreModule {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
|
|
||||||
if (isNotEmpty(parentModule)) {
|
|
||||||
throw new Error('CoreModule is already loaded. Import it in the AppModule only');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
import { VocabularyEntriesResponseParsingService } from '../submission/vocabularies/vocabulary-entries-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -442,6 +443,15 @@ export class MyDSpaceRequest extends GetRequest {
|
|||||||
public responseMsToLive = 10 * 1000;
|
public responseMsToLive = 10 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to get vocabulary entries
|
||||||
|
*/
|
||||||
|
export class VocabularyEntriesRequest extends FindListRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return VocabularyEntriesResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
statusText: string;
|
statusText: string;
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
import { ResourceType } from '../../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for vocabulary models
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const VOCABULARY = new ResourceType('vocabulary');
|
||||||
|
export const VOCABULARY_ENTRY = new ResourceType('vocabularyEntry');
|
@@ -0,0 +1,103 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
|
||||||
|
import { HALLink } from '../../../shared/hal-link.model';
|
||||||
|
import { VOCABULARY_ENTRY } from './vocabularies.resource-type';
|
||||||
|
import { typedObject } from '../../../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../../../utilities/equals.decorators';
|
||||||
|
import { PLACEHOLDER_PARENT_METADATA } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
|
||||||
|
import { OtherInformation } from '../../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { ListableObject } from '../../../../shared/object-collection/shared/listable-object.model';
|
||||||
|
import { GenericConstructor } from '../../../shared/generic-constructor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model class for a Vocabulary
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class VocabularyEntry extends ListableObject {
|
||||||
|
static type = VOCABULARY_ENTRY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of this vocabulary entry
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
authority: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The display value of this vocabulary entry
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
display: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of this vocabulary entry
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing additional information related to this vocabulary entry
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
otherInformation: OtherInformation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing the kind of vocabulary entry
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
public type: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this ExternalSourceEntry
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
vocabularyEntryDetail: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if entry has an authority value
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
hasAuthority(): boolean {
|
||||||
|
return isNotEmpty(this.authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if entry has a value
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
hasValue(): boolean {
|
||||||
|
return isNotEmpty(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if entry has related information object
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
hasOtherInformation(): boolean {
|
||||||
|
return isNotEmpty(this.otherInformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if entry has a placeholder as value
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
hasPlaceholder(): boolean {
|
||||||
|
return this.hasValue() && this.value === PLACEHOLDER_PARENT_METADATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns as which type of object this object should be rendered
|
||||||
|
*/
|
||||||
|
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||||
|
return [this.constructor as GenericConstructor<ListableObject>];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
|
||||||
|
import { HALLink } from '../../../shared/hal-link.model';
|
||||||
|
import { VOCABULARY } from './vocabularies.resource-type';
|
||||||
|
import { CacheableObject } from '../../../cache/object-cache.reducer';
|
||||||
|
import { typedObject } from '../../../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../../../utilities/equals.decorators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model class for a Vocabulary
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class Vocabulary implements CacheableObject {
|
||||||
|
static type = VOCABULARY;
|
||||||
|
/**
|
||||||
|
* The identifier of this Vocabulary
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this Vocabulary
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if it is possible to scroll all the entries in the vocabulary without providing a filter parameter
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
scrollable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the vocabulary exposes a tree structure where some entries are parent of others
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
hierarchical: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For hierarchical vocabularies express the preference to preload the tree at a specific
|
||||||
|
* level of depth (0 only the top nodes are shown, 1 also their children are preloaded and so on)
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
preloadLevel: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing the kind of Vocabulary model
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
public type: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Vocabulary
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
entries: HALLink
|
||||||
|
};
|
||||||
|
}
|
@@ -0,0 +1,111 @@
|
|||||||
|
import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock';
|
||||||
|
import { ErrorResponse, GenericSuccessResponse } from '../../cache/response.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { VocabularyEntriesResponseParsingService } from './vocabulary-entries-response-parsing.service';
|
||||||
|
import { VocabularyEntriesRequest } from '../../data/request.models';
|
||||||
|
|
||||||
|
fdescribe('VocabularyEntriesResponseParsingService', () => {
|
||||||
|
let service: VocabularyEntriesResponseParsingService;
|
||||||
|
const metadata = 'dc.type';
|
||||||
|
const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a';
|
||||||
|
const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/types/entries?metadata=${metadata}&collection=${collectionUUID}`
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new VocabularyEntriesResponseParsingService(getMockObjectCacheService());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
const request = new VocabularyEntriesRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', entriesRequestURL);
|
||||||
|
|
||||||
|
const validResponse = {
|
||||||
|
payload: {
|
||||||
|
_embedded: {
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
display: 'testValue1',
|
||||||
|
value: 'testValue1',
|
||||||
|
otherInformation: {},
|
||||||
|
type: 'vocabularyEntry'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'testValue2',
|
||||||
|
value: 'testValue2',
|
||||||
|
otherInformation: {},
|
||||||
|
type: 'vocabularyEntry'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'testValue3',
|
||||||
|
value: 'testValue3',
|
||||||
|
otherInformation: {},
|
||||||
|
type: 'vocabularyEntry'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: 'authorityId1',
|
||||||
|
display: 'testValue1',
|
||||||
|
value: 'testValue1',
|
||||||
|
otherInformation: {
|
||||||
|
id: 'VR131402',
|
||||||
|
parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work',
|
||||||
|
hasChildren: 'false',
|
||||||
|
note: 'Familjeforskning'
|
||||||
|
},
|
||||||
|
type: 'vocabularyEntry',
|
||||||
|
_links: {
|
||||||
|
vocabularyEntryDetail: {
|
||||||
|
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:VR131402'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
_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,
|
||||||
|
statusText: 'OK'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
const invalidResponseNotAList = {
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
const invalidResponseStatusCode = {
|
||||||
|
payload: {}, statusCode: 500, statusText: 'Internal Server Error'
|
||||||
|
} 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { BrowseEntriesResponseParsingService } from '../../data/browse-entries-response-parsing.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for parsing data for a vocabulary entries response
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class VocabularyEntriesResponseParsingService extends BrowseEntriesResponseParsingService {
|
||||||
|
|
||||||
|
protected toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) {
|
||||||
|
super(objectCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
269
src/app/core/submission/vocabularies/vocabulary.service.spec.ts
Normal file
269
src/app/core/submission/vocabularies/vocabulary.service.spec.ts
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { RequestService } from '../../data/request.service';
|
||||||
|
import { FindListOptions, VocabularyEntriesRequest } from '../../data/request.models';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { PageInfo } from '../../shared/page-info.model';
|
||||||
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
|
import { RequestEntry } from '../../data/request.reducer';
|
||||||
|
import { RestResponse } from '../../cache/response.models';
|
||||||
|
import { VocabularyService } from './vocabulary.service';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
|
||||||
|
describe('VocabularyService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: VocabularyService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
const vocabulary: any = {
|
||||||
|
id: 'types',
|
||||||
|
name: 'types',
|
||||||
|
scrollable: true,
|
||||||
|
hierarchical: false,
|
||||||
|
preloadLevel: 1,
|
||||||
|
type: 'vocabulary',
|
||||||
|
uuid: 'vocabulary-types',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/rest/api/submission/vocabularies/types'
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'https://rest.api/rest/api/submission/vocabularies/types/entries'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const anotherVocabulary: any = {
|
||||||
|
id: 'srsc',
|
||||||
|
name: 'srsc',
|
||||||
|
scrollable: false,
|
||||||
|
hierarchical: true,
|
||||||
|
preloadLevel: 2,
|
||||||
|
type: 'vocabulary',
|
||||||
|
uuid: 'vocabulary-srsc',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/rest/api/submission/vocabularies/types'
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'https://rest.api/rest/api/submission/vocabularies/types/entries'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const vocabularyEntry: any = {
|
||||||
|
display: 'testValue1',
|
||||||
|
value: 'testValue1',
|
||||||
|
otherInformation: {},
|
||||||
|
type: 'vocabularyEntry'
|
||||||
|
};
|
||||||
|
|
||||||
|
const vocabularyEntryWithAuthority: any = {
|
||||||
|
authority: 'authorityId1',
|
||||||
|
display: 'testValue1',
|
||||||
|
value: 'testValue1',
|
||||||
|
otherInformation: {
|
||||||
|
id: 'VR131402',
|
||||||
|
parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work',
|
||||||
|
hasChildren: 'false',
|
||||||
|
note: 'Familjeforskning'
|
||||||
|
},
|
||||||
|
type: 'vocabularyEntry',
|
||||||
|
_links: {
|
||||||
|
vocabularyEntryDetail: {
|
||||||
|
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:VR131402'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const endpointURL = `https://rest.api/rest/api/submission/vocabularies`;
|
||||||
|
const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`;
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
const vocabularyId = 'types';
|
||||||
|
const metadata = 'dc.type';
|
||||||
|
const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a';
|
||||||
|
const searchRequestURL = `https://rest.api/rest/api/submission/vocabularies/search/byMetadataAndCollection?metadata=${metadata}&collection=${collectionUUID}`;
|
||||||
|
const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}`;
|
||||||
|
|
||||||
|
const pageInfo = new PageInfo();
|
||||||
|
const array = [vocabulary, anotherVocabulary];
|
||||||
|
const paginatedList = new PaginatedList(pageInfo, array);
|
||||||
|
const vocabularyRD = createSuccessfulRemoteDataObject(vocabulary);
|
||||||
|
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
||||||
|
const getRequestEntry$ = (successful: boolean) => {
|
||||||
|
return observableOf({
|
||||||
|
response: { isSuccessful: successful, payload: vocabulary } as any
|
||||||
|
} as RequestEntry)
|
||||||
|
};
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new VocabularyService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: cold('a', { a: endpointURL })
|
||||||
|
});
|
||||||
|
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
configure: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: hot('a|', {
|
||||||
|
a: vocabularyRD
|
||||||
|
}),
|
||||||
|
buildList: hot('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
spyOn((service as any).dataService, 'findById').and.callThrough();
|
||||||
|
spyOn((service as any).dataService, 'findAll').and.callThrough();
|
||||||
|
spyOn((service as any).dataService, 'findByHref').and.callThrough();
|
||||||
|
spyOn((service as any).dataService, 'searchBy').and.callThrough();
|
||||||
|
spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL));
|
||||||
|
spyOn((service as any).dataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
service = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findById', () => {
|
||||||
|
it('should proxy the call to dataservice.findById', () => {
|
||||||
|
scheduler.schedule(() => service.findById(vocabularyId));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).dataService.findById).toHaveBeenCalledWith(vocabularyId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<Vocabulary> for the object with the given id', () => {
|
||||||
|
const result = service.findById(vocabularyId);
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: vocabularyRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findByHref', () => {
|
||||||
|
it('should proxy the call to dataservice.findByHref', () => {
|
||||||
|
scheduler.schedule(() => service.findByHref(requestURL));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<Vocabulary> for the object with the given URL', () => {
|
||||||
|
const result = service.findByHref(requestURL);
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: vocabularyRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findAll', () => {
|
||||||
|
it('should proxy the call to dataservice.findAll', () => {
|
||||||
|
scheduler.schedule(() => service.findAll());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).dataService.findAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<PaginatedList<Vocabulary>>', () => {
|
||||||
|
const result = service.findAll();
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchByMetadataAndCollection', () => {
|
||||||
|
it('should proxy the call to dataservice.findByHref', () => {
|
||||||
|
const options = new FindListOptions();
|
||||||
|
options.searchParams = [
|
||||||
|
new RequestParam('metadata', metadata),
|
||||||
|
new RequestParam('collection', collectionUUID)
|
||||||
|
];
|
||||||
|
scheduler.schedule(() => service.searchByMetadataAndCollection(metadata, collectionUUID).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).dataService.findByHref).toHaveBeenCalledWith(searchRequestURL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<Vocabulary> for the search', () => {
|
||||||
|
const result = service.searchByMetadataAndCollection(metadata, collectionUUID);
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: vocabularyRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getVocabularyEntries', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a new VocabularyEntriesRequest', () => {
|
||||||
|
const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getVocabularyEntries(vocabularyId, metadata, collectionUUID).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
|
service.getVocabularyEntries(vocabularyId, metadata, collectionUUID);
|
||||||
|
|
||||||
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
194
src/app/core/submission/vocabularies/vocabulary.service.ts
Normal file
194
src/app/core/submission/vocabularies/vocabulary.service.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, first, flatMap, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { DataService } from '../../data/data.service';
|
||||||
|
import { RequestService } from '../../data/request.service';
|
||||||
|
import { FindListOptions, RestRequest, VocabularyEntriesRequest } from '../../data/request.models';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { RemoteData } from '../../data/remote-data';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { ChangeAnalyzer } from '../../data/change-analyzer';
|
||||||
|
import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service';
|
||||||
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { Vocabulary } from './models/vocabulary.model';
|
||||||
|
import { VOCABULARY } from './models/vocabularies.resource-type';
|
||||||
|
import { VocabularyEntry } from './models/vocabulary-entry.model';
|
||||||
|
import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
|
import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../../shared/operators';
|
||||||
|
import { GenericSuccessResponse } from '../../cache/response.models';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private DataService implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
class DataServiceImpl extends DataService<Vocabulary> {
|
||||||
|
protected linkPath = 'vocabularies';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: ChangeAnalyzer<Vocabulary>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for fetching/sending data from/to the REST API on the vocabularies endpoint
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
@dataService(VOCABULARY)
|
||||||
|
export class VocabularyService {
|
||||||
|
protected searchByMetadataAndCollectionMethod = 'byMetadataAndCollection';
|
||||||
|
private dataService: DataServiceImpl;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<Vocabulary>) {
|
||||||
|
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of {@link RemoteData} of a {@link Vocabulary}, based on an href, with a list of {@link FollowLinkConfig},
|
||||||
|
* to automatically resolve {@link HALLink}s of the {@link Vocabulary}
|
||||||
|
* @param href The url of {@link Vocabulary} we want to retrieve
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<Vocabulary>>}
|
||||||
|
* Return an observable that emits vocabulary object
|
||||||
|
*/
|
||||||
|
findByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<Vocabulary>>): Observable<RemoteData<any>> {
|
||||||
|
return this.dataService.findByHref(href, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of {@link RemoteData} of a {@link Vocabulary}, based on its ID, with a list of {@link FollowLinkConfig},
|
||||||
|
* to automatically resolve {@link HALLink}s of the object
|
||||||
|
* @param id ID of {@link Vocabulary} we want to retrieve
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<Vocabulary>>}
|
||||||
|
* Return an observable that emits vocabulary object
|
||||||
|
*/
|
||||||
|
findById(id: string, ...linksToFollow: Array<FollowLinkConfig<Vocabulary>>): Observable<RemoteData<Vocabulary>> {
|
||||||
|
return this.dataService.findById(id, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options Find list options object
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<Vocabulary>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
findAll(options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Vocabulary>>): Observable<RemoteData<PaginatedList<Vocabulary>>> {
|
||||||
|
return this.dataService.findAll(options, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link VocabularyEntry} list for a given {@link Vocabulary}
|
||||||
|
*
|
||||||
|
* @param id The vocabulary id to retrieve the entries for
|
||||||
|
* @param metadata The metadata name
|
||||||
|
* @param collectionUUID The collection UUID
|
||||||
|
* @param options The {@link FindListOptions} for the request
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<VocabularyEntry>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
getVocabularyEntries(id: string, metadata: string, collectionUUID: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<VocabularyEntry>>> {
|
||||||
|
options = Object.assign({}, options, {
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('metadata', metadata),
|
||||||
|
new RequestParam('collection', collectionUUID)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.dataService.getFindAllHref(options, `${id}/entries`).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
getVocabularyEntriesFor(this.requestService, this.rdbService)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the controlled {@link Vocabulary} configured for the specified metadata and collection if any.
|
||||||
|
*
|
||||||
|
* @param metadata The metadata name
|
||||||
|
* @param collectionUUID The collection UUID
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<Vocabulary>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
searchByMetadataAndCollection(metadata: string, collectionUUID: string, ...linksToFollow: Array<FollowLinkConfig<Vocabulary>>): Observable<RemoteData<Vocabulary>> {
|
||||||
|
const options = new FindListOptions();
|
||||||
|
options.searchParams = [
|
||||||
|
new RequestParam('metadata', metadata),
|
||||||
|
new RequestParam('collection', collectionUUID)
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.dataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe(
|
||||||
|
first((href: string) => hasValue(href)),
|
||||||
|
flatMap((href: string) => this.dataService.findByHref(href))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a href into a PaginatedList of VocabularyEntry
|
||||||
|
* @param requestService
|
||||||
|
* @param rdb
|
||||||
|
*/
|
||||||
|
export const getVocabularyEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
|
(source: Observable<string>): Observable<RemoteData<PaginatedList<VocabularyEntry>>> =>
|
||||||
|
source.pipe(
|
||||||
|
map((href: string) => new VocabularyEntriesRequest(requestService.generateRequestId(), href)),
|
||||||
|
configureRequest(requestService),
|
||||||
|
toRDPaginatedVocabularyEntries(requestService, rdb)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a RestRequest into a PaginatedList of VocabularyEntry
|
||||||
|
* @param requestService
|
||||||
|
* @param rdb
|
||||||
|
*/
|
||||||
|
export const toRDPaginatedVocabularyEntries = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
|
(source: Observable<RestRequest>): Observable<RemoteData<PaginatedList<VocabularyEntry>>> => {
|
||||||
|
const href$ = source.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService));
|
||||||
|
|
||||||
|
const payload$ = requestEntry$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((response: GenericSuccessResponse<VocabularyEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
|
map((list: PaginatedList<VocabularyEntry>) => Object.assign(list, {
|
||||||
|
page: list.page ? list.page.map((entry: VocabularyEntry) => Object.assign(new VocabularyEntry(), entry)) : list.page
|
||||||
|
})),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
return rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
|
};
|
Reference in New Issue
Block a user