mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #490 from mspalti/routing_by_id_2
Lookup and redirect by uuid and handle
This commit is contained in:
@@ -207,6 +207,7 @@
|
|||||||
"error.collection": "Error fetching collection",
|
"error.collection": "Error fetching collection",
|
||||||
"error.collections": "Error fetching collections",
|
"error.collections": "Error fetching collections",
|
||||||
"error.community": "Error fetching community",
|
"error.community": "Error fetching community",
|
||||||
|
"error.identifier": "No item found for the identifier",
|
||||||
"error.default": "Error",
|
"error.default": "Error",
|
||||||
"error.item": "Error fetching item",
|
"error.item": "Error fetching item",
|
||||||
"error.items": "Error fetching items",
|
"error.items": "Error fetching items",
|
||||||
|
41
src/app/+lookup-by-id/lookup-by-id-routing.module.ts
Normal file
41
src/app/+lookup-by-id/lookup-by-id-routing.module.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { LookupGuard } from './lookup-guard';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, UrlSegment } from '@angular/router';
|
||||||
|
import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component';
|
||||||
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
matcher: (url) => {
|
||||||
|
// The expected path is :idType/:id
|
||||||
|
const idType = url[0].path;
|
||||||
|
// Allow for handles that are delimited with a forward slash.
|
||||||
|
const id = url
|
||||||
|
.slice(1)
|
||||||
|
.map((us: UrlSegment) => us.path)
|
||||||
|
.join('/');
|
||||||
|
if (isNotEmpty(idType) && isNotEmpty(id)) {
|
||||||
|
return {
|
||||||
|
consumed: url,
|
||||||
|
posParams: {
|
||||||
|
idType: new UrlSegment(idType, {}),
|
||||||
|
id: new UrlSegment(id, {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
canActivate: [LookupGuard],
|
||||||
|
component: ObjectNotFoundComponent }
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
LookupGuard
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class LookupRoutingModule {
|
||||||
|
|
||||||
|
}
|
23
src/app/+lookup-by-id/lookup-by-id.module.ts
Normal file
23
src/app/+lookup-by-id/lookup-by-id.module.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { LookupRoutingModule } from './lookup-by-id-routing.module';
|
||||||
|
import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component';
|
||||||
|
import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
LookupRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
ObjectNotFoundComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
DsoRedirectDataService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LookupIdModule {
|
||||||
|
|
||||||
|
}
|
50
src/app/+lookup-by-id/lookup-guard.spec.ts
Normal file
50
src/app/+lookup-by-id/lookup-guard.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { LookupGuard } from './lookup-guard';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { IdentifierType } from '../core/data/request.models';
|
||||||
|
|
||||||
|
describe('LookupGuard', () => {
|
||||||
|
let dsoService: any;
|
||||||
|
let guard: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dsoService = {
|
||||||
|
findById: jasmine.createSpy('findById').and.returnValue(observableOf({ hasFailed: false,
|
||||||
|
hasSucceeded: true }))
|
||||||
|
};
|
||||||
|
guard = new LookupGuard(dsoService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call findById with handle params', () => {
|
||||||
|
const scopedRoute = {
|
||||||
|
params: {
|
||||||
|
id: '1234',
|
||||||
|
idType: '123456789'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
guard.canActivate(scopedRoute as any, undefined);
|
||||||
|
expect(dsoService.findById).toHaveBeenCalledWith('123456789/1234', IdentifierType.HANDLE)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call findById with handle params', () => {
|
||||||
|
const scopedRoute = {
|
||||||
|
params: {
|
||||||
|
id: '123456789%2F1234',
|
||||||
|
idType: 'handle'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
guard.canActivate(scopedRoute as any, undefined);
|
||||||
|
expect(dsoService.findById).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call findById with UUID params', () => {
|
||||||
|
const scopedRoute = {
|
||||||
|
params: {
|
||||||
|
id: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||||
|
idType: 'uuid'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
guard.canActivate(scopedRoute as any, undefined);
|
||||||
|
expect(dsoService.findById).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID)
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
53
src/app/+lookup-by-id/lookup-guard.ts
Normal file
53
src/app/+lookup-by-id/lookup-guard.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { IdentifierType } from '../core/data/request.models';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { FindByIDRequest } from '../core/data/request.models';
|
||||||
|
import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service';
|
||||||
|
|
||||||
|
interface LookupParams {
|
||||||
|
type: IdentifierType;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LookupGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(private dsoService: DsoRedirectDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> {
|
||||||
|
const params = this.getLookupParams(route);
|
||||||
|
return this.dsoService.findById(params.id, params.type).pipe(
|
||||||
|
map((response: RemoteData<FindByIDRequest>) => response.hasFailed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLookupParams(route: ActivatedRouteSnapshot): LookupParams {
|
||||||
|
let type;
|
||||||
|
let id;
|
||||||
|
const idType = route.params.idType;
|
||||||
|
|
||||||
|
// If the idType is not recognized, assume a legacy handle request (handle/prefix/id)
|
||||||
|
if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) {
|
||||||
|
type = IdentifierType.HANDLE;
|
||||||
|
const prefix = route.params.idType;
|
||||||
|
const handleId = route.params.id;
|
||||||
|
id = `${prefix}/${handleId}`;
|
||||||
|
|
||||||
|
} else if (route.params.idType === IdentifierType.HANDLE) {
|
||||||
|
type = IdentifierType.HANDLE;
|
||||||
|
id = route.params.id;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
type = IdentifierType.UUID;
|
||||||
|
id = route.params.id;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: type,
|
||||||
|
id: id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="object-not-found container">
|
||||||
|
<h1>{{"error.identifier" | translate}}</h1>
|
||||||
|
<h2><small><em>{{missingItem}}</em></small></h2>
|
||||||
|
<br />
|
||||||
|
<p class="text-center">
|
||||||
|
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
@@ -0,0 +1,79 @@
|
|||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ObjectNotFoundComponent } from './objectnotfound.component';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
describe('ObjectNotFoundComponent', () => {
|
||||||
|
let comp: ObjectNotFoundComponent;
|
||||||
|
let fixture: ComponentFixture<ObjectNotFoundComponent>;
|
||||||
|
const testUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||||
|
const uuidType = 'uuid';
|
||||||
|
const handlePrefix = '123456789';
|
||||||
|
const handleId = '22';
|
||||||
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
params: observableOf({id: testUUID, idType: uuidType})
|
||||||
|
});
|
||||||
|
const activatedRouteStubHandle = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
params: observableOf({id: handleId, idType: handlePrefix})
|
||||||
|
});
|
||||||
|
describe('uuid request', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
], providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: activatedRouteStub}
|
||||||
|
],
|
||||||
|
declarations: [ObjectNotFoundComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ObjectNotFoundComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create instance', () => {
|
||||||
|
expect(comp).toBeDefined()
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have id and idType', () => {
|
||||||
|
expect(comp.id).toEqual(testUUID);
|
||||||
|
expect(comp.idType).toEqual(uuidType);
|
||||||
|
expect(comp.missingItem).toEqual('uuid: ' + testUUID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe( 'legacy handle request', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
], providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: activatedRouteStubHandle}
|
||||||
|
],
|
||||||
|
declarations: [ObjectNotFoundComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ObjectNotFoundComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have handle prefix and id', () => {
|
||||||
|
expect(comp.id).toEqual(handleId);
|
||||||
|
expect(comp.idType).toEqual(handlePrefix);
|
||||||
|
expect(comp.missingItem).toEqual('handle: ' + handlePrefix + '/' + handleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component representing the `PageNotFound` DSpace page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-objnotfound',
|
||||||
|
styleUrls: ['./objectnotfound.component.scss'],
|
||||||
|
templateUrl: './objectnotfound.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default
|
||||||
|
})
|
||||||
|
export class ObjectNotFoundComponent implements OnInit {
|
||||||
|
|
||||||
|
idType: string;
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
missingItem: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {AuthService} authservice
|
||||||
|
* @param {ServerResponseService} responseService
|
||||||
|
*/
|
||||||
|
constructor(private route: ActivatedRoute) {
|
||||||
|
route.params.subscribe((params) => {
|
||||||
|
this.idType = params.idType;
|
||||||
|
this.id = params.id;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.idType.startsWith('handle') || this.idType.startsWith('uuid')) {
|
||||||
|
this.missingItem = this.idType + ': ' + this.id;
|
||||||
|
} else {
|
||||||
|
this.missingItem = 'handle: ' + this.idType + '/' + this.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -27,6 +27,8 @@ export function getAdminModulePath() {
|
|||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
||||||
|
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
|
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||||
|
@@ -128,7 +128,7 @@ const EXPORTS = [
|
|||||||
...PROVIDERS
|
...PROVIDERS
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS,
|
...DECLARATIONS
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS
|
||||||
|
1
src/app/core/cache/object-cache.reducer.ts
vendored
1
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -44,6 +44,7 @@ export abstract class TypedObject {
|
|||||||
*/
|
*/
|
||||||
export class CacheableObject extends TypedObject {
|
export class CacheableObject extends TypedObject {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
|
handle?: string;
|
||||||
self: string;
|
self: string;
|
||||||
// isNew: boolean;
|
// isNew: boolean;
|
||||||
// dirtyType: DirtyType;
|
// dirtyType: DirtyType;
|
||||||
|
5
src/app/core/cache/object-cache.service.ts
vendored
5
src/app/core/cache/object-cache.service.ts
vendored
@@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
@@ -80,7 +80,8 @@ export class ObjectCacheService {
|
|||||||
* @return Observable<NormalizedObject<T>>
|
* @return Observable<NormalizedObject<T>>
|
||||||
* An observable of the requested object in normalized form
|
* An observable of the requested object in normalized form
|
||||||
*/
|
*/
|
||||||
getObjectByUUID<T extends CacheableObject>(uuid: string): Observable<NormalizedObject<T>> {
|
getObjectByUUID<T extends CacheableObject>(uuid: string):
|
||||||
|
Observable<NormalizedObject<T>> {
|
||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(selfLinkFromUuidSelector(uuid)),
|
select(selfLinkFromUuidSelector(uuid)),
|
||||||
mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink)
|
mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink)
|
||||||
|
@@ -153,8 +153,9 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): Observable<RemoteData<T>> {
|
findById(id: string): Observable<RemoteData<T>> {
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, id)));
|
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id))));
|
||||||
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)))
|
find((href: string) => hasValue(href)))
|
||||||
|
155
src/app/core/data/dso-redirect-data.service.spec.ts
Normal file
155
src/app/core/data/dso-redirect-data.service.spec.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { FindByIDRequest, IdentifierType } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { DsoRedirectDataService } from './dso-redirect-data.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
|
||||||
|
describe('DsoRedirectDataService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: DsoRedirectDataService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let router;
|
||||||
|
let remoteData;
|
||||||
|
const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746';
|
||||||
|
const dsoHandle = '1234567789/22';
|
||||||
|
const encodedHandle = encodeURIComponent(dsoHandle);
|
||||||
|
const pidLink = 'https://rest.api/rest/api/pid/find{?id}';
|
||||||
|
const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`;
|
||||||
|
const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`;
|
||||||
|
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||||
|
const objectCache = {} as ObjectCacheService;
|
||||||
|
let setup;
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: cold('a', {a: pidLink})
|
||||||
|
});
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
configure: true
|
||||||
|
});
|
||||||
|
router = {
|
||||||
|
navigate: jasmine.createSpy('navigate')
|
||||||
|
};
|
||||||
|
|
||||||
|
remoteData = {
|
||||||
|
isSuccessful: true,
|
||||||
|
error: undefined,
|
||||||
|
hasSucceeded: true,
|
||||||
|
isLoading: false,
|
||||||
|
payload: {
|
||||||
|
type: 'item',
|
||||||
|
uuid: '123456789'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setup = () => {
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: cold('a', {
|
||||||
|
a: remoteData
|
||||||
|
})
|
||||||
|
});
|
||||||
|
service = new DsoRedirectDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
dataBuildService,
|
||||||
|
store,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator,
|
||||||
|
router
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findById', () => {
|
||||||
|
it('should call HALEndpointService with the path to the pid endpoint', () => {
|
||||||
|
setup();
|
||||||
|
scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(halService.getEndpoint).toHaveBeenCalledWith('pid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call HALEndpointService with the path to the dso endpoint', () => {
|
||||||
|
setup();
|
||||||
|
scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(halService.getEndpoint).toHaveBeenCalledWith('dso');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call HALEndpointService with the path to the dso endpoint when identifier type not specified', () => {
|
||||||
|
setup();
|
||||||
|
scheduler.schedule(() => service.findById(dsoUUID));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(halService.getEndpoint).toHaveBeenCalledWith('dso');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure the proper FindByIDRequest for uuid', () => {
|
||||||
|
setup();
|
||||||
|
scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestUUIDURL, dsoUUID), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure the proper FindByIDRequest for handle', () => {
|
||||||
|
setup();
|
||||||
|
scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to item route', () => {
|
||||||
|
remoteData.payload.type = 'item';
|
||||||
|
setup();
|
||||||
|
const redir = service.findById(dsoHandle, IdentifierType.HANDLE);
|
||||||
|
// The framework would normally subscribe but do it here so we can test navigation.
|
||||||
|
redir.subscribe();
|
||||||
|
scheduler.schedule(() => redir);
|
||||||
|
scheduler.flush();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to collections route', () => {
|
||||||
|
remoteData.payload.type = 'collection';
|
||||||
|
setup();
|
||||||
|
const redir = service.findById(dsoHandle, IdentifierType.HANDLE);
|
||||||
|
redir.subscribe();
|
||||||
|
scheduler.schedule(() => redir);
|
||||||
|
scheduler.flush();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to communities route', () => {
|
||||||
|
remoteData.payload.type = 'community';
|
||||||
|
setup();
|
||||||
|
const redir = service.findById(dsoHandle, IdentifierType.HANDLE);
|
||||||
|
redir.subscribe();
|
||||||
|
scheduler.schedule(() => redir);
|
||||||
|
scheduler.flush();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['communities/' + remoteData.payload.uuid]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
90
src/app/core/data/dso-redirect-data.service.ts
Normal file
90
src/app/core/data/dso-redirect-data.service.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { DataService } from './data.service';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { FindAllOptions, FindByIDRequest, IdentifierType } from './request.models';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { filter, take, tap } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { getFinishedRemoteData } from '../shared/operators';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DsoRedirectDataService extends DataService<any> {
|
||||||
|
|
||||||
|
// Set the default link path to the identifier lookup endpoint.
|
||||||
|
protected linkPath = 'pid';
|
||||||
|
protected forceBypassCache = false;
|
||||||
|
private uuidEndpoint = 'dso';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer<any>,
|
||||||
|
private router: Router) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLinkPath(identifierType: IdentifierType) {
|
||||||
|
// The default 'pid' endpoint for identifiers does not support uuid lookups.
|
||||||
|
// For uuid lookups we need to change the linkPath.
|
||||||
|
if (identifierType === IdentifierType.UUID) {
|
||||||
|
this.linkPath = this.uuidEndpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIDHref(endpoint, resourceID): string {
|
||||||
|
// Supporting both identifier (pid) and uuid (dso) endpoints
|
||||||
|
return endpoint.replace(/\{\?id\}/, `?id=${resourceID}`)
|
||||||
|
.replace(/\{\?uuid\}/, `?uuid=${resourceID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> {
|
||||||
|
this.setLinkPath(identifierType);
|
||||||
|
return super.findById(id).pipe(
|
||||||
|
getFinishedRemoteData(),
|
||||||
|
take(1),
|
||||||
|
tap((response) => {
|
||||||
|
if (response.hasSucceeded) {
|
||||||
|
const uuid = response.payload.uuid;
|
||||||
|
const newRoute = this.getEndpointFromDSOType(response.payload.type);
|
||||||
|
if (hasValue(uuid) && hasValue(newRoute)) {
|
||||||
|
this.router.navigate([newRoute + '/' + uuid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Is there an existing method somewhere else that converts dso type to route?
|
||||||
|
getEndpointFromDSOType(dsoType: string): string {
|
||||||
|
// Are there other types to consider?
|
||||||
|
if (dsoType.startsWith('item')) {
|
||||||
|
return 'items'
|
||||||
|
} else if (dsoType.startsWith('community')) {
|
||||||
|
return 'communities';
|
||||||
|
} else if (dsoType.startsWith('collection')) {
|
||||||
|
return 'collections'
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -22,6 +22,12 @@ import { MappedCollectionsReponseParsingService } from './mapped-collections-rep
|
|||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
// uuid and handle requests have separate endpoints
|
||||||
|
export enum IdentifierType {
|
||||||
|
UUID ='uuid',
|
||||||
|
HANDLE = 'handle'
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class RestRequest {
|
export abstract class RestRequest {
|
||||||
public responseMsToLive = 10 * 1000;
|
public responseMsToLive = 10 * 1000;
|
||||||
public forceBypassCache = false;
|
public forceBypassCache = false;
|
||||||
@@ -50,7 +56,7 @@ export class GetRequest extends RestRequest {
|
|||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions,
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.GET, body, options)
|
super(uuid, href, RestRequestMethod.GET, body, options)
|
||||||
}
|
}
|
||||||
|
@@ -298,10 +298,11 @@ describe('RequestService', () => {
|
|||||||
describe('in the ObjectCache', () => {
|
describe('in the ObjectCache', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(objectCache.hasBySelfLink as any).and.returnValue(true);
|
(objectCache.hasBySelfLink as any).and.returnValue(true);
|
||||||
|
(objectCache.hasByUUID as any).and.returnValue(true);
|
||||||
spyOn(serviceAsAny, 'hasByHref').and.returnValue(false);
|
spyOn(serviceAsAny, 'hasByHref').and.returnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true', () => {
|
it('should return true for GetRequest', () => {
|
||||||
const result = serviceAsAny.isCachedOrPending(testGetRequest);
|
const result = serviceAsAny.isCachedOrPending(testGetRequest);
|
||||||
const expected = true;
|
const expected = true;
|
||||||
|
|
||||||
|
@@ -53,8 +53,9 @@ export const requestUUIDIndexSelector: MemoizedSelector<AppState, IndexState> =
|
|||||||
/**
|
/**
|
||||||
* Return the self link of an object in the object-cache based on its UUID
|
* Return the self link of an object in the object-cache based on its UUID
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param id
|
||||||
* the UUID for which you want to find the matching self link
|
* the UUID for which you want to find the matching self link
|
||||||
|
* @param identifierType the type of index, used to select index from state
|
||||||
* @returns
|
* @returns
|
||||||
* a MemoizedSelector to select the self link
|
* a MemoizedSelector to select the self link
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user