mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 15:33:04 +00:00
Merge branch 'response-cache-refactoring' into w2p-54472_Create-community-and-collection-pages
Conflicts: src/app/core/auth/auth-response-parsing.service.ts src/app/core/auth/auth.service.ts src/app/core/auth/models/auth-status.model.ts src/app/core/auth/models/normalized-auth-status.model.ts src/app/core/auth/server-auth.service.ts src/app/core/cache/builders/remote-data-build.service.ts src/app/core/data/collection-data.service.ts src/app/core/data/comcol-data.service.spec.ts src/app/core/data/comcol-data.service.ts src/app/core/data/community-data.service.ts src/app/core/data/data.service.spec.ts src/app/core/data/data.service.ts src/app/core/data/dspace-object-data.service.ts src/app/core/data/item-data.service.ts src/app/core/data/request.models.ts src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts src/app/core/shared/hal-endpoint.service.ts src/app/core/shared/operators.ts src/app/shared/testing/auth-request-service-stub.ts src/app/shared/testing/auth-service-stub.ts
This commit is contained in:
@@ -7,6 +7,8 @@ import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||
|
||||
function isObjectLevel(halObj: any) {
|
||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||
@@ -25,7 +27,6 @@ export abstract class BaseResponseParsingService {
|
||||
protected abstract toCache: boolean;
|
||||
|
||||
protected process<ObjectDomain, ObjectType>(data: any, requestHref: string): any {
|
||||
|
||||
if (isNotEmpty(data)) {
|
||||
if (hasNoValue(data) || (typeof data !== 'object')) {
|
||||
return data;
|
||||
@@ -34,6 +35,7 @@ export abstract class BaseResponseParsingService {
|
||||
} else if (Array.isArray(data)) {
|
||||
return this.processArray(data, requestHref);
|
||||
} else if (isObjectLevel(data)) {
|
||||
data = this.fixBadEPersonRestResponse(data);
|
||||
const object = this.deserialize(data);
|
||||
if (isNotEmpty(data._embedded)) {
|
||||
Object
|
||||
@@ -53,6 +55,7 @@ export abstract class BaseResponseParsingService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.cache(object, requestHref);
|
||||
return object;
|
||||
}
|
||||
@@ -122,7 +125,7 @@ export abstract class BaseResponseParsingService {
|
||||
if (hasNoValue(co) || hasNoValue(co.self)) {
|
||||
throw new Error('The server returned an invalid object');
|
||||
}
|
||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive.default, requestHref);
|
||||
}
|
||||
|
||||
processPageInfo(payload: any): PageInfo {
|
||||
@@ -145,4 +148,23 @@ export abstract class BaseResponseParsingService {
|
||||
}
|
||||
return obj[keys[0]];
|
||||
}
|
||||
|
||||
// TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed
|
||||
// See https://github.com/DSpace/dspace-angular/issues/292
|
||||
private fixBadEPersonRestResponse(obj: any): any {
|
||||
if (obj.type === ResourceType.EPerson) {
|
||||
const groups = obj.groups;
|
||||
const normGroups = [];
|
||||
if (isNotEmpty(groups)) {
|
||||
groups.forEach((group) => {
|
||||
const parts = ['eperson', 'groups', group.uuid];
|
||||
const href = new RESTURLCombiner(this.EnvConfig, ...parts).toString();
|
||||
normGroups.push(href);
|
||||
}
|
||||
)
|
||||
}
|
||||
return Object.assign({}, obj, { groups: normGroups });
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
||||
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models';
|
||||
import { ErrorResponse, GenericSuccessResponse } from '../cache/response.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';
|
||||
|
@@ -7,7 +7,7 @@ import {
|
||||
ErrorResponse,
|
||||
GenericSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
} from '../cache/response.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';
|
||||
|
168
src/app/core/data/browse-items-response-parsing-service.spec.ts
Normal file
168
src/app/core/data/browse-items-response-parsing-service.spec.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
||||
import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
|
||||
import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models';
|
||||
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
||||
|
||||
describe('BrowseItemsResponseParsingService', () => {
|
||||
let service: BrowseItemsResponseParsingService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new BrowseItemsResponseParsingService(undefined, getMockObjectCacheService());
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
const request = new BrowseItemsRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/items');
|
||||
|
||||
const validResponse = {
|
||||
payload: {
|
||||
_embedded: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
||||
handle: '10986/17472',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.creator',
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
],
|
||||
inArchive: true,
|
||||
discoverable: true,
|
||||
withdrawn: false,
|
||||
lastModified: '2018-05-25T09:32:58.005+0000',
|
||||
type: 'item',
|
||||
_links: {
|
||||
bitstreams: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
|
||||
},
|
||||
owningCollection: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
|
||||
},
|
||||
templateItemOf: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
|
||||
},
|
||||
self: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
|
||||
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
|
||||
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
|
||||
handle: '10986/17475',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.creator',
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
],
|
||||
inArchive: true,
|
||||
discoverable: true,
|
||||
withdrawn: false,
|
||||
lastModified: '2018-05-25T09:33:42.526+0000',
|
||||
type: 'item',
|
||||
_links: {
|
||||
bitstreams: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/bitstreams'
|
||||
},
|
||||
owningCollection: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/owningCollection'
|
||||
},
|
||||
templateItemOf: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/templateItemOf'
|
||||
},
|
||||
self: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
_links: {
|
||||
first: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=0&size=2'
|
||||
},
|
||||
self: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items'
|
||||
},
|
||||
next: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=1&size=2'
|
||||
},
|
||||
last: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=7&size=2'
|
||||
}
|
||||
},
|
||||
page: {
|
||||
size: 2,
|
||||
totalElements: 16,
|
||||
totalPages: 8,
|
||||
number: 0
|
||||
}
|
||||
},
|
||||
statusCode: '200'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
const invalidResponseNotAList = {
|
||||
payload: {
|
||||
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
||||
handle: '10986/17472',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.creator',
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
],
|
||||
inArchive: true,
|
||||
discoverable: true,
|
||||
withdrawn: false,
|
||||
lastModified: '2018-05-25T09:32:58.005+0000',
|
||||
type: 'item',
|
||||
_links: {
|
||||
bitstreams: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
|
||||
},
|
||||
owningCollection: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
|
||||
},
|
||||
templateItemOf: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
|
||||
},
|
||||
self: {
|
||||
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
|
||||
}
|
||||
}
|
||||
},
|
||||
statusCode: '200'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
const invalidResponseStatusCode = {
|
||||
payload: {}, statusCode: '500'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
it('should return a GenericSuccessResponse if data contains a valid browse items 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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
58
src/app/core/data/browse-items-response-parsing-service.ts
Normal file
58
src/app/core/data/browse-items-response-parsing-service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
|
||||
/**
|
||||
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[])
|
||||
*/
|
||||
@Injectable()
|
||||
export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||
|
||||
protected objectFactory = {
|
||||
getConstructor: () => DSpaceObject
|
||||
};
|
||||
protected toCache = false;
|
||||
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService,
|
||||
) { super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data from the browse endpoint to a list of DSpaceObjects
|
||||
* @param {RestRequest} request
|
||||
* @param {DSpaceRESTV2Response} data
|
||||
* @returns {RestResponse}
|
||||
*/
|
||||
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(DSpaceObject);
|
||||
const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||
return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload));
|
||||
} else {
|
||||
return new ErrorResponse(
|
||||
Object.assign(
|
||||
new Error('Unexpected response from browse endpoint'),
|
||||
{ statusText: data.statusCode }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||
import { BrowseEndpointRequest } from './request.models';
|
||||
import { GenericSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
||||
import { GenericSuccessResponse, ErrorResponse } from '../cache/response.models';
|
||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
|
||||
@@ -10,134 +10,148 @@ describe('BrowseResponseParsingService', () => {
|
||||
beforeEach(() => {
|
||||
service = new BrowseResponseParsingService();
|
||||
});
|
||||
let validRequest;
|
||||
let validResponse;
|
||||
let invalidResponse1;
|
||||
let invalidResponse2;
|
||||
let invalidResponse3;
|
||||
let definitions;
|
||||
|
||||
describe('parse', () => {
|
||||
const validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
|
||||
beforeEach(() => {
|
||||
validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
|
||||
|
||||
const validResponse = {
|
||||
payload: {
|
||||
_embedded: {
|
||||
browses: [{
|
||||
metadataBrowse: false,
|
||||
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||
validResponse = {
|
||||
payload: {
|
||||
_embedded: {
|
||||
browses: [{
|
||||
metadataBrowse: false,
|
||||
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||
order: 'ASC',
|
||||
type: 'browse',
|
||||
metadata: ['dc.date.issued'],
|
||||
_links: {
|
||||
self: { href: 'https://rest.api/discover/browses/dateissued' },
|
||||
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
||||
}
|
||||
}, {
|
||||
metadataBrowse: true,
|
||||
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||
order: 'ASC',
|
||||
type: 'browse',
|
||||
metadata: ['dc.contributor.*', 'dc.creator'],
|
||||
_links: {
|
||||
self: { href: 'https://rest.api/discover/browses/author' },
|
||||
entries: { href: 'https://rest.api/discover/browses/author/entries' },
|
||||
items: { href: 'https://rest.api/discover/browses/author/items' }
|
||||
}
|
||||
}]
|
||||
},
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '200'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
invalidResponse1 = {
|
||||
payload: {
|
||||
_embedded: {
|
||||
browse: {
|
||||
metadataBrowse: false,
|
||||
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||
order: 'ASC',
|
||||
type: 'browse',
|
||||
metadata: ['dc.date.issued'],
|
||||
_links: {
|
||||
self: { href: 'https://rest.api/discover/browses/dateissued' },
|
||||
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
||||
}
|
||||
}
|
||||
},
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '200'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
invalidResponse2 = {
|
||||
payload: {
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '200'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
invalidResponse3 = {
|
||||
payload: {
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '500'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
definitions = [
|
||||
Object.assign(new BrowseDefinition(), {
|
||||
metadataBrowse: false,
|
||||
sortOptions: [
|
||||
{
|
||||
name: 'title',
|
||||
metadata: 'dc.title'
|
||||
},
|
||||
{
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||
order: 'ASC',
|
||||
type: 'browse',
|
||||
metadata: ['dc.date.issued'],
|
||||
_links: {
|
||||
self: { href: 'https://rest.api/discover/browses/dateissued' },
|
||||
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
||||
},
|
||||
{
|
||||
name: 'dateaccessioned',
|
||||
metadata: 'dc.date.accessioned'
|
||||
}
|
||||
}, {
|
||||
metadataBrowse: true,
|
||||
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||
],
|
||||
defaultSortOrder: 'ASC',
|
||||
type: 'browse',
|
||||
metadataKeys: [
|
||||
'dc.date.issued'
|
||||
],
|
||||
_links: {
|
||||
self: 'https://rest.api/discover/browses/dateissued',
|
||||
items: 'https://rest.api/discover/browses/dateissued/items'
|
||||
}
|
||||
}),
|
||||
Object.assign(new BrowseDefinition(), {
|
||||
metadataBrowse: true,
|
||||
sortOptions: [
|
||||
{
|
||||
name: 'title',
|
||||
metadata: 'dc.title'
|
||||
},
|
||||
{
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||
order: 'ASC',
|
||||
type: 'browse',
|
||||
metadata: ['dc.contributor.*', 'dc.creator'],
|
||||
_links: {
|
||||
self: { href: 'https://rest.api/discover/browses/author' },
|
||||
entries: { href: 'https://rest.api/discover/browses/author/entries' },
|
||||
items: { href: 'https://rest.api/discover/browses/author/items' }
|
||||
}
|
||||
}]
|
||||
},
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '200'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
const invalidResponse1 = {
|
||||
payload: {
|
||||
_embedded: {
|
||||
browse: {
|
||||
metadataBrowse: false,
|
||||
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||
order: 'ASC',
|
||||
type: 'browse',
|
||||
metadata: ['dc.date.issued'],
|
||||
_links: {
|
||||
self: { href: 'https://rest.api/discover/browses/dateissued' },
|
||||
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
||||
},
|
||||
{
|
||||
name: 'dateaccessioned',
|
||||
metadata: 'dc.date.accessioned'
|
||||
}
|
||||
],
|
||||
defaultSortOrder: 'ASC',
|
||||
type: 'browse',
|
||||
metadataKeys: [
|
||||
'dc.contributor.*',
|
||||
'dc.creator'
|
||||
],
|
||||
_links: {
|
||||
self: 'https://rest.api/discover/browses/author',
|
||||
entries: 'https://rest.api/discover/browses/author/entries',
|
||||
items: 'https://rest.api/discover/browses/author/items'
|
||||
}
|
||||
},
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '200'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
const invalidResponse2 = {
|
||||
payload: {
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '200'
|
||||
} as DSpaceRESTV2Response ;
|
||||
|
||||
const invalidResponse3 = {
|
||||
payload: {
|
||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||
}, statusCode: '500'
|
||||
} as DSpaceRESTV2Response;
|
||||
|
||||
const definitions = [
|
||||
Object.assign(new BrowseDefinition(), {
|
||||
metadataBrowse: false,
|
||||
sortOptions: [
|
||||
{
|
||||
name: 'title',
|
||||
metadata: 'dc.title'
|
||||
},
|
||||
{
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
},
|
||||
{
|
||||
name: 'dateaccessioned',
|
||||
metadata: 'dc.date.accessioned'
|
||||
}
|
||||
],
|
||||
defaultSortOrder: 'ASC',
|
||||
type: 'browse',
|
||||
metadataKeys: [
|
||||
'dc.date.issued'
|
||||
],
|
||||
_links: { }
|
||||
}),
|
||||
Object.assign(new BrowseDefinition(), {
|
||||
metadataBrowse: true,
|
||||
sortOptions: [
|
||||
{
|
||||
name: 'title',
|
||||
metadata: 'dc.title'
|
||||
},
|
||||
{
|
||||
name: 'dateissued',
|
||||
metadata: 'dc.date.issued'
|
||||
},
|
||||
{
|
||||
name: 'dateaccessioned',
|
||||
metadata: 'dc.date.accessioned'
|
||||
}
|
||||
],
|
||||
defaultSortOrder: 'ASC',
|
||||
type: 'browse',
|
||||
metadataKeys: [
|
||||
'dc.contributor.*',
|
||||
'dc.creator'
|
||||
],
|
||||
_links: { }
|
||||
})
|
||||
];
|
||||
|
||||
})
|
||||
];
|
||||
});
|
||||
it('should return a GenericSuccessResponse if data contains a valid browse endpoint response', () => {
|
||||
const response = service.parse(validRequest, validResponse);
|
||||
expect(response.constructor).toBe(GenericSuccessResponse);
|
||||
|
@@ -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 { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedCollection } from '../cache/models/normalized-collection.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { Collection } from '../shared/collection.model';
|
||||
import { ComColDataService } from './comcol-data.service';
|
||||
@@ -21,7 +19,6 @@ export class CollectionDataService extends ComColDataService<NormalizedCollectio
|
||||
protected linkPath = 'collections';
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { Observable, TestScheduler } from 'rxjs/Rx';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { GlobalConfig } from '../../../config';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ComColDataService } from './comcol-data.service';
|
||||
import { CommunityDataService } from './community-data.service';
|
||||
import { FindByIDRequest } from './request.models';
|
||||
import { FindAllOptions, FindByIDRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Community } from '../shared/community.model';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -28,7 +28,6 @@ class NormalizedTestObject extends NormalizedObject {
|
||||
class TestService extends ComColDataService<NormalizedTestObject, any> {
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
@@ -44,12 +43,12 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
describe('ComColDataService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: TestService;
|
||||
let responseCache: ResponseCacheService;
|
||||
let requestService: RequestService;
|
||||
let cds: CommunityDataService;
|
||||
let objectCache: ObjectCacheService;
|
||||
@@ -63,6 +62,15 @@ describe('ComColDataService', () => {
|
||||
const http = {} as HttpClient;
|
||||
|
||||
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||
const options = Object.assign(new FindAllOptions(), {
|
||||
scopeID: scopeID
|
||||
});
|
||||
const getRequestEntry$ = (successful: boolean) => {
|
||||
return observableOf({
|
||||
response: { isSuccessful: successful } as any
|
||||
} as RequestEntry)
|
||||
};
|
||||
|
||||
const communitiesEndpoint = 'https://rest.api/core/communities';
|
||||
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
|
||||
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
|
||||
@@ -80,14 +88,6 @@ describe('ComColDataService', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
|
||||
return jasmine.createSpyObj('responseCache', {
|
||||
get: cold('c-', {
|
||||
c: { response: { isSuccessful } }
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function initMockObjectCacheService(): ObjectCacheService {
|
||||
return jasmine.createSpyObj('objectCache', {
|
||||
getByUUID: cold('d-', {
|
||||
@@ -110,7 +110,6 @@ describe('ComColDataService', () => {
|
||||
|
||||
function initTestService(): TestService {
|
||||
return new TestService(
|
||||
responseCache,
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
@@ -129,52 +128,62 @@ describe('ComColDataService', () => {
|
||||
cds = initMockCommunityDataService();
|
||||
requestService = getMockRequestService();
|
||||
objectCache = initMockObjectCacheService();
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
halService = mockHalService;
|
||||
authService = initMockAuthService();
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
describe('getScopedEndpoint', () => {
|
||||
describe('getBrowseEndpoint', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
});
|
||||
|
||||
it('should configure a new FindByIDRequest for the scope Community', () => {
|
||||
cds = initMockCommunityDataService();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
objectCache = initMockObjectCacheService();
|
||||
service = initTestService();
|
||||
|
||||
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
|
||||
|
||||
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
||||
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
describe('if the scope Community can be found', () => {
|
||||
beforeEach(() => {
|
||||
cds = initMockCommunityDataService();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
objectCache = initMockObjectCacheService();
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
it('should fetch the scope Community from the cache', () => {
|
||||
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
||||
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
|
||||
scheduler.flush();
|
||||
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID);
|
||||
});
|
||||
|
||||
it('should return the endpoint to fetch resources within the given scope', () => {
|
||||
const result = service.getScopedEndpoint(scopeID);
|
||||
const expected = cold('--e-', { e: scopedEndpoint });
|
||||
const result = service.getBrowseEndpoint(options);
|
||||
const expected = '--e-';
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
scheduler.expectObservable(result).toBe(expected, { e: scopedEndpoint });
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the scope Community can\'t be found', () => {
|
||||
beforeEach(() => {
|
||||
cds = initMockCommunityDataService();
|
||||
requestService = getMockRequestService();
|
||||
requestService = getMockRequestService(getRequestEntry$(false));
|
||||
objectCache = initMockObjectCacheService();
|
||||
responseCache = initMockResponseCacheService(false);
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
it('should throw an error', () => {
|
||||
const result = service.getScopedEndpoint(scopeID);
|
||||
const result = service.getBrowseEndpoint(options);
|
||||
const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`));
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
|
@@ -1,27 +1,27 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { isEmpty, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
mergeMap,
|
||||
share,
|
||||
take,
|
||||
tap
|
||||
} from 'rxjs/operators';
|
||||
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||
import { CommunityDataService } from './community-data.service';
|
||||
|
||||
import { DataService } from './data.service';
|
||||
import { FindByIDRequest, PostRequest, PutRequest, RequestError, RestRequest } from './request.models';
|
||||
import { FindAllOptions, FindByIDRequest } from './request.models';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { Community } from '../shared/community.model';
|
||||
import { Collection } from '../shared/collection.model';
|
||||
import { catchError, distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { configureRequest, getResponseFromSelflink } from '../shared/operators';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
|
||||
export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> {
|
||||
export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> {
|
||||
protected abstract cds: CommunityDataService;
|
||||
protected abstract objectCache: ObjectCacheService;
|
||||
protected abstract halService: HALEndpointService;
|
||||
@@ -36,34 +36,52 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
|
||||
* @return { Observable<string> }
|
||||
* an Observable<string> containing the scoped URL
|
||||
*/
|
||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||
if (isEmpty(scopeID)) {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||
if (isEmpty(options.scopeID)) {
|
||||
return this.halService.getEndpoint(linkPath);
|
||||
} else {
|
||||
const scopeCommunityHrefObs = this.cds.getEndpoint()
|
||||
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID))
|
||||
.filter((href: string) => isNotEmpty(href))
|
||||
.take(1)
|
||||
.do((href: string) => {
|
||||
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID);
|
||||
const scopeCommunityHrefObs = this.cds.getEndpoint().pipe(
|
||||
mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)),
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
take(1),
|
||||
tap((href: string) => {
|
||||
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID);
|
||||
this.requestService.configure(request);
|
||||
});
|
||||
}));
|
||||
|
||||
const [successResponse, errorResponse] = scopeCommunityHrefObs
|
||||
.flatMap((href: string) => this.responseCache.get(href))
|
||||
.map((entry: ResponseCacheEntry) => entry.response)
|
||||
.share()
|
||||
.partition((response: RestResponse) => response.isSuccessful);
|
||||
// return scopeCommunityHrefObs.pipe(
|
||||
// mergeMap((href: string) => this.responseCache.get(href)),
|
||||
// map((entry: ResponseCacheEntry) => entry.response),
|
||||
// mergeMap((response) => {
|
||||
// if (response.isSuccessful) {
|
||||
// const community$: Observable<NormalizedCommunity> = this.objectCache.getByUUID(scopeID);
|
||||
// return community$.pipe(
|
||||
// map((community) => community._links[linkPath]),
|
||||
// filter((href) => isNotEmpty(href)),
|
||||
// distinctUntilChanged()
|
||||
// );
|
||||
// } else if (!response.isSuccessful) {
|
||||
// return observableThrowError(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))
|
||||
// }
|
||||
// }),
|
||||
// distinctUntilChanged()
|
||||
// );
|
||||
const responses = scopeCommunityHrefObs.pipe(
|
||||
mergeMap((href: string) => this.requestService.getByHref(href)),
|
||||
getResponseFromEntry()
|
||||
);
|
||||
const errorResponses = responses.pipe(
|
||||
filter((response) => !response.isSuccessful),
|
||||
mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`)))
|
||||
);
|
||||
const successResponses = responses.pipe(
|
||||
filter((response) => response.isSuccessful),
|
||||
mergeMap(() => this.objectCache.getByUUID(options.scopeID)),
|
||||
map((nc: NormalizedCommunity) => nc._links[linkPath]),
|
||||
filter((href) => isNotEmpty(href))
|
||||
);
|
||||
|
||||
return Observable.merge(
|
||||
errorResponse.flatMap((response: ErrorResponse) =>
|
||||
Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))),
|
||||
successResponse
|
||||
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID))
|
||||
.map((nc: NormalizedCommunity) => nc._links[this.linkPath])
|
||||
.filter((href) => isNotEmpty(href))
|
||||
).distinctUntilChanged();
|
||||
return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { filter, mergeMap, take } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { Community } from '../shared/community.model';
|
||||
import { ComColDataService } from './comcol-data.service';
|
||||
@@ -14,7 +14,7 @@ import { AuthService } from '../auth/auth.service';
|
||||
import { FindAllOptions, FindAllRequest } from './request.models';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
@@ -26,7 +26,6 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
|
||||
protected cds = this;
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
@@ -44,12 +43,10 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
|
||||
}
|
||||
|
||||
findTop(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Community>>> {
|
||||
const hrefObs = this.halService.getEndpoint(this.topLinkPath).filter((href: string) => isNotEmpty(href))
|
||||
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
|
||||
|
||||
hrefObs
|
||||
.filter((href: string) => hasValue(href))
|
||||
.take(1)
|
||||
const hrefObs = this.getFindAllHref(options, this.topLinkPath);
|
||||
hrefObs.pipe(
|
||||
filter((href: string) => hasValue(href)),
|
||||
take(1))
|
||||
.subscribe((href: string) => {
|
||||
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
|
||||
this.requestService.configure(request);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ConfigSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
||||
import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models';
|
||||
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
|
@@ -3,7 +3,7 @@ import { Inject, 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 { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { ConfigObjectFactory } from '../shared/config/config-object-factory';
|
||||
|
||||
|
@@ -1,223 +1,181 @@
|
||||
import { DataService } from './data.service';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { SortOptions, SortDirection } from '../cache/models/sort-options.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { Operation } from '../../../../node_modules/fast-json-patch';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
||||
import { EmptyError } from 'rxjs/util/EmptyError';
|
||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||
import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { RemoteDataError } from './remote-data-error';
|
||||
|
||||
const LINK_NAME = 'test';
|
||||
const endpoint = 'https://rest.api/core';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
class NormalizedTestObject extends NormalizedObject {
|
||||
}
|
||||
|
||||
class TestService extends DataService<NormalizedTestObject, any> {
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected linkPath: string,
|
||||
protected halService: HALEndpointService,
|
||||
protected authService: AuthService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getScopedEndpoint(scope: string): Observable<string> {
|
||||
throw new Error('getScopedEndpoint is abstract in DataService');
|
||||
}
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected linkPath: string,
|
||||
protected halService: HALEndpointService,
|
||||
protected objectCache: ObjectCacheService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||
return observableOf(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
describe('DataService', () => {
|
||||
let service: TestService;
|
||||
let options: FindAllOptions;
|
||||
let responseCache = getMockResponseCacheService();
|
||||
let rdbService = {} as RemoteDataBuildService;
|
||||
const authService = {} as AuthService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const store = {} as Store<CoreState>;
|
||||
const endpoint = 'https://rest.api/core';
|
||||
const halService = Object.assign({
|
||||
getEndpoint: (linkpath) => Observable.of(endpoint)
|
||||
});
|
||||
const requestService = Object.assign(getMockRequestService(), {
|
||||
getByUUID: () => Observable.of(new RequestEntry()),
|
||||
configure: (request) => request
|
||||
});
|
||||
let service: TestService;
|
||||
let options: FindAllOptions;
|
||||
const requestService = {} as RequestService;
|
||||
const halService = {} as HALEndpointService;
|
||||
const rdbService = {} as RemoteDataBuildService;
|
||||
const objectCache = {
|
||||
addPatch: () => {
|
||||
/* empty */
|
||||
},
|
||||
getBySelfLink: () => {
|
||||
/* empty */
|
||||
}
|
||||
} as any;
|
||||
const store = {} as Store<CoreState>;
|
||||
|
||||
const dso = new DSpaceObject();
|
||||
const successfulRd$ = Observable.of(new RemoteData(false, false, true, undefined, dso));
|
||||
const successfulResponseCacheEntry = Object.assign({
|
||||
response: {
|
||||
isSuccessful: true,
|
||||
payload: dso,
|
||||
toCache: true,
|
||||
statusCode: '200',
|
||||
resourceSelfLinks: [
|
||||
endpoint
|
||||
]
|
||||
} as DSOSuccessResponse
|
||||
}) as ResponseCacheEntry;
|
||||
function initTestService(): TestService {
|
||||
return new TestService(
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
endpoint,
|
||||
halService,
|
||||
objectCache
|
||||
);
|
||||
}
|
||||
|
||||
function initSuccessfulRemoteDataBuildService(): RemoteDataBuildService {
|
||||
return {
|
||||
buildSingle: (selfLinks$: Observable<any>) => {
|
||||
selfLinks$.subscribe();
|
||||
return successfulRd$;
|
||||
service = initTestService();
|
||||
|
||||
describe('getFindAllHref', () => {
|
||||
|
||||
it('should return an observable with the endpoint', () => {
|
||||
options = {};
|
||||
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(endpoint);
|
||||
}
|
||||
} as RemoteDataBuildService;
|
||||
}
|
||||
function initSuccessfulResponseCacheService(): ResponseCacheService {
|
||||
return getMockResponseCacheService(Observable.of(new ResponseCacheEntry()), Observable.of(successfulResponseCacheEntry));
|
||||
}
|
||||
|
||||
function initTestService(): TestService {
|
||||
return new TestService(
|
||||
responseCache,
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
LINK_NAME,
|
||||
halService,
|
||||
authService,
|
||||
notificationsService,
|
||||
http
|
||||
);
|
||||
}
|
||||
|
||||
service = initTestService();
|
||||
|
||||
describe('getFindAllHref', () => {
|
||||
|
||||
it('should return an observable with the endpoint', () => {
|
||||
options = {};
|
||||
|
||||
(service as any).getFindAllHref(endpoint).subscribe((value) => {
|
||||
expect(value).toBe(endpoint);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// getScopedEndpoint is not implemented in abstract DataService
|
||||
it('should throw error if scopeID provided in options', () => {
|
||||
options = { scopeID: 'somevalue' };
|
||||
|
||||
expect(() => { (service as any).getFindAllHref(endpoint, options) })
|
||||
.toThrowError('getScopedEndpoint is abstract in DataService');
|
||||
});
|
||||
|
||||
it('should include page in href if currentPage provided in options', () => {
|
||||
options = { currentPage: 2 };
|
||||
const expected = `${endpoint}?page=${options.currentPage - 1}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include size in href if elementsPerPage provided in options', () => {
|
||||
options = { elementsPerPage: 5 };
|
||||
const expected = `${endpoint}?size=${options.elementsPerPage}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include sort href if SortOptions provided in options', () => {
|
||||
const sortOptions = new SortOptions('field1', SortDirection.ASC);
|
||||
options = { sort: sortOptions};
|
||||
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include startsWith in href if startsWith provided in options', () => {
|
||||
options = { startsWith: 'ab' };
|
||||
const expected = `${endpoint}?startsWith=${options.startsWith}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include all provided options in href', () => {
|
||||
const sortOptions = new SortOptions('field1', SortDirection.DESC);
|
||||
options = {
|
||||
currentPage: 6,
|
||||
elementsPerPage: 10,
|
||||
sort: sortOptions,
|
||||
startsWith: 'ab'
|
||||
};
|
||||
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
||||
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should include page in href if currentPage provided in options', () => {
|
||||
options = { currentPage: 2 };
|
||||
const expected = `${endpoint}?page=${options.currentPage - 1}`;
|
||||
|
||||
describe('when the request was successful', () => {
|
||||
beforeEach(() => {
|
||||
responseCache = initSuccessfulResponseCacheService();
|
||||
rdbService = initSuccessfulRemoteDataBuildService();
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
it('should return a RemoteData of a DSpaceObject', () => {
|
||||
service.create(dso, undefined).subscribe((rd: RemoteData<DSpaceObject>) => {
|
||||
expect(rd.payload).toBe(dso);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the response from cache with the correct url when parent is empty', () => {
|
||||
const expectedUrl = endpoint;
|
||||
|
||||
service.create(dso, undefined).subscribe((value) => {
|
||||
expect(responseCache.get).toHaveBeenCalledWith(expectedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the response from cache with the correct url when parent is not empty', () => {
|
||||
const parent = 'fake-parent-uuid';
|
||||
const expectedUrl = `${endpoint}?parent=${parent}`;
|
||||
|
||||
service.create(dso, parent).subscribe((value) => {
|
||||
expect(responseCache.get).toHaveBeenCalledWith(expectedUrl);
|
||||
});
|
||||
});
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should include size in href if elementsPerPage provided in options', () => {
|
||||
options = { elementsPerPage: 5 };
|
||||
const expected = `${endpoint}?size=${options.elementsPerPage}`;
|
||||
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include sort href if SortOptions provided in options', () => {
|
||||
const sortOptions = new SortOptions('field1', SortDirection.ASC);
|
||||
options = { sort: sortOptions };
|
||||
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
|
||||
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include startsWith in href if startsWith provided in options', () => {
|
||||
options = { startsWith: 'ab' };
|
||||
const expected = `${endpoint}?startsWith=${options.startsWith}`;
|
||||
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include all provided options in href', () => {
|
||||
const sortOptions = new SortOptions('field1', SortDirection.DESC)
|
||||
options = {
|
||||
currentPage: 6,
|
||||
elementsPerPage: 10,
|
||||
sort: sortOptions,
|
||||
startsWith: 'ab'
|
||||
}
|
||||
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
||||
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
||||
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
describe('patch', () => {
|
||||
let operations;
|
||||
let selfLink;
|
||||
|
||||
beforeEach(() => {
|
||||
operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
spyOn(objectCache, 'addPatch');
|
||||
});
|
||||
|
||||
it('should call addPatch on the object cache with the right parameters', () => {
|
||||
service.patch(selfLink, operations);
|
||||
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
let operations;
|
||||
let selfLink;
|
||||
let dso;
|
||||
let dso2;
|
||||
const name1 = 'random string';
|
||||
const name2 = 'another random string';
|
||||
beforeEach(() => {
|
||||
operations = [{ op: 'replace', path: '/name', value: name2 } as Operation];
|
||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
|
||||
dso = new DSpaceObject();
|
||||
dso.self = selfLink;
|
||||
dso.name = name1;
|
||||
|
||||
dso2 = new DSpaceObject();
|
||||
dso2.self = selfLink;
|
||||
dso2.name = name2;
|
||||
|
||||
spyOn(objectCache, 'getBySelfLink').and.returnValue(dso);
|
||||
spyOn(objectCache, 'addPatch');
|
||||
});
|
||||
|
||||
it('should call addPatch on the object cache with the right parameters when there are differences', () => {
|
||||
service.update(dso2);
|
||||
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
||||
});
|
||||
|
||||
it('should not call addPatch on the object cache with the right parameters when there are no differences', () => {
|
||||
service.update(dso);
|
||||
expect(objectCache.addPatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { distinctUntilChanged, filter, first, map, take } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
@@ -13,78 +13,68 @@ import {
|
||||
FindAllOptions,
|
||||
FindAllRequest,
|
||||
FindByIDRequest,
|
||||
GetRequest,
|
||||
RestRequest
|
||||
GetRequest
|
||||
} from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { distinctUntilChanged, first, map, take, tap } from 'rxjs/operators';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {
|
||||
configureRequest,
|
||||
filterSuccessfulResponses,
|
||||
getResponseFromSelflink
|
||||
getResponseFromEntry
|
||||
} from '../shared/operators';
|
||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DSOSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||
|
||||
export abstract class DataService<TNormalized extends NormalizedObject, TDomain> {
|
||||
protected abstract responseCache: ResponseCacheService;
|
||||
protected abstract requestService: RequestService;
|
||||
protected abstract rdbService: RemoteDataBuildService;
|
||||
protected abstract store: Store<CoreState>;
|
||||
protected abstract linkPath: string;
|
||||
protected abstract halService: HALEndpointService;
|
||||
protected abstract objectCache: ObjectCacheService;
|
||||
protected abstract authService: AuthService;
|
||||
protected abstract notificationsService: NotificationsService;
|
||||
protected abstract http: HttpClient;
|
||||
|
||||
public abstract getScopedEndpoint(scope: string): Observable<string>
|
||||
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
|
||||
|
||||
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> {
|
||||
protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
|
||||
let result: Observable<string>;
|
||||
const args = [];
|
||||
|
||||
if (hasValue(options.scopeID)) {
|
||||
result = this.getScopedEndpoint(options.scopeID).distinctUntilChanged();
|
||||
} else {
|
||||
result = Observable.of(endpoint);
|
||||
}
|
||||
|
||||
result = this.getBrowseEndpoint(options, linkPath);
|
||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
|
||||
args.push(`page=${options.currentPage - 1}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.elementsPerPage)) {
|
||||
args.push(`size=${options.elementsPerPage}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.sort)) {
|
||||
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.startsWith)) {
|
||||
args.push(`startsWith=${options.startsWith}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString());
|
||||
return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()));
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href))
|
||||
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
|
||||
const hrefObs = this.getFindAllHref(options);
|
||||
|
||||
hrefObs
|
||||
.filter((href: string) => hasValue(href))
|
||||
.take(1)
|
||||
hrefObs.pipe(
|
||||
filter((href: string) => hasValue(href)),
|
||||
take(1))
|
||||
.subscribe((href: string) => {
|
||||
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
|
||||
this.requestService.configure(request);
|
||||
@@ -98,11 +88,11 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
||||
}
|
||||
|
||||
findById(id: string): Observable<RemoteData<TDomain>> {
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath)
|
||||
.map((endpoint: string) => this.getFindByIDHref(endpoint, id));
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
map((endpoint: string) => this.getFindByIDHref(endpoint, id)));
|
||||
|
||||
hrefObs
|
||||
.first((href: string) => hasValue(href))
|
||||
hrefObs.pipe(
|
||||
first((href: string) => hasValue(href)))
|
||||
.subscribe((href: string) => {
|
||||
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id);
|
||||
this.requestService.configure(request);
|
||||
@@ -116,6 +106,28 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
||||
return this.rdbService.buildSingle<TNormalized, TDomain>(href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new patch to the object cache to a specified object
|
||||
* @param {string} href The selflink of the object that will be patched
|
||||
* @param {Operation[]} operations The patch operations to be performed
|
||||
*/
|
||||
patch(href: string, operations: Operation[]) {
|
||||
this.objectCache.addPatch(href, operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new patch to the object cache
|
||||
* The patch is derived from the differences between the given object and its version in the object cache
|
||||
* @param {DSpaceObject} object The given object
|
||||
*/
|
||||
update(object: DSpaceObject) {
|
||||
const oldVersion = this.objectCache.getBySelfLink(object.self);
|
||||
const operations = compare(oldVersion, object);
|
||||
if (isNotEmpty(operations)) {
|
||||
this.objectCache.addPatch(object.self, operations);
|
||||
}
|
||||
}
|
||||
|
||||
create(dso: TDomain, parentUUID: string): Observable<RemoteData<TDomain>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
@@ -131,22 +143,20 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
||||
);
|
||||
|
||||
const selfLink$ = request$.pipe(
|
||||
map((request: RestRequest) => request.href),
|
||||
getResponseFromSelflink(this.responseCache),
|
||||
map((response: ResponseCacheEntry) => {
|
||||
if (!response.response.isSuccessful && response.response instanceof ErrorResponse) {
|
||||
const errorResponse: ErrorResponse = response.response;
|
||||
this.notificationsService.error('Server Error:', errorResponse.errorMessage, new NotificationOptions(-1));
|
||||
getResponseFromEntry(),
|
||||
map((response: RestResponse) => {
|
||||
if (!response.isSuccessful && response instanceof ErrorResponse) {
|
||||
this.notificationsService.error('Server Error:', response.errorMessage, new NotificationOptions(-1));
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
filterSuccessfulResponses(),
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: DSOSuccessResponse) => {
|
||||
return response.resourceSelfLinks[0];
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
) as Observable<string>;
|
||||
return this.rdbService.buildSingle(selfLink$) as Observable<RemoteData<TDomain>>;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RestResponse } from '../cache/response-cache.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
@@ -7,7 +7,7 @@ import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { RestResponse, DSOSuccessResponse } from '../cache/response-cache.models';
|
||||
import { RestResponse, DSOSuccessResponse } from '../cache/response.models';
|
||||
import { RestRequest } from './request.models';
|
||||
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
@@ -23,12 +23,14 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService,
|
||||
) { super();
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const processRequestDTO = this.process<NormalizedObject,ResourceType>(data.payload, request.href);
|
||||
const processRequestDTO = this.process<NormalizedObject, ResourceType>(data.payload, request.href);
|
||||
let objectList = processRequestDTO;
|
||||
|
||||
if (hasNoValue(processRequestDTO)) {
|
||||
return new DSOSuccessResponse([], data.statusCode, undefined)
|
||||
}
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from '../../../../node_modules/rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { FindByIDRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { DSpaceObjectDataService } from './dspace-object-data.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
|
||||
describe('DSpaceObjectDataService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
@@ -13,6 +14,7 @@ describe('DSpaceObjectDataService', () => {
|
||||
let halService: HALEndpointService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
const testObject = {
|
||||
uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746'
|
||||
} as DSpaceObject;
|
||||
@@ -37,11 +39,13 @@ describe('DSpaceObjectDataService', () => {
|
||||
}
|
||||
})
|
||||
});
|
||||
objectCache = {} as ObjectCacheService;
|
||||
|
||||
service = new DSpaceObjectDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
halService
|
||||
halService,
|
||||
objectCache
|
||||
)
|
||||
});
|
||||
|
||||
|
@@ -1,15 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { DataService } from './data.service';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestService } from './request.service';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
@@ -19,10 +20,10 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
|
||||
protected linkPath = 'dso';
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected authService: AuthService,
|
||||
protected notificationsService: NotificationsService,
|
||||
@@ -30,8 +31,8 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
|
||||
super();
|
||||
}
|
||||
|
||||
getScopedEndpoint(scope: string): Observable<string> {
|
||||
return undefined;
|
||||
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||
return this.halService.getEndpoint(linkPath);
|
||||
}
|
||||
|
||||
getFindByIDHref(endpoint, resourceID): string {
|
||||
@@ -47,11 +48,12 @@ export class DSpaceObjectDataService {
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected authService: AuthService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient) {
|
||||
this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, authService, notificationsService, http);
|
||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, authService, notificationsService, http);
|
||||
}
|
||||
|
||||
findById(uuid: string): Observable<RemoteData<DSpaceObject>> {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response-cache.models';
|
||||
import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
FacetConfigSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
} from '../cache/response.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
|
@@ -4,7 +4,7 @@ import {
|
||||
FacetValueMapSuccessResponse,
|
||||
FacetValueSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
} from '../cache/response.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
|
@@ -4,7 +4,7 @@ import {
|
||||
FacetValueMapSuccessResponse,
|
||||
FacetValueSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
} from '../cache/response.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
|
@@ -1,25 +1,34 @@
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/Rx';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { BrowseService } from '../browse/browse.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ItemDataService } from './item-data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { FindAllOptions } from './request.models';
|
||||
|
||||
describe('ItemDataService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: ItemDataService;
|
||||
let bs: BrowseService;
|
||||
const requestService = {} as RequestService;
|
||||
const responseCache = {} as ResponseCacheService;
|
||||
const rdbService = {} as RemoteDataBuildService;
|
||||
const objectCache = {} as ObjectCacheService;
|
||||
const store = {} as Store<CoreState>;
|
||||
const halEndpointService = {} as HALEndpointService;
|
||||
|
||||
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||
const options = Object.assign(new FindAllOptions(), {
|
||||
scopeID: scopeID,
|
||||
sort: {
|
||||
field: '',
|
||||
direction: undefined
|
||||
}
|
||||
});
|
||||
|
||||
const browsesEndpoint = 'https://rest.api/discover/browses';
|
||||
const itemBrowseEndpoint = `${browsesEndpoint}/author/items`;
|
||||
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
|
||||
@@ -37,25 +46,25 @@ describe('ItemDataService', () => {
|
||||
|
||||
function initTestService() {
|
||||
return new ItemDataService(
|
||||
responseCache,
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
bs,
|
||||
halEndpointService
|
||||
halEndpointService,
|
||||
objectCache
|
||||
);
|
||||
}
|
||||
|
||||
describe('getScopedEndpoint', () => {
|
||||
describe('getBrowseEndpoint', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
});
|
||||
|
||||
it('should return the endpoint to fetch Items within the given scope', () => {
|
||||
it('should return the endpoint to fetch Items within the given scope and starting with the given string', () => {
|
||||
bs = initMockBrowseService(true);
|
||||
service = initTestService();
|
||||
|
||||
const result = service.getScopedEndpoint(scopeID);
|
||||
const result = service.getBrowseEndpoint(options);
|
||||
const expected = cold('--b-', { b: scopedEndpoint });
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
@@ -67,7 +76,7 @@ describe('ItemDataService', () => {
|
||||
service = initTestService();
|
||||
});
|
||||
it('should throw an error', () => {
|
||||
const result = service.getScopedEndpoint(scopeID);
|
||||
const result = service.getBrowseEndpoint(options);
|
||||
const expected = cold('--#-', undefined, browseError);
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
|
||||
import {distinctUntilChanged, map, filter} from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { Observable } from 'rxjs';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { BrowseService } from '../browse/browse.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedItem } from '../cache/models/normalized-item.model';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
@@ -15,6 +14,8 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { DataService } from './data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
@@ -24,11 +25,11 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||
protected linkPath = 'items';
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
private bs: BrowseService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected authService: AuthService,
|
||||
protected notificationsService: NotificationsService,
|
||||
@@ -36,15 +37,21 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||
super();
|
||||
}
|
||||
|
||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||
if (isEmpty(scopeID)) {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
} else {
|
||||
return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath)
|
||||
.filter((href: string) => isNotEmpty(href))
|
||||
.map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString())
|
||||
.distinctUntilChanged();
|
||||
/**
|
||||
* Get the endpoint for browsing items
|
||||
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
|
||||
* @param {FindAllOptions} options
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||
let field = 'dc.date.issued';
|
||||
if (options.sort && options.sort.field) {
|
||||
field = options.sort.field;
|
||||
}
|
||||
return this.bs.getBrowseURLFor(field, linkPath).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()),
|
||||
distinctUntilChanged(),);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
||||
import { RestRequest } from './request.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataschemaParsingService implements ResponseParsingService {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { RestRequest } from './request.models';
|
||||
import { RestResponse } from '../cache/response-cache.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
export interface ResponseParsingService {
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
RegistryMetadatafieldsSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
} from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { RestRequest } from './request.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { RestRequest } from './request.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
import { RestRequest } from './request.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
/**
|
||||
* The list of RequestAction type definitions
|
||||
@@ -8,7 +9,8 @@ import { RestRequest } from './request.models';
|
||||
export const RequestActionTypes = {
|
||||
CONFIGURE: type('dspace/core/data/request/CONFIGURE'),
|
||||
EXECUTE: type('dspace/core/data/request/EXECUTE'),
|
||||
COMPLETE: type('dspace/core/data/request/COMPLETE')
|
||||
COMPLETE: type('dspace/core/data/request/COMPLETE'),
|
||||
RESET_TIMESTAMPS: type('dspace/core/data/request/RESET_TIMESTAMPS')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@@ -43,7 +45,10 @@ export class RequestExecuteAction implements Action {
|
||||
*/
|
||||
export class RequestCompleteAction implements Action {
|
||||
type = RequestActionTypes.COMPLETE;
|
||||
payload: string;
|
||||
payload: {
|
||||
uuid: string,
|
||||
response: RestResponse
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new RequestCompleteAction
|
||||
@@ -51,10 +56,32 @@ export class RequestCompleteAction implements Action {
|
||||
* @param uuid
|
||||
* the request's uuid
|
||||
*/
|
||||
constructor(uuid: string) {
|
||||
this.payload = uuid;
|
||||
constructor(uuid: string, response: RestResponse) {
|
||||
this.payload = {
|
||||
uuid,
|
||||
response
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to reset the timeAdded property of all responses in the cached objects
|
||||
*/
|
||||
export class ResetResponseTimestampsAction implements Action {
|
||||
type = RequestActionTypes.RESET_TIMESTAMPS;
|
||||
payload: number;
|
||||
|
||||
/**
|
||||
* Create a new ResetResponseTimestampsAction
|
||||
*
|
||||
* @param newTimestamp
|
||||
* the new timeAdded all objects should get
|
||||
*/
|
||||
constructor(newTimestamp: number) {
|
||||
this.payload = newTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
@@ -63,4 +90,5 @@ export class RequestCompleteAction implements Action {
|
||||
export type RequestAction
|
||||
= RequestConfigureAction
|
||||
| RequestExecuteAction
|
||||
| RequestCompleteAction;
|
||||
| RequestCompleteAction
|
||||
| ResetResponseTimestampsAction;
|
||||
|
@@ -1,42 +1,48 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { Inject, Injectable, Injector } from '@angular/core';
|
||||
import { Request } from '@angular/http';
|
||||
import { RequestArgs } from '@angular/http/src/interfaces';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
// tslint:disable-next-line:import-blacklist
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
|
||||
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction } from './request.actions';
|
||||
import {
|
||||
RequestActionTypes,
|
||||
RequestCompleteAction,
|
||||
RequestExecuteAction,
|
||||
ResetResponseTimestampsAction
|
||||
} from './request.actions';
|
||||
import { RequestError, RestRequest } from './request.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { RequestService } from './request.service';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
||||
import { catchError, flatMap, map, take, tap } from 'rxjs/operators';
|
||||
import { catchError, filter, flatMap, map, take, tap } from 'rxjs/operators';
|
||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
|
||||
export const addToResponseCacheAndCompleteAction = (request: RestRequest, responseCache: ResponseCacheService, envConfig: GlobalConfig) =>
|
||||
(source: Observable<ErrorResponse>): Observable<RequestCompleteAction> =>
|
||||
export const addToResponseCacheAndCompleteAction = (request: RestRequest, envConfig: GlobalConfig) =>
|
||||
(source: Observable<RestResponse>): Observable<RequestCompleteAction> =>
|
||||
source.pipe(
|
||||
tap((response: RestResponse) => responseCache.add(request.href, response, envConfig.cache.msToLive)),
|
||||
map((response: RestResponse) => new RequestCompleteAction(request.uuid))
|
||||
map((response: RestResponse) => {
|
||||
return new RequestCompleteAction(request.uuid, response)
|
||||
})
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class RequestEffects {
|
||||
|
||||
@Effect() execute = this.actions$.ofType(RequestActionTypes.EXECUTE).pipe(
|
||||
@Effect() execute = this.actions$.pipe(
|
||||
ofType(RequestActionTypes.EXECUTE),
|
||||
flatMap((action: RequestExecuteAction) => {
|
||||
return this.requestService.getByUUID(action.payload).pipe(
|
||||
take(1)
|
||||
);
|
||||
}),
|
||||
filter((entry: RequestEntry) => hasValue(entry)),
|
||||
map((entry: RequestEntry) => entry.request),
|
||||
tap((entry: RequestEntry) => console.log(entry)),
|
||||
flatMap((request: RestRequest) => {
|
||||
let body;
|
||||
if (isNotEmpty(request.body)) {
|
||||
@@ -45,20 +51,32 @@ export class RequestEffects {
|
||||
}
|
||||
return this.restApi.request(request.method, request.href, body, request.options).pipe(
|
||||
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
|
||||
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig),
|
||||
catchError((error: RequestError) => Observable.of(new ErrorResponse(error)).pipe(
|
||||
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig)
|
||||
addToResponseCacheAndCompleteAction(request, this.EnvConfig),
|
||||
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
|
||||
addToResponseCacheAndCompleteAction(request, this.EnvConfig)
|
||||
))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* When the store is rehydrated in the browser, set all cache
|
||||
* timestamps to 'now', because the time zone of the server can
|
||||
* differ from the client.
|
||||
*
|
||||
* This assumes that the server cached everything a negligible
|
||||
* time ago, and will likely need to be revisited later
|
||||
*/
|
||||
@Effect() fixTimestampsOnRehydrate = this.actions$
|
||||
.pipe(ofType(StoreActionTypes.REHYDRATE),
|
||||
map(() => new ResetResponseTimestampsAction(new Date().getTime()))
|
||||
);
|
||||
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
|
||||
private actions$: Actions,
|
||||
private restApi: DSpaceRESTv2Service,
|
||||
private injector: Injector,
|
||||
private responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService
|
||||
) { }
|
||||
|
||||
|
@@ -9,51 +9,41 @@ import { ConfigResponseParsingService } from './config-response-parsing.service'
|
||||
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Represents a Request Method.
|
||||
*
|
||||
* I didn't reuse the RequestMethod enum in @angular/http because
|
||||
* it uses numbers. The string values here are more clear when
|
||||
* debugging.
|
||||
*
|
||||
* The ones commented out are still unsupported in the rest of the codebase
|
||||
*/
|
||||
export enum RestRequestMethod {
|
||||
Get = 'GET',
|
||||
Post = 'POST',
|
||||
Put = 'PUT',
|
||||
Delete = 'DELETE',
|
||||
Options = 'OPTIONS',
|
||||
Head = 'HEAD',
|
||||
Patch = 'PATCH'
|
||||
}
|
||||
|
||||
export abstract class RestRequest {
|
||||
public responseMsToLive = 0;
|
||||
constructor(
|
||||
public uuid: string,
|
||||
public href: string,
|
||||
public method: RestRequestMethod = RestRequestMethod.Get,
|
||||
public method: RestRequestMethod = RestRequestMethod.GET,
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
public options?: HttpOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return DSOResponseParsingService;
|
||||
}
|
||||
|
||||
get toCache(): boolean {
|
||||
return this.responseMsToLive > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class GetRequest extends RestRequest {
|
||||
public responseMsToLive = 60 * 15 * 1000;
|
||||
|
||||
constructor(
|
||||
public uuid: string,
|
||||
public href: string,
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
public options?: HttpOptions,
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.Get, body)
|
||||
super(uuid, href, RestRequestMethod.GET, body, options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +54,7 @@ export class PostRequest extends RestRequest {
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.Post, body)
|
||||
super(uuid, href, RestRequestMethod.POST, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +65,7 @@ export class PutRequest extends RestRequest {
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.Put, body)
|
||||
super(uuid, href, RestRequestMethod.PUT, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +76,7 @@ export class DeleteRequest extends RestRequest {
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.Delete, body)
|
||||
super(uuid, href, RestRequestMethod.DELETE, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +87,7 @@ export class OptionsRequest extends RestRequest {
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.Options, body)
|
||||
super(uuid, href, RestRequestMethod.OPTIONS, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +98,7 @@ export class HeadRequest extends RestRequest {
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.Head, body)
|
||||
super(uuid, href, RestRequestMethod.HEAD, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +109,7 @@ export class PatchRequest extends RestRequest {
|
||||
public body?: any,
|
||||
public options?: HttpOptions
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.Patch, body)
|
||||
super(uuid, href, RestRequestMethod.PATCH, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +171,12 @@ export class BrowseEntriesRequest extends GetRequest {
|
||||
}
|
||||
}
|
||||
|
||||
export class BrowseItemsRequest extends GetRequest {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return BrowseItemsResponseParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigRequest extends GetRequest {
|
||||
constructor(uuid: string, href: string) {
|
||||
super(uuid, href);
|
||||
|
@@ -2,16 +2,20 @@ import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { requestReducer, RequestState } from './request.reducer';
|
||||
import {
|
||||
RequestCompleteAction, RequestConfigureAction, RequestExecuteAction
|
||||
RequestCompleteAction,
|
||||
RequestConfigureAction,
|
||||
RequestExecuteAction, ResetResponseTimestampsAction
|
||||
} from './request.actions';
|
||||
import { GetRequest, RestRequest } from './request.models';
|
||||
import { GetRequest } from './request.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
const response = new RestResponse(true, 'OK');
|
||||
class NullAction extends RequestCompleteAction {
|
||||
type = null;
|
||||
payload = null;
|
||||
|
||||
constructor() {
|
||||
super(null);
|
||||
super(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +29,8 @@ describe('requestReducer', () => {
|
||||
request: new GetRequest(id1, link1),
|
||||
requestPending: false,
|
||||
responsePending: false,
|
||||
completed: false
|
||||
completed: false,
|
||||
response: undefined
|
||||
}
|
||||
};
|
||||
deepFreeze(testState);
|
||||
@@ -56,6 +61,7 @@ describe('requestReducer', () => {
|
||||
expect(newState[id2].requestPending).toEqual(true);
|
||||
expect(newState[id2].responsePending).toEqual(false);
|
||||
expect(newState[id2].completed).toEqual(false);
|
||||
expect(newState[id2].response).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should set \'requestPending\' to false, \'responsePending\' to true and leave \'completed\' untouched for the given RestRequest in the state, in response to an EXECUTE action', () => {
|
||||
@@ -69,11 +75,13 @@ describe('requestReducer', () => {
|
||||
expect(newState[id1].requestPending).toEqual(false);
|
||||
expect(newState[id1].responsePending).toEqual(true);
|
||||
expect(newState[id1].completed).toEqual(state[id1].completed);
|
||||
expect(newState[id1].response).toEqual(undefined)
|
||||
});
|
||||
|
||||
it('should leave \'requestPending\' untouched, set \'responsePending\' to false and \'completed\' to true for the given RestRequest in the state, in response to a COMPLETE action', () => {
|
||||
const state = testState;
|
||||
|
||||
const action = new RequestCompleteAction(id1);
|
||||
const action = new RequestCompleteAction(id1, response);
|
||||
const newState = requestReducer(state, action);
|
||||
|
||||
expect(newState[id1].request.uuid).toEqual(id1);
|
||||
@@ -81,5 +89,25 @@ describe('requestReducer', () => {
|
||||
expect(newState[id1].requestPending).toEqual(state[id1].requestPending);
|
||||
expect(newState[id1].responsePending).toEqual(false);
|
||||
expect(newState[id1].completed).toEqual(true);
|
||||
expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful)
|
||||
expect(newState[id1].response.statusCode).toEqual(response.statusCode)
|
||||
expect(newState[id1].response.timeAdded).toBeTruthy()
|
||||
});
|
||||
|
||||
it('should leave \'requestPending\' untouched, should leave \'responsePending\' untouched and leave \'completed\' untouched, but update the response\'s timeAdded for the given RestRequest in the state, in response to a COMPLETE action', () => {
|
||||
const update = Object.assign({}, testState[id1], {response});
|
||||
const state = Object.assign({}, testState, {[id1]: update});
|
||||
const timeStamp = 1000;
|
||||
const action = new ResetResponseTimestampsAction(timeStamp);
|
||||
const newState = requestReducer(state, action);
|
||||
|
||||
expect(newState[id1].request.uuid).toEqual(state[id1].request.uuid);
|
||||
expect(newState[id1].request.href).toEqual(state[id1].request.href);
|
||||
expect(newState[id1].requestPending).toEqual(state[id1].requestPending);
|
||||
expect(newState[id1].responsePending).toEqual(state[id1].responsePending);
|
||||
expect(newState[id1].completed).toEqual(state[id1].completed);
|
||||
expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful);
|
||||
expect(newState[id1].response.statusCode).toEqual(response.statusCode);
|
||||
expect(newState[id1].response.timeAdded).toBe(timeStamp);
|
||||
});
|
||||
});
|
||||
|
@@ -1,14 +1,16 @@
|
||||
import {
|
||||
RequestActionTypes, RequestAction, RequestConfigureAction,
|
||||
RequestExecuteAction, RequestCompleteAction
|
||||
RequestExecuteAction, RequestCompleteAction, ResetResponseTimestampsAction
|
||||
} from './request.actions';
|
||||
import { RestRequest } from './request.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
export class RequestEntry {
|
||||
request: RestRequest;
|
||||
requestPending: boolean;
|
||||
responsePending: boolean;
|
||||
completed: boolean;
|
||||
response: RestResponse
|
||||
}
|
||||
|
||||
export interface RequestState {
|
||||
@@ -32,6 +34,9 @@ export function requestReducer(state = initialState, action: RequestAction): Req
|
||||
case RequestActionTypes.COMPLETE: {
|
||||
return completeRequest(state, action as RequestCompleteAction);
|
||||
}
|
||||
case RequestActionTypes.RESET_TIMESTAMPS: {
|
||||
return resetResponseTimestamps(state, action as ResetResponseTimestampsAction);
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
@@ -45,18 +50,19 @@ function configureRequest(state: RequestState, action: RequestConfigureAction):
|
||||
request: action.payload,
|
||||
requestPending: true,
|
||||
responsePending: false,
|
||||
completed: false
|
||||
completed: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState {
|
||||
return Object.assign({}, state, {
|
||||
const obs = Object.assign({}, state, {
|
||||
[action.payload]: Object.assign({}, state[action.payload], {
|
||||
requestPending: false,
|
||||
responsePending: true
|
||||
})
|
||||
});
|
||||
return obs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,10 +76,22 @@ function executeRequest(state: RequestState, action: RequestExecuteAction): Requ
|
||||
* the new state, with the response added to the request
|
||||
*/
|
||||
function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState {
|
||||
const time = new Date().getTime();
|
||||
return Object.assign({}, state, {
|
||||
[action.payload]: Object.assign({}, state[action.payload], {
|
||||
[action.payload.uuid]: Object.assign({}, state[action.payload.uuid], {
|
||||
responsePending: false,
|
||||
completed: true
|
||||
completed: true,
|
||||
response: Object.assign({}, action.payload.response, { timeAdded: time })
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) {
|
||||
const newState = Object.create(null);
|
||||
Object.keys(state).forEach((key) => {
|
||||
newState[key] = Object.assign({}, state[key],
|
||||
{ response: Object.assign({}, state[key].response, { timeAdded: action.payload }) }
|
||||
);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
@@ -1,16 +1,12 @@
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||
import { getMockStore } from '../../shared/mocks/mock-store';
|
||||
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { UUIDService } from '../shared/uuid.service';
|
||||
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
|
||||
import * as ngrx from '@ngrx/store';
|
||||
import {
|
||||
DeleteRequest,
|
||||
GetRequest,
|
||||
@@ -18,15 +14,19 @@ import {
|
||||
OptionsRequest,
|
||||
PatchRequest,
|
||||
PostRequest,
|
||||
PutRequest, RestRequest
|
||||
PutRequest,
|
||||
RestRequest
|
||||
} from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { ActionsSubject, Store } from '@ngrx/store';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
|
||||
describe('RequestService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: RequestService;
|
||||
let serviceAsAny: any;
|
||||
let objectCache: ObjectCacheService;
|
||||
let responseCache: ResponseCacheService;
|
||||
let uuidService: UUIDService;
|
||||
let store: Store<CoreState>;
|
||||
|
||||
@@ -39,23 +39,25 @@ describe('RequestService', () => {
|
||||
const testOptionsRequest = new OptionsRequest(testUUID, testHref);
|
||||
const testHeadRequest = new HeadRequest(testUUID, testHref);
|
||||
const testPatchRequest = new PatchRequest(testUUID, testHref);
|
||||
|
||||
let selectSpy;
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
objectCache = getMockObjectCacheService();
|
||||
(objectCache.hasBySelfLink as any).and.returnValue(false);
|
||||
|
||||
responseCache = getMockResponseCacheService();
|
||||
(responseCache.has as any).and.returnValue(false);
|
||||
(responseCache.get as any).and.returnValue(Observable.of(undefined));
|
||||
|
||||
uuidService = getMockUUIDService();
|
||||
|
||||
store = getMockStore<CoreState>();
|
||||
(store.select as any).and.returnValue(Observable.of(undefined));
|
||||
store = new Store<CoreState>(new BehaviorSubject({}), new ActionsSubject(), null);
|
||||
selectSpy = spyOnProperty(ngrx, 'select');
|
||||
selectSpy.and.callFake(() => {
|
||||
return () => {
|
||||
return () => cold('a', { a: undefined });
|
||||
};
|
||||
});
|
||||
|
||||
service = new RequestService(
|
||||
objectCache,
|
||||
responseCache,
|
||||
uuidService,
|
||||
store
|
||||
);
|
||||
@@ -74,7 +76,7 @@ describe('RequestService', () => {
|
||||
describe('isPending', () => {
|
||||
describe('before the request is configured', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined));
|
||||
spyOn(service, 'getByHref').and.returnValue(observableOf(undefined));
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
@@ -87,7 +89,7 @@ describe('RequestService', () => {
|
||||
|
||||
describe('when the request has been configured but hasn\'t reached the store yet', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined));
|
||||
spyOn(service, 'getByHref').and.returnValue(observableOf(undefined));
|
||||
serviceAsAny.requestsOnTheirWayToTheStore = [testHref];
|
||||
});
|
||||
|
||||
@@ -101,7 +103,7 @@ describe('RequestService', () => {
|
||||
|
||||
describe('when the request has reached the store, before the server responds', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'getByHref').and.returnValue(Observable.of({
|
||||
spyOn(service, 'getByHref').and.returnValue(observableOf({
|
||||
completed: false
|
||||
}))
|
||||
});
|
||||
@@ -116,7 +118,7 @@ describe('RequestService', () => {
|
||||
|
||||
describe('after the server responds', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'getByHref').and.returnValues(Observable.of({
|
||||
spyOn(service, 'getByHref').and.returnValues(observableOf({
|
||||
completed: true
|
||||
}));
|
||||
});
|
||||
@@ -134,11 +136,15 @@ describe('RequestService', () => {
|
||||
describe('getByUUID', () => {
|
||||
describe('if the request with the specified UUID exists in the store', () => {
|
||||
beforeEach(() => {
|
||||
(store.select as any).and.returnValues(hot('a', {
|
||||
a: {
|
||||
completed: true
|
||||
}
|
||||
}));
|
||||
selectSpy.and.callFake(() => {
|
||||
return () => {
|
||||
return () => hot('a', {
|
||||
a: {
|
||||
completed: true
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an Observable of the RequestEntry', () => {
|
||||
@@ -155,18 +161,20 @@ describe('RequestService', () => {
|
||||
|
||||
describe('if the request with the specified UUID doesn\'t exist in the store', () => {
|
||||
beforeEach(() => {
|
||||
(store.select as any).and.returnValues(hot('a', {
|
||||
a: undefined
|
||||
}));
|
||||
selectSpy.and.callFake(() => {
|
||||
return () => {
|
||||
return () => hot('a', { a: undefined });
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an Observable of undefined', () => {
|
||||
const result = service.getByUUID(testUUID);
|
||||
const expected = cold('b', {
|
||||
b: undefined
|
||||
});
|
||||
// const expected = cold('b', {
|
||||
// b: undefined
|
||||
// });
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
scheduler.expectObservable(result).toBe('b', {b: undefined});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,9 +183,11 @@ describe('RequestService', () => {
|
||||
describe('getByHref', () => {
|
||||
describe('when the request with the specified href exists in the store', () => {
|
||||
beforeEach(() => {
|
||||
(store.select as any).and.returnValues(hot('a', {
|
||||
a: testUUID
|
||||
}));
|
||||
selectSpy.and.callFake(() => {
|
||||
return () => {
|
||||
return () => hot('a', { a: testUUID });
|
||||
};
|
||||
});
|
||||
spyOn(service, 'getByUUID').and.returnValue(cold('b', {
|
||||
b: {
|
||||
completed: true
|
||||
@@ -199,9 +209,11 @@ describe('RequestService', () => {
|
||||
|
||||
describe('when the request with the specified href doesn\'t exist in the store', () => {
|
||||
beforeEach(() => {
|
||||
(store.select as any).and.returnValues(hot('a', {
|
||||
a: undefined
|
||||
}));
|
||||
selectSpy.and.callFake(() => {
|
||||
return () => {
|
||||
return () => hot('a', { a: undefined });
|
||||
};
|
||||
});
|
||||
spyOn(service, 'getByUUID').and.returnValue(cold('b', {
|
||||
b: undefined
|
||||
}));
|
||||
@@ -241,7 +253,8 @@ describe('RequestService', () => {
|
||||
});
|
||||
|
||||
it('should dispatch the request', () => {
|
||||
service.configure(request);
|
||||
scheduler.schedule(() => service.configure(request));
|
||||
scheduler.flush();
|
||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request);
|
||||
});
|
||||
});
|
||||
@@ -306,7 +319,7 @@ describe('RequestService', () => {
|
||||
describe('when the request is cached', () => {
|
||||
describe('in the ObjectCache', () => {
|
||||
beforeEach(() => {
|
||||
(objectCache.hasBySelfLink as any).and.returnValues(true);
|
||||
(objectCache.hasBySelfLink as any).and.returnValue(true);
|
||||
});
|
||||
|
||||
it('should return true', () => {
|
||||
@@ -318,12 +331,13 @@ describe('RequestService', () => {
|
||||
});
|
||||
describe('in the responseCache', () => {
|
||||
beforeEach(() => {
|
||||
(responseCache.has as any).and.returnValues(true);
|
||||
spyOn(serviceAsAny, 'isReusable').and.returnValue(observableOf(true));
|
||||
spyOn(serviceAsAny, 'getByHref').and.returnValue(observableOf(undefined));
|
||||
});
|
||||
|
||||
describe('and it\'s a DSOSuccessResponse', () => {
|
||||
beforeEach(() => {
|
||||
(responseCache.get as any).and.returnValues(Observable.of({
|
||||
(serviceAsAny.getByHref as any).and.returnValue(observableOf({
|
||||
response: {
|
||||
isSuccessful: true,
|
||||
resourceSelfLinks: [
|
||||
@@ -345,6 +359,7 @@ describe('RequestService', () => {
|
||||
});
|
||||
it('should return false if not all top level links in the response are cached in the object cache', () => {
|
||||
(objectCache.hasBySelfLink as any).and.returnValues(false, true, false);
|
||||
spyOn(service, 'isPending').and.returnValue(false);
|
||||
|
||||
const result = serviceAsAny.isCachedOrPending(testGetRequest);
|
||||
const expected = false;
|
||||
@@ -352,11 +367,12 @@ describe('RequestService', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and it isn\'t a DSOSuccessResponse', () => {
|
||||
beforeEach(() => {
|
||||
(objectCache.hasBySelfLink as any).and.returnValues(false);
|
||||
(responseCache.has as any).and.returnValues(true);
|
||||
(responseCache.get as any).and.returnValues(Observable.of({
|
||||
(objectCache.hasBySelfLink as any).and.returnValue(false);
|
||||
(service as any).isReusable.and.returnValue(observableOf(true));
|
||||
(serviceAsAny.getByHref as any).and.returnValue(observableOf({
|
||||
response: {
|
||||
isSuccessful: true
|
||||
}
|
||||
@@ -398,6 +414,10 @@ describe('RequestService', () => {
|
||||
});
|
||||
|
||||
describe('dispatchRequest', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(store, 'dispatch');
|
||||
});
|
||||
|
||||
it('should dispatch a RequestConfigureAction', () => {
|
||||
const request = testGetRequest;
|
||||
serviceAsAny.dispatchRequest(request);
|
||||
@@ -428,7 +448,11 @@ describe('RequestService', () => {
|
||||
|
||||
describe('when the request is added to the store', () => {
|
||||
it('should stop tracking the request', () => {
|
||||
(store.select as any).and.returnValues(Observable.of({ request }));
|
||||
selectSpy.and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf({ request });
|
||||
};
|
||||
});
|
||||
serviceAsAny.trackRequestsOnTheirWayToTheStore(request);
|
||||
expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy();
|
||||
});
|
||||
|
@@ -1,30 +1,43 @@
|
||||
import { merge as observableMerge, Observable, of as observableOf } from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
find,
|
||||
first,
|
||||
map,
|
||||
mergeMap,
|
||||
reduce,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap
|
||||
} from 'rxjs/operators';
|
||||
import { race as observableRace } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { hasNoValue, hasValue, isNotUndefined } from '../../shared/empty.util';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { coreSelector, CoreState } from '../core.reducers';
|
||||
import { IndexName } from '../index/index.reducer';
|
||||
import { pathSelector } from '../shared/selectors';
|
||||
import { UUIDService } from '../shared/uuid.service';
|
||||
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
|
||||
import { GetRequest, RestRequest, RestRequestMethod } from './request.models';
|
||||
import { GetRequest, RestRequest } from './request.models';
|
||||
|
||||
import { RequestEntry, RequestState } from './request.reducer';
|
||||
import { ResponseCacheRemoveAction } from '../cache/response-cache.actions';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
import { AddToIndexAction } from '../index/index.actions';
|
||||
|
||||
@Injectable()
|
||||
export class RequestService {
|
||||
private requestsOnTheirWayToTheStore: string[] = [];
|
||||
|
||||
constructor(private objectCache: ObjectCacheService,
|
||||
private responseCache: ResponseCacheService,
|
||||
private uuidService: UUIDService,
|
||||
private store: Store<CoreState>) {
|
||||
}
|
||||
@@ -37,6 +50,10 @@ export class RequestService {
|
||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.REQUEST, href);
|
||||
}
|
||||
|
||||
private originalUUIDFromUUIDSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.UUID_MAPPING, uuid);
|
||||
}
|
||||
|
||||
generateRequestId(): string {
|
||||
return `client/${this.uuidService.generate()}`;
|
||||
}
|
||||
@@ -49,8 +66,8 @@ export class RequestService {
|
||||
|
||||
// then check the store
|
||||
let isPending = false;
|
||||
this.getByHref(request.href)
|
||||
.take(1)
|
||||
this.getByHref(request.href).pipe(
|
||||
take(1))
|
||||
.subscribe((re: RequestEntry) => {
|
||||
isPending = (hasValue(re) && !re.completed)
|
||||
});
|
||||
@@ -59,51 +76,69 @@ export class RequestService {
|
||||
}
|
||||
|
||||
getByUUID(uuid: string): Observable<RequestEntry> {
|
||||
return this.store.select(this.entryFromUUIDSelector(uuid));
|
||||
return observableRace(
|
||||
this.store.pipe(select(this.entryFromUUIDSelector(uuid))),
|
||||
this.store.pipe(
|
||||
select(this.originalUUIDFromUUIDSelector(uuid)),
|
||||
switchMap((originalUUID) => {
|
||||
return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID)))
|
||||
},
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
getByHref(href: string): Observable<RequestEntry> {
|
||||
return this.store.select(this.uuidFromHrefSelector(href))
|
||||
.flatMap((uuid: string) => this.getByUUID(uuid));
|
||||
return this.store.pipe(
|
||||
select(this.uuidFromHrefSelector(href)),
|
||||
mergeMap((uuid: string) => this.getByUUID(uuid))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
|
||||
configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void {
|
||||
const isGetRequest = request.method === RestRequestMethod.Get;
|
||||
const isGetRequest = request.method === RestRequestMethod.GET;
|
||||
if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) {
|
||||
this.dispatchRequest(request);
|
||||
if (isGetRequest && !forceBypassCache) {
|
||||
this.trackRequestsOnTheirWayToTheStore(request);
|
||||
}
|
||||
} else {
|
||||
this.getByHref(request.href).pipe(
|
||||
filter((entry) => hasValue(entry)),
|
||||
take(1)
|
||||
).subscribe((entry) => {
|
||||
return this.store.dispatch(new AddToIndexAction(IndexName.UUID_MAPPING, request.uuid, entry.request.uuid))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private isCachedOrPending(request: GetRequest) {
|
||||
let isCached = this.objectCache.hasBySelfLink(request.href);
|
||||
if (!isCached && this.responseCache.has(request.href)) {
|
||||
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||
.take(1)
|
||||
.map((entry: ResponseCacheEntry) => entry.response)
|
||||
.share()
|
||||
.partition((response: RestResponse) => response.isSuccessful);
|
||||
if (isCached) {
|
||||
const responses: Observable<RestResponse> = this.isReusable(request.uuid).pipe(
|
||||
filter((reusable: boolean) => reusable),
|
||||
switchMap(() => {
|
||||
return this.getByHref(request.href).pipe(
|
||||
getResponseFromEntry(),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
));
|
||||
|
||||
const [dsoSuccessResponse, otherSuccessResponse] = successResponse
|
||||
.share()
|
||||
.partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks));
|
||||
const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error.
|
||||
const dsoSuccessResponses = responses.pipe(
|
||||
filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)),
|
||||
map((response: DSOSuccessResponse) => response.resourceSelfLinks),
|
||||
map((resourceSelfLinks: string[]) => resourceSelfLinks
|
||||
.every((selfLink) => this.objectCache.hasBySelfLink(selfLink))
|
||||
));
|
||||
|
||||
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)
|
||||
.map((resourceSelfLinks: string[]) => resourceSelfLinks
|
||||
.every((selfLink) => this.objectCache.hasBySelfLink(selfLink))
|
||||
)
|
||||
).subscribe((c) => isCached = c);
|
||||
const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true));
|
||||
|
||||
observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c);
|
||||
}
|
||||
|
||||
const isPending = this.isPending(request);
|
||||
|
||||
return isCached || isPending;
|
||||
}
|
||||
|
||||
@@ -121,11 +156,45 @@ export class RequestService {
|
||||
*/
|
||||
private trackRequestsOnTheirWayToTheStore(request: GetRequest) {
|
||||
this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href];
|
||||
this.store.select(this.entryFromUUIDSelector(request.href))
|
||||
.filter((re: RequestEntry) => hasValue(re))
|
||||
.take(1)
|
||||
.subscribe((re: RequestEntry) => {
|
||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href)
|
||||
});
|
||||
this.store.pipe(select(this.entryFromUUIDSelector(request.href)),
|
||||
filter((re: RequestEntry) => hasValue(re)),
|
||||
take(1)
|
||||
).subscribe((re: RequestEntry) => {
|
||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href)
|
||||
});
|
||||
}
|
||||
|
||||
commit(method?: RestRequestMethod) {
|
||||
this.store.dispatch(new CommitSSBAction(method))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a Response should still be cached
|
||||
*
|
||||
* @param entry
|
||||
* the entry to check
|
||||
* @return boolean
|
||||
* false if the entry is null, undefined, or its time to
|
||||
* live has been exceeded, true otherwise
|
||||
*/
|
||||
private isReusable(uuid: string): Observable<boolean> {
|
||||
if (hasNoValue(uuid)) {
|
||||
return observableOf(false);
|
||||
} else {
|
||||
const requestEntry$ = this.getByUUID(uuid);
|
||||
return requestEntry$.pipe(
|
||||
filter((entry: RequestEntry) => hasValue(entry) && hasValue(entry.response)),
|
||||
map((entry: RequestEntry) => {
|
||||
if (hasValue(entry) && entry.response.isSuccessful) {
|
||||
const timeOutdated = entry.response.timeAdded + entry.request.responseMsToLive;
|
||||
const isOutDated = new Date().getTime() > timeOutdated;
|
||||
return !isOutDated;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
return observableOf(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
src/app/core/data/rest-request-method.ts
Normal file
18
src/app/core/data/rest-request-method.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Represents a Request Method.
|
||||
*
|
||||
* I didn't reuse the RequestMethod enum in @angular/http because
|
||||
* it uses numbers. The string values here are more clear when
|
||||
* debugging.
|
||||
*
|
||||
* The ones commented out are still unsupported in the rest of the codebase
|
||||
*/
|
||||
export enum RestRequestMethod {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PUT = 'PUT',
|
||||
DELETE = 'DELETE',
|
||||
OPTIONS = 'OPTIONS',
|
||||
HEAD = 'HEAD',
|
||||
PATCH = 'PATCH'
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RestResponse, SearchSuccessResponse } from '../cache/response-cache.models';
|
||||
import { RestResponse, SearchSuccessResponse } from '../cache/response.models';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
Reference in New Issue
Block a user