Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { hasNoValue } from '../../shared/empty.util';
import { InjectionToken } from '@angular/core';
import { GenericConstructor } from '../../core/shared/generic-constructor';

export enum BrowseByType {
Title = 'title',
Expand All @@ -8,6 +10,11 @@ export enum BrowseByType {

export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata;

export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor<any>>('getComponentByBrowseByType', {
providedIn: 'root',
factory: () => getComponentByBrowseByType
});

const map = new Map();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import * as decorator from './browse-by-decorator';
import { BehaviorSubject } from 'rxjs';
import { environment } from '../../../environments/environment';
import createSpy = jasmine.createSpy;
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';

xdescribe('BrowseBySwitcherComponent', () => {
describe('BrowseBySwitcherComponent', () => {
let comp: BrowseBySwitcherComponent;
let fixture: ComponentFixture<BrowseBySwitcherComponent>;

Expand All @@ -23,7 +22,8 @@ xdescribe('BrowseBySwitcherComponent', () => {
TestBed.configureTestingModule({
declarations: [BrowseBySwitcherComponent],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub }
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
Expand All @@ -32,7 +32,6 @@ xdescribe('BrowseBySwitcherComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
comp = fixture.componentInstance;
spyOnProperty(decorator, 'getComponentByBrowseByType').and.returnValue(createSpy('getComponentByItemType'));
}));

types.forEach((type) => {
Expand All @@ -43,7 +42,7 @@ xdescribe('BrowseBySwitcherComponent', () => {
});

it(`should call getComponentByBrowseByType with type "${type.type}"`, () => {
expect(decorator.getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Component, Inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
import { map } from 'rxjs/operators';
import { getComponentByBrowseByType } from './browse-by-decorator';
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
import { environment } from '../../../environments/environment';
import { GenericConstructor } from '../../core/shared/generic-constructor';

@Component({
selector: 'ds-browse-by-switcher',
Expand All @@ -20,7 +21,8 @@ export class BrowseBySwitcherComponent implements OnInit {
*/
browseByComponent: Observable<any>;

public constructor(protected route: ActivatedRoute) {
public constructor(protected route: ActivatedRoute,
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType) => GenericConstructor<any>) {
}

/**
Expand All @@ -32,7 +34,7 @@ export class BrowseBySwitcherComponent implements OnInit {
const id = params.id;
return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id);
}),
map((config: BrowseByTypeConfig) => getComponentByBrowseByType(config.type))
map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import {
getFirstSucceededRemoteData
} from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { InjectionToken } from '@angular/core';

export const PAGINATED_RELATIONS_TO_ITEMS_OPERATOR = new InjectionToken<(thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>>('paginatedRelationsToItems', {
providedIn: 'root',
factory: () => paginatedRelationsToItems
});

/**
* Operator for comparing arrays using a mapping function
Expand Down
14 changes: 14 additions & 0 deletions src/app/core/cache/builders/build-decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import {
TypedObject,
getResourceTypeValueFor
} from '../object-cache.reducer';
import { InjectionToken } from '@angular/core';

export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<any>>('getDataServiceFor', {
providedIn: 'root',
factory: () => getDataServiceFor
});
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
providedIn: 'root',
factory: () => getLinkDefinition
});
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
providedIn: 'root',
factory: () => getLinkDefinitions
});

const resolvedLinkKey = Symbol('resolvedLink');

Expand Down
91 changes: 38 additions & 53 deletions src/app/core/cache/builders/link.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@ 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 { 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;
};
import { DATA_SERVICE_FACTORY, LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators';
import { isEmpty } from 'rxjs/operators';

const TEST_MODEL = new ResourceType('testmodel');
let result: any;
Expand Down Expand Up @@ -51,7 +45,7 @@ let testDataService: TestDataService;

let testModel: TestModel;

xdescribe('LinkService', () => {
describe('LinkService', () => {
let service: LinkService;

beforeEach(() => {
Expand All @@ -76,6 +70,30 @@ xdescribe('LinkService', () => {
providers: [LinkService, {
provide: TestDataService,
useValue: testDataService
}, {
provide: DATA_SERVICE_FACTORY,
useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService),
}, {
provide: LINK_DEFINITION_FACTORY,
useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
}),
}, {
provide: LINK_DEFINITION_MAP_FACTORY,
useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([
{
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor',
},
{
resourceType: TEST_MODEL,
linkName: 'successor',
propertyName: 'successor',
}
]),
}]
});
service = TestBed.inject(LinkService);
Expand All @@ -84,12 +102,6 @@ xdescribe('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', {}, true, true, true, followLink('successor')));
});
it('should call dataservice.findByHref with the correct href and nested links', () => {
Expand All @@ -98,13 +110,12 @@ xdescribe('LinkService', () => {
});
describe(`when the linkdefinition concerns a list`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
((service as any).getLinkDefinition as jasmine.Spy).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, true, true, true, followLink('successor')));
});
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
Expand All @@ -113,21 +124,15 @@ xdescribe('LinkService', () => {
});
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', {}, true, true, true, followLink('successor')));
});

it('should call getLinkDefinition with the correct model and link', () => {
expect(decorators.getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
expect((service as any).getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
});

it('should call getDataServiceFor with the correct resource type', () => {
expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
});

it('should return the model with the resolved link', () => {
Expand All @@ -140,7 +145,7 @@ xdescribe('LinkService', () => {

describe(`when the specified link doesn't exist on the model's class`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined);
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
Expand All @@ -151,12 +156,7 @@ xdescribe('LinkService', () => {

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);
((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
Expand Down Expand Up @@ -188,18 +188,6 @@ xdescribe('LinkService', () => {
beforeEach(() => {
testModel.predecessor = 'predecessor value' as any;
testModel.successor = 'successor value' as any;
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', () => {
Expand Down Expand Up @@ -231,16 +219,10 @@ xdescribe('LinkService', () => {
}
}
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
});

describe('resolving the available link', () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
result = service.resolveLinks(testModel, followLink('predecessor'));
});

Expand All @@ -251,16 +233,19 @@ xdescribe('LinkService', () => {

describe('resolving the missing link', () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
resourceType: TEST_MODEL,
linkName: 'successor',
propertyName: 'successor'
});
result = service.resolveLinks(testModel, followLink('successor'));
});

it('should return the model with no resolved link', () => {
expect(result.successor).toBeUndefined();
it('should resolve to an empty observable', (done) => {
result.successor.pipe(isEmpty()).subscribe((v) => {
expect(v).toEqual(true);
done();
});
});
});
});
Expand Down
18 changes: 11 additions & 7 deletions src/app/core/cache/builders/link.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Injectable, Injector } from '@angular/core';
import { Inject, Injectable, Injector } from '@angular/core';
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model';
import {
getDataServiceFor,
getLinkDefinition,
getLinkDefinitions,
DATA_SERVICE_FACTORY,
LINK_DEFINITION_FACTORY,
LINK_DEFINITION_MAP_FACTORY,
LinkDefinition
} from './build-decorators';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/internal/Observable';
import { EMPTY } from 'rxjs';
import { ResourceType } from '../../shared/resource-type';

/**
* A Service to handle the resolving and removing
Expand All @@ -24,6 +25,9 @@ export class LinkService {

constructor(
protected parentInjector: Injector,
@Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor<any>,
@Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>,
@Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: <T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>,
) {
}

Expand All @@ -49,12 +53,12 @@ export class LinkService {
* @param linkToFollow the {@link FollowLinkConfig} to resolve
*/
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);

if (hasNoValue(matchingLinkDef)) {
throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
} else {
const provider = getDataServiceFor(matchingLinkDef.resourceType);
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);

if (hasNoValue(provider)) {
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
Expand Down Expand Up @@ -104,7 +108,7 @@ export class LinkService {
*/
public removeResolvedLinks<T extends HALResource>(model: T): T {
const result = Object.assign(new (model.constructor as GenericConstructor<T>)(), model);
const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor<T>);
const linkDefs = this.getLinkDefinitions(model.constructor as GenericConstructor<T>);
if (isNotEmpty(linkDefs)) {
linkDefs.forEach((linkDef: LinkDefinition<T>) => {
result[linkDef.propertyName] = undefined;
Expand Down
Loading