mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 07:23:03 +00:00
tests and documentation
This commit is contained in:
46
src/app/core/cache/builders/build-decorators.ts
vendored
46
src/app/core/cache/builders/build-decorators.ts
vendored
@@ -33,6 +33,15 @@ export function getClassForType(type: string | ResourceType) {
|
||||
return typeMap.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* A class decorator to indicate that this class is a dataservice
|
||||
* for a given resource type.
|
||||
*
|
||||
* "dataservice" in this context means that it has findByHref and
|
||||
* findAllByHref methods.
|
||||
*
|
||||
* @param resourceType the resource type the class is a dataservice for
|
||||
*/
|
||||
export function dataService(resourceType: ResourceType): any {
|
||||
return (target: any) => {
|
||||
if (hasNoValue(resourceType)) {
|
||||
@@ -48,6 +57,11 @@ export function dataService(resourceType: ResourceType): any {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dataservice matching the given resource type
|
||||
*
|
||||
* @param resourceType the resource type you want the matching dataservice for
|
||||
*/
|
||||
export function getDataServiceFor<T extends CacheableObject>(resourceType: ResourceType) {
|
||||
return dataServiceMap.get(resourceType.value);
|
||||
}
|
||||
@@ -71,6 +85,9 @@ export function resolvedLink<T extends DataService<any>, K extends keyof T>(prov
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to represent the data that can be set by the @link decorator
|
||||
*/
|
||||
export class LinkDefinition<T extends HALResource> {
|
||||
resourceType: ResourceType;
|
||||
isList = false;
|
||||
@@ -78,6 +95,19 @@ export class LinkDefinition<T extends HALResource> {
|
||||
propertyName: keyof T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A property decorator to indicate that a certain property is the placeholder
|
||||
* where the contents of a resolved link should be stored.
|
||||
*
|
||||
* e.g. if an Item has an hal link for bundles, and an item.bundles property
|
||||
* this decorator should decorate that item.bundles property.
|
||||
*
|
||||
* @param resourceType the resource type of the object(s) the link retrieves
|
||||
* @param isList an optional boolean indicating whether or not it concerns a list,
|
||||
* defaults to false
|
||||
* @param linkName an optional string in case the HALLink name differs from the
|
||||
* property name
|
||||
*/
|
||||
export const link = <T extends HALResource>(
|
||||
resourceType: ResourceType,
|
||||
isList = false,
|
||||
@@ -105,10 +135,20 @@ export const link = <T extends HALResource>(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all LinkDefinitions for a model class
|
||||
* @param source
|
||||
*/
|
||||
export const getLinkDefinitions = <T extends HALResource>(source: GenericConstructor<T>): Map<keyof T['_links'], LinkDefinition<T>> => {
|
||||
return linkMap.get(source);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a specific LinkDefinition for a model class
|
||||
*
|
||||
* @param source the model class
|
||||
* @param linkName the name of the link
|
||||
*/
|
||||
export const getLinkDefinition = <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']): LinkDefinition<T> => {
|
||||
const sourceMap = linkMap.get(source);
|
||||
if (hasValue(sourceMap)) {
|
||||
@@ -118,6 +158,12 @@ export const getLinkDefinition = <T extends HALResource>(source: GenericConstruc
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A class level decorator to indicate you want to inherit @link annotations
|
||||
* from a parent class.
|
||||
*
|
||||
* @param parent the parent class to inherit @link annotations from
|
||||
*/
|
||||
export const inheritLinkAnnotations = (parent: any): any => {
|
||||
return (child: any) => {
|
||||
const parentMap: Map<string, LinkDefinition<any>> = linkMap.get(parent) || new Map();
|
||||
|
222
src/app/core/cache/builders/link.service.spec.ts
vendored
Normal file
222
src/app/core/cache/builders/link.service.spec.ts
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { FindListOptions } from '../../data/request.models';
|
||||
import { HALLink } from '../../shared/hal-link.model';
|
||||
import { HALResource } from '../../shared/hal-resource.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import * as decorators from './build-decorators';
|
||||
import { getDataServiceFor } from './build-decorators';
|
||||
import { LinkService } from './link.service';
|
||||
|
||||
const spyOnFunction = <T>(obj: T, func: keyof T) => {
|
||||
const spy = jasmine.createSpy(func as string);
|
||||
spyOnProperty(obj, func, 'get').and.returnValue(spy);
|
||||
|
||||
return spy;
|
||||
};
|
||||
|
||||
const TEST_MODEL = new ResourceType('testmodel');
|
||||
let result: any;
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class TestModel implements HALResource {
|
||||
static type = TEST_MODEL;
|
||||
|
||||
type = TEST_MODEL;
|
||||
|
||||
value: string;
|
||||
|
||||
_links: {
|
||||
self: HALLink;
|
||||
predecessor: HALLink;
|
||||
successor: HALLink;
|
||||
};
|
||||
|
||||
predecessor?: TestModel;
|
||||
successor?: TestModel;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class TestDataService {
|
||||
findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<any>>) {
|
||||
return 'findAllByHref'
|
||||
}
|
||||
findByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<any>>) {
|
||||
return 'findByHref'
|
||||
}
|
||||
}
|
||||
|
||||
let testDataService: TestDataService;
|
||||
|
||||
let testModel: TestModel;
|
||||
|
||||
describe('LinkService', () => {
|
||||
let service: LinkService;
|
||||
|
||||
beforeEach(() => {
|
||||
testModel = Object.assign(new TestModel(), {
|
||||
value: 'a test value',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'http://self.link'
|
||||
},
|
||||
predecessor: {
|
||||
href: 'http://predecessor.link'
|
||||
},
|
||||
successor: {
|
||||
href: 'http://successor.link'
|
||||
},
|
||||
}
|
||||
});
|
||||
testDataService = new TestDataService();
|
||||
spyOn(testDataService, 'findAllByHref').and.callThrough();
|
||||
spyOn(testDataService, 'findByHref').and.callThrough();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [LinkService, {
|
||||
provide: TestDataService,
|
||||
useValue: testDataService
|
||||
}]
|
||||
});
|
||||
service = TestBed.get(LinkService);
|
||||
});
|
||||
|
||||
describe('resolveLink', () => {
|
||||
describe(`when the linkdefinition concerns a single object`, () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor'
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
});
|
||||
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
||||
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, followLink('successor'));
|
||||
});
|
||||
});
|
||||
describe(`when the linkdefinition concerns a list`, () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor',
|
||||
isList: true
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
service.resolveLink(testModel, followLink('predecessor', { some: 'options '} as any, followLink('successor')))
|
||||
});
|
||||
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
||||
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options '} as any, followLink('successor'));
|
||||
});
|
||||
});
|
||||
describe('either way', () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor'
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
});
|
||||
|
||||
it('should call getLinkDefinition with the correct model and link', () => {
|
||||
expect(decorators.getLinkDefinition).toHaveBeenCalledWith(testModel.constructor, 'predecessor');
|
||||
});
|
||||
|
||||
it('should call getDataServiceFor with the correct resource type', () => {
|
||||
expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
|
||||
});
|
||||
|
||||
it('should return the model with the resolved link', () => {
|
||||
expect(result.type).toBe(TEST_MODEL);
|
||||
expect(result.value).toBe('a test value');
|
||||
expect(result._links.self.href).toBe('http://self.link');
|
||||
expect(result.predecessor).toBe('findByHref');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when the specified link doesn't exist on the model's class`, () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined);
|
||||
});
|
||||
it('should throw an error', () => {
|
||||
expect(() => {
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there is no dataservice for the resourcetype in the link`, () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor'
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(undefined);
|
||||
});
|
||||
it('should throw an error', () => {
|
||||
expect(() => {
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveLinks', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'resolveLink');
|
||||
service.resolveLinks(testModel, followLink('predecessor'), followLink('successor'))
|
||||
});
|
||||
|
||||
it('should call resolveLink with the model for each of the provided links', () => {
|
||||
expect(service.resolveLink).toHaveBeenCalledWith(testModel, followLink('predecessor'));
|
||||
expect(service.resolveLink).toHaveBeenCalledWith(testModel, followLink('successor'));
|
||||
});
|
||||
|
||||
it('should return the model', () => {
|
||||
expect(result.type).toBe(TEST_MODEL);
|
||||
expect(result.value).toBe('a test value');
|
||||
expect(result._links.self.href).toBe('http://self.link');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeResolvedLinks', () => {
|
||||
beforeEach(() => {
|
||||
testModel.predecessor = 'predecessor value';
|
||||
testModel.successor = 'successor value';
|
||||
spyOnFunction(decorators, 'getLinkDefinitions').and.returnValue([
|
||||
{
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor',
|
||||
},
|
||||
{
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'successor',
|
||||
propertyName: 'successor',
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
it('should return a new version of the object without any resolved links', () => {
|
||||
result = service.removeResolvedLinks(testModel);
|
||||
expect(result.value).toBe(testModel.value);
|
||||
expect(result.type).toBe(testModel.type);
|
||||
expect(result._links).toBe(testModel._links);
|
||||
expect(result.predecessor).toBeUndefined();
|
||||
expect(result.successor).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should leave the original object untouched', () => {
|
||||
service.removeResolvedLinks(testModel);
|
||||
expect(testModel.predecessor).toBe('predecessor value');
|
||||
expect(testModel.successor).toBe('successor value');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
/* tslint:enable:max-classes-per-file */
|
25
src/app/core/cache/builders/link.service.ts
vendored
25
src/app/core/cache/builders/link.service.ts
vendored
@@ -5,6 +5,10 @@ import { GenericConstructor } from '../../shared/generic-constructor';
|
||||
import { HALResource } from '../../shared/hal-resource.model';
|
||||
import { getDataServiceFor, getLinkDefinition, getLinkDefinitions, LinkDefinition } from './build-decorators';
|
||||
|
||||
/**
|
||||
* A Service to handle the resolving and removing
|
||||
* of resolved HALLinks on HALResources
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@@ -15,13 +19,26 @@ export class LinkService {
|
||||
) {
|
||||
}
|
||||
|
||||
public resolveLinks<T extends HALResource>(model: T, ...linksToFollow: Array<FollowLinkConfig<T>>) {
|
||||
/**
|
||||
* Resolve the given {@link FollowLinkConfig}s for the given model
|
||||
*
|
||||
* @param model the {@link HALResource} to resolve the links for
|
||||
* @param linksToFollow the {@link FollowLinkConfig}s to resolve
|
||||
*/
|
||||
public resolveLinks<T extends HALResource>(model: T, ...linksToFollow: Array<FollowLinkConfig<T>>): T {
|
||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||
this.resolveLink(model, linkToFollow);
|
||||
});
|
||||
return model;
|
||||
}
|
||||
|
||||
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>) {
|
||||
/**
|
||||
* Resolve the given {@link FollowLinkConfig} for the given model
|
||||
*
|
||||
* @param model the {@link HALResource} to resolve the link for
|
||||
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
||||
*/
|
||||
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T {
|
||||
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
|
||||
|
||||
if (hasNoValue(matchingLinkDef)) {
|
||||
@@ -50,10 +67,14 @@ export class LinkService {
|
||||
throw new Error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} from ${href}`);
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any resolved links that the model may have.
|
||||
*
|
||||
* @param model the {@link HALResource} to remove the links from
|
||||
* @returns a copy of the given model, without resolved links.
|
||||
*/
|
||||
public removeResolvedLinks<T extends HALResource>(model: T): T {
|
||||
const result = Object.assign(new (model.constructor as GenericConstructor<T>)(), model);
|
||||
|
@@ -80,9 +80,9 @@ export class RemoteDataBuildService {
|
||||
}
|
||||
}),
|
||||
hasValueOperator(),
|
||||
map((obj: T) => {
|
||||
return this.build<T>(obj, ...linksToFollow);
|
||||
}),
|
||||
map((obj: T) =>
|
||||
this.linkService.resolveLinks(obj, ...linksToFollow)
|
||||
),
|
||||
startWith(undefined),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
@@ -135,9 +135,9 @@ export class RemoteDataBuildService {
|
||||
switchMap((resourceUUIDs: string[]) => {
|
||||
return this.objectCache.getList(resourceUUIDs).pipe(
|
||||
map((objs: T[]) => {
|
||||
return objs.map((obj: T) => {
|
||||
return this.build<T>(obj, ...linksToFollow);
|
||||
});
|
||||
return objs.map((obj: T) =>
|
||||
this.linkService.resolveLinks(obj, ...linksToFollow)
|
||||
);
|
||||
}));
|
||||
}),
|
||||
startWith([]),
|
||||
@@ -166,11 +166,6 @@ export class RemoteDataBuildService {
|
||||
return this.toRemoteDataObservable(requestEntry$, payload$);
|
||||
}
|
||||
|
||||
build<T extends CacheableObject>(model: T, ...linksToFollow: Array<FollowLinkConfig<T>>): T {
|
||||
this.linkService.resolveLinks(model, ...linksToFollow);
|
||||
return model;
|
||||
}
|
||||
|
||||
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
|
||||
|
||||
if (isEmpty(input)) {
|
||||
|
Reference in New Issue
Block a user